http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/topology/qdrTopology.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/qdrTopology.js 
b/console/stand-alone/plugin/js/topology/qdrTopology.js
index 15e73c4..9a48ba2 100644
--- a/console/stand-alone/plugin/js/topology/qdrTopology.js
+++ b/console/stand-alone/plugin/js/topology/qdrTopology.js
@@ -16,1593 +16,1091 @@ KIND, either express or implied.  See the License for 
the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 
-/* global angular d3 separateAddresses Traffic */
+/* global angular d3 */
 /**
  * @module QDR
  */
-var QDR = (function(QDR) {
-
-  /**
-   * @method TopologyController
-   *
-   * Controller that handles the QDR topology page
-   */
-
-  QDR.module.controller('QDR.TopologyController', ['$scope', '$rootScope', 
'QDRService', '$location', '$timeout', '$uibModal', '$sce',
-    function($scope, $rootScope, QDRService, $location, $timeout, $uibModal, 
$sce) {
-
-      const TOPOOPTIONSKEY = 'topoOptions';
-      const radius = 25;
-      const radiusNormal = 15;
-
-      //  - nodes is an array of router/client info. these are the circles
-      //  - links is an array of connections between the routers. these are 
the lines with arrows
-      let nodes = [];
-      let links = [];
-      let forceData = {nodes: nodes, links: links};
-      let urlPrefix = $location.absUrl();
-      urlPrefix = urlPrefix.split('#')[0];
-      QDR.log.debug('started QDR.TopologyController with urlPrefix: ' + 
urlPrefix);
-
-      $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || 
{showTraffic: false, trafficType: 'dots'};
-      if (!$scope.legendOptions.trafficType)
-        $scope.legendOptions.trafficType = 'dots';
-      $scope.legend = {status: {legendOpen: true, optionsOpen: true}};
-      $scope.legend.status.optionsOpen = $scope.legendOptions.showTraffic;
-      let traffic = new Traffic($scope, $timeout, QDRService, 
separateAddresses, 
-        radius, forceData, nextHop, $scope.legendOptions.trafficType, 
urlPrefix);
-
-      // the showTraaffic checkbox was just toggled (or initialized)
-      $scope.$watch('legend.status.optionsOpen', function () {
-        $scope.legendOptions.showTraffic = $scope.legend.status.optionsOpen;
-        localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
-        if ($scope.legend.status.optionsOpen) {
-          traffic.start();
-        } else {
-          traffic.stop();
-          traffic.remove();
-          restart();
-        }
-      });
-      $scope.$watch('legendOptions.trafficType', function () {
-        localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
-        if ($scope.legendOptions.showTraffic) {
-          restart();
-          traffic.setAnimationType($scope.legendOptions.trafficType, 
separateAddresses, radius);
-          traffic.start();
-        }
-      });
-
-      // mouse event vars
-      let selected_node = null,
-        selected_link = null,
-        mousedown_link = null,
-        mousedown_node = null,
-        mouseover_node = null,
-        mouseup_node = null,
-        initial_mouse_down_position = null;
-
-      $scope.schema = 'Not connected';
-
-      $scope.contextNode = null; // node that is associated with the current 
context menu
-      $scope.isRight = function(mode) {
-        return mode.right;
-      };
-
-      var setNodesFixed = function (name, b) {
-        nodes.some(function (n) {
-          if (n.name === name) {
-            n.fixed = b;
-            return true;
-          }
-        });
-      };
-      $scope.setFixed = function(b) {
-        if ($scope.contextNode) {
-          $scope.contextNode.fixed = b;
-          setNodesFixed($scope.contextNode.name, b);
-          savePositions();
-        }
+import { QDRLogger, QDRRedirectWhenConnected } from '../qdrGlobals.js';
+import { Traffic } from './traffic.js';
+import { separateAddresses } from '../chord/filters.js';
+import { Nodes } from './nodes.js';
+import { Links } from './links.js';
+import { nextHop, connectionPopupHTML } from './topoUtils.js';
+/**
+ * @module QDR
+ */
+export class TopologyController {
+  constructor(QDRService, $scope, $log, $rootScope, $location, $timeout, 
$uibModal, $sce) {
+    this.controllerName = 'QDR.TopologyController';
+
+    let QDRLog = new QDRLogger($log, 'TopologyController');
+    const TOPOOPTIONSKEY = 'topoOptions';
+    const radius = 25;
+    const radiusNormal = 15;
+
+    //  - nodes is an array of router/client info. these are the circles
+    //  - links is an array of connections between the routers. these are the 
lines with arrows
+    let nodes = new Nodes(QDRService, QDRLog);
+    let links = new Links(QDRService, QDRLog);
+    let forceData = {nodes: nodes, links: links};
+    // urlPrefix is used when referring to svg:defs
+    let urlPrefix = $location.absUrl();
+    urlPrefix = urlPrefix.split('#')[0];
+
+    $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || 
{showTraffic: false, trafficType: 'dots'};
+    if (!$scope.legendOptions.trafficType)
+      $scope.legendOptions.trafficType = 'dots';
+    $scope.legend = {status: {legendOpen: true, optionsOpen: true}};
+    $scope.legend.status.optionsOpen = $scope.legendOptions.showTraffic;
+    let traffic = new Traffic($scope, $timeout, QDRService, separateAddresses, 
+      radius, forceData, $scope.legendOptions.trafficType, urlPrefix);
+
+    // the showTraaffic checkbox was just toggled (or initialized)
+    $scope.$watch('legend.status.optionsOpen', function () {
+      $scope.legendOptions.showTraffic = $scope.legend.status.optionsOpen;
+      localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
+      if ($scope.legend.status.optionsOpen) {
+        traffic.start();
+      } else {
+        traffic.stop();
+        traffic.remove();
         restart();
+      }
+    });
+    $scope.$watch('legendOptions.trafficType', function () {
+      localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
+      if ($scope.legendOptions.showTraffic) {
+        restart();
+        traffic.setAnimationType($scope.legendOptions.trafficType, 
separateAddresses, radius);
+        traffic.start();
+      }
+    });
+
+    // mouse event vars
+    let selected_node = null,
+      selected_link = null,
+      mousedown_link = null,
+      mousedown_node = null,
+      mouseover_node = null,
+      mouseup_node = null,
+      initial_mouse_down_position = null;
+
+    $scope.schema = 'Not connected';
+
+    $scope.contextNode = null; // node that is associated with the current 
context menu
+    $scope.isRight = function(mode) {
+      return mode.right;
+    };
+
+    $scope.setFixed = function(b) {
+      if ($scope.contextNode) {
+        $scope.contextNode.fixed = b;
+        nodes.setNodesFixed($scope.contextNode.name, b);
+        nodes.savePositions();
+      }
+      restart();
+    };
+    $scope.isFixed = function() {
+      if (!$scope.contextNode)
+        return false;
+      return ($scope.contextNode.fixed & 1);
+    };
+
+    let mouseX, mouseY;
+    var relativeMouse = function () {
+      let offset = $('#main_container').offset();
+      return {left: (mouseX + $(document).scrollLeft()) - 1,
+        top: (mouseY  + $(document).scrollTop()) - 1,
+        offset: offset
       };
-      $scope.isFixed = function() {
-        if (!$scope.contextNode)
-          return false;
-        return ($scope.contextNode.fixed & 1);
-      };
-
-      let mouseX, mouseY;
-      var relativeMouse = function () {
-        let offset = $('#main_container').offset();
-        return {left: (mouseX + $(document).scrollLeft()) - 1,
-          top: (mouseY  + $(document).scrollTop()) - 1,
-          offset: offset
-        };
-      };
-      // event handlers for popup context menu
-      $(document).mousemove(function(e) {
-        mouseX = e.clientX;
-        mouseY = e.clientY;
-      });
-      $(document).mousemove();
-      $(document).click(function() {
-        $scope.contextNode = null;
-        $('.contextMenu').fadeOut(200);
-      });
-
-      const radii = {
-        'inter-router': 25,
-        'normal': 15,
-        'on-demand': 15,
-        'route-container': 15,
-      };
-      let svg, lsvg;
-      let force;
-      let animate = false; // should the force graph organize itself when it 
is displayed
-      let path, circle;
-      let savedKeys = {};
-      let width = 0;
-      let height = 0;
-
-      var getSizes = function() {
-        const gap = 5;
-        let legendWidth = 194;
-        let topoWidth = $('#topology').width();
-        if (topoWidth < 768)
-          legendWidth = 0;
-        let width = $('#topology').width() - gap - legendWidth;
-        let top = $('#topology').offset().top;
-        let height = window.innerHeight - top - gap;
-        if (width < 10) {
-          QDR.log.info('page width and height are abnormal w:' + width + ' 
height:' + height);
-          return [0, 0];
-        }
-        return [width, height];
-      };
-      var resize = function() {
-        if (!svg)
-          return;
-        let sizes = getSizes();
-        width = sizes[0];
-        height = sizes[1];
-        if (width > 0) {
-          // set attrs and 'resume' force
-          svg.attr('width', width);
-          svg.attr('height', height);
-          force.size(sizes).resume();
-        }
-        $timeout(createLegend);
-      };
-
-      // the window is narrow and the page menu icon was clicked.
-      // Re-create the legend
-      $scope.$on('pageMenuClicked', function () {
-        $timeout(createLegend);
-      });
-
-      window.addEventListener('resize', resize);
+    };
+    // event handlers for popup context menu
+    $(document).mousemove(e => {
+      mouseX = e.clientX;
+      mouseY = e.clientY;
+    });
+    $(document).mousemove();
+    $(document).click(function() {
+      $scope.contextNode = null;
+      $('.contextMenu').fadeOut(200);
+    });
+
+    const radii = {
+      'inter-router': 25,
+      'normal': 15,
+      'on-demand': 15,
+      'route-container': 15,
+    };
+    let svg, lsvg;  // main svg and legend svg
+    let force;
+    let animate = false; // should the force graph organize itself when it is 
displayed
+    let path, circle;
+    let savedKeys = {};
+    let width = 0;
+    let height = 0;
+
+    var getSizes = function() {
+      const gap = 5;
+      let legendWidth = 194;
+      let topoWidth = $('#topology').width();
+      if (topoWidth < 768)
+        legendWidth = 0;
+      let width = $('#topology').width() - gap - legendWidth;
+      let top = $('#topology').offset().top;
+      let height = window.innerHeight - top - gap;
+      if (width < 10) {
+        QDRLog.info(`page width and height are abnormal w: ${width} h: 
${height}`);
+        return [0, 0];
+      }
+      return [width, height];
+    };
+    var resize = function() {
+      if (!svg)
+        return;
       let sizes = getSizes();
       width = sizes[0];
       height = sizes[1];
-      if (width <= 0 || height <= 0)
-        return;
+      if (width > 0) {
+        // set attrs and 'resume' force
+        svg.attr('width', width);
+        svg.attr('height', height);
+        force.size(sizes).resume();
+      }
+      $timeout(createLegend);
+    };
+
+    // the window is narrow and the page menu icon was clicked.
+    // Re-create the legend
+    $scope.$on('pageMenuClicked', function () {
+      $timeout(createLegend);
+    });
+
+    window.addEventListener('resize', resize);
+    let sizes = getSizes();
+    width = sizes[0];
+    height = sizes[1];
+    if (width <= 0 || height <= 0)
+      return;
+
+    // vary the following force graph attributes based on nodeCount
+    // <= 6 routers returns min, >= 80 routers returns max, interpolate 
linearly
+    var forceScale = function(nodeCount, min, max) {
+      let count = Math.max(Math.min(nodeCount, 80), 6);
+      let x = d3.scale.linear()
+        .domain([6,80])
+        .range([min, max]);
+      //QDRLog.debug("forceScale(" + nodeCount + ", " + min + ", " + max + "  
returns " + x(count) + " " + x(nodeCount))
+      return x(count);
+    };
+    var linkDistance = function (d, nodeCount) {
+      if (d.target.nodeType === 'inter-router')
+        return forceScale(nodeCount, 150, 70);
+      return forceScale(nodeCount, 75, 40);
+    };
+    var charge = function (d, nodeCount) {
+      if (d.nodeType === 'inter-router')
+        return forceScale(nodeCount, -1800, -900);
+      return -900;
+    };
+    var gravity = function (d, nodeCount) {
+      return forceScale(nodeCount, 0.0001, 0.1);
+    };
+    // initialize the nodes and links array from the 
QDRService.topology._nodeInfo object
+    var initForceGraph = function() {
+      forceData.nodes = nodes = new Nodes(QDRService, QDRLog);
+      forceData.links = links = new Links(QDRService, QDRLog);
+      let nodeInfo = QDRService.management.topology.nodeInfo();
+      let nodeCount = Object.keys(nodeInfo).length;
+
+      let oldSelectedNode = selected_node;
+      let oldMouseoverNode = mouseover_node;
+      mouseover_node = null;
+      selected_node = null;
+      selected_link = null;
+
+      nodes.savePositions();
+      d3.select('#SVG_ID').remove();
+      svg = d3.select('#topology')
+        .append('svg')
+        .attr('id', 'SVG_ID')
+        .attr('width', width)
+        .attr('height', height);
+
+      // the legend
+      d3.select('#topo_svg_legend svg').remove();
+      lsvg = d3.select('#topo_svg_legend')
+        .append('svg')
+        .attr('id', 'svglegend');
+      lsvg = lsvg.append('svg:g')
+        .attr('transform', `translate( ${(radii['inter-router'] + 
2)},${(radii['inter-router'] + 2)})`)
+        .selectAll('g');
 
-      var nodeExists = function (connectionContainer) {
-        return nodes.findIndex( function (node) {
-          return node.container === connectionContainer;
-        });
-      };
-      var normalExists = function (connectionContainer) {
-        let normalInfo = {};
-        for (let i=0; i<nodes.length; ++i) {
-          if (nodes[i].normals) {
-            if (nodes[i].normals.some(function (normal, j) {
-              if (normal.container === connectionContainer && i !== j) {
-                normalInfo = {nodesIndex: i, normalsIndex: j};
-                return true;
-              }
-              return false;
-            }))
-              break;
-          }
-        }
-        return normalInfo;
-      };
-      var getLinkSource = function (nodesIndex) {
-        for (let i=0; i<links.length; ++i) {
-          if (links[i].target === nodesIndex)
-            return i;
-        }
-        return -1;
-      };
-      var aNode = function(id, name, nodeType, nodeInfo, nodeIndex, x, y, 
connectionContainer, resultIndex, fixed, properties) {
-        properties = properties || {};
-        for (let i=0; i<nodes.length; ++i) {
-          if (nodes[i].name === name || nodes[i].container === 
connectionContainer) {
-            if (properties.product)
-              nodes[i].properties = properties;
-            return nodes[i];
+      // mouse event vars
+      mousedown_link = null;
+      mousedown_node = null;
+      mouseup_node = null;
+
+      // initialize the list of nodes
+      forceData.nodes = nodes = new Nodes(QDRService, QDRLog);
+      animate = nodes.initialize(nodeInfo, localStorage, width, height);
+      nodes.savePositions();
+
+      // initialize the list of links
+      let unknowns = [];
+      forceData.links = links = new Links(QDRService, QDRLog);
+      if (links.initializeLinks(nodeInfo, nodes, unknowns, localStorage, 
height)) {
+        animate = true;
+      }
+      $scope.schema = QDRService.management.schema();
+      // init D3 force layout
+      force = d3.layout.force()
+        .nodes(nodes.nodes)
+        .links(links.links)
+        .size([width, height])
+        .linkDistance(function(d) { return linkDistance(d, nodeCount); })
+        .charge(function(d) { return charge(d, nodeCount); })
+        .friction(.10)
+        .gravity(function(d) { return gravity(d, nodeCount); })
+        .on('tick', tick)
+        .on('end', function () {nodes.savePositions();})
+        .start();
+
+      // This section adds in the arrows
+      svg.append('svg:defs').attr('class', 'marker-defs').selectAll('marker')
+        .data(['end-arrow', 'end-arrow-selected', 'end-arrow-small', 
'end-arrow-highlighted', 
+          'start-arrow', 'start-arrow-selected', 'start-arrow-small', 
'start-arrow-highlighted'])
+        .enter().append('svg:marker') 
+        .attr('id', function (d) { return d; })
+        .attr('viewBox', '0 -5 10 10')
+        .attr('refX', function (d) { 
+          if (d.substr(0, 3) === 'end') {
+            return 24;
           }
-        }
-        let routerId = QDRService.management.topology.nameFromId(id);
-        return {
-          key: id,
-          name: name,
-          nodeType: nodeType,
-          properties: properties,
-          routerId: routerId,
-          x: x,
-          y: y,
-          id: nodeIndex,
-          resultIndex: resultIndex,
-          fixed: !!+fixed,
-          cls: '',
-          container: connectionContainer
-        };
-      };
-
-      var getLinkDir = function (id, connection, onode) {
-        let links = onode['router.link'];
-        if (!links) {
-          return 'unknown';
-        }
-        let inCount = 0, outCount = 0;
-        links.results.forEach( function (linkResult) {
-          let link = QDRService.utilities.flatten(links.attributeNames, 
linkResult);
-          if (link.linkType === 'endpoint' && link.connectionId === 
connection.identity)
-            if (link.linkDir === 'in')
-              ++inCount;
-            else
-              ++outCount;
-        });
-        if (inCount > 0 && outCount > 0)
-          return 'both';
-        if (inCount > 0)
-          return 'in';
-        if (outCount > 0)
-          return 'out';
-        return 'unknown';
-      };
-
-      var savePositions = function () {
-        nodes.forEach( function (d) {
-          localStorage[d.name] = angular.toJson({
-            x: Math.round(d.x),
-            y: Math.round(d.y),
-            fixed: (d.fixed & 1) ? 1 : 0,
-          });
+          return d !== 'start-arrow-small' ? -14 : -24;})
+        .attr('markerWidth', 4)
+        .attr('markerHeight', 4)
+        .attr('orient', 'auto')
+        .classed('small', function (d) {return d.indexOf('small') > -1;})
+        .append('svg:path')
+        .attr('d', function (d) {
+          return d.substr(0, 3) === 'end' ? 'M 0 -5 L 10 0 L 0 5 z' : 'M 10 -5 
L 0 0 L 10 5 z';
         });
-      };
-
-      var initializeNodes = function (nodeInfo) {
-        let nodeCount = Object.keys(nodeInfo).length;
-        let yInit = 50;
-        forceData.nodes = nodes = [];
-        for (let id in nodeInfo) {
-          let name = QDRService.management.topology.nameFromId(id);
-          // if we have any new nodes, animate the force graph to position them
-          let position = angular.fromJson(localStorage[name]);
-          if (!angular.isDefined(position)) {
-            animate = true;
-            position = {
-              x: Math.round(width / 4 + ((width / 2) / nodeCount) * 
nodes.length),
-              y: Math.round(height / 2 + Math.sin(nodes.length / 
(Math.PI*2.0)) * height / 4),
-              fixed: false,
-            };
-            //QDR.log.debug("new node pos (" + position.x + ", " + position.y 
+ ")")
-          }
-          if (position.y > height) {
-            position.y = 200 - yInit;
-            yInit *= -1;
-          }
-          nodes.push(aNode(id, name, 'inter-router', nodeInfo, nodes.length, 
position.x, position.y, name, undefined, position.fixed));
-        }
-      };
 
-      var initializeLinks = function (nodeInfo, unknowns) {
-        forceData.links = links = [];
-        let source = 0;
-        let client = 1.0;
-        for (let id in nodeInfo) {
-          let onode = nodeInfo[id];
-          if (!onode['connection'])
-            continue;
-          let conns = onode['connection'].results;
-          let attrs = onode['connection'].attributeNames;
-          //QDR.log.debug("external client parent is " + parent);
-          let normalsParent = {}; // 1st normal node for this parent
-
-          for (let j = 0; j < conns.length; j++) {
-            let connection = QDRService.utilities.flatten(attrs, conns[j]);
-            let role = connection.role;
-            let properties = connection.properties || {};
-            let dir = connection.dir;
-            if (role == 'inter-router') {
-              let connId = connection.container;
-              let target = getContainerIndex(connId, nodeInfo);
-              if (target >= 0) {
-                getLink(source, target, dir, '', source + '-' + target);
-              }
-            } /* else if (role == "normal" || role == "on-demand" || role === 
"route-container")*/ {
-              // not an connection between routers, but an external connection
-              let name = QDRService.management.topology.nameFromId(id) + '.' + 
connection.identity;
-
-              // if we have any new clients, animate the force graph to 
position them
-              let position = angular.fromJson(localStorage[name]);
-              if (!angular.isDefined(position)) {
-                animate = true;
-                position = {
-                  x: Math.round(nodes[source].x + 40 * Math.sin(client / 
(Math.PI * 2.0))),
-                  y: Math.round(nodes[source].y + 40 * Math.cos(client / 
(Math.PI * 2.0))),
-                  fixed: false
-                };
-                //QDR.log.debug("new client pos (" + position.x + ", " + 
position.y + ")")
-              }// else QDR.log.debug("using previous location")
-              if (position.y > height) {
-                position.y = Math.round(nodes[source].y + 40 + Math.cos(client 
/ (Math.PI * 2.0)));
-              }
-              let existingNodeIndex = nodeExists(connection.container);
-              let normalInfo = normalExists(connection.container);
-              let node = aNode(id, name, role, nodeInfo, nodes.length, 
position.x, position.y, connection.container, j, position.fixed, properties);
-              let nodeType = QDRService.utilities.isAConsole(properties, 
connection.identity, role, node.key) ? 'console' : 'client';
-              let cdir = getLinkDir(id, connection, onode);
-              if (existingNodeIndex >= 0) {
-                // make a link between the current router (source) and the 
existing node
-                getLink(source, existingNodeIndex, dir, 'small', 
connection.name);
-              } else if (normalInfo.nodesIndex) {
-                // get node index of node that contained this connection in 
its normals array
-                let normalSource = getLinkSource(normalInfo.nodesIndex);
-                if (normalSource >= 0) {
-                  if (cdir === 'unknown')
-                    cdir = dir;
-                  node.cdir = cdir;
-                  nodes.push(node);
-                  // create link from original node to the new node
-                  getLink(links[normalSource].source, nodes.length-1, cdir, 
'small', connection.name);
-                  // create link from this router to the new node
-                  getLink(source, nodes.length-1, cdir, 'small', 
connection.name);
-                  // remove the old node from the normals list
-                  
nodes[normalInfo.nodesIndex].normals.splice(normalInfo.normalsIndex, 1);
-                }
-              } else if (role === 'normal') {
-              // normal nodes can be collapsed into a single node if they are 
all the same dir
-                if (cdir !== 'unknown') {
-                  node.user = connection.user;
-                  node.isEncrypted = connection.isEncrypted;
-                  node.host = connection.host;
-                  node.connectionId = connection.identity;
-                  node.cdir = cdir;
-                  // determine arrow direction by using the link directions
-                  if (!normalsParent[nodeType+cdir]) {
-                    normalsParent[nodeType+cdir] = node;
-                    nodes.push(node);
-                    node.normals = [node];
-                    // now add a link
-                    getLink(source, nodes.length - 1, cdir, 'small', 
connection.name);
-                    client++;
-                  } else {
-                    normalsParent[nodeType+cdir].normals.push(node);
-                  }
-                } else {
-                  node.id = nodes.length - 1 + unknowns.length;
-                  unknowns.push(node);
-                }
-              } else {
-                nodes.push(node);
-                // now add a link
-                getLink(source, nodes.length - 1, dir, 'small', 
connection.name);
-                client++;
-              }
-            }
-          }
-          source++;
-        }
-      };
-
-      // vary the following force graph attributes based on nodeCount
-      // <= 6 routers returns min, >= 80 routers returns max, interpolate 
linearly
-      var forceScale = function(nodeCount, min, max) {
-        let count = nodeCount;
-        if (nodeCount < 6) count = 6;
-        if (nodeCount > 80) count = 80;
-        let x = d3.scale.linear()
-          .domain([6,80])
-          .range([min, max]);
-        //QDR.log.debug("forceScale(" + nodeCount + ", " + min + ", " + max + 
"  returns " + x(count) + " " + x(nodeCount))
-        return x(count);
-      };
-      var linkDistance = function (d, nodeCount) {
-        if (d.target.nodeType === 'inter-router')
-          return forceScale(nodeCount, 150, 70);
-        return forceScale(nodeCount, 75, 40);
-      };
-      var charge = function (d, nodeCount) {
-        if (d.nodeType === 'inter-router')
-          return forceScale(nodeCount, -1800, -900);
-        return -900;
-      };
-      var gravity = function (d, nodeCount) {
-        return forceScale(nodeCount, 0.0001, 0.1);
-      };
-      // initialize the nodes and links array from the 
QDRService.topology._nodeInfo object
-      var initForceGraph = function() {
-        forceData.nodes = nodes = [];
-        forceData.links = links = [];
-        let nodeInfo = QDRService.management.topology.nodeInfo();
-        let nodeCount = Object.keys(nodeInfo).length;
-
-        let oldSelectedNode = selected_node;
-        let oldMouseoverNode = mouseover_node;
-        mouseover_node = null;
-        selected_node = null;
-        selected_link = null;
-
-        savePositions();
-        d3.select('#SVG_ID').remove();
-        svg = d3.select('#topology')
-          .append('svg')
-          .attr('id', 'SVG_ID')
-          .attr('width', width)
-          .attr('height', height);
-
-        // the legend
-        d3.select('#topo_svg_legend svg').remove();
-        lsvg = d3.select('#topo_svg_legend')
-          .append('svg')
-          .attr('id', 'svglegend');
-        lsvg = lsvg.append('svg:g')
-          .attr('transform', 'translate(' + (radii['inter-router'] + 2) + ',' 
+ (radii['inter-router'] + 2) + ')')
-          .selectAll('g');
-
-        // mouse event vars
-        mousedown_link = null;
-        mousedown_node = null;
-        mouseup_node = null;
-
-        // initialize the list of nodes
-        initializeNodes(nodeInfo);
-        savePositions();
-
-        // initialize the list of links
-        let unknowns = [];
-        initializeLinks(nodeInfo, unknowns);
-        $scope.schema = QDRService.management.schema();
-        // init D3 force layout
-        force = d3.layout.force()
-          .nodes(nodes)
-          .links(links)
-          .size([width, height])
-          .linkDistance(function(d) { return linkDistance(d, nodeCount); })
-          .charge(function(d) { return charge(d, nodeCount); })
-          .friction(.10)
-          .gravity(function(d) { return gravity(d, nodeCount); })
-          .on('tick', tick)
-          .on('end', function () {savePositions();})
-          .start();
-
-        // This section adds in the arrows
-        svg.append('svg:defs').attr('class', 'marker-defs').selectAll('marker')
-          .data(['end-arrow', 'end-arrow-selected', 'end-arrow-small', 
'end-arrow-highlighted', 
-            'start-arrow', 'start-arrow-selected', 'start-arrow-small', 
'start-arrow-highlighted'])
-          .enter().append('svg:marker') 
-          .attr('id', function (d) { return d; })
-          .attr('viewBox', '0 -5 10 10')
-          .attr('refX', function (d) { 
-            if (d.substr(0, 3) === 'end') {
-              return 24;
-            }
-            return d !== 'start-arrow-small' ? -14 : -24;})
-          .attr('markerWidth', 4)
-          .attr('markerHeight', 4)
-          .attr('orient', 'auto')
-          .classed('small', function (d) {return d.indexOf('small') > -1;})
-          .append('svg:path')
-          .attr('d', function (d) {
-            return d.substr(0, 3) === 'end' ? 'M 0 -5 L 10 0 L 0 5 z' : 'M 10 
-5 L 0 0 L 10 5 z';
-          });
-
-        // gradient for sender/receiver client
-        let grad = svg.append('svg:defs').append('linearGradient')
-          .attr('id', 'half-circle')
-          .attr('x1', '0%')
-          .attr('x2', '0%')
-          .attr('y1', '100%')
-          .attr('y2', '0%');
-        grad.append('stop').attr('offset', '50%').style('stop-color', 
'#C0F0C0');
-        grad.append('stop').attr('offset', '50%').style('stop-color', 
'#F0F000');
-
-        // handles to link and node element groups
-        path = svg.append('svg:g').selectAll('path'),
-        circle = svg.append('svg:g').selectAll('g');
-
-        // app starts here
-        restart(false);
-        force.start();
-        if (oldSelectedNode) {
-          d3.selectAll('circle.inter-router').classed('selected', function (d) 
{
-            if (d.key === oldSelectedNode.key) {
-              selected_node = d;
-              return true;
-            }
-            return false;
-          });
-        }
-        if (oldMouseoverNode && selected_node) {
-          d3.selectAll('circle.inter-router').each(function (d) {
-            if (d.key === oldMouseoverNode.key) {
-              mouseover_node = d;
-              QDRService.management.topology.ensureAllEntities([{entity: 
'router.node', attrs: ['id','nextHop']}], function () {
-                nextHop(selected_node, d);
-                restart();
-              });
-            }
-          });
-        }
-
-        // if any clients don't yet have link directions, get the links for 
those nodes and restart the graph
-        if (unknowns.length > 0)
-          setTimeout(resolveUnknowns, 10, nodeInfo, unknowns);
-
-        var continueForce = function (extra) {
-          if (extra > 0) {
-            --extra;
-            force.start();
-            setTimeout(continueForce, 100, extra);
+      // gradient for sender/receiver client
+      let grad = svg.append('svg:defs').append('linearGradient')
+        .attr('id', 'half-circle')
+        .attr('x1', '0%')
+        .attr('x2', '0%')
+        .attr('y1', '100%')
+        .attr('y2', '0%');
+      grad.append('stop').attr('offset', '50%').style('stop-color', '#C0F0C0');
+      grad.append('stop').attr('offset', '50%').style('stop-color', '#F0F000');
+
+      // handles to link and node element groups
+      path = svg.append('svg:g').selectAll('path'),
+      circle = svg.append('svg:g').selectAll('g');
+
+      // app starts here
+      restart(false);
+      force.start();
+      if (oldSelectedNode) {
+        d3.selectAll('circle.inter-router').classed('selected', function (d) {
+          if (d.key === oldSelectedNode.key) {
+            selected_node = d;
+            return true;
           }
-        };
-        continueForce(forceScale(nodeCount, 0, 200));  // give graph time to 
settle down
-      };
-
-      var resolveUnknowns = function (nodeInfo, unknowns) {
-        let unknownNodes = {};
-        // collapse the unknown node.keys using an object
-        for (let i=0; i<unknowns.length; ++i) {
-          unknownNodes[unknowns[i].key] = 1;
-        }
-        unknownNodes = Object.keys(unknownNodes);
-        //QDR.log.info("-- resolveUnknowns: ensuring .connection and 
.router.link are present for each node")
-        QDRService.management.topology.ensureEntities(unknownNodes, [{entity: 
'connection', force: true}, 
-          {entity: 'router.link', attrs: 
['linkType','connectionId','linkDir'], force: true}], function () {
-          nodeInfo = QDRService.management.topology.nodeInfo();
-          initializeLinks(nodeInfo, []);
-          // collapse any router-container nodes that are duplicates
-          animate = true;
-          force.nodes(nodes).links(links).start();
-          restart(false);
+          return false;
         });
-      };
-
-      function getContainerIndex(_id, nodeInfo) {
-        let nodeIndex = 0;
-        for (let id in nodeInfo) {
-          if (QDRService.management.topology.nameFromId(id) === _id)
-            return nodeIndex;
-          ++nodeIndex;
-        }
-        return -1;
-      }
-
-      function getLink(_source, _target, dir, cls, uid) {
-        for (let i = 0; i < links.length; i++) {
-          let s = links[i].source,
-            t = links[i].target;
-          if (typeof links[i].source == 'object') {
-            s = s.id;
-            t = t.id;
-          }
-          if (s == _source && t == _target) {
-            return i;
-          }
-          // same link, just reversed
-          if (s == _target && t == _source) {
-            return -i;
-          }
-        }
-        //QDR.log.debug("creating new link (" + (links.length) + ") between " 
+ nodes[_source].name + " and " + nodes[_target].name);
-        if (links.some( function (l) { return l.uid === uid;}))
-          uid = uid + '.' + links.length;
-        let link = {
-          source: _source,
-          target: _target,
-          left: dir != 'out',
-          right: (dir == 'out' || dir == 'both'),
-          cls: cls,
-          uid: uid,
-        };
-        return links.push(link) - 1;
       }
-
-
-      function resetMouseVars() {
-        mousedown_node = null;
-        mouseover_node = null;
-        mouseup_node = null;
-        mousedown_link = null;
-      }
-
-      // update force layout (called automatically each iteration)
-      function tick() {
-        circle.attr('transform', function(d) {
-          let cradius;
-          if (d.nodeType == 'inter-router') {
-            cradius = d.left ? radius + 8 : radius;
-          } else {
-            cradius = d.left ? radiusNormal + 18 : radiusNormal;
+      if (oldMouseoverNode && selected_node) {
+        d3.selectAll('circle.inter-router').each(function (d) {
+          if (d.key === oldMouseoverNode.key) {
+            mouseover_node = d;
+            QDRService.management.topology.ensureAllEntities([{entity: 
'router.node', attrs: ['id','nextHop']}], function () {
+              nextHopHighlight(selected_node, d);
+              restart();
+            });
           }
-          d.x = Math.max(d.x, radiusNormal * 2);
-          d.y = Math.max(d.y, radiusNormal * 2);
-          d.x = Math.max(0, Math.min(width - cradius, d.x));
-          d.y = Math.max(0, Math.min(height - cradius, d.y));
-          return 'translate(' + d.x + ',' + d.y + ')';
         });
+      }
 
-        // draw directed edges with proper padding from node centers
-        path.attr('d', function(d) {
-          let sourcePadding, targetPadding, r;
-
-          r = d.target.nodeType === 'inter-router' ? radius : radiusNormal - 
18;
-          sourcePadding = targetPadding = 0;
-          let dtx = Math.max(targetPadding, Math.min(width - r, d.target.x)),
-            dty = Math.max(targetPadding, Math.min(height - r, d.target.y)),
-            dsx = Math.max(sourcePadding, Math.min(width - r, d.source.x)),
-            dsy = Math.max(sourcePadding, Math.min(height - r, d.source.y));
-
-          let deltaX = dtx - dsx,
-            deltaY = dty - dsy,
-            dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
-          if (dist == 0)
-            dist = 0.001;
-          let normX = deltaX / dist,
-            normY = deltaY / dist;
-          let sourceX = dsx + (sourcePadding * normX),
-            sourceY = dsy + (sourcePadding * normY),
-            targetX = dtx - (targetPadding * normX),
-            targetY = dty - (targetPadding * normY);
-          sourceX = Math.max(0, sourceX);
-          sourceY = Math.max(0, sourceY);
-          targetX = Math.max(0, targetX);
-          targetY = Math.max(0, targetY);
-
-          return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
-        })
-          .attr('id', function (d) {
-            return ['path', d.source.index, d.target.index].join('-');
-          });
+      // if any clients don't yet have link directions, get the links for 
those nodes and restart the graph
+      if (unknowns.length > 0)
+        setTimeout(resolveUnknowns, 10, nodeInfo, unknowns);
 
-        if (!animate) {
-          animate = true;
-          force.stop();
+      var continueForce = function (extra) {
+        if (extra > 0) {
+          --extra;
+          force.start();
+          setTimeout(continueForce, 100, extra);
         }
+      };
+      continueForce(forceScale(nodeCount, 0, 200));  // give large graphs time 
to settle down
+    };
+
+    // To start up quickly, we only get the connection info for each router.
+    // That means we don't have the router.link info when links.initialize() 
is first called.
+    // The router.link info is needed to determine which direction the arrows 
between routers should point.
+    // So, the first time through links.initialize() we keep track of the 
nodes for which we 
+    // need router.link info and fill in that info here.
+    var resolveUnknowns = function (nodeInfo, unknowns) {
+      let unknownNodes = {};
+      // collapse the unknown node.keys using an object
+      for (let i=0; i<unknowns.length; ++i) {
+        unknownNodes[unknowns[i].key] = 1;
       }
+      unknownNodes = Object.keys(unknownNodes);
+      //QDRLog.info("-- resolveUnknowns: ensuring .connection and .router.link 
are present for each node")
+      QDRService.management.topology.ensureEntities(unknownNodes, [{entity: 
'connection', force: true}, 
+        {entity: 'router.link', attrs: ['linkType','connectionId','linkDir'], 
force: true}], function () {
+        nodeInfo = QDRService.management.topology.nodeInfo();
+        forceData.links = links = new Links(QDRService, QDRLog);
+        links.initializeLinks(nodeInfo, nodes, [], localStorage, height);
+        animate = true;
+        force.nodes(nodes.nodes).links(links.links).start();
+        restart(false);
+      });
+    };
 
-      // highlight the paths between the selected node and the hovered node
-      function findNextHopNode(from, d) {
-        // d is the node that the mouse is over
-        // from is the selected_node ....
-        if (!from)
-          return null;
-
-        if (from == d)
-          return selected_node;
-
-        //QDR.log.debug("finding nextHop from: " + from.name + " to " + 
d.name);
-        let sInfo = QDRService.management.topology.nodeInfo()[from.key];
+    function resetMouseVars() {
+      mousedown_node = null;
+      mouseover_node = null;
+      mouseup_node = null;
+      mousedown_link = null;
+    }
 
-        if (!sInfo) {
-          QDR.log.warn('unable to find topology node info for ' + from.key);
-          return null;
+    // update force layout (called automatically each iteration)
+    function tick() {
+      circle.attr('transform', function(d) {
+        let cradius;
+        if (d.nodeType == 'inter-router') {
+          cradius = d.left ? radius + 8 : radius;
+        } else {
+          cradius = d.left ? radiusNormal + 18 : radiusNormal;
         }
+        d.x = Math.max(d.x, radiusNormal * 2);
+        d.y = Math.max(d.y, radiusNormal * 2);
+        d.x = Math.max(0, Math.min(width - cradius, d.x));
+        d.y = Math.max(0, Math.min(height - cradius, d.y));
+        return `translate(${d.x},${d.y})`;
+      });
 
-        // find the hovered name in the selected name's .router.node results
-        if (!sInfo['router.node'])
-          return null;
-        let aAr = sInfo['router.node'].attributeNames;
-        let vAr = sInfo['router.node'].results;
-        for (let hIdx = 0; hIdx < vAr.length; ++hIdx) {
-          let addrT = QDRService.utilities.valFor(aAr, vAr[hIdx], 'id');
-          if (addrT == d.name) {
-            //QDR.log.debug("found " + d.name + " at " + hIdx);
-            let nextHop = QDRService.utilities.valFor(aAr, vAr[hIdx], 
'nextHop');
-            //QDR.log.debug("nextHop was " + nextHop);
-            return (nextHop == null) ? nodeFor(addrT) : nodeFor(nextHop);
-          }
-        }
-        return null;
-      }
+      // draw directed edges with proper padding from node centers
+      path.attr('d', function(d) {
+        let sourcePadding, targetPadding, r;
+
+        r = d.target.nodeType === 'inter-router' ? radius : radiusNormal - 18;
+        sourcePadding = targetPadding = 0;
+        let dtx = Math.max(targetPadding, Math.min(width - r, d.target.x)),
+          dty = Math.max(targetPadding, Math.min(height - r, d.target.y)),
+          dsx = Math.max(sourcePadding, Math.min(width - r, d.source.x)),
+          dsy = Math.max(sourcePadding, Math.min(height - r, d.source.y));
+
+        let deltaX = dtx - dsx,
+          deltaY = dty - dsy,
+          dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
+        if (dist == 0)
+          dist = 0.001;
+        let normX = deltaX / dist,
+          normY = deltaY / dist;
+        let sourceX = dsx + (sourcePadding * normX),
+          sourceY = dsy + (sourcePadding * normY),
+          targetX = dtx - (targetPadding * normX),
+          targetY = dty - (targetPadding * normY);
+        sourceX = Math.max(0, sourceX);
+        sourceY = Math.max(0, sourceY);
+        targetX = Math.max(0, targetX);
+        targetY = Math.max(0, targetY);
+
+        return `M${sourceX},${sourceY}L${targetX},${targetY}`;
+      })
+        .attr('id', function (d) {
+          return ['path', d.source.index, d.target.index].join('-');
+        });
 
-      function nodeFor(name) {
-        for (let i = 0; i < nodes.length; ++i) {
-          if (nodes[i].name == name)
-            return nodes[i];
-        }
-        return null;
+      if (!animate) {
+        animate = true;
+        force.stop();
       }
+    }
 
-      function linkFor(source, target) {
-        for (let i = 0; i < links.length; ++i) {
-          if ((links[i].source == source) && (links[i].target == target))
-            return links[i];
-          if ((links[i].source == target) && (links[i].target == source))
-            return links[i];
-        }
-        // the selected node was a client/broker
-        return null;
-      }
+    function nextHopHighlight(selected_node, d) {
+      nextHop(selected_node, d, nodes, links, QDRService, selected_node, 
function (hlLink, hnode) {
+        hlLink.highlighted = true;
+        hnode.highlighted = true;
+      });
+      let hnode = nodes.nodeFor(d.name);
+      hnode.highlighted = true;
+    }
 
-      function clearPopups() {
-        d3.select('#crosssection').style('display', 'none');
-        $('.hastip').empty();
-        d3.select('#multiple_details').style('display', 'none');
-        d3.select('#link_details').style('display', 'none');
-        d3.select('#node_context_menu').style('display', 'none');
+    function clearPopups() {
+      d3.select('#crosssection').style('display', 'none');
+      $('.hastip').empty();
+      d3.select('#multiple_details').style('display', 'none');
+      d3.select('#link_details').style('display', 'none');
+      d3.select('#node_context_menu').style('display', 'none');
 
-      }
+    }
 
-      function clerAllHighlights() {
-        for (let i = 0; i < links.length; ++i) {
-          links[i]['highlighted'] = false;
-        }
-        for (let i = 0; i<nodes.length; ++i) {
-          nodes[i]['highlighted'] = false;
-        }
-      }
-      // takes the nodes and links array of objects and adds svg elements for 
everything that hasn't already
-      // been added
-      function restart(start) {
-        if (!circle)
-          return;
-        circle.call(force.drag);
-
-        // path (link) group
-        path = path.data(links, function(d) {return d.uid;});
-
-        // update existing links
-        path.classed('selected', function(d) {
-          return d === selected_link;
-        })
-          .classed('highlighted', function(d) {
-            return d.highlighted;
-          });
-        if (!$scope.legend.status.optionsOpen || 
$scope.legendOptions.trafficType === 'dots') {
-          path
-            .attr('marker-start', function(d) {
-              let sel = d === selected_link ? '-selected' : (d.cls === 'small' 
? '-small' : '');
-              if (d.highlighted)
-                sel = '-highlighted';
-              return d.left ? 'url(' + urlPrefix + '#start-arrow' + sel + ')' 
: '';
-            })
-            .attr('marker-end', function(d) {
-              let sel = d === selected_link ? '-selected' : (d.cls === 'small' 
? '-small' : '');
-              if (d.highlighted)
-                sel = '-highlighted';
-              return d.right ? 'url(' + urlPrefix + '#end-arrow' + sel + ')' : 
'';
-            });
-        }
-        // add new links. if a link with a new uid is found in the data, add a 
new path
-        path.enter().append('svg:path')
-          .attr('class', 'link')
+    function clearAllHighlights() {
+      links.clearHighlighted();
+      nodes.clearHighlighted();
+    }
+    // takes the nodes and links array of objects and adds svg elements for 
everything that hasn't already
+    // been added
+    function restart(start) {
+      if (!circle)
+        return;
+      circle.call(force.drag);
+
+      // path (link) group
+      path = path.data(links.links, function(d) {return d.uid;});
+
+      // update existing links
+      path.classed('selected', function(d) {
+        return d === selected_link;
+      })
+        .classed('highlighted', function(d) {
+          return d.highlighted;
+        });
+      if (!$scope.legend.status.optionsOpen || 
$scope.legendOptions.trafficType === 'dots') {
+        path
           .attr('marker-start', function(d) {
             let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? 
'-small' : '');
-            return d.left ? 'url(' + urlPrefix + '#start-arrow' + sel + ')' : 
'';
+            if (d.highlighted)
+              sel = '-highlighted';
+            return d.left ? `url(${urlPrefix}#start-arrow${sel})` : '';
           })
           .attr('marker-end', function(d) {
             let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? 
'-small' : '');
-            return d.right ? 'url(' + urlPrefix + '#end-arrow' + sel + ')' : 
'';
-          })
-          .classed('small', function(d) {
-            return d.cls == 'small';
-          })
-          .on('mouseover', function(d) { // mouse over a path
-            let event = d3.event;
-            mousedown_link = d;
-            selected_link = mousedown_link;
-            let updateTooltip = function () {
-              $timeout(function () {
-                $scope.trustedpopoverContent = 
$sce.trustAsHtml(connectionPopupHTML(d));
-                if (selected_link)
-                  displayTooltip(event);
-              });
-            };
-            // update the contents of the popup tooltip each time the data is 
polled
-            
QDRService.management.topology.addUpdatedAction('connectionPopupHTML', 
updateTooltip);
-            QDRService.management.topology.ensureAllEntities(
-              [{ entity: 'router.link', force: true},{entity: 'connection'}], 
function () {
-                updateTooltip();
-              });
-            // show the tooltip
-            updateTooltip();
-            restart();
-
-          })
-          .on('mouseout', function() { // mouse out of a path
-            
QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
-            d3.select('#popover-div')
-              .style('display', 'none');
-            selected_link = null;
-            restart();
-          })
-          // left click a path
-          .on('click', function () {
-            d3.event.stopPropagation();
-            clearPopups();
+            if (d.highlighted)
+              sel = '-highlighted';
+            return d.right ? `url(${urlPrefix}#end-arrow${sel})` : '';
           });
-        // remove old links
-        path.exit().remove();
+      }
+      // add new links. if a link with a new uid is found in the data, add a 
new path
+      path.enter().append('svg:path')
+        .attr('class', 'link')
+        .attr('marker-start', function(d) {
+          let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? 
'-small' : '');
+          return d.left ? `url(${urlPrefix}#start-arrow${sel})` : '';
+        })
+        .attr('marker-end', function(d) {
+          let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? 
'-small' : '');
+          return d.right ? `url(${urlPrefix}#end-arrow${sel})` : '';
+        })
+        .classed('small', function(d) {
+          return d.cls == 'small';
+        })
+        .on('mouseover', function(d) { // mouse over a path
+          let event = d3.event;
+          mousedown_link = d;
+          selected_link = mousedown_link;
+          let updateTooltip = function () {
+            $timeout(function () {
+              $scope.trustedpopoverContent = 
$sce.trustAsHtml(connectionPopupHTML(d, QDRService));
+              if (selected_link)
+                displayTooltip(event);
+            });
+          };
+          // update the contents of the popup tooltip each time the data is 
polled
+          
QDRService.management.topology.addUpdatedAction('connectionPopupHTML', 
updateTooltip);
+          QDRService.management.topology.ensureAllEntities(
+            [{ entity: 'router.link', force: true},{entity: 'connection'}], 
function () {
+              updateTooltip();
+            });
+          // show the tooltip
+          updateTooltip();
+          restart();
+
+        })
+        .on('mouseout', function() { // mouse out of a path
+          
QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
+          d3.select('#popover-div')
+            .style('display', 'none');
+          selected_link = null;
+          restart();
+        })
+        // left click a path
+        .on('click', function () {
+          d3.event.stopPropagation();
+          clearPopups();
+        });
+      // remove old links
+      path.exit().remove();
 
 
-        // circle (node) group
-        // nodes are known by id
-        circle = circle.data(nodes, function(d) {
-          return d.name;
+      // circle (node) group
+      // nodes are known by id
+      circle = circle.data(nodes.nodes, function(d) {
+        return d.name;
+      });
+
+      // update existing nodes visual states
+      circle.selectAll('circle')
+        .classed('highlighted', function(d) {
+          return d.highlighted;
+        })
+        .classed('selected', function(d) {
+          return (d === selected_node);
+        })
+        .classed('fixed', function(d) {
+          return d.fixed & 1;
         });
 
-        // update existing nodes visual states
-        circle.selectAll('circle')
-          .classed('highlighted', function(d) {
-            return d.highlighted;
-          })
-          .classed('selected', function(d) {
-            return (d === selected_node);
-          })
-          .classed('fixed', function(d) {
-            return d.fixed & 1;
+      // add new circle nodes
+      let g = circle.enter().append('svg:g')
+        .classed('multiple', function(d) {
+          return (d.normals && d.normals.length > 1);
+        })
+        .attr('id', function (d) { return (d.nodeType !== 'normal' ? 'router' 
: 'client') + '-' + d.index; });
+
+      appendCircle(g)
+        .on('mouseover', function(d) {  // mouseover a circle
+          
QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
+          if (d.nodeType === 'normal') {
+            showClientTooltip(d, d3.event);
+          } else
+            showRouterTooltip(d, d3.event);
+          if (d === mousedown_node)
+            return;
+          // enlarge target node
+          d3.select(this).attr('transform', 'scale(1.1)');
+          if (!selected_node) {
+            return;
+          }
+          // highlight the next-hop route from the selected node to this node
+          clearAllHighlights();
+          // we need .router.node info to highlight hops
+          QDRService.management.topology.ensureAllEntities([{entity: 
'router.node', attrs: ['id','nextHop']}], function () {
+            mouseover_node = d;  // save this node in case the topology 
changes so we can restore the highlights
+            nextHopHighlight(selected_node, d);
+            restart();
           });
-
-        // add new circle nodes
-        let g = circle.enter().append('svg:g')
-          .classed('multiple', function(d) {
-            return (d.normals && d.normals.length > 1);
-          })
-          .attr('id', function (d) { return (d.nodeType !== 'normal' ? 
'router' : 'client') + '-' + d.index; });
-
-        appendCircle(g)
-          .on('mouseover', function(d) {  // mouseover a circle
-            
QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
-            if (d.nodeType === 'normal') {
-              showClientTooltip(d, d3.event);
-            } else
-              showRouterTooltip(d, d3.event);
-            if (d === mousedown_node)
-              return;
-            // enlarge target node
-            d3.select(this).attr('transform', 'scale(1.1)');
-            if (!selected_node) {
-              return;
-            }
-            // highlight the next-hop route from the selected node to this node
-            clerAllHighlights();
-            // we need .router.node info to highlight hops
-            QDRService.management.topology.ensureAllEntities([{entity: 
'router.node', attrs: ['id','nextHop']}], function () {
-              mouseover_node = d;  // save this node in case the topology 
changes so we can restore the highlights
-              nextHop(selected_node, d);
-              restart();
-            });
-          })
-          .on('mouseout', function() { // mouse out for a circle
-            // unenlarge target node
-            d3.select('#popover-div')
-              .style('display', 'none');
-            d3.select(this).attr('transform', '');
-            clerAllHighlights();
-            mouseover_node = null;
+        })
+        .on('mouseout', function() { // mouse out for a circle
+          // unenlarge target node
+          d3.select('#popover-div')
+            .style('display', 'none');
+          d3.select(this).attr('transform', '');
+          clearAllHighlights();
+          mouseover_node = null;
+          restart();
+        })
+        .on('mousedown', function(d) { // mouse down for circle
+          if (d3.event.button !== 0) { // ignore all but left button
+            return;
+          }
+          mousedown_node = d;
+          // mouse position relative to svg
+          initial_mouse_down_position = 
d3.mouse(this.parentNode.parentNode.parentNode).slice();
+        })
+        .on('mouseup', function(d) {  // mouse up for circle
+          if (!mousedown_node)
+            return;
+
+          selected_link = null;
+          // unenlarge target node
+          d3.select(this).attr('transform', '');
+
+          // check for drag
+          mouseup_node = d;
+
+          let mySvg = this.parentNode.parentNode.parentNode;
+          // if we dragged the node, make it fixed
+          let cur_mouse = d3.mouse(mySvg);
+          if (cur_mouse[0] != initial_mouse_down_position[0] ||
+            cur_mouse[1] != initial_mouse_down_position[1]) {
+            d.fixed = true;
+            nodes.setNodesFixed(d.name, true);
+            resetMouseVars();
             restart();
-          })
-          .on('mousedown', function(d) { // mouse down for circle
-            if (d3.event.button !== 0) { // ignore all but left button
-              return;
-            }
-            mousedown_node = d;
-            // mouse position relative to svg
-            initial_mouse_down_position = 
d3.mouse(this.parentNode.parentNode.parentNode).slice();
-          })
-          .on('mouseup', function(d) {  // mouse up for circle
-            if (!mousedown_node)
-              return;
-
-            selected_link = null;
-            // unenlarge target node
-            d3.select(this).attr('transform', '');
-
-            // check for drag
-            mouseup_node = d;
-
-            let mySvg = this.parentNode.parentNode.parentNode;
-            // if we dragged the node, make it fixed
-            let cur_mouse = d3.mouse(mySvg);
-            if (cur_mouse[0] != initial_mouse_down_position[0] ||
-              cur_mouse[1] != initial_mouse_down_position[1]) {
-              d.fixed = true;
-              setNodesFixed(d.name, true);
-              resetMouseVars();
-              restart();
-              return;
-            }
+            return;
+          }
 
-            // if this node was selected, unselect it
-            if (mousedown_node === selected_node) {
-              selected_node = null;
-            } else {
-              if (d.nodeType !== 'normal' && d.nodeType !== 'on-demand')
-                selected_node = mousedown_node;
-            }
-            clerAllHighlights();
-            mousedown_node = null;
-            if (!$scope.$$phase) $scope.$apply();
-            restart(false);
+          // if this node was selected, unselect it
+          if (mousedown_node === selected_node) {
+            selected_node = null;
+          } else {
+            if (d.nodeType !== 'normal' && d.nodeType !== 'on-demand')
+              selected_node = mousedown_node;
+          }
+          clearAllHighlights();
+          mousedown_node = null;
+          if (!$scope.$$phase) $scope.$apply();
+          restart(false);
 
-          })
-          .on('dblclick', function(d) { // circle
-            if (d.fixed) {
-              d.fixed = false;
-              setNodesFixed(d.name, false);
-              restart(); // redraw the node without a dashed line
-              force.start(); // let the nodes move to a new position
-            }
-          })
-          .on('contextmenu', function(d) {  // circle
-            $(document).click();
-            d3.event.preventDefault();
-            let rm = relativeMouse();
-            d3.select('#node_context_menu')
-              .style({
-                display: 'block',
-                left: rm.left + 'px',
-                top: (rm.top - rm.offset.top) + 'px'
-              });
-            $timeout( function () {
-              $scope.contextNode = d;
+        })
+        .on('dblclick', function(d) { // circle
+          if (d.fixed) {
+            d.fixed = false;
+            nodes.setNodesFixed(d.name, false);
+            restart(); // redraw the node without a dashed line
+            force.start(); // let the nodes move to a new position
+          }
+        })
+        .on('contextmenu', function(d) {  // circle
+          $(document).click();
+          d3.event.preventDefault();
+          let rm = relativeMouse();
+          d3.select('#node_context_menu')
+            .style({
+              display: 'block',
+              left: rm.left + 'px',
+              top: (rm.top - rm.offset.top) + 'px'
             });
-          })
-          .on('click', function(d) {  // circle
-            if (!mouseup_node)
-              return;
-            // clicked on a circle
-            clearPopups();
-            if (!d.normals) {
-              // circle was a router or a broker
-              if (QDRService.utilities.isArtemis(d)) {
-                const artemisPath = '/jmx/attributes?tab=artemis&con=Artemis';
-                if (QDR.isStandalone)
-                  window.location = $location.protocol() + 
'://localhost:8161/hawtio' + artemisPath;
-                else
-                  $location.path(artemisPath);
-              }
-              return;
-            }
-            d3.event.stopPropagation();
+          $timeout( function () {
+            $scope.contextNode = d;
           });
-
-        appendContent(g);
-        //appendTitle(g);
-
-        // remove old nodes
-        circle.exit().remove();
-
-        // add text to client circles if there are any that represent multiple 
clients
-        svg.selectAll('.subtext').remove();
-        let multiples = svg.selectAll('.multiple');
-        multiples.each(function(d) {
-          let g = d3.select(this);
-          g.append('svg:text')
-            .attr('x', radiusNormal + 3)
-            .attr('y', Math.floor(radiusNormal / 2))
-            .attr('class', 'subtext')
-            .text('x ' + d.normals.length);
+        })
+        .on('click', function(d) {  // circle
+          if (!mouseup_node)
+            return;
+          // clicked on a circle
+          clearPopups();
+          if (!d.normals) {
+            // circle was a router or a broker
+            if (QDRService.utilities.isArtemis(d)) {
+              const artemisPath = '/jmx/attributes?tab=artemis&con=Artemis';
+              window.location = $location.protocol() + 
'://localhost:8161/hawtio' + artemisPath;
+            }
+            return;
+          }
+          d3.event.stopPropagation();
         });
-        // call createLegend in timeout because:
-        // If we create the legend right away, then it will be destroyed when 
the accordian
-        // gets initialized as the page loads.
-        $timeout(createLegend);
 
-        if (!mousedown_node || !selected_node)
-          return;
+      appendContent(g);
+      //appendTitle(g);
 
-        if (!start)
-          return;
-        // set the graph in motion
-        //QDR.log.debug("mousedown_node is " + mousedown_node);
-        force.start();
+      // remove old nodes
+      circle.exit().remove();
 
-      }
-      let createLegend = function () {
-        // dynamically create the legend based on which node types are present
-        // the legend
-        d3.select('#topo_svg_legend svg').remove();
-        lsvg = d3.select('#topo_svg_legend')
-          .append('svg')
-          .attr('id', 'svglegend');
-        lsvg = lsvg.append('svg:g')
-          .attr('transform', 'translate(' + (radii['inter-router'] + 2) + ',' 
+ (radii['inter-router'] + 2) + ')')
-          .selectAll('g');
-        let legendNodes = [];
-        legendNodes.push(aNode('Router', '', 'inter-router', '', undefined, 0, 
0, 0, 0, false, {}));
-
-        if (!svg.selectAll('circle.console').empty()) {
-          legendNodes.push(aNode('Console', '', 'normal', '', undefined, 1, 0, 
0, 0, false, {
-            console_identifier: 'Dispatch console'
-          }));
-        }
-        if (!svg.selectAll('circle.client.in').empty()) {
-          let node = aNode('Sender', '', 'normal', '', undefined, 2, 0, 0, 0, 
false, {});
-          node.cdir = 'in';
-          legendNodes.push(node);
-        }
-        if (!svg.selectAll('circle.client.out').empty()) {
-          let node = aNode('Receiver', '', 'normal', '', undefined, 3, 0, 0, 
0, false, {});
-          node.cdir = 'out';
-          legendNodes.push(node);
-        }
-        if (!svg.selectAll('circle.client.inout').empty()) {
-          let node = aNode('Sender/Receiver', '', 'normal', '', undefined, 4, 
0, 0, 0, false, {});
-          node.cdir = 'both';
-          legendNodes.push(node);
-        }
-        if (!svg.selectAll('circle.qpid-cpp').empty()) {
-          legendNodes.push(aNode('Qpid broker', '', 'route-container', '', 
undefined, 5, 0, 0, 0, false, {
-            product: 'qpid-cpp'
-          }));
-        }
-        if (!svg.selectAll('circle.artemis').empty()) {
-          legendNodes.push(aNode('Artemis broker', '', 'route-container', '', 
undefined, 6, 0, 0, 0, false,
-            {product: 'apache-activemq-artemis'}));
-        }
-        if (!svg.selectAll('circle.route-container').empty()) {
-          legendNodes.push(aNode('Service', '', 'route-container', 
'external-service', undefined, 7, 0, 0, 0, false,
-            {product: ' External Service'}));
-        }
-        lsvg = lsvg.data(legendNodes, function(d) {
-          return d.key;
-        });
-        let lg = lsvg.enter().append('svg:g')
-          .attr('transform', function(d, i) {
-            // 45px between lines and add 10px space after 1st line
-            return 'translate(0, ' + (45 * i + (i > 0 ? 10 : 0)) + ')';
-          });
-
-        appendCircle(lg);
-        appendContent(lg);
-        appendTitle(lg);
-        lg.append('svg:text')
-          .attr('x', 35)
-          .attr('y', 6)
-          .attr('class', 'label')
-          .text(function(d) {
-            return d.key;
-          });
-        lsvg.exit().remove();
-        let svgEl = document.getElementById('svglegend');
-        if (svgEl) {
-          let bb;
-          // firefox can throw an exception on getBBox on an svg element
-          try {
-            bb = svgEl.getBBox();
-          } catch (e) {
-            bb = {
-              y: 0,
-              height: 200,
-              x: 0,
-              width: 200
-            };
-          }
-          svgEl.style.height = (bb.y + bb.height) + 'px';
-          svgEl.style.width = (bb.x + bb.width) + 'px';
-        }
-      };
-      let appendCircle = function(g) {
-        // add new circles and set their attr/class/behavior
-        return g.append('svg:circle')
-          .attr('class', 'node')
-          .attr('r', function(d) {
-            return radii[d.nodeType];
-          })
-          .attr('fill', function (d) {
-            if (d.cdir === 'both' && !QDRService.utilities.isConsole(d)) {
-              return 'url(' + urlPrefix + '#half-circle)';
-            }
-            return null;
-          })
-          .classed('fixed', function(d) {
-            return d.fixed & 1;
-          })
-          .classed('normal', function(d) {
-            return d.nodeType == 'normal' || QDRService.utilities.isConsole(d);
-          })
-          .classed('in', function(d) {
-            return d.cdir == 'in';
-          })
-          .classed('out', function(d) {
-            return d.cdir == 'out';
-          })
-          .classed('inout', function(d) {
-            return d.cdir == 'both';
-          })
-          .classed('inter-router', function(d) {
-            return d.nodeType == 'inter-router';
-          })
-          .classed('on-demand', function(d) {
-            return d.nodeType == 'on-demand';
-          })
-          .classed('console', function(d) {
-            return QDRService.utilities.isConsole(d);
-          })
-          .classed('artemis', function(d) {
-            return QDRService.utilities.isArtemis(d);
-          })
-          .classed('qpid-cpp', function(d) {
-            return QDRService.utilities.isQpid(d);
-          })
-          .classed('route-container', function (d) {
-            return (!QDRService.utilities.isArtemis(d) && 
!QDRService.utilities.isQpid(d) && d.nodeType === 'route-container');
-          })
-          .classed('client', function(d) {
-            return d.nodeType === 'normal' && !d.properties.console_identifier;
-          });
-      };
-      let appendContent = function(g) {
-        // show node IDs
+      // add text to client circles if there are any that represent multiple 
clients
+      svg.selectAll('.subtext').remove();
+      let multiples = svg.selectAll('.multiple');
+      multiples.each(function(d) {
+        let g = d3.select(this);
         g.append('svg:text')
-          .attr('x', 0)
-          .attr('y', function(d) {
-            let y = 7;
-            if (QDRService.utilities.isArtemis(d))
-              y = 8;
-            else if (QDRService.utilities.isQpid(d))
-              y = 9;
-            else if (d.nodeType === 'inter-router')
-              y = 4;
-            else if (d.nodeType === 'route-container')
-              y = 5;
-            return y;
-          })
-          .attr('class', 'id')
-          .classed('console', function(d) {
-            return QDRService.utilities.isConsole(d);
-          })
-          .classed('normal', function(d) {
-            return d.nodeType === 'normal';
-          })
-          .classed('on-demand', function(d) {
-            return d.nodeType === 'on-demand';
-          })
-          .classed('artemis', function(d) {
-            return QDRService.utilities.isArtemis(d);
-          })
-          .classed('qpid-cpp', function(d) {
-            return QDRService.utilities.isQpid(d);
-          })
-          .text(function(d) {
-            if (QDRService.utilities.isConsole(d)) {
-              return '\uf108'; // icon-desktop for this console
-            } else if (QDRService.utilities.isArtemis(d)) {
-              return '\ue900';
-            } else if (QDRService.utilities.isQpid(d)) {
-              return '\ue901';
-            } else if (d.nodeType === 'route-container') {
-              return d.properties.product ? 
d.properties.product[0].toUpperCase() : 'S';
-            } else if (d.nodeType === 'normal')
-              return '\uf109'; // icon-laptop for clients
-            return d.name.length > 7 ? d.name.substr(0, 6) + '...' : d.name;
-          });
-      };
-      let appendTitle = function(g) {
-        g.append('svg:title').text(function(d) {
-          return generateTitle(d);
-        });
-      };
+          .attr('x', radiusNormal + 3)
+          .attr('y', Math.floor(radiusNormal / 2))
+          .attr('class', 'subtext')
+          .text('x ' + d.normals.length);
+      });
+      // call createLegend in timeout because:
+      // If we create the legend right away, then it will be destroyed when 
the accordian
+      // gets initialized as the page loads.
+      $timeout(createLegend);
 
-      let generateTitle = function (d) {
-        let x = '';
-        if (d.normals && d.normals.length > 1)
-          x = ' x ' + d.normals.length;
-        if (QDRService.utilities.isConsole(d))
-          return 'Dispatch console' + x;
-        else if (QDRService.utilities.isArtemis(d))
-          return 'Broker - Artemis' + x;
-        else if (d.properties.product == 'qpid-cpp')
-          return 'Broker - qpid-cpp' + x;
-        else if (d.cdir === 'in')
-          return 'Sender' + x;
-        else if (d.cdir === 'out')
-          return 'Receiver' + x;
-        else if (d.cdir === 'both')
-          return 'Sender/Receiver' + x;
-        else if (d.nodeType === 'normal')
-          return 'client' + x;
-        else if (d.nodeType === 'on-demand')
-          return 'broker';
-        else if (d.properties.product) {
-          return d.properties.product;
-        }
-        else {
-          return '';
-        }
-      };
+      if (!mousedown_node || !selected_node)
+        return;
 
-      let showClientTooltip = function (d, event) {
-        let type = generateTitle(d);
-        let title = '<table class="popupTable"><tr><td>Type</td><td>' + type + 
'</td></tr>';
-        if (!d.normals || d.normals.length < 2)
-          title += ('<tr><td>Host</td><td>' + d.host + '</td></tr>');
-        title += '</table>';
-        showToolTip(title, event);
-      };
+      if (!start)
+        return;
+      // set the graph in motion
+      //QDRLog.debug("mousedown_node is " + mousedown_node);
+      force.start();
 
-      let showRouterTooltip = function (d, event) {
-        QDRService.management.topology.ensureEntities(d.key, [
-          {entity: 'listener', attrs: ['role', 'port', 'http']},
-          {entity: 'router', attrs: ['name', 'version', 'hostName']}
-        ], function () {
-          // update all the router title text
-          let nodes = QDRService.management.topology.nodeInfo();
-          let node = nodes[d.key];
-          let listeners = node['listener'];
-          let router = node['router'];
-          let r = QDRService.utilities.flatten(router.attributeNames, 
router.results[0]);
-          let title = '<table class="popupTable">';
-          title += ('<tr><td>Router</td><td>' + r.name + '</td></tr>');
-          if (r.hostName)
-            title += ('<tr><td>Host Name</td><td>' + r.hostHame + 
'</td></tr>');
-          title += ('<tr><td>Version</td><td>' + r.version + '</td></tr>');
-          let ports = [];
-          for (let l=0; l<listeners.results.length; l++) {
-            let listener = 
QDRService.utilities.flatten(listeners.attributeNames, listeners.results[l]);
-            if (listener.role === 'normal') {
-              ports.push(listener.port+'');
-            }
-          }
-          if (ports.length > 0) {
-            title += ('<tr><td>Ports</td><td>' + ports.join(', ') + 
'</td></tr>');
-          }
-          title += '</table>';
-          showToolTip(title, event);
+    }
+    let createLegend = function () {
+      // dynamically create the legend based on which node types are present
+      // the legend
+      d3.select('#topo_svg_legend svg').remove();
+      lsvg = d3.select('#topo_svg_legend')
+        .append('svg')
+        .attr('id', 'svglegend');
+      lsvg = lsvg.append('svg:g')
+        .attr('transform', `translate(${(radii['inter-router'] + 
2)},${(radii['inter-router'] + 2)})`)
+        .selectAll('g');
+      let legendNodes = new Nodes(QDRService, QDRLog);
+      legendNodes.addUsing('Router', '', 'inter-router', '', undefined, 0, 0, 
0, 0, false, {});
+
+      if (!svg.selectAll('circle.console').empty()) {
+        legendNodes.addUsing('Console', 'Console', 'normal', '', undefined, 0, 
0, 1, 0, false, {
+          console_identifier: 'Dispatch console'
         });
-      };
-      let showToolTip = function (title, event) {
-        // show the tooltip
-        $timeout ( function () {
-          $scope.trustedpopoverContent = $sce.trustAsHtml(title);
-          displayTooltip(event);
+      }
+      if (!svg.selectAll('circle.client.in').empty()) {
+        legendNodes.addUsing('Sender', 'Sender', 'normal', '', undefined, 0, 
0, 2, 0, false, {}).cdir = 'in';
+      }
+      if (!svg.selectAll('circle.client.out').empty()) {
+        legendNodes.addUsing('Receiver', 'Receiver', 'normal', '', undefined, 
0, 0, 3, 0, false, {}).cdir = 'out';
+      }
+      if (!svg.selectAll('circle.client.inout').empty()) {
+        legendNodes.addUsing('Sender/Receiver', 'Sender/Receiver', 'normal', 
'', undefined, 0, 0, 4, 0, false, {}).cdir = 'both';
+      }
+      if (!svg.selectAll('circle.qpid-cpp').empty()) {
+        legendNodes.addUsing('Qpid broker', 'Qpid broker', 'route-container', 
'', undefined, 0, 0, 5, 0, false, {
+          product: 'qpid-cpp'
         });
-      };
-
-      let displayTooltip = function (event) {
-        $timeout( function () {
-          let top = $('#topology').offset().top - 5;
-          let width = $('#topology').width();
-          d3.select('#popover-div')
-            .style('visibility', 'hidden')
-            .style('display', 'block')
-            .style('left', (event.pageX+5)+'px')
-            .style('top', (event.pageY-top)+'px');
-          let pwidth = $('#popover-div').width();
-          d3.select('#popover-div')
-            .style('visibility', 'visible')
-            .style('left',(Math.min(width-pwidth, event.pageX+5) + 'px'));
+      }
+      if (!svg.selectAll('circle.artemis').empty()) {
+        legendNodes.addUsing('Artemis broker', 'Artemis broker', 
'route-container', '', undefined, 0, 0, 6, 0, false,
+          {product: 'apache-activemq-artemis'});
+      }
+      if (!svg.selectAll('circle.route-container').empty()) {
+        legendNodes.addUsing('Service', 'Service', 'route-container', 
'external-service', undefined, 0, 0, 7, 0, false,
+          {product: ' External Service'});
+      }
+      lsvg = lsvg.data(legendNodes.nodes, function(d) {
+        return d.key;
+      });
+      let lg = lsvg.enter().append('svg:g')
+        .attr('transform', function(d, i) {
+          // 45px between lines and add 10px space after 1st line
+          return 'translate(0, ' + (45 * i + (i > 0 ? 10 : 0)) + ')';
         });
-      };
 
-      function nextHop(thisNode, d, cb) {
-        if ((thisNode) && (thisNode != d)) {
-          let target = findNextHopNode(thisNode, d);
-          //QDR.log.debug("highlight link from node ");
-          //console.dump(nodeFor(selected_node.name));
-          //console.dump(target);
-          if (target) {
-            let hnode = nodeFor(thisNode.name);
-            let hlLink = linkFor(hnode, target);
-            //QDR.log.debug("need to highlight");
-            //console.dump(hlLink);
-            if (hlLink) {
-              if (cb) {
-                cb(hlLink, hnode, target);
-              } else {
-                hlLink['highlighted'] = true;
-                hnode['highlighted'] = true;
-              }
-            }
-            else
-              target = null;
-          }
-          nextHop(target, d, cb);
-        }
-        if (thisNode == d && !cb) {
-          let hnode = nodeFor(thisNode.name);
-          hnode['highlighted'] = true;
+      appendCircle(lg);
+      appendContent(lg);
+      appendTitle(lg);
+      lg.append('svg:text')
+        .attr('x', 35)
+        .attr('y', 6)
+        .attr('class', 'label')
+        .text(function(d) {
+          return d.key;
+        });
+      lsvg.exit().remove();
+      let svgEl = document.getElementById('svglegend');
+      if (svgEl) {
+        let bb;
+        // firefox can throw an exception on getBBox on an svg element
+        try {
+          bb = svgEl.getBBox();
+        } catch (e) {
+          bb = {
+            y: 0,
+            height: 200,
+            x: 0,
+            width: 200
+          };
         }
+        svgEl.style.height = (bb.y + bb.height) + 'px';
+        svgEl.style.width = (bb.x + bb.width) + 'px';
       }
-
-      function hasChanged() {
-        // Don't update the underlying topology diagram if we are adding a new 
node.
-        // Once adding is completed, the topology will update automatically if 
it has changed
-        let nodeInfo = QDRService.management.topology.nodeInfo();
-        // don't count the nodes without connection info
-        let cnodes = Object.keys(nodeInfo).filter ( function (node) {
-          return (nodeInfo[node]['connection']);
+    };
+    let appendCircle = function(g) {
+      // add new circles and set their attr/class/behavior
+      return g.append('svg:circle')
+        .attr('class', 'node')
+        .attr('r', function(d) {
+          return radii[d.nodeType];
+        })
+        .attr('fill', function (d) {
+          if (d.cdir === 'both' && !QDRService.utilities.isConsole(d)) {
+            return 'url(' + urlPrefix + '#half-circle)';
+          }
+          return null;
+        })
+        .classed('fixed', function(d) {
+          return d.fixed & 1;
+        })
+        .classed('normal', function(d) {
+          return d.nodeType == 'normal' || QDRService.utilities.isConsole(d);
+        })
+        .classed('in', function(d) {
+          return d.cdir == 'in';
+        })
+        .classed('out', function(d) {
+          return d.cdir == 'out';
+        })
+        .classed('inout', function(d) {
+          return d.cdir == 'both';
+        })
+        .classed('inter-router', function(d) {
+          return d.nodeType == 'inter-router';
+        })
+        .classed('on-demand', function(d) {
+          return d.nodeType == 'on-demand';
+        })
+        .classed('console', function(d) {
+          return QDRService.utilities.isConsole(d);
+        })
+        .classed('artemis', function(d) {
+          return QDRService.utilities.isArtemis(d);
+        })
+        .classed('qpid-cpp', function(d) {
+          return QDRService.utilities.isQpid(d);
+        })
+        .classed('route-container', function (d) {
+          return (!QDRService.utilities.isArtemis(d) && 
!QDRService.utilities.isQpid(d) && d.nodeType === 'route-container');
+        })
+        .classed('client', function(d) {
+          return d.nodeType === 'normal' && !d.properties.console_identifier;
         });
-        let routers = nodes.filter( function (node) {
-          return node.nodeType === 'inter-router';
+    };
+    let appendContent = function(g) {
+      // show node IDs
+      g.append('svg:text')
+        .attr('x', 0)
+        .attr('y', function(d) {
+          let y = 7;
+          if (QDRService.utilities.isArtemis(d))
+            y = 8;
+          else if (QDRService.utilities.isQpid(d))
+            y = 9;
+          else if (d.nodeType === 'inter-router')
+            y = 4;
+          else if (d.nodeType === 'route-container')
+            y = 5;
+          return y;
+        })
+        .attr('class', 'id')
+        .classed('console', function(d) {
+          return QDRService.utilities.isConsole(d);
+        })
+        .classed('normal', function(d) {
+          return d.nodeType === 'normal';
+        })
+        .classed('on-demand', function(d) {
+          return d.nodeType === 'on-demand';
+        })
+        .classed('artemis', function(d) {
+          return QDRService.utilities.isArtemis(d);
+        })
+        .classed('qpid-cpp', function(d) {
+          return QDRService.utilities.isQpid(d);
+        })
+        .text(function(d) {
+          if (QDRService.utilities.isConsole(d)) {
+            return '\uf108'; // icon-desktop for this console
+          } else if (QDRService.utilities.isArtemis(d)) {
+            return '\ue900';
+          } else if (QDRService.utilities.isQpid(d)) {
+            return '\ue901';
+          } else if (d.nodeType === 'route-container') {
+            return d.properties.product ? 
d.properties.product[0].toUpperCase() : 'S';
+          } else if (d.nodeType === 'normal')
+            return '\uf109'; // icon-laptop for clients
+          return d.name.length > 7 ? d.name.substr(0, 6) + '...' : d.name;
         });
-        if (routers.length > cnodes.length) {
-          return -1;
-        }
-
-
-        if (cnodes.length != Object.keys(savedKeys).length) {
-          return cnodes.length > Object.keys(savedKeys).length ? 1 : -1;
-        }
-        // we may have dropped a node and added a different node in the same 
update cycle
-        for (let i=0; i<cnodes.length; i++) {
-          let key = cnodes[i];
-          // if this node isn't in the saved node list
-          if (!savedKeys.hasOwnProperty(key))
-            return 1;
-          // if the number of connections for this node chaanged
-          if (!nodeInfo[key]['connection'])
-            return -1;
-          if (nodeInfo[key]['connection'].results.length != savedKeys[key]) {
-            return -1;
+    };
+    let appendTitle = function(g) {
+      g.append('svg:title').text(function(d) {
+        return generateTitle(d);
+      });
+    };
+
+    let generateTitle = function (d) {
+      let x = '';
+      if (d.normals && d.normals.length > 1)
+        x = ' x ' + d.normals.length;
+      if (QDRService.utilities.isConsole(d))
+        return 'Dispatch console' + x;
+      else if (QDRService.utilities.isArtemis(d))
+        return 'Broker - Artemis' + x;
+      else if (d.properties.product == 'qpid-cpp')
+        return 'Broker - qpid-cpp' + x;
+      else if (d.cdir === 'in')
+        return 'Sender' + x;
+      else if (d.cdir === 'out')
+        return 'Receiver' + x;
+      else if (d.cdir === 'both')
+        return 'Sender/Receiver' + x;
+      else if (d.nodeType === 'normal')
+        return 'client' + x;
+      else if (d.nodeType === 'on-demand')
+        return 'broker';
+      else if (d.properties.product) {
+        return d.properties.product;
+      }
+      else {
+        return '';
+      }
+    };
+
+    let showClientTooltip = function (d, event) {
+      let type = generateTitle(d);
+      let title = `<table 
class="popupTable"><tr><td>Type</td><td>${type}</td></tr>`;
+      if (!d.normals || d.normals.length < 2)
+        title += ('<tr><td>Host</td><td>' + d.host + '</td></tr>');
+      title += '</table>';
+      showToolTip(title, event);
+    };
+
+    let showRouterTooltip = function (d, event) {
+      QDRService.management.topology.ensureEntities(d.key, [
+        {entity: 'listener', attrs: ['role', 'port', 'http']},
+        {entity: 'router', attrs: ['name', 'version', 'hostName']}
+      ], function () {
+        // update all the router title text
+        let nodes = QDRService.management.topology.nodeInfo();
+        let node = nodes[d.key];
+        let listeners = node['listener'];
+        let router = node['router'];
+        let r = QDRService.utilities.flatten(router.attributeNames, 
router.results[0]);
+        let title = '<table class="popupTable">';
+        title += ('<tr><td>Router</td><td>' + r.name + '</td></tr>');
+        if (r.hostName)
+          title += ('<tr><td>Host Name</td><td>' + r.hostHame + '</td></tr>');
+        title += ('<tr><td>Version</td><td>' + r.version + '</td></tr>');
+        let ports = [];
+        for (let l=0; l<listeners.results.length; l++) {
+          let listener = 
QDRService.utilities.flatten(listeners.attributeNames, listeners.results[l]);
+          if (listener.role === 'normal') {
+            ports.push(listener.port+'');
           }
         }
-        return 0;
-      }
-
-      function saveChanged() {
-        savedKeys = {};
-        let nodeInfo = QDRService.management.topology.nodeInfo();
-        // save the number of connections per node
-        for (let key in nodeInfo) {
-          if (nodeInfo[key]['connection'])
-            savedKeys[key] = nodeInfo[key]['connection'].results.length;
+        if (ports.length > 0) {
+          title += ('<tr><td>Ports</td><td>' + ports.join(', ') + 
'</td></tr>');
         }
-      }
-      // we are about to leave the page, save the node positions
-      $rootScope.$on('$locationChangeStart', function() {
-        //QDR.log.debug("locationChangeStart");
-        savePositions();
+        title += '</table>';
+        showToolTip(title, event);
       });
-      // When the DOM element is removed from the page,
-      // AngularJS will trigger the $destroy event on
-      // the scope
-      $scope.$on('$destroy', function() {
-        //QDR.log.debug("scope on destroy");
-        savePositions();
-        QDRService.management.topology.setUpdateEntities([]);
-        QDRService.management.topology.stopUpdating();
-        QDRService.management.topology.delUpdatedAction('normalsStats');
-        QDRService.management.topology.delUpdatedAction('topology');
-        QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
-
-        d3.select('#SVG_ID').remove();
-        window.removeEventListener('resize', resize);
-        traffic.stop();
+    };
+    let showToolTip = function (title, event) {
+      // show the tooltip
+      $timeout ( function () {
+        $scope.trustedpopoverContent = $sce.trustAsHtml(title);
+        displayTooltip(event);
       });
+    };
+
+    let displayTooltip = function (event) {
+      $timeout( function () {
+        let top = $('#topology').offset().top - 5;
+        let width = $('#topology').width();
+        d3.select('#popover-div')
+          .style('visibility', 'hidden')
+          .style('display', 'block')
+          .style('left', (event.pageX+5)+'px')
+          .style('top', (event.pageY-top)+'px');
+        let pwidth = $('#popover-div').width();
+        d3.select('#popover-div')
+          .style('visibility', 'visible')
+          .style('left',(Math.min(width-pwidth, event.pageX+5) + 'px'));
+      });
+    };
+
+    function hasChanged() {
+      // Don't update the underlying topology diagram if we are adding a new 
node.
+      // Once adding is completed, the topology will update automatically if 
it has changed
+      let nodeInfo = QDRService.management.topology.nodeInfo();
+      // don't count the nodes without connection info
+      let cnodes = Object.keys(nodeInfo).filter ( function (node) {
+        return (nodeInfo[node]['connection']);
+      });
+      let routers = nodes.nodes.filter( function (node) {
+        return node.nodeType === 'inter-router';
+      });
+      if (routers.length > cnodes.length) {
+        return -1;
+      }
 
-      function handleInitialUpdate() {
-        // we only need to update connections during steady-state
-        QDRService.management.topology.setUpdateEntities(['connection']);
-        // we currently have all entities available on all routers
-        save

<TRUNCATED>

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org
For additional commands, e-mail: commits-h...@qpid.apache.org

Reply via email to