GUI -- Fixed turbulent behavior of incoming nodes with no fixed position.

Change-Id: Ic73c0c8b91bd5ab07faec84ffcd0b67d2e357b29
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index aeaad2d..2dd2a05 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -24,14 +24,33 @@
     opacity: 0.5;
 }
 
+
 /* NODES */
 
-#topo svg .node.device {
-    stroke: none;
-    stroke-width: 1.5px;
+#topo svg .node {
     cursor: pointer;
 }
 
+#topo svg .node.selected rect,
+#topo svg .node.selected circle {
+    filter: url(#blue-glow);
+}
+
+/* for debugging */
+#topo svg .node circle.debug {
+    fill: white;
+    stroke: red;
+}
+
+#topo svg .node text {
+    pointer-events: none;
+}
+
+/* Device Nodes */
+
+#topo svg .node.device {
+}
+
 #topo svg .node.device rect {
     stroke-width: 1.5px;
 }
@@ -54,31 +73,28 @@
     fill: #03c;
 }
 
-#topo svg .node.host {
-    fill: #846;
-}
-
 /* note: device is offline without the 'online' class */
 #topo svg .node.device text {
     fill: #aaa;
     font: 10pt sans-serif;
-    pointer-events: none;
 }
 
 #topo svg .node.device.online text {
     fill: white;
 }
 
+
+/* Host Nodes */
+
+#topo svg .node.host {
+    fill: #846;
+}
+
 #topo svg .node.host text {
     fill: #846;
     font: 9pt sans-serif;
-    pointer-events: none;
 }
 
-#topo svg .node.selected rect,
-#topo svg .node.selected circle {
-    filter: url(#blue-glow);
-}
 
 /* LINKS */
 
@@ -91,20 +107,13 @@
     stroke-width: 6px;
 }
 
-/* for debugging */
-#topo svg .node circle.debug {
-    fill: white;
-    stroke: red;
-}
 
-
-/* detail topo-detail pane */
+/* Fly-in details pane */
 
 #topo-detail {
 /* gets base CSS from .fpanel in floatPanel.css */
 }
 
-
 #topo-detail h2 {
     margin: 8px 4px;
     color: black;
@@ -128,7 +137,6 @@
 }
 
 #topo-detail td.value {
-
 }
 
 #topo-detail hr {
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index a23f48d..681562e 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -127,7 +127,8 @@
         P: togglePorts,
         U: unpin,
 
-        X: requestPath
+        Z: requestPath,
+        X: cancelMonitor
     };
 
     // state variables
@@ -518,6 +519,13 @@
         sendMessage('requestPath', payload);
     }
 
+    function cancelMonitor() {
+        var payload = {
+                id: "need_the_intent_id"  // FIXME: where are we storing this?
+            };
+        sendMessage('cancelMonitor', payload);
+    }
+
     // request details for the selected element
     function requestDetails() {
         var data = getSel(0).obj,
@@ -701,18 +709,57 @@
 
     function positionNode(node) {
         var meta = node.metaUi,
-            x = 0,
-            y = 0;
+            x = meta && meta.x,
+            y = meta && meta.y,
+            xy;
 
-        if (meta) {
-            x = meta.x;
-            y = meta.y;
-        }
+        // If we have [x,y] already, use that...
         if (x && y) {
             node.fixed = true;
+            node.x = x;
+            node.y = y;
+            return;
         }
-        node.x = x || network.view.width() / 2;
-        node.y = y || network.view.height() / 2;
+
+        // Note: Placing incoming unpinned nodes at exactly the same point
+        //        (center of the view) causes them to explode outwards when
+        //        the force layout kicks in. So, we spread them out a bit
+        //        initially, to provide a more serene layout convergence.
+        //       Additionally, if the node is a host, we place it near
+        //        the device it is connected to.
+
+        function spread(s) {
+            return Math.floor((Math.random() * s) - s/2);
+        }
+
+        function randDim(dim) {
+            return dim / 2 + spread(dim * 0.7071);
+        }
+
+        function rand() {
+            return {
+                x: randDim(network.view.width()),
+                y: randDim(network.view.height())
+            };
+        }
+
+        function near(node) {
+            var min = 12,
+                dx = spread(12),
+                dy = spread(12);
+            return {
+                x: node.x + min + dx,
+                y: node.y + min + dy
+            };
+        }
+
+        function getDevice(cp) {
+            var d = network.lookup[cp.device];
+            return d || rand();
+        }
+
+        xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
+        $.extend(node, xy);
     }
 
     function iconUrl(d) {