ONOS-2385 -- Bug fixes for removing individual links on the Topo View. 5 or more links between devices have a label indicating how many there are between each one.

Change-Id: I301ca6da8c453b54e16980a47e09dfd9f2f80f8b
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.css b/web/gui/src/main/webapp/app/view/topo/topo.css
index c76d9d5..bbbea31 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.css
+++ b/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -579,6 +579,26 @@
     fill: #eee;
 }
 
+/* Number of Links Labels */
+#ov-topo line.numLinkHash {
+    stroke-width: 3;
+}
+
+#ov-topo text.numLinkText {
+    font-size: 15pt;
+}
+
+#ov-topo text.numLinkText {
+    text-anchor: middle;
+}
+
+.light #ov-topo text.numLinkText {
+    fill: #444;
+}
+.dark #ov-topo text.numLinkText {
+    fill: #eee;
+}
+
 /* ------------------------------------------------- */
 /* Sprite Layer */
 
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 4dcf3f0..d29748b 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoD3.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoD3.js
@@ -429,6 +429,100 @@
         });
     }
 
+    function labelPoint(linkPos) {
+        var lengthUpLine = 1 / 3,
+            dx = linkPos.x2 - linkPos.x1,
+            dy = linkPos.y2 - linkPos.y1,
+            movedX = dx * lengthUpLine,
+            movedY = dy * lengthUpLine;
+
+        return {
+            x: movedX,
+            y: movedY
+        };
+    }
+
+    function calcGroupPos(linkPos) {
+        var moved = labelPoint(linkPos);
+        return sus.translate(linkPos.x1 + moved.x, linkPos.y1 + moved.y);
+    }
+
+    // calculates where on the link that the hash line for 5+ label appears
+    function hashAttrs(linkPos) {
+        var hashLength = 25,
+            halfLength = hashLength / 2,
+            dx = linkPos.x2 - linkPos.x1,
+            dy = linkPos.y2 - linkPos.y1,
+            length = Math.sqrt((dx * dx) + (dy * dy)),
+            moveAmtX = (dx / length) * halfLength,
+            moveAmtY = (dy / length) * halfLength,
+            mid = labelPoint(linkPos),
+            angle = Math.atan(dy / dx) + 45;
+
+        return {
+            x1: mid.x - moveAmtX,
+            y1: mid.y - moveAmtY,
+            x2: mid.x + moveAmtX,
+            y2: mid.y + moveAmtY,
+            stroke: api.linkConfig()[ts.theme()].baseColor,
+            transform: 'rotate(' + angle + ',' + mid.x + ',' + mid.y + ')'
+        };
+    }
+
+    function textLabelPos(linkPos) {
+        var point = labelPoint(linkPos),
+            dist = 20;
+        return {
+            x: point.x + dist,
+            y: point.y + dist
+        };
+    }
+
+    function applyNumLinkLabels(data, lblsG) {
+        var labels = lblsG.selectAll('g.numLinkLabel')
+                .data(data, function (d) { return 'pair-' + d.id; }),
+            entering;
+
+        // update existing labels
+        labels.each(function (d) {
+            var el = d3.select(this);
+
+            el.attr({
+                transform: function (d) { return calcGroupPos(d.linkCoords); }
+            });
+            el.select('line')
+                .attr(hashAttrs(d.linkCoords));
+            el.select('text')
+                .attr(textLabelPos(d.linkCoords))
+                .text(d.num);
+        });
+
+        // add new labels
+        entering = labels
+            .enter()
+            .append('g')
+            .attr({
+                transform: function (d) { return calcGroupPos(d.linkCoords); },
+                id: function (d) { return 'pair-' + d.id; }
+            })
+            .classed('numLinkLabel', true);
+
+        entering.each(function (d) {
+            var el = d3.select(this);
+
+            el.append('line')
+                .classed('numLinkHash', true)
+                .attr(hashAttrs(d.linkCoords));
+            el.append('text')
+                .classed('numLinkText', true)
+                .attr(textLabelPos(d.linkCoords))
+                .text(d.num);
+        });
+
+        // remove old labels
+        labels.exit().remove();
+    }
+
     // ==========================
     // Module definition
 
@@ -475,7 +569,8 @@
                 linkEntering: linkEntering,
                 applyLinkLabels: applyLinkLabels,
                 transformLabel: transformLabel,
-                applyPortLabels: applyPortLabels
+                applyPortLabels: applyPortLabels,
+                applyNumLinkLabels: applyNumLinkLabels
             };
         }]);
 }());
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 48309f0..5fb6841 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -61,10 +61,11 @@
         fTimer,                 // timer for delayed force layout
         fNodesTimer,            // timer for delayed nodes update
         fLinksTimer,            // timer for delayed links update
