| /* |
| * 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 = 40, // 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, |
| }, |
| }; |
| |
| |
| // ========================== |
| // === 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 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() { |
| var scale = uplink.zoomer().scale(), |
| 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(scale) { |
| var scale = uplink.zoomer().scale(); |
| return linkScale(widthRatio) / scale; |
| } |
| |
| function portLabelScale(scale) { |
| var scale = uplink.zoomer().scale(); |
| return portLabelDim / (portLabelDim * scale); |
| } |
| |
| function setNodeScale(scale) { |
| // Scale the network nodes |
| _.each(network.nodes, function (node) { |
| if (node.class === 'host') { |
| node.el.selectAll('g').style('transform', 'scale(' + deviceScale(scale) + ')'); |
| node.el.selectAll('text').style('transform', 'scale(' + deviceScale(scale) + ')'); |
| return; |
| } |
| node.el.selectAll('*') |
| .style('transform', 'scale(' + deviceScale(scale) + ')'); |
| }); |
| |
| // Scale the network links |
| _.each(network.links, function (link) { |
| link.el.style('stroke-width', linkWidthScale(scale) + 'px'); |
| }); |
| |
| d3.select('#topo-portLabels') |
| .selectAll('.portLabel') |
| .selectAll('*') |
| .style('transform', 'scale(' + portLabelScale(scale) + ')'); |
| } |
| |
| 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, |
| unpin: unpin, |
| showMastership: showMastership, |
| showBadLinks: showBadLinks, |
| setNodeScale: setNodeScale, |
| |
| 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, |
| }; |
| }]); |
| }()); |