ONOS-2328 GUI -- Started work on seeing multiple links between devices on the topology view. Device links are now grouped based on what device they are between. Also minor bug fixes and investigations into other bugs. WIP.

Change-Id: I444f016268efc5c489ba6ad0282d0f7827fff462
diff --git a/web/gui/src/main/webapp/app/view/topo/topoD3.js b/web/gui/src/main/webapp/app/view/topo/topoD3.js
index 1b0a128..09883b1 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoD3.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoD3.js
@@ -367,6 +367,7 @@
             .classed('linkLabel', true)
             .attr('id', function (d) { return d.id; });
 
+        // FIXME: x and y position calculated here, use link.position obj
         entering.each(function (d) {
             var el = d3.select(this),
                 rect,
@@ -410,6 +411,7 @@
         return box;
     }
 
+    // FIXME: x and y position calculated here
     function transformLabel(p) {
         var dx = p.x2 - p.x1,
             dy = p.y2 - p.y1,
diff --git a/web/gui/src/main/webapp/app/view/topo/topoForce.js b/web/gui/src/main/webapp/app/view/topo/topoForce.js
index b99cb9a..0380c65 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -24,8 +24,7 @@
 
     // injected refs
     var $log, $timeout, fs, sus, is, ts, flash, wss,
-        tis, tms, td3, tss, tts, tos, fltr, tls,
-        icfg, uplink, svg;
+        tis, tms, td3, tss, tts, tos, fltr, tls, uplink, svg;
 
     // configuration
     var linkConfig = {
@@ -50,6 +49,7 @@
         network = {
             nodes: [],
             links: [],
+            linksByDevice: {},
             lookup: {},
             revLinkToKey: {}
         },
@@ -215,6 +215,7 @@
         d = tms.createLink(data);
         if (d) {
             network.links.push(d);
+            aggregateLink(d, data);
             lu[d.key] = d;
             updateLinks();
             fStart();
@@ -241,10 +242,40 @@
 
     // ========================
 
+    function makeNodeKey(node1, node2) {
+        return node1 + '-' + node2;
+    }
+
+    function findNodePair(key, keyRev) {
+        if (network.linksByDevice[key]) {
+            return key;
+        } else if (network.linksByDevice[keyRev]) {
+            return keyRev;
+        } else {
+            return false;
+        }
+    }
+
+    function aggregateLink(ldata, link) {
+        var key = makeNodeKey(link.src, link.dst),
+            keyRev = makeNodeKey(link.dst, link.src),
+            found = findNodePair(key, keyRev);
+
+        if (found) {
+            network.linksByDevice[found].push(ldata);
+            ldata.devicePair = found;
+        } else {
+            network.linksByDevice[key] = [ ldata ];
+            ldata.devicePair = key;
+        }
+    }
+
     function addLinkUpdate(ldata, link) {
         // add link event, but we already have the reverse link installed
         ldata.fromTarget = link;
         rlk[link.id] = ldata.key;
+        // possible solution to el being undefined in restyleLinkElement:
+        //_updateLinks();
         restyleLinkElement(ldata);
     }
 
@@ -267,7 +298,12 @@
             delay = immediate ? 0 : 1000;
 
         // FIXME: understand why el is sometimes undefined on addLink events...
-        if (el) {
+        // Investigated:
+        // el is undefined when it's a reverse link that is being added.
+        // updateLinks (which sets ldata.el) isn't called before this is called.
+        // Calling _updateLinks in addLinkUpdate fixes it, but there might be
+        // a more efficient way to fix it.
+        if (el && !el.empty()) {
             el.classed('link', true);
             el.classed('inactive', !online);
             el.classed(allLinkTypes, false);
@@ -383,7 +419,7 @@
         d.metaUi = metaUi;
         wss.sendEvent('updateMeta', {
             id: d.id,
-            'class': d.class,
+            class: d.class,
             memento: metaUi
         });
     }
@@ -503,7 +539,11 @@
             .attr({
                 id: function (d) { return sus.safeId(d.id); },
                 class: mkSvgClass,
-                transform: function (d) { return sus.translate(d.x, d.y); },
+                transform: function (d) {
+                    // sometimes says d.x and d.y are NaN?
+                    // I have a feeling it's a timing issue
+                    return sus.translate(d.x, d.y);
+                },
                 opacity: 0
             })
             .call(drag)
@@ -548,6 +588,9 @@
         link = linkG.selectAll('.link')
             .data(network.links, function (d) { return d.key; });
 
+        // This seems to do nothing? I've only triggered it on timeout errors
+        // when adding links, link var is empty because there aren't any links
+        // when removing links, link var is empty already
         // operate on existing links:
         link.each(function (d) {
             // this is supposed to be an existing link, but we have observed
@@ -559,6 +602,7 @@
         });
 
         // operate on entering links:
+        // FIXME: x and y position calculated here - calculate position and add it to the link
         var entering = link.enter()
             .append('line')
             .attr({
@@ -620,6 +664,7 @@
         nodeAttr: {
             transform: function (d) { return sus.translate(d.x, d.y); }
         },
+        // FIXME: x and y position calculated here, will be deleted
         linkAttr: {
             x1: function (d) { return d.source.x; },
             y1: function (d) { return d.source.y; },
@@ -630,6 +675,7 @@
             transform: function (d) {
                 var lnk = tms.findLinkById(d.key);
                 if (lnk) {
+                    // FIXME: x and y position calculated here, use link.position object
                     return td3.transformLabel({
                         x1: lnk.source.x,
                         y1: lnk.source.y,
@@ -643,9 +689,16 @@
 
     function tick() {
         // guard against null (which can happen when our view pages out)...
-        if (node) node.attr(tickStuff.nodeAttr);
-        if (link) link.attr(tickStuff.linkAttr);
-        if (linkLabel) linkLabel.attr(tickStuff.linkLabelAttr);
+        if (node) {
+            node.attr(tickStuff.nodeAttr);
+        }
+        if (link) {
+            // FIXME: instead of tickStuff here, use link.position object
+            link.attr(tickStuff.linkAttr);
+        }
+        if (linkLabel) {
+            linkLabel.attr(tickStuff.linkLabelAttr);
+        }
     }
 
 
@@ -734,7 +787,7 @@
             showHosts: function () { return showHosts; },
             restyleLinkElement: restyleLinkElement,
             updateLinkLabelModel: updateLinkLabelModel
-        }
+        };
     }
 
     function mkSelectApi(uplink) {
@@ -755,7 +808,7 @@
             hovered: tss.hovered,
             validateSelectionContext: tss.validateSelectionContext,
             selectOrder: tss.selectOrder
-        }
+        };
     }
 
     function mkObliqueApi(uplink, fltr) {
@@ -797,19 +850,18 @@
 
     angular.module('ovTopo')
     .factory('TopoForceService',
-        ['$log', '$timeout', 'FnService', 'SvgUtilService', 'IconService',
+        ['$log', '$timeout', 'FnService', 'SvgUtilService',
             'ThemeService', 'FlashService', 'WebSocketService',
             'TopoInstService', 'TopoModelService',
             'TopoD3Service', 'TopoSelectService', 'TopoTrafficService',
             'TopoObliqueService', 'TopoFilterService', 'TopoLinkService',
 
-        function (_$log_, _$timeout_, _fs_, _sus_, _is_, _ts_, _flash_, _wss_,
+        function (_$log_, _$timeout_, _fs_, _sus_, _ts_, _flash_, _wss_,
                   _tis_, _tms_, _td3_, _tss_, _tts_, _tos_, _fltr_, _tls_) {
             $log = _$log_;
             $timeout = _$timeout_;
             fs = _fs_;
             sus = _sus_;
-            is = _is_;
             ts = _ts_;
             flash = _flash_;
             wss = _wss_;
@@ -822,8 +874,6 @@
             fltr = _fltr_;
             tls = _tls_;
 
-            icfg = is.iconConfig();
-
             var themeListener = ts.addListener(function () {
                 updateLinks();
                 updateNodes();
@@ -902,12 +952,24 @@
                 // clean up internal state
                 network.nodes = [];
                 network.links = [];
+                network.linksByDevice = {};
                 network.lookup = {};
                 network.revLinkToKey = {};
 
                 linkG = linkLabelG = nodeG = portLabelG = null;
                 link = linkLabel = node = null;
                 force = drag = null;
+
+                // clean up $timeout promises
+                if (fTimer) {
+                    $timeout.cancel(fTimer);
+                }
+                if (fNodesTimer) {
+                    $timeout.cancel(fNodesTimer);
+                }
+                if (fLinksTimer) {
+                    $timeout.cancel(fLinksTimer);
+                }
             }
 
             return {
diff --git a/web/gui/src/main/webapp/app/view/topo/topoLink.js b/web/gui/src/main/webapp/app/view/topo/topoLink.js
index 9cd49dc..834f024 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoLink.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoLink.js
@@ -104,6 +104,7 @@
             return {x:x4, y:y4};
         }
 
+        // FIXME: x and y position calculated here, use link.position
         function lineSeg(d) {
             return {
                 x1: d.source.x,
@@ -200,6 +201,7 @@
         td3.applyPortLabels(data, api.portLabelG());
     }
 
+    // FIXME: x and y position calculated here somewhere
     function locatePortLabel(link, src) {
         var near = src ? 'source' : 'target',
             far = src ? 'target' : 'source',
diff --git a/web/gui/src/main/webapp/app/view/topo/topoModel.js b/web/gui/src/main/webapp/app/view/topo/topoModel.js
index 2a4921b..9c8d46b 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoModel.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoModel.js
@@ -36,7 +36,7 @@
      */
 
     // shorthand
-    var lu, rlk, nodes, links;
+    var lu, rlk, nodes, links, linksByDevice;
 
     var dim;    // dimensions of layout [w,h]
 
@@ -185,6 +185,12 @@
             fromSource: link,
             srcPort: link.srcPort,
             tgtPort: link.dstPort,
+            position: {
+                x1: 0,
+                y1: 0,
+                x2: 0,
+                y2: 0
+            },
 
             // functions to aggregate dual link state
             type: function () {
@@ -292,6 +298,11 @@
 
                 } else {
                     result.removeRawLink = function () {
+                        // remove link out of aggregate linksByDevice list
+                        var linksForDevPair = linksByDevice[ldata.devicePair],
+                            rmvIdx = fs.find(ldata.key, linksForDevPair, 'key');
+                        linksForDevPair.splice(rmvIdx, 1);
+
                         if (link) {
                             // remove fromSource
                             ldata.fromSource = null;
@@ -393,6 +404,7 @@
                 rlk = api.network.revLinkToKey;
                 nodes = api.network.nodes;
                 links = api.network.links;
+                linksByDevice = api.network.linksByDevice;
             }
 
             function newDim(_dim_) {