-        dim;                    // the dimensions of the force layout [w,h]
+        dim,                    // the dimensions of the force layout [w,h]
+        linkNums = [];          // array of link number labels
 
     // SVG elements;
-    var linkG, linkLabelG, portLabelG, nodeG;
+    var linkG, linkLabelG, numLinkLblsG, portLabelG, nodeG;
 
     // D3 selections;
     var link, linkLabel, node;
@@ -608,6 +609,7 @@
     function calcPosition() {
         var lines = this,
             linkSrcId;
+        linkNums = [];
         lines.each(function (d) {
             if (d.type() === 'hostLink') {
                 d.position = getDefaultPos(d);
@@ -625,13 +627,14 @@
             return link.source.id !== linkSrcId;
         }
 
-        angular.forEach(network.linksByDevice, function (linkArr) {
+        angular.forEach(network.linksByDevice, function (linkArr, key) {
             var numLinks = linkArr.length,
                 link;
 
             if (numLinks === 1) {
                 link = linkArr[0];
                 link.position = getDefaultPos(link);
+                link.position.multiLink = false;
             } else if (numLinks >= 5) {
                 // this code is inefficient, in the future the way links
                 // are modeled will be changed
@@ -639,13 +642,18 @@
                     link.position = getDefaultPos(link);
                     link.position.multiLink = true;
                 });
+                linkNums.push({
+                    id: key,
+                    num: numLinks,
+                    linkCoords: linkArr[0].position
+                });
             } 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);
+                    link.position.multiLink = false;
                 });
             }
         });
@@ -696,6 +704,9 @@
         // operate on both existing and new links:
         //link.each(...)
 
+        // add labels for how many links are in a thick line
+        td3.applyNumLinkLabels(linkNums, numLinkLblsG);
+
         // apply or remove labels
         td3.applyLinkLabels();
 
@@ -764,6 +775,7 @@
         if (link) {
             link.call(calcPosition)
                 .attr(tickStuff.linkAttr);
+            td3.applyNumLinkLabels(linkNums, numLinkLblsG);
         }
         if (linkLabel) {
             linkLabel.attr(tickStuff.linkLabelAttr);
@@ -855,7 +867,8 @@
             posNode: tms.positionNode,
             showHosts: function () { return showHosts; },
             restyleLinkElement: restyleLinkElement,
-            updateLinkLabelModel: updateLinkLabelModel
+            updateLinkLabelModel: updateLinkLabelModel,
+            linkConfig: function () { return linkConfig; }
         };
     }
 
@@ -897,7 +910,10 @@
             },
             opacifyMap: uplink.opacifyMap,
             inLayer: fltr.inLayer,
-            calcLinkPos: calcPosition
+            calcLinkPos: calcPosition,
+            applyNumLinkLabels: function () {
+                td3.applyNumLinkLabels(linkNums, numLinkLblsG);
+            }
         };
     }
 
@@ -975,6 +991,7 @@
 
                 linkG = forceG.append('g').attr('id', 'topo-links');
                 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
+                numLinkLblsG = forceG.append('g').attr('id', 'topo-numLinkLabels');
                 nodeG = forceG.append('g').attr('id', 'topo-nodes');
                 portLabelG = forceG.append('g').attr('id', 'topo-portLabels');
 
@@ -1026,7 +1043,9 @@
                 network.lookup = {};
                 network.revLinkToKey = {};
 
-                linkG = linkLabelG = nodeG = portLabelG = null;
+                linkNums = [];
+
+                linkG = linkLabelG = numLinkLblsG = nodeG = portLabelG = null;
                 link = linkLabel = node = null;
                 force = drag = null;
 
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 c42e3ec..fb98fc2 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoModel.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoModel.js
@@ -301,7 +301,10 @@
                         // remove link out of aggregate linksByDevice list
                         var linksForDevPair = linksByDevice[ldata.devicePair],
                             rmvIdx = fs.find(ldata.key, linksForDevPair, 'key');
-                        linksForDevPair.splice(rmvIdx, 1);
+                        if (rmvIdx >= 0) {
+                            linksForDevPair.splice(rmvIdx, 1);
+                        }
+                        ldata.position.multilink = linksForDevPair.length >= 5;
 
                         if (link) {
                             // remove fromSource
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 326271a..e84b117 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoOblique.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoOblique.js
@@ -157,7 +157,8 @@
         api.link().transition()
             .duration(time)
             .call(api.calcLinkPos)
-            .attr(api.tickStuff.linkAttr);
+            .attr(api.tickStuff.linkAttr)
+            .call(api.applyNumLinkLabels);
         api.linkLabel().transition()
             .duration(time)
             .attr(api.tickStuff.linkLabelAttr);