ONOS-2328 GUI -- Enhanced Topology View to show multiple links individually between devices (1 - 4). 5 or more is a thick line.

Change-Id: Ie096086454fd8d1d5d40f09396681f9cba8597a1
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 09883b1..4dcf3f0 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoD3.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoD3.js
@@ -367,17 +367,10 @@
             .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,
-                text,
-                parms = {
-                    x1: d.ldata.source.x,
-                    y1: d.ldata.source.y,
-                    x2: d.ldata.target.x,
-                    y2: d.ldata.target.y
-                };
+                text;
 
             if (d.ldata.type() === 'hostLink') {
                 el.classed('hostLinkLabel', true);
@@ -390,7 +383,7 @@
             rect.attr(rectAroundText(el));
             text.attr('dy', linkLabelOffset);
 
-            el.attr('transform', transformLabel(parms));
+            el.attr('transform', transformLabel(d.ldata.position));
         });
 
         // Remove any labels that are no longer required.
@@ -411,7 +404,6 @@
         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 0380c65..48309f0 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -575,6 +575,82 @@
 
     // ==========================
 
+    function getDefaultPos(link) {
+        return {
+            x1: link.source.x,
+            y1: link.source.y,
+            x2: link.target.x,
+            y2: link.target.y
+        };
+    }
+
+    // returns amount of adjustment along the normal for given link
+    function amt(numLinks, linkIdx) {
+        var gap = 6;
+        return (linkIdx - ((numLinks - 1) / 2)) * gap;
+    }
+
+    function calcMovement(d, amt, flipped) {
+        var pos = getDefaultPos(d),
+            mult = flipped ? -amt : amt,
+            dx = pos.x2 - pos.x1,
+            dy = pos.y2 - pos.y1,
+            length = Math.sqrt((dx * dx) + (dy * dy));
+
+        return {
+            x1: pos.x1 + (mult * dy / length),
+            y1: pos.y1 + (mult * -dx / length),
+            x2: pos.x2 + (mult * dy / length),
+            y2: pos.y2 + (mult * -dx / length)
+        };
+    }
+
+    function calcPosition() {
+        var lines = this,
+            linkSrcId;
+        lines.each(function (d) {
+            if (d.type() === 'hostLink') {
+                d.position = getDefaultPos(d);
+            }
+        });
+
+        function normalizeLinkSrc(link) {
+            // ensure source device is consistent across set of links
+            // temporary measure until link modeling is refactored
+            if (!linkSrcId) {
+                linkSrcId = link.source.id;
+                return false;
+            }
+
+            return link.source.id !== linkSrcId;
+        }
+
+        angular.forEach(network.linksByDevice, function (linkArr) {
+            var numLinks = linkArr.length,
+                link;
+
+            if (numLinks === 1) {
+                link = linkArr[0];
+                link.position = getDefaultPos(link);
+            } else if (numLinks >= 5) {
+                // this code is inefficient, in the future the way links
+                // are modeled will be changed
+                angular.forEach(linkArr, function (link) {
+                    link.position = getDefaultPos(link);
+                    link.position.multiLink = true;
+                });
+            } else {
+                // calculate position of links
+                linkSrcId = null;
+                angular.forEach(linkArr, function (link, index) {
+                    var offsetAmt = amt(numLinks, index),
+                        needToFlip = normalizeLinkSrc(link);
+                    link.position = calcMovement(link, offsetAmt, needToFlip);
+                });
+            }
+        });
+    }
+
     function updateLinks() {
         if (fLinksTimer) {
             $timeout.cancel(fLinksTimer);
@@ -602,14 +678,14 @@
         });
 
         // 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')
+            .call(calcPosition)
             .attr({
-                x1: function (d) { return d.source.x; },
-                y1: function (d) { return d.source.y; },
-                x2: function (d) { return d.target.x; },
-                y2: function (d) { return d.target.y; },
+                x1: function (d) { return d.position.x1; },
+                y1: function (d) { return d.position.y1; },
+                x2: function (d) { return d.position.x2; },
+                y2: function (d) { return d.position.y2; },
                 stroke: linkConfig[th].inColor,
                 'stroke-width': linkConfig.inWidth
             });
@@ -664,24 +740,17 @@
         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; },
