/* * 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 moveHost(data) { var id = data.id, d = lu[id], lnk; if (d) { // first remove the old host link removeLinkElement(d.linkData); // merge new data angular.extend(d, data); if (tms.positionNode(d, true)) { sendUpdateMeta(d); } // now create a new host link lnk = tms.createHostLink(data); if (lnk) { d.linkData = lnk; network.links.push(lnk); lu[d.ingress] = lnk; lu[d.egress] = lnk; } 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) { //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 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'; 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 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; } }; } 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 { 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; } }; } 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, moveHost: moveHost, removeHost: removeHost, addLink: addLink, updateLink: updateLink, removeLink: removeLink, topoStartDone: topoStartDone }; }]); }());