aboutsummaryrefslogtreecommitdiffstats
path: root/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js
diff options
context:
space:
mode:
Diffstat (limited to 'framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js')
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js1134
1 files changed, 1134 insertions, 0 deletions
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js
new file mode 100644
index 00000000..dbe8f9f5
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -0,0 +1,1134 @@
+/*
+ * Copyright 2015 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.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $timeout, fs, sus, ts, flash, wss, tov,
+ tis, tms, td3, tss, tts, tos, fltr, tls, uplink, svg;
+
+ // configuration
+ var linkConfig = {
+ light: {
+ baseColor: '#666',
+ inColor: '#66f',
+ outColor: '#f00'
+ },
+ dark: {
+ baseColor: '#aaa',
+ inColor: '#66f',
+ outColor: '#f66'
+ },
+ 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
+
+ // 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
+ }
+ };
+
+
+ // ==========================
+ // === EVENT HANDLERS
+
+ 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;
+ angular.extend(d, data);
+ if (tms.positionNode(d, true)) {
+ sendUpdateMeta(d);
+ }
+ updateNodes();
+ if (wasOnline !== d.online) {
+ tms.findAttachedLinks(d.id).forEach(restyleLinkElement);
+ updateOfflineVisibility(d);
+ }
+ }
+ }
+
+ function removeDevice(data) {
+ var id = data.id,
+ d = lu[id];
+ if (d) {
+ removeDeviceElement(d);
+ }
+ }
+
+ 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 = tms.createHostNode(data);
+ network.nodes.push(d);
+ lu[id] = d;
+ updateNodes();
+
+ lnk = tms.createHostLink(data);
+ if (lnk) {
+ 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 (tms.positionNode(d, true)) {
+ sendUpdateMeta(d);
+ }
+ updateNodes();
+ }
+ }
+
+ 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) {
+ //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 = 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) {
+ //logicError(bad + ': ' + link.id);
+ return;
+ }
+ result.updateWith(link);
+ }
+
+ function removeLink(data) {
+ var result = tms.findLink(data, 'remove');
+
+ if (!result.badLogic) {
+ result.removeRawLink();
+ }
+ }
+
+ // ========================
+
+ 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';
+
+ 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(),
+ delay = immediate ? 0 : 1000;
+
+ // FIXME: 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('inactive', !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 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,
+ 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, longitude, latitude...
+ if (!clearPos) {
+ ll = tms.lngLatFromCoord([d.x, d.y]);
+ metaUi = {x: d.x, y: d.y, 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 b ? 'visible' : 'hidden';
+ }
+
+ function toggleHosts(x) {
+ var kev = (x === 'keyev'),
+ on = kev ? !showHosts : !!x;
+
+ showHosts = on;
+ updateHostVisibility();
+ flash.flash('Hosts ' + vis(on));
+ return on;
+ }
+
+ function toggleOffline(x) {
+ var kev = (x === 'keyev'),
+ on = kev ? !showOffline : !!x;
+
+ showOffline = on;
+ updateOfflineVisibility();
+ flash.flash('Offline devices ' + vis(on));
+ return on;
+ }
+
+ function cycleDeviceLabels() {
+ flash.flash(td3.incDevLabIndex());
+ tms.findDevices().forEach(function (d) {
+ td3.updateDeviceLabel(d);
+ });
+ }
+
+ 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('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 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);
+ }
+
+ // ==========================
+
+ 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);
+ }
+ }
+ }
+ };
+
+ 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-Kbps port-traffic-Mbps port-traffic-Gbps ' +
+ 'port-traffic-Gbps-choked';
+
+ function clearLinkTrafficStyle() {
+ link.style('stroke-width', null)
+ .classed(allTrafficClasses, false);
+ }
+
+ function removeLinkLabels() {
+ network.links.forEach(function (d) {
+ d.label = '';
+ });
+ }
+
+ 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; }
+ };
+ }
+
+ function mkSelectApi() {
+ return {
+ node: function () { return node; },
+ zoomingOrPanning: zoomingOrPanning,
+ updateDeviceColors: td3.updateDeviceColors,
+ deselectLink: tls.deselectLink
+ };
+ }
+
+ function mkTrafficApi() {
+ return {
+ hovered: tss.hovered,
+ somethingSelected: tss.somethingSelected,
+ selectOrder: tss.selectOrder
+ };
+ }
+
+ function mkOverlayApi() {
+ return {
+ 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; }
+ };
+ }
+
+ angular.module('ovTopo')
+ .factory('TopoForceService',
+ ['$log', '$timeout', 'FnService', 'SvgUtilService',
+ 'ThemeService', 'FlashService', 'WebSocketService',
+ 'TopoOverlayService', 'TopoInstService', 'TopoModelService',
+ 'TopoD3Service', 'TopoSelectService', 'TopoTrafficService',
+ 'TopoObliqueService', 'TopoFilterService', 'TopoLinkService',
+
+ function (_$log_, _$timeout_, _fs_, _sus_, _ts_, _flash_, _wss_, _tov_,
+ _tis_, _tms_, _td3_, _tss_, _tts_, _tos_, _fltr_, _tls_) {
+ $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_;
+
+ var themeListener = ts.addListener(function () {
+ updateLinks();
+ updateNodes();
+ });
+
+ // 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());
+ tss.initSelect(mkSelectApi());
+ tts.initTraffic(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();
+ tss.destroySelect();
+ td3.destroyD3();
+ tms.destroyModel();
+ // note: no need to destroy overlay service
+ ts.removeListener(themeListener);
+ themeListener = null;
+
+ // 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,
+ unpin: unpin,
+ showMastership: showMastership,
+ showBadLinks: showBadLinks,
+
+ addDevice: addDevice,
+ updateDevice: updateDevice,
+ removeDevice: removeDevice,
+ addHost: addHost,
+ updateHost: updateHost,
+ removeHost: removeHost,
+ addLink: addLink,
+ updateLink: updateLink,
+ removeLink: removeLink
+ };
+ }]);
+}());