text: /*
   * Copyright 2015-present 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) {
              $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;
  
          // 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(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 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, (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 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 resetAllLocations() {
          tms.resetAllLocations();
          updateNodes();
          tick(); // force nodes to be redrawn in their new locations
          flash.flash('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);
      }
  
      // ==========================
  
      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,
  
                  resetAllLocations: resetAllLocations,
                  addDevice: addDevice,
                  updateDevice: updateDevice,
                  removeDevice: removeDevice,
                  addHost: addHost,
                  updateHost: updateHost,
                  moveHost: moveHost,
                  removeHost: removeHost,
                  addLink: addLink,
                  updateLink: updateLink,
                  removeLink: removeLink,
                  topoStartDone: topoStartDone
              };
          }]);
  }());
  