Topo2Link - Fixed width of rectangle and centered text
Topo2Layout/Link - Added port number on link hover
Topo2Layout/Select - Added Drag functionality
Topo2SubRegion - Added onClick event to node
Topo2Device - Added Color Theme
TopoForce - Removed console.log

Change-Id: Icd85d92c8f3c5f96cb896068fe9375c250717f5f
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 ef9f633..6d992f4 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -103,7 +103,6 @@
     // === EVENT HANDLERS
 
     function addDevice(data) {
-        console.log(data);
         var id = data.id,
             d;
 
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2.js b/web/gui/src/main/webapp/app/view/topo2/topo2.js
index ee6fa78..ab36e4e 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2.js
@@ -69,7 +69,7 @@
         ps.setPrefs('topo_zoom', {tx:tr[0], ty:tr[1], sc:sc});
 
         // keep the map lines constant width while zooming
-//        mapG.style('stroke-width', (2.0 / sc) + 'px');
+        mapG.style('stroke-width', (2.0 / sc) + 'px');
     }
 
     function setUpZoom() {
@@ -108,8 +108,8 @@
                     // provides function calls back into this space
                     // showNoDevs: showNoDevs,
                     // projection: function () { return projection; },
-                    // zoomLayer: function () { return zoomLayer; },
-                    // zoomer: function () { return zoomer; },
+                    zoomLayer: function () { return zoomLayer; },
+                    zoomer: function () { return zoomer; },
                     // opacifyMap: opacifyMap,
                     // topoStartDone: topoStartDone
                 };
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
index c8478cf..6e48b83 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
@@ -30,6 +30,7 @@
 
     // configuration
     var devIconDim = 36,
+        labelPad = 10,
         hostRadius = 14,
         badgeConfig = {
             radius: 12,
@@ -43,7 +44,8 @@
             i: 'badgeInfo',
             w: 'badgeWarn',
             e: 'badgeError'
-        };
+        },
+        deviceLabelIndex = 0;
 
     function createDeviceCollection(data, region) {
 
@@ -81,13 +83,25 @@
         }
     }
 
