GUI -- Added updateLink and removeLink event handling.

Change-Id: Iae2d1f47bd4849e8ac80bebe721a2aa0ad5f4964
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_10_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_10_onos.json
new file mode 100644
index 0000000..f09cc9b
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_10_onos.json
@@ -0,0 +1,15 @@
+{
+  "event": "updateLink",
+  "payload": {
+    "id": "of:0000ffffffff0003/21-of:0000ffffffff0008/20",
+    "type": "direct",
+    "linkWidth": 6,
+    "src": "of:0000ffffffff0003",
+    "srcPort": "21",
+    "dst": "of:0000ffffffff0008",
+    "dstPort": "20",
+    "props" : {
+      "BW": "512 Gb"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_11_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_11_onos.json
new file mode 100644
index 0000000..447ded3
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_11_onos.json
@@ -0,0 +1,15 @@
+{
+  "event": "updateLink",
+  "payload": {
+    "id": "of:0000ffffffff0003/21-of:0000ffffffff0008/20",
+    "type": "direct",
+    "linkWidth": 2,
+    "src": "of:0000ffffffff0003",
+    "srcPort": "21",
+    "dst": "of:0000ffffffff0008",
+    "dstPort": "20",
+    "props" : {
+      "BW": "80 Gb"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_12_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_12_onos.json
new file mode 100644
index 0000000..96018f3
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_12_onos.json
@@ -0,0 +1,15 @@
+{
+  "event": "removeLink",
+  "payload": {
+    "id": "of:0000ffffffff0003/21-of:0000ffffffff0008/20",
+    "type": "direct",
+    "linkWidth": 2,
+    "src": "of:0000ffffffff0003",
+    "srcPort": "21",
+    "dst": "of:0000ffffffff0008",
+    "dstPort": "20",
+    "props" : {
+      "BW": "80 Gb"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_10_ui.json b/web/gui/src/main/webapp/json/ev/simple/ev_13_ui.json
similarity index 63%
rename from web/gui/src/main/webapp/json/ev/simple/ev_10_ui.json
rename to web/gui/src/main/webapp/json/ev/simple/ev_13_ui.json
index 188bc58..9d6e737 100644
--- a/web/gui/src/main/webapp/json/ev/simple/ev_10_ui.json
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_13_ui.json
@@ -1,5 +1,5 @@
 {
-  "event": "doUiThing",
+  "event": "noop",
   "payload": {
     "id": "xyyzy"
   }
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 9874ec7..8656a90 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
@@ -11,8 +11,8 @@
       ""
     ],
     "metaUi": {
-      "x": 400,
-      "y": 280
+      "x": 520,
+      "y": 350
     }
   }
 }
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 73013a4..56651c0 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
@@ -11,8 +11,8 @@
       ""
     ],
     "metaUi": {
-      "x": 400,
-      "y": 280
+      "x": 520,
+      "y": 350
     }
   }
 }
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 ac521c4..6f390b5 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
@@ -9,7 +9,7 @@
     "dst": "of:0000ffffffff0008",
     "dstPort": "20",
     "props" : {
-      "BW": "70 G"
+      "BW": "70 Gb"
     }
   }
 }
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 19d6190..5fb8869 100644
--- a/web/gui/src/main/webapp/json/ev/simple/scenario.json
+++ b/web/gui/src/main/webapp/json/ev/simple/scenario.json
@@ -10,13 +10,16 @@
   "description": [
     "1. add device [8] (offline)",
     "2. add device [3] (offline)",
-    "3. update device [8] (online)",
-    "4. update device [3] (online)",
+    "3. update device [8] (online, label3 change)",
+    "4. update device [3] (online, label3 change)",
     "5. add link [3] --> [8]",
     "6. add host (to [3])",
     "7. add host (to [8])",
     "8. update host[3] (IP now 10.0.0.13)",
     "9. update host[8] (IP now 10.0.0.17)",
+    "10. update link (increase width, update props)",
+    "11. update link (reduce width, update props)",
+    "12. remove link",
     ""
   ]
 }
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index f6a8456..94b2e9e 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -260,36 +260,6 @@
         bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
     }
 
