blob: dacbda91e858e3650ce9190ec47aeb67effaa11e [file] [log] [blame]
/*
* Copyright 2015-present Open Networking Foundation
*
* 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.
*/
(function () {
'use strict';
// injected refs
var $log, $timeout, fs, sus, ts, flash, wss, tov,
tis, tms, td3, tss, tts, tos, fltr, tls, uplink, svg, tpis;
// function to be replaced by the localization bundle function
var topoLion = function (x) {
return '#tfs#' + x + '#';
};
// 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
devIconDim = 36, // node target dimension
devIconDimMin = 20, // node minimum dimension when zoomed out
devIconDimMax = 70, // node maximum dimension when zoomed in
portLabelDim = 30;
// SVG elements;
var linkG, linkLabelG, numLinkLblsG, portLabelG, nodeG;
// D3 selections;
var link, linkLabel, node;
// 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,
},
};
var hostScaleFactor = {icon: 1.0, text: 1.0};
// ==========================
// === EVENT HANDLERS
function mergeNodeData(o, n) {
angular.extend(o, n);
if (!n.location) {
delete o.location;
}
}
function addDevice(data) {
var id = data.id,
d;
uplink.showNoDevs(false);
// although this is an add device event, if we already have the
// device, treat it as an update instead..
if (lu[id]) {
updateDevice(data);
return;
}
d = tms.createDeviceNode(data);
network.nodes.push(d);
lu[id] = d;
updateNodes();
fStart();
}
function updateDevice(data) {
var id = data.id,
d = lu[id],
wasOnline;
if (d) {
wasOnline = d.online;
mergeNodeData(d, data);
if (tms.positionNode(d, true)) {
sendUpdateMeta(d);
}
updateNodes();
tick();
if (wasOnline !== d.online) {
tms.findAttachedLinks(d.id).forEach(restyleLinkElement);
updateOfflineVisibility(d);
}
fStart();
}
}
function removeDevice(data) {
var id = data.id,
d = lu[id];
if (d) {
removeDeviceElement(d);
}
}
function addHost(data) {
var id = data.id,
d;
// 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 = tms.createHostNode(data);
network.nodes.push(d);
lu[id] = d;
updateNodes();
// need to handle possible multiple links (multi-homed host)
createHostLinks(data.allCps, d);
if (d.links.length) {
updateLinks();
}
fStart();
}
function updateHost(data) {
var id = data.id,
d = lu[id];
if (d) {
mergeNodeData(d, data);
if (tms.positionNode(d, true)) {
sendUpdateMeta(d);
}
updateNodes();
tick();
fStart();
}
}
function createHostLinks(cps, model) {
model.links = [];
cps.forEach(function (cp) {
var linkData = {
key: model.id + '/0-' + cp.device + '/' + cp.port,
dst: cp.device,
dstPort: cp.port,
};
model.links.push(linkData);
var lnk = tms.createHostLink(model.id, cp.device, cp.port);
if (lnk) {
network.links.push(lnk);
lu[linkData.key] = lnk;
}
});
}
function moveHost(data) {
var id = data.id,
d = lu[id];
if (d) {
removeAllLinkElements(d.links);
// merge new data
angular.extend(d, data);
if (tms.positionNode(d, true)) {
sendUpdateMeta(d);
}
// now create new host link(s)
createHostLinks(data.allCps, d);
updateNodes();
updateLinks();
fResume();
}
}
function removeHost(data) {
var id = data.id,
d = lu[id];
if (d) {
removeHostElement(d, true);
}
}
function addLink(data) {
var result = tms.findLink(data, 'add'),
bad = result.badLogic,
d = result.ldata;
if (bad) {
$log.debug(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 = tms.createLink(data);
if (d) {
network.links.push(d);
aggregateLink(d, data);
lu[d.key] = d;
updateLinks();
fStart();
}
}
function updateLink(data) {
var result = tms.findLink(data, 'update'),
bad = result.badLogic;
if (bad) {
$log.debug(bad + ': ' + link.id);
return;
}
result.updateWith(data);
}
function removeLink(data) {
var result = tms.findLink(data, 'remove');
if (!result.badLogic) {
result.removeRawLink();
}
}
function topoStartDone(data) {
// called when the initial barrage of data has been sent from server
uplink.topoStartDone();
}
// ========================
function nodeById(id) {
return lu[id];
}
function makeNodeKey(node1, node2) {
return node1 + '-' + node2;
}
function findNodePair(key, keyRev) {
if (network.linksByDevice[key]) {
return key;
} else if (network.linksByDevice[keyRev]) {
return keyRev;
} else {
return false;
}
}
function aggregateLink(ldata, link) {
var key = makeNodeKey(link.src, link.dst),
keyRev = makeNodeKey(link.dst, link.src),
found = findNodePair(key, keyRev);
if (found) {
network.linksByDevice[found].push(ldata);
ldata.devicePair = found;
} else {
network.linksByDevice[key] = [ldata];
ldata.devicePair = key;
}
}
function addLinkUpdate(ldata, link) {
// add link event, but we already have the reverse link installed
ldata.fromTarget = link;
rlk[link.id] = ldata.key;
// possible solution to el being undefined in restyleLinkElement:
// _updateLinks();
restyleLinkElement(ldata);
}
var widthRatio = 1.4,
linkScale = d3.scale.linear()
.domain([1, 12])
.range([widthRatio, 12 * widthRatio])
.clamp(true),
allLinkTypes = 'direct indirect optical tunnel',
allLinkSubTypes = 'inactive not-permitted';
function restyleLinkElement(ldata, 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 = ldata.el,
type = ldata.type(),
lw = ldata.linkWidth(),
online = ldata.online(),
modeCls = ldata.expected() ? 'inactive' : 'not-permitted',
delay = immediate ? 0 : 1000;
// 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);
}
}
function removeAllLinkElements(links) {
links.forEach(function (lnk) {
removeLinkElement(lnk);
});
}
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(s)...
removeAllLinkElements(d.links);
// 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,
idx;
// first, remove associated hosts and links..
tms.findAttachedHosts(id).forEach(removeHostElement);
tms.findAttachedLinks(id).forEach(removeLinkElement);
// remove from lookup cache
delete lu[id];
// remove from nodes array
idx = fs.find(id, network.nodes);
if (idx > -1) {
network.nodes.splice(idx, 1);
}
if (!network.nodes.length) {
uplink.showNoDevs(true);
}
// remove from SVG
updateNodes();
fResume();
}
function updateHostVisibility() {
sus.visible(nodeG.selectAll('.host'), showHosts);
sus.visible(linkG.selectAll('.hostLink'), showHosts);
sus.visible(linkLabelG.selectAll('.hostLinkLabel'), showHosts);
}
function updateOfflineVisibility(dev) {
function updDev(d, show) {
var b;
sus.visible(d.el, show);
tms.findAttachedLinks(d.id).forEach(function (link) {
b = show && ((link.type() !== 'hostLink') || showHosts);
sus.visible(link.el, b);
});
tms.findAttachedHosts(d.id).forEach(function (host) {
b = show && showHosts;
sus.visible(host.el, b);
});
}
if (dev) {
// updating a specific device that just toggled off/on-line
updDev(dev, dev.online || showOffline);
} else {
// updating all offline devices
tms.findDevices(true).forEach(function (d) {
updDev(d, showOffline);
});
}
}
function sendUpdateMeta(d, clearPos) {
var metaUi = {},
ll;
// if we are not clearing the position data (unpinning),
// attach the x, y, (and equivalent longitude, latitude)...
if (!clearPos) {
ll = tms.lngLatFromCoord([d.x, d.y]);
metaUi = {
x: d.x,
y: d.y,
equivLoc: {
lng: ll[0],
lat: ll[1],
},
};
}
d.metaUi = metaUi;
wss.sendEvent('updateMeta', {
id: d.id,
class: d.class,
memento: metaUi,
});
}
function mkSvgClass(d) {
return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
}
function vis(b) {
return topoLion(b ? 'visible' : 'hidden');
}
function toggleHosts(x) {
var kev = (x === 'keyev'),
on = kev ? !showHosts : !!x;
showHosts = on;
updateHostVisibility();
flash.flash(topoLion('hosts') + ' ' + vis(on));
return on;
}
function toggleOffline(x) {
var kev = (x === 'keyev'),
on = kev ? !showOffline : !!x;
showOffline = on;
updateOfflineVisibility();
flash.flash(topoLion('fl_offline_devices') + ' ' + vis(on));
return on;
}
function cycleDeviceLabels() {
flash.flash(td3.incDevLabIndex());
tms.findDevices().forEach(function (d) {
td3.updateDeviceLabel(d);
});
}
function cycleHostLabels() {
flash.flash(td3.incHostLabIndex());
tms.findHosts().forEach(function (d) {
td3.updateHostLabel(d);
});
}
function cycleLinkLabels() {
td3.toggleLinkLabels();
}
function unpin() {
var hov = tss.hovered();
if (hov) {
sendUpdateMeta(hov, true);
hov.fixed = false;
hov.el.classed('fixed', false);
fResume();
}
}
function showMastership(masterId) {
if (!masterId) {
restoreLayerState();
} else {
showMastershipFor(masterId);
}
}
function restoreLayerState() {
// NOTE: this level of indirection required, for when we have
// the layer filter functionality re-implemented
suppressLayers(false);
}
function showMastershipFor(id) {
suppressLayers(true);
node.each(function (n) {
if (n.master === id) {
n.el.classed('suppressedmax', false);
}
});
}
function supAmt(less) {
return less ? 'suppressed' : 'suppressedmax';
}
function suppressLayers(b, less) {
var cls = supAmt(less);
node.classed(cls, b);
link.classed(cls, b);
}
function unsuppressNode(id, less) {
var cls = supAmt(less);
node.each(function (n) {
if (n.id === id) {
n.el.classed(cls, false);
}
});
}
function unsuppressLink(key, less) {
var cls = supAmt(less);
link.each(function (n) {
if (n.key === key) {
n.el.classed(cls, false);
}
});
}
function showBadLinks() {
var badLinks = tms.findBadLinks();
flash.flash(topoLion('fl_bad_links') + ': ' + badLinks.length);
$log.debug('Bad Link List (' + badLinks.length + '):');
badLinks.forEach(function (d) {
$log.debug('bad link: (' + d.bad + ') ' + d.key, d);
if (d.el) {
d.el.attr('stroke-width', linkScale(2.8))
.attr('stroke', 'red');
}
});
// back to normal after 2 seconds...
$timeout(updateLinks, 2000);
}
function deviceScale(scaleFactor) {
var scale = uplink.zoomer().scale() * scaleFactor,
dim = devIconDim,
multiplier = 1;
if (dim * scale < devIconDimMin) {
multiplier = devIconDimMin / (dim * scale);
} else if (dim * scale > devIconDimMax) {
multiplier = devIconDimMax / (dim * scale);
}
return multiplier;
}
function linkWidthScale() {
var scale = uplink.zoomer().scale();
return linkScale(widthRatio) / scale;
}
function portLabelScale() {
var scale = uplink.zoomer().scale();
return portLabelDim / (portLabelDim * scale);
}
function adjustNodeScale() {
// Scale the network nodes
_.each(network.nodes, function (node) {
if (node.class === 'host') {
node.el.selectAll('g').style('transform', 'scale(' + deviceScale(hostScaleFactor.icon) + ')');
node.el.selectAll('text').style('transform', 'scale(' + deviceScale(hostScaleFactor.text) + ')');
return;
}
node.el.selectAll('*').style('transform', 'scale(' + deviceScale(1.0) + ')');
});
// Scale the network links
_.each(network.links, function (link) {
link.el.style('stroke-width', linkWidthScale() + 'px');
});
d3.select('#topo-portLabels')
.selectAll('.portLabel')
.selectAll('*')
.style('transform', 'scale(' + portLabelScale() + ')');
}
function toggleScale(scale) {
if (scale < 0.5) {
return 1.0
}
return scale - 0.2;
}
function toggleHostTextSize() {
hostScaleFactor.text = toggleScale(hostScaleFactor.text);
adjustNodeScale();
}
function toggleHostIconSize() {
hostScaleFactor.icon = toggleScale(hostScaleFactor.icon);
adjustNodeScale();
}
function resetAllLocations() {
tms.resetAllLocations();
updateNodes();
tick(); // force nodes to be redrawn in their new locations
flash.flash(topoLion('fl_reset_node_locations'));
}
// ==========================================
function updateNodes() {
if (fNodesTimer) {
$timeout.cancel(fNodesTimer);
}
fNodesTimer = $timeout(_updateNodes, 150);
}
// IMPLEMENTATION NOTE: _updateNodes() should NOT stop, start, or resume
// the force layout; that needs to be determined and implemented elsewhere
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:
node.filter('.device').each(td3.deviceExisting);
node.filter('.host').each(td3.hostExisting);
// operate on entering nodes:
var entering = node.enter()
.append('g')
.attr({
id: function (d) { return sus.safeId(d.id); },
class: mkSvgClass,
transform: function (d) {
// Need to guard against NaN here ??
return sus.translate(d.x, d.y);
},
opacity: 0,
})
.call(drag)
.on('mouseover', tss.nodeMouseOver)
.on('mouseout', tss.nodeMouseOut)
.transition()
.attr('opacity', 1);
// augment entering nodes:
entering.filter('.device').each(td3.deviceEnter);
entering.filter('.host').each(td3.hostEnter);
// operate on both existing and new nodes:
td3.updateDeviceColors();
// operate on exiting nodes:
// Note that the node is removed after 2 seconds.
// Sub element animations should be shorter than 2 seconds.
var exiting = node.exit()
.transition()
.duration(2000)
.style('opacity', 0)
.remove();
// exiting node specifics:
exiting.filter('.host').each(td3.hostExit);
exiting.filter('.device').each(td3.deviceExit);
tick();
}
// ==========================
function getDefaultPos(link) {
return {
x1: link.source.x,
y1: link.source.y,
x2: link.target.x,
y2: link.target.y,
};
}
// returns amount of adjustment along the normal for given link
function amt(numLinks, linkIdx) {
var gap = 6;
return (linkIdx - ((numLinks - 1) / 2)) * gap;
}
function calcMovement(d, amt, flipped) {
var pos = getDefaultPos(d),
mult = flipped ? -amt : amt,
dx = pos.x2 - pos.x1,
dy = pos.y2 - pos.y1,
length = Math.sqrt((dx * dx) + (dy * dy));
return {
x1: pos.x1 + (mult * dy / length),
y1: pos.y1 + (mult * -dx / length),
x2: pos.x2 + (mult * dy / length),
y2: pos.y2 + (mult * -dx / length),
};
}
function calcPosition() {
var lines = this,
linkSrcId;
linkNums = [];
lines.each(function (d) {
if (d.type() === 'hostLink') {
d.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;
}
angular.forEach(network.linksByDevice, function (linkArr, key) {
var numLinks = linkArr.length,
link;
if (numLinks === 1) {
link = linkArr[0];
link.position = getDefaultPos(link);
link.position.multiLink = false;
} else if (numLinks >= 5) {
// this code is inefficient, in the future the way links
// are modeled will be changed
angular.forEach(linkArr, function (link) {
link.position = getDefaultPos(link);
link.position.multiLink = true;
});
linkNums.push({
id: key,
num: numLinks,
linkCoords: linkArr[0].position,
});
} else {
linkSrcId = null;
angular.forEach(linkArr, function (link, index) {
var offsetAmt = amt(numLinks, index),
needToFlip = normalizeLinkSrc(link);
link.position = calcMovement(link, offsetAmt, needToFlip);
link.position.multiLink = false;
});
}
});
}
function updateLinks() {
if (fLinksTimer) {
$timeout.cancel(fLinksTimer);
}
fLinksTimer = $timeout(_updateLinks, 150);
}
// IMPLEMENTATION NOTE: _updateLinks() should NOT stop, start, or resume
// the force layout; that needs to be determined and implemented elsewhere
function _updateLinks() {
var th = ts.theme();
link = linkG.selectAll('.link')
.data(network.links, function (d) { return d.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.position.x1; },
y1: function (d) { return d.position.y1; },
x2: function (d) { return d.position.x2; },
y2: function (d) { return d.position.y2; },
stroke: linkConfig[th].inColor,
'stroke-width': linkConfig.inWidth,
});
// augment links
entering.each(td3.linkEntering);
// operate on both existing and new links:
// link.each(...)
// add labels for how many links are in a thick line
td3.applyNumLinkLabels(linkNums, numLinkLblsG);
// apply or remove labels
td3.applyLinkLabels();
// operate on exiting links:
link.exit()
.attr('stroke-dasharray', '3 3')
.attr('stroke', linkConfig[th].outColor)
.style('opacity', 0.5)
.transition()
.duration(1500)
.attr({
'stroke-dasharray': '3 12',
'stroke-width': linkConfig.outWidth,
})
.style('opacity', 0.0)
.remove();
}
// ==========================
// force layout tick function
function fResume() {
if (!tos.isOblique()) {
force.resume();
}
}
function fStart() {
if (!tos.isOblique()) {
if (fTimer) {
$timeout.cancel(fTimer);
}
fTimer = $timeout(function () {
$log.debug('Starting force-layout');
force.start();
}, 200);
}
}
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.position.x1; },
y1: function (d) { return d.position.y1; },
x2: function (d) { return d.position.x2; },
y2: function (d) { return d.position.y2; },
},
linkLabelAttr: {
transform: function (d) {
var lnk = tms.findLinkById(d.key);
if (lnk) {
return td3.transformLabel(lnk.position, d.key);
}
},
},
};
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);
td3.applyNumLinkLabels(linkNums, numLinkLblsG);
}
if (linkLabel && linkLabel.size()) {
linkLabel.attr(tickStuff.linkLabelAttr);
}
}
// ==========================
// === MOUSE GESTURE HANDLERS
function zoomingOrPanning(ev) {
return ev.metaKey || ev.altKey;
}
function atDragEnd(d) {
// once we've finished moving, pin the node in position
d.fixed = true;
d3.select(this).classed('fixed', true);
sendUpdateMeta(d);
tss.clickConsumed(true);
}
// predicate that indicates when dragging is active
function dragEnabled() {
var ev = d3.event.sourceEvent;
// nodeLock means we aren't allowing nodes to be dragged...
return !nodeLock && !zoomingOrPanning(ev);
}
// predicate that indicates when clicking is active
function clickEnabled() {
return true;
}
// =============================================
// function entry points for overlay module
// TODO: find an automatic way of tracking via the "showHighlights" events
var allTrafficClasses = 'primary secondary optical animated ' +
'port-traffic-green port-traffic-yellow port-traffic-orange ' +
'port-traffic-red';
function clearLinkTrafficStyle() {
link.style('stroke-width', null)
.classed(allTrafficClasses, false);
}
function removeLinkLabels() {
network.links.forEach(function (d) {
d.label = '';
});
}
function clearNodeDeco() {
node.selectAll('g.badge').remove();
}
function removeNodeBadges() {
network.nodes.forEach(function (d) {
d.badge = null;
});
}
function updateLinkLabelModel() {
// 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,
});
}
});
linkLabel = linkLabelG.selectAll('.linkLabel')
.data(data, function (d) { return d.id; });
}
// ==========================
// Module definition
function mkModelApi(uplink) {
return {
projection: uplink.projection,
network: network,
restyleLinkElement: restyleLinkElement,
removeLinkElement: removeLinkElement,
};
}
function mkD3Api() {
return {
node: function () { return node; },
link: function () { return link; },
linkLabel: function () { return linkLabel; },
instVisible: function () { return tis.isVisible(); },
posNode: tms.positionNode,
showHosts: function () { return showHosts; },
restyleLinkElement: restyleLinkElement,
updateLinkLabelModel: updateLinkLabelModel,
linkConfig: function () { return linkConfig; },
deviceScale: deviceScale,
linkWidthScale: linkWidthScale,
};
}
function mkSelectApi() {
return {
node: function () { return node; },
zoomingOrPanning: zoomingOrPanning,
updateDeviceColors: td3.updateDeviceColors,
deselectAllLinks: tls.deselectAllLinks,
};
}
function mkTrafficApi() {
return {
hovered: tss.hovered,
somethingSelected: tss.somethingSelected,
selectOrder: tss.selectOrder,
};
}
function mkOverlayApi() {
return {
clearNodeDeco: clearNodeDeco,
removeNodeBadges: removeNodeBadges,
clearLinkTrafficStyle: clearLinkTrafficStyle,
removeLinkLabels: removeLinkLabels,
findLinkById: tms.findLinkById,
findNodeById: nodeById,
updateLinks: updateLinks,
updateNodes: updateNodes,
supLayers: suppressLayers,
unsupNode: unsuppressNode,
unsupLink: unsuppressLink,
};
}
function mkObliqueApi(uplink, fltr) {
return {
force: function () { return force; },
zoomLayer: uplink.zoomLayer,
nodeGBBox: function () { return nodeG.node().getBBox(); },
node: function () { return node; },
link: function () { return link; },
linkLabel: function () { return linkLabel; },
nodes: function () { return network.nodes; },
tickStuff: tickStuff,
nodeLock: function (b) {
var old = nodeLock;
nodeLock = b;
return old;
},
opacifyMap: uplink.opacifyMap,
inLayer: fltr.inLayer,
calcLinkPos: calcPosition,
applyNumLinkLabels: function () {
td3.applyNumLinkLabels(linkNums, numLinkLblsG);
},
};
}
function mkFilterApi() {
return {
node: function () { return node; },
link: function () { return link; },
};
}
function mkLinkApi(svg, uplink) {
return {
svg: svg,
zoomer: uplink.zoomer(),
network: network,
portLabelG: function () { return portLabelG; },
showHosts: function () { return showHosts; },
};
}
function updateLinksAndNodes() {
updateLinks();
updateNodes();
}
// invoked after the localization bundle has been received from the server
function setLionBundle(bundle) {
topoLion = bundle;
td3.setLionBundle(bundle);
fltr.setLionBundle(bundle);
tls.setLionBundle(bundle);
tos.setLionBundle(bundle);
tov.setLionBundle(bundle);
tss.setLionBundle(bundle);
}
angular.module('ovTopo')
.factory('TopoForceService',
['$log', '$timeout', 'FnService', 'SvgUtilService',
'ThemeService', 'FlashService', 'WebSocketService',
'TopoOverlayService', 'TopoInstService', 'TopoModelService',
'TopoD3Service', 'TopoSelectService', 'TopoTrafficService',
'TopoObliqueService', 'TopoFilterService', 'TopoLinkService',
'TopoProtectedIntentsService',
function (_$log_, _$timeout_, _fs_, _sus_, _ts_, _flash_, _wss_, _tov_,
_tis_, _tms_, _td3_, _tss_, _tts_, _tos_, _fltr_, _tls_, _tpis_) {
$log = _$log_;
$timeout = _$timeout_;
fs = _fs_;
sus = _sus_;
ts = _ts_;
flash = _flash_;
wss = _wss_;
tov = _tov_;
tis = _tis_;
tms = _tms_;
td3 = _td3_;
tss = _tss_;
tts = _tts_;
tos = _tos_;
fltr = _fltr_;
tls = _tls_;
tpis = _tpis_;
ts.addListener(updateLinksAndNodes);
// forceG is the SVG group to display the force layout in
// uplink is the api from the main topo source file
// dim is the initial dimensions of the SVG as [w,h]
// opts are, well, optional :)
function initForce(_svg_, forceG, _uplink_, _dim_, opts) {
uplink = _uplink_;
dim = _dim_;
svg = _svg_;
lu = network.lookup;
rlk = network.revLinkToKey;
$log.debug('initForce().. dim = ' + dim);
tov.setApi(mkOverlayApi(), tss);
tms.initModel(mkModelApi(uplink), dim);
td3.initD3(mkD3Api(), uplink.zoomer());
tss.initSelect(mkSelectApi());
tts.initTraffic(mkTrafficApi());
tpis.initProtectedIntents(mkTrafficApi());
tos.initOblique(mkObliqueApi(uplink, fltr));
fltr.initFilter(mkFilterApi());
tls.initLink(mkLinkApi(svg, uplink), td3);
settings = angular.extend({}, defaultSettings, opts);
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');
force = d3.layout.force()
.size(dim)
.nodes(network.nodes)
.links(network.links)
.gravity(settings.gravity)
.friction(settings.friction)
.charge(settings.charge._def_)
.linkDistance(settings.linkDistance._def_)
.linkStrength(settings.linkStrength._def_)
.on('tick', tick);
drag = sus.createDragBehavior(force,
tss.selectObject, atDragEnd, dragEnabled, clickEnabled);
}
function newDim(_dim_) {
dim = _dim_;
force.size(dim);
tms.newDim(dim);
}
function destroyForce() {
force.stop();
tls.destroyLink();
tos.destroyOblique();
tts.destroyTraffic();
tpis.destroyProtectedIntents();
tss.destroySelect();
td3.destroyD3();
tms.destroyModel();
// note: no need to destroy overlay service
ts.removeListener(updateLinksAndNodes);
// clean up the DOM
svg.selectAll('g').remove();
svg.selectAll('defs').remove();
// clean up internal state
network.nodes = [];
network.links = [];
network.linksByDevice = {};
network.lookup = {};
network.revLinkToKey = {};
linkNums = [];
linkG = linkLabelG = numLinkLblsG = nodeG = portLabelG = null;
link = linkLabel = node = null;
force = drag = null;
// clean up $timeout promises
if (fTimer) {
$timeout.cancel(fTimer);
}
if (fNodesTimer) {
$timeout.cancel(fNodesTimer);
}
if (fLinksTimer) {
$timeout.cancel(fLinksTimer);
}
}
return {
initForce: initForce,
newDim: newDim,
destroyForce: destroyForce,
updateDeviceColors: td3.updateDeviceColors,
toggleHosts: toggleHosts,
togglePorts: tls.togglePorts,
toggleOffline: toggleOffline,
cycleDeviceLabels: cycleDeviceLabels,
cycleHostLabels: cycleHostLabels,
cycleLinkLabels: cycleLinkLabels,
unpin: unpin,
showMastership: showMastership,
showBadLinks: showBadLinks,
adjustNodeScale: adjustNodeScale,
toggleHostTextSize: toggleHostTextSize,
toggleHostIconSize: toggleHostIconSize,
resetAllLocations: resetAllLocations,
addDevice: addDevice,
updateDevice: updateDevice,
removeDevice: removeDevice,
addHost: addHost,
updateHost: updateHost,
moveHost: moveHost,
removeHost: removeHost,
addLink: addLink,
updateLink: updateLink,
removeLink: removeLink,
topoStartDone: topoStartDone,
setLionBundle: setLionBundle,
};
}]);
}());