Repository: qpid-dispatch
Updated Branches:
  refs/heads/master 0d6dbac78 -> 3d79fda11


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/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 b99e2ef..c94eb81 100644
--- a/console/stand-alone/plugin/html/qdrTopology.html
+++ b/console/stand-alone/plugin/html/qdrTopology.html
@@ -196,6 +196,39 @@ table.popupTable tr.header {
 table.popupTable td {
     padding: 0 4px;
 }
+
+.graticule {
+  fill: none;
+  stroke: #777;
+  stroke-width: .5px;
+  stroke-opacity: .5;
+}
+
+g.geo path.land {
+  fill: #dcedf7;
+  stroke: #000;
+  stroke-opacity: 1;
+  stroke-width: 1px;
+}
+
+.boundary {
+  fill: none;
+  stroke: #333;
+  stroke-width: 5px;
+}
+
+.panel-group {
+    margin-bottom: 0;
+}
+
+span.map-label {
+    display: inline-block;
+    width: 4em;
+}
+#main_container {
+     padding-left: 0;
+     padding-right: 0;
+}
 </style>
 <div class="qdrTopology" ng-controller="QDR.TopologyController">
     <div class="legend-container page-menu navbar-collapse collapse">
@@ -251,6 +284,20 @@ table.popupTable td {
             <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="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>
+                            </li>
+                            <li>
+                                <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>
     </div>
     <div class="diagram">

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/plugin/js/amqp/connection.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/amqp/connection.js 
b/console/stand-alone/plugin/js/amqp/connection.js
index db21e01..b1eee5a 100644
--- a/console/stand-alone/plugin/js/amqp/connection.js
+++ b/console/stand-alone/plugin/js/amqp/connection.js
@@ -271,8 +271,9 @@ class ConnectionManager {
       this.connection = rhea.connect(c);
     }).bind(this));
   }