-    function updateDeviceLabel(d) {
-        var label = niceLabel(deviceLabel(d)),
-            node = d.el,
-            box;
-
-        node.select('text')
-            .text(label)
-            .style('opacity', 0)
-            .transition()
-            .style('opacity', 1);
-
-        box = adjustRectToFitText(node);
-
-        node.select('rect')
-            .transition()
-            .attr(box);
-
-        node.select('image')
-            .transition()
-            .attr('x', box.x + config.icons.xoff)
-            .attr('y', box.y + config.icons.yoff);
-    }
-
-    function updateHostLabel(d) {
-        var label = hostLabel(d),
-            host = d.el;
-
-        host.select('text').text(label);
-    }
-
     function cycleLabels() {
         deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1)
             ? 0 : deviceLabelIndex + 1;
@@ -371,10 +341,10 @@
         addLink: addLink,
         addHost: addHost,
         updateDevice: updateDevice,
-        updateLink: stillToImplement,
+        updateLink: updateLink,
         updateHost: updateHost,
         removeDevice: stillToImplement,
-        removeLink: stillToImplement,
+        removeLink: removeLink,
         removeHost: stillToImplement,
         showPath: showPath
     };
@@ -429,6 +399,18 @@
         }
     }
 
+    function updateLink(data) {
+        var link = data.payload,
+            id = link.id,
+            linkData = network.lookup[id];
+        if (linkData) {
+            $.extend(linkData, link);
+            updateLinkState(linkData);
+        } else {
+            logicError('updateLink lookup fail. ID = "' + id + '"');
+        }
+    }
+
     function updateHost(data) {
         var host = data.payload,
             id = host.id,
@@ -441,6 +423,17 @@
         }
     }
 
+    function removeLink(data) {
+        var link = data.payload,
+            id = link.id,
+            linkData = network.lookup[id];
+        if (linkData) {
+            removeLinkElement(linkData);
+        } else {
+            logicError('removeLink lookup fail. ID = "' + id + '"');
+        }
+    }
+
     function showPath(data) {
         var links = data.payload.links,
             s = [ data.event + "\n" + links.length ];
@@ -483,74 +476,81 @@
         return 'translate(' + x + ',' + y + ')';
     }
 