-    function deviceGlyphColor(d) {
+    // note: these are the device icon colors without affinity (no master)
+    var dColTheme = {
+        light: {
+            online: '#444444',
+            offline: '#cccccc'
+        },
+        dark: {
+            // TODO: theme
+            online: '#444444',
+            offline: '#cccccc'
+        }
+    };
 
+    function deviceGlyphColor(d) {
         var o = this.node.online,
             id = this.node.master, // TODO: This should be from node.master
             otag = o ? 'online' : 'offline';
         return o ? sus.cat7().getColor(id, 0, ts.theme())
-                 : '#ff0000';
+                 : dColTheme[ts.theme()][otag];
     }
 
     function setDeviceColor() {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
index d2a49ea..9b4dc5e 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
@@ -22,12 +22,12 @@
 (function () {
     'use strict';
 
-    var $log, sus, t2rs, t2d3, t2vs;
+    var $log, sus, t2rs, t2d3, t2vs, t2ss;
 
-    var linkG, linkLabelG, numLinkLabelsG, nodeG, portLabelG;
+    var uplink, linkG, linkLabelG, numLinkLabelsG, nodeG, portLabelG;
     var link, linkLabel, node;
 
-    var nodes, links;
+    var nodes, links, highlightedLink;
 
     var force;
 
@@ -124,7 +124,7 @@
     function init(_svg_, forceG, _uplink_, _dim_, opts) {
 
         $log.debug("Initialising Topology Layout");
-
+        uplink = _uplink_;
         settings = angular.extend({}, defaultSettings, opts);
 
         linkG = forceG.append('g').attr('id', 'topo-links');
@@ -147,6 +147,35 @@
             .linkDistance(settings.linkDistance._def_)
             .linkStrength(settings.linkStrength._def_)
             .on('tick', tick);
+
+            drag = sus.createDragBehavior(force,
+                t2ss.selectObject, atDragEnd, dragEnabled, clickEnabled);
+
+            _svg_.on('mousemove', mouseMoveHandler)
+    }
+
+    function zoomingOrPanning(ev) {
+        return ev.metaKey || ev.altKey;
+    }
+
+    function atDragEnd(d) {
+        // once we've finished moving, pin the node in position
+        d.fixed = true;
+        d3.select(this).classed('fixed', true);
+        // TODO: sendUpdateMeta(d);
+        t2ss.clickConsumed(true);
+    }
+
+    // predicate that indicates when dragging is active
+    function dragEnabled() {
+        var ev = d3.event.sourceEvent;
+        // nodeLock means we aren't allowing nodes to be dragged...
+        return !nodeLock && !zoomingOrPanning(ev);
+    }
+
+    // predicate that indicates when clicking is active
+    function clickEnabled() {
+        return true;
     }
 
     function tick() {
@@ -188,6 +217,7 @@
                 },
                 opacity: 0
             })
+            .call(drag)
             // .on('mouseover', tss.nodeMouseOver)
             // .on('mouseout', tss.nodeMouseOut)
             .transition()
@@ -308,18 +338,140 @@
         force.start();
     }
 
+    // Mouse Events
+    function mouseMoveHandler() {
+        var mp = getLogicalMousePosition(this),
+            link = computeNearestLink(mp);
+
+
+        if (highlightedLink) {
+            highlightedLink.unenhance();
+            highlightedLink = null;
+        }
+
+        if (link) {
+            link.enhance();
+            highlightedLink = link;
+        }
+    }
+
+    // ======== ALGORITHM TO FIND LINK CLOSEST TO MOUSE ========
+
+    function getLogicalMousePosition(container) {
+        var m = d3.mouse(container),
+            sc = uplink.zoomer().scale(),
+            tr = uplink.zoomer().translate(),
+            mx = (m[0] - tr[0]) / sc,
+            my = (m[1] - tr[1]) / sc;
+        return {x: mx, y: my};
+    }
+
+
+    function sq(x) { return x * x; }
+
+    function mdist(p, m) {
+        return Math.sqrt(sq(p.x - m.x) + sq(p.y - m.y));
+    }
+
+    function prox(dist) {
+        return dist / uplink.zoomer().scale();
+    }
+
+    function computeNearestNode(mouse) {
+        var proximity = prox(30),
+            nearest = null,
+            minDist,
+            regionNodes = t2rs.regionNodes();
+
+        if (regionNodes.length) {
+            minDist = proximity * 2;
+
+            regionNodes.forEach(function (d) {
+                var dist;
+
+                if (!api.showHosts() && d.class === 'host') {
+                    return; // skip hidden hosts
+                }
+
+                dist = mdist({x: d.x, y: d.y}, mouse);
+                if (dist < minDist && dist < proximity) {
+                    minDist = dist;
+                    nearest = d;
+                }
+            });
+        }
+        return nearest;
+    }
+
+
+    function computeNearestLink(mouse) {
+        var proximity = prox(30),
+            nearest = null,
+            minDist,
+            regionLinks = t2rs.regionLinks();
+
+        function pdrop(line, mouse) {
+
+            var x1 = line.x1,
+                y1 = line.y1,
+                x2 = line.x2,
+                y2 = line.y2,
+                x3 = mouse.x,
+                y3 = mouse.y,
+                k = ((y2-y1) * (x3-x1) - (x2-x1) * (y3-y1)) /
+                    (sq(y2-y1) + sq(x2-x1)),
+                x4 = x3 - k * (y2-y1),
+                y4 = y3 + k * (x2-x1);
+            return {x:x4, y:y4};
+        }
+
+        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;
+            if (p.y < line.y1 && p.y < line.y2) return false;
+            if (p.y > line.y1 && p.y > line.y2) return false;
+            // line intersects, but are we close enough?
+            return mdist(p, m) <= proximity;
+        }
+
+        if (regionLinks.length) {
+            minDist = proximity * 2;
+
+            regionLinks.forEach(function (d) {
+                // if (!api.showHosts() && d.type() === 'hostLink') {
+                //     return; // skip hidden host links
+                // }
+
+                var line = d.get('position'),
+                    point = pdrop(line, mouse),
+                    hit = lineHit(line, point, mouse),
+                    dist;
+
+                if (hit) {
+                    dist = mdist(point, mouse);
+                    if (dist < minDist) {
+                        minDist = dist;
+                        nearest = d;
+                    }
+                }
+            });
+        }
+        return nearest;
+    }
+
     angular.module('ovTopo2')
     .factory('Topo2LayoutService',
         [
             '$log', 'SvgUtilService', 'Topo2RegionService',
-            'Topo2D3Service', 'Topo2ViewService',
+            'Topo2D3Service', 'Topo2ViewService', 'Topo2SelectService',
 
-            function (_$log_, _sus_, _t2rs_, _t2d3_, _t2vs_) {
+            function (_$log_, _sus_, _t2rs_, _t2d3_, _t2vs_, _t2ss_) {
 
                 $log = _$log_;
                 t2rs = _t2rs_;
                 t2d3 = _t2d3_;
                 t2vs = _t2vs_;
+                t2ss = _t2ss_;
                 sus = _sus_;
 
                 return {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
index b7c3ea6..dcf1b28 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
@@ -23,7 +23,9 @@
     'use strict';
 
     var $log;
-    var Collection, Model, region, ts;
+    var Collection, Model, region, ts, sus;
+
+    var linkLabelOffset = '0.35em';
 
     var widthRatio = 1.4,
         linkScale = d3.scale.linear()
@@ -70,12 +72,26 @@
                 y2: 0
             }
             // functions to aggregate dual link state
-//            extra: link.extra
+            // extra: link.extra
         });
 
         this.set(attrs);
     }
 
+    function rectAroundText(el) {
+        var text = el.select('text'),
+            box = text.node().getBBox();
+
+        // translate the bbox so that it is centered on [x,y]
+        box.x = -box.width / 2;
+        box.y = -box.height / 2;
+
+        // add padding
+        box.x -= 4;
+        box.width += 8;
+        return box;
+    }
+
     function linkEndPoints(srcId, dstId) {
 
         var sourceNode = this.region.findNodeById(srcId)
@@ -106,13 +122,71 @@
                 return this.get('type');
             },
             expected: function () {
-                //TODO: original code is: (s && s.expected) && (t && t.expected);
+                // TODO: original code is: (s && s.expected) && (t && t.expected);
                 return true;
             },
             online: function () {
+                // TODO: remove next line
                 return true;
+
                 return both && (s && s.online) && (t && t.online);
             },
+            enhance: function () {
+                var data = [],
+                    point;
+
+                angular.forEach(this.collection.models, function (link) {
+                    link.unenhance();
+                });
+
+                this.el.classed('enhanced', true);
+                point = this.locatePortLabel();
+                angular.extend(point, {
+                    id: 'topo-port-tgt',
+                    num: this.get('portB')
+                });
+                data.push(point);
+
+                var entering = d3.select('#topo-portLabels').selectAll('.portLabel')
+                    .data(data).enter().append('g')
+                    .classed('portLabel', true)
+                    .attr('id', function (d) { return d.id; });
+
+                entering.each(function (d) {
+                    var el = d3.select(this),
+                        rect = el.append('rect'),
+                        text = el.append('text').text(d.num);
+
+                    rect.attr(rectAroundText(el))
+                        .attr('rx', 2)
+                        .attr('ry', 2);
+
+                    text.attr('dy', linkLabelOffset)
+                        .attr('text-anchor', 'middle');
+
+                    el.attr('transform', sus.translate(d.x, d.y));
+                });
+            },
+            unenhance: function () {
+                this.el.classed('enhanced', false);
+                d3.select('#topo-portLabels').selectAll('.portLabel').remove();
+            },
+            locatePortLabel: function (link, src) {
+                var offset = 32,
+                    pos = this.get('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 = farX - nearX,
+                    dy = farY - nearY,
+                    k = offset / dist(dx, dy);
+
+                return {x: k * dx + nearX, y: k * dy + nearY};
+            },
             restyleLinkElement: function (immediate) {
                 // 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
@@ -144,9 +218,10 @@
                         .attr('stroke', linkConfig[th].baseColor);
                 }
             },
-
             onEnter: function (el) {
-                var link = d3.select(el);
+                var _this = this,
+                    link = d3.select(el);
+
                 this.el = link;
 
                 this.restyleLinkElement();
@@ -166,12 +241,13 @@
 
     angular.module('ovTopo2')
     .factory('Topo2LinkService',
-        ['$log', 'Topo2Collection', 'Topo2Model', 'ThemeService',
+        ['$log', 'Topo2Collection', 'Topo2Model', 'ThemeService', 'SvgUtilService',
 
-            function (_$log_, _Collection_, _Model_, _ts_) {
+            function (_$log_, _Collection_, _Model_, _ts_, _sus_) {
 
                 $log = _$log_;
                 ts = _ts_;
+                sus = _sus_;
                 Collection = _Collection_;
                 Model = _Model_;
 
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Select.js b/web/gui/src/main/webapp/app/view/topo2/topo2Select.js
index e69de29..83d5195 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Select.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Select.js
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Select Module.
+ */
+
+(function () {
+    'use strict';
+
+    // internal state
+    var hovered, selections, selectOrder, consumeClick;
+
+    function selectObject(obj) {
+        var el = this,
+            nodeEv = el && el.tagName === 'g',
+            ev = d3.event.sourceEvent || {},
+            n;
+
+        console.log(el, nodeEv, ev, n);
+    }
+
+    function clickConsumed(x) {
+        var cc = consumeClick;
+        consumeClick = !!x;
+        return cc;
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2SelectService',
+    [
+        function () {
+
+            return {
+                selectObject: selectObject,
+                clickConsumed: clickConsumed
+            };
+        }
+    ]);
+
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
index 016c7b4..67de6cb 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
@@ -22,7 +22,8 @@
 (function () {
     'use strict';
 
-    var Collection, Model, is, sus, ts, t2vs;
+    var wss, is, sus, ts, t2vs;
+    var Collection, Model;
 
     var remappedDeviceTypes = {
         virtual: 'cord'
@@ -71,11 +72,12 @@
 
     angular.module('ovTopo2')
     .factory('Topo2SubRegionService',
-        ['Topo2Collection', 'Topo2NodeModel', 'IconService', 'SvgUtilService',
+        ['WebSocketService', 'Topo2Collection', 'Topo2NodeModel', 'IconService', 'SvgUtilService',
         'ThemeService', 'Topo2ViewService',
 
-            function (_Collection_, _NodeModel_, _is_, _sus_, _ts_, classnames, _t2vs_) {
+            function (_wss_, _Collection_, _NodeModel_, _is_, _sus_, _ts_, classnames, _t2vs_) {
 
+                wss = _wss_;
                 t2vs = _t2vs_;
                 is = _is_;
                 sus = _sus_;
@@ -89,6 +91,12 @@
                     },
                     nodeType: 'sub-region',
                     mapDeviceTypeToGlyph: mapDeviceTypeToGlyph,
+                    onClick: function () {
+                        wss.sendEvent('topo2navRegion', {
+                            dir: 'down',
+                            rid: this.get('id')
+                        });
+                    },
                     onEnter: function (el) {
 
                         var node = d3.select(el),
@@ -97,6 +105,7 @@
                             glyph, labelWidth;
 
                         this.el = node;
+                        this.el.on('click', this.onClick.bind(this));
 
                         // Label
                         var labelElements = this.addLabelElements(label);