Simon Hunt

GUI -- Added handling of hosts and links. (Still WIP).

Change-Id: I0ad3b16d47b264b6812f732f220230a2ae92de02
......@@ -20,7 +20,7 @@
(function () {
'use strict';
var $log, fs, gs;
var $log, fs, gs, sus;
var vboxSize = 50,
cornerSize = vboxSize / 10,
......@@ -144,8 +144,21 @@
return g;
}
function addHostIcon(elem, glyphId) {
// TODO:
function addHostIcon(elem, radius, glyphId) {
var dim = radius * 1.5,
xlate = -dim / 2,
g = elem.append('g')
.attr('class', 'svgIcon hostIcon');
g.append('circle').attr('r', radius);
g.append('use').attr({
'xlink:href': '#' + glyphId,
width: dim,
height: dim,
transform: sus.translate(xlate,xlate)
});
return g;
}
......@@ -154,10 +167,13 @@
angular.module('onosSvg')
.factory('IconService', ['$log', 'FnService', 'GlyphService',
function (_$log_, _fs_, _gs_) {
'SvgUtilService',
function (_$log_, _fs_, _gs_, _sus_) {
$log = _$log_;
fs = _fs_;
gs = _gs_;
sus = _sus_;
return {
loadIcon: loadIcon,
......
......@@ -325,19 +325,121 @@
/* Host Nodes */
#ov-topo svg .node.host {
stroke: #000;
}
#ov-topo svg .node.host text {
fill: #846;
stroke: none;
font: 9pt sans-serif;
}
.light #ov-topo svg .node.host text {
fill: #846;
}
.dark #ov-topo svg .node.host text {
fill: #BB809D;
}
svg .node.host circle {
.light svg .node.host circle {
stroke: #000;
fill: #edb;
}
.dark svg .node.host circle {
stroke: #eee;
fill: #B2A180;
}
.light svg .node.host .svgIcon {
fill: #444;
}
.dark svg .node.host .svgIcon {
fill: #222;
}
/* --- Topo Links --- */
#ov-topo svg .link {
opacity: .9;
}
#ov-topo svg .link.inactive {
opacity: .5;
stroke-dasharray: 8 4;
}
#ov-topo svg .link.secondary {
stroke-width: 3px;
}
.light #ov-topo svg .link.secondary {
stroke: rgba(0,153,51,0.5);
}
.dark #ov-topo svg .link.secondary {
stroke: rgba(121,231,158,0.5);
}
#ov-topo svg .link.primary {
stroke-width: 4px;
}
.light #ov-topo svg .link.primary {
stroke: #ffA300;
}
.dark #ov-topo svg .link.primary {
stroke: #D58E0F;
}
.light #ov-topo svg .link.animated {
stroke: #ffA300;
}
.dark #ov-topo svg .link.animated {
stroke: #D58E0F;
}
#ov-topo svg .link.secondary.optical {
stroke-width: 4px;
}
.light #ov-topo svg .link.secondary.optical {
stroke: rgba(128,64,255,0.5);
}
.dark #ov-topo svg .link.secondary.optical {
stroke: rgba(164,139,215,0.5);
}
#ov-topo svg .link.primary.optical {
stroke-width: 6px;
}
.light #ov-topo svg .link.primary.optical {
stroke: #74f;
}
.dark #ov-topo svg .link.primary.optical {
stroke: #7352CD;
}
#ov-topo svg .link.animated.optical {
stroke-width: 10px;
}
.light #ov-topo svg .link.animated.optical {
stroke: #74f;
}
.dark #ov-topo svg .link.animated.optical {
stroke: #7352CD;
}
#ov-topo svg .linkLabel rect {
stroke: none;
}
.light #ov-topo svg .linkLabel rect {
fill: #eee;
}
.dark #ov-topo svg .linkLabel rect {
fill: #eee;
}
#ov-topo svg .linkLabel text {
text-anchor: middle;
stroke-width: 0.1;
font-size: 9pt;
}
.light #ov-topo svg .linkLabel text {
stroke: #777;
}
.dark #ov-topo svg .linkLabel text {
stroke: #777;
}
......
......@@ -198,12 +198,6 @@
return ms.loadMapInto(mapG, '*continental_us');
}
// --- Force Layout --------------------------------------------------
function setUpForce(xlink) {
forceG = zoomLayer.append('g').attr('id', 'topo-force');
tfs.initForce(forceG, xlink, svg.attr('width'), svg.attr('height'));
}
// --- Controller Definition -----------------------------------------
......@@ -219,8 +213,12 @@
function ($scope, _$log_, $loc, $timeout, _fs_, mast,
_ks_, _zs_, _gs_, _ms_, _sus_, tes, _tfs_, tps, _tis_) {
var self = this,
xlink = {
showNoDevs: showNoDevs
projection,
uplink = {
// provides function calls back into this space
showNoDevs: showNoDevs,
projection: function () { return projection; },
sendEvent: tes.sendEvent
};
$log = _$log_;
......@@ -255,9 +253,15 @@
setUpDefs();
setUpZoom();
setUpNoDevs();
xlink.projectionPromise = setUpMap();
setUpForce(xlink);
setUpMap().then(
function (proj) {
projection = proj;
$log.debug('** We installed the projection: ', proj);
}
);
forceG = zoomLayer.append('g').attr('id', 'topo-force');
tfs.initForce(forceG, uplink, svg.attr('width'), svg.attr('height'));
tis.initInst();
tps.initPanels();
tes.openSock();
......
......@@ -34,9 +34,16 @@
updateInstance: updateInstance,
removeInstance: removeInstance,
addDevice: addDevice,
updateDevice: updateDevice
// TODO: implement remaining handlers..
updateDevice: updateDevice,
removeDevice: removeDevice,
addHost: addHost,
updateHost: updateHost,
removeHost: removeHost,
addLink: addLink,
updateLink: updateLink,
removeLink: removeLink
// TODO: implement remaining handlers..
};
function unknownEvent(ev) {
......@@ -45,6 +52,9 @@
// === Event Handlers ===
// NOTE: --- once these are done, we will collapse them into
// a more compact data structure... but for now, write in full..
function showSummary(ev) {
$log.debug(' **** Show Summary **** ', ev.payload);
tps.showSummary(ev.payload);
......@@ -75,6 +85,42 @@
tfs.updateDevice(ev.payload);
}
function removeDevice(ev) {
$log.debug(' **** Remove Device **** ', ev.payload);
tfs.removeDevice(ev.payload);
}
function addHost(ev) {
$log.debug(' **** Add Host **** ', ev.payload);
tfs.addHost(ev.payload);
}
function updateHost(ev) {
$log.debug(' **** Update Host **** ', ev.payload);
tfs.updateHost(ev.payload);
}
function removeHost(ev) {
$log.debug(' **** Remove Host **** ', ev.payload);
tfs.removeHost(ev.payload);
}
function addLink(ev) {
$log.debug(' **** Add Link **** ', ev.payload);
tfs.addLink(ev.payload);
}
function updateLink(ev) {
$log.debug(' **** Update Link **** ', ev.payload);
tfs.updateLink(ev.payload);
}
function removeLink(ev) {
$log.debug(' **** Remove Link **** ', ev.payload);
tfs.removeLink(ev.payload);
}
// ==========================
var dispatcher = {
......@@ -122,14 +168,9 @@
tis = _tis_;
tfs = _tfs_;
function bindDispatcher(TopoDomElementsPassedHere) {
// TODO: store refs to topo DOM elements...
return dispatcher;
}
// TODO: handle "guiSuccessor" functionality (replace host)
// TODO: implement retry on close functionality
function openSock() {
wsock = wss.createWebSocket('topology', {
onOpen: onWsOpen,
......@@ -151,9 +192,9 @@
}
return {
bindDispatcher: bindDispatcher,
openSock: openSock,
closeSock: closeSock
closeSock: closeSock,
sendEvent: dispatcher.sendEvent
};
}]);
}());
......
......@@ -23,7 +23,9 @@
'use strict';
// injected refs
var $log, sus, is, ts, tis, xlink;
var $log, fs, sus, is, ts, tis, uplink;
var icfg;
// configuration
var labelConfig = {
......@@ -44,6 +46,21 @@
yoff: -18
};
var linkConfig = {
light: {
baseColor: '#666',
inColor: '#66f',
outColor: '#f00',
},
dark: {
baseColor: '#666',
inColor: '#66f',
outColor: '#f00',
},
inWidth: 12,
outWidth: 10
};
// internal state
var settings, // merged default settings and options
force, // force layout object
......@@ -54,9 +71,11 @@
lookup: {},
revLinkToKey: {}
},
projection, // background map projection
lu = network.lookup, // shorthand
deviceLabelIndex = 0, // for device label cycling
hostLabelIndex = 0; // for host label cycling
hostLabelIndex = 0, // for host label cycling
showHosts = 1, // whether hosts are displayed
width, height;
// SVG elements;
var linkG, linkLabelG, nodeG;
......@@ -99,18 +118,18 @@
var id = data.id,
d;
xlink.showNoDevs(false);
uplink.showNoDevs(false);
// although this is an add device event, if we already have the
// device, treat it as an update instead..
if (network.lookup[id]) {
if (lu[id]) {
updateDevice(data);
return;
}
d = createDeviceNode(data);
network.nodes.push(d);
network.lookup[id] = d;
lu[id] = d;
$log.debug("Created new device.. ", d.id, d.x, d.y);
......@@ -120,7 +139,7 @@
function updateDevice(data) {
var id = data.id,
d = network.lookup[id],
d = lu[id],
wasOnline;
if (d) {
......@@ -141,26 +160,379 @@
}
}
function removeDevice(data) {
var id = data.id,
d = lu[id];
if (d) {
removeDeviceElement(d);
} else {
// TODO: decide whether we want to capture logic errors
//logicError('removeDevice lookup fail. ID = "' + id + '"');
}
}
function addHost(data) {
var id = data.id,
d, lnk;
// although this is an add host event, if we already have the
// host, treat it as an update instead..
if (lu[id]) {
updateHost(data);
return;
}
d = createHostNode(data);
network.nodes.push(d);
lu[id] = d;
$log.debug("Created new host.. ", d.id, d.x, d.y);
updateNodes();
lnk = createHostLink(data);
if (lnk) {
$log.debug("Created new host-link.. ", lnk.key);
d.linkData = lnk; // cache ref on its host
network.links.push(lnk);
lu[d.ingress] = lnk;
lu[d.egress] = lnk;
updateLinks();
}
fStart();
}
function updateHost(data) {
var id = data.id,
d = lu[id];
if (d) {
angular.extend(d, data);
if (positionNode(d, true)) {
sendUpdateMeta(d, true);
}
updateNodes();
} else {
// TODO: decide whether we want to capture logic errors
//logicError('updateHost lookup fail. ID = "' + id + '"');
}
}
function removeHost(data) {
var id = data.id,
d = lu[id];
if (d) {
removeHostElement(d, true);
} else {
// may have already removed host, if attached to removed device
//console.warn('removeHost lookup fail. ID = "' + id + '"');
}
}
function addLink(data) {
var result = findLink(data, 'add'),
bad = result.badLogic,
d = result.ldata;
if (bad) {
//logicError(bad + ': ' + link.id);
return;
}
if (d) {
// we already have a backing store link for src/dst nodes
addLinkUpdate(d, data);
return;
}
// no backing store link yet
d = createLink(data);
if (d) {
network.links.push(d);
lu[d.key] = d;
updateLinks();
fStart();
}
}
function updateLink(data) {
var result = findLink(data, 'update'),
bad = result.badLogic;
if (bad) {
//logicError(bad + ': ' + link.id);
return;
}
result.updateWith(link);
}
function removeLink(data) {
var result = findLink(data, 'remove'),
bad = result.badLogic;
if (bad) {
// may have already removed link, if attached to removed device
//console.warn(bad + ': ' + link.id);
return;
}
result.removeRawLink();
}
// ========================
function addLinkUpdate(ldata, link) {
// add link event, but we already have the reverse link installed
ldata.fromTarget = link;
network.revLinkToKey[link.id] = ldata.key;
restyleLinkElement(ldata);
}
function createLink(link) {
var lnk = linkEndPoints(link.src, link.dst);
if (!lnk) {
return null;
}
angular.extend(lnk, {
key: link.id,
class: 'link',
fromSource: link,
// functions to aggregate dual link state
type: function () {
var s = lnk.fromSource,
t = lnk.fromTarget;
return (s && s.type) || (t && t.type) || defaultLinkType;
},
online: function () {
var s = lnk.fromSource,
t = lnk.fromTarget,
both = lnk.source.online && lnk.target.online;
return both && ((s && s.online) || (t && t.online));
},
linkWidth: function () {
var s = lnk.fromSource,
t = lnk.fromTarget,
ws = (s && s.linkWidth) || 0,
wt = (t && t.linkWidth) || 0;
return Math.max(ws, wt);
}
});
return lnk;
}
function makeNodeKey(d, what) {
var port = what + 'Port';
return d[what] + '/' + d[port];
}
function makeLinkKey(d, flipped) {
var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
return one + '-' + two;
}
var widthRatio = 1.4,
linkScale = d3.scale.linear()
.domain([1, 12])
.range([widthRatio, 12 * widthRatio])
.clamp(true);
var allLinkTypes = 'direct indirect optical tunnel',
defaultLinkType = 'direct';
function restyleLinkElement(ldata) {
// 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 = ldata.el,
type = ldata.type(),
lw = ldata.linkWidth(),
online = ldata.online();
el.classed('link', true);
el.classed('inactive', !online);
el.classed(allLinkTypes, false);
if (type) {
el.classed(type, true);
}
el.transition()
.duration(1000)
.attr('stroke-width', linkScale(lw))
.attr('stroke', linkConfig[th].baseColor);
}
function findLink(linkData, op) {
var key = makeLinkKey(linkData),
keyrev = makeLinkKey(linkData, 1),
link = lu[key],
linkRev = lu[keyrev],
result = {},
ldata = link || linkRev,
rawLink;
if (op === 'add') {
if (link) {
// trying to add a link that we already know about
result.ldata = link;
result.badLogic = 'addLink: link already added';
} else if (linkRev) {
// we found the reverse of the link to be added
result.ldata = linkRev;
if (linkRev.fromTarget) {
result.badLogic = 'addLink: link already added';
}
}
} else if (op === 'update') {
if (!ldata) {
result.badLogic = 'updateLink: link not found';
} else {
rawLink = link ? ldata.fromSource : ldata.fromTarget;
result.updateWith = function (data) {
angular.extend(rawLink, data);
restyleLinkElement(ldata);
}
}
} else if (op === 'remove') {
if (!ldata) {
result.badLogic = 'removeLink: link not found';
} else {
rawLink = link ? ldata.fromSource : ldata.fromTarget;
if (!rawLink) {
result.badLogic = 'removeLink: link not found';
} else {
result.removeRawLink = function () {
if (link) {
// remove fromSource
ldata.fromSource = null;
if (ldata.fromTarget) {
// promote target into source position
ldata.fromSource = ldata.fromTarget;
ldata.fromTarget = null;
ldata.key = keyrev;
delete network.lookup[key];
network.lookup[keyrev] = ldata;
delete network.revLinkToKey[keyrev];
}
} else {
// remove fromTarget
ldata.fromTarget = null;
delete network.revLinkToKey[keyrev];
}
if (ldata.fromSource) {
restyleLinkElement(ldata);
} else {
removeLinkElement(ldata);
}
}
}
}
}
return result;
}
function findAttachedHosts(devId) {
var hosts = [];
network.nodes.forEach(function (d) {
if (d.class === 'host' && d.cp.device === devId) {
hosts.push(d);
}
});
return hosts;
}
function findAttachedLinks(devId) {
var links = [];
network.links.forEach(function (d) {
if (d.source.id === devId || d.target.id === devId) {
links.push(d);
}
});
return links;
}
function removeLinkElement(d) {
var idx = fs.find(d.key, network.links, 'key'),
removed;
if (idx >=0) {
// remove from links array
removed = network.links.splice(idx, 1);
// remove from lookup cache
delete lu[removed[0].key];
updateLinks();
fResume();
}
}
function removeHostElement(d, upd) {
// first, remove associated hostLink...
removeLinkElement(d.linkData);
// remove hostLink bindings
delete lu[d.ingress];
delete lu[d.egress];
// remove from lookup cache
delete lu[d.id];
// remove from nodes array
var idx = fs.find(d.id, network.nodes);
network.nodes.splice(idx, 1);
// remove from SVG
// NOTE: upd is false if we were called from removeDeviceElement()
if (upd) {
updateNodes();
fResume();
}
}
function removeDeviceElement(d) {
var id = d.id;
// first, remove associated hosts and links..
findAttachedHosts(id).forEach(removeHostElement);
findAttachedLinks(id).forEach(removeLinkElement);
// remove from lookup cache
delete lu[id];
// remove from nodes array
var idx = fs.find(id, network.nodes);
network.nodes.splice(idx, 1);
if (!network.nodes.length) {
xlink.showNoDevs(true);
}
// remove from SVG
updateNodes();
fResume();
}
function sendUpdateMeta(d, store) {
var metaUi = {},
ll;
// TODO: fix this code to send event to server...
//if (store) {
// ll = geoMapProj.invert([d.x, d.y]);
// metaUi = {
// x: d.x,
// y: d.y,
// lng: ll[0],
// lat: ll[1]
// };
//}
//d.metaUi = metaUi;
//sendMessage('updateMeta', {
// id: d.id,
// 'class': d.class,
// memento: metaUi
//});
if (store) {
ll = lngLatFromCoord([d.x, d.y]);
metaUi = {
x: d.x,
y: d.y,
lng: ll[0],
lat: ll[1]
};
}
d.metaUi = metaUi;
uplink.sendEvent('updateMeta', {
id: d.id,
'class': d.class,
memento: metaUi
});
}
......@@ -178,9 +550,13 @@
// === Devices and hosts - helper functions
function coordFromLngLat(loc) {
// Our hope is that the projection is installed before we start
// handling incoming nodes. But if not, we'll just return the origin.
return projection ? projection([loc.lng, loc.lat]) : [0, 0];
var p = uplink.projection();
return p ? p([loc.lng, loc.lat]) : [0, 0];
}
function lngLatFromCoord(coord) {
var p = uplink.projection();
return p ? p.invert([coord.x, coord.y]) : [0, 0];
}
function positionNode(node, forUpdate) {
......@@ -230,8 +606,8 @@
function rand() {
return {
x: randDim(network.view.width()),
y: randDim(network.view.height())
x: randDim(width),
y: randDim(height)
};
}
......@@ -246,7 +622,7 @@
}
function getDevice(cp) {
var d = network.lookup[cp.device];
var d = lu[cp.device];
return d || rand();
}
......@@ -267,9 +643,83 @@
return node;
}
function createHostNode(host) {
var node = host;
// Augment as needed...
node.class = 'host';
if (!node.type) {
node.type = 'endstation';
}
node.svgClass = 'node host ' + node.type;
positionNode(node);
return node;
}
function createHostLink(host) {
var src = host.id,
dst = host.cp.device,
id = host.ingress,
lnk = linkEndPoints(src, dst);
if (!lnk) {
return null;
}
// Synthesize link ...
angular.extend(lnk, {
key: id,
class: 'link',
type: function () { return 'hostLink'; },
online: function () {
// hostlink target is edge switch
return lnk.target.online;
},
linkWidth: function () { return 1; }
});
return lnk;
}
function linkEndPoints(srcId, dstId) {
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:\n' + sMiss + dMiss);
//logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
return null;
}
return {
source: srcNode,
target: dstNode,
x1: srcNode.x,
y1: srcNode.y,
x2: dstNode.x,
y2: dstNode.y
};
}
function missMsg(what, id) {
return '\n[' + what + '] "' + id + '" missing ';
}
// ==========================
// === Devices and hosts - D3 rendering
function nodeMouseOver(m) {
// TODO
$log.debug("TODO nodeMouseOver()...", m);
}
function nodeMouseOut(m) {
// TODO
$log.debug("TODO nodeMouseOut()...", m);
}
// Returns the newly computed bounding box of the rectangle
function adjustRectToFitText(n) {
var text = n.select('text'),
......@@ -323,7 +773,7 @@
var label = trimLabel(deviceLabel(d)),
noLabel = !label,
node = d.el,
dim = is.iconConfig().device.dim,
dim = icfg.device.dim,
devCfg = deviceIconConfig,
box, dx, dy;
......@@ -357,16 +807,6 @@
d.el.select('text').text(label);
}
function nodeMouseOver(m) {
// TODO
$log.debug("TODO nodeMouseOver()...", m);
}
function nodeMouseOut(m) {
// TODO
$log.debug("TODO nodeMouseOut()...", m);
}
function updateDeviceColors(d) {
if (d) {
setDeviceColor(d);
......@@ -445,13 +885,14 @@
return sus.cat7().getColor(id, !online, ts.theme());
}
//============
// ==========================
function updateNodes() {
// select all the nodes in the layout:
node = nodeG.selectAll('.node')
.data(network.nodes, function (d) { return d.id; });
// operate on existing nodes...
// operate on existing nodes:
node.filter('.device').each(deviceExisting);
node.filter('.host').each(hostExisting);
......@@ -470,7 +911,7 @@
.transition()
.attr('opacity', 1);
// augment nodes...
// augment entering nodes:
entering.filter('.device').each(deviceEnter);
entering.filter('.host').each(hostEnter);
......@@ -486,7 +927,7 @@
.style('opacity', 0)
.remove();
// node specific....
// exiting node specifics:
exiting.filter('.host').each(hostExit);
exiting.filter('.device').each(deviceExit);
......@@ -539,25 +980,20 @@
}
function hostEnter(d) {
var node = d3.select(this);
//cfg = config.icons.host,
//r = cfg.radius[d.type] || cfg.defaultRadius,
//textDy = r + 10,
//TODO: iid = iconGlyphUrl(d),
// _dummy;
var node = d3.select(this),
gid = d.type || 'unknown',
rad = icfg.host.radius,
r = d.type ? rad.withGlyph : rad.noGlyph,
textDy = r + 10;
d.el = node;
sus.makeVisible(node, showHosts);
//TODO: showHostVis(node);
is.addHostIcon(node, r, gid);
node.append('circle').attr('r', r);
//if (iid) {
//TODO: addHostIcon(node, r, iid);
//}
node.append('text')
.text(hostLabel)
//.attr('dy', textDy)
.attr('dy', textDy)
.attr('text-anchor', 'middle');
}
......@@ -598,6 +1034,160 @@
.style('opacity', 0.5);
}
// ==========================
function updateLinks() {
var th = ts.theme();
link = linkG.selectAll('.link')
.data(network.links, function (d) { return d.key; });
// operate on existing links:
//link.each(linkExisting);
// operate on entering links:
var entering = link.enter()
.append('line')
.attr({
x1: function (d) { return d.x1; },
y1: function (d) { return d.y1; },
x2: function (d) { return d.x2; },
y2: function (d) { return d.y2; },
stroke: linkConfig[th].inColor,
'stroke-width': linkConfig.inWidth
});
// augment links
entering.each(linkEntering);
// operate on both existing and new links:
//link.each(...)
// apply or remove labels
var labelData = getLabelData();
applyLinkLabels(labelData);
// operate on exiting links:
link.exit()
.attr('stroke-dasharray', '3 3')
.style('opacity', 0.5)
.transition()
.duration(1500)
.attr({
'stroke-dasharray': '3 12',
stroke: linkConfig[th].outColor,
'stroke-width': linkConfig.outWidth
})
.style('opacity', 0.0)
.remove();
// NOTE: invoke a single tick to force the labels to position
// onto their links.
tick();
// FIXME: this is a bug when in oblique view
// It causes the nodes to jump into "overhead" view positions, even
// though the oblique planes are still showing...
}
// ==========================
// updateLinks - subfunctions
function getLabelData() {
// create the backing data for showing labels..
var data = [];
link.each(function (d) {
if (d.label) {
data.push({
id: 'lab-' + d.key,
key: d.key,
label: d.label,
ldata: d
});
}
});
return data;
}
//function linkExisting(d) { }
function linkEntering(d) {
var link = d3.select(this);
d.el = link;
restyleLinkElement(d);
if (d.type() === 'hostLink') {
sus.makeVisible(link, showHosts);
}
}
//function linkExiting(d) { }
var linkLabelOffset = '0.3em';
function applyLinkLabels(data) {
var entering;
linkLabel = linkLabelG.selectAll('.linkLabel')
.data(data, function (d) { return d.id; });
// for elements already existing, we need to update the text
// and adjust the rectangle size to fit
linkLabel.each(function (d) {
var el = d3.select(this),
rect = el.select('rect'),
text = el.select('text');
text.text(d.label);
rect.attr(rectAroundText(el));
});
entering = linkLabel.enter().append('g')
.classed('linkLabel', true)
.attr('id', function (d) { return d.id; });
entering.each(function (d) {
var el = d3.select(this),
rect,
text,
parms = {
x1: d.ldata.x1,
y1: d.ldata.y1,
x2: d.ldata.x2,
y2: d.ldata.y2
};
d.el = el;
rect = el.append('rect');
text = el.append('text').text(d.label);
rect.attr(rectAroundText(el));
text.attr('dy', linkLabelOffset);
el.attr('transform', transformLabel(parms));
});
// Remove any labels that are no longer required.
linkLabel.exit().remove();
}
function rectAroundText(el) {
var text = el.select('text'),
box = text.node().getBBox();
// translate the bbox so that it is centered on [x,y]
box.x = -box.width / 2;
box.y = -box.height / 2;
// add padding
box.x -= 1;
box.width += 2;
return box;
}
function transformLabel(p) {
var dx = p.x2 - p.x1,
dy = p.y2 - p.y1,
xMid = dx/2 + p.x1,
yMid = dy/2 + p.y1;
return sus.translate(xMid, yMid);
}
// ==========================
// force layout tick function
......@@ -620,34 +1210,31 @@
angular.module('ovTopo')
.factory('TopoForceService',
['$log', 'SvgUtilService', 'IconService', 'ThemeService',
['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
'TopoInstService',
function (_$log_, _sus_, _is_, _ts_, _tis_) {
function (_$log_, _fs_, _sus_, _is_, _ts_, _tis_) {
$log = _$log_;
fs = _fs_;
sus = _sus_;
is = _is_;
ts = _ts_;
tis = _tis_;
icfg = is.iconConfig();
// forceG is the SVG group to display the force layout in
// xlink is the cross-link api from the main topo source file
// w, h are the initial dimensions of the SVG
// opts are, well, optional :)
function initForce(forceG, _xlink_, w, h, opts) {
function initForce(forceG, _uplink_, w, h, opts) {
$log.debug('initForce().. WxH = ' + w + 'x' + h);
xlink = _xlink_;
uplink = _uplink_;
width = w;
height = h;
settings = angular.extend({}, defaultSettings, opts);
// when the projection promise is resolved, cache the projection
xlink.projectionPromise.then(
function (proj) {
projection = proj;
$log.debug('** We installed the projection: ', proj);
}
);
linkG = forceG.append('g').attr('id', 'topo-links');
linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
nodeG = forceG.append('g').attr('id', 'topo-nodes');
......@@ -672,7 +1259,9 @@
}
function resize(dim) {
force.size([dim.width, dim.height]);
width = dim.width;
height = dim.height;
force.size([width, height]);
// Review -- do we need to nudge the layout ?
}
......@@ -683,7 +1272,14 @@
updateDeviceColors: updateDeviceColors,
addDevice: addDevice,
updateDevice: updateDevice
updateDevice: updateDevice,
removeDevice: removeDevice,
addHost: addHost,
updateHost: updateHost,
removeHost: removeHost,
addLink: addLink,
updateLink: updateLink,
removeLink: removeLink
};
}]);
}());
......
......@@ -323,9 +323,11 @@
return {
initInst: initInst,
destroyInst: destroyInst,
addInstance: addInstance,
updateInstance: updateInstance,
removeInstance: removeInstance,
isVisible: function () { return oiBox.isVisible(); },
show: function () { oiBox.show(); },
hide: function () { oiBox.hide(); }
......
......@@ -34,7 +34,7 @@ describe('factory: view/topo/topoEvent.js', function() {
it('should define api functions', function () {
expect(fs.areFunctions(tes, [
'bindDispatcher', 'openSock', 'closeSock'
'openSock', 'closeSock', 'sendEvent'
])).toBeTruthy();
});
......
......@@ -35,7 +35,9 @@ describe('factory: view/topo/topoForce.js', function() {
it('should define api functions', function () {
expect(fs.areFunctions(tfs, [
'initForce', 'resize', 'updateDeviceColors',
'addDevice', 'updateDevice'
'addDevice', 'updateDevice', 'removeDevice',
'addHost', 'updateHost', 'removeHost',
'addLink', 'updateLink', 'removeLink'
])).toBeTruthy();
});
......
......@@ -12,6 +12,10 @@
"unknown",
"0E:2A:69:30:13:86"
],
"metaUi": {
"x": 800,
"y": 180
},
"props": {}
}
}
......
......@@ -12,6 +12,10 @@
"unknown",
"A6:96:E5:03:52:5F"
],
"metaUi": {
"x": 520,
"y": 250
},
"props": {}
}
}
......