Steven Burrows
Committed by Gerrit Code Review

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
......@@ -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
};
}]);
......
......@@ -320,6 +320,7 @@
// updateLinks - subfunctions
function linkEntering(d) {
var link = d3.select(this);
d.el = link;
api.restyleLinkElement(d);
......
......@@ -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',
......
......@@ -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;
}
......
<!-- 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 -->
......
......@@ -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() {
......
......@@ -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) {
......
/*
* 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
}
}
]
);
})();
......@@ -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
......
......@@ -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,
......
......@@ -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
};
}
]);
})();
......
/*
* 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
}
}
]
);
})();
......@@ -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
......
/*
* 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;
}
]);
})();
......
/*
* 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;
}
});
}]
);
})();
......@@ -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
};
}]);
......
/*
* 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
}
}
]
);
})();
......@@ -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">
......
......@@ -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();
});
......
{
"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"
}