You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ea...@apache.org on 2018/12/10 16:59:23 UTC

[1/3] qpid-dispatch git commit: DISPATCH-1217 Treat node.fixed as bitmap

Repository: qpid-dispatch
Updated Branches:
  refs/heads/master e9b7bd5ae -> 81e58b462


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/js/topology/traffic.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/traffic.js b/console/stand-alone/plugin/js/topology/traffic.js
index ff46d63..9a1d2e0 100644
--- a/console/stand-alone/plugin/js/topology/traffic.js
+++ b/console/stand-alone/plugin/js/topology/traffic.js
@@ -19,9 +19,18 @@ under the License.
 
 /* global d3 Promise */
 
-import { ChordData } from "../chord/data.js";
-import { MIN_CHORD_THRESHOLD } from "../chord/matrix.js";
-import { nextHop } from "./topoUtils.js";
+import {
+  ChordData
+} from "../chord/data.js";
+import {
+  MIN_CHORD_THRESHOLD
+} from "../chord/matrix.js";
+import {
+  nextHop
+} from "./topoUtils.js";
+import {
+  utils
+} from "../amqp/utilities.js";
 
 const transitionDuration = 1000;
 const CHORDFILTERKEY = "chordFilter";
@@ -71,9 +80,9 @@ export class Traffic {
     this.remove();
     this.type = type;
     this.vis =
-      type === "dots"
-        ? new Dots(this, converter, radius)
-        : new Congestion(this);
+      type === "dots" ?
+        new Dots(this, converter, radius) :
+        new Congestion(this);
   }
   // called periodically to refresh the traffic flow
   doUpdate() {
@@ -95,7 +104,7 @@ class TrafficAnimation {
     for (let i = 0; i < nodes.length; i++) {
       let node = nodes[i];
       if (node.normals) {
-        let normalIndex = node.normals.findIndex(function(normal) {
+        let normalIndex = node.normals.findIndex(function (normal) {
           return normal.container === name;
         });
         if (normalIndex >= 0) return i;
@@ -127,7 +136,7 @@ class Congestion extends TrafficAnimation {
     if (attrIndex >= 0) {
       for (let i = 0; i < node[entity].results.length; i++) {
         if (node[entity].results[i][attrIndex] === value) {
-          return this.traffic.QDRService.utilities.flatten(
+          return utils.flatten(
             node[entity].attributeNames,
             node[entity].results[i]
           );
@@ -139,8 +148,13 @@ class Congestion extends TrafficAnimation {
   doUpdate() {
     let self = this;
     this.traffic.QDRService.management.topology.ensureAllEntities(
-      [{ entity: "router.link", force: true }, { entity: "connection" }],
-      function() {
+      [{
+        entity: "router.link",
+        force: true
+      }, {
+        entity: "connection"
+      }],
+      function () {
         let links = {};
         let nodeInfo = self.traffic.QDRService.management.topology.nodeInfo();
         const nodes = self.traffic.topology.nodes.nodes;
@@ -194,14 +208,18 @@ class Congestion extends TrafficAnimation {
             let dir = path.attr("marker-end") === "" ? "start" : "end";
             let small = path.attr("class").indexOf("small") > -1;
             let id = dir + "-" + congestion.substr(1) + (small ? "-s" : "");
-            colors[id] = { dir: dir, color: congestion, small: small };
+            colors[id] = {
+              dir: dir,
+              color: congestion,
+              small: small
+            };
             path
               .classed("traffic", true)
-              .attr("marker-start", function() {
+              .attr("marker-start", function () {
                 return null;
                 //return d.left ? 'url(' + self.traffic.prefix + '#' + id + ')' : null;
               })
-              .attr("marker-end", function() {
+              .attr("marker-end", function () {
                 return null;
                 //return d.right ? 'url(' + self.traffic.prefix + '#' + id + ')' : null;
               });
@@ -216,31 +234,31 @@ class Congestion extends TrafficAnimation {
         let colorKeys = Object.keys(colors);
         let custom_markers = self.custom_markers_def
           .selectAll("marker")
-          .data(colorKeys, function(d) {
+          .data(colorKeys, function (d) {
             return d;
           });
         custom_markers
           .enter()
           .append("svg:marker")
-          .attr("id", function(d) {
+          .attr("id", function (d) {
             return d;
           })
           .attr("viewBox", "0 -5 10 10")
-          .attr("refX", function(d) {
+          .attr("refX", function (d) {
             return colors[d].dir === "end" ? 24 : colors[d].small ? -24 : -14;
           })
           .attr("markerWidth", 14)
           .attr("markerHeight", 14)
           .attr("markerUnits", "userSpaceOnUse")
           .attr("orient", "auto")
-          .style("fill", function(d) {
+          .style("fill", function (d) {
             return colors[d].color;
           })
           .append("svg:path")
-          .attr("d", function(d) {
-            return colors[d].dir === "end"
-              ? "M 0 -5 L 10 0 L 0 5 z"
-              : "M 10 -5 L 0 0 L 10 5 z";
+          .attr("d", function (d) {
+            return colors[d].dir === "end" ?
+              "M 0 -5 L 10 0 L 0 5 z" :
+              "M 10 -5 L 0 0 L 10 5 z";
           });
         custom_markers.exit().remove();
       }
@@ -263,7 +281,7 @@ class Congestion extends TrafficAnimation {
       .domain([0, 1, 2, 3])
       .interpolate(d3.interpolateHcl)
       .range([
-        d3.rgb("#000000"),
+        d3.rgb("#999999"),
         d3.rgb("#00FF00"),
         d3.rgb("#FFA500"),
         d3.rgb("#FF0000")
@@ -286,9 +304,9 @@ class Congestion extends TrafficAnimation {
 class Dots extends TrafficAnimation {
   constructor(traffic, converter, radius) {
     super(traffic);
-    this.excludedAddresses = localStorage[CHORDFILTERKEY]
-      ? JSON.parse(localStorage[CHORDFILTERKEY])
-      : [];
+    this.excludedAddresses = localStorage[CHORDFILTERKEY] ?
+      JSON.parse(localStorage[CHORDFILTERKEY]) :
+      [];
     this.radius = radius; // the radius of a router circle
     this.lastFlows = {}; // the number of dots animated between routers
     this.stopped = false;
@@ -296,9 +314,9 @@ class Dots extends TrafficAnimation {
     this.chordData.setFilter(this.excludedAddresses);
     traffic.$scope.addresses = {};
     this.chordData.getMatrix().then(
-      function() {
+      function () {
         this.traffic.$timeout(
-          function() {
+          function () {
             this.traffic.$scope.addresses = this.chordData.getAddresses();
             for (let address in this.traffic.$scope.addresses) {
               this.fillColor(address);
@@ -314,25 +332,25 @@ class Dots extends TrafficAnimation {
     }
     let self = this;
     // event notification that an address checkbox has changed
-    traffic.$scope.addressFilterChanged = function() {
-      self.updateAddresses().then(function() {
+    traffic.$scope.addressFilterChanged = function () {
+      self.updateAddresses().then(function () {
         // don't wait for the next polling cycle. update now
         self.traffic.stop();
         self.traffic.start();
       });
     };
     // called by angular when mouse enters one of the address legends
-    traffic.$scope.enterLegend = function(address) {
+    traffic.$scope.enterLegend = function (address) {
       // fade all flows that aren't for this address
       self.fadeOtherAddresses(address);
     };
     // called when the mouse leaves one of the address legends
-    traffic.$scope.leaveLegend = function() {
+    traffic.$scope.leaveLegend = function () {
       self.unFadeAll();
     };
     // clicked on the address name. toggle the address checkbox
-    traffic.$scope.addressClick = function(address) {
-      self.toggleAddress(address).then(function() {
+    traffic.$scope.addressClick = function (address) {
+      self.toggleAddress(address).then(function () {
         self.updateAddresses();
       });
     };
@@ -352,7 +370,7 @@ class Dots extends TrafficAnimation {
     }
     localStorage[CHORDFILTERKEY] = JSON.stringify(this.excludedAddresses);
     if (this.chordData) this.chordData.setFilter(this.excludedAddresses);
-    return new Promise(function(resolve) {
+    return new Promise(function (resolve) {
       return resolve();
     });
   }
@@ -360,12 +378,12 @@ class Dots extends TrafficAnimation {
     this.traffic.$scope.addresses[address] = !this.traffic.$scope.addresses[
       address
     ];
-    return new Promise(function(resolve) {
+    return new Promise(function (resolve) {
       return resolve();
     });
   }
   fadeOtherAddresses(address) {
-    d3.selectAll("circle.flow").classed("fade", function(d) {
+    d3.selectAll("circle.flow").classed("fade", function (d) {
       return d.address !== address;
     });
   }
@@ -377,10 +395,13 @@ class Dots extends TrafficAnimation {
     this.stopped = false;
     // we need the nextHop data to show traffic between routers that are connected by intermediaries
     this.traffic.QDRService.management.topology.ensureAllEntities(
-      [{ entity: "router.node", attrs: ["id", "nextHop"] }],
-      function() {
+      [{
+        entity: "router.node",
+        attrs: ["id", "nextHop"]
+      }],
+      function () {
         // get the ingressHistogram data for all routers
-        self.chordData.getMatrix().then(self.render.bind(self), function(e) {
+        self.chordData.getMatrix().then(self.render.bind(self), function (e) {
           console.log("Could not get message histogram" + e);
         });
       }
@@ -389,7 +410,7 @@ class Dots extends TrafficAnimation {
   render(matrix) {
     if (this.stopped === false) {
       this.traffic.$timeout(
-        function() {
+        function () {
           this.traffic.$scope.addresses = this.chordData.getAddresses();
         }.bind(this)
       );
@@ -404,9 +425,9 @@ class Dots extends TrafficAnimation {
         .range([1, 1.1]);
       // row is ingress router, col is egress router. Value at [row][col] is the rate
       matrixMessages.forEach(
-        function(row, r) {
+        function (row, r) {
           row.forEach(
-            function(val, c) {
+            function (val, c) {
               if (val > MIN_CHORD_THRESHOLD) {
                 // translate between matrix row/col and node index
                 let f = this.nodeIndexFor(
@@ -425,9 +446,9 @@ class Dots extends TrafficAnimation {
                     this.traffic.topology.nodes.nodes[t],
                     this.traffic.topology.nodes,
                     this.traffic.topology.links,
-                    this.traffic.QDRService,
+                    this.traffic.QDRService.management.topology.nodeInfo(),
                     this.traffic.topology.nodes.nodes[f],
-                    function(link, fnode, tnode) {
+                    function (link, fnode, tnode) {
                       let key = "-" + link.uid;
                       let back = fnode.index < tnode.index;
                       if (!hops[key]) hops[key] = [];
@@ -520,7 +541,10 @@ class Dots extends TrafficAnimation {
     let len = Math.max(Math.floor(path.node().getTotalLength() / 50), 1);
     let dots = [];
     for (let i = 0, offset = this.addressIndex(this, address); i < len; ++i) {
-      dots[i] = { i: i + 10 * offset, address: address };
+      dots[i] = {
+        i: i + 10 * offset,
+        address: address
+      };
     }
     // keep track of the number of dots for each link. If the length of the link is changed,
     // re-create the animation
@@ -536,7 +560,7 @@ class Dots extends TrafficAnimation {
     let flow = d3
       .select("#SVG_ID")
       .selectAll("circle.flow" + id)
-      .data(dots, function(d) {
+      .data(dots, function (d) {
         return d.i + d.address;
       });
     let circles = flow
@@ -573,10 +597,10 @@ class Dots extends TrafficAnimation {
       const ioa = links.attributeNames.indexOf("owningAddr");
       const ici = links.attributeNames.indexOf("connectionId");
       const ild = links.attributeNames.indexOf("linkDir");
-      let foundLinks = links.results.filter(function(l) {
+      let foundLinks = links.results.filter(function (l) {
         return (
           (l[ilt] === "endpoint" || l[ilt] === "edge-downlink") &&
-          address === this.traffic.QDRService.utilities.addr_text(l[ioa]) &&
+          address === utils.addr_text(l[ioa]) &&
           l[ild] === cdir
         );
       }, this);
@@ -585,19 +609,23 @@ class Dots extends TrafficAnimation {
       // Now find the created node that each link is associated with
       for (let linkIndex = 0; linkIndex < foundLinks.length; linkIndex++) {
         // use .some so the loop stops at the 1st match
-        nodes.some( function (node) {
+        nodes.some(function (node) {
           if (
             node.normals &&
-            node.normals.some(function(normal) {
+            node.normals.some(function (normal) {
               return testNode(normal, key, cdir, foundLinks[linkIndex][ici]);
             })
           ) {
             // one of the normals for this node has the traffic
             const uuid2 = node.uid();
             const key = ["", uuid, uuid2].join("-");
-            if (!hops[key]) 
+            if (!hops[key])
               hops[key] = [];
-            hops[key].push({ val: val, back: !sender, address: address });
+            hops[key].push({
+              val: val,
+              back: !sender,
+              address: address
+            });
             return true;
           }
           return false;
@@ -612,9 +640,9 @@ class Dots extends TrafficAnimation {
   translateDots(radius, path, count, back) {
     let pnode = path.node();
     // will be called for each element in the flow selection (for each dot)
-    return function(d) {
+    return function (d) {
       // will be called with t going from 0 to 1 for each dot
-      return function(t) {
+      return function (t) {
         // start the points at different positions depending on their value (d)
         let tt = t * 1000;
         let f = ((tt + (d.i * 1000) / count) % 1000) / 1000;
@@ -630,7 +658,7 @@ class Dots extends TrafficAnimation {
 
 // see if this node, or any of the nodes it also connects to
 // match the key, dir, and connectionId
-let testNode = function(node, key, dir, connectionId) {
+let testNode = function (node, key, dir, connectionId) {
   // does the node match
   if (
     node.key === key &&
@@ -638,10 +666,10 @@ let testNode = function(node, key, dir, connectionId) {
     (node.cdir === dir || node.cdir === "both")
   )
     return true;
-  if (!node.alsoConnectsTo) 
+  if (!node.alsoConnectsTo)
     return false;
   // do any of the alsoConnectsTo nodes match
-  return node.alsoConnectsTo.some(function(ac2) {
+  return node.alsoConnectsTo.some(function (ac2) {
     return testNode(ac2, key, dir, connectionId);
   });
-};
+};
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/test/links.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/test/links.js b/console/stand-alone/test/links.js
index ef8ea44..5bc3d01 100644
--- a/console/stand-alone/test/links.js
+++ b/console/stand-alone/test/links.js
@@ -26,15 +26,13 @@ import { Nodes } from '../plugin/js/topology/nodes.js';
 class Log {
   constructor() {
   }
-  log (msg) {console.log(msg);}
-  debug (msg) {console.log(`Debug: ${msg}`);}
-  error (msg) {console.log(`Error: ${msg}`);}
-  info (msg) {console.log(`Info: ${msg}`);}
-  warn (msg) {console.log(`Warning: ${msg}`);}
+  log(msg) { console.log(msg); }
+  debug(msg) { console.log(`Debug: ${msg}`); }
+  error(msg) { console.log(`Error: ${msg}`); }
+  info(msg) { console.log(`Info: ${msg}`); }
+  warn(msg) { console.log(`Warning: ${msg}`); }
 }
 var log = new Log();
-var loc = {protocol: function () { return 'http://';}};
-var timeout = {};
 var links = new Links(log);
 var edgeLinks = new Links(log);
 var nodes = new Nodes(log);
@@ -46,61 +44,61 @@ const width = 1024;
 const height = 768;
 
 
-before(function(done){
+before(function (done) {
   let src = '';
-  let LAST_PARAM = process.argv[process.argv.length-1];
+  let LAST_PARAM = process.argv[process.argv.length - 1];
 
-  let PARAM_NAME  = LAST_PARAM.split('=')[0].replace('--','');
+  let PARAM_NAME = LAST_PARAM.split('=')[0].replace('--', '');
   let PARAM_VALUE = LAST_PARAM.split('=')[1];
   if (PARAM_NAME == 'src') {
     src = PARAM_VALUE;
   }
-  
-  fs.readFile(src + './test/nodes.json', 'utf8', function(err, fileContents) {
+
+  fs.readFile(src + './test/nodes.json', 'utf8', function (err, fileContents) {
     if (err) throw err;
     nodeInfo = JSON.parse(fileContents);
   });
-  fs.readFile(src + './test/nodes-edge.json', 'utf8', function(err, fileContents) {
+  fs.readFile(src + './test/nodes-edge.json', 'utf8', function (err, fileContents) {
     if (err) throw err;
     edgeInfo = JSON.parse(fileContents);
     done();
   });
 });
 
-describe('Nodes', function() {
-  describe('#exists', function() {
-    it('should exist', function() {
+describe('Nodes', function () {
+  describe('#exists', function () {
+    it('should exist', function () {
       expect(nodes).to.exist;
     });
   });
-  describe('#initializes', function() {
-    it('should initialize', function() {
-      nodes.initialize(nodeInfo, {}, width, height);
+  describe('#initializes', function () {
+    it('should initialize', function () {
+      nodes.initialize(nodeInfo, {}, width, height, {});
       assert.equal(nodes.nodes.length, 6);
     });
-    it('should initialize edge nodes', function() {
-      edgeNodes.initialize(edgeInfo, {}, width, height);
+    it('should initialize edge nodes', function () {
+      edgeNodes.initialize(edgeInfo, {}, width, height, {});
       assert.equal(edgeNodes.nodes.length, 2);
     });
   });
 
 });
-describe('Links', function() {
-  describe('#exists', function() {
-    it('should exist', function() {
+describe('Links', function () {
+  describe('#exists', function () {
+    it('should exist', function () {
       expect(links).to.exist;
     });
   });
-  describe('#initializes', function() {
-    it('should initialize', function() {
+  describe('#initializes', function () {
+    it('should initialize', function () {
       links.initialize(nodeInfo, nodes, unknowns, {}, width);
       assert.equal(links.links.length, 10);
     });
-    it('should initialize edge links', function() {
+    it('should initialize edge links', function () {
       edgeLinks.initialize(edgeInfo, edgeNodes, unknowns, {}, width);
       assert.equal(edgeLinks.links.length, 6);
     });
-    it('should add nodes for edge router groups', function() {
+    it('should add nodes for edge router groups', function () {
       assert.equal(edgeNodes.nodes.length, 6);
     });
   });


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


[2/3] qpid-dispatch git commit: DISPATCH-1217 Treat node.fixed as bitmap

Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/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 54168ec..4ada081 100644
--- a/console/stand-alone/plugin/js/topology/qdrTopology.js
+++ b/console/stand-alone/plugin/js/topology/qdrTopology.js
@@ -21,60 +21,95 @@ under the License.
 /**
  * @module QDR
  */
-import { QDRLogger, QDRRedirectWhenConnected, QDRTemplatePath } 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, addStyles } from './topoUtils.js';
-import { BackgroundMap } from './map.js';
+import { QDRLogger, QDRRedirectWhenConnected, QDRTemplatePath } 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, getSizes } from "./topoUtils.js";
+import { BackgroundMap } from "./map.js";
+import { utils } from "../amqp/utilities.js";
+import { Legend } from "./legend.js";
+import { appendCircle, appendContent, addGradient, addDefs, updateState } from "./svgUtils.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';
+  constructor(
+    QDRService,
+    $scope,
+    $log,
+    $rootScope,
+    $location,
+    $timeout,
+    $uibModal,
+    $sce
+  ) {
+    this.controllerName = "QDR.TopologyController";
+
+    let QDRLog = new QDRLogger($log, "TopologyController");
+    const TOPOOPTIONSKEY = "topoOptions";
 
     //  - 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(QDRLog);
-    let links = new Links(QDRLog);
-    let forceData = {nodes: nodes, links: links};
+    let forceData = {
+      nodes: new Nodes(QDRLog),
+      links: new Links(QDRLog)
+    };
 
-    $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || {showTraffic: false, trafficType: 'dots', mapOpen: false, legendOpen: true};
-    if (typeof $scope.legendOptions.mapOpen == 'undefined')
+    // restore the state of the legend sections
+    $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || {
+      showTraffic: false,
+      trafficType: "dots",
+      mapOpen: false,
+      legendOpen: true
+    };
+    if (typeof $scope.legendOptions.mapOpen == "undefined")
       $scope.legendOptions.mapOpen = false;
-    if (typeof $scope.legendOptions.legendOpen == 'undefined')
+    if (typeof $scope.legendOptions.legendOpen == "undefined")
       $scope.legendOptions.legendOpen = false;
-    let backgroundMap = new BackgroundMap($scope, 
+    let backgroundMap = new BackgroundMap(
+      $scope,
       // notify: called each time a pan/zoom is performed
       function () {
         if ($scope.legend.status.mapOpen) {
           // set all the nodes' x,y position based on their saved lon,lat
-          nodes.setXY(backgroundMap);
-          nodes.savePositions();
+          forceData.nodes.setXY(backgroundMap);
+          forceData.nodes.savePositions();
           // redraw the nodes in their x,y position and let non-fixed nodes bungie
           force.start();
           clearPopups();
         }
-      });
+      }
+    );
     // urlPrefix is used when referring to svg:defs
     let urlPrefix = $location.absUrl();
-    urlPrefix = urlPrefix.split('#')[0];
+    urlPrefix = urlPrefix.split("#")[0];
 
     if (!$scope.legendOptions.trafficType)
-      $scope.legendOptions.trafficType = 'dots';
-    $scope.legend = {status: {legendOpen: true, optionsOpen: true, mapOpen: false}};
+      $scope.legendOptions.trafficType = "dots";
+    $scope.legend = {
+      status: {
+        legendOpen: true,
+        optionsOpen: true,
+        mapOpen: false
+      }
+    };
     $scope.legend.status.optionsOpen = $scope.legendOptions.showTraffic;
     $scope.legend.status.mapOpen = $scope.legendOptions.mapOpen;
-    let traffic = new Traffic($scope, $timeout, QDRService, separateAddresses, 
-      Nodes.radius('inter-router'), forceData, $scope.legendOptions.trafficType, urlPrefix);
+    let traffic = new Traffic(
+      $scope,
+      $timeout,
+      QDRService,
+      separateAddresses,
+      Nodes.radius("inter-router"),
+      forceData,
+      $scope.legendOptions.trafficType,
+      urlPrefix
+    );
 
     // the showTraaffic checkbox was just toggled (or initialized)
-    $scope.$watch('legend.status.optionsOpen', function () {
+    $scope.$watch("legend.status.optionsOpen", function () {
       $scope.legendOptions.showTraffic = $scope.legend.status.optionsOpen;
       localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
       if ($scope.legend.status.optionsOpen) {
@@ -85,15 +120,21 @@ export class TopologyController {
         restart();
       }
     });
-    $scope.$watch('legendOptions.trafficType', function () {
+    // the traffic type was just changed or initialized
+    $scope.$watch("legendOptions.trafficType", function () {
       localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
       if ($scope.legendOptions.showTraffic) {
         restart();
-        traffic.setAnimationType($scope.legendOptions.trafficType, separateAddresses, Nodes.radius('inter-router'));
+        traffic.setAnimationType(
+          $scope.legendOptions.trafficType,
+          separateAddresses,
+          Nodes.radius("inter-router")
+        );
         traffic.start();
       }
     });
-    $scope.$watch('legend.status.mapOpen', function (newvalue, oldvalue) {
+    // the background map was shown or hidden
+    $scope.$watch("legend.status.mapOpen", function (newvalue, oldvalue) {
       $scope.legendOptions.mapOpen = $scope.legend.status.mapOpen;
       localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
       // map was shown
@@ -102,16 +143,12 @@ export class TopologyController {
         backgroundMap.restartZoom();
         // set the main_container div's background color to the ocean color
         backgroundMap.updateOceanColor();
-        d3.select('g.geo')
-          .style('opacity', 1);
+        d3.select("g.geo").style("opacity", 1);
       } else {
-        if (newvalue !== oldvalue)
-          backgroundMap.cancelZoom();
+        if (newvalue !== oldvalue) backgroundMap.cancelZoom();
         // hide the map and reset the background color
-        d3.select('g.geo')
-          .style('opacity', 0);
-        d3.select('#main_container')
-          .style('background-color', '#FFF');
+        d3.select("g.geo").style("opacity", 0);
+        d3.select("#main_container").style("background-color", "#FFF");
       }
     });
 
@@ -121,37 +158,38 @@ export class TopologyController {
       mouseup_node = null,
       initial_mouse_down_position = null;
 
-    $scope.schema = 'Not connected';
-    $scope.current_node = null,
-    $scope.mousedown_node = null,
-
+    $scope.schema = "Not connected";
+    $scope.current_node = null;
+    $scope.mousedown_node = null;
     $scope.contextNode = null; // node that is associated with the current context menu
-    $scope.isRight = function(mode) {
+    $scope.isRight = function (mode) {
       return mode.right;
     };
 
+    // show the details dialog for a client or group of clients
     function doDialog(d) {
-      $uibModal.open({
-        backdrop: true,
-        keyboard: true,
-        backdropClick: true,
-        templateUrl: QDRTemplatePath + 'tmplClientDetail.html',
-        controller: 'QDR.DetailDialogController',
-        resolve: {
-          d: function() {
-            return d;
+      $uibModal
+        .open({
+          backdrop: true,
+          keyboard: true,
+          backdropClick: true,
+          templateUrl: QDRTemplatePath + "tmplClientDetail.html",
+          controller: "QDR.DetailDialogController",
+          resolve: {
+            d: function () {
+              return d;
+            }
           }
-        }
-      }).result.then(function() {
-      });
+        })
+        .result.then(function () { });
     }
 
     // called from the html page's popup menu
-    $scope.setFixed = function(b) {
+    $scope.setFixed = function (b) {
       if ($scope.contextNode) {
-        $scope.contextNode.setFixed(b);
-        nodes.savePositions();
-        nodes.saveLonLat(backgroundMap, $scope.contextNode);
+        forceData.nodes.setFixed($scope.contextNode, b);
+        forceData.nodes.savePositions();
+        forceData.nodes.saveLonLat(backgroundMap, $scope.contextNode);
       }
       // redraw the circles/links
       restart();
@@ -159,20 +197,25 @@ export class TopologyController {
       if (!b) {
         // let the nodes move to a new position
         animate = true;
-        force.start(); 
+        force.start();
       }
     };
-    $scope.isFixed = function() {
-      if (!$scope.contextNode)
-        return false;
-      return ($scope.contextNode.fixed);
+    $scope.isFixed = function () {
+      if (!$scope.contextNode) return false;
+      return $scope.contextNode.fixed;
+    };
+    $scope.addressStyle = function (address) {
+      return {
+        "background-color": $scope.addressColors[address]
+      };
     };
 
     let mouseX, mouseY;
     var relativeMouse = function () {
-      let offset = $('#main_container').offset();
-      return {left: (mouseX + $(document).scrollLeft()) - 1,
-        top: (mouseY  + $(document).scrollTop()) - 1,
+      let offset = $("#main_container").offset();
+      return {
+        left: mouseX + $(document).scrollLeft() - 1,
+        top: mouseY + $(document).scrollTop() - 1,
         offset: offset
       };
     };
@@ -182,66 +225,50 @@ export class TopologyController {
       mouseY = e.clientY;
     });
     $(document).mousemove();
-    $(document).click(function() {
+    $(document).click(function () {
       $scope.contextNode = null;
-      $('.contextMenu').fadeOut(200);
+      $(".contextMenu").fadeOut(200);
     });
 
-    let svg, lsvg;  // main svg and legend svg
+    let svg; // main svg
     let force;
     let animate = false; // should the force graph organize itself when it is displayed
-    let path, circle;
-    let savedKeys = {};
+    let path, circle;   // the d3 selections for links and nodes respectively
+    let savedKeys = {}; // so we can redraw the svg if the topology changes
     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();
+    var resize = function () {
+      if (!svg) return;
+      let sizes = getSizes(QDRLog);
       width = sizes[0];
       height = sizes[1];
       if (width > 0) {
         // set attrs and 'resume' force
-        svg.attr('width', width);
-        svg.attr('height', height);
+        svg.attr("width", width);
+        svg.attr("height", height);
         force.size(sizes).resume();
       }
-      $timeout(createLegend);
+      $timeout(updateLegend);
     };
 
     // the window is narrow and the page menu icon was clicked.
     // Re-create the legend
-    $scope.$on('pageMenuClicked', function () {
-      $timeout(createLegend);
+    $scope.$on("pageMenuClicked", function () {
+      $timeout(updateLegend);
     });
 
-    window.addEventListener('resize', resize);
-    let sizes = getSizes();
+    window.addEventListener("resize", resize);
+    let sizes = getSizes(QDRLog);
     width = sizes[0];
     height = sizes[1];
-    if (width <= 0 || height <= 0)
-      return;
+    if (width <= 0 || height <= 0) return;
 
     // initialize the nodes and links array from the QDRService.topology._nodeInfo object
-    var initForceGraph = function() {
-      forceData.nodes = nodes = new Nodes(QDRLog);
-      forceData.links = links = new Links(QDRLog);
+    var initForceGraph = function () {
+      if (width < 768) {
+        $scope.legend.status.mapOpen = false;
+      }
       let nodeInfo = QDRService.management.topology.nodeInfo();
       let nodeCount = Object.keys(nodeInfo).length;
 
@@ -250,110 +277,78 @@ export class TopologyController {
       mouseover_node = null;
       selected_node = null;
 
-      d3.select('#SVG_ID').remove();
-      svg = d3.select('#topology')
-        .append('svg')
-        .attr('id', 'SVG_ID')
-        .attr('width', width)
-        .attr('height', height)
-        .on('click', function () {
-          clearPopups();
+      if (d3.select("#SVG_ID").empty()) {
+        svg = d3
+          .select("#topology")
+          .append("svg")
+          .attr("id", "SVG_ID")
+          .attr("width", width)
+          .attr("height", height)
+          .on("click", function () {
+            clearPopups();
+          });
+        // read the map data from the data file and build the map layer
+        backgroundMap.init($scope, svg, width, height).then(function () {
+          forceData.nodes.saveLonLat(backgroundMap);
+          backgroundMap.setMapOpacity($scope.legend.status.mapOpen);
         });
-
-      // the legend
-      //d3.select('#topo_svg_legend svg').remove();
-      if (d3.select('#svglegend').empty()) {
-        lsvg = d3.select('#topo_svg_legend')
-          .append('svg')
-          .attr('id', 'svglegend');
-        lsvg = lsvg.append('svg:g')
-          .attr('transform', `translate(${Nodes.maxRadius()}, ${Nodes.maxRadius()})`)
-          .selectAll('g');
+        addDefs(svg);
+        addGradient(svg);
+        // handles to link and node element groups
+        path = svg.append("svg:g").attr("class", "links").selectAll("g");
+        circle = svg.append("svg:g").attr("class", "nodes").selectAll("g");
       }
-
       // mouse event vars
       $scope.mousedown_node = null;
       mouseup_node = null;
 
       // initialize the list of nodes
-      animate = nodes.initialize(nodeInfo, localStorage, width, height);
-      nodes.savePositions();
-      // read the map data from the data file and build the map layer
-      backgroundMap.init($scope, svg, width, height)
-        .then( function () {
-          nodes.saveLonLat(backgroundMap);
-          backgroundMap.setMapOpacity($scope.legend.status.mapOpen);
-        });
+      animate = forceData.nodes.initialize(nodeInfo, width, height, localStorage);
+      forceData.nodes.savePositions();
 
       // initialize the list of links
       let unknowns = [];
-      if (links.initialize(nodeInfo, nodes, unknowns, localStorage, height)) {
+      if (forceData.links.initialize(nodeInfo,
+        forceData.nodes,
+        unknowns,
+        height,
+        localStorage))
         animate = true;
-      }
       $scope.schema = QDRService.management.schema();
       // init D3 force layout
-      force = d3.layout.force()
-        .nodes(nodes.nodes)
-        .links(links.links)
+      force = d3.layout
+        .force()
+        .nodes(forceData.nodes.nodes)
+        .links(forceData.links.links)
         .size([width, height])
-        .linkDistance(function(d) { return nodes.linkDistance(d, nodeCount); })
-        .charge(function(d) { return nodes.charge(d, nodeCount); })
-        .friction(.10)
-        .gravity(function(d) { return nodes.gravity(d, nodeCount); })
-        .on('tick', tick)
-        .on('end', function () {nodes.savePositions(); nodes.saveLonLat(backgroundMap);})
+        .linkDistance(function (d) {
+          return forceData.nodes.linkDistance(d, nodeCount);
+        })
+        .charge(function (d) {
+          return forceData.nodes.charge(d, nodeCount);
+        })
+        .friction(0.1)
+        .gravity(function (d) {
+          return forceData.nodes.gravity(d, nodeCount);
+        })
+        .on("tick", tick)
+        .on("end", function () {
+          forceData.nodes.savePositions();
+          forceData.nodes.saveLonLat(backgroundMap);
+        })
         .start();
 
-      // This section adds in the arrows
-      // Generate a marker for each combination of:
-      //  start|end, ''|selected highlighted, and each possible node radius
-      {
-        let sten = ['start', 'end'];
-        let states = ['', 'selected', 'highlighted', 'unknown'];
-        let radii = Nodes.discrete();
-        let defs = [];
-        for (let isten=0; isten<sten.length; isten++) {
-          for (let istate=0; istate<states.length; istate++) {
-            for (let iradii=0; iradii<radii.length; iradii++) {
-              defs.push({sten: sten[isten], state: states[istate], r: radii[iradii]});
-            }
-          }
-        }
-        svg.append('svg:defs').attr('class', 'marker-defs').selectAll('marker')
-          .data(defs)
-          .enter().append('svg:marker')
-          .attr('id', function (d) { return [d.sten, d.state, d.r].join('-'); })
-          .attr('viewBox', '0 -5 10 10')
-          .attr('refX', function (d) { return Nodes.refX(d.sten, d.r); })
-          .attr('markerWidth', 14)
-          .attr('markerHeight', 14)
-          .attr('markerUnits', 'userSpaceOnUse')
-          .attr('orient', 'auto')
-          .append('svg:path')
-          .attr('d', function (d) {
-            return d.sten === 'end' ? 'M 0 -5 L 10 0 L 0 5 z' : 'M 10 -5 L 0 0 L 10 5 z';
-          });
-        addStyles (sten, {selected: '#33F', highlighted: '#6F6', unknown: '#888'}, radii);
-      }
-      // 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').attr('class', 'links').selectAll('g'),
-      circle = svg.append('svg:g').attr('class', 'nodes').selectAll('g');
-
       // app starts here
-      //if (unknowns.length === 0)
-      restart();
+      if (unknowns.length === 0)
+        restart();
+      // the legend
+      // call updateLegend 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(updateLegend);
+
       if (oldSelectedNode) {
-        d3.selectAll('circle.inter-router').classed('selected', function (d) {
+        d3.selectAll("circle.inter-router").classed("selected", function (d) {
           if (d.key === oldSelectedNode.key) {
             selected_node = d;
             return true;
@@ -362,13 +357,19 @@ export class TopologyController {
         });
       }
       if (oldMouseoverNode && selected_node) {
-        d3.selectAll('circle.inter-router').each(function (d) {
+        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();
-            });
+            QDRService.management.topology.ensureAllEntities(
+              [{
+                entity: "router.node",
+                attrs: ["id", "nextHop"]
+              }],
+              function () {
+                nextHopHighlight(selected_node, d);
+                restart();
+              }
+            );
           }
         });
       }
@@ -384,38 +385,45 @@ export class TopologyController {
           setTimeout(continueForce, 100, extra);
         }
       };
-      continueForce(Nodes.forceScale(nodeCount, [0, 200]));  // give large graphs time to settle down
+      continueForce(Nodes.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.
+    // That means we don't have the router.link info when links.initialize() is first called
+    // and the initial graph is drawn.
     // The router.link info is needed to determine which direction the arrows between routers
     // and client should point. (Direction between interior routers is determined by connection.dir)
-    // So, the first time through links.initialize() we keep track of the nodes for which we 
+    // 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 nodes using an object
-      for (let i=0; i<unknowns.length; ++i) {
+      for (let i = 0; i < unknowns.length; ++i) {
         unknownNodes[unknowns[i]] = 1;
       }
       unknownNodes = Object.keys(unknownNodes);
-      QDRService.management.topology.ensureEntities(unknownNodes, 
-        [{entity: 'router.link', 
-          attrs: ['linkType','connectionId','linkDir','owningAddr'], 
-          force: true}], 
+      QDRService.management.topology.ensureEntities(
+        unknownNodes,
+        [{
+          entity: "router.link",
+          attrs: ["linkType", "connectionId", "linkDir", "owningAddr"],
+          force: true
+        }],
         function () {
           let nodeInfo = QDRService.management.topology.nodeInfo();
-          forceData.nodes = nodes = new Nodes(QDRLog);
-          nodes.initialize(nodeInfo, localStorage, width, height);
-          forceData.links = links = new Links(QDRLog);
+          forceData.nodes.initialize(nodeInfo, width, height, localStorage);
           let edgeUnknowns = [];
-          links.initialize(nodeInfo, nodes, edgeUnknowns, localStorage, height);
+          forceData.links.initialize(nodeInfo, forceData.nodes, edgeUnknowns, height, localStorage);
           animate = true;
-          force.nodes(nodes.nodes).links(links.links).start();
-          nodes.saveLonLat(backgroundMap);
+          force
+            .nodes(forceData.nodes.nodes)
+            .links(forceData.links.links)
+            .start();
+          forceData.nodes.saveLonLat(backgroundMap);
           restart();
-        });
+          updateLegend();
+        }
+      );
     };
 
     function resetMouseVars() {
@@ -427,7 +435,7 @@ export class TopologyController {
     // update force layout (called automatically each iteration)
     function tick() {
       // move the circles
-      circle.attr('transform', function(d) {
+      circle.attr("transform", function (d) {
         // don't let the edges of the circle go beyond the edges of the svg
         let r = Nodes.radius(d.nodeType);
         d.x = Math.max(Math.min(d.x, width - r), r);
@@ -436,10 +444,9 @@ export class TopologyController {
       });
 
       // draw lines from node centers
-      path.selectAll('path')
-        .attr('d', function(d) {
-          return `M${d.source.x},${d.source.y}L${d.target.x},${d.target.y}`;
-        });
+      path.selectAll("path").attr("d", function (d) {
+        return `M${d.source.x},${d.source.y}L${d.target.x},${d.target.y}`;
+      });
 
       if (!animate) {
         animate = true;
@@ -448,218 +455,259 @@ export class TopologyController {
     }
 
     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);
+      nextHop(
+        selected_node,
+        d,
+        forceData.nodes,
+        forceData.links,
+        QDRService.management.topology.nodeInfo(),
+        selected_node,
+        function (hlLink, hnode) {
+          hlLink.highlighted = true;
+          hnode.highlighted = true;
+        }
+      );
+      let hnode = forceData.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');
-      d3.select('#popover-div').style('display', 'none');
+      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");
+      d3.select("#popover-div").style("display", "none");
     }
 
     function clearAllHighlights() {
-      links.clearHighlighted();
-      nodes.clearHighlighted();
+      forceData.links.clearHighlighted();
+      forceData.nodes.clearHighlighted();
     }
     // Takes the forceData.nodes and forceData.links array and creates svg elements
     // Also updates any existing svg elements based on the updated values in forceData.nodes
     // and forceData.links
     function restart() {
-      if (!circle)
-        return;
+      if (!circle) return;
       circle.call(force.drag);
 
       // path is a selection of all g elements under the g.links svg:group
       // here we associate the links.links array with the {g.links g} selection
       // based on the link.uid
-      path = path.data(links.links, function(d) {return d.uid;});
+      path = path.data(forceData.links.links, function (d) {
+        return d.uid;
+      });
 
       // update each existing {g.links g.link} element
-      path.select('.link')
-        .classed('selected', function(d) {
+      path
+        .select(".link")
+        .classed("selected", function (d) {
           return d.selected;
         })
-        .classed('highlighted', function(d) {
+        .classed("highlighted", function (d) {
           return d.highlighted;
         })
-        .classed('unknown', function (d) {
+        .classed("unknown", function (d) {
           return !d.right && !d.left;
         });
 
       // reset the markers based on current highlighted/selected
-      if (!$scope.legend.status.optionsOpen || $scope.legendOptions.trafficType === 'dots') {
-        path.select('.link')
-          .attr('marker-end', function(d) {
-            return d.right ? `url(${urlPrefix}#end${d.markerId('end')})` : null;
+      if (
+        !$scope.legend.status.optionsOpen ||
+        $scope.legendOptions.trafficType === "dots"
+      ) {
+        path
+          .select(".link")
+          .attr("marker-end", function (d) {
+            return d.right ? `url(${urlPrefix}#end${d.markerId("end")})` : null;
           })
-          .attr('marker-start', function(d) {
-            return (d.left || (!d.left && !d.right)) ? `url(${urlPrefix}#start${d.markerId('start')})` : null;
+          .attr("marker-start", function (d) {
+            return d.left || (!d.left && !d.right) ?
+              `url(${urlPrefix}#start${d.markerId("start")})` :
+              null;
           });
       }
       // add new links. if a link with a new uid is found in the data, add a new path
-      let enterpath = path.enter().append('g')
-        .on('mouseover', function(d) { // mouse over a path
+      let enterpath = path
+        .enter()
+        .append("g")
+        .on("mouseover", function (d) {
+          // mouse over a path
           let event = d3.event;
           d.selected = true;
           let updateTooltip = function () {
             $timeout(function () {
               if (d.selected) {
-                $scope.trustedpopoverContent = $sce.trustAsHtml(connectionPopupHTML(d, QDRService));
+                $scope.trustedpopoverContent = $sce.trustAsHtml(
+                  connectionPopupHTML(
+                    d,
+                    QDRService.management.topology.nodeInfo()
+                  )
+                );
                 displayTooltip(event);
               }
             });
           };
           // update the contents of the popup tooltip each time the data is polled
-          QDRService.management.topology.addUpdatedAction('connectionPopupHTML', updateTooltip);
+          QDRService.management.topology.addUpdatedAction(
+            "connectionPopupHTML",
+            updateTooltip
+          );
           // request the data and update the tooltip as soon as it arrives
           QDRService.management.topology.ensureAllEntities(
-            [{ entity: 'router.link', force: true},{entity: 'connection'}], function () {
+            [{
+              entity: "router.link",
+              force: true
+            }, {
+              entity: "connection"
+            }],
+            function () {
               updateTooltip();
-            });
+            }
+          );
           // just show the tooltip with whatever data we have
           updateTooltip();
           restart();
-
         })
-        .on('mouseout', function(d) { // mouse out of a path
-          QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
-          d3.select('#popover-div')
-            .style('display', 'none');
+        .on("mouseout", function (d) {
+          // mouse out of a path
+          QDRService.management.topology.delUpdatedAction(
+            "connectionPopupHTML"
+          );
+          d3.select("#popover-div").style("display", "none");
           d.selected = false;
           connectionPopupHTML();
           restart();
         })
         // left click a path
-        .on('click', function () {
+        .on("click", function () {
           d3.event.stopPropagation();
           clearPopups();
         });
 
-      enterpath.append('path')
-        .attr('class', 'link')
-        .attr('marker-end', function(d) {
-          return d.right ? `url(${urlPrefix}#end${d.markerId('end')})` : null;
+      enterpath
+        .append("path")
+        .attr("class", "link")
+        .attr("marker-end", function (d) {
+          return d.right ? `url(${urlPrefix}#end${d.markerId("end")})` : null;
         })
-        .attr('marker-start', function(d) {
-          return (d.left || (!d.left && !d.right)) ? `url(${urlPrefix}#start${d.markerId('start')})` : null;
+        .attr("marker-start", function (d) {
+          return d.left || (!d.left && !d.right) ?
+            `url(${urlPrefix}#start${d.markerId("start")})` :
+            null;
         })
-        .attr('id', function (d) {
-          const si = d.source.uid(QDRService);
-          const ti = d.target.uid(QDRService);
-          return ['path', si, ti].join('-');
+        .attr("id", function (d) {
+          const si = d.source.uid();
+          const ti = d.target.uid();
+          return ["path", si, ti].join("-");
         })
-        .classed('unknown', function (d) {
+        .classed("unknown", function (d) {
           return !d.right && !d.left;
         });
 
-      enterpath.append('path')
-        .attr('class', 'hittarget');
+      enterpath.append("path").attr("class", "hittarget");
 
       // remove old links
       path.exit().remove();
 
-
       // circle (node) group
-      // nodes are known by router id, or for groups, by the router id + 1st connectionId
-      circle = circle.data(nodes.nodes, function(d) {
-        return d.uid();
-      });
+      circle = d3.select("g.nodes").selectAll("g")
+        .data(forceData.nodes.nodes, function (d) {
+          return d.uid();
+        });
 
       // 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;
-        });
-      circle
-        .classed('multiple', function (d) {
-          return (d.normals && d.normals.length > 1);
-        });
+      updateState(circle, selected_node);
 
       // 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; });
+      let enterCircle = circle
+        .enter()
+        .append("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
+      appendCircle(enterCircle)
+        .on("mouseover", function (d) {
+          // mouseover a circle
           $scope.current_node = d;
-          QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
+          QDRService.management.topology.delUpdatedAction(
+            "connectionPopupHTML"
+          );
           let e = d3.event;
-          d.toolTip(QDRService.management.topology)
-            .then( function (toolTip) {
-              showToolTip(toolTip, e);
-            });
-          if (d === $scope.mousedown_node)
-            return;
+          d.toolTip(QDRService.management.topology).then(function (toolTip) {
+            showToolTip(toolTip, e);
+          });
+          if (d === $scope.mousedown_node) return;
           // enlarge target node
-          d3.select(this).attr('transform', 'scale(1.1)');
+          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();
-          });
+          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();
+            }
+          );
         })
-        .on('mouseout', function() { // mouse out for a circle
+        .on("mouseout", function () {
+          // mouse out for a circle
           $scope.current_node = null;
           // unenlarge target node
-          d3.select('#popover-div')
-            .style('display', 'none');
-          d3.select(this).attr('transform', '');
+          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
+        .on("mousedown", function (d) {
+          // mouse down for circle
           backgroundMap.cancelZoom();
           $scope.current_node = d;
-          if (d3.event.button !== 0) { // ignore all but left button
+          if (d3.event.button !== 0) {
+            // ignore all but left button
             return;
           }
           $scope.mousedown_node = d;
           // mouse position relative to svg
-          initial_mouse_down_position = d3.mouse(this.parentNode.parentNode.parentNode).slice();
+          initial_mouse_down_position = d3
+            .mouse(this.parentNode.parentNode.parentNode)
+            .slice();
         })
-        .on('mouseup', function(d) {  // mouse up for circle
+        .on("mouseup", function (d) {
+          // mouse up for circle
           backgroundMap.restartZoom();
-          if (!$scope.mousedown_node)
-            return;
+          if (!$scope.mousedown_node) return;
 
           // unenlarge target node
-          d3.select(this).attr('transform', '');
+          d3.select(this).attr("transform", "");
 
           // check for drag
           mouseup_node = d;
 
-          let mySvg = d3.select('#SVG_ID').node();
           // 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.setFixed(true);
-            nodes.savePositions();
-            nodes.saveLonLat(backgroundMap);
+          let cur_mouse = d3.mouse(svg.node());
+          if (
+            cur_mouse[0] != initial_mouse_down_position[0] ||
+            cur_mouse[1] != initial_mouse_down_position[1]
+          ) {
+            forceData.nodes.setFixed(d, true);
+            forceData.nodes.savePositions();
+            forceData.nodes.saveLonLat(backgroundMap);
             resetMouseVars();
             restart();
             return;
@@ -669,10 +717,12 @@ export class TopologyController {
           if ($scope.mousedown_node === selected_node) {
             selected_node = null;
           } else {
-            if (d.nodeType !== 'normal' && 
-                d.nodeType !== 'on-demand' && 
-                d.nodeType !== 'edge' &&
-                d.nodeTYpe !== '_edge')
+            if (
+              d.nodeType !== "normal" &&
+              d.nodeType !== "on-demand" &&
+              d.nodeType !== "edge" &&
+              d.nodeTYpe !== "_edge"
+            )
               selected_node = $scope.mousedown_node;
           }
           clearAllHighlights();
@@ -684,314 +734,115 @@ export class TopologyController {
           }
           // apply any data changes to the interface
           restart();
-
         })
-        .on('dblclick', function(d) { // circle
+        .on("dblclick", function (d) {
+          // circle
           d3.event.preventDefault();
           if (d.fixed) {
-            d.setFixed(false);
+            forceData.nodes.setFixed(d, false);
             restart(); // redraw the node without a dashed line
             force.start(); // let the nodes move to a new position
           }
         })
-        .on('contextmenu', function(d) {  // circle
+        .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 () {
+          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('click', function(d) {  // circle
-          if (!mouseup_node)
-            return;
+        .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;
+            if (utils.isArtemis(d)) {
+              const artemisPath = "/jmx/attributes?tab=artemis&con=Artemis";
+              window.location =
+                $location.protocol() + "://localhost:8161/hawtio" + artemisPath;
             }
             return;
           }
           d3.event.stopPropagation();
         });
 
-      appendContent(g);
-      //appendTitle(g);
+      appendContent(enterCircle);
 
       // 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) {
+      svg.selectAll(".subtext").remove();
+      let multiples = svg.selectAll(".multiple");
+      multiples.each(function (d) {
         let g = d3.select(this);
         let r = Nodes.radius(d.nodeType);
-        g.append('svg:text')
-          .attr('x', r + 4)
-          .attr('y', Math.floor((r / 2) - 4))
-          .attr('class', 'subtext')
-          .text('* ' + d.normals.length);
+        g.append("svg:text")
+          .attr("x", r + 4)
+          .attr("y", Math.floor(r / 2 - 4))
+          .attr("class", "subtext")
+          .text("* " + 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);
 
       if (!$scope.mousedown_node || !selected_node)
         return;
 
       // set the graph in motion
-      //QDRLog.debug("mousedown_node is " + mousedown_node);
       force.start();
-
     }
-    let createLegend = function () {
-      // dynamically create the legend based on which node types are present
-      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(${Nodes.maxRadius()}, ${Nodes.maxRadius()})`)
-        .selectAll('g');
-      let legendNodes = new Nodes(QDRLog);
-      legendNodes.addUsing('Router', '', 'inter-router', undefined, 0, 0, 0, 0, false, {});
-      if (!svg.selectAll('circle.edge').empty() || !svg.selectAll('circle._edge').empty()) {
-        legendNodes.addUsing('Router', 'Edge', 'edge', undefined, 0, 0, 1, 0, false, {});
-      }
-      if (!svg.selectAll('circle.console').empty()) {
-        legendNodes.addUsing('Console', 'Console', 'normal', undefined, 0, 0, 2, 0, false, {
-          console_identifier: 'Dispatch console'
-        });
-      }
-      if (!svg.selectAll('circle.client.in').empty()) {
-        legendNodes.addUsing('Sender', 'Sender', 'normal', undefined, 0, 0, 3, 0, false, {}).cdir = 'in';
-      }
-      if (!svg.selectAll('circle.client.out').empty()) {
-        legendNodes.addUsing('Receiver', 'Receiver', 'normal', undefined, 0, 0, 4, 0, false, {}).cdir = 'out';
-      }
-      if (!svg.selectAll('circle.client.inout').empty()) {
-        legendNodes.addUsing('Sender/Receiver', 'Sender/Receiver', 'normal', undefined, 0, 0, 5, 0, false, {}).cdir = 'both';
-      }
-      if (!svg.selectAll('circle.qpid-cpp').empty()) {
-        legendNodes.addUsing('Qpid broker', 'Qpid broker', 'route-container', undefined, 0, 0, 6, 0, false, {
-          product: 'qpid-cpp'
-        });
-      }
-      if (!svg.selectAll('circle.artemis').empty()) {
-        legendNodes.addUsing('Artemis broker', 'Artemis broker', 'route-container', undefined, 0, 0, 7, 0, false,
-          {product: 'apache-activemq-artemis'});
-      }
-      if (!svg.selectAll('circle.route-container').empty()) {
-        legendNodes.addUsing('Service', 'Service', 'route-container', undefined, 0, 0, 8, 0, false,
-          {product: ' External Service'});
-      }
-      lsvg = lsvg.data(legendNodes.nodes, function(d) {
-        return d.key + d.name;
-      });
-      let cury = 0;
-      let lg = lsvg.enter().append('svg:g')
-        .attr('transform', function(d) {
-          let t = `translate(0, ${cury})`;
-          cury += (Nodes.radius(d.nodeType) * 2 + 10);
-          return t;
-        });
 
-      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 Nodes.radius(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;
-        })
-        .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' || d.nodeType === '_topo';
-        })
-        .classed('on-demand', function(d) {
-          return d.nodeType == 'on-demand';
-        })
-        .classed('edge', function(d) {
-          return d.nodeType === 'edge' || d.nodeType === '_edge';
-        })
-        .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
-      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;
-          else if (d.nodeType === 'edge' || d.nodeType === '_edge')
-            y = 4;
-          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('edge', function(d) {
-          return d.nodeType === 'edge';
-        })
-        .classed('edge', function(d) {
-          return d.nodeType === '_edge';
-        })
-        .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
-          } else if (d.nodeType === 'edge' || d.nodeType === '_edge') {
-            return 'Edge';
-          }
-          return d.name.length > 7 ?
-            d.name.substr(0, 3) + '...' + d.name.substr(d.name.length-3, 3) :
-            d.name;
-        });
-    };
-    let appendTitle = function(g) {
-      g.append('svg:title').text(function(d) {
-        return d.title();
-      });
-    };
+    function updateLegend() {
+      // dynamically create/update the legend based on which node types are present
+      let lsvg = new Legend(svg);
+      lsvg.update(svg, QDRLog, urlPrefix);
+    }
 
-    let showToolTip = function (title, event) {
+    function showToolTip(title, event) {
       // show the tooltip
-      $timeout ( function () {
+      $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'))
-          .on('mouseout', function () {
-            d3.select(this)
-              .style('display', 'none');
+    function displayTooltip(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")
+          .on("mouseout", function () {
+            d3.select(this).style("display", "none");
           });
       });
-    };
+    }
 
     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 cnodes = Object.keys(nodeInfo).filter(function (node) {
+        return nodeInfo[node]["connection"];
       });
-      let routers = nodes.nodes.filter( function (node) {
-        return node.nodeType === '_topo';
+      let routers = forceData.nodes.nodes.filter(function (node) {
+        return node.nodeType === "_topo";
       });
       if (routers.length > cnodes.length) {
         return -1;
@@ -1000,14 +851,13 @@ export class TopologyController {
         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++) {
+      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 (!savedKeys.hasOwnProperty(key)) return 1;
         // if the number of connections for this node chaanged
-        if (nodeInfo[key]['connection'].results.length !== savedKeys[key])
-          return nodeInfo[key]['connection'].results.length - savedKeys[key];
+        if (nodeInfo[key]["connection"].results.length !== savedKeys[key])
+          return nodeInfo[key]["connection"].results.length - savedKeys[key];
       }
       return 0;
     }
@@ -1017,52 +867,53 @@ export class TopologyController {
       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 (nodeInfo[key]["connection"])
+          savedKeys[key] = nodeInfo[key]["connection"].results.length;
       }
     }
-    function destroy () {
-      nodes.savePositions();
+
+    function destroy() {
+      forceData.nodes.savePositions();
       QDRService.management.topology.setUpdateEntities([]);
       QDRService.management.topology.stopUpdating();
-      QDRService.management.topology.delUpdatedAction('normalsStats');
-      QDRService.management.topology.delUpdatedAction('topology');
-      QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
+      QDRService.management.topology.delUpdatedAction("normalsStats");
+      QDRService.management.topology.delUpdatedAction("topology");
+      QDRService.management.topology.delUpdatedAction("connectionPopupHTML");
 
-      d3.select('#SVG_ID').remove();
-      window.removeEventListener('resize', resize);
+      d3.select("#SVG_ID").remove();
+      window.removeEventListener("resize", resize);
       traffic.stop();
-      d3.select('#main_container')
-        .style('background-color', 'white');
+      d3.select("#main_container").style("background-color", "white");
     }
     // When the DOM element is removed from the page,
     // AngularJS will trigger the $destroy event on
     // the scope
-    $scope.$on('$destroy', function() {
+    $scope.$on("$destroy", function () {
       destroy();
     });
     // we are about to leave the page, save the node positions
-    $rootScope.$on('$locationChangeStart', function() {
+    $rootScope.$on("$locationChangeStart", function () {
       destroy();
     });
 
     function handleInitialUpdate() {
       // we only need to update connections during steady-state
-      QDRService.management.topology.setUpdateEntities(['connection']);
+      QDRService.management.topology.setUpdateEntities(["connection"]);
       // we currently have all entities available on all routers
       initForceGraph();
       saveChanged();
       // after the graph is displayed fetch all .router.node info. This is done so highlighting between nodes
       // doesn't incur a delay
-      QDRService.management.topology.addUpdateEntities([
-        {entity: 'router.node', attrs: ['id','nextHop']}
-      ]);
+      QDRService.management.topology.addUpdateEntities([{
+        entity: "router.node",
+        attrs: ["id", "nextHop"]
+      }]);
       // call this function every time a background update is done
-      QDRService.management.topology.addUpdatedAction('topology', function() {
+      QDRService.management.topology.addUpdatedAction("topology", function () {
         let changed = hasChanged();
         // there is a new node, we need to get all of it's entities before drawing the graph
         if (changed > 0) {
-          QDRService.management.topology.delUpdatedAction('topology');
+          QDRService.management.topology.delUpdatedAction("topology");
           animate = true;
           setupInitialUpdate();
         } else if (changed === -1) {
@@ -1073,27 +924,36 @@ export class TopologyController {
         } else {
           //QDRLog.debug("topology didn't change")
         }
-
       });
     }
+
     function setupInitialUpdate() {
       // make sure all router nodes have .connection info. if not then fetch any missing info
       QDRService.management.topology.ensureAllEntities(
-        [{entity: 'connection'}],
-        handleInitialUpdate);
+        [{
+          entity: "connection"
+        }],
+        handleInitialUpdate
+      );
     }
     if (!QDRService.management.connection.is_connected()) {
       // we are not connected. we probably got here from a bookmark or manual page reload
-      QDRRedirectWhenConnected($location, 'topology');
+      QDRRedirectWhenConnected($location, "topology");
       return;
     }
 
-
-
     animate = true;
     setupInitialUpdate();
     QDRService.management.topology.startUpdating(true);
-
   }
 }
-TopologyController.$inject = ['QDRService', '$scope', '$log', '$rootScope', '$location', '$timeout', '$uibModal', '$sce'];
+TopologyController.$inject = [
+  "QDRService",
+  "$scope",
+  "$log",
+  "$rootScope",
+  "$location",
+  "$timeout",
+  "$uibModal",
+  "$sce"
+];
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/js/topology/svgUtils.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/svgUtils.js b/console/stand-alone/plugin/js/topology/svgUtils.js
new file mode 100644
index 0000000..999bcf5
--- /dev/null
+++ b/console/stand-alone/plugin/js/topology/svgUtils.js
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2018 Red Hat Inc.
+ *
+ * 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.
+ */
+
+import { Nodes } from "./nodes.js";
+import { utils } from "../amqp/utilities.js";
+
+export function updateState(circle, selected_node) {
+  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 ? d.fixed & 1 : false;
+    })
+    .classed("multiple", function (d) {
+      return d.normals && d.normals.length > 1;
+    });
+}
+
+export function appendCircle(g, urlPrefix) {
+  // add new circles and set their attr/class/behavior
+  return g
+    .append("svg:circle")
+    .attr("class", "node")
+    .attr("r", function (d) {
+      return Nodes.radius(d.nodeType);
+    })
+    .attr("fill", function (d) {
+      if (d.cdir === "both" && !utils.isConsole(d)) {
+        return "url(" + urlPrefix + "#half-circle)";
+      }
+      return null;
+    })
+    .classed("fixed", function (d) {
+      return d.fixed ? d.fixed & 1 : false;
+    })
+    .classed("normal", function (d) {
+      return d.nodeType == "normal" || utils.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" || d.nodeType === "_topo";
+    })
+    .classed("on-demand", function (d) {
+      return d.nodeType == "on-demand";
+    })
+    .classed("edge", function (d) {
+      return d.nodeType === "edge" || d.nodeType === "_edge";
+    })
+    .classed("console", function (d) {
+      return utils.isConsole(d);
+    })
+    .classed("artemis", function (d) {
+      return utils.isArtemis(d);
+    })
+    .classed("qpid-cpp", function (d) {
+      return utils.isQpid(d);
+    })
+    .classed("route-container", function (d) {
+      return (
+        !utils.isArtemis(d) &&
+        !utils.isQpid(d) &&
+        d.nodeType === "route-container"
+      );
+    })
+    .classed("client", function (d) {
+      return d.nodeType === "normal" && !d.properties.console_identifier;
+    });
+}
+
+export function appendContent(g) {
+  // show node IDs
+  g.append("svg:text")
+    .attr("x", 0)
+    .attr("y", function (d) {
+      let y = 7;
+      if (utils.isArtemis(d)) y = 8;
+      else if (utils.isQpid(d)) y = 9;
+      else if (d.nodeType === "inter-router") y = 4;
+      else if (d.nodeType === "route-container") y = 5;
+      else if (d.nodeType === "edge" || d.nodeType === "_edge") y = 4;
+      return y;
+    })
+    .attr("class", "id")
+    .classed("console", function (d) {
+      return utils.isConsole(d);
+    })
+    .classed("normal", function (d) {
+      return d.nodeType === "normal";
+    })
+    .classed("on-demand", function (d) {
+      return d.nodeType === "on-demand";
+    })
+    .classed("edge", function (d) {
+      return d.nodeType === "edge";
+    })
+    .classed("edge", function (d) {
+      return d.nodeType === "_edge";
+    })
+    .classed("artemis", function (d) {
+      return utils.isArtemis(d);
+    })
+    .classed("qpid-cpp", function (d) {
+      return utils.isQpid(d);
+    })
+    .text(function (d) {
+      if (utils.isConsole(d)) {
+        return "\uf108"; // icon-desktop for a console
+      } else if (utils.isArtemis(d)) {
+        return "\ue900";  // custom font character
+      } else if (utils.isQpid(d)) {
+        return "\ue901";  // custom font character
+      } 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
+      } else if (d.nodeType === "edge" || d.nodeType === "_edge") {
+        return "Edge";
+      }
+      return d.name.length > 7 ?
+        d.name.substr(0, 3) + "..." + d.name.substr(d.name.length - 3, 3) :
+        d.name;
+    });
+}
+
+export function appendTitle(g) {
+  g.append("svg:title").text(function (d) {
+    return d.title();
+  });
+}
+
+// Generate a marker for each combination of:
+//  start|end, ''|selected highlighted, and each possible node radius
+export function addDefs(svg) {
+  let sten = ["start", "end"];
+  let states = ["", "selected", "highlighted", "unknown"];
+  let radii = Nodes.discrete();
+  let defs = [];
+  for (let isten = 0; isten < sten.length; isten++) {
+    for (let istate = 0; istate < states.length; istate++) {
+      for (let iradii = 0; iradii < radii.length; iradii++) {
+        defs.push({
+          sten: sten[isten],
+          state: states[istate],
+          r: radii[iradii]
+        });
+      }
+    }
+  }
+  svg
+    .append("svg:defs")
+    .attr("class", "marker-defs")
+    .selectAll("marker")
+    .data(defs)
+    .enter()
+    .append("svg:marker")
+    .attr("id", function (d) {
+      return [d.sten, d.state, d.r].join("-");
+    })
+    .attr("viewBox", "0 -5 10 10")
+    .attr("refX", function (d) {
+      return Nodes.refX(d.sten, d.r);
+    })
+    .attr("markerWidth", 14)
+    .attr("markerHeight", 14)
+    .attr("markerUnits", "userSpaceOnUse")
+    .attr("orient", "auto")
+    .append("svg:path")
+    .attr("d", function (d) {
+      return d.sten === "end" ?
+        "M 0 -5 L 10 0 L 0 5 z" :
+        "M 10 -5 L 0 0 L 10 5 z";
+    });
+  addStyles(
+    sten, {
+      selected: "#33F",
+      highlighted: "#6F6",
+      unknown: "#888"
+    },
+    radii
+  );
+}
+export function addGradient(svg) {
+  // 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");
+}
+
+function addStyles(stend, stateColor, radii) {
+  // the <style>
+  let element = document.querySelector("style");
+  // Reference to the stylesheet
+  let sheet = element.sheet;
+
+  let states = Object.keys(stateColor);
+  // create styles for each combo of 'stend-state-radii'
+  for (let istend = 0; istend < stend.length; istend++) {
+    for (let istate = 0; istate < states.length; istate++) {
+      let selectors = [];
+      for (let iradii = 0; iradii < radii.length; iradii++) {
+        selectors.push(`#${stend[istend]}-${states[istate]}-${radii[iradii]}`);
+      }
+      let color = stateColor[states[istate]];
+      let sels = `${selectors.join(",")} {fill: ${color}; stroke: ${color};}`;
+      sheet.insertRule(sels, 0);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/js/topology/topoUtils.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/topoUtils.js b/console/stand-alone/plugin/js/topology/topoUtils.js
index 71e5e4c..99e2c9a 100644
--- a/console/stand-alone/plugin/js/topology/topoUtils.js
+++ b/console/stand-alone/plugin/js/topology/topoUtils.js
@@ -18,36 +18,44 @@ under the License.
 */
 
 /* global Set */
+import {
+  utils
+} from "../amqp/utilities.js";
 
 // highlight the paths between the selected node and the hovered node
-function findNextHopNode(from, d, QDRService, selected_node, nodes) {
+function findNextHopNode(from, d, nodeInfo, selected_node, nodes) {
   // d is the node that the mouse is over
   // from is the selected_node ....
-  if (!from)
-    return null;
+  if (!from) return null;
 
-  if (from == d)
-    return selected_node;
+  if (from == d) return selected_node;
 
-  let sInfo = QDRService.management.topology.nodeInfo()[from.key];
+  let sInfo = nodeInfo[from.key];
 
   // 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;
+  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 (d.name && (addrT == d.name)) {
-      let next = QDRService.utilities.valFor(aAr, vAr[hIdx], 'nextHop');
-      return (next == null) ? nodes.nodeFor(addrT) : nodes.nodeFor(next);
+    let addrT = utils.valFor(aAr, vAr[hIdx], "id");
+    if (d.name && addrT == d.name) {
+      let next = utils.valFor(aAr, vAr[hIdx], "nextHop");
+      return next == null ? nodes.nodeFor(addrT) : nodes.nodeFor(next);
     }
   }
   return null;
 }
-export function nextHop(thisNode, d, nodes, links, QDRService, selected_node, cb) {
-  if ((thisNode) && (thisNode != d)) {
-    let target = findNextHopNode(thisNode, d, QDRService, selected_node, nodes);
+export function nextHop(
+  thisNode,
+  d,
+  nodes,
+  links,
+  nodeInfo,
+  selected_node,
+  cb
+) {
+  if (thisNode && thisNode != d) {
+    let target = findNextHopNode(thisNode, d, nodeInfo, selected_node, nodes);
     if (target) {
       let hnode = nodes.nodeFor(thisNode.name);
       let hlLink = links.linkFor(hnode, target);
@@ -55,164 +63,177 @@ export function nextHop(thisNode, d, nodes, links, QDRService, selected_node, cb
         if (cb) {
           cb(hlLink, hnode, target);
         }
-      }
-      else
-        target = null;
+      } else target = null;
     }
-    nextHop(target, d, nodes, links, QDRService, selected_node, cb);
+    nextHop(target, d, nodes, links, nodeInfo, selected_node, cb);
   }
 }
 
 let linkRateHistory = {};
-export function connectionPopupHTML (d, QDRService) {
+export function connectionPopupHTML(d, nodeInfo) {
   if (!d) {
     linkRateHistory = {};
     return;
   }
-  let utils = QDRService.utilities;
   // return all of onode's connections that connecto to right
   let getConnsArray = function (onode, key, right) {
     if (right.normals) {
       // if we want connections between a router and a client[s]
       let connIds = new Set();
-      let connIndex = onode.connection.attributeNames.indexOf('identity');
-      for (let n=0; n<right.normals.length; n++) {
+      let connIndex = onode.connection.attributeNames.indexOf("identity");
+      for (let n = 0; n < right.normals.length; n++) {
         let normal = right.normals[n];
         if (normal.key === key) {
           connIds.add(normal.connectionId);
         } else if (normal.alsoConnectsTo) {
-          normal.alsoConnectsTo.forEach( function (ac2) {
-            if (ac2.key === key)
-              connIds.add(ac2.connectionId);
+          normal.alsoConnectsTo.forEach(function (ac2) {
+            if (ac2.key === key) connIds.add(ac2.connectionId);
           });
         }
       }
-      return onode.connection.results.filter( function (result) {
-        return connIds.has(result[connIndex]);
-      }).map( function (c) {
-        return utils.flatten(onode.connection.attributeNames, c);
-      });
-    }
-    else {
-    // we want the connection between two routers
+      return onode.connection.results
+        .filter(function (result) {
+          return connIds.has(result[connIndex]);
+        })
+        .map(function (c) {
+          return utils.flatten(onode.connection.attributeNames, c);
+        });
+    } else {
+      // we want the connection between two routers
       let container = utils.nameFromId(right.key);
-      let containerIndex = onode.connection.attributeNames.indexOf('container');
-      let roleIndex = onode.connection.attributeNames.indexOf('role');
-      return onode.connection.results.filter( function (conn) {
-        return conn[containerIndex] === container && conn[roleIndex] === 'inter-router';
-      }).map( function (c) {
-        return utils.flatten(onode.connection.attributeNames, c);
-      });
+      let containerIndex = onode.connection.attributeNames.indexOf("container");
+      let roleIndex = onode.connection.attributeNames.indexOf("role");
+      return onode.connection.results
+        .filter(function (conn) {
+          return (
+            conn[containerIndex] === container &&
+            conn[roleIndex] === "inter-router"
+          );
+        })
+        .map(function (c) {
+          return utils.flatten(onode.connection.attributeNames, c);
+        });
     }
   };
   // construct HTML to be used in a popup when the mouse is moved over a link.
   // The HTML is sanitized elsewhere before it is displayed
   let linksHTML = function (onode, conns) {
     const max_links = 10;
-    const fields = ['deliveryCount', 'undeliveredCount', 'unsettledCount', 'rejectedCount', 'releasedCount', 'modifiedCount'];
+    const fields = [
+      "deliveryCount",
+      "undeliveredCount",
+      "unsettledCount",
+      "rejectedCount",
+      "releasedCount",
+      "modifiedCount"
+    ];
     // local function to determine if a link's connectionId is in any of the connections
     let isLinkFor = function (connectionId, conns) {
-      for (let c=0; c<conns.length; c++) {
-        if (conns[c].identity === connectionId)
-          return true;
+      for (let c = 0; c < conns.length; c++) {
+        if (conns[c].identity === connectionId) return true;
       }
       return false;
     };
     let fnJoin = function (ar, sepfn) {
-      let out = '';
+      let out = "";
       out = ar[0];
-      for (let i=1; i<ar.length; i++) {
-        let sep = sepfn(ar[i], i===ar.length-1);
-        out += (sep[0] + sep[1]);
+      for (let i = 1; i < ar.length; i++) {
+        let sep = sepfn(ar[i], i === ar.length - 1);
+        out += sep[0] + sep[1];
       }
       return out;
     };
     // if the data for the line is from a client (small circle), we may have multiple connections
     // loop through all links for this router and accumulate those belonging to the connection(s)
-    let nodeLinks = onode['router.link'];
-    if (!nodeLinks)
-      return '';
+    let nodeLinks = onode["router.link"];
+    if (!nodeLinks) return "";
     let links = [];
     let hasAddress = false;
-    for (let n=0; n<nodeLinks.results.length; n++) {
+    for (let n = 0; n < nodeLinks.results.length; n++) {
       let link = utils.flatten(nodeLinks.attributeNames, nodeLinks.results[n]);
       let allZero = true;
-      if (link.linkType !== 'router-control') {
+      if (link.linkType !== "router-control") {
         if (isLinkFor(link.connectionId, conns)) {
-          if (link.owningAddr)
-            hasAddress = true;
+          if (link.owningAddr) hasAddress = true;
           if (link.name) {
-            let rates = utils.rates(link, fields, linkRateHistory, link.name, 1);
+            let rates = utils.rates(
+              link,
+              fields,
+              linkRateHistory,
+              link.name,
+              1
+            );
             // replace the raw value with the rate
-            for (let i=0; i<fields.length; i++) {
-              if (rates[fields[i]] > 0)
-                allZero = false;
+            for (let i = 0; i < fields.length; i++) {
+              if (rates[fields[i]] > 0) allZero = false;
               link[fields[i]] = rates[fields[i]];
             }
           }
-          if (!allZero)
-            links.push(link);
+          if (!allZero) links.push(link);
         }
       }
     }
     // we may need to limit the number of links displayed, so sort descending by the sum of the field values
     let sum = function (a) {
       let s = 0;
-      for (let i=0; i<fields.length; i++) {
+      for (let i = 0; i < fields.length; i++) {
         s += a[fields[i]];
       }
       return s;
     };
-    links.sort( function (a, b) {
+    links.sort(function (a, b) {
       let asum = sum(a);
       let bsum = sum(b);
       return asum < bsum ? 1 : asum > bsum ? -1 : 0;
     });
 
-    let HTMLHeading = '<h5>Rates (per second) for links</h5>';
+    let HTMLHeading = "<h5>Rates (per second) for links</h5>";
     let HTML = '<table class="popupTable">';
     // copy of fields since we may be prepending an address
     let th = fields.slice();
     let td = fields;
-    th.unshift('dir');
-    td.unshift('linkDir');
-    th.push('priority');
-    td.push('priority');
+    th.unshift("dir");
+    td.unshift("linkDir");
+    th.push("priority");
+    td.push("priority");
     // add an address field if any of the links had an owningAddress
     if (hasAddress) {
-      th.unshift('address');
-      td.unshift('owningAddr');
+      th.unshift("address");
+      td.unshift("owningAddr");
     }
 
     let rate_th = function (th) {
-      let rth = th.map( function (t) {
-        if (t.endsWith('Count'))
-          t = t.replace('Count', 'Rate');
+      let rth = th.map(function (t) {
+        if (t.endsWith("Count")) t = t.replace("Count", "Rate");
         return utils.humanify(t);
       });
       return rth;
     };
-    HTML += ('<tr class="header"><td>' + rate_th(th).join('</td><td>') + '</td></tr>');
+    HTML +=
+      '<tr class="header"><td>' + rate_th(th).join("</td><td>") + "</td></tr>";
     // add rows to the table for each link
-    for (let l=0; l<links.length; l++) {
-      if (l>=max_links) {
+    for (let l = 0; l < links.length; l++) {
+      if (l >= max_links) {
         HTMLHeading = `<h5>Rates (per second) for top ${max_links} links</h5>`;
         break;
       }
       let link = links[l];
-      let vals = td.map( function (f) {
-        if (f === 'owningAddr') {
+      let vals = td.map(function (f) {
+        if (f === "owningAddr") {
           let identity = utils.identity_clean(link.owningAddr);
           return utils.addr_text(identity);
         }
         return link[f];
       });
       let joinedVals = fnJoin(vals, function (v1, last) {
-        return [`</td><td${(isNaN(+v1) ? '': ' align="right"')}>`, last ? v1 : utils.pretty(v1 || '0', ',.2f')];
+        return [
+          `</td><td${isNaN(+v1) ? "" : ' align="right"'}>`,
+          last ? v1 : utils.pretty(v1 || "0", ",.2f")
+        ];
       });
       HTML += `<tr><td> ${joinedVals} </td></tr>`;
     }
-    return links.length > 0 ? `${HTMLHeading}${HTML}</table>` : '';
+    return links.length > 0 ? `${HTMLHeading}${HTML}</table>` : "";
   };
 
   let left, right;
@@ -228,43 +249,40 @@ export function connectionPopupHTML (d, QDRService) {
     [left, right] = [right, left];
   }
   // left is a router. right is either a router or a client[s]
-  let onode = QDRService.management.topology.nodeInfo()[left.key];
+  let onode = nodeInfo[left.key];
   // find all the connections for left that go to right
   let conns = getConnsArray(onode, left.key, right);
 
-  let HTML = '';
-  HTML += '<h5>Connection'+(conns.length > 1 ? 's' : '')+'</h5>';
-  HTML += '<table class="popupTable"><tr class="header"><td>Security</td><td>Authentication</td><td>Tenant</td><td>Host</td>';
+  let HTML = "";
+  HTML += "<h5>Connection" + (conns.length > 1 ? "s" : "") + "</h5>";
+  HTML +=
+    '<table class="popupTable"><tr class="header"><td>Security</td><td>Authentication</td><td>Tenant</td><td>Host</td>';
 
-  for (let c=0; c<Math.min(conns.length, 10); c++) {
-    HTML += ('<tr><td>' + utils.connSecurity(conns[c]) + '</td>');
-    HTML += ('<td>' + utils.connAuth(conns[c]) + '</td>');
-    HTML += ('<td>' + (utils.connTenant(conns[c]) || '--') + '</td>');
-    HTML += ('<td>' + conns[c].host + '</td>');
-    HTML += '</tr>';
+  for (let c = 0; c < Math.min(conns.length, 10); c++) {
+    HTML += "<tr><td>" + utils.connSecurity(conns[c]) + "</td>";
+    HTML += "<td>" + utils.connAuth(conns[c]) + "</td>";
+    HTML += "<td>" + (utils.connTenant(conns[c]) || "--") + "</td>";
+    HTML += "<td>" + conns[c].host + "</td>";
+    HTML += "</tr>";
   }
-  HTML += '</table>';
+  HTML += "</table>";
   HTML += linksHTML(onode, conns);
   return HTML;
 }
 
-export function addStyles (stend, stateColor, radii) {
-  // the <style>
-  let element = document.querySelector('style');
-  // Reference to the stylesheet
-  let sheet = element.sheet;
-
-  let states = Object.keys(stateColor);
-  // create styles for each combo of 'stend-state-radii'
-  for (let istend=0; istend<stend.length; istend++) {
-    for (let istate=0; istate<states.length; istate++) {
-      let selectors = [];
-      for (let iradii=0; iradii<radii.length; iradii++) {
-        selectors.push(`#${stend[istend]}-${states[istate]}-${radii[iradii]}`);
-      }
-      let color = stateColor[states[istate]];
-      let sels = `${selectors.join(',')} {fill: ${color}; stroke: ${color};}`;
-      sheet.insertRule(sels, 0);
-    }
+export function getSizes(QDRLog) {
+  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];
 }


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


[3/3] qpid-dispatch git commit: DISPATCH-1217 Treat node.fixed as bitmap

Posted by ea...@apache.org.
DISPATCH-1217 Treat node.fixed as bitmap


Project: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/repo
Commit: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/commit/81e58b46
Tree: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/tree/81e58b46
Diff: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/diff/81e58b46

Branch: refs/heads/master
Commit: 81e58b4628173d1392d32dcfedce0312065bea41
Parents: e9b7bd5
Author: Ernest Allen <ea...@redhat.com>
Authored: Mon Dec 10 11:58:58 2018 -0500
Committer: Ernest Allen <ea...@redhat.com>
Committed: Mon Dec 10 11:58:58 2018 -0500

----------------------------------------------------------------------
 console/stand-alone/plugin/css/dispatch.css     |   10 -
 .../stand-alone/plugin/html/qdrTopology.html    |  179 ++-
 console/stand-alone/plugin/js/amqp/utilities.js |   39 +-
 .../plugin/js/dlgDetailController.js            |   53 +-
 .../stand-alone/plugin/js/topology/legend.js    |  138 +++
 console/stand-alone/plugin/js/topology/links.js |   54 +-
 console/stand-alone/plugin/js/topology/map.js   |   69 +-
 console/stand-alone/plugin/js/topology/nodes.js |   99 +-
 .../plugin/js/topology/qdrTopology.js           | 1086 ++++++++----------
 .../stand-alone/plugin/js/topology/svgUtils.js  |  249 ++++
 .../stand-alone/plugin/js/topology/topoUtils.js |  242 ++--
 .../stand-alone/plugin/js/topology/traffic.js   |  144 ++-
 console/stand-alone/test/links.js               |   54 +-
 13 files changed, 1359 insertions(+), 1057 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/css/dispatch.css
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/css/dispatch.css b/console/stand-alone/plugin/css/dispatch.css
index f41754b..709ce43 100644
--- a/console/stand-alone/plugin/css/dispatch.css
+++ b/console/stand-alone/plugin/css/dispatch.css
@@ -1143,12 +1143,6 @@ svg {
     /*height: 100%; */
   }
   
-  div#topologyForm .ngViewport, div#topologyForm .gridStyle {
-    height: auto !important;
-    min-height: initial !important;
-    overflow: initial;
-  }
-  
   div#multiple_details, div#link_details {
       height: 300px;
       width: 700px;
@@ -1819,10 +1813,6 @@ span.logo {
     top: 10px;
   }
   
-  #topologyForm > div {
-    width: auto;
-  }
-  
   div.chartContainer {
     width: auto;
     margin-top: 1em;

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/html/qdrTopology.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/qdrTopology.html b/console/stand-alone/plugin/html/qdrTopology.html
index 5ba514b..0057fab 100644
--- a/console/stand-alone/plugin/html/qdrTopology.html
+++ b/console/stand-alone/plugin/html/qdrTopology.html
@@ -18,7 +18,7 @@ under the License.
 -->
 
 <style>
-@media (min-width: 768px) {
+    @media (min-width: 768px) {
   .showLeft {
       display: block;
   }
@@ -27,6 +27,9 @@ under the License.
   .showLeft {
       display: none;
   }
+  #backgroundMap {
+      display: none;
+  }
   div.qdrTopology div.legend-container.page-menu {
       top: 0;
       right: auto;
@@ -35,7 +38,7 @@ under the License.
   }
 }
 
-#popover-div, #fixed-popup {
+#popover-div {
     position: absolute;
     z-index: 200;
     border-radius: 4px;
@@ -53,41 +56,6 @@ under the License.
     font-size: 10px;
   }
 
-#topologyForm {
-  border-right: 1px solid lightgray;
-  border-bottom: 1px solid lightgray;
-  padding: 2px;
-  /* position: absolute; */
-  background-color: #333333;
-  width: 300px;
-}
-
-  #topologyForm .infoGrid span {
-      display: inline-block;
-      width: 50%;
-      height: 28px;
-      padding: 3px 6px 2px 6px;
-      border-right: 1px solid #666666;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-      overflow: hidden;
-  }
-
-  #topologyForm .infoGrid span:last-child {
-      text-align: right;
-      border-right: 0;
-  }
-  #topologyForm .infoGrid div {
-      height: 28px;
-  }
-  #topologyForm .infoGrid div.odd {
-      background-color: #444444;
-  }
-
-  #topologyForm .infoGrid div.listening-on {
-      background-color: #336633;
-  }
-
   .legend-container {
       position: absolute;
       top: 1em;
@@ -135,8 +103,8 @@ under the License.
     margin-bottom: 0;
     padding-bottom: 0;
 }
-  /* the checkboxes for the addresses */
-  #topo_legend ul li input[type=checkbox]:checked + label::before {
+/* the checkboxes for the addresses */
+#topo_legend ul li input[type=checkbox]:checked + label::before {
     content:'\2713';
     font-weight: bold;
     font-size: 16px;
@@ -146,12 +114,12 @@ under the License.
     position: absolute;
     top: -8px;
     left: -1px;
-  }
-  /* The aggregate addresses need a black checkbox on the white background */
-  #topo_legend ul li input[type=checkbox]:checked + label.aggregate::before {
+}
+/* The aggregate addresses need a black checkbox on the white background */
+#topo_legend ul li input[type=checkbox]:checked + label.aggregate::before {
     color: black;
-    /* left: 1px; */
-  }
+/* left: 1px; */
+}
 #topo_legend ul.addresses button.btn-default {
     background-image: none;
     color: white;
@@ -159,8 +127,8 @@ under the License.
 }
 
 #topo_legend li.legend-sublist ul {
-        margin-bottom: 0.5em;
-    }
+    margin-bottom: 0.5em;
+}
 
 #topo_legend li.legend-sublist ul.addresses{
     max-height: 11.6em;   /* show up to 4 addresses */
@@ -171,11 +139,11 @@ li.legend-sublist > ul ul {
     margin-left: 1em;
 }
 
-#popover-div h5, #fixed-popup h5 {
+#popover-div h5 {
     margin-top: 1em;
     margin-bottom: 0;
 }
-#popover-div h5:first-of-type, #fixed-popup h5:first-of-type {
+#popover-div h5:first-of-type {
     margin-top:0;
 }
 
@@ -198,23 +166,23 @@ table.popupTable td {
 }
 
 .graticule {
-  fill: none;
-  stroke: #777;
-  stroke-width: .5px;
-  stroke-opacity: .5;
+    fill: none;
+    stroke: #777;
+    stroke-width: .5px;
+    stroke-opacity: .5;
 }
 
 g.geo path.land {
-  fill: #dcedf7;
-  stroke: #000;
-  stroke-opacity: 1;
-  stroke-width: 1px;
+    fill: #dcedf7;
+    stroke: #000;
+    stroke-opacity: 1;
+    stroke-width: 1px;
 }
 
 .boundary {
-  fill: none;
-  stroke: #333;
-  stroke-width: 5px;
+    fill: none;
+    stroke: #333;
+    stroke-width: 5px;
 }
 
 .panel-group {
@@ -254,40 +222,44 @@ td.more-info {
     <div class="legend-container page-menu navbar-collapse collapse">
         <uib-accordion id="topo_legend" close-others="false">
             <div uib-accordion-group class="panel-default" is-open="legend.status.optionsOpen" heading="Show Traffic">
-              <ul class="options">
-                  <li class="legend-sublist" ng-hide="!legendOptions.showTraffic">
-                    <ul>
-                        <li><label>
-                            <input type='radio' ng-model="legendOptions.trafficType" value="dots" />
-                        Message path by address
-                        </label></li>
-                        <li>
-                            <ul class="addresses" ng-show="legendOptions.trafficType === 'dots'">
+                <ul class="options">
+                    <li class="legend-sublist" ng-hide="!legendOptions.showTraffic">
+                        <ul>
+                            <li><label>
+                                    <input type='radio' ng-model="legendOptions.trafficType" value="dots" />
+                                    Message path by address
+                                </label></li>
+                            <li>
+                                <ul class="addresses" ng-show="legendOptions.trafficType === 'dots'">
                                     <li ng-repeat="(address, color) in addresses" class="legend-line">
-                                        <checkbox style="background-color: {{addressColors[address]}};"
-                                        title="{{address}}" ng-change="addressFilterChanged()"
-                                        ng-model="addresses[address]" ng-mouseenter="enterLegend(address)" ng-mouseleave="leaveLegend()"></checkbox>
-                                        <span class="legend-text" ng-mouseenter="enterLegend(address)" ng-mouseleave="leaveLegend()" ng-click="addressClick(address)" title="{{address}}">{{address | limitTo : 15}}{{address.length>15 ? '…' : ''}}</span>
+                                        <checkbox style="background-color: {{addressColors[address]}};" title="{{address}}"
+                                            ng-change="addressFilterChanged()" ng-model="addresses[address]"
+                                            ng-mouseenter="enterLegend(address)" ng-mouseleave="leaveLegend()"></checkbox>
+                                        <span class="legend-text" ng-mouseenter="enterLegend(address)" ng-mouseleave="leaveLegend()"
+                                            ng-click="addressClick(address)" title="{{address}}">{{address | limitTo :
+                                            15}}{{address.length>15 ? '…' : ''}}</span>
                                     </li>
-                            </ul>
-                        </li>
-                    </ul>
-                    <ul>
-                        <li><label>
-                            <input type='radio' ng-model="legendOptions.trafficType" value="congestion" />
-                        Link utilization
-                        </label></li>
-                        <li>
-                            <ul class="congestion" ng-show="legendOptions.trafficType === 'congestion'">
+                                </ul>
+                            </li>
+                        </ul>
+                        <ul>
+                            <li><label>
+                                    <input type='radio' ng-model="legendOptions.trafficType" value="congestion" />
+                                    Link utilization
+                                </label></li>
+                            <li>
+                                <ul class="congestion" ng-show="legendOptions.trafficType === 'congestion'">
                                     <li>
-                                        <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" preserveAspectRatio="xMidYMid meet" width="140" height="40">
+                                        <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+                                            version="1.1" preserveAspectRatio="xMidYMid meet" width="140" height="40">
                                             <defs>
-                                                <linearGradient xmlns="http://www.w3.org/2000/svg" id="gradienta1bEihLEHL" gradientUnits="userSpaceOnUse" x1="0%" y1="0%" x2="100%" y2="0%">
-                                                    <stop style="stop-color: #000000;stop-opacity: 1" offset="0"/>
-                                                    <stop style="stop-color: #000000;stop-opacity: 1" offset="0.06"/>
-                                                    <stop style="stop-color: #00FF00;stop-opacity: 1" offset="0.333"/>
-                                                    <stop style="stop-color: #FFA500;stop-opacity: 1" offset="0.666"/>
-                                                    <stop style="stop-color: #FF0000;stop-opacity: 1" offset="1"/></linearGradient>
+                                                <linearGradient xmlns="http://www.w3.org/2000/svg" id="gradienta1bEihLEHL"
+                                                    gradientUnits="userSpaceOnUse" x1="0%" y1="0%" x2="100%" y2="0%">
+                                                    <stop style="stop-color: #999999;stop-opacity: 1" offset="0" />
+                                                    <stop style="stop-color: #00FF00;stop-opacity: 1" offset="0.333" />
+                                                    <stop style="stop-color: #FFA500;stop-opacity: 1" offset="0.666" />
+                                                    <stop style="stop-color: #FF0000;stop-opacity: 1" offset="1" />
+                                                </linearGradient>
                                             </defs>
                                             <g>
                                                 <rect width="140" height="20" x="0" y="0" fill="url(#gradienta1bEihLEHL)"></rect>
@@ -295,34 +267,39 @@ td.more-info {
                                                 <text x="106" y="30" class="label">Busy</text>
                                             </g>
                                         </svg></li>
-                            </ul>
-                        </li>
-                    </ul>
-                </li>
-              </ul>
+                                </ul>
+                            </li>
+                        </ul>
+                    </li>
+                </ul>
             </div>
             <div uib-accordion-group class="panel-default" is-open="legend.status.legendOpen" heading="Legend">
                 <div id="topo_svg_legend"></div>
             </div>
-            <div uib-accordion-group class="panel-default" is-open="legend.status.mapOpen" heading="Background map">
+            <div id="backgroundMap" uib-accordion-group class="panel-default" is-open="legend.status.mapOpen" heading="Background map">
                 <div id="topo_mapOptions">
                     <div class="colorPicker">
                         <ul>
                             <li>
-                                <label><span class='map-label'>Land</span> <input id="areaColor" name="areaColor" type="color" ng-model="mapOptions.areaColor"/></label>
+                                <label><span class='map-label'>Land</span> <input id="areaColor" name="areaColor" type="color"
+                                        ng-model="mapOptions.areaColor" /></label>
                             </li>
                             <li>
-                                <label><span class='map-label'>Ocean</span> <input id="oceanColor" name="oceanColor" type="color" ng-model="mapOptions.oceanColor"/></label>
+                                <label><span class='map-label'>Ocean</span> <input id="oceanColor" name="oceanColor"
+                                        type="color" ng-model="mapOptions.oceanColor" /></label>
                             </li>
                         </ul>
                     </div>
                 </div>
             </div>
-          </uib-accordion>
+        </uib-accordion>
     </div>
     <div class="diagram">
-        <div id="topology"><!-- d3 toplogy here --></div>
-        <div id="crosssection"></div><div id="crosshtml" ng-bind-html="crosshtml"></div>
+        <div id="topology">
+            <!-- d3 toplogy here -->
+        </div>
+        <div id="crosssection"></div>
+        <div id="crosshtml" ng-bind-html="crosshtml"></div>
 
         <div id="node_context_menu" class="contextMenu">
             <ul>
@@ -344,4 +321,4 @@ td.more-info {
 </script>
 <script type="text/ng-template" id="titleCellTemplate.html">
     <div title="{{row.entity.attributeValue}}" class="ui-grid-cell-contents">{{COL_FIELD CUSTOM_FILTERS | pretty}}</div>
-</script>
+</script>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/js/amqp/utilities.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/amqp/utilities.js b/console/stand-alone/plugin/js/amqp/utilities.js
index 517b4e1..34f1d7d 100644
--- a/console/stand-alone/plugin/js/amqp/utilities.js
+++ b/console/stand-alone/plugin/js/amqp/utilities.js
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Red Hat Inc.
+ * Copyright 2018 Red Hat Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,7 +15,7 @@
  */
 
 /* global d3 */
-var ddd = typeof window === 'undefined' ? require ('d3') : d3;
+var ddd = typeof window === 'undefined' ? require('d3') : d3;
 
 var utils = {
   isAConsole: function (properties, connectionId, nodeType, key) {
@@ -40,7 +40,7 @@ var utils = {
     if (!attributes || !result)
       return {};
     var flat = {};
-    attributes.forEach(function(attr, i) {
+    attributes.forEach(function (attr, i) {
       if (result && result.length > i)
         flat[attr] = result[i];
     });
@@ -48,9 +48,11 @@ var utils = {
   },
   flattenAll: function (entity, filter) {
     if (!filter)
-      filter = function (e) {return e;};
+      filter = function (e) {
+        return e;
+      };
     let results = [];
-    for (let i=0; i<entity.results.length; i++) {
+    for (let i = 0; i < entity.results.length; i++) {
       let f = filter(this.flatten(entity.attributeNames, entity.results[i]));
       if (f)
         results.push(f);
@@ -72,7 +74,7 @@ var utils = {
   addr_text: function (addr) {
     if (!addr)
       return '-';
-    if (addr[0] === addr[0].toLowerCase()) 
+    if (addr[0] === addr[0].toLowerCase())
       return addr;
     if (addr[0] == 'M')
       return addr.substring(2);
@@ -122,7 +124,7 @@ var utils = {
   countsFor: function (aAr, vAr, key) {
     let counts = {};
     let idx = aAr.indexOf(key);
-    for (let i=0; i<vAr.length; i++) {
+    for (let i = 0; i < vAr.length; i++) {
       if (!counts[vAr[i][idx]])
         counts[vAr[i][idx]] = 0;
       counts[vAr[i][idx]]++;
@@ -139,7 +141,7 @@ var utils = {
 
     var parts = id.split('/');
     // remove $management
-    parts.pop(); 
+    parts.pop();
 
     // remove the area if present
     if (parts[2] === '0')
@@ -171,18 +173,21 @@ var utils = {
       list.shift();
     }
     let rates = {};
-    list.push({date: new Date(), val: Object.assign({}, obj)});
+    list.push({
+      date: new Date(),
+      val: Object.assign({}, obj)
+    });
 
-    for (let i=0; i<fields.length; i++) {
+    for (let i = 0; i < fields.length; i++) {
       let cumulative = 0;
       let field = fields[i];
-      for (let j=0; j<list.length-1; j++) {
-        let elapsed = list[j+1].date - list[j].date;
-        let diff = list[j+1].val[field] - list[j].val[field];
+      for (let j = 0; j < list.length - 1; j++) {
+        let elapsed = list[j + 1].date - list[j].date;
+        let diff = list[j + 1].val[field] - list[j].val[field];
         if (elapsed > 100)
-          cumulative += diff/(elapsed / 1000);
+          cumulative += diff / (elapsed / 1000);
       }
-      rates[field] = list.length > 1 ? cumulative / (list.length-1) : 0;
+      rates[field] = list.length > 1 ? cumulative / (list.length - 1) : 0;
     }
     return rates;
   },
@@ -216,4 +221,6 @@ var utils = {
   }
 
 };
-export { utils };
\ No newline at end of file
+export {
+  utils
+};
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/js/dlgDetailController.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/dlgDetailController.js b/console/stand-alone/plugin/js/dlgDetailController.js
index 1c9b45a..dba5d3b 100644
--- a/console/stand-alone/plugin/js/dlgDetailController.js
+++ b/console/stand-alone/plugin/js/dlgDetailController.js
@@ -33,7 +33,7 @@ export class DetailDialogController {
     // count the number of characters in an array of strings
     let countChars = function (ar) {
       let count = 0;
-      ar.forEach( a => count += a.length);
+      ar.forEach(a => count += a.length);
       return count;
     };
 
@@ -110,7 +110,7 @@ export class DetailDialogController {
     };
     // keep an array of column sizes
     let updateSizes = function (fields, sizes, obj) {
-      fields.forEach( function (key) {
+      fields.forEach(function (key) {
         if (!sizes[key])
           sizes[key] = utils.humanify(key).length;
         sizes[key] = Math.max(sizes[key], utils.pretty(obj[key]).length);
@@ -128,8 +128,8 @@ export class DetailDialogController {
       // queued function to get the .router info for an edge router
       let q_getEdgeInfo = function (n, infoPerId, callback) {
         let nodeId = utils.idFromName(n.container, '_edge');
-        QDRService.management.topology.fetchEntities(nodeId, 
-          [{entity: 'router', attrs: []},
+        QDRService.management.topology.fetchEntities(nodeId,
+          [{ entity: 'router', attrs: [] },
           ],
           function (results) {
             let r = results[nodeId].router;
@@ -142,21 +142,22 @@ export class DetailDialogController {
             callback(null);
           });
       };
-      return new Promise( (function (resolve) {
+      return new Promise((function (resolve) {
         let infoPerId = {};
         // we are getting info for an edge router
         if (d.nodeType === 'edge') {
           // called for each expanded row to get further details about the edge router
           $scope.detail.moreInfo = function (id) {
             let nodeId = utils.idFromName(id, '_edge');
-            QDRService.management.topology.fetchEntities(nodeId, 
-              [{entity: 'router.link', attrs: []},
-                {entity: 'linkRoute', attrs: $scope.fields.linkRouteFields.cols},
-                {entity: 'autoLink', attrs: $scope.fields.autoLinkFields.cols},
-                {entity: 'address', attrs: []},
+            QDRService.management.topology.fetchEntities(nodeId,
+              [
+                { entity: 'router.link', attrs: [] },
+                { entity: 'linkRoute', attrs: $scope.fields.linkRouteFields.cols },
+                { entity: 'autoLink', attrs: $scope.fields.autoLinkFields.cols },
+                { entity: 'address', attrs: [] },
               ],
               function (results) {
-                $timeout( function () {
+                $timeout(function () {
                   // save the results (and sizes) for each entity requested
                   infoPerId[id].linkRouteSizes = {};
                   infoPerId[id].linkRoutes = utils.flattenAll(results[nodeId].linkRoute,
@@ -165,13 +166,13 @@ export class DetailDialogController {
                       return route;
                     });
                   infoPerId[id].autoLinkSizes = {};
-                  infoPerId[id].autoLinks = utils.flattenAll(results[nodeId].autoLink, 
+                  infoPerId[id].autoLinks = utils.flattenAll(results[nodeId].autoLink,
                     function (link) {
                       updateSizes($scope.fields.autoLinkFields.cols, infoPerId[id].autoLinkSizes, link);
                       return link;
                     });
                   infoPerId[id].addressSizes = {};
-                  infoPerId[id].addresses = utils.flattenAll(results[nodeId].address, 
+                  infoPerId[id].addresses = utils.flattenAll(results[nodeId].address,
                     function (addr) {
                       updateSizes($scope.fields.addressFields.cols, infoPerId[id].addressSizes, addr);
                       return addr;
@@ -182,7 +183,7 @@ export class DetailDialogController {
 
           // async send up to 10 requests
           let q = d3.queue(10);
-          for (let n=0; n<d.normals.length; n++) {
+          for (let n = 0; n < d.normals.length; n++) {
             q.defer(q_getEdgeInfo, d.normals[n], infoPerId);
             if (expandedRows.has(d.normals[n].container)) {
               $scope.detail.moreInfo(d.normals[n].container);
@@ -200,14 +201,14 @@ export class DetailDialogController {
           });
         } else {
           // we are getting info for a group of clients or consoles
-          $scope.detail.moreInfo = function () {};
+          $scope.detail.moreInfo = function () { };
           let attrs = utils.copy($scope.fields.linkFields.cols);
           attrs.unshift('connectionId');
-          QDRService.management.topology.fetchEntities(d.key, 
-            [{entity: 'router.link', attrs: attrs}],
+          QDRService.management.topology.fetchEntities(d.key,
+            [{ entity: 'router.link', attrs: attrs }],
             function (results) {
               let links = results[d.key]['router.link'];
-              for (let i=0; i<d.normals.length; i++) {
+              for (let i = 0; i < d.normals.length; i++) {
                 let n = d.normals[i];
                 let conn = {};
                 infoPerId[n.container] = conn;
@@ -231,7 +232,7 @@ export class DetailDialogController {
               let count = d.normals.length;
               let verb = count > 1 ? 'are' : 'is';
               let preposition = d.cdir === 'in' ? 'to' : d.cdir === 'both' ? 'for' : 'from';
-              let plural = count > 1 ? 's': '';
+              let plural = count > 1 ? 's' : '';
               $scope.detail.template = 'clients.html';
               $scope.detail.title = 'for client';
               resolve({
@@ -242,16 +243,16 @@ export class DetailDialogController {
         }
       }));
     };
-  
+
     let updateDetail = function () {
       groupDetail.call(this)
-        .then( function (det) {
-          $timeout( function () {
+        .then(function (det) {
+          $timeout(function () {
             $scope.detail.title = `for ${d.normals.length} ${$scope.detail.title}${d.normals.length > 1 ? 's' : ''}`;
             $scope.detail.description = det.description;
-            $scope.detail.infoPerId = Object.keys(det.infoPerId).map( function (id) {
+            $scope.detail.infoPerId = Object.keys(det.infoPerId).map(function (id) {
               return det.infoPerId[id];
-            }).sort( function (a, b) {
+            }).sort(function (a, b) {
               return a.name > b.name ? 1 : -1;
             });
           });
@@ -266,7 +267,7 @@ DetailDialogController.$inject = ['QDRService', '$scope', '$timeout', '$uibModal
 
 // SubTable directive
 export class SubTable {
-  constructor () {
+  constructor() {
     this.restrict = 'E';
     this.scope = {
       sizes: '=sizes',
@@ -275,7 +276,7 @@ export class SubTable {
     };
     this.templateUrl = 'sub-table.html';
   }
-  link (scope) {
+  link(scope) {
     scope.fieldWidth = function (val, sizes) {
       if (!sizes)
         return '10%';

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/js/topology/legend.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/legend.js b/console/stand-alone/plugin/js/topology/legend.js
new file mode 100644
index 0000000..b3908c5
--- /dev/null
+++ b/console/stand-alone/plugin/js/topology/legend.js
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2018 Red Hat Inc.
+ *
+ * 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.
+ */
+
+/* global d3 */
+
+import { Nodes } from "./nodes.js";
+import { appendCircle, appendContent, appendTitle } from "./svgUtils.js";
+// for testing, window will be undefined
+var ddd = typeof window === 'undefined' ? require('d3') : d3;
+
+const lookFor = [
+  { role: "inter-router", title: "Router", text: "", cls: '' },
+  { role: "edge", title: "Router", text: "Edge", cls: 'edge' },
+  { role: "normal", title: "Console", text: "console", cls: 'console', props: { console_identifier: "Dispatch console" } },
+  { role: "normal", title: "Sender", text: "Sender", cls: 'client.in', cdir: "in" },
+  { role: "normal", title: "Receiver", text: "Receiver", cls: 'client.out', cdir: "out" },
+  { role: "normal", title: "Sender/Receiver", text: "Sender/Receiver", cls: 'client.inout', cdir: "both" },
+  { role: "route-container", title: "Qpid broker", text: "Qpid broker", cls: 'client.route-container', props: { product: "qpid-cpp" } },
+  { role: "route-container", title: "Artemis broker", text: "Artemis broker", cls: 'route-container', props: { product: "apache-activemp-artemis" } },
+  { role: "route-container", title: "Service", text: "Service", cls: 'route-container', props: { product: " External Service" } }
+];
+
+export class Legend {
+  constructor(svg, QDRLog, urlPrefix) {
+    this.svg = svg;
+    this.log = QDRLog;
+    this.urlPrefix = urlPrefix;
+  }
+
+  // create a new legend container svg
+  init() {
+    return ddd
+      .select("#topo_svg_legend")
+      .append("svg")
+      .attr("id", "svglegend")
+      .append("svg:g")
+      .attr(
+        "transform",
+        `translate(${Nodes.maxRadius()}, ${Nodes.maxRadius()})`
+      )
+      .selectAll("g");
+  }
+
+  // create or update the legend
+  update() {
+    let lsvg;
+    if (ddd.select("#topo_svg_legend svg").empty()) {
+      lsvg = this.init();
+    } else {
+      lsvg = ddd.select("#topo_svg_legend svg g").selectAll("g");
+    }
+    // add a node to legendNodes for each node type that is currently in the svg
+    let legendNodes = new Nodes(this.log);
+    lookFor.forEach(function (node, i) {
+      if (!node.cls || !this.svg.selectAll(`circle.${node.cls}`).empty()) {
+        let lnode = legendNodes.addUsing(
+          node.title,
+          node.text,
+          node.role,
+          undefined,
+          0, 0, i, 0,
+          false,
+          node.props ? node.props : {}
+        );
+        if (node.cdir)
+          lnode.cdir = node.cdir;
+      }
+    }, this);
+
+    // determine the y coordinate of the last existing node in the legend 
+    let cury = 0;
+    lsvg.each(function (d) {
+      cury += Nodes.radius(d.nodeType) * 2 + 10;
+    });
+
+    // associate the legendNodes with lsvg
+    lsvg = lsvg.data(legendNodes.nodes, function (d) {
+      return d.uid();
+    });
+
+    // add any new nodes
+    let legendEnter = lsvg
+      .enter()
+      .append("svg:g")
+      .attr("transform", function (d) {
+        let t = `translate(0, ${cury})`;
+        cury += Nodes.radius(d.nodeType) * 2 + 10;
+        return t;
+      });
+    appendCircle(legendEnter, this.urlPrefix);
+    appendContent(legendEnter);
+    appendTitle(legendEnter);
+    legendEnter.append("svg:text")
+      .attr("x", 35)
+      .attr("y", 6)
+      .attr("class", "label")
+      .text(function (d) {
+        return d.key;
+      });
+
+    // remove any nodes that dropped out of legendNodes
+    lsvg.exit().remove();
+
+    // position the legend based on it's size
+    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";
+    }
+  }
+}
+
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/js/topology/links.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/links.js b/console/stand-alone/plugin/js/topology/links.js
index 16b086c..1260415 100644
--- a/console/stand-alone/plugin/js/topology/links.js
+++ b/console/stand-alone/plugin/js/topology/links.js
@@ -17,7 +17,9 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import { utils } from "../amqp/utilities.js";
+import {
+  utils
+} from "../amqp/utilities.js";
 
 class Link {
   constructor(source, target, dir, cls, uid) {
@@ -29,15 +31,13 @@ class Link {
     this.uid = uid;
   }
   markerId(end) {
-    let selhigh = this.highlighted
-      ? "highlighted"
-      : this.selected
-        ? "selected"
-        : "";
+    let selhigh = this.highlighted ?
+      "highlighted" :
+      (this.selected ?
+        "selected" :
+        "");
     if (selhigh === "" && (!this.left && !this.right)) selhigh = "unknown";
-    return `-${selhigh}-${
-      end === "end" ? this.target.radius() : this.source.radius()
-    }`;
+    return `-${selhigh}-${end === "end" ? this.target.radius() : this.source.radius()}`;
   }
 }
 
@@ -46,6 +46,9 @@ export class Links {
     this.links = [];
     this.logger = logger;
   }
+  reset() {
+    this.links.length = 0;
+  }
   getLinkSource(nodesIndex) {
     for (let i = 0; i < this.links.length; ++i) {
       if (this.links[i].target === nodesIndex) return i;
@@ -70,7 +73,7 @@ export class Links {
     }
     //this.logger.debug("creating new link (" + (links.length) + ") between " + nodes[_source].name + " and " + nodes[_target].name);
     if (
-      this.links.some(function(l) {
+      this.links.some(function (l) {
         return l.uid === uid;
       })
     )
@@ -88,10 +91,8 @@ export class Links {
     return null;
   }
 
-  getPosition(name, nodes, source, client, localStorage, height) {
-    let position = localStorage[name]
-      ? JSON.parse(localStorage[name])
-      : undefined;
+  getPosition(name, nodes, source, client, height, localStorage) {
+    let position = localStorage[name] ? JSON.parse(localStorage[name]) : undefined;
     if (typeof position == "undefined") {
       position = {
         x: Math.round(
@@ -109,10 +110,12 @@ export class Links {
         nodes.get(source).y + 40 + Math.cos(client / (Math.PI * 2.0))
       );
     }
+    position.fixed = position.fixed ? true : false;
     return position;
   }
 
-  initialize(nodeInfo, nodes, unknowns, localStorage, height) {
+  initialize(nodeInfo, nodes, unknowns, height, localStorage) {
+    this.reset();
     let connectionsPerContainer = {};
     let nodeIds = Object.keys(nodeInfo);
     // collect connection info for each router
@@ -158,7 +161,10 @@ export class Links {
     // create map of type:id:dir to [containers]
     for (let container in connectionsPerContainer) {
       let key = getKey(connectionsPerContainer[container]);
-      if (!unique[key]) unique[key] = { c: [], nodes: [] };
+      if (!unique[key]) unique[key] = {
+        c: [],
+        nodes: []
+      };
       unique[key].c.push(container);
     }
     for (let key in unique) {
@@ -176,8 +182,8 @@ export class Links {
           nodes,
           container.source,
           container.resultsIndex,
-          localStorage,
-          height
+          height,
+          localStorage
         );
 
         let node = nodes.getOrCreateNode(
@@ -242,7 +248,7 @@ export class Links {
   }
 }
 
-var getContainerIndex = function(_id, nodeInfo) {
+var getContainerIndex = function (_id, nodeInfo) {
   let nodeIndex = 0;
   for (let id in nodeInfo) {
     if (utils.nameFromId(id) === _id) return nodeIndex;
@@ -251,7 +257,7 @@ var getContainerIndex = function(_id, nodeInfo) {
   return -1;
 };
 
-var getLinkDir = function(connection, onode) {
+var getLinkDir = function (connection, onode) {
   let links = onode["router.link"];
   if (!links) {
     return "unknown";
@@ -261,12 +267,12 @@ var getLinkDir = function(connection, onode) {
   let typeIndex = links.attributeNames.indexOf("linkType");
   let connectionIdIndex = links.attributeNames.indexOf("connectionId");
   let dirIndex = links.attributeNames.indexOf("linkDir");
-  links.results.forEach(function(linkResult) {
+  links.results.forEach(function (linkResult) {
     if (
       linkResult[typeIndex] === "endpoint" &&
       linkResult[connectionIdIndex] === connection.identity
     )
-      if (linkResult[dirIndex] === "in") ++inCount;
+      if (linkResult[dirIndex] === "in")++inCount;
       else ++outCount;
   });
   if (inCount > 0 && outCount > 0) return "both";
@@ -274,7 +280,7 @@ var getLinkDir = function(connection, onode) {
   if (outCount > 0) return "out";
   return "unknown";
 };
-var getKey = function(containers) {
+var getKey = function (containers) {
   let parts = [];
   let connection = containers[0].connection;
   let d = {
@@ -291,4 +297,4 @@ var getKey = function(containers) {
     parts.push(`${container.source}-${container.linksDir}`);
   }
   return `${connectionType}:${parts.join(":")}`;
-};
+};
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/js/topology/map.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/map.js b/console/stand-alone/plugin/js/topology/map.js
index 972d894..c03cfa5 100644
--- a/console/stand-alone/plugin/js/topology/map.js
+++ b/console/stand-alone/plugin/js/topology/map.js
@@ -30,8 +30,14 @@ export class BackgroundMap { // eslint-disable-line no-unused-vars
     this.$scope = $scope;
     this.initialized = false;
     this.notify = notifyFn;
-    $scope.mapOptions = angular.fromJson(localStorage[MAPOPTIONSKEY]) || {areaColor: defaultLandColor, oceanColor: defaultOceanColor};
-    this.last = {translate: [0,0], scale: null};
+    $scope.mapOptions = angular.fromJson(localStorage[MAPOPTIONSKEY]) || {
+      areaColor: defaultLandColor,
+      oceanColor: defaultOceanColor
+    };
+    this.last = {
+      translate: [0, 0],
+      scale: null
+    };
   }
   updateLandColor(color) {
     localStorage[MAPOPTIONSKEY] = JSON.stringify(this.$scope.mapOptions);
@@ -55,7 +61,7 @@ export class BackgroundMap { // eslint-disable-line no-unused-vars
   }
 
   init($scope, svg, width, height) {
-    return new Promise( (function (resolve, reject) {
+    return new Promise((function (resolve, reject) {
 
       if (this.initialized) {
         resolve();
@@ -82,9 +88,9 @@ export class BackgroundMap { // eslint-disable-line no-unused-vars
 
       // setup the projection with some defaults
       this.projection = d3.geo.mercator()
-        .rotate([this.rotate,0])
+        .rotate([this.rotate, 0])
         .scale(1)
-        .translate([width/2, height/2]);
+        .translate([width / 2, height / 2]);
 
       // this path will hold the land coordinates once they are loaded
       this.geoPath = d3.geo.path()
@@ -92,22 +98,26 @@ export class BackgroundMap { // eslint-disable-line no-unused-vars
 
       // set up the scale extent and initial scale for the projection
       var b = getMapBounds(this.projection, Math.max(maxnorth, maxsouth)),
-        s = width/(b[1][0]-b[0][0]);
-      this.scaleExtent = [s, 15*s];
+        s = width / (b[1][0] - b[0][0]);
+      this.scaleExtent = [s, 15 * s];
 
       this.projection
         .scale(this.scaleExtent[0]);
-      this.lastProjection = angular.fromJson(localStorage[MAPPOSITIONKEY]) || {rotate: 20, scale: this.scaleExtent[0], translate: [width/2, height/2]};
+      this.lastProjection = angular.fromJson(localStorage[MAPPOSITIONKEY]) || {
+        rotate: 20,
+        scale: this.scaleExtent[0],
+        translate: [width / 2, height / 2]
+      };
 
       this.zoom = d3.behavior.zoom()
         .scaleExtent(this.scaleExtent)
         .scale(this.projection.scale())
-        .translate([0,0])               // not linked directly to projection
+        .translate([0, 0]) // not linked directly to projection
         .on('zoom', this.zoomed.bind(this));
 
       this.geo = svg.append('g')
         .attr('class', 'geo')
-        .style('opacity', this.$scope.legend.status.mapOpen ? '1': '0');
+        .style('opacity', this.$scope.legend.status.mapOpen ? '1' : '0');
 
       this.geo.append('rect')
         .attr('class', 'ocean')
@@ -115,13 +125,14 @@ export class BackgroundMap { // eslint-disable-line no-unused-vars
         .attr('height', height)
         .attr('fill', '#FFF');
 
-      if (this.$scope.legend.status.mapOpen)
+      if (this.$scope.legend.status.mapOpen) {
         this.svg.call(this.zoom)
           .on('dblclick.zoom', null);
+      }
 
       // async load of data file. calls resolve when this completes to let caller know
-      d3.json('plugin/data/countries.json', function(error, world) {
-        if (error) 
+      d3.json('plugin/data/countries.json', function (error, world) {
+        if (error)
           reject(error);
 
         this.geo.append('path')
@@ -148,6 +159,8 @@ export class BackgroundMap { // eslint-disable-line no-unused-vars
 
   setMapOpacity(opacity) {
     opacity = opacity ? 1 : 0;
+    if (this.width && this.width < 768)
+      opacity = 0;
     if (this.geo)
       this.geo.style('opacity', opacity);
   }
@@ -180,11 +193,11 @@ export class BackgroundMap { // eslint-disable-line no-unused-vars
   }
 
   zoomed() {
-    if (d3.event && !this.$scope.current_node && !this.$scope.mousedown_node && this.$scope.legend.status.mapOpen) { 
+    if (d3.event && !this.$scope.current_node && !this.$scope.mousedown_node && this.$scope.legend.status.mapOpen) {
       let scale = d3.event.scale,
         t = d3.event.translate,
-        dx = t[0]-this.last.translate[0],
-        dy = t[1]-this.last.translate[1],
+        dx = t[0] - this.last.translate[0],
+        dy = t[1] - this.last.translate[1],
         yaw = this.projection.rotate()[0],
         tp = this.projection.translate();
       // zoomed
@@ -209,21 +222,21 @@ export class BackgroundMap { // eslint-disable-line no-unused-vars
         dy = my - this.projection([0, lonlat[1]])[1];
 
         // rotate the map so that the longitude under the mouse is where it was before the scale
-        this.projection.rotate([yaw+dx ,0, 0]);
+        this.projection.rotate([yaw + dx, 0, 0]);
 
         // translate the map so that the lattitude under the mouse is where it was before the scale
-        this.projection.translate([tp[0], tp[1]+dy]);
+        this.projection.translate([tp[0], tp[1] + dy]);
       } else {
         // rotate instead of translate in the x direction
-        this.projection.rotate([yaw+360.0*dx/this.width*this.scaleExtent[0]/scale, 0, 0]);
+        this.projection.rotate([yaw + 360.0 * dx / this.width * this.scaleExtent[0] / scale, 0, 0]);
         // translate only in the y direction. don't translate beyond the max lattitude north or south
         var bnorth = getMapBounds(this.projection, maxnorth),
           bsouth = getMapBounds(this.projection, maxsouth);
-        if (bnorth[0][1] + dy > 0) 
+        if (bnorth[0][1] + dy > 0)
           dy = -bnorth[0][1];
-        else if (bsouth[1][1] + dy < this.height) 
-          dy = this.height-bsouth[1][1];
-        this.projection.translate([tp[0],tp[1]+dy]);
+        else if (bsouth[1][1] + dy < this.height)
+          dy = this.height - bsouth[1][1];
+        this.projection.translate([tp[0], tp[1] + dy]);
       }
       this.last.scale = scale;
       this.last.translate = t;
@@ -247,8 +260,8 @@ export class BackgroundMap { // eslint-disable-line no-unused-vars
 // find the top left and bottom right of current projection
 function getMapBounds(projection, maxlat) {
   var yaw = projection.rotate()[0],
-    xymax = projection([-yaw+180-1e-6,-maxlat]),
-    xymin = projection([-yaw-180+1e-6, maxlat]);
-  
-  return [xymin,xymax];
-}
+    xymax = projection([-yaw + 180 - 1e-6, -maxlat]),
+    xymin = projection([-yaw - 180 + 1e-6, maxlat]);
+
+  return [xymin, xymax];
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/js/topology/nodes.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/nodes.js b/console/stand-alone/plugin/js/topology/nodes.js
index 50c9a37..26ac9bc 100644
--- a/console/stand-alone/plugin/js/topology/nodes.js
+++ b/console/stand-alone/plugin/js/topology/nodes.js
@@ -17,7 +17,9 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import { utils } from "../amqp/utilities.js";
+import {
+  utils
+} from "../amqp/utilities.js";
 
 /* global d3 Promise */
 export class Node {
@@ -43,7 +45,7 @@ export class Node {
     this.y = y;
     this.id = nodeIndex;
     this.resultIndex = resultIndex;
-    this.fixed = !!+fixed;
+    this.fixed = fixed ? true : false;
     this.cls = "";
     this.container = connectionContainer;
     this.isConsole = utils.isConsole(this);
@@ -71,11 +73,11 @@ export class Node {
   }
   toolTip(topology) {
     return new Promise(
-      function(resolve) {
+      function (resolve) {
         if (this.nodeType === "normal" || this.nodeType === "edge") {
           resolve(this.clientTooltip());
         } else
-          this.routerTooltip(topology).then(function(toolTip) {
+          this.routerTooltip(topology).then(function (toolTip) {
             resolve(toolTip);
           });
       }.bind(this)
@@ -99,19 +101,23 @@ export class Node {
 
   routerTooltip(topology) {
     return new Promise(
-      function(resolve) {
+      function (resolve) {
         topology.ensureEntities(
           this.key,
-          [
-            { entity: "listener", attrs: ["role", "port", "http"] },
-            { entity: "router", attrs: ["name", "version", "hostName"] }
+          [{
+            entity: "listener",
+            attrs: ["role", "port", "http"]
+          },
+          {
+            entity: "router",
+            attrs: ["name", "version", "hostName"]
+          }
           ],
-          function(foo, nodes) {
+          function (foo, nodes) {
             // update all the router title text
             let node = nodes[this.key];
             const err = `<table class="popupTable"><tr><td>Error</td><td>Unable to get router info for ${
-              this.key
-            }</td></tr></table>`;
+              this.key}</td></tr></table>`;
             if (!node) {
               resolve(err);
               return;
@@ -154,32 +160,43 @@ export class Node {
     return nodeProperties[this.nodeType].radius;
   }
   uid() {
-    if (!this.uuid) this.uuid = this.container;
+    if (!this.uuid)
+      this.uuid = `${this.container}`;
     return this.normals ? `${this.uuid}-${this.normals.length}` : this.uuid;
   }
   setFixed(fixed) {
-    if (!fixed) this.lat = this.lon = null;
-    this.fixed = fixed;
+    if (!fixed & 1)
+      this.lat = this.lon = null;
+    this.fixed = fixed & 1 ? true : false;
   }
 }
 const nodeProperties = {
   // router types
   "inter-router": {
     radius: 28,
-    refX: { end: 32, start: -19 },
+    refX: {
+      end: 32,
+      start: -19
+    },
     linkDistance: [150, 70],
     charge: [-1800, -900]
   },
   edge: {
     radius: 20,
-    refX: { end: 24, start: -12 },
+    refX: {
+      end: 24,
+      start: -12
+    },
     linkDistance: [110, 55],
     charge: [-1350, -900]
   },
   // generated nodes from connections. key is from connection.role
   normal: {
     radius: 15,
-    refX: { end: 20, start: -7 },
+    refX: {
+      end: 20,
+      start: -7
+    },
     linkDistance: [75, 40],
     charge: [-900, -900]
   }
@@ -240,7 +257,13 @@ export class Nodes {
   gravity(d, nodeCount) {
     return Nodes.forceScale(nodeCount, [0.0001, 0.1]);
   }
-
+  setFixed(d, fixed) {
+    let n = this.find(d.container, d.properties, d.name);
+    if (n) {
+      n.fixed = fixed;
+    }
+    d.setFixed(fixed);
+  }
   getLength() {
     return this.nodes.length;
   }
@@ -253,14 +276,6 @@ export class Nodes {
     );
     return undefined;
   }
-  setNodesFixed(name, b) {
-    this.nodes.some(function(n) {
-      if (n.name === name) {
-        n.fixed(b);
-        return true;
-      }
-    });
-  }
   nodeFor(name) {
     for (let i = 0; i < this.nodes.length; ++i) {
       if (this.nodes[i].name == name) return this.nodes[i];
@@ -268,7 +283,7 @@ export class Nodes {
     return null;
   }
   nodeExists(connectionContainer) {
-    return this.nodes.findIndex(function(node) {
+    return this.nodes.findIndex(function (node) {
       return node.container === connectionContainer;
     });
   }
@@ -277,9 +292,12 @@ export class Nodes {
     for (let i = 0; i < this.nodes.length; ++i) {
       if (this.nodes[i].normals) {
         if (
-          this.nodes[i].normals.some(function(normal, j) {
+          this.nodes[i].normals.some(function (normal, j) {
             if (normal.container === connectionContainer && i !== j) {
-              normalInfo = { nodesIndex: i, normalsIndex: j };
+              normalInfo = {
+                nodesIndex: i,
+                normalsIndex: j
+              };
               return true;
             }
             return false;
@@ -295,11 +313,11 @@ export class Nodes {
     if (Object.prototype.toString.call(nodes) !== "[object Array]") {
       nodes = [nodes];
     }
-    this.nodes.forEach(function(d) {
+    this.nodes.forEach(function (d) {
       localStorage[d.name] = JSON.stringify({
         x: Math.round(d.x),
         y: Math.round(d.y),
-        fixed: d.fixed & 1 ? 1 : 0
+        fixed: d.fixed
       });
     });
   }
@@ -413,24 +431,23 @@ export class Nodes {
       fixed,
       properties
     );
-    this.nodes.push(obj);
-    return obj;
+    return this.add(obj);
   }
   clearHighlighted() {
     for (let i = 0; i < this.nodes.length; ++i) {
       this.nodes[i].highlighted = false;
     }
   }
-  initialize(nodeInfo, localStorage, width, height) {
+
+  initialize(nodeInfo, width, height, localStorage) {
+    this.nodes.length = 0;
     let nodeCount = Object.keys(nodeInfo).length;
     let yInit = 50;
     let animate = false;
     for (let id in nodeInfo) {
       let name = utils.nameFromId(id);
       // if we have any new nodes, animate the force graph to position them
-      let position = localStorage[name]
-        ? JSON.parse(localStorage[name])
-        : undefined;
+      let position = localStorage[name] ? JSON.parse(localStorage[name]) : undefined;
       if (!position) {
         animate = true;
         position = {
@@ -439,7 +456,7 @@ export class Nodes {
           ),
           y: Math.round(
             height / 2 +
-              (Math.sin(this.nodes.length / (Math.PI * 2.0)) * height) / 4
+            (Math.sin(this.nodes.length / (Math.PI * 2.0)) * height) / 4
           ),
           fixed: false
         };
@@ -448,6 +465,7 @@ export class Nodes {
         position.y = 200 - yInit;
         yInit *= -1;
       }
+      position.fixed = position.fixed ? true : false;
       let parts = id.split("/");
       this.addUsing(
         id,
@@ -458,10 +476,9 @@ export class Nodes {
         position.y,
         name,
         undefined,
-        position.fixed,
-        {}
+        position.fixed, {}
       );
     }
     return animate;
   }
-}
+}
\ No newline at end of file


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