GUI -- store ref to node/link selection in backing data.
- tweaking force-layout parameters; now host-to-host intent path is highlighted.
- injectTestEvent() now uses recursion to look for appropriate json files.
- implemented updateHost() event.
- some refactoring cleanup in topo2.js

Change-Id: I888f05032d3c9df6470bd4d2f399f61efb9dbd46
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_1_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_1_onos.json
index 1776f94..9874ec7 100644
--- a/web/gui/src/main/webapp/json/ev/simple/ev_1_onos.json
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_1_onos.json
@@ -7,7 +7,8 @@
     "labels": [
       "0000ffffffff0008",
       "FF:FF:FF:FF:00:08",
-      "sw-8"
+      "sw-8",
+      ""
     ],
     "metaUi": {
       "x": 400,
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_2_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_2_onos.json
index 4f9b32a..87f35d6 100644
--- a/web/gui/src/main/webapp/json/ev/simple/ev_2_onos.json
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_2_onos.json
@@ -7,7 +7,8 @@
     "labels": [
       "0000ffffffff0003",
       "FF:FF:FF:FF:00:03",
-      "sw-3"
+      "sw-3",
+      ""
     ],
     "metaUi": {
       "x": 800,
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_3_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_3_onos.json
index 3312682..ac521c4 100644
--- a/web/gui/src/main/webapp/json/ev/simple/ev_3_onos.json
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_3_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffff0003/21-of:0000ffffffff0008/20",
+    "type": "direct",
+    "linkWidth": 2,
     "src": "of:0000ffffffff0003",
     "srcPort": "21",
     "dst": "of:0000ffffffff0008",
     "dstPort": "20",
-    "type": "infra",
-    "linkWidth": 2,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_4_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_4_onos.json
index 51fdb8c..993570b 100644
--- a/web/gui/src/main/webapp/json/ev/simple/ev_4_onos.json
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_4_onos.json
@@ -1,16 +1,17 @@
 {
   "event": "addHost",
   "payload": {
-    "id": "00:00:00:00:00:03/-1",
+    "id": "0E:2A:69:30:13:86/-1",
+    "ingress": "0E:2A:69:30:13:86/-1/0-of:0000ffffffff0003/2",
+    "egress": "of:0000ffffffff0003/2-0E:2A:69:30:13:86/-1/0",
     "cp": {
       "device": "of:0000ffffffff0003",
-      "port": 1
+      "port": 2
     },
     "labels": [
-      "10.0.0.3",
-      "00:00:00:00:00:03"
+      "unknown",
+      "0E:2A:69:30:13:86"
     ],
-    "metaUi": {
-    }
+    "props": {}
   }
 }
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_5_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_5_onos.json
index a6489b2..17864a6 100644
--- a/web/gui/src/main/webapp/json/ev/simple/ev_5_onos.json
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_5_onos.json
@@ -1,16 +1,17 @@
 {
   "event": "addHost",
   "payload": {
-    "id": "00:00:00:00:00:08/-1",
+    "id": "A6:96:E5:03:52:5F/-1",
+    "ingress": "A6:96:E5:03:52:5F/-1/0-of:0000ffffffff0008/1",
+    "egress": "of:0000ffffffff0008/1-A6:96:E5:03:52:5F/-1/0",
     "cp": {
       "device": "of:0000ffffffff0008",
       "port": 1
     },
     "labels": [
-      "10.0.0.8",
-      "00:00:00:00:00:08"
+      "unknown",
+      "A6:96:E5:03:52:5F"
     ],
-    "metaUi": {
-    }
+    "props": {}
   }
 }
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_6_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_6_onos.json
new file mode 100644
index 0000000..3a3ea9e
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_6_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "updateHost",
+  "payload": {
+    "id": "0E:2A:69:30:13:86/-1",
+    "ingress": "0E:2A:69:30:13:86/-1/0-of:0000ffffffff0003/2",
+    "egress": "of:0000ffffffff0003/2-0E:2A:69:30:13:86/-1/0",
+    "cp": {
+      "device": "of:0000ffffffff0003",
+      "port": 2
+    },
+    "labels": [
+      "10.0.0.13",
+      "0E:2A:69:30:13:86"
+    ],
+    "props": {}
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_7_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_7_onos.json
new file mode 100644
index 0000000..0fb56fa
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_7_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "updateHost",
+  "payload": {
+    "id": "A6:96:E5:03:52:5F/-1",
+    "ingress": "A6:96:E5:03:52:5F/-1/0-of:0000ffffffff0008/1",
+    "egress": "of:0000ffffffff0008/1-A6:96:E5:03:52:5F/-1/0",
+    "cp": {
+      "device": "of:0000ffffffff0008",
+      "port": 1
+    },
+    "labels": [
+      "10.0.0.17",
+      "A6:96:E5:03:52:5F"
+    ],
+    "props": {}
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_8_ui.json b/web/gui/src/main/webapp/json/ev/simple/ev_8_ui.json
new file mode 100644
index 0000000..188bc58
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_8_ui.json
@@ -0,0 +1,6 @@
+{
+  "event": "doUiThing",
+  "payload": {
+    "id": "xyyzy"
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/simple/scenario.json b/web/gui/src/main/webapp/json/ev/simple/scenario.json
index d24626f..e320413 100644
--- a/web/gui/src/main/webapp/json/ev/simple/scenario.json
+++ b/web/gui/src/main/webapp/json/ev/simple/scenario.json
@@ -1,6 +1,7 @@
 {
   "comments": [
-    "Add two devices and one link (auto), and two hosts."
+    "Add two devices and one link (auto), and two hosts.",
+    "Then update the two hosts (with IP address labels)."
   ],
   "title": "Simple Startup Scenario",
   "params": {
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index a5bf661..acd0bc9 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -53,13 +53,18 @@
     fill: #846;
 }
 
-#topo svg .node text {
-    stroke: none;
+#topo svg .node.device text {
     fill: white;
     font: 10pt sans-serif;
     pointer-events: none;
 }
 
+#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);
@@ -73,7 +78,7 @@
 
 #topo svg .link.showPath {
     stroke: #f00;
-    stroke-width: 4px;
+    stroke-width: 6px;
 }
 
 /* for debugging */
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 18aa917..5bffcdd 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -82,18 +82,21 @@
             opt: 'img/opt.png'
         },
         force: {
-            note: 'node.class or link.class is used to differentiate',
+            note_for_links: 'link.type is used to differentiate',
             linkDistance: {
-                infra: 200,
-                host: 40
+                direct: 100,
+                optical: 120,
+                hostLink: 20
             },
             linkStrength: {
-                infra: 1.0,
-                host: 1.0
+                direct: 1.0,
+                optical: 1.0,
+                hostLink: 1.0
             },
+            note_for_nodes: 'node.class is used to differentiate',
             charge: {
-                device: -400,
-                host: -100
+                device: -8000,
+                host: -300
             },
             pad: 20,
             translate: function() {
@@ -204,39 +207,37 @@
             evn = ++sc.evNumber,
             pfx = sc.evDir + sc.ctx + sc.evPrefix + evn,
             onosUrl = pfx + sc.evOnos,
-            uiUrl = pfx + sc.evUi;
-
-        tryOnosEvent(onosUrl, uiUrl);
+            uiUrl = pfx + sc.evUi,
+            stack = [
+                { url: onosUrl, cb: handleServerEvent },
+                { url: uiUrl, cb: handleUiEvent }
+            ];
+        recurseFetchEvent(stack, evn);
     }
 
-    // TODO: tryOnosEvent/tryUiEvent folded into recursive function.
-    function tryOnosEvent(onosUrl, uiUrl) {
-        var v = scenario.view;
-        d3.json(onosUrl, function(err, data) {
+    function recurseFetchEvent(stack, evn) {
+        var v = scenario.view,
+            frame;
+        if (stack.length === 0) {
+            v.alert('Error:\n\nNo event #' + evn + ' found.');
+            return;
+        }
+        frame = stack.shift();
+
+        d3.json(frame.url, function (err, data) {
             if (err) {
                 if (err.status === 404) {
-                    tryUiEvent(uiUrl);
+                    // if we didn't find the data, try the next stack frame
+                    recurseFetchEvent(stack, evn);
                 } else {
-                    v.alert('non-404 error:\n\n' + onosUrl + '\n\n' + err);
+                    v.alert('non-404 error:\n\n' + frame.url + '\n\n' + err);
                 }
             } else {
-                testDebug('loaded: ' + onosUrl);
-                handleServerEvent(data);
+                testDebug('loaded: ' + frame.url);
+                frame.cb(data);
             }
         });
-    }
 
-    function tryUiEvent(uiUrl) {
-        var v = scenario.view;
-        d3.json(uiUrl, function(err, data) {
-            if (err) {
-                v.alert('Error:\n\n' + uiUrl + '\n\n' +
-                        err.status + ': ' + err.statusText);
-            } else {
-                testDebug('loaded: ' + uiUrl);
-                handleUiEvent(data);
-            }
-        });
     }
 
     function handleUiEvent(data) {
@@ -261,19 +262,15 @@
     function cycleLabels() {
         deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1) ? 0 : deviceLabelIndex + 1;
 
-        function niceLabel(label) {
-            return (label && label.trim()) ? label : '.';
-        }
-
         network.nodes.forEach(function (d) {
             if (d.class !== 'device') { return; }
 
-            var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0,
-                node = d3.select('#' + safeId(d.id)),
+            var label = niceLabel(deviceLabel(d)),
+                node = d.el,
                 box;
 
             node.select('text')
-                .text(niceLabel(d.labels[idx]))
+                .text(label)
                 .style('opacity', 0)
                 .transition()
                 .style('opacity', 1);
@@ -359,18 +356,18 @@
         updateLink: stillToImplement,
         removeLink: stillToImplement,
         addHost: addHost,
-        updateHost: stillToImplement,
+        updateHost: updateHost,
         removeHost: stillToImplement,
         showPath: showPath
     };
 
     function addDevice(data) {
         var device = data.payload,
-            node = createDeviceNode(device);
+            nodeData = createDeviceNode(device);
         note('addDevice', device.id);
 
-        network.nodes.push(node);
-        network.lookup[node.id] = node;
+        network.nodes.push(nodeData);
+        network.lookup[nodeData.id] = nodeData;
         updateNodes();
         network.force.start();
     }
@@ -380,7 +377,7 @@
             lnk = createLink(link);
 
         if (lnk) {
-            note('addLink', lnk.id);
+            note('addLink', link.id);
 
             network.links.push(lnk);
             network.lookup[lnk.id] = lnk;
@@ -393,8 +390,8 @@
         var host = data.payload,
             node = createHostNode(host),
             lnk;
-
         note('addHost', node.id);
+
         network.nodes.push(node);
         network.lookup[host.id] = node;
         updateNodes();
@@ -409,6 +406,15 @@
         network.force.start();
     }
 
+    function updateHost(data) {
+        var host = data.payload,
+            hostData = network.lookup[host.id];
+        note('updateHost', host.id);
+
+        $.extend(hostData, host);
+        updateNodes();
+    }
+
     function showPath(data) {
         var links = data.payload.links,
             s = [ data.event + "\n" + links.length ];
@@ -420,7 +426,7 @@
         links.forEach(function (d, i) {
             var link = network.lookup[d];
             if (link) {
-                d3.select('#' + link.svgId).classed('showPath', true);
+                link.el.classed('showPath', true);
             }
         });
 
@@ -432,7 +438,7 @@
     function stillToImplement(data) {
         var p = data.payload;
         note(data.event, p.id);
-        //network.view.alert('Not yet implemented: "' + data.event + '"');
+        network.view.alert('Not yet implemented: "' + data.event + '"');
     }
 
     function unknownEvent(data) {
@@ -454,7 +460,7 @@
     function createHostLink(host) {
         var src = host.id,
             dst = host.cp.device,
-            id = host.id,
+            id = host.ingress,
             srcNode = network.lookup[src],
             dstNode = network.lookup[dst],
             lnk;
@@ -466,31 +472,32 @@
             return null;
         }
 
+        // Compose link ...
         lnk = {
-            svgId: safeId(src) + '-' + safeId(dst),
             id: id,
             source: srcNode,
             target: dstNode,
             class: 'link',
+            type: 'hostLink',
             svgClass: 'link hostLink',
             x1: srcNode.x,
             y1: srcNode.y,
             x2: dstNode.x,
             y2: dstNode.y,
             width: 1
-        };
+        }
         return lnk;
     }
 
     function createLink(link) {
-        var type = link.type,
+        // start with the link object as is
+        var lnk = link,
+            type = link.type,
             src = link.src,
             dst = link.dst,
-            id = link.id,
             w = link.linkWidth,
             srcNode = network.lookup[src],
-            dstNode = network.lookup[dst],
-            lnk;
+            dstNode = network.lookup[dst];
 
         if (!(srcNode && dstNode)) {
             // TODO: send warning message back to server on websocket
@@ -499,19 +506,18 @@
             return null;
         }
 
-        lnk = {
-                svgId: safeId(src) + '-' + safeId(dst),
-                id: id,
-                source: srcNode,
-                target: dstNode,
-                class: 'link',
-                svgClass: type ? 'link ' + type : 'link',
-                x1: srcNode.x,
-                y1: srcNode.y,
-                x2: dstNode.x,
-                y2: dstNode.y,
-                width: w
-            };
+        // Augment as needed...
+        $.extend(lnk, {
+            source: srcNode,
+            target: dstNode,
+            class: 'link',
+            svgClass: type ? 'link ' + type : 'link',
+            x1: srcNode.x,
+            y1: srcNode.y,
+            x2: dstNode.x,
+            y2: dstNode.y,
+            width: w
+        });
         return lnk;
     }
 
@@ -532,7 +538,6 @@
         var entering = link.enter()
             .append('line')
             .attr({
-                id: function (d) { return d.svgId; },
                 class: function (d) { return d.svgClass; },
                 x1: function (d) { return d.x1; },
                 y1: function (d) { return d.y1; },
@@ -548,8 +553,13 @@
             });
 
         // augment links
-        // TODO: add src/dst port labels etc.
+        entering.each(function (d) {
+            var link = d3.select(this);
+            // provide ref to element selection from backing data....
+            d.el = link;
 
+            // TODO: add src/dst port labels etc.
+        });
 
         // operate on both existing and new links, if necessary
         //link .foo() .bar() ...
@@ -577,7 +587,6 @@
 
         // cache label array length
         network.deviceLabelCount = device.labels.length;
-
         return node;
     }
 
@@ -587,13 +596,16 @@
 
         // Augment as needed...
         node.class = 'host';
+        if (!node.type) {
+            // TODO: perhaps type would be: {phone, tablet, laptop, endstation} ?
+            node.type = 'endstation';
+        }
         node.svgClass = 'node host';
         // TODO: consider placing near its switch, if [x,y] not defined
         positionNode(node);
 
         // cache label array length
         network.hostLabelCount = host.labels.length;
-
         return node;
     }
 
@@ -645,11 +657,27 @@
         return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
     }
 
+    function hostLabel(d) {
+        var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
+        return d.labels[idx];
+    }
+    function deviceLabel(d) {
+        var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
+        return d.labels[idx];
+    }
+    function niceLabel(label) {
+        return (label && label.trim()) ? label : '.';
+    }
+
     function updateNodes() {
         node = nodeG.selectAll('.node')
             .data(network.nodes, function (d) { return d.id; });
 
         // operate on existing nodes, if necessary
+        //  update host labels
+        node.filter('.host').select('text')
+            .text(hostLabel);
+
         //node .foo() .bar() ...
 
         // operate on entering nodes:
@@ -671,9 +699,12 @@
         entering.filter('.device').each(function (d) {
             var node = d3.select(this),
                 icon = iconUrl(d),
-                idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0,
+                label = niceLabel(deviceLabel(d)),
                 box;
 
+            // provide ref to element from backing data....
+            d.el = node;
+
             node.append('rect')
                 .attr({
                     'rx': 5,
@@ -681,7 +712,7 @@
                 });
 
             node.append('text')
-                .text(d.labels[idx])
+                .text(label)
                 .attr('dy', '1.1em');
 
             box = adjustRectToFitText(node);
@@ -717,16 +748,19 @@
         // augment host nodes...
         entering.filter('.host').each(function (d) {
             var node = d3.select(this),
-                idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0,
                 box;
 
+            // provide ref to element from backing data....
+            d.el = node;
+
             node.append('circle')
                 .attr('r', 8);     // TODO: define host circle radius
 
             // TODO: are we attaching labels to hosts?
             node.append('text')
-                .text(d.labels[idx])
-                .attr('dy', '1.1em');
+                .text(hostLabel)
+                .attr('dy', '1.3em')
+                .attr('text-anchor', 'middle');
 
             // debug function to show the modelled x,y coordinates of nodes...
             if (debug('showNodeXY')) {
@@ -964,16 +998,15 @@
         link = linkG.selectAll('.link');
         node = nodeG.selectAll('.node');
 
+        function chrg(d) {
+            return fcfg.charge[d.class] || -12000;
+        }
         function ldist(d) {
-            return 2 * 30;
-            //return fcfg.linkDistance[d.class] || 150;
+            return fcfg.linkDistance[d.type] || 50;
         }
         function lstrg(d) {
-            return 2 * 0.6;
-            //return fcfg.linkStrength[d.class] || 1;
-        }
-        function lchrg(d) {
-            return fcfg.charge[d.class] || -200;
+            // 0.0 - 1.0
+            return fcfg.linkStrength[d.type] || 1.0;
         }
 
         function selectCb(d, self) {
@@ -1003,23 +1036,13 @@
             .size(forceDim)
             .nodes(network.nodes)
             .links(network.links)
-            .gravity(0.3)
-            .charge(-15000)
-            .friction(0.1)
-            //.charge(lchrg)
+            .gravity(0.4)
+            .friction(0.7)
+            .charge(chrg)
             .linkDistance(ldist)
             .linkStrength(lstrg)
             .on('tick', tick);
 
-            // TVUE
-            //.gravity(0.3)
-            //.charge(-15000)
-            //.friction(0.1)
-            //.linkDistance(function(d) { return d.value * 30; })
-            //.linkStrength(function(d) { return d.value * 0.6; })
-            //.size([w, h])
-            //.start();
-
         network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
     }