-            x2: function (d) { return d.target.x; },
-            y2: function (d) { return d.target.y; }
+            x1: function (d) { return d.position.x1; },
+            y1: function (d) { return d.position.y1; },
+            x2: function (d) { return d.position.x2; },
+            y2: function (d) { return d.position.y2; }
         },
         linkLabelAttr: {
             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,
-                        x2: lnk.target.x,
-                        y2: lnk.target.y
-                    });
+                    return td3.transformLabel(lnk.position);
                 }
             }
         }
@@ -693,8 +762,8 @@
             node.attr(tickStuff.nodeAttr);
         }
         if (link) {
-            // FIXME: instead of tickStuff here, use link.position object
-            link.attr(tickStuff.linkAttr);
+            link.call(calcPosition)
+                .attr(tickStuff.linkAttr);
         }
         if (linkLabel) {
             linkLabel.attr(tickStuff.linkLabelAttr);
@@ -827,7 +896,8 @@
                 return old;
             },
             opacifyMap: uplink.opacifyMap,
-            inLayer: fltr.inLayer
+            inLayer: fltr.inLayer,
+            calcLinkPos: calcPosition
         };
     }
 
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 834f024..38f3a6c 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoLink.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoLink.js
@@ -104,16 +104,6 @@
             return {x:x4, y:y4};
         }
 
-        // FIXME: x and y position calculated here, use link.position
-        function lineSeg(d) {
-            return {
-                x1: d.source.x,
-                y1: d.source.y,
-                x2: d.target.x,
-                y2: d.target.y
-            };
-        }
-
         function lineHit(line, p, m) {
             if (p.x < line.x1 && p.x < line.x2) return false;
             if (p.x > line.x1 && p.x > line.x2) return false;
@@ -131,7 +121,7 @@
                     return; // skip hidden host links
                 }
 
-                var line = lineSeg(d),
+                var line = d.position,
                     point = pdrop(line, mouse),
                     hit = lineHit(line, point, mouse),
                     dist;
@@ -201,24 +191,23 @@
         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',
-            ln = link[near],
-            lf = link[far],
-            offset = 32;
+        var offset = 32,
+            pos = link.position,
+            nearX = src ? pos.x1 : pos.x2,
+            nearY = src ? pos.y1 : pos.y2,
+            farX = src ? pos.x2 : pos.x1,
+            farY = src ? pos.y2 : pos.y1;
 
         function dist(x, y) { return Math.sqrt(x*x + y*y); }
 
-        var dx = lf.x - ln.x,
-            dy = lf.y - ln.y,
+        var dx = farX - nearX,
+            dy = farY - nearY,
             k = offset / dist(dx, dy);
 
-        return {x: k * dx + ln.x, y: k * dy + ln.y};
+        return {x: k * dx + nearX, y: k * dy + nearY};
     }
 
-
     function selectLink(ldata) {
         // if the new link is same as old link, do nothing
         if (selectedLink && ldata && selectedLink.key === ldata.key) return;
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 9c8d46b..c42e3ec 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoModel.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoModel.js
@@ -209,7 +209,7 @@
                     t = lnk.fromTarget,
                     ws = (s && s.linkWidth) || 0,
                     wt = (t && t.linkWidth) || 0;
-                return Math.max(ws, wt);
+                return lnk.position.multiLink ? 5 : Math.max(ws, wt);
             }
         });
         return lnk;
diff --git a/web/gui/src/main/webapp/app/view/topo/topoOblique.js b/web/gui/src/main/webapp/app/view/topo/topoOblique.js
index d04965c..326271a 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoOblique.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoOblique.js
@@ -39,6 +39,7 @@
      nodeLock(b)                    // test-and-set nodeLock state
      opacifyMap(b)                  // show or hide map layer
      inLayer(d, layer)              // return true if d in layer {'pkt'|'opt'}
+     calcLinkPos()                  // recomputes link pos based on node data
      */
 
     // configuration
@@ -155,6 +156,7 @@
             .attr(api.tickStuff.nodeAttr);
         api.link().transition()
             .duration(time)
+            .call(api.calcLinkPos)
             .attr(api.tickStuff.linkAttr);
         api.linkLabel().transition()
             .duration(time)