From ec1f45c00cf9ebce6a1af31f08250c5d7acda3fe Mon Sep 17 00:00:00 2001 From: Steven Burrows Date: Mon, 8 Aug 2016 16:14:41 +0100 Subject: [PATCH] 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 --- web/gui/src/main/webapp/app/fw/util/fn.js | 31 +- .../src/main/webapp/app/view/topo/topoD3.js | 1 + .../main/webapp/app/view/topo/topoForce.js | 3 +- .../webapp/app/view/topo2/topo2-theme.css | 348 +++++++++++++++++- .../src/main/webapp/app/view/topo2/topo2.html | 5 +- .../src/main/webapp/app/view/topo2/topo2.js | 3 +- .../webapp/app/view/topo2/topo2Collection.js | 7 +- .../src/main/webapp/app/view/topo2/topo2D3.js | 163 ++++++++ .../main/webapp/app/view/topo2/topo2Device.js | 127 ++++++- .../main/webapp/app/view/topo2/topo2Force.js | 83 ++--- .../main/webapp/app/view/topo2/topo2Host.js | 24 +- .../main/webapp/app/view/topo2/topo2Layout.js | 334 +++++++++++++++++ .../main/webapp/app/view/topo2/topo2Link.js | 163 +++++++- .../main/webapp/app/view/topo2/topo2Model.js | 121 ++++-- .../webapp/app/view/topo2/topo2NodeModel.js | 134 +++++++ .../main/webapp/app/view/topo2/topo2Region.js | 43 ++- .../main/webapp/app/view/topo2/topo2Select.js | 0 .../main/webapp/app/view/topo2/topo2Theme.js | 0 .../main/webapp/app/view/topo2/topo2View.js | 46 +++ web/gui/src/main/webapp/index.html | 8 +- .../main/webapp/tests/app/fw/util/fn-spec.js | 3 +- web/gui/src/test/_karma/package.json | 15 + 22 files changed, 1535 insertions(+), 127 deletions(-) create mode 100644 web/gui/src/main/webapp/app/view/topo2/topo2D3.js create mode 100644 web/gui/src/main/webapp/app/view/topo2/topo2Layout.js create mode 100644 web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js create mode 100644 web/gui/src/main/webapp/app/view/topo2/topo2Select.js create mode 100644 web/gui/src/main/webapp/app/view/topo2/topo2Theme.js create mode 100644 web/gui/src/main/webapp/app/view/topo2/topo2View.js create mode 100644 web/gui/src/test/_karma/package.json 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 77c2b96a8b..33ad2f666e 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 da7d7292cf..5b669ce33f 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 b3fc5a321f..ef9f633c4f 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 63b5dfba87..3bdb8b2d74 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, #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 1f0c6d6945..32913a95a8 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 @@
-
+ + 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 063129924d..f62bf5c2a5 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 3116a6f68e..e0aefb7a6b 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 0000000000..604c907cde --- /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 ae04111720..88bf086e49 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 481b96b66c..bbd7f8d465 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 19c2012d6a..25d088a4a9 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 0000000000..8cfaadfb63 --- /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 5f2b6b7dc0..44c5ec9d76 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 fa40d65618..20fb5e046d 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 0000000000..54a27481ec --- /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 ff1d52f3a4..45c26521b4 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 0000000000..e69de29bb2 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 0000000000..e69de29bb2 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 0000000000..e856a1fd3c --- /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 5f3cfb56ac..18e250fab0 100644 --- a/web/gui/src/main/webapp/index.html +++ b/web/gui/src/main/webapp/index.html @@ -128,15 +128,21 @@ + - + + + + + + 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 7e7dae563c..e535460bb9 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 @@ describe('factory: fw/util/fn.js', function() { '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 0000000000..20042ec3f5 --- /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" +}