GUI -- Major rework to link processing so that we consolidate links A->B and B->A into a single backing object.
- added blue glow to ONOS instance when showing switch affinity.

Change-Id: Ia2a52d9d0571bc8c5eed964c85862f5798c7c5db
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 37e098e..0d84125 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -339,13 +339,16 @@
         link: {
             hostLink: 'pkt',
             direct: 'pkt',
+            indirect: '',
+            tunnel: '',
             optical: 'opt'
         }
     };
 
     function inLayer(d, layer) {
-        var look = layerLookup[d.class],
-            lyr = look && look[d.type];
+        var type = d.class === 'link' ? d.type() : d.type,
+            look = layerLookup[d.class],
+            lyr = look && look[type];
         return lyr === layer;
     }
 
@@ -408,6 +411,115 @@
         });
     }
 
+    function makeNodeKey(d, what) {
+        var port = what + 'Port';
+        return d[what] + '/' + d[port];
+    }
+
+    function makeLinkKey(d, flipped) {
+        var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
+            two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
+        return one + '-' + two;
+    }
+
+    function findLink(linkData, op) {
+        var key = makeLinkKey(linkData),
+            keyrev = makeLinkKey(linkData, 1),
+            link = network.lookup[key],
+            linkRev = network.lookup[keyrev],
+            result = {},
+            ldata = link || linkRev,
+            rawLink;
+
+        if (op === 'add') {
+            if (link) {
+                // trying to add a link that we already know about
+                result.ldata = link;
+                result.badLogic = 'addLink: link already added';
+
+            } else if (linkRev) {
+                // we found the reverse of the link to be added
+                result.ldata = linkRev;
+                if (linkRev.fromTarget) {
+                    result.badLogic = 'addLink: link already added';
+                }
+            }
+        } else if (op === 'update') {
+            if (!ldata) {
+                result.badLogic = 'updateLink: link not found';
+            } else {
+                rawLink = link ? ldata.fromSource : ldata.fromTarget;
+                result.updateWith = function (data) {
+                    $.extend(rawLink, data);
+                    restyleLinkElement(ldata);
+                }
+            }
+        } else if (op === 'remove') {
+            if (!ldata) {
+                result.badLogic = 'removeLink: link not found';
+            } else {
+                rawLink = link ? ldata.fromSource : ldata.fromTarget;
+
+                if (!rawLink) {
+                    result.badLogic = 'removeLink: link not found';
+
+                } else {
+                    result.removeRawLink = function () {
+                        if (link) {
+                            // remove fromSource
+                            ldata.fromSource = null;
+                            if (ldata.fromTarget) {
+                                // promote target into source position
+                                ldata.fromSource = ldata.fromTarget;
+                                ldata.fromTarget = null;
+                                ldata.key = keyrev;
+                                delete network.lookup[key];
+                                network.lookup[keyrev] = ldata;
+                            }
+                        } else {
+                            // remove fromTarget
+                            ldata.fromTarget = null;
+                        }
+                        if (ldata.fromSource) {
+                            restyleLinkElement(ldata);
+                        } else {
+                            removeLinkElement(ldata);
+                        }
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    function addLinkUpdate(ldata, link) {
+        // add link event, but we already have the reverse link installed
+        ldata.fromTarget = link;
+        restyleLinkElement(ldata);
+    }
+
+    var allLinkTypes = 'direct indirect optical tunnel',
+        defaultLinkType = 'direct';
+
+    function restyleLinkElement(ldata) {
+        // this fn's job is to look at raw links and decide what svg classes
+        // need to be applied to the line element in the DOM
+        var el = ldata.el,
+            type = ldata.type(),
+            lw = ldata.linkWidth(),
+            online = ldata.online();
+
+        el.classed('link', true);
+        el.classed('inactive', !online);
+        el.classed(allLinkTypes, false);
+        if (type) {
+            el.classed(type, true);
+        }
+        el.transition()
+            .duration(1000)
+            .attr('stroke-width', linkScale(lw))
+            .attr('stroke', '#666');  // TODO: remove explicit stroke (use CSS)
+    }
 
     // ==============================
     // Event handlers for server-pushed events
@@ -465,10 +577,26 @@
     function addLink(data) {
         evTrace(data);
         var link = data.payload,
-            lnk = createLink(link);
-        if (lnk) {
-            network.links.push(lnk);
-            network.lookup[lnk.id] = lnk;
+            result = findLink(link, 'add'),
+            bad = result.badLogic,
+            ldata = result.ldata;
+
+        if (bad) {
+            logicError(bad + ': ' + link.id);
+            return;
+        }
+
+        if (ldata) {
+            // we already have a backing store link for src/dst nodes
+            addLinkUpdate(ldata, link);
+            return;
+        }
+
+        // no backing store link yet
+        ldata = createLink(link);
+        if (ldata) {
+            network.links.push(ldata);
+            network.lookup[ldata.key] = ldata;
             updateLinks();
             network.force.start();
         }
@@ -511,14 +639,13 @@
     function updateLink(data) {
         evTrace(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 + '"');
+            result = findLink(link, 'update'),
+            bad = result.badLogic;
+        if (bad) {
+            logicError(bad + ': ' + link.id);
+            return;
         }
+        result.updateWith(link);
     }
 
     function updateHost(data) {
@@ -538,13 +665,13 @@
     function removeLink(data) {
         evTrace(data);
         var link = data.payload,
-            id = link.id,
-            linkData = network.lookup[id];
-        if (linkData) {
-            removeLinkElement(linkData);
-        } else {
-            logicError('removeLink lookup fail. ID = "' + id + '"');
+            result = findLink(link, 'remove'),
+            bad = result.badLogic;
+        if (bad) {
+            logicError(bad + ': ' + link.id);
+            return;
         }
+        result.removeRawLink();
     }
 
     function removeHost(data) {
@@ -805,11 +932,13 @@
 
         // Synthesize link ...
         $.extend(lnk, {
-            id: id,
+            key: id,
             class: 'link',
-            type: 'hostLink',
-            svgClass: 'link hostLink',
-            linkWidth: 1
+
+            type: function () { return 'hostLink'; },
+            // TODO: ideally, we should see if our edge switch is online...
+            online: function () { return true; },
+            linkWidth: function () { return 1; }
         });
         return lnk;
     }
@@ -822,10 +951,29 @@
             return null;
         }
 
-        // merge in remaining data
-        $.extend(lnk, link, {
+        $.extend(lnk, {
+            key: link.id,
             class: 'link',
-            svgClass: (type ? 'link ' + type : 'link')
+            fromSource: link,
+
+            // functions to aggregate dual link state
+            type: function () {
+                var s = lnk.fromSource,
+                    t = lnk.fromTarget;
+                return (s && s.type) || (t && t.type) || defaultLinkType;
+            },
+            online: function () {
+                var s = lnk.fromSource,
+                    t = lnk.fromTarget;
+                return (s && s.online) || (t && t.online);
+            },
+            linkWidth: function () {
+                var s = lnk.fromSource,
+                    t = lnk.fromTarget,
+                    ws = (s && s.linkWidth) || 0,
+                    wt = (t && t.linkWidth) || 0;
+                return Math.max(ws, wt);
+            }
         });
         return lnk;
     }
@@ -836,17 +984,9 @@
             .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; });
+            .data(network.links, function (d) { return d.key; });
 
         // operate on existing links, if necessary
         // link .foo() .bar() ...
@@ -855,19 +995,12 @@
         var entering = link.enter()
             .append('line')
             .attr({
-                class: function (d) { return d.svgClass; },
                 x1: function (d) { return d.x1; },
                 y1: function (d) { return d.y1; },
                 x2: function (d) { return d.x2; },
                 y2: function (d) { return d.y2; },
                 stroke: config.topo.linkInColor,
                 'stroke-width': config.topo.linkInWidth
-            })
-            .classed('inactive', function(d) { return !d.online; })
-            .transition().duration(1000)
-            .attr({
-                'stroke-width': function (d) { return linkScale(d.linkWidth); },
-                stroke: '#666'      // TODO: remove explicit stroke, rather...
             });
 
         // augment links
@@ -875,6 +1008,7 @@
             var link = d3.select(this);
             // provide ref to element selection from backing data....
             d.el = link;
+            restyleLinkElement(d);
 
             // TODO: add src/dst port labels etc.
         });
@@ -1240,9 +1374,9 @@
         // TODO: device node exits
     }
 
-    function find(id, array) {
+    function find(key, array) {
         for (var idx = 0, n = array.length; idx < n; idx++) {
-            if (array[idx].id === id) {
+            if (array[idx].key === key) {
                 return idx;
             }
         }
@@ -1250,14 +1384,16 @@
     }
 
     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(idx, 1);
-        // remove from SVG
-        updateLinks();
-        network.force.resume();
+        var idx = find(linkData.key, network.links),
+            removed;
+        if (idx >=0) {
+            // remove from links array
+            removed = network.links.splice(idx, 1);
+            // remove from lookup cache
+            delete network.lookup[removed[0].key];
+            updateLinks();
+            network.force.resume();
+        }
     }
 
     function removeHostElement(hostData) {