+    function missMsg(what, id) {
+        return '\n[' + what + '] "' + id + '" missing ';
+    }
+
+    function linkEndPoints(srcId, dstId) {
+        var srcNode = network.lookup[srcId],
+            dstNode = network.lookup[dstId],
+            sMiss = !srcNode ? missMsg('src', srcId) : '',
+            dMiss = !dstNode ? missMsg('dst', dstId) : '';
+
+        if (sMiss || dMiss) {
+            logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
+            return null;
+        }
+        return {
+            source: srcNode,
+            target: dstNode,
+            x1: srcNode.x,
+            y1: srcNode.y,
+            x2: dstNode.x,
+            y2: dstNode.y
+        };
+    }
+
     function createHostLink(host) {
         var src = host.id,
             dst = host.cp.device,
             id = host.ingress,
-            srcNode = network.lookup[src],
-            dstNode = network.lookup[dst],
-            lnk;
+            lnk = linkEndPoints(src, dst);
 
-        if (!dstNode) {
-            logicError('switch not on map for link\n\n' +
-                        'src = ' + src + '\ndst = ' + dst);
+        if (!lnk) {
             return null;
         }
 
-        // Compose link ...
-        lnk = {
+        // Synthesize link ...
+        $.extend(lnk, {
             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) {
-        // start with the link object as is
-        var lnk = link,
-            type = link.type,
-            src = link.src,
-            dst = link.dst,
-            w = link.linkWidth,
-            srcNode = network.lookup[src],
-            dstNode = network.lookup[dst];
-
-        if (!(srcNode && dstNode)) {
-            logicError('nodes not on map for link\n\n' +
-            'src = ' + src + '\ndst = ' + dst);
-            return null;
-        }
-
-        // 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
+            linkWidth: 1
         });
         return lnk;
     }
 
-    function linkWidth(w) {
-        // w is number of links between nodes. Scale appropriately.
-        // TODO: use a d3.scale (linear, log, ... ?)
-        return w * 1.2;
+    function createLink(link) {
+        var lnk = linkEndPoints(link.src, link.dst),
+            type = link.type;
+
+        if (!lnk) {
+            return null;
+        }
+
+        // merge in remaining data
+        $.extend(lnk, link, {
+            class: 'link',
+            svgClass: type ? 'link ' + type : 'link'
+        });
+        return lnk;
     }
 
+    var widthRatio = 1.4,
+        linkScale = d3.scale.linear()
+            .domain([1, 12])
+            .range([widthRatio, 12 * widthRatio])
+            .clamp(true);
+
+    function updateLinkWidth (d) {
+        // TODO: watch out for .showPath/.showTraffic classes
+        d.el.transition()
+            .duration(1000)
+            .attr('stroke-width', linkScale(d.linkWidth));
+    }
+
+
     function updateLinks() {
         link = linkG.selectAll('.link')
             .data(network.links, function (d) { return d.id; });
@@ -572,7 +572,7 @@
             })
             .transition().duration(1000)
             .attr({
-                'stroke-width': function (d) { return linkWidth(d.width); },
+                'stroke-width': function (d) { return linkScale(d.linkWidth); },
                 stroke: '#666'      // TODO: remove explicit stroke, rather...
             });
 
@@ -589,13 +589,20 @@
         //link .foo() .bar() ...
 
         // operate on exiting links:
-        // TODO: figure out how to remove the node 'g' AND its children
+        // TODO: better transition (longer as a dashed, grey line)
         link.exit()
-            .transition()
-            .duration(750)
             .attr({
-                opacity: 0
+                'stroke-dasharray': '3, 3'
             })
+            .style('opacity', 0.4)
+            .transition()
+            .duration(2000)
+            .attr({
+                'stroke-dasharray': '3, 12'
+            })
+            .transition()
+            .duration(1000)
+            .style('opacity', 0.0)
             .remove();
     }
 
@@ -650,7 +657,6 @@
         node.y = y || network.view.height() / 2;
     }
 
-
     function iconUrl(d) {
         return 'img/' + d.type + '.png';
     }
@@ -694,12 +700,48 @@
         return (label && label.trim()) ? label : '.';
     }
 
+    function updateDeviceLabel(d) {
+        var label = niceLabel(deviceLabel(d)),
+            node = d.el,
+            box;
+
+        node.select('text')
+            .text(label)
+            .style('opacity', 0)
+            .transition()
+            .style('opacity', 1);
+
+        box = adjustRectToFitText(node);
+
+        node.select('rect')
+            .transition()
+            .attr(box);
+
+        node.select('image')
+            .transition()
+            .attr('x', box.x + config.icons.xoff)
+            .attr('y', box.y + config.icons.yoff);
+    }
+
+    function updateHostLabel(d) {
+        var label = hostLabel(d),
+            host = d.el;
+
+        host.select('text').text(label);
+    }
+
     function updateDeviceState(nodeData) {
         nodeData.el.classed('online', nodeData.online);
         updateDeviceLabel(nodeData);
         // TODO: review what else might need to be updated
     }
 
+    function updateLinkState(linkData) {
+        updateLinkWidth(linkData);
+        // TODO: review what else might need to be updated
+        //  update label, if showing
+    }
+
     function updateHostState(hostData) {
         updateHostLabel(hostData);
         // TODO: review what else might need to be updated
@@ -826,6 +868,25 @@
             .remove();
     }
 
+    function find(id, array) {
+        for (var idx = 0, n = array.length; idx < n; idx++) {
+            if (array[idx].id === id) {
+                return idx;
+            }
+        }
+        return -1;
+    }
+
+    function removeLinkElement(linkData) {
+        // remove from lookup cache
+        delete network.lookup[linkData.id];
+        // remove from links array
+        var idx = find(linkData.id, network.links);
+
+        network.links.splice(linkData.index, 1);
+        // remove from SVG
+        updateLinks();
+    }
 
     function tick() {
         node.attr({