Updated fn-spec to include classNames
Removed Classnames file and added code to fn.js
Fixed typo dimentions to dimensions
Moved Device/Link logic from Topo2D3 into the model
Model now calls onChange when any property is changed via the set Method
WIP - Added d3 force layout for devices and lines
Change-Id: I4d1afd3cd4cecf2f719e27f4be5d1e874bd9e342
diff --git a/web/gui/src/main/webapp/app/fw/util/fn.js b/web/gui/src/main/webapp/app/fw/util/fn.js
index 77c2b96..33ad2f6 100644
--- a/web/gui/src/main/webapp/app/fw/util/fn.js
+++ b/web/gui/src/main/webapp/app/fw/util/fn.js
@@ -386,6 +386,34 @@
}
+ var hasOwn = {}.hasOwnProperty;
+
+ function classNames () {
+ var classes = [];
+
+ for (var i = 0; i < arguments.length; i++) {
+ var arg = arguments[i];
+ if (!arg) continue;
+
+ var argType = typeof arg;
+
+ if (argType === 'string' || argType === 'number') {
+ classes.push(arg);
+ } else if (Array.isArray(arg)) {
+ classes.push(classNames.apply(null, arg));
+ } else if (argType === 'object') {
+ for (var key in arg) {
+ if (hasOwn.call(arg, key) && arg[key]) {
+ classes.push(key);
+ }
+ }
+ }
+ }
+
+ return classes.join(' ');
+ }
+
+
angular.module('onosUtil')
.factory('FnService',
['$window', '$location', '$log', function (_$window_, $loc, _$log_) {
@@ -423,7 +451,8 @@
parseBitRate: parseBitRate,
addToTrie: addToTrie,
removeFromTrie: removeFromTrie,
- trieLookup: trieLookup
+ trieLookup: trieLookup,
+ classNames: classNames
};
}]);
diff --git a/web/gui/src/main/webapp/app/view/topo/topoD3.js b/web/gui/src/main/webapp/app/view/topo/topoD3.js
index da7d729..5b669ce 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoD3.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoD3.js
@@ -320,6 +320,7 @@
// updateLinks - subfunctions
function linkEntering(d) {
+
var link = d3.select(this);
d.el = link;
api.restyleLinkElement(d);
diff --git a/web/gui/src/main/webapp/app/view/topo/topoForce.js b/web/gui/src/main/webapp/app/view/topo/topoForce.js
index b3fc5a3..ef9f633 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -103,6 +103,7 @@
// === EVENT HANDLERS
function addDevice(data) {
+ console.log(data);
var id = data.id,
d;
@@ -1044,7 +1045,7 @@
updateLinks();
updateNodes();
}
-
+
angular.module('ovTopo')
.factory('TopoForceService',
['$log', '$timeout', 'FnService', 'SvgUtilService',
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css b/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
index 63b5dfb..3bdb8b2 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-
/*
ONOS GUI -- Topology View (theme) -- CSS file
*/
@@ -22,8 +21,7 @@
/* --- Base SVG Layer --- */
#ov-topo2 svg {
- /*background-color: #f4f4f4;*/
- background-color: goldenrod; /* just for testing */
+ background-color: #f4f4f4;
}
/* --- "No Devices" Layer --- */
@@ -32,15 +30,355 @@
fill: #db7773;
}
-#ov-topo2 svg #topo2-noDevsLayer text {
+#ov-topo2 svg #topo-noDevsLayer text {
fill: #7e9aa8;
}
/* --- Topo Map --- */
-#ov-topo2 svg #topo2-map {
+#ov-topo2 svg #topo-map {
stroke-width: 2px;
stroke: #f4f4f4;
fill: #e5e5e6;
}
+/* --- general topo-panel styling --- */
+
+.topo-p svg {
+ background: #c0242b;
+}
+
+.topo-p svg .glyph {
+ fill: #ffffff;
+}
+
+.topo-p hr {
+ background-color: #cccccc;
+}
+
+#topo-p-detail svg {
+ background: none;
+}
+
+#topo-p-detail .header svg .glyph {
+ fill: #c0242b;
+}
+
+
+/* --- Topo Instance Panel --- */
+
+#topo-p-instance svg rect {
+ stroke-width: 0;
+ fill: #fbfbfb;
+}
+
+/* body of an instance */
+#topo-p-instance .online svg rect {
+ opacity: 1;
+ fill: #fbfbfb;
+}
+
+#topo-p-instance svg .glyph {
+ fill: #fff;
+}
+#topo-p-instance .online svg .glyph {
+ fill: #fff;
+}
+
+
+/* offline */
+#topo-p-instance svg .badgeIcon {
+ opacity: 0.4;
+ fill: #939598;
+}
+
+/* online */
+#topo-p-instance .online svg .badgeIcon {
+ opacity: 1.0;
+ fill: #939598;
+}
+#topo-p-instance .online svg .badgeIcon.bird {
+ fill: #ffffff;
+}
+
+#topo-p-instance svg .readyBadge {
+ visibility: hidden;
+}
+#topo-p-instance .ready svg .readyBadge {
+ visibility: visible;
+}
+
+#topo-p-instance svg text {
+ text-anchor: left;
+ opacity: 0.5;
+ fill: #3c3a3a;
+}
+
+#topo-p-instance .online svg text {
+ opacity: 1.0;
+ fill: #3c3a3a;
+}
+
+#topo-p-instance .onosInst.mastership {
+ opacity: 0.3;
+}
+#topo-p-instance .onosInst.mastership.affinity {
+ opacity: 1.0;
+}
+#topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url(#blue-glow);
+}
+
+.firefox #topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow");
+}
+
+/* --- Topo Nodes --- */
+
+#ov-topo2 svg .suppressed {
+ opacity: 0.5 !important;
+}
+
+#ov-topo2 svg .suppressedmax {
+ opacity: 0.2 !important;
+}
+
+/* Device Nodes */
+
+/* note: device without the 'online' class is offline */
+#ov-topo2 svg .node.device rect {
+ /* TODO: theme */
+ fill: #f0f0f0;
+}
+#ov-topo2 svg .node.device text {
+ /*TODO: theme*/
+ fill: #bbb;
+}
+#ov-topo2 svg .node.device use {
+ /*TODO: theme*/
+ fill: #777;
+}
+
+
+#ov-topo2 svg .node.device.online rect {
+ fill: #ffffff;
+}
+#ov-topo2 svg .node.device.online text {
+ fill: #3c3a3a;
+}
+#ov-topo2 svg .node.device.online use {
+ /* NOTE: this gets overridden programatically */
+ fill: #454545;
+}
+
+
+#ov-topo2 svg .node.device.selected rect {
+ stroke-width: 2.0;
+ stroke: #009fdb;
+}
+
+/* Badges */
+/* (... works for bothand dark themes...) */
+#ov-topo2 svg .node .badge circle {
+ stroke: #aaa;
+}
+
+#ov-topo2 svg .node .badge.badgeInfo circle {
+ fill: #99d;
+}
+
+#ov-topo2 svg .node .badge.badgeWarn circle {
+ fill: #da2;
+}
+
+#ov-topo2 svg .node .badge.badgeError circle {
+ fill: #e44;
+}
+
+#ov-topo2 svg .node .badge use {
+ fill: white !important;
+}
+
+#ov-topo2 svg .node .badge.badgeInfo use {
+ fill: #448;
+}
+
+#ov-topo2 svg .node .badge text {
+ fill: white !important;
+}
+
+#ov-topo2 svg .node .badge.badgeInfo text {
+ fill: #448;
+}
+
+/* Host Nodes */
+
+#ov-topo2 svg .node.host {
+}
+
+#ov-topo2 svg .node.host text {
+ stroke: none;
+ font: 9pt sans-serif;
+ fill: #846;
+}
+
+#ov-topo2 svg .node.host circle {
+ stroke: #a3a596;
+ fill: #e0dfd6;
+}
+#ov-topo2 svg .node.host.selected .hostIcon > circle {
+ stroke-width: 2.0;
+ stroke: #009fdb;
+}
+
+#ov-topo2 svg .node.host use {
+ fill: #3c3a3a;
+}
+
+/* --- Topo Links --- */
+
+#ov-topo2 svg .link {
+ opacity: .9;
+}
+
+#ov-topo2 svg .link.selected,
+#ov-topo2 svg .link.enhanced {
+ stroke-width: 3.5;
+ stroke: #009fdb;
+}
+
+#ov-topo2 svg .link.inactive {
+ opacity: .5;
+ stroke-dasharray: 8 4;
+}
+/* TODO: Review for not-permitted links */
+#ov-topo2 svg .link.not-permitted {
+ stroke: rgb(255,0,0);
+ stroke-width: 5.0;
+ stroke-dasharray: 8 4;
+}
+
+#ov-topo2 svg .link.secondary {
+ stroke-width: 3px;
+ stroke: rgba(0,153,51,0.5);
+}
+
+/* Port traffic color visualization for Kbps, Mbps, and Gbps */
+
+#ov-topo2 svg .link.secondary.port-traffic-Kbps {
+ stroke: rgb(0,153,51);
+ stroke-width: 5.0;
+}
+
+#ov-topo2 svg .link.secondary.port-traffic-Mbps {
+ stroke: rgb(128,145,27);
+ stroke-width: 6.5;
+}
+
+#ov-topo2 svg .link.secondary.port-traffic-Gbps {
+ stroke: rgb(255, 137, 3);
+ stroke-width: 8.0;
+}
+
+#ov-topo2 svg .link.secondary.port-traffic-Gbps-choked {
+ stroke: rgb(183, 30, 21);
+ stroke-width: 8.0;
+}
+
+
+
+#ov-topo2 svg .link.animated {
+ stroke-dasharray: 8 5;
+ animation: ants 5s infinite linear;
+ /* below line could be added via Javascript, based on path, if we cared
+ * enough about the direction of ant-flow
+ */
+ /*animation-direction: reverse;*/
+}
+@keyframes ants {
+ from {
+ stroke-dashoffset: 0;
+ }
+ to {
+ stroke-dashoffset: 400;
+ }
+}
+
+#ov-topo2 svg .link.primary {
+ stroke-width: 4px;
+ stroke: #ffA300;
+}
+
+#ov-topo2 svg .link.secondary.optical {
+ stroke-width: 4px;
+ stroke: rgba(128,64,255,0.5);
+}
+
+#ov-topo2 svg .link.primary.optical {
+ stroke-width: 6px;
+ stroke: #74f;
+}
+
+/* Link Labels */
+#ov-topo2 svg .linkLabel rect {
+ stroke: none;
+ fill: #ffffff;
+}
+
+#ov-topo2 svg .linkLabel text {
+ fill: #444;
+}
+
+/* Port Labels */
+
+#ov-topo2 svg .portLabel rect {
+ stroke: #a3a596;
+ fill: #ffffff;
+}
+
+#ov-topo2 svg .portLabel text {
+ fill: #444;
+}
+
+/* Number of Links Labels */
+
+
+#ov-topo2 text.numLinkText {
+ fill: #444;
+}
+
+/* ------------------------------------------------- */
+/* Sprite Layer */
+
+#ov-topo2 svg #topo-sprites .gold1 use {
+ stroke: #fda;
+ fill: none;
+}
+#ov-topo2 svg #topo-sprites .gold1 text {
+ fill: #eda;
+}
+
+#ov-topo2 svg #topo-sprites .blue1 use {
+ stroke: #bbd;
+ fill: none;
+}
+#ov-topo2 svg #topo-sprites .blue1 text {
+ fill: #cce;
+}
+
+#ov-topo2 svg #topo-sprites .gray1 use {
+ stroke: #ccc;
+ fill: none;
+}
+#ov-topo2 svg #topo-sprites .gray1 text {
+ fill: #ddd;
+}
+
+/* fills */
+#ov-topo2 svg #topo-sprites use.fill-gray2 {
+ fill: #eee;
+}
+
+#ov-topo2 svg #topo-sprites use.fill-blue2 {
+ fill: #bce;
+}
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2.html b/web/gui/src/main/webapp/app/view/topo2/topo2.html
index 1f0c6d6..32913a9 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2.html
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2.html
@@ -1,6 +1,7 @@
<!-- Topology View partial HTML -->
<div id="ov-topo2">
- <div id="topo2tmp">
+
+ <!-- <div id="topo2tmp">
<div class="parentRegion">
Parent Region: <span> - </span>
</div>
@@ -27,7 +28,7 @@
<h4>Peers</h4>
<div></div>
</div>
- </div>
+ </div> -->
<!-- Below here is good; Above here is temporary, for debugging -->
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2.js b/web/gui/src/main/webapp/app/view/topo2/topo2.js
index 0631299..f62bf5c 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2.js
@@ -48,6 +48,7 @@
// callback invoked when the SVG view has been resized..
function svgResized(s) {
$log.debug('topo2 view resized', s);
+ t2fs.newDim([s.width, s.height]);
}
function setUpKeys(overlayKeys) {
@@ -68,7 +69,7 @@
ps.setPrefs('topo_zoom', {tx:tr[0], ty:tr[1], sc:sc});
// keep the map lines constant width while zooming
- mapG.style('stroke-width', (2.0 / sc) + 'px');
+// mapG.style('stroke-width', (2.0 / sc) + 'px');
}
function setUpZoom() {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
index 3116a6f..e0aefb7 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
@@ -55,8 +55,6 @@
_this._byId[d.id] = model;
});
}
-
-// this.sort();
},
get: function (id) {
if (!id) {
@@ -77,7 +75,10 @@
_reset: function () {
this._byId = [];
this.models = [];
- }
+ },
+ toJSON: function(options) {
+ return this.models.map(function(model) { return model.toJSON(options); });
+ },
};
Collection.extend = function (protoProps, staticProps) {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2D3.js b/web/gui/src/main/webapp/app/view/topo2/topo2D3.js
new file mode 100644
index 0000000..604c907
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2D3.js
@@ -0,0 +1,163 @@
+/*
+* Copyright 2016-present Open Networking Laboratory
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+/*
+ONOS GUI -- Topology Layout Module.
+Module that contains the d3.force.layout logic
+*/
+
+(function () {
+ 'use strict';
+
+ var sus, is, ts;
+
+ // internal state
+ var deviceLabelIndex = 0,
+ hostLabelIndex = 0;
+
+ // configuration
+ var devIconDim = 36,
+ labelPad = 4,
+ hostRadius = 14,
+ badgeConfig = {
+ radius: 12,
+ yoff: 5,
+ gdelta: 10
+ },
+ halfDevIcon = devIconDim / 2,
+ devBadgeOff = { dx: -halfDevIcon, dy: -halfDevIcon },
+ hostBadgeOff = { dx: -hostRadius, dy: -hostRadius },
+ status = {
+ i: 'badgeInfo',
+ w: 'badgeWarn',
+ e: 'badgeError'
+ };
+
+ // note: these are the device icon colors without affinity (no master)
+ var dColTheme = {
+ light: {
+ online: '#444444',
+ offline: '#cccccc'
+ },
+ dark: {
+ // TODO: theme
+ online: '#444444',
+ offline: '#cccccc'
+ }
+ };
+
+ function init() {}
+
+ function renderBadge(node, bdg, boff) {
+ var bsel,
+ bcr = badgeConfig.radius,
+ bcgd = badgeConfig.gdelta;
+
+ node.select('g.badge').remove();
+
+ bsel = node.append('g')
+ .classed('badge', true)
+ .classed(badgeStatus(bdg), true)
+ .attr('transform', sus.translate(boff.dx, boff.dy));
+
+ bsel.append('circle')
+ .attr('r', bcr);
+
+ if (bdg.txt) {
+ bsel.append('text')
+ .attr('dy', badgeConfig.yoff)
+ .attr('text-anchor', 'middle')
+ .text(bdg.txt);
+ } else if (bdg.gid) {
+ bsel.append('use')
+ .attr({
+ width: bcgd * 2,
+ height: bcgd * 2,
+ transform: sus.translate(-bcgd, -bcgd),
+ 'xlink:href': '#' + bdg.gid
+ });
+ }
+ }
+
+ // TODO: Move to Device Model when working on the Exit Devices
+ function updateDeviceRendering(d) {
+ var node = d.el,
+ bdg = d.badge,
+ label = trimLabel(deviceLabel(d)),
+ labelWidth;
+
+ node.select('text').text(label);
+ labelWidth = label ? computeLabelWidth(node) : 0;
+
+ node.select('rect')
+ .transition()
+ .attr(iconBox(devIconDim, labelWidth));
+
+ if (bdg) {
+ renderBadge(node, bdg, devBadgeOff);
+ }
+ }
+
+ function deviceEnter(device) {
+ device.onEnter(this, device);
+ }
+
+ function hostLabel(d) {
+ return d.get('id');
+
+ // var idx = (hostLabelIndex < d.get('labels').length) ? hostLabelIndex : 0;
+ // return d.labels[idx];
+ }
+
+ function hostEnter(d) {
+ var node = d3.select(this),
+ gid = d.get('type') || 'unknown',
+ textDy = hostRadius + 10;
+
+ d.el = node;
+ // sus.visible(node, api.showHosts());
+
+ is.addHostIcon(node, hostRadius, gid);
+
+ node.append('text')
+ .text(hostLabel)
+ .attr('dy', textDy)
+ .attr('text-anchor', 'middle');
+ }
+
+ function linkEntering(link) {
+ link.onEnter(this);
+ }
+
+ angular.module('ovTopo2')
+ .factory('Topo2D3Service',
+ ['SvgUtilService', 'IconService', 'ThemeService',
+
+ function (_sus_, _is_, _ts_) {
+ sus = _sus_;
+ is = _is_;
+ ts = _ts_;
+
+ return {
+ init: init,
+ deviceEnter: deviceEnter,
+ hostEnter: hostEnter,
+ linkEntering: linkEntering
+ }
+ }
+ ]
+);
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
index ae04111..88bf086 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
@@ -22,16 +22,37 @@
(function () {
'use strict';
- var Collection, Model;
+ var Collection, Model, is, sus, ts, t2vs;
+
+ var remappedDeviceTypes = {
+ virtual: 'cord'
+ };
+
+ // configuration
+ var devIconDim = 36,
+ labelPad = 10,
+ hostRadius = 14,
+ badgeConfig = {
+ radius: 12,
+ yoff: 5,
+ gdelta: 10
+ },
+ halfDevIcon = devIconDim / 2,
+ devBadgeOff = { dx: -halfDevIcon, dy: -halfDevIcon },
+ hostBadgeOff = { dx: -hostRadius, dy: -hostRadius },
+ status = {
+ i: 'badgeInfo',
+ w: 'badgeWarn',
+ e: 'badgeError'
+ },
+ deviceLabelIndex = 0;
function createDeviceCollection(data, region) {
var DeviceCollection = Collection.extend({
model: Model,
- get: function () {},
comparator: function(a, b) {
-
- var order = region.layerOrder;
+ var order = region.get('layerOrder');
return order.indexOf(a.get('layer')) - order.indexOf(b.get('layer'));
}
});
@@ -49,14 +70,106 @@
return deviceCollection;
}
+ function mapDeviceTypeToGlyph(type) {
+ return remappedDeviceTypes[type] || type || 'unknown';
+ }
+
+ function deviceLabel(d) {
+ //TODO: Device Json is missing labels array
+ return "";
+ var labels = this.get('labels'),
+ idx = (deviceLabelIndex < labels.length) ? deviceLabelIndex : 0;
+ return labels[idx];
+ }
+
+ function trimLabel(label) {
+ return (label && label.trim()) || '';
+ }
+
+ function computeLabelWidth() {
+ var text = this.select('text'),
+ box = text.node().getBBox();
+ return box.width + labelPad * 2;
+ }
+
+ function iconBox(dim, labelWidth) {
+ return {
+ x: -dim / 2,
+ y: -dim / 2,
+ width: dim + labelWidth,
+ height: dim
+ }
+ }
+
+ function deviceGlyphColor(d) {
+
+ var o = this.node.online,
+ id = "127.0.0.1", // TODO: This should be from node.master
+ otag = o ? 'online' : 'offline';
+ return o ? sus.cat7().getColor(id, 0, ts.theme())
+ : dColTheme[ts.theme()][otag];
+ }
+
+ function setDeviceColor() {
+ this.el.select('use')
+ .style('fill', this.deviceGlyphColor());
+ }
+
angular.module('ovTopo2')
.factory('Topo2DeviceService',
- ['Topo2Collection', 'Topo2Model',
+ ['Topo2Collection', 'Topo2NodeModel', 'IconService', 'SvgUtilService',
+ 'ThemeService', 'Topo2ViewService',
- function (_Collection_, _Model_) {
+ function (_Collection_, _NodeModel_, _is_, _sus_, _ts_, classnames, _t2vs_) {
+ t2vs = _t2vs_;
+ is = _is_;
+ sus = _sus_;
+ ts = _ts_;
Collection = _Collection_;
- Model = _Model_.extend({});
+
+ Model = _NodeModel_.extend({
+ initialize: function () {
+ this.set('weight', 0);
+ this.constructor.__super__.initialize.apply(this, arguments);
+ },
+ nodeType: 'device',
+ deviceLabel: deviceLabel,
+ deviceGlyphColor: deviceGlyphColor,
+ mapDeviceTypeToGlyph: mapDeviceTypeToGlyph,
+ trimLabel: trimLabel,
+ setDeviceColor: setDeviceColor,
+ onEnter: function (el) {
+
+ var node = d3.select(el),
+ glyphId = mapDeviceTypeToGlyph(this.get('type')),
+ label = trimLabel(this.deviceLabel()),
+ rect, text, glyph, labelWidth;
+
+ this.el = node;
+
+ rect = node.append('rect');
+
+ text = node.append('text').text(label)
+ .attr('text-anchor', 'left')
+ .attr('y', '0.3em')
+ .attr('x', halfDevIcon + labelPad);
+
+ glyph = is.addDeviceIcon(node, glyphId, devIconDim);
+
+ labelWidth = label ? computeLabelWidth(node) : 0;
+
+ rect.attr(iconBox(devIconDim, labelWidth));
+ glyph.attr(iconBox(devIconDim, 0));
+
+ node.attr('transform', sus.translate(-halfDevIcon, -halfDevIcon));
+ this.render();
+ },
+ onExit: function () {},
+ render: function () {
+ this.setDeviceColor();
+ }
+ });
return {
createDeviceCollection: createDeviceCollection
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
index 481b96b..bbd7f8d 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
@@ -60,62 +60,17 @@
linkLabel,
node;
- var $log, wss, t2is, t2rs;
+ var $log, wss, t2is, t2rs, t2ls, t2vs;
+ var svg, forceG, uplink, dim, opts;
// ========================== Helper Functions
- function init(_svg_, forceG, _uplink_, _dim_, opts) {
-
- $log.debug('Initialize topo force layout');
-
- nodeG = forceG.append('g').attr('id', 'topo-nodes');
- node = nodeG.selectAll('.node');
-
- linkG = forceG.append('g').attr('id', 'topo-links');
- linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
- numLinkLblsG = forceG.append('g').attr('id', 'topo-numLinkLabels');
- nodeG = forceG.append('g').attr('id', 'topo-nodes');
- portLabelG = forceG.append('g').attr('id', 'topo-portLabels');
-
- link = linkG.selectAll('.link');
- linkLabel = linkLabelG.selectAll('.linkLabel');
- node = nodeG.selectAll('.node');
-
- var width = 640,
- height = 480;
-
- var nodes = [
- { x: width/3, y: height/2 },
- { x: 2*width/3, y: height/2 }
- ];
-
- var links = [
- { source: 0, target: 1 }
- ];
-
- var svg = d3.select('body').append('svg')
- .attr('width', width)
- .attr('height', height);
-
- var force = d3.layout.force()
- .size([width, height])
- .nodes(nodes)
- .links(links);
-
- force.linkDistance(width/2);
-
-
- var link = svg.selectAll('.link')
- .data(links)
- .enter().append('line')
- .attr('class', 'link');
-
- var node = svg.selectAll('.node')
- .data(nodes)
- .enter().append('circle')
- .attr('class', 'node');
-
- force.start();
+ function init(_svg_, _forceG_, _uplink_, _dim_, _opts_) {
+ svg = _svg_;
+ forceG = _forceG_;
+ uplink = _uplink_;
+ dim = _dim_;
+ opts = _opts_
}
function destroy() {
@@ -206,6 +161,9 @@
$log.debug('>> topo2CurrentRegion event:', data);
doTmpCurrentRegion(data);
t2rs.addRegion(data);
+ t2ls.init(svg, forceG, uplink, dim, opts);
+ t2ls.update();
+ t2ls.start();
}
function topo2PeerRegions(data) {
@@ -257,20 +215,37 @@
// link.classed(cls, b);
}
+ function newDim(_dim_) {
+ dim = _dim_;
+ t2vs.newDim(dim);
+ // force.size(dim);
+ // tms.newDim(dim);
+ t2ls.setDimensions();
+ }
+
+ function getDim() {
+ return dim;
+ }
+
// ========================== Main Service Definition
angular.module('ovTopo2')
.factory('Topo2ForceService',
['$log', 'WebSocketService', 'Topo2InstanceService', 'Topo2RegionService',
- function (_$log_, _wss_, _t2is_, _t2rs_) {
+ 'Topo2LayoutService', 'Topo2ViewService',
+ function (_$log_, _wss_, _t2is_, _t2rs_, _t2ls_, _t2vs_) {
+
$log = _$log_;
wss = _wss_;
t2is = _t2is_;
t2rs = _t2rs_;
+ t2ls = _t2ls_;
+ t2vs = _t2vs_;
return {
init: init,
+ newDim: newDim,
destroy: destroy,
topo2AllInstances: allInstances,
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Host.js b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
index 19c2012..25d088a 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
@@ -22,7 +22,7 @@
(function () {
'use strict';
- var Collection, Model;
+ var Collection, Model, t2vs;
function createHostCollection(data, region) {
@@ -42,17 +42,21 @@
angular.module('ovTopo2')
.factory('Topo2HostService',
- ['Topo2Collection', 'Topo2Model',
+ [
+ 'Topo2Collection', 'Topo2NodeModel', 'Topo2ViewService',
+ function (_Collection_, _NodeModel_, classnames, _t2vs_) {
- function (_Collection_, _Model_) {
+ t2vs = _t2vs_;
+ Collection = _Collection_;
- Collection = _Collection_;
- Model = _Model_.extend();
+ Model = _NodeModel_.extend({
+ nodeType: 'host'
+ });
- return {
- createHostCollection: createHostCollection
- };
- }
- ]);
+ return {
+ createHostCollection: createHostCollection
+ };
+ }
+ ]);
})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
new file mode 100644
index 0000000..8cfaadf
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Layout Module.
+ Module that contains the d3.force.layout logic
+ */
+
+(function () {
+ 'use strict';
+
+ var $log, sus, t2rs, t2d3, t2vs;
+
+ var linkG, linkLabelG, numLinkLabelsG, nodeG, portLabelG;
+ var link, linkLabel, node;
+
+ var nodes, links;
+
+ var force;
+
+ // default settings for force layout
+ var defaultSettings = {
+ gravity: 0.4,
+ friction: 0.7,
+ charge: {
+ // note: key is node.class
+ device: -8000,
+ host: -5000,
+ _def_: -12000
+ },
+ linkDistance: {
+ // note: key is link.type
+ direct: 100,
+ optical: 120,
+ hostLink: 3,
+ _def_: 50
+ },
+ linkStrength: {
+ // note: key is link.type
+ // range: {0.0 ... 1.0}
+ //direct: 1.0,
+ //optical: 1.0,
+ //hostLink: 1.0,
+ _def_: 1.0
+ }
+ };
+
+ // configuration
+ var linkConfig = {
+ light: {
+ baseColor: '#939598',
+ inColor: '#66f',
+ outColor: '#f00'
+ },
+ dark: {
+ // TODO : theme
+ baseColor: '#939598',
+ inColor: '#66f',
+ outColor: '#f00'
+ },
+ inWidth: 12,
+ outWidth: 10
+ };
+
+ // internal state
+ var settings, // merged default settings and options
+ force, // force layout object
+ drag, // drag behavior handler
+ network = {
+ nodes: [],
+ links: [],
+ linksByDevice: {},
+ lookup: {},
+ revLinkToKey: {}
+ },
+ lu, // shorthand for lookup
+ rlk, // shorthand for revLinktoKey
+ showHosts = false, // whether hosts are displayed
+ showOffline = true, // whether offline devices are displayed
+ nodeLock = false, // whether nodes can be dragged or not (locked)
+ fTimer, // timer for delayed force layout
+ fNodesTimer, // timer for delayed nodes update
+ fLinksTimer, // timer for delayed links update
+ dim, // the dimensions of the force layout [w,h]
+ linkNums = []; // array of link number labels
+
+ var tickStuff = {
+ nodeAttr: {
+ transform: function (d) {
+ var dx = isNaN(d.x) ? 0 : d.x,
+ dy = isNaN(d.y) ? 0 : d.y;
+ return sus.translate(dx, dy);
+ }
+ },
+ linkAttr: {
+ x1: function (d) { return d.get('position').x1; },
+ y1: function (d) { return d.get('position').y1; },
+ x2: function (d) { return d.get('position').x2; },
+ y2: function (d) { return d.get('position').y2; }
+ },
+ linkLabelAttr: {
+ transform: function (d) {
+ var lnk = tms.findLinkById(d.get('key'));
+ if (lnk) {
+ return t2d3.transformLabel(lnk.get('position'));
+ }
+ }
+ }
+ };
+
+ function init(_svg_, forceG, _uplink_, _dim_, opts) {
+
+ $log.debug("Initialising Topology Layout");
+
+ settings = angular.extend({}, defaultSettings, opts);
+
+ linkG = forceG.append('g').attr('id', 'topo-links');
+ linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
+ numLinkLabelsG = forceG.append('g').attr('id', 'topo-numLinkLabels');
+ nodeG = forceG.append('g').attr('id', 'topo-nodes');
+ portLabelG = forceG.append('g').attr('id', 'topo-portLabels');
+
+ link = linkG.selectAll('.link');
+ linkLabel = linkLabelG.selectAll('.linkLabel');
+ node = nodeG.selectAll('.node');
+
+ force = d3.layout.force()
+ .size(t2vs.getDimensions())
+ .nodes(t2rs.regionNodes())
+ .links(t2rs.regionLinks())
+ .gravity(settings.gravity)
+ .friction(settings.friction)
+ .charge(settings.charge._def_)
+ .linkDistance(settings.linkDistance._def_)
+ .linkStrength(settings.linkStrength._def_)
+ .on('tick', tick);
+ }
+
+ function tick() {
+ // guard against null (which can happen when our view pages out)...
+ if (node && node.size()) {
+ node.attr(tickStuff.nodeAttr);
+ }
+ if (link && link.size()) {
+ link.call(calcPosition)
+ .attr(tickStuff.linkAttr);
+ // t2d3.applyNumLinkLabels(linkNums, numLinkLabelsG);
+ }
+ if (linkLabel && linkLabel.size()) {
+ linkLabel.attr(tickStuff.linkLabelAttr);
+ }
+ }
+
+ function update() {
+ _updateNodes();
+ _updateLinks();
+ }
+
+ function _updateNodes() {
+
+ var regionNodes = t2rs.regionNodes();
+
+ // select all the nodes in the layout:
+ node = nodeG.selectAll('.node')
+ .data(regionNodes, function (d) { return d.get('id'); });
+
+ var entering = node.enter()
+ .append('g')
+ .attr({
+ id: function (d) { return sus.safeId(d.get('id')); },
+ class: function (d) { return d.svgClassName() },
+ transform: function (d) {
+ // Need to guard against NaN here ??
+ return sus.translate(d.node.x, d.node.y);
+ },
+ opacity: 0
+ })
+ // .on('mouseover', tss.nodeMouseOver)
+ // .on('mouseout', tss.nodeMouseOut)
+ .transition()
+ .attr('opacity', 1);
+
+ entering.filter('.device').each(t2d3.deviceEnter);
+ entering.filter('.host').each(t2d3.hostEnter);
+
+ // operate on both existing and new nodes:
+ // node.filter('.device').each(function (device) {
+ // t2d3.updateDeviceColors(device);
+ // });
+ }
+
+ function _updateLinks() {
+
+ // var th = ts.theme();
+ var regionLinks = t2rs.regionLinks();
+
+ link = linkG.selectAll('.link')
+ .data(regionLinks, function (d) { return d.get('key'); });
+
+ // operate on existing links:
+ link.each(function (d) {
+ // this is supposed to be an existing link, but we have observed
+ // occasions (where links are deleted and added rapidly?) where
+ // the DOM element has not been defined. So protect against that...
+ if (d.el) {
+ restyleLinkElement(d, true);
+ }
+ });
+
+ // operate on entering links:
+ var entering = link.enter()
+ .append('line')
+ .call(calcPosition)
+ .attr({
+ x1: function (d) { return d.get('position').x1; },
+ y1: function (d) { return d.get('position').y1; },
+ x2: function (d) { return d.get('position').x2; },
+ y2: function (d) { return d.get('position').y2; },
+ stroke: linkConfig['light'].inColor,
+ 'stroke-width': linkConfig.inWidth
+ });
+
+ entering.each(t2d3.linkEntering);
+
+ // operate on both existing and new links:
+ //link.each(...)
+
+ // add labels for how many links are in a thick line
+ // t2d3.applyNumLinkLabels(linkNums, numLinkLabelsG);
+
+ // apply or remove labels
+ // t2d3.applyLinkLabels();
+
+ // operate on exiting links:
+ link.exit()
+ .attr('stroke-dasharray', '3 3')
+ .attr('stroke', linkConfig['light'].outColor)
+ .style('opacity', 0.5)
+ .transition()
+ .duration(1500)
+ .attr({
+ 'stroke-dasharray': '3 12',
+ 'stroke-width': linkConfig.outWidth
+ })
+ .style('opacity', 0.0)
+ .remove();
+ }
+
+ function calcPosition() {
+ var lines = this,
+ linkSrcId,
+ linkNums = [];
+
+ lines.each(function (d) {
+ if (d.get('type') === 'hostLink') {
+ d.set('position', getDefaultPos(d));
+ }
+ });
+
+ function normalizeLinkSrc(link) {
+ // ensure source device is consistent across set of links
+ // temporary measure until link modeling is refactored
+ if (!linkSrcId) {
+ linkSrcId = link.source.id;
+ return false;
+ }
+
+ return link.source.id !== linkSrcId;
+ }
+
+ lines.each(function (d) {
+ d.set('position', getDefaultPos(d));
+ });
+ }
+
+ function getDefaultPos(link) {
+
+ return {
+ x1: link.get('source').x,
+ y1: link.get('source').y,
+ x2: link.get('target').x,
+ y2: link.get('target').y
+ };
+ }
+
+ function setDimensions() {
+ if (force) {
+ force.size(t2vs.getDimensions());
+ }
+ }
+
+
+ function start() {
+ force.start();
+ }
+
+ angular.module('ovTopo2')
+ .factory('Topo2LayoutService',
+ [
+ '$log', 'SvgUtilService', 'Topo2RegionService',
+ 'Topo2D3Service', 'Topo2ViewService',
+
+ function (_$log_, _sus_, _t2rs_, _t2d3_, _t2vs_) {
+
+ $log = _$log_;
+ t2rs = _t2rs_;
+ t2d3 = _t2d3_;
+ t2vs = _t2vs_;
+ sus = _sus_;
+
+ return {
+ init: init,
+ update: update,
+ start: start,
+
+ setDimensions: setDimensions
+ }
+ }
+ ]
+ );
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
index 5f2b6b7..44c5ec9 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
@@ -22,12 +22,162 @@
(function () {
'use strict';
- var Collection, Model;
+ var Collection, Model, region, ts;
- function createLinkCollection(data, region) {
+ var widthRatio = 1.4,
+ linkScale = d3.scale.linear()
+ .domain([1, 12])
+ .range([widthRatio, 12 * widthRatio])
+ .clamp(true),
+ allLinkTypes = 'direct indirect optical tunnel UiDeviceLink',
+ allLinkSubTypes = 'inactive not-permitted';
+
+ // configuration
+ var linkConfig = {
+ light: {
+ baseColor: '#939598',
+ inColor: '#66f',
+ outColor: '#f00'
+ },
+ dark: {
+ // TODO : theme
+ baseColor: '#939598',
+ inColor: '#66f',
+ outColor: '#f00'
+ },
+ inWidth: 12,
+ outWidth: 10
+ };
+
+ var defaultLinkType = 'direct',
+ nearDist = 15;
+
+ function createLink() {
+
+ var linkPoints = this.linkEndPoints(this.get('epA'), this.get('epB'));
+ console.log(this);
+
+ var attrs = angular.extend({}, linkPoints, {
+ key: this.get('id'),
+ class: 'link',
+ weight: 1,
+ srcPort: this.get('srcPort'),
+ tgtPort: this.get('dstPort'),
+ position: {
+ x1: 0,
+ y1: 0,
+ x2: 0,
+ y2: 0
+ }
+ // functions to aggregate dual link state
+// extra: link.extra
+ });
+
+ this.set(attrs);
+ }
+
+ function linkEndPoints(srcId, dstId) {
+
+ var sourceNode = this.region.get('devices').get(srcId.substring(0, srcId.length -2));
+ var targetNode = this.region.get('devices').get(dstId.substring(0, dstId.length -2));
+
+// var srcNode = lu[srcId],
+// dstNode = lu[dstId],
+// sMiss = !srcNode ? missMsg('src', srcId) : '',
+// dMiss = !dstNode ? missMsg('dst', dstId) : '';
+//
+// if (sMiss || dMiss) {
+// $log.error('Node(s) not on map for link:' + sMiss + dMiss);
+// //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
+// return null;
+// }
+
+ this.source = sourceNode.toJSON();
+ this.target = targetNode.toJSON();
+
+ return {
+ source: sourceNode,
+ target: targetNode
+ };
+ }
+
+ function createLinkCollection(data, _region) {
+
+ var LinkModel = Model.extend({
+ region: _region,
+ createLink: createLink,
+ linkEndPoints: linkEndPoints,
+ type: function () {
+ return this.get('type');
+ },
+ expected: function () {
+ //TODO: original code is: (s && s.expected) && (t && t.expected);
+ return true;
+ },
+ online: function () {
+ return true;
+ return both && (s && s.online) && (t && t.online);
+ },
+ linkWidth: function () {
+ var s = this.get('fromSource'),
+ t = this.get('fromTarget'),
+ ws = (s && s.linkWidth) || 0,
+ wt = (t && t.linkWidth) || 0;
+
+ // console.log(s);
+ // TODO: Current json is missing linkWidth
+ return 1.2;
+ return this.get('position').multiLink ? 5 : Math.max(ws, wt);
+ },
+
+ restyleLinkElement: function (immediate) {
+ // this fn's job is to look at raw links and decide what svg classes
+ // need to be applied to the line element in the DOM
+ var th = ts.theme(),
+ el = this.el,
+ type = this.get('type'),
+ lw = this.linkWidth(),
+ online = this.online(),
+ modeCls = this.expected() ? 'inactive' : 'not-permitted',
+ delay = immediate ? 0 : 1000;
+
+ console.log(type);
+
+ // NOTE: understand why el is sometimes undefined on addLink events...
+ // Investigated:
+ // el is undefined when it's a reverse link that is being added.
+ // updateLinks (which sets ldata.el) isn't called before this is called.
+ // Calling _updateLinks in addLinkUpdate fixes it, but there might be
+ // a more efficient way to fix it.
+ if (el && !el.empty()) {
+ el.classed('link', true);
+ el.classed(allLinkSubTypes, false);
+ el.classed(modeCls, !online);
+ el.classed(allLinkTypes, false);
+ if (type) {
+ el.classed(type, true);
+ }
+ el.transition()
+ .duration(delay)
+ .attr('stroke-width', linkScale(lw))
+ .attr('stroke', linkConfig[th].baseColor);
+ }
+ },
+
+ onEnter: function (el) {
+ var link = d3.select(el);
+ this.el = link;
+
+ this.restyleLinkElement();
+
+ if (this.get('type') === 'hostLink') {
+ sus.visible(link, api.showHosts());
+ }
+ }
+ });
var LinkCollection = Collection.extend({
- model: Model
+ model: LinkModel,
});
return new LinkCollection(data);
@@ -35,12 +185,13 @@
angular.module('ovTopo2')
.factory('Topo2LinkService',
- ['Topo2Collection', 'Topo2Model',
+ ['Topo2Collection', 'Topo2Model', 'ThemeService',
- function (_Collection_, _Model_) {
+ function (_Collection_, _Model_, _ts_) {
+ ts = _ts_;
Collection = _Collection_;
- Model = _Model_.extend({});
+ Model = _Model_;
return {
createLinkCollection: createLinkCollection
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Model.js b/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
index fa40d65..20fb5e0 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
@@ -1,23 +1,23 @@
/*
- * Copyright 2016-present Open Networking Laboratory
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+* Copyright 2016-present Open Networking Laboratory
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
/*
- ONOS GUI -- Topology Force Module.
- Visualization of the topology in an SVG layer, using a D3 Force Layout.
- */
+ONOS GUI -- Topology Force Module.
+Visualization of the topology in an SVG layer, using a D3 Force Layout.
+*/
(function () {
'use strict';
@@ -28,17 +28,86 @@
this.attributes = {};
attrs = angular.extend({}, attrs);
- this.set(attrs);
+ this.set(attrs, { silent: true });
+ this.initialize.apply(this, arguments);
}
Model.prototype = {
+ initialize: function () {},
+
+ onChange: function (property, value, options) {},
+
get: function (attr) {
return this.attributes[attr];
},
- set: function(data) {
- angular.extend(this.attributes, data);
+ set: function(key, val, options) {
+
+ if (!key) {
+ return this;
+ }
+
+ var attributes;
+ if (typeof key === 'object') {
+ attributes = key;
+ options = val;
+ } else {
+ (attributes = {})[key] = val;
+ }
+
+ options || (options = {});
+
+ var unset = options.unset,
+ silent = options.silent,
+ changes = [],
+ changing = this._changing;
+
+ this._changing = true;
+
+ if (!changing) {
+
+ // NOTE: angular.copy causes issues in chrome
+ this._previousAttributes = Object.create(Object.getPrototypeOf(this.attributes));
+ this.changed = {};
+ }
+
+ var current = this.attributes,
+ changed = this.changed,
+ previous = this._previousAttributes;
+
+ angular.forEach(attributes, function (attribute, index) {
+
+ val = attribute;
+
+ if (!angular.equals(current[index], val)) {
+ changes.push(index);
+ }
+
+ if (!angular.equals(previous[index], val)) {
+ changed[index] = val;
+ } else {
+ delete changed[index];
+ }
+
+ unset ? delete current[index] : current[index] = val;
+ });
+
+ // Trigger all relevant attribute changes.
+ if (!silent) {
+ if (changes.length) {
+ this._pending = options;
+ }
+ for (var i = 0; i < changes.length; i++) {
+ this.onChange(changes[i], this, current[changes[i]], options);
+ }
+ }
+
+ this._changing = false;
+ return this;
+ },
+ toJSON: function(options) {
+ return angular.copy(this.attributes)
},
};
@@ -67,11 +136,11 @@
};
angular.module('ovTopo2')
- .factory('Topo2Model',
- [
- function () {
- return Model;
- }
- ]);
+ .factory('Topo2Model',
+ [
+ function () {
+ return Model;
+ }
+ ]);
})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
new file mode 100644
index 0000000..54a2748
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Layout Module.
+ Module that contains the d3.force.layout logic
+ */
+
+(function () {
+ 'use strict';
+
+ var randomService;
+ var fn;
+
+ //internal state;
+ var defaultLinkType = 'direct',
+ nearDist = 15;
+
+ function positionNode(node, forUpdate) {
+
+ var meta = node.metaUi,
+ x = meta && meta.x,
+ y = meta && meta.y,
+ dim = [800, 600],
+ xy;
+
+ // if the device contains explicit LONG/LAT data, use that to position
+ if (setLongLat(node)) {
+ //indicate we want to update cached meta data...
+ return true;
+ }
+
+ // else if we have [x,y] cached in meta data, use that...
+ if (x !== undefined && y !== undefined) {
+ node.fixed = true;
+ node.px = node.x = x;
+ node.py = node.y = y;
+ return;
+ }
+
+ // if this is a node update (not a node add).. skip randomizer
+ if (forUpdate) {
+ return;
+ }
+
+ // Note: Placing incoming unpinned nodes at exactly the same point
+ // (center of the view) causes them to explode outwards when
+ // the force layout kicks in. So, we spread them out a bit
+ // initially, to provide a more serene layout convergence.
+ // Additionally, if the node is a host, we place it near
+ // the device it is connected to.
+
+ function rand() {
+ return {
+ x: randomService.randDim(dim[0]),
+ y: randomService.randDim(dim[1])
+ };
+ }
+
+ function near(node) {
+ return {
+ x: node.x + nearDist + randomService.spread(nearDist),
+ y: node.y + nearDist + randomService.spread(nearDist)
+ };
+ }
+
+ function getDevice(cp) {
+ // console.log(cp);
+ // var d = lu[cp.device];
+ // return d || rand();
+ return rand();
+ }
+
+ xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
+ angular.extend(node, xy);
+ }
+
+ function setLongLat(node) {
+ var loc = node.location,
+ coord;
+
+ if (loc && loc.type === 'lnglat') {
+ coord = [0, 0];
+ node.fixed = true;
+ node.px = node.x = coord[0];
+ node.py = node.y = coord[1];
+ return true;
+ }
+ }
+
+ angular.module('ovTopo2')
+ .factory('Topo2NodeModel',
+ ['Topo2Model', 'FnService', 'RandomService',
+ function (Model, _fn_, _RandomService_) {
+
+ randomService = _RandomService_;
+ fn = _fn_;
+
+ return Model.extend({
+ initialize: function () {
+ this.node = this.createNode();
+ },
+ svgClassName: function () {
+ return fn.classNames('node', this.nodeType, this.get('type'), {
+ online: this.get('online')
+ });
+ },
+ createNode: function () {
+
+ var node = angular.extend({}, this.attributes);
+
+ // Augment as needed...
+ node.class = this.nodeType;
+ node.svgClass = this.svgClassName();
+ positionNode(node);
+ return node;
+ }
+ });
+ }]
+ );
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
index ff1d52f..45c2652 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
@@ -24,12 +24,13 @@
var $log,
wss,
+ Model,
t2sr,
t2ds,
t2hs,
t2ls;
- var regions;
+ var region;
function init() {
regions = {};
@@ -37,25 +38,46 @@
function addRegion(data) {
- var region = {
- subregions: t2sr.createSubRegionCollection(data.subregions),
- devices: t2ds.createDeviceCollection(data.devices, data),
- hosts: t2hs.createHostCollection(data.hosts),
- links: t2ls.createLinkCollection(data.links),
- };
+ region = new Model({
+ id: data.id,
+ layerOrder: data.layerOrder
+ });
+
+ region.set({
+ subregions: t2sr.createSubRegionCollection(data.subregions, region),
+ devices: t2ds.createDeviceCollection(data.devices, region),
+ hosts: t2hs.createHostCollection(data.hosts, region),
+ links: t2ls.createLinkCollection(data.links, region),
+ });
+
+ region.set('test', 2);
+
+ angular.forEach(region.get('links').models, function (link) {
+ link.createLink();
+ });
$log.debug('Region: ', region);
}
+ function regionNodes() {
+ return [].concat(region.get('devices').models, region.get('hosts').models);
+ }
+
+
+ function regionLinks() {
+ return region.get('links').models;
+ }
+
angular.module('ovTopo2')
.factory('Topo2RegionService',
- ['$log', 'WebSocketService', 'Topo2SubRegionService', 'Topo2DeviceService',
+ ['$log', 'WebSocketService', 'Topo2Model', 'Topo2SubRegionService', 'Topo2DeviceService',
'Topo2HostService', 'Topo2LinkService',
- function (_$log_, _wss_, _t2sr_, _t2ds_, _t2hs_, _t2ls_) {
+ function (_$log_, _wss_, _Model_, _t2sr_, _t2ds_, _t2hs_, _t2ls_) {
$log = _$log_;
wss = _wss_;
+ Model = _Model_
t2sr = _t2sr_;
t2ds = _t2ds_;
t2hs = _t2hs_;
@@ -65,6 +87,9 @@
init: init,
addRegion: addRegion,
+ regionNodes: regionNodes,
+ regionLinks: regionLinks,
+
getSubRegions: t2sr.getSubRegions
};
}]);
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Select.js b/web/gui/src/main/webapp/app/view/topo2/topo2Select.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Select.js
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Theme.js b/web/gui/src/main/webapp/app/view/topo2/topo2Theme.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Theme.js
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2View.js b/web/gui/src/main/webapp/app/view/topo2/topo2View.js
new file mode 100644
index 0000000..e856a1f
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2View.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Layout Module.
+ Module that contains the d3.force.layout logic
+ */
+
+(function () {
+ 'use strict';
+
+ var dimensions;
+
+ function newDim(_dimensions) {
+ dimensions = _dimensions;
+ }
+
+ function getDimensions() {
+ return dimensions;
+ }
+
+ angular.module('ovTopo2')
+ .factory('Topo2ViewService',
+ [
+ function () {
+ return {
+ newDim: newDim,
+ getDimensions: getDimensions
+ }
+ }
+ ]
+ );
+})();
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index 5f3cfb5..18e250f 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -128,15 +128,21 @@
<!-- Under development for Region support. -->
<script src="app/view/topo2/topo2.js"></script>
<script src="app/view/topo2/topo2Collection.js"></script>
+ <script src="app/view/topo2/topo2D3.js"></script>
<script src="app/view/topo2/topo2Device.js"></script>
- <script src="app/view/topo2/topo2Model.js"></script>
<script src="app/view/topo2/topo2Event.js"></script>
<script src="app/view/topo2/topo2Force.js"></script>
<script src="app/view/topo2/topo2Host.js"></script>
<script src="app/view/topo2/topo2Instance.js"></script>
+ <script src="app/view/topo2/topo2Layout.js"></script>
<script src="app/view/topo2/topo2Link.js"></script>
+ <script src="app/view/topo2/topo2Model.js"></script>
+ <script src="app/view/topo2/topo2NodeModel.js"></script>
<script src="app/view/topo2/topo2Region.js"></script>
+ <script src="app/view/topo2/topo2Select.js"></script>
<script src="app/view/topo2/topo2SubRegion.js"></script>
+ <script src="app/view/topo2/topo2Theme.js"></script>
+ <script src="app/view/topo2/topo2View.js"></script>
<link rel="stylesheet" href="app/view/topo2/topo2.css">
<link rel="stylesheet" href="app/view/topo2/topo2-theme.css">
diff --git a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
index 7e7dae5..e535460 100644
--- a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
@@ -216,7 +216,8 @@
'isMobile', 'isChrome', 'isSafari', 'isFirefox',
'debugOn', 'debug',
'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'sameObjProps', 'containsObj', 'cap',
- 'eecode', 'noPx', 'noPxStyle', 'endsWith', 'parseBitRate', 'addToTrie', 'removeFromTrie', 'trieLookup'
+ 'eecode', 'noPx', 'noPxStyle', 'endsWith', 'parseBitRate', 'addToTrie', 'removeFromTrie', 'trieLookup',
+ 'classNames'
])).toBeTruthy();
});
diff --git a/web/gui/src/test/_karma/package.json b/web/gui/src/test/_karma/package.json
new file mode 100644
index 0000000..20042ec
--- /dev/null
+++ b/web/gui/src/test/_karma/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "karma",
+ "version": "1.0.0",
+ "description": "",
+ "main": "mockserver.js",
+ "dependencies": {
+ "websocket": "^1.0.23"
+ },
+ "devDependencies": {},
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "ISC"
+}