-  sendMgmtQuery(operation) {
-    return this.send([], '/$management', operation);
+  sendMgmtQuery(operation, to) {
+    to = to || '/$management';
+    return this.send([], to, operation);
   }
   sendQuery(toAddr, entity, attrs, operation) {
     operation = operation || 'QUERY';

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/plugin/js/amqp/topology.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/amqp/topology.js 
b/console/stand-alone/plugin/js/amqp/topology.js
index e208a6f..6e806c9 100644
--- a/console/stand-alone/plugin/js/amqp/topology.js
+++ b/console/stand-alone/plugin/js/amqp/topology.js
@@ -74,48 +74,104 @@ class Topology {
   get() {
     return new Promise((function (resolve, reject) {
       this.connection.sendMgmtQuery('GET-MGMT-NODES')
-        .then((function (response) {
-          response = response.response;
+        .then((function (results) {
+          let response = results.response;
           if (Object.prototype.toString.call(response) === '[object Array]') {
-            var workInfo = {};
             // if there is only one node, it will not be returned
             if (response.length === 0) {
               var parts = this.connection.getReceiverAddress().split('/');
               parts[parts.length - 1] = '$management';
               response.push(parts.join('/'));
             }
-            for (var i = 0; i < response.length; ++i) {
-              workInfo[response[i]] = {};
-            }
-            var gotResponse = function (nodeName, entity, response) {
-              workInfo[nodeName][entity] = response;
+            let finish = function (workInfo) {
+              this._nodeInfo = utils.copy(workInfo);
+              this.onDone(this._nodeInfo);
+              resolve(this._nodeInfo);
             };
-            var q = d3.queue(this.connection.availableQeueuDepth());
-            for (var id in workInfo) {
-              for (var entity in this.entityAttribs) {
-                q.defer((this.q_fetchNodeInfo).bind(this), id, entity, 
this.entityAttribs[entity], q, gotResponse);
-              }
-            }
-            q.await((function () {
-              // filter out nodes that have no connection info
-              if (this.filtering) {
-                for (var id in workInfo) {
-                  if (!(workInfo[id].connection)) {
-                    this.flux = true;
-                    delete workInfo[id];
+            let connectedToEdge = function (response, workInfo) {
+              let routerId = null;
+              if (response.length === 1) {
+                let parts = response[0].split('/');
+                // we are connected to an edge router
+                if (parts[1] === '_edge') {
+                  // find the role:edge connection
+                  let conn = workInfo[response[0]].connection;
+                  if (conn) {
+                    let roleIndex = conn.attributeNames.indexOf('role');
+                    for (let i=0; i<conn.results.length; i++) {
+                      if (conn.results[i][roleIndex] === 'edge') {
+                        let container = utils.valFor(conn.attributeNames, 
conn.results[i], 'container');
+                        return utils.idFromName(container, '_topo');
+                      }
+                    }
                   }
                 }
               }
-              this._nodeInfo = utils.copy(workInfo);
-              this.onDone(this._nodeInfo);
-              resolve(this._nodeInfo);
-            }).bind(this));
+              return routerId;
+            };
+            this.doget(response)
+              .then( function (workInfo) {
+                // test for edge case
+                let routerId = connectedToEdge(response, workInfo);
+                if (routerId) {
+                  let edgeId = response[0];
+                  this.connection.sendMgmtQuery('GET-MGMT-NODES', routerId)
+                    .then((function (results) {
+                      let response = results.response;
+                      if (Object.prototype.toString.call(response) === 
'[object Array]') {
+                        // special case of edge case:
+                        // we are connected to an edge router that is 
connected to
+                        // a router that is not connected to any other 
interior routers
+                        if (response.length === 0) {
+                          response = [routerId];
+                        }
+                        this.doget(response)
+                          .then( function (workInfo) {
+                            finish.call(this, workInfo);
+                          }.bind(this));
+
+                      }
+                    }).bind(this));
+                } else {
+                  finish.call(this, workInfo);
+                }
+              }.bind(this));
           }
         }).bind(this), function (error) {
           reject(error);
         });
     }).bind(this));
   }
+  doget(ids) {
+    return new Promise((function (resolve) {
+      let workInfo = {};
+      for (var i = 0; i < ids.length; ++i) {
+        workInfo[ids[i]] = {};
+      }
+      var gotResponse = function (nodeName, entity, response) {
+        workInfo[nodeName][entity] = response;
+      };
+      var q = d3.queue(this.connection.availableQeueuDepth());
+      for (var id in workInfo) {
+        for (var entity in this.entityAttribs) {
+          q.defer((this.q_fetchNodeInfo).bind(this), id, entity, 
this.entityAttribs[entity], q, gotResponse);
+        }
+      }
+      q.await((function () {
+        // filter out nodes that have no connection info
+        if (this.filtering) {
+          for (var id in workInfo) {
+            if (!(workInfo[id].connection)) {
+              this.flux = true;
+              delete workInfo[id];
+            }
+          }
+        }
+        resolve(workInfo);
+      }).bind(this));
+    }).bind(this));
+  }
+
   onDone(result) {
     clearTimeout(this._getTimer);
     if (this.updating)

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/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 7b10c57..c94b498 100644
--- a/console/stand-alone/plugin/js/amqp/utilities.js
+++ b/console/stand-alone/plugin/js/amqp/utilities.js
@@ -103,13 +103,22 @@ var utils = {
   },
   // extract the name of the router from the router id
   nameFromId: function (id) {
-    // the router id looks like 'amqp:/topo/0/routerName/$management'
+    // the router id looks like 'amqp:/_topo/0/routerName/$management'
     var parts = id.split('/');
     // handle cases where the router name contains a /
-    parts.splice(0, 3); // remove amqp, topo, 0
+    parts.splice(0, parts.length - 2); // remove amqp, _topo, 0
     parts.pop(); // remove $management
     return parts.join('/');
   },
+
+  // construct a router id given a router name and type (_topo or _edge)
+  idFromName: function (name, type) {
+    let parts = ['amqp:', type, name, '$management'];
+    if (type === '_topo')
+      parts.splice(2, 0, '0');
+    return parts.join('/');
+  },
+
   // calculate the average rate of change per second for a list of fields on 
the given obj
   // store the historical raw values in storage[key] for future rate calcs
   // keep 'history' number of historical values

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/plugin/js/chord/data.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/data.js 
b/console/stand-alone/plugin/js/chord/data.js
index d9625aa..6116436 100644
--- a/console/stand-alone/plugin/js/chord/data.js
+++ b/console/stand-alone/plugin/js/chord/data.js
@@ -114,7 +114,7 @@ class ChordData { // eslint-disable-line no-unused-vars
             if (link.linkType === 'endpoint' && link.linkDir === 'out' && 
!link.owningAddr.startsWith('Ltemp.')) {
               // keep track of the raw egress values as well as their ingress 
and egress routers and the address
               for (let j = 0; j < ingressRouters.length; j++) {
-                let messages = link.ingressHistogram[j];
+                let messages = link.ingressHistogram ? 
link.ingressHistogram[j] : 0;
                 if (messages) {
                   values.push({
                     ingress: ingressRouters[j],

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/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 d7f4110..dd722bb 100644
--- a/console/stand-alone/plugin/js/topology/links.js
+++ b/console/stand-alone/plugin/js/topology/links.js
@@ -79,6 +79,8 @@ export class Links {
     let source = 0;
     let client = 1.0;
     for (let id in nodeInfo) {
+      let parts = id.split('/');
+      let routerType = parts[1]; // _topo || _edge
       let onode = nodeInfo[id];
       if (!onode['connection'])
         continue;
@@ -93,81 +95,85 @@ export class Links {
         let properties = connection.properties || {};
         let dir = connection.dir;
         if (role == 'inter-router') {
+          // there are already 2 router nodes, just link them
           let connId = connection.container;
           let target = getContainerIndex(connId, nodeInfo, this.QDRService);
           if (target >= 0) {
             this.getLink(source, target, dir, '', source + '-' + target);
           }
-        } /* else if (role == "normal" || role == "on-demand" || role === 
"route-container")*/ {
-          // not an connection between routers, but an external connection
-          let name = this.QDRService.utilities.nameFromId(id) + '.' + 
connection.identity;
+        }
+        // handle external connections
+        let name = this.QDRService.utilities.nameFromId(id) + '.' + 
connection.identity;
+        // is this connection for a router connected to an edge router
+        if (role == 'edge' && routerType === '_edge') {
+          name = connection.container;
+          role = 'inter-router';
+        }
 
-          // if we have any new clients, animate the force graph to position 
them
-          let position = localStorage[name] ? JSON.parse(localStorage[name]) : 
undefined;
-          if ((typeof position == 'undefined')) {
-            animate = true;
-            position = {
-              x: Math.round(nodes.get(source).x + 40 * Math.sin(client / 
(Math.PI * 2.0))),
-              y: Math.round(nodes.get(source).y + 40 * Math.cos(client / 
(Math.PI * 2.0))),
-              fixed: false
-            };
-            //QDRLog.debug("new client pos (" + position.x + ", " + position.y 
+ ")")
-          }// else QDRLog.debug("using previous location")
-          if (position.y > height) {
-            position.y = Math.round(nodes.get(source).y + 40 + Math.cos(client 
/ (Math.PI * 2.0)));
+        // if we have any new clients, animate the force graph to position them
+        let position = localStorage[name] ? JSON.parse(localStorage[name]) : 
undefined;
+        if ((typeof position == 'undefined')) {
+          animate = true;
+          position = {
+            x: Math.round(nodes.get(source).x + 40 * Math.sin(client / 
(Math.PI * 2.0))),
+            y: Math.round(nodes.get(source).y + 40 * Math.cos(client / 
(Math.PI * 2.0))),
+            fixed: false
+          };
+        }
+        if (position.y > height) {
+          position.y = Math.round(nodes.get(source).y + 40 + Math.cos(client / 
(Math.PI * 2.0)));
+        }
+        let existingNodeIndex = nodes.nodeExists(connection.container);
+        let normalInfo = nodes.normalExists(connection.container);
+        let node = nodes.getOrCreateNode(id, name, role, nodeInfo, 
nodes.getLength(), position.x, position.y, connection.container, j, 
position.fixed, properties);
+        let nodeType = this.QDRService.utilities.isAConsole(properties, 
connection.identity, role, node.key) ? 'console' : 'client';
+        let cdir = getLinkDir(id, connection, onode, this.QDRService);
+        if (existingNodeIndex >= 0) {
+          // make a link between the current router (source) and the existing 
node
+          this.getLink(source, existingNodeIndex, dir, 'small', 
connection.name);
+        } else if (normalInfo.nodesIndex) {
+          // get node index of node that contained this connection in its 
normals array
+          let normalSource = this.getLinkSource(normalInfo.nodesIndex);
+          if (normalSource >= 0) {
+            if (cdir === 'unknown')
+              cdir = dir;
+            node.cdir = cdir;
+            nodes.add(node);
+            // create link from original node to the new node
+            this.getLink(this.links[normalSource].source, nodes.getLength()-1, 
cdir, 'small', connection.name);
+            // create link from this router to the new node
+            this.getLink(source, nodes.getLength()-1, cdir, 'small', 
connection.name);
+            // remove the old node from the normals list
+            
nodes.get(normalInfo.nodesIndex).normals.splice(normalInfo.normalsIndex, 1);
           }
-          let existingNodeIndex = nodes.nodeExists(connection.container);
-          let normalInfo = nodes.normalExists(connection.container);
-          let node = nodes.getOrCreateNode(id, name, role, nodeInfo, 
nodes.getLength(), position.x, position.y, connection.container, j, 
position.fixed, properties);
-          let nodeType = this.QDRService.utilities.isAConsole(properties, 
connection.identity, role, node.key) ? 'console' : 'client';
-          let cdir = getLinkDir(id, connection, onode, this.QDRService);
-          if (existingNodeIndex >= 0) {
-            // make a link between the current router (source) and the 
existing node
-            this.getLink(source, existingNodeIndex, dir, 'small', 
connection.name);
-          } else if (normalInfo.nodesIndex) {
-            // get node index of node that contained this connection in its 
normals array
-            let normalSource = this.getLinkSource(normalInfo.nodesIndex);
-            if (normalSource >= 0) {
-              if (cdir === 'unknown')
-                cdir = dir;
-              node.cdir = cdir;
+        } else if (role === 'normal' || role === 'edge') {
+        // normal nodes can be collapsed into a single node if they are all 
the same dir
+          if (cdir !== 'unknown') {
+            node.user = connection.user;
+            node.isEncrypted = connection.isEncrypted;
+            node.host = connection.host;
+            node.connectionId = connection.identity;
+            node.cdir = cdir;
+            // determine arrow direction by using the link directions
+            if (!normalsParent[nodeType+cdir]) {
+              normalsParent[nodeType+cdir] = node;
               nodes.add(node);
-              // create link from original node to the new node
-              this.getLink(this.links[normalSource].source, 
nodes.getLength()-1, cdir, 'small', connection.name);
-              // create link from this router to the new node
-              this.getLink(source, nodes.getLength()-1, cdir, 'small', 
connection.name);
-              // remove the old node from the normals list
-              
nodes.get(normalInfo.nodesIndex).normals.splice(normalInfo.normalsIndex, 1);
-            }
-          } else if (role === 'normal') {
-          // normal nodes can be collapsed into a single node if they are all 
the same dir
-            if (cdir !== 'unknown') {
-              node.user = connection.user;
-              node.isEncrypted = connection.isEncrypted;
-              node.host = connection.host;
-              node.connectionId = connection.identity;
-              node.cdir = cdir;
-              // determine arrow direction by using the link directions
-              if (!normalsParent[nodeType+cdir]) {
-                normalsParent[nodeType+cdir] = node;
-                nodes.add(node);
-                node.normals = [node];
-                // now add a link
-                this.getLink(source, nodes.getLength() - 1, cdir, 'small', 
connection.name);
-                client++;
-              } else {
-                normalsParent[nodeType+cdir].normals.push(node);
-              }
+              node.normals = [node];
+              // now add a link
+              this.getLink(source, nodes.getLength() - 1, cdir, 'small', 
connection.name);
+              client++;
             } else {
-              node.id = nodes.getLength() - 1 + unknowns.length;
-              unknowns.push(node);
+              normalsParent[nodeType+cdir].normals.push(node);
             }
           } else {
-            nodes.add(node);
-            // now add a link
-            this.getLink(source, nodes.getLength() - 1, dir, 'small', 
connection.name);
-            client++;
+            node.id = nodes.getLength() - 1 + unknowns.length;
+            unknowns.push(node);
           }
+        } else {
+          nodes.add(node);
+          // now add a link
+          this.getLink(source, nodes.getLength() - 1, dir, 'small', 
connection.name);
+          client++;
         }
       }
       source++;

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/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
new file mode 100644
index 0000000..9886245
--- /dev/null
+++ b/console/stand-alone/plugin/js/topology/map.js
@@ -0,0 +1,255 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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 angular d3 topojson Promise */
+const maxnorth = 84;
+const maxsouth = 60;
+const MAPOPTIONSKEY = 'QDRMapOptions';
+const MAPPOSITIONKEY = 'QDRMapPosition';
+const defaultLandColor = '#A3D3E0';
+const defaultOceanColor = '#FFFFFF';
+
+export class BackgroundMap { // eslint-disable-line no-unused-vars
+  constructor($scope, notifyFn) {
+    this.$scope = $scope;
+    this.initialized = false;
+    this.notify = notifyFn;
+    $scope.mapOptions = angular.fromJson(localStorage[MAPOPTIONSKEY]) || 
{areaColor: defaultLandColor, oceanColor: defaultOceanColor};
+    initLast(this);
+  }
+  updateLandColor(color) {
+    localStorage[MAPOPTIONSKEY] = JSON.stringify(this.$scope.mapOptions);
+    d3.select('g.geo path.land')
+      .style('fill', color)
+      .style('stroke', d3.rgb(color).darker());
+  }
+  updateOceanColor(color) {
+    localStorage[MAPOPTIONSKEY] = JSON.stringify(this.$scope.mapOptions);
+    if (!color)
+      color = this.$scope.mapOptions.oceanColor;
+    d3.select('g.geo rect.ocean')
+      .style('fill', color);
+    if (this.$scope.legend.status.mapOpen) {
+      d3.select('#main_container')
+        .style('background-color', color);
+    } else {
+      d3.select('#main_container')
+        .style('background-color', '#FFF');
+    }
+  }
+
+  init($scope, svg, width, height) {
+    return new Promise( (function (resolve, reject) {
+
+      this.svg = svg;
+      this.width = width;
+      this.height = height;
+      // track last translation and scale event we processed
+      this.rotate = 20;
+      this.scaleExtent = [1, 10];
+
+      // handle ui events to change the colors
+      $scope.$watch('mapOptions.areaColor', function (newValue, oldValue) {
+        if (newValue !== oldValue) {
+          this.updateLandColor(newValue);
+        }
+      }.bind(this));
+      $scope.$watch('mapOptions.oceanColor', function (newValue, oldValue) {
+        if (newValue !== oldValue) {
+          this.updateOceanColor(newValue);
+        }
+      }.bind(this));
+
+      // setup the projection with some defaults
+      this.projection = d3.geo.mercator()
+        .rotate([this.rotate,0])
+        .scale(1)
+        .translate([width/2, height/2]);
+
+      // this path will hold the land coordinates once they are loaded
+      this.geoPath = d3.geo.path()
+        .projection(this.projection);
+
+      // 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];
+
+      this.projection
+        .scale(this.scaleExtent[0]);
+      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
+        .on('zoom', this.zoomed.bind(this));
+
+      this.geo = svg.append('g')
+        .attr('class', 'geo')
+        .style('opacity', this.$scope.legend.status.mapOpen ? '1': '0');
+
+      this.geo.append('rect')
+        .attr('class', 'ocean')
+        .attr('width', width)
+        .attr('height', height)
+        .attr('fill', '#FFF');
+
+      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/world-110m.json', function(error, world) {
+      d3.json('plugin/data/countries.json', function(error, world) {
+        if (error) 
+          reject(error);
+
+        this.geo.append('path')
+          .datum(topojson.feature(world, world.objects.countries))
+          .attr('class', 'land')
+          .attr('d', this.geoPath)
+          .style('stroke', d3.rgb(this.$scope.mapOptions.areaColor).darker());
+
+        this.updateLandColor(this.$scope.mapOptions.areaColor);
+        this.updateOceanColor(this.$scope.mapOptions.oceanColor);
+
+        // restore map rotate, scale, translate
+        this.restoreState();
+
+        // draw with current positions
+        this.geo.selectAll('.land')
+          .attr('d', this.geoPath);
+
+        this.initialized = true;
+        resolve();
+      }.bind(this));
+    }.bind(this)));
+  }
+
+  setMapOpacity(opacity) {
+    opacity = opacity ? 1 : 0;
+    if (this.geo)
+      this.geo.style('opacity', opacity);
+  }
+  restoreState() {
+    this.projection.rotate([this.lastProjection.rotate, 0]);
+    this.projection.translate(this.lastProjection.translate);
+    this.projection.scale(this.lastProjection.scale);
+    this.zoom.scale(this.lastProjection.scale);
+    this.zoom.translate(this.lastProjection.translate);
+  }
+
+  // stop responding to pan/zoom events
+  cancelZoom() {
+    this.saveProjection();
+  }
+
+  // tell the svg to respond to mouse pan/zoom events
+  restartZoom() {
+    this.svg.call(this.zoom)
+      .on('dblclick.zoom', null);
+    this.restoreState();
+    this.last.scale = null;
+  }
+
+  getXY(lon, lat) {
+    return this.projection([lon, lat]);
+  }
+  getLonLat(x, y) {
+    return this.projection.invert([x, y]);
+  }
+
+  zoomed() {
+    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],
+        yaw = this.projection.rotate()[0],
+        tp = this.projection.translate();
+      // zoomed
+      if (scale != this.last.scale) {
+        // get the mouse's x,y relative to the svg
+        let top = d3.select('#main_container').node().offsetTop;
+        let left = d3.select('#main_container').node().offsetLeft;
+        let mx = d3.event.sourceEvent.clientX - left;
+        let my = d3.event.sourceEvent.clientY - top - 1;
+
+        // get the lon,lat at the mouse position
+        let lonlat = this.projection.invert([mx, my]);
+
+        // do the requested scale operation
+        this.projection.scale(scale);
+
+        // get the lonlat that is under the mouse after the scale
+        let lonlat1 = this.projection.invert([mx, my]);
+        // calc the distance to rotate based on change in longitude
+        dx = lonlat1[0] - lonlat[0];
+        // calc the distance to translate based on change in lattitude
+        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]);
+
+        // 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]);
+      } else {
+        // rotate instead of translate in the x direction
+        
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) 
+          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]);
+      }
+      this.last.scale = scale;
+      this.last.translate = t;
+      this.saveProjection();
+      this.notify();
+    }
+    // update the land path with our current projection
+    this.geo.selectAll('.land')
+      .attr('d', this.geoPath);
+  }
+  saveProjection() {
+    if (this.projection) {
+      this.lastProjection.rotate = this.projection.rotate()[0];
+      this.lastProjection.scale = this.projection.scale();
+      this.lastProjection.translate = this.projection.translate();
+      localStorage[MAPPOSITIONKEY] = JSON.stringify(this.lastProjection);
+    }
+  }
+}
+
+// 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];
+}
+function initLast(map) {
+  map.last = {translate: [0,0], scale: null};
+}
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/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 45ef23f..4be7ff7 100644
--- a/console/stand-alone/plugin/js/topology/nodes.js
+++ b/console/stand-alone/plugin/js/topology/nodes.js
@@ -17,8 +17,9 @@ specific language governing permissions and limitations
 under the License.
 */
 
+/* global d3 Promise */
 export class Node {
-  constructor(id, name, nodeType, properties, routerId, x, y, nodeIndex, 
resultIndex, fixed, connectionContainer) {
+  constructor(QDRService, id, name, nodeType, properties, routerId, x, y, 
nodeIndex, resultIndex, fixed, connectionContainer) {
     this.key = id;
     this.name = name;
     this.nodeType = nodeType;
@@ -31,8 +32,108 @@ export class Node {
     this.fixed = !!+fixed;
     this.cls = '';
     this.container = connectionContainer;
+    this.isConsole = QDRService.utilities.isConsole(this);
+    this.isArtemis = QDRService.utilities.isArtemis(this);
   }
+  title () {
+    let x = '';
+    if (this.normals && this.normals.length > 1)
+      x = ' x ' + this.normals.length;
+    if (this.isConsole)
+      return 'Dispatch console' + x;
+    else if (this.isArtemis)
+      return 'Broker - Artemis' + x;
+    else if (this.properties.product == 'qpid-cpp')
+      return 'Broker - qpid-cpp' + x;
+    else if (this.nodeType === 'edge')
+      return 'Edge Router';
+    else if (this.cdir === 'in')
+      return 'Sender' + x;
+    else if (this.cdir === 'out')
+      return 'Receiver' + x;
+    else if (this.cdir === 'both')
+      return 'Sender/Receiver' + x;
+    else if (this.nodeType === 'normal')
+      return 'client' + x;
+    else if (this.nodeType === 'on-demand')
+      return 'broker';
+    else if (this.properties.product) {
+      return this.properties.product;
+    }
+    else {
+      return '';
+    }
+  }
+  toolTip (QDRService) {
+    return new Promise( (function (resolve) {
+      if (this.nodeType === 'normal' || this.nodeType === 'edge') {
+        resolve(this.clientTooltip());
+      } else
+        this.routerTooltip(QDRService)
+          .then( function (toolTip) {
+            resolve(toolTip);
+          });
+    }.bind(this)));
+  }
+
+  clientTooltip () {
+    let type = this.title();
+    let title = `<table 
class="popupTable"><tr><td>Type</td><td>${type}</td></tr>`;
+    if (!this.normals || this.normals.length < 2)
+      title += `<tr><td>Host</td><td>${this.host}</td></tr>`;
+    else {
+      title += `<tr><td>Count</td><td>${this.normals.length}</td></tr>`;
+    }
+    title += '</table>';
+    return title;
+  }
+
+  routerTooltip (QDRService) {
+    return new Promise( (function (resolve) {
+      QDRService.management.topology.ensureEntities(this.key, [
+        {entity: 'listener', attrs: ['role', 'port', 'http']},
+        {entity: 'router', attrs: ['name', 'version', 'hostName']}
+      ], function () {
+        // update all the router title text
+        let nodes = QDRService.management.topology.nodeInfo();
+        let node = nodes[this.key];
+        let listeners = node['listener'];
+        let router = node['router'];
+        let r = QDRService.utilities.flatten(router.attributeNames, 
router.results[0]);
+        let title = '<table class="popupTable">';
+        title += ('<tr><td>Router</td><td>' + r.name + '</td></tr>');
+        if (r.hostName)
+          title += ('<tr><td>Host Name</td><td>' + r.hostHame + '</td></tr>');
+        title += ('<tr><td>Version</td><td>' + r.version + '</td></tr>');
+        let ports = [];
+        for (let l=0; l<listeners.results.length; l++) {
+          let listener = 
QDRService.utilities.flatten(listeners.attributeNames, listeners.results[l]);
+          if (listener.role === 'normal') {
+            ports.push(listener.port+'');
+          }
+        }
+        if (ports.length > 0) {
+          title += ('<tr><td>Ports</td><td>' + ports.join(', ') + 
'</td></tr>');
+        }
+        title += '</table>';
+        resolve(title);
+        return title;
+      }.bind(this));
+    }.bind(this)));
+  }
+
 }
+const nodeProperties = {
+  // router types
+  'inter-router': {radius: 28, linkDistance: [150, 70], charge: [-1800, -900]},
+  '_edge':  {radius: 20, linkDistance: [110, 55], charge: [-1350, -900]},
+  '_topo': {radius: 28, linkDistance: [150, 70], charge: [-1800, -900]},
+  // generated nodes from connections. key is from connection.role
+  'normal':       {radius: 15, linkDistance: [75, 40], charge: [-900, -900]},
+  'on-demand':    {radius: 15, linkDistance: [75, 40], charge: [-900, -900]},
+  'route-container': {radius: 15, linkDistance: [75, 40], charge: [-900, 
-900]},
+  'edge':  {radius: 20, linkDistance: [110, 55], charge: [-1350, -900]}
+};
 
 export class Nodes {
   constructor(QDRService, logger) {
@@ -40,6 +141,39 @@ export class Nodes {
     this.QDRService = QDRService;
     this.logger = logger;
   }
+  static radius(type) {
+    if (nodeProperties[type].radius)
+      return nodeProperties[type].radius;
+    console.log(`Requested radius for unknown node type: ${type}`);
+    return 15;
+  }
+  static maxRadius() {
+    let max = 0;
+    for (let key in nodeProperties) {
+      max = Math.max(max, nodeProperties[key].radius);
+    }
+    return max;
+  }
+  // vary the following force graph attributes based on nodeCount
+  static forceScale (nodeCount, minmax) {
+    let count = Math.max(Math.min(nodeCount, 80), 6);
+    let x = d3.scale.linear()
+      .domain([6,80])
+      .range(minmax);
+    return x(count);
+  }
+  linkDistance (d, nodeCount) {
+    let range = nodeProperties[d.target.nodeType].linkDistance;
+    return Nodes.forceScale(nodeCount, range);
+  }
+  charge (d, nodeCount) {
+    let charge = nodeProperties[d.nodeType].charge;
+    return Nodes.forceScale(nodeCount, charge);
+  }
+  gravity (d, nodeCount) {
+    return Nodes.forceScale(nodeCount, [0.0001, 0.1]);
+  }
+
   getLength () {
     return this.nodes.length;
   }
@@ -54,6 +188,8 @@ export class Nodes {
     this.nodes.some(function (n) {
       if (n.name === name) {
         n.fixed = b;
+        if (!b)
+          n.lat = n.lon = null;
         return true;
       }
     });
@@ -86,7 +222,12 @@ export class Nodes {
     }
     return normalInfo;
   }
-  savePositions () {
+  savePositions (nodes) {
+    if (!nodes)
+      nodes = this.nodes;
+    if (Object.prototype.toString.call(nodes) !== '[object Array]') {
+      nodes = [nodes];
+    }
     this.nodes.forEach( function (d) {
       localStorage[d.name] = JSON.stringify({
         x: Math.round(d.x),
@@ -95,6 +236,46 @@ export class Nodes {
       });
     });
   }
+  // Convert node's x,y coordinates to longitude, lattitude
+  saveLonLat (backgroundMap, nodes) {
+    if (!backgroundMap)
+      return;
+    // didn't pass nodes, use all nodes
+    if (!nodes)
+      nodes = this.nodes;
+    // passed a single node, wrap it in an array
+    if (Object.prototype.toString.call(nodes) !== '[object Array]') {
+      nodes = [nodes];
+    }
+    for (let i=0; i<nodes.length; i++) {
+      let n = nodes[i];
+      if (n.fixed) {
+        let lonlat = backgroundMap.getLonLat(n.x, n.y);
+        if (lonlat) {
+          n.lon = lonlat[0];
+          n.lat = lonlat[1];
+        }
+      } else {
+        n.lon = n.lat = null;
+      }
+    }
+  }
+  // convert all nodes' longitude,lattitude to x,y coordinates
+  setXY (backgroundMap) {
+    if (!backgroundMap)
+      return;
+    for (let i=0; i<this.nodes.length; i++) {
+      let n = this.nodes[i];
+      if (n.lon && n.lat) {
+        let xy = backgroundMap.getXY(n.lon, n.lat);
+        if (xy) {
+          n.x = n.px = xy[0];
+          n.y = n.py = xy[1];
+        }
+      }
+    }
+  }
+
   find (connectionContainer, properties, name) {
     properties = properties || {};
     for (let i=0; i<this.nodes.length; ++i) {
@@ -114,7 +295,7 @@ export class Nodes {
       return gotNode;
     }
     let routerId = this.QDRService.utilities.nameFromId(id);
-    return new Node(id, name, nodeType, properties, routerId, x, y, 
+    return new Node(this.QDRService, id, name, nodeType, properties, routerId, 
x, y, 
       nodeIndex, resultIndex, fixed, connectionContainer);
   }
   add (obj) {
@@ -153,10 +334,10 @@ export class Nodes {
         position.y = 200 - yInit;
         yInit *= -1;
       }
-      this.addUsing(id, name, 'inter-router', nodeInfo, this.nodes.length, 
position.x, position.y, name, undefined, position.fixed, {});
+      let parts = id.split('/');
+      this.addUsing(id, name, parts[1], nodeInfo, this.nodes.length, 
position.x, position.y, name, undefined, position.fixed, {});
     }
     return animate;
   }
-
 }
 

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/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 ed85f74..667f9ed 100644
--- a/console/stand-alone/plugin/js/topology/qdrTopology.js
+++ b/console/stand-alone/plugin/js/topology/qdrTopology.js
@@ -27,6 +27,7 @@ import { separateAddresses } from '../chord/filters.js';
 import { Nodes } from './nodes.js';
 import { Links } from './links.js';
 import { nextHop, connectionPopupHTML } from './topoUtils.js';
+import { BackgroundMap } from './map.js';
 /**
  * @module QDR
  */
@@ -36,25 +37,41 @@ export class TopologyController {
 
     let QDRLog = new QDRLogger($log, 'TopologyController');
     const TOPOOPTIONSKEY = 'topoOptions';
-    const radius = 25;
-    const radiusNormal = 15;
 
     //  - nodes is an array of router/client info. these are the circles
     //  - links is an array of connections between the routers. these are the 
lines with arrows
     let nodes = new Nodes(QDRService, QDRLog);
     let links = new Links(QDRService, QDRLog);
     let forceData = {nodes: nodes, links: links};
+
+    $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')
+      $scope.legendOptions.legendOpen = false;
+    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();
+          // 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];
 
-    $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || 
{showTraffic: false, trafficType: 'dots'};
     if (!$scope.legendOptions.trafficType)
       $scope.legendOptions.trafficType = 'dots';
-    $scope.legend = {status: {legendOpen: true, optionsOpen: true}};
+    $scope.legend = {status: {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, 
-      radius, forceData, $scope.legendOptions.trafficType, urlPrefix);
+      Nodes.radius('inter-router'), forceData, 
$scope.legendOptions.trafficType, urlPrefix);
 
     // the showTraaffic checkbox was just toggled (or initialized)
     $scope.$watch('legend.status.optionsOpen', function () {
@@ -72,21 +89,43 @@ export class TopologyController {
       localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
       if ($scope.legendOptions.showTraffic) {
         restart();
-        traffic.setAnimationType($scope.legendOptions.trafficType, 
separateAddresses, radius);
+        traffic.setAnimationType($scope.legendOptions.trafficType, 
separateAddresses, Nodes.radius('inter-router'));
         traffic.start();
       }
     });
+    $scope.$watch('legend.status.mapOpen', function (newvalue, oldvalue) {
+      $scope.legendOptions.mapOpen = $scope.legend.status.mapOpen;
+      localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
+      // map was shown
+      if ($scope.legend.status.mapOpen && backgroundMap.initialized) {
+        // respond to pan/zoom events
+        backgroundMap.restartZoom();
+        // set the main_container div's background color to the ocean color
+        backgroundMap.updateOceanColor();
+        d3.select('g.geo')
+          .style('opacity', 1);
+      } else {
+        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');
+      }
+    });
 
     // mouse event vars
     let selected_node = null,
       selected_link = null,
       mousedown_link = null,
-      mousedown_node = null,
       mouseover_node = null,
       mouseup_node = null,
       initial_mouse_down_position = null;
 
     $scope.schema = 'Not connected';
+    $scope.current_node = null,
+    $scope.mousedown_node = null,
 
     $scope.contextNode = null; // node that is associated with the current 
context menu
     $scope.isRight = function(mode) {
@@ -98,6 +137,7 @@ export class TopologyController {
         $scope.contextNode.fixed = b;
         nodes.setNodesFixed($scope.contextNode.name, b);
         nodes.savePositions();
+        nodes.saveLonLat(backgroundMap, $scope.contextNode);
       }
       restart();
     };
@@ -126,12 +166,6 @@ export class TopologyController {
       $('.contextMenu').fadeOut(200);
     });
 
-    const radii = {
-      'inter-router': 25,
-      'normal': 15,
-      'on-demand': 15,
-      'route-container': 15,
-    };
     let svg, lsvg;  // main svg and legend svg
     let force;
     let animate = false; // should the force graph organize itself when it is 
displayed
@@ -183,29 +217,6 @@ export class TopologyController {
     if (width <= 0 || height <= 0)
       return;
 
-    // vary the following force graph attributes based on nodeCount
-    // <= 6 routers returns min, >= 80 routers returns max, interpolate 
linearly
-    var forceScale = function(nodeCount, min, max) {
-      let count = Math.max(Math.min(nodeCount, 80), 6);
-      let x = d3.scale.linear()
-        .domain([6,80])
-        .range([min, max]);
-      //QDRLog.debug("forceScale(" + nodeCount + ", " + min + ", " + max + "  
returns " + x(count) + " " + x(nodeCount))
-      return x(count);
-    };
-    var linkDistance = function (d, nodeCount) {
-      if (d.target.nodeType === 'inter-router')
-        return forceScale(nodeCount, 150, 70);
-      return forceScale(nodeCount, 75, 40);
-    };
-    var charge = function (d, nodeCount) {
-      if (d.nodeType === 'inter-router')
-        return forceScale(nodeCount, -1800, -900);
-      return -900;
-    };
-    var gravity = function (d, nodeCount) {
-      return forceScale(nodeCount, 0.0001, 0.1);
-    };
     // initialize the nodes and links array from the 
QDRService.topology._nodeInfo object
     var initForceGraph = function() {
       forceData.nodes = nodes = new Nodes(QDRService, QDRLog);
@@ -219,7 +230,6 @@ export class TopologyController {
       selected_node = null;
       selected_link = null;
 
-      nodes.savePositions();
       d3.select('#SVG_ID').remove();
       svg = d3.select('#topology')
         .append('svg')
@@ -230,24 +240,38 @@ export class TopologyController {
           clearPopups();
         });
 
+      /*
+      var graticule = d3.geo.graticule();
+      geo.append('path')
+        .datum(graticule)
+        .attr('class', 'graticule')
+        .attr('d', geoPath);
+      */
+
       // the legend
       d3.select('#topo_svg_legend svg').remove();
       lsvg = d3.select('#topo_svg_legend')
         .append('svg')
         .attr('id', 'svglegend');
       lsvg = lsvg.append('svg:g')
-        .attr('transform', `translate( ${(radii['inter-router'] + 
2)},${(radii['inter-router'] + 2)})`)
+        .attr('transform', `translate(${Nodes.maxRadius()}, 
${Nodes.maxRadius()})`)
         .selectAll('g');
 
       // mouse event vars
       mousedown_link = null;
-      mousedown_node = null;
+      $scope.mousedown_node = null;
       mouseup_node = null;
 
       // initialize the list of nodes
       forceData.nodes = nodes = new Nodes(QDRService, QDRLog);
       animate = nodes.initialize(nodeInfo, localStorage, width, height);
       nodes.savePositions();
+      // 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);
+        });
 
       // initialize the list of links
       let unknowns = [];
@@ -261,12 +285,12 @@ export class TopologyController {
         .nodes(nodes.nodes)
         .links(links.links)
         .size([width, height])
-        .linkDistance(function(d) { return linkDistance(d, nodeCount); })
-        .charge(function(d) { return charge(d, nodeCount); })
+        .linkDistance(function(d) { return nodes.linkDistance(d, nodeCount); })
+        .charge(function(d) { return nodes.charge(d, nodeCount); })
         .friction(.10)
-        .gravity(function(d) { return gravity(d, nodeCount); })
+        .gravity(function(d) { return nodes.gravity(d, nodeCount); })
         .on('tick', tick)
-        .on('end', function () {nodes.savePositions();})
+        .on('end', function () {nodes.savePositions(); 
nodes.saveLonLat(backgroundMap);})
         .start();
 
       // This section adds in the arrows
@@ -301,8 +325,8 @@ export class TopologyController {
       grad.append('stop').attr('offset', '50%').style('stop-color', '#F0F000');
 
       // handles to link and node element groups
-      path = svg.append('svg:g').selectAll('path'),
-      circle = svg.append('svg:g').selectAll('g');
+      path = svg.append('svg:g').attr('class', 'links').selectAll('path'),
+      circle = svg.append('svg:g').attr('class', 'nodes').selectAll('g');
 
       // app starts here
       restart(false);
@@ -339,7 +363,7 @@ export class TopologyController {
           setTimeout(continueForce, 100, extra);
         }
       };
-      continueForce(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.
@@ -367,7 +391,7 @@ export class TopologyController {
     };
 
     function resetMouseVars() {
-      mousedown_node = null;
+      $scope.mousedown_node = null;
       mouseover_node = null;
       mouseup_node = null;
       mousedown_link = null;
@@ -376,24 +400,17 @@ export class TopologyController {
     // update force layout (called automatically each iteration)
     function tick() {
       circle.attr('transform', function(d) {
-        let cradius;
-        if (d.nodeType == 'inter-router') {
-          cradius = d.left ? radius + 8 : radius;
-        } else {
-          cradius = d.left ? radiusNormal + 18 : radiusNormal;
-        }
-        d.x = Math.max(d.x, radiusNormal * 2);
-        d.y = Math.max(d.y, radiusNormal * 2);
-        d.x = Math.max(0, Math.min(width - cradius, d.x));
-        d.y = Math.max(0, Math.min(height - cradius, d.y));
+        let r = Nodes.radius(d.nodeType);
+        d.x = Math.max(Math.min(d.x, width - r), r);
+        d.y = Math.max(Math.min(d.y, height - r), r);
+
         return `translate(${d.x},${d.y})`;
       });
 
-      // draw directed edges with proper padding from node centers
+      // draw lines with arrows with proper padding from node centers
       path.attr('d', function(d) {
-        let sourcePadding, targetPadding, r;
-
-        r = d.target.nodeType === 'inter-router' ? radius : radiusNormal - 18;
+        let sourcePadding, targetPadding;
+        let r = Nodes.radius(d.target.nodeType);
         sourcePadding = targetPadding = 0;
         let dtx = Math.max(targetPadding, Math.min(width - r, d.target.x)),
           dty = Math.max(targetPadding, Math.min(height - r, d.target.y)),
@@ -568,12 +585,14 @@ export class TopologyController {
 
       appendCircle(g)
         .on('mouseover', function(d) {  // mouseover a circle
+          $scope.current_node = d;
           
QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
-          if (d.nodeType === 'normal') {
-            showClientTooltip(d, d3.event);
-          } else
-            showRouterTooltip(d, d3.event);
-          if (d === mousedown_node)
+          let e = d3.event;
+          d.toolTip(QDRService)
+            .then( function (toolTip) {
+              showToolTip(toolTip, e);
+            });
+          if (d === $scope.mousedown_node)
             return;
           // enlarge target node
           d3.select(this).attr('transform', 'scale(1.1)');
@@ -590,6 +609,7 @@ export class TopologyController {
           });
         })
         .on('mouseout', function() { // mouse out for a circle
+          $scope.current_node = null;
           // unenlarge target node
           d3.select('#popover-div')
             .style('display', 'none');
@@ -599,15 +619,18 @@ export class TopologyController {
           restart();
         })
         .on('mousedown', function(d) { // mouse down for circle
+          backgroundMap.cancelZoom();
+          $scope.current_node = d;
           if (d3.event.button !== 0) { // ignore all but left button
             return;
           }
-          mousedown_node = d;
+          $scope.mousedown_node = d;
           // mouse position relative to svg
           initial_mouse_down_position = 
d3.mouse(this.parentNode.parentNode.parentNode).slice();
         })
         .on('mouseup', function(d) {  // mouse up for circle
-          if (!mousedown_node)
+          backgroundMap.restartZoom();
+          if (!$scope.mousedown_node)
             return;
 
           selected_link = null;
@@ -624,25 +647,31 @@ export class TopologyController {
             cur_mouse[1] != initial_mouse_down_position[1]) {
             d.fixed = true;
             nodes.setNodesFixed(d.name, true);
+            nodes.savePositions(d);
+            nodes.saveLonLat(backgroundMap, d);
             resetMouseVars();
             restart();
             return;
           }
 
           // if this node was selected, unselect it
-          if (mousedown_node === selected_node) {
+          if ($scope.mousedown_node === selected_node) {
             selected_node = null;
           } else {
-            if (d.nodeType !== 'normal' && d.nodeType !== 'on-demand')
-              selected_node = mousedown_node;
+            if (d.nodeType !== 'normal' && 
+                d.nodeType !== 'on-demand' && 
+                d.nodeType !== 'edge' &&
+                d.nodeTYpe !== '_edge')
+              selected_node = $scope.mousedown_node;
           }
           clearAllHighlights();
-          mousedown_node = null;
+          $scope.mousedown_node = null;
           if (!$scope.$$phase) $scope.$apply();
           restart(false);
 
         })
         .on('dblclick', function(d) { // circle
+          d3.event.preventDefault();
           if (d.fixed) {
             d.fixed = false;
             nodes.setNodesFixed(d.name, false);
@@ -691,18 +720,19 @@ export class TopologyController {
       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', radiusNormal + 3)
-          .attr('y', Math.floor(radiusNormal / 2))
+          .attr('x', r + 4)
+          .attr('y', Math.floor((r / 2) - 4))
           .attr('class', 'subtext')
-          .text('x ' + d.normals.length);
+          .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 (!mousedown_node || !selected_node)
+      if (!$scope.mousedown_node || !selected_node)
         return;
 
       if (!start)
@@ -714,51 +744,54 @@ export class TopologyController {
     }
     let createLegend = function () {
       // dynamically create the legend based on which node types are present
-      // the legend
       d3.select('#topo_svg_legend svg').remove();
       lsvg = d3.select('#topo_svg_legend')
         .append('svg')
         .attr('id', 'svglegend');
       lsvg = lsvg.append('svg:g')
-        .attr('transform', `translate(${(radii['inter-router'] + 
2)},${(radii['inter-router'] + 2)})`)
+        .attr('transform', `translate(${Nodes.maxRadius()}, 
${Nodes.maxRadius()})`)
         .selectAll('g');
       let legendNodes = new Nodes(QDRService, 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, 1, 0, false, {
+        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, 2, 0, false, {}).cdir = 'in';
+        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, 3, 0, false, {}).cdir = 'out';
+        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, 4, 0, false, {}).cdir = 'both';
+        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, 5, 0, false, {
+        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, 6, 0, false,
+        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', 
'external-service', undefined, 0, 0, 7, 0, false,
+        legendNodes.addUsing('Service', 'Service', 'route-container', 
'external-service', undefined, 0, 0, 8, 0, false,
           {product: ' External Service'});
       }
       lsvg = lsvg.data(legendNodes.nodes, function(d) {
-        return d.key;
+        return d.key + d.name;
       });
+      let cury = 0;
       let lg = lsvg.enter().append('svg:g')
-        .attr('transform', function(d, i) {
-          // 45px between lines and add 10px space after 1st line
-          return 'translate(0, ' + (45 * i + (i > 0 ? 10 : 0)) + ')';
+        .attr('transform', function(d) {
+          let t = `translate(0, ${cury})`;
+          cury += (Nodes.radius(d.nodeType) * 2 + 10);
+          return t;
         });
 
       appendCircle(lg);
@@ -795,7 +828,7 @@ export class TopologyController {
       return g.append('svg:circle')
         .attr('class', 'node')
         .attr('r', function(d) {
-          return radii[d.nodeType];
+          return Nodes.radius(d.nodeType);
         })
         .attr('fill', function (d) {
           if (d.cdir === 'both' && !QDRService.utilities.isConsole(d)) {
@@ -819,11 +852,14 @@ export class TopologyController {
           return d.cdir == 'both';
         })
         .classed('inter-router', function(d) {
-          return d.nodeType == 'inter-router';
+          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);
         })
@@ -854,6 +890,8 @@ export class TopologyController {
             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')
@@ -866,6 +904,12 @@ export class TopologyController {
         .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);
         })
@@ -881,84 +925,20 @@ export class TopologyController {
             return '\ue901';
           } else if (d.nodeType === 'route-container') {
             return d.properties.product ? 
d.properties.product[0].toUpperCase() : 'S';
-          } else if (d.nodeType === 'normal')
+          } 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, 6) + '...' : d.name;
         });
     };
     let appendTitle = function(g) {
       g.append('svg:title').text(function(d) {
-        return generateTitle(d);
+        return d.title();
       });
     };
 
-    let generateTitle = function (d) {
-      let x = '';
-      if (d.normals && d.normals.length > 1)
-        x = ' x ' + d.normals.length;
-      if (QDRService.utilities.isConsole(d))
-        return 'Dispatch console' + x;
-      else if (QDRService.utilities.isArtemis(d))
-        return 'Broker - Artemis' + x;
-      else if (d.properties.product == 'qpid-cpp')
-        return 'Broker - qpid-cpp' + x;
-      else if (d.cdir === 'in')
-        return 'Sender' + x;
-      else if (d.cdir === 'out')
-        return 'Receiver' + x;
-      else if (d.cdir === 'both')
-        return 'Sender/Receiver' + x;
-      else if (d.nodeType === 'normal')
-        return 'client' + x;
-      else if (d.nodeType === 'on-demand')
-        return 'broker';
-      else if (d.properties.product) {
-        return d.properties.product;
-      }
-      else {
-        return '';
-      }
-    };
-
-    let showClientTooltip = function (d, event) {
-      let type = generateTitle(d);
-      let title = `<table 
class="popupTable"><tr><td>Type</td><td>${type}</td></tr>`;
-      if (!d.normals || d.normals.length < 2)
-        title += ('<tr><td>Host</td><td>' + d.host + '</td></tr>');
-      title += '</table>';
-      showToolTip(title, event);
-    };
-
-    let showRouterTooltip = function (d, event) {
-      QDRService.management.topology.ensureEntities(d.key, [
-        {entity: 'listener', attrs: ['role', 'port', 'http']},
-        {entity: 'router', attrs: ['name', 'version', 'hostName']}
-      ], function () {
-        // update all the router title text
-        let nodes = QDRService.management.topology.nodeInfo();
-        let node = nodes[d.key];
-        let listeners = node['listener'];
-        let router = node['router'];
-        let r = QDRService.utilities.flatten(router.attributeNames, 
router.results[0]);
-        let title = '<table class="popupTable">';
-        title += ('<tr><td>Router</td><td>' + r.name + '</td></tr>');
-        if (r.hostName)
-          title += ('<tr><td>Host Name</td><td>' + r.hostHame + '</td></tr>');
-        title += ('<tr><td>Version</td><td>' + r.version + '</td></tr>');
-        let ports = [];
-        for (let l=0; l<listeners.results.length; l++) {
-          let listener = 
QDRService.utilities.flatten(listeners.attributeNames, listeners.results[l]);
-          if (listener.role === 'normal') {
-            ports.push(listener.port+'');
-          }
-        }
-        if (ports.length > 0) {
-          title += ('<tr><td>Ports</td><td>' + ports.join(', ') + 
'</td></tr>');
-        }
-        title += '</table>';
-        showToolTip(title, event);
-      });
-    };
     let showToolTip = function (title, event) {
       // show the tooltip
       $timeout ( function () {
@@ -1027,16 +1007,7 @@ export class TopologyController {
           savedKeys[key] = nodeInfo[key]['connection'].results.length;
       }
     }
-    // we are about to leave the page, save the node positions
-    $rootScope.$on('$locationChangeStart', function() {
-      //QDRLog.debug("locationChangeStart");
-      nodes.savePositions();
-    });
-    // When the DOM element is removed from the page,
-    // AngularJS will trigger the $destroy event on
-    // the scope
-    $scope.$on('$destroy', function() {
-      //QDRLog.debug("scope on destroy");
+    function destroy () {
       nodes.savePositions();
       QDRService.management.topology.setUpdateEntities([]);
       QDRService.management.topology.stopUpdating();
@@ -1047,6 +1018,18 @@ export class TopologyController {
       d3.select('#SVG_ID').remove();
       window.removeEventListener('resize', resize);
       traffic.stop();
+      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() {
+      destroy();
+    });
+    // we are about to leave the page, save the node positions
+    $rootScope.$on('$locationChangeStart', function() {
+      destroy();
     });
 
     function handleInitialUpdate() {

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/vendor-js.txt
----------------------------------------------------------------------
diff --git a/console/stand-alone/vendor-js.txt 
b/console/stand-alone/vendor-js.txt
index 8fca75b..935d9a4 100644
--- a/console/stand-alone/vendor-js.txt
+++ b/console/stand-alone/vendor-js.txt
@@ -33,10 +33,14 @@ node_modules/angular-ui-bootstrap/dist/ui-bootstrap-tpls.js
 node_modules/angular-bootstrap-checkbox/angular-bootstrap-checkbox.js
 node_modules/bootstrap/dist/js/bootstrap.min.js
 node_modules/d3/d3.min.js
+node_modules/d3-array/dist/d3-array.js
+node_modules/d3-geo/dist/d3-geo.js
+node_modules/d3-geo-projection/dist/d3-geo-projection.js
 node_modules/d3-queue/build/d3-queue.min.js
 node_modules/d3-time/build/d3-time.min.js
 node_modules/d3-time-format/build/d3-time-format.min.js
 node_modules/d3-path/build/d3-path.min.js
+node_modules/topojson-client/dist/topojson-client.js
 node_modules/c3/c3.min.js
 node_modules/notifyjs-browser/dist/notify.js
 node_modules/patternfly/dist/js/patternfly.min.js


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

Reply via email to