Topo2: Compute nearest link by mouse position
Topo2: Deselect Nodes and Links on ESC command
Topo2: Added deselect methods to nodes
Topo2: Updated to new icon

Change-Id: Ia0aaa24e887d645123787f42bb1f847ef1de11b0
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 79762cb..6845aa0 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2.js
@@ -196,7 +196,7 @@
             // initialize the force layout, ready to render the topology
             forceG = zoomLayer.append('g').attr('id', 'topo-force');
 
-            t2fs.init(svg, forceG, uplink, dim);
+            t2fs.init(svg, forceG, uplink, dim, zoomer);
             t2bcs.init();
             t2kcs.init(t2fs);
             t2is.initInst({ showMastership: t2fs.showMastership });
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
index b6ed850..852e0a5 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
@@ -27,21 +27,22 @@
         wss;
 
     var t2is, t2rs, t2ls, t2vs, t2bcs;
-    var svg, forceG, uplink, dim, opts;
+    var svg, forceG, uplink, dim, opts, zoomer;
 
     // D3 Selections
     var node;
 
     // ========================== Helper Functions
 
-    function init(_svg_, _forceG_, _uplink_, _dim_, _opts_) {
+    function init(_svg_, _forceG_, _uplink_, _dim_, _zoomer_, _opts_) {
         svg = _svg_;
         forceG = _forceG_;
         uplink = _uplink_;
         dim = _dim_;
         opts = _opts_;
+        zoomer = _zoomer_;
 
-        t2ls.init(svg, forceG, uplink, dim, opts);
+        t2ls.init(svg, forceG, uplink, dim, zoomer, opts);
     }
 
     function destroy() {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Host.js b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
index 2fa7beb..ee13581 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
@@ -79,7 +79,7 @@
                 nodeType: 'host',
                 icon: function () {
                     var type = this.get('type');
-                    return remappedDeviceTypes[type] || type || 'endstation';
+                    return remappedDeviceTypes[type] || type || 'm_endstation';
                 },
                 label: function () {
                     var labelText = this.get('id'),
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2KeyCommands.js b/web/gui/src/main/webapp/app/view/topo2/topo2KeyCommands.js
index 2cc8570..02aa984 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2KeyCommands.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2KeyCommands.js
@@ -17,9 +17,7 @@
 (function () {
 
     // Injected Services
-    var ks, flash, wss, t2ps, t2ms, ps, t2is, t2sp, t2vs;
-
-    var t2fs;
+    var ks, flash, wss, t2ps, t2ms, ps, t2is, t2sp, t2vs, t2rs, t2fs;
 
     // Commmands
     var actionMap = {
@@ -56,13 +54,24 @@
     }
 
     function handleEscape() {
-        if (t2ddp.isVisible()) {
-            t2ddp.toggle();
-        } else if (t2sp.isVisible()) {
-            t2sp.toggle();
+
+        if (false) {
+            // TODO: Cancel show mastership
+            // TODO: Cancel Active overlay
+
+        } else if (t2rs.deselectAllNodes()) {
+            // else if we have node selections, deselect them all
+            // (work already done)
+        } else if (t2rs.deselectLink()) {
+            // else if we have a link selection, deselect it
+            // (work already done)
         } else if (t2is.isVisible()) {
-            t2is.toggle(); 
-        }  
+            // If the instance panel is visible, close it
+            t2is.toggle();
+        } else if (t2sp.isVisible()) {
+            // If the summary panel is visible, close it
+            t2sp.toggle();
+        }
     }
 
     var prefsState = {};
@@ -135,7 +144,9 @@
     ['KeyService', 'FlashService', 'WebSocketService', 'Topo2PrefsService',
     'Topo2MapService', 'PrefsService', 'Topo2InstanceService',
     'Topo2SummaryPanelService', 'Topo2DeviceDetailsPanel', 'Topo2ViewService',
-        function (_ks_, _flash_, _wss_, _t2ps_, _t2ms_, _ps_, _t2is_, _t2sp_, _t2ddp_, _t2vs_) {
+    'Topo2RegionService',
+        function (_ks_, _flash_, _wss_, _t2ps_, _t2ms_, _ps_, _t2is_, _t2sp_,
+                  _t2ddp_, _t2vs_, _t2rs_) {
 
             ks = _ks_;
             flash = _flash_;
@@ -147,6 +158,7 @@
             t2sp = _t2sp_;
             t2ddp = _t2ddp_;
             t2vs = _t2vs_;
+            t2rs = _t2rs_;
 
             return {
                 init: init,
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 7274761..6e5df6c 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
@@ -25,7 +25,7 @@
     var $log, wss, sus, t2rs, t2d3, t2vs, t2ss;
 
     var linkG, linkLabelG, nodeG;
-    var link, node;
+    var link, node, zoomer;
 
     // default settings for force layout
     var defaultSettings = {
@@ -73,12 +73,14 @@
     };
 
     // internal state
-    var settings,   // merged default settings and options
-        force,      // force layout object
-        drag,       // drag behavior handler
+    var settings,               // merged default settings and options
+        force,                  // force layout object
+        drag,                   // drag behavior handler
+        previousNearestLink,    // previous link to mouse position
         nodeLock = false;       // whether nodes can be dragged or not (locked)
 
-    function init(_svg_, forceG, _uplink_, _dim_, opts) {
+
+    function init(_svg_, forceG, _uplink_, _dim_, _zoomer_, opts) {
 
         $log.debug("Initialising Topology Layout");
         settings = angular.extend({}, defaultSettings, opts);
@@ -92,6 +94,10 @@
         link = linkG.selectAll('.link');
         linkLabelG.selectAll('.linkLabel');
         node = nodeG.selectAll('.node');
+
+        zoomer = _zoomer_;
+        _svg_.on('mousemove', mouseMoveHandler);
+        _svg_.on('click', mouseClickHandler);
     }
 
     function getDeviceChargeForType(node) {
@@ -325,6 +331,121 @@
         force.start();
     }
 
+    function mouseClickHandler() {
+
+        if (!d3.event.shiftKey) {
+            t2rs.deselectLink();
+        }
+
+        if (!t2ss.clickConsumed()) {
+            if (previousNearestLink) {
+                previousNearestLink.select();
+            }
+        }
+
+    }
+
+    // Select Links
+    function mouseMoveHandler() {
+        var mp = getLogicalMousePosition(this),
+            link = computeNearestLink(mp);
+
+        // link.enhance();
+        if (link) {
+            if (previousNearestLink && previousNearestLink != link) {
+                previousNearestLink.unenhance();
+            }
+            link.enhance();
+        } else {
+            if (previousNearestLink) {
+                previousNearestLink.unenhance();
+            }
+        }
+
+        previousNearestLink = link;
+    }
+
+
+    function getLogicalMousePosition(container) {
+        var m = d3.mouse(container),
+            sc = zoomer.scale(),
+            tr = 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 / zoomer.scale();
+    }
+
+    function computeNearestLink(mouse) {
+        var proximity = prox(30),
+            nearest = null,
+            minDist;
+
+        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;
+        }
+
+        var links = t2rs.regionLinks();
+
+        if (links.length) {
+            minDist = proximity * 2;
+
+            links.forEach(function (d) {
+                var line = d.get('position'),
+                    point,
+                    hit,
+                    dist;
+
+                // TODO: Reinstate when showHost() is implemented
+                // if (!api.showHosts() && d.type() === 'hostLink') {
+                //     return; // skip hidden host links
+                // }
+
+                if (line) {
+                    point = pdrop(line, mouse);
+                    hit = lineHit(line, point, mouse);
+                    if (hit) {
+                        dist = mdist(point, mouse);
+                        if (dist < minDist) {
+                            minDist = dist;
+                            nearest = d;
+                        }
+                    }
+                }
+            });
+        }
+
+        return nearest;
+    }
+
     angular.module('ovTopo2')
     .factory('Topo2LayoutService',
         [
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 b552a9e..d769631 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
@@ -157,9 +157,9 @@
                 var data = [],
                     point;
 
-                angular.forEach(this.collection.models, function (link) {
-                    link.unenhance();
-                });
+                // angular.forEach(this.collection.models, function (link) {
+                //     link.unenhance();
+                // });
 
                 this.set('enhanced', true);
 
@@ -212,7 +212,13 @@
                 this.set('enhanced', false);
                 d3.select('#topo-portLabels').selectAll('.portLabel').remove();
             },
+            getSelected: function () {
+                return this.collection.filter(function (m) {
+                    return m.get('selected');
+                });
+            },
             select: function () {
+
                 var ev = d3.event;
 
                 // TODO: if single selection clear selected devices, hosts, sub-regions
@@ -223,15 +229,16 @@
                 });
 
                 this.set('selected', !s);
+                this.showDetails();
 
-                var selected = this.collection.filter(function (m) {
-                    return m.get('selected');
-                });
-
-                return selected;
+                return this.getSelected();
+            },
+            deselect: function () {
+                this.set('selected', false);
+                this.set('enhanced', false);
             },
             showDetails: function () {
-                var selected = this.select(d3.event);
+                var selected = this.getSelected();
 
                 if (selected) {
                     t2lps.displayLink(this);
@@ -298,12 +305,6 @@
                 this.el = link;
                 this.restyleLinkElement();
 
-                // TODO: Needs improving - originally this was calculated
-                // from mouse position.
-                this.el.on('mouseover', this.enhance.bind(this));
-                this.el.on('mouseout', this.unenhance.bind(this));
-                this.el.on('click', this.showDetails.bind(this));
-
                 if (this.get('type') === 'hostLink') {
                     // sus.visible(link, api.showHosts());
                 }
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
index 18ad7cc..cf5bb15 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
@@ -101,6 +101,9 @@
 
                     return selected;
                 },
+                deselect: function () {
+                    this.set('selected', false);
+                },
                 createNode: function () {
                     this.set('svgClass', this.svgClassName());
                     t2nps.positionNode(this);
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
index 73b76a5..8df63d6 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
@@ -23,11 +23,11 @@
     'use strict';
 
     // Injected Services
-    var $log, t2sr, t2ds, t2hs, t2ls, t2zs;
+    var $log, t2sr, t2ds, t2hs, t2ls, t2zs, t2dps;
     var Model;
 
     // Internal
-    var region;
+    var region
 
     function init() {}
 
@@ -84,8 +84,8 @@
 
 
         setTimeout(function () {
-            var reigionPZ = regionPanZooms[region.get('id')];
-            t2zs.panAndZoom(reigionPZ.translate, reigionPZ.scale);
+            var regionPZ = regionPanZooms[region.get('id')];
+            t2zs.panAndZoom(regionPZ.translate, regionPZ.scale);
         }, 10);
 
         $log.debug('Region: ', region);
@@ -127,13 +127,53 @@
         return (region) ? region.get('links').models : [];
     }
 
+    function deselectAllNodes() {
+
+        var selected = filterRegionNodes(function (node) {
+            return node.get('selected', true);
+        });
+
+        if (selected.length) {
+
+            selected.forEach(function (node) {
+                node.deselect();
+            });
+
+            t2dps().el.hide();
+            return true;
+        }
+
+        // TODO: close details panel
+
+        return false;
+    }
+
+    function deselectLink() {
+
+        var selected = _.filter(regionLinks(), function (link) {
+            return link.get('selected', true);
+        });
+
+        if (selected.length) {
+
+            selected.forEach(function (link) {
+                link.deselect();
+            });
+
+            t2dps().el.hide();
+            return true;
+        }
+
+        return false;
+    }
+
     angular.module('ovTopo2')
     .factory('Topo2RegionService',
         ['$log', 'Topo2Model',
         'Topo2SubRegionService', 'Topo2DeviceService',
-        'Topo2HostService', 'Topo2LinkService', 'Topo2ZoomService',
+        'Topo2HostService', 'Topo2LinkService', 'Topo2ZoomService', 'Topo2DetailsPanelService',
 
-        function (_$log_, _Model_, _t2sr_, _t2ds_, _t2hs_, _t2ls_, _t2zs_) {
+        function (_$log_, _Model_, _t2sr_, _t2ds_, _t2hs_, _t2ls_, _t2zs_, _t2dps_) {
 
             $log = _$log_;
             Model = _Model_;
@@ -142,6 +182,7 @@
             t2hs = _t2hs_;
             t2ls = _t2ls_;
             t2zs = _t2zs_;
+            t2dps = _t2dps_;
 
             return {
                 init: init,
@@ -151,6 +192,9 @@
                 regionLinks: regionLinks,
                 filterRegionNodes: filterRegionNodes,
 
+                deselectAllNodes: deselectAllNodes,
+                deselectLink: deselectLink,
+
                 getSubRegions: t2sr.getSubRegions
             };
         }]);