GUI -- Link selection showing link details implemented.
- note: basic link data shown for now. will need enhancing.

Change-Id: I067edec6f336b5ea5c83c610622346d5fcedce38
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 9c0ebe0..870d4f8 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.css
+++ b/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -432,9 +432,15 @@
     opacity: .9;
 }
 
+#ov-topo svg .link.selected,
+#ov-topo svg .link.enhanced {
+    stroke-width: 4.5px;
+}
+.light #ov-topo svg .link.selected,
 .light #ov-topo svg .link.enhanced {
     filter: url(#blue-glow);
 }
+.dark #ov-topo svg .link.selected,
 .dark #ov-topo svg .link.enhanced {
     filter: url(#yellow-glow);
 }
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index 82f1173..50e465d 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -29,7 +29,7 @@
 
     // references to injected services etc.
     var $log, fs, ks, zs, gs, ms, sus, flash, wss,
-        tes, tfs, tps, tis, tss, tts, tos, ttbs;
+        tes, tfs, tps, tis, tss, tls, tts, tos, ttbs;
 
     // DOM elements
     var ovtopo, svg, defs, zoomLayer, mapG, forceG, noDevsLayer;
@@ -45,7 +45,7 @@
         actionMap = {
             I: [toggleInstances, 'Toggle ONOS instances pane'],
             O: [tps.toggleSummary, 'Toggle ONOS summary pane'],
-            D: [tss.toggleDetails, 'Disable / enable details pane'],
+            D: [tps.toggleDetails, 'Disable / enable details pane'],
 
             H: [tfs.toggleHosts, 'Toggle host visibility'],
             M: [tfs.toggleOffline, 'Toggle offline visibility'],
@@ -117,9 +117,13 @@
             // if an instance is selected, cancel the affinity mapping
             tis.cancelAffinity()
 
-        } else if (tss.haveDetails()) {
+        } else if (tss.deselectAll()) {
             // else if we have node selections, deselect them all
-            tss.deselectAll();
+            // (work already done)
+
+        } else if (tls.deselectLink()) {
+            // else if we have a link selected, deselect it
+            // (work already done)
 
         } else if (tis.isVisible()) {
             // else if the Instance Panel is visible, hide it
@@ -238,12 +242,12 @@
             'GlyphService', 'MapService', 'SvgUtilService', 'FlashService',
             'WebSocketService',
             'TopoEventService', 'TopoForceService', 'TopoPanelService',
-            'TopoInstService', 'TopoSelectService', 'TopoTrafficService',
-            'TopoObliqueService', 'TopoToolbarService',
+            'TopoInstService', 'TopoSelectService', 'TopoLinkService',
+            'TopoTrafficService', 'TopoObliqueService', 'TopoToolbarService',
 
-        function ($scope, _$log_, $loc, $timeout, _fs_, mast,
-                  _ks_, _zs_, _gs_, _ms_, _sus_, _flash_, _wss_,
-                  _tes_, _tfs_, _tps_, _tis_, _tss_, _tts_, _tos_, _ttbs_) {
+        function ($scope, _$log_, $loc, $timeout, _fs_, mast, _ks_, _zs_,
+                  _gs_, _ms_, _sus_, _flash_, _wss_, _tes_, _tfs_, _tps_,
+                  _tis_, _tss_, _tls_, _tts_, _tos_, _ttbs_) {
             var self = this,
                 projection,
                 dim,
@@ -273,6 +277,7 @@
             tps = _tps_;
             tis = _tis_;
             tss = _tss_;
+            tls = _tls_;
             tts = _tts_;
             tos = _tos_;
             ttbs = _ttbs_;
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 4a40782..11f4934 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -615,6 +615,7 @@
         d.fixed = true;
         d3.select(this).classed('fixed', true);
         sendUpdateMeta(d);
+        tss.clickConsumed(true);
     }
 
     // predicate that indicates when dragging is active
@@ -692,7 +693,8 @@
         return {
             node: function () { return node; },
             zoomingOrPanning: zoomingOrPanning,
-            updateDeviceColors: td3.updateDeviceColors
+            updateDeviceColors: td3.updateDeviceColors,
+            deselectLink: tls.deselectLink
         };
     }
 
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 3686bc5..8c8fd82 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoLink.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoLink.js
@@ -23,34 +23,35 @@
     'use strict';
 
     // injected refs
-    var $log, fs, sus, ts, flash;
+    var $log, fs, sus, ts, flash, tss, tps;
 
+    // internal state
     var api,
         td3,
         network,
-        enhancedLink = null;    // the link which the mouse is hovering over
+        showPorts = true,       // enable port highlighting by default
+        enhancedLink = null,    // the link over which the mouse is hovering
+        selectedLink = null;    // the link which is currently selected
 
     // SVG elements;
     var svg;
 
-    // internal state
-    var showPorts = true;       // enable port highlighting by default
-
 
     // ======== ALGORITHM TO FIND LINK CLOSEST TO MOUSE ========
 
-    function mouseMoveHandler() {
-        var m = d3.mouse(this),
+    function getLogicalMousePosition(container) {
+        var m = d3.mouse(container),
             sc = api.zoomer.scale(),
             tr = api.zoomer.translate(),
             mx = (m[0] - tr[0]) / sc,
             my = (m[1] - tr[1]) / sc;
-        computeNearestLink({x: mx, y: my});
+        return {x: mx, y: my};
     }
 
     function computeNearestLink(mouse) {
         var proximity = 30 / api.zoomer.scale(),
-            nearest, minDist;
+            nearest = null,
+            minDist;
 
         function sq(x) { return x * x; }
 
@@ -91,7 +92,6 @@
         }
 
         if (network.links.length) {
-            nearest = null;
             minDist = proximity * 2;
 
             network.links.forEach(function (d) {
@@ -112,13 +112,11 @@
                     }
                 }
             });
-
-            enhanceNearestLink(nearest);
         }
+        return nearest;
     }
 
-
-    function enhanceNearestLink(ldata) {
+    function enhanceLink(ldata) {
         // if the new link is same as old link, do nothing
         if (enhancedLink && ldata && enhancedLink.key === ldata.key) return;
 
@@ -148,7 +146,6 @@
         if (!d.el) return;
 
         d.el.classed('enhanced', true);
-        $log.debug('[' + (d.srcPort || 'H') + '] ---> [' + d.tgtPort + ']', d.key);
 
         // Define port label data objects.
         // NOTE: src port is absent in the case of host-links.
@@ -188,6 +185,62 @@
         return {x: k * dx + ln.x, y: k * dy + ln.y};
     }
 
+
+    function selectLink(ldata) {
+        // if the new link is same as old link, do nothing
+        if (selectedLink && ldata && selectedLink.key === ldata.key) return;
+
+        // make sure no nodes are selected
+        tss.deselectAll();
+
+        // first, unenhance the currently enhanced link
+        if (selectedLink) {
+            unselLink(selectedLink);
+        }
+        selectedLink = ldata;
+        if (selectedLink) {
+            selLink(selectedLink);
+        }
+    }
+
+    function unselLink(d) {
+        // guard against link element not set
+        if (d.el) {
+            d.el.classed('selected', false);
+        }
+    }
+
+    function selLink(d) {
+        // guard against link element not set
+        if (!d.el) return;
+
+        d.el.classed('selected', true);
+
+        tps.displayLink(d);
+        tps.displaySomething();
+    }
+
+    // ====== MOUSE EVENT HANDLERS ======
+
+    function mouseMoveHandler() {
+        var mp = getLogicalMousePosition(this),
+            link = computeNearestLink(mp);
+        enhanceLink(link);
+    }
+
+    function mouseClickHandler() {
+        var mp, link;
+
+        if (!tss.clickConsumed()) {
+            mp = getLogicalMousePosition(this);
+            link = computeNearestLink(mp);
+            selectLink(link);
+        }
+    }
+
+
+    // ======================
+
     function togglePorts() {
         showPorts = !showPorts;
 
@@ -195,25 +248,37 @@
             handler = showPorts ? mouseMoveHandler : null;
 
         if (!showPorts) {
-            enhanceNearestLink(null);
+            enhanceLink(null);
         }
         svg.on('mousemove', handler);
         flash.flash(what + ' port highlighting');
     }
 
+    function deselectLink() {
+        if (selectedLink) {
+            unselLink(selectedLink);
+            selectedLink = null;
+            return true;
+        }
+        return false;
+    }
+
     // ==========================
     // Module definition
 
     angular.module('ovTopo')
         .factory('TopoLinkService',
         ['$log', 'FnService', 'SvgUtilService', 'ThemeService', 'FlashService',
+            'TopoSelectService', 'TopoPanelService',
 
-        function (_$log_, _fs_, _sus_, _ts_, _flash_) {
+        function (_$log_, _fs_, _sus_, _ts_, _flash_, _tss_, _tps_) {
             $log = _$log_;
             fs = _fs_;
             sus = _sus_;
             ts = _ts_;
             flash = _flash_;
+            tss = _tss_;
+            tps = _tps_;
 
             function initLink(_api_, _td3_) {
                 api = _api_;
@@ -223,17 +288,20 @@
                 if (showPorts) {
                     svg.on('mousemove', mouseMoveHandler);
                 }
+                svg.on('click', mouseClickHandler);
             }
 
             function destroyLink() {
-                // unconditionally remove any mousemove event handler
+                // unconditionally remove any event handlers
                 svg.on('mousemove', null);
+                svg.on('click', null);
             }
 
             return {
                 initLink: initLink,
                 destroyLink: destroyLink,
-                togglePorts: togglePorts
+                togglePorts: togglePorts,
+                deselectLink: deselectLink
             };
         }]);
 }());
diff --git a/web/gui/src/main/webapp/app/view/topo/topoPanel.js b/web/gui/src/main/webapp/app/view/topo/topoPanel.js
index 485bfc8..e5a4eef 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoPanel.js
@@ -23,7 +23,7 @@
     'use strict';
 
     // injected refs
-    var $log, fs, ps, gs, wss;
+    var $log, fs, ps, gs, flash, wss;
 
     // constants
     var pCls = 'topo-p',
@@ -37,6 +37,9 @@
     var summaryPanel,
         detailPanel;
 
+    // internal state
+    var useDetails = true,      // should we show details if we have 'em?
+        haveDetails = false;    // do we have details that we could show?
 
     // === -----------------------------------------------------
     // Utility functions
@@ -129,6 +132,42 @@
             .on('click', cb);
     }
 
+    function displayLink(data) {
+        detailPanel.empty();
+
+        var svg = dpa('svg'),
+            title = dpa('h2'),
+            table = dpa('table'),
+            tbody = table.append('tbody');
+
+        gs.addGlyph(svg, 'ports', 40);
+        title.text('Link');
+        listProps(tbody, {
+            propOrder: [
+                'type', '-', 'src', 'srcPort', '-', 'tgt', 'tgtPort'
+            ],
+            props: {
+                type: data.type(),
+                src: data.source.id,
+                srcPort: data.srcPort,
+                tgt: data.target.id,
+                tgtPort: data.tgtPort
+            }
+        });
+    }
+
+    function displayNothing() {
+        haveDetails = false;
+        hideDetailPanel();
+    }
+
+    function displaySomething() {
+        haveDetails = true;
+        if (useDetails) {
+            showDetailPanel();
+        }
+    }
+
     // === -----------------------------------------------------
     //  Event Handlers
 
@@ -201,6 +240,19 @@
         dp.up = function (cb) { dp._move(dp.ypos.up, cb); };
     }
 
+    function toggleDetails() {
+        useDetails = !useDetails;
+        if (useDetails) {
+            flash.flash('Enable details panel');
+            if (haveDetails) {
+                showDetailPanel();
+            }
+        } else {
+            flash.flash('Disable details panel');
+            hideDetailPanel();
+        }
+    }
+
     // ==========================
 
     function initPanels() {
@@ -223,13 +275,15 @@
 
     angular.module('ovTopo')
     .factory('TopoPanelService',
-        ['$log', 'FnService', 'PanelService', 'GlyphService', 'WebSocketService',
+        ['$log', 'FnService', 'PanelService', 'GlyphService',
+            'FlashService', 'WebSocketService',
 
-        function (_$log_, _fs_, _ps_, _gs_, _wss_) {
+        function (_$log_, _fs_, _ps_, _gs_, _flash_, _wss_) {
             $log = _$log_;
             fs = _fs_;
             ps = _ps_;
             gs = _gs_;
+            flash = _flash_;
             wss = _wss_;
 
             return {
@@ -239,13 +293,15 @@
                 showSummary: showSummary,
                 toggleSummary: toggleSummary,
 
+                toggleDetails: toggleDetails,
                 displaySingle: displaySingle,
                 displayMulti: displayMulti,
                 addAction: addAction,
+                displayLink: displayLink,
+                displayNothing: displayNothing,
+                displaySomething: displaySomething,
 
                 hideSummaryPanel: hideSummaryPanel,
-                showDetailPanel: showDetailPanel,
-                hideDetailPanel: hideDetailPanel,
 
                 detailVisible: function () { return detailPanel.isVisible(); },
                 summaryVisible: function () { return summaryPanel.isVisible(); }
diff --git a/web/gui/src/main/webapp/app/view/topo/topoSelect.js b/web/gui/src/main/webapp/app/view/topo/topoSelect.js
index af0e041..5d456aa 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoSelect.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoSelect.js
@@ -23,7 +23,7 @@
     'use strict';
 
     // injected refs
-    var $log, fs, flash, wss, tps, tts;
+    var $log, fs, wss, tps, tts;
 
     // api to topoForce
     var api;
@@ -31,14 +31,14 @@
        node()                         // get ref to D3 selection of nodes
        zoomingOrPanning( ev )
        updateDeviceColors( [dev] )
+       deselectLink()
      */
 
     // internal state
     var hovered,                // the node over which the mouse is hovering
         selections = {},        // currently selected nodes (by id)
         selectOrder = [],       // the order in which we made selections
-        haveDetails = false,    // do we have details of one or more nodes?
-        useDetails = true;      // should we show details if we have 'em?
+        consumeClick = false;   // used to coordinate with SVG click handler
 
     // ==========================
 
@@ -101,6 +101,9 @@
         }
         if (!n) return;
 
+        consumeClick = true;
+        api.deselectLink();
+
         if (ev.shiftKey && n.classed('selected')) {
             deselectObject(obj.id);
             updateDetail();
@@ -130,12 +133,17 @@
     }
 
     function deselectAll() {
+        var something = (selectOrder.length > 0);
+
         // deselect all nodes in the network...
         api.node().classed('selected', false);
         selections = {};
         selectOrder = [];
         api.updateDeviceColors();
         updateDetail();
+
+        // return true if something was selected
+        return something;
     }
 
     // === -----------------------------------------------------
@@ -162,9 +170,8 @@
     }
 
     function emptySelect() {
-        haveDetails = false;
-        tps.hideDetailPanel();
         tts.cancelTraffic();
+        tps.displayNothing();
     }
 
     function singleSelect() {
@@ -175,8 +182,6 @@
     }
 
     function multiSelect() {
-        haveDetails = true;
-
         // display the selected nodes in the detail panel
         tps.displayMulti(selectOrder);
 
@@ -192,6 +197,7 @@
 
         tts.cancelTraffic();
         tts.requestTrafficForMode();
+        tps.displaySomething();
     }
 
 
@@ -199,8 +205,6 @@
     //  Event Handlers
 
     function showDetails(data) {
-        haveDetails = true;
-
         // display the data for the single selected node
         tps.displaySingle(data);
 
@@ -212,23 +216,7 @@
             tps.addAction('Show Device Flows', tts.showDeviceLinkFlowsAction);
         }
 
-        // only show the details panel if the user hasn't "hidden" it
-        if (useDetails) {
-            tps.showDetailPanel();
-        }
-    }
-
-    function toggleDetails() {
-        useDetails = !useDetails;
-        if (useDetails) {
-            flash.flash('Enable details panel');
-            if (haveDetails) {
-                tps.showDetailPanel();
-            }
-        } else {
-            flash.flash('Disable details panel');
-            tps.hideDetailPanel();
-        }
+        tps.displaySomething();
     }
 
     function validateSelectionContext() {
@@ -239,18 +227,23 @@
         return true;
     }
 
+    function clickConsumed(x) {
+        var cc = consumeClick;
+        consumeClick = !!x;
+        return cc;
+    }
+
     // === -----------------------------------------------------
     // === MODULE DEFINITION ===
 
     angular.module('ovTopo')
     .factory('TopoSelectService',
-        ['$log', 'FnService', 'FlashService', 'WebSocketService',
+        ['$log', 'FnService', 'WebSocketService',
             'TopoPanelService', 'TopoTrafficService',
 
-        function (_$log_, _fs_, _flash_, _wss_, _tps_, _tts_) {
+        function (_$log_, _fs_, _wss_, _tps_, _tts_) {
             $log = _$log_;
             fs = _fs_;
-            flash = _flash_;
             wss = _wss_;
             tps = _tps_;
             tts = _tts_;
@@ -266,7 +259,6 @@
                 destroySelect: destroySelect,
 
                 showDetails: showDetails,
-                toggleDetails: toggleDetails,
 
                 nodeMouseOver: nodeMouseOver,
                 nodeMouseOut: nodeMouseOut,
@@ -275,9 +267,10 @@
                 deselectAll: deselectAll,
 
                 hovered: function () { return hovered; },
-                haveDetails: function () { return haveDetails; },
                 selectOrder: function () { return selectOrder; },
-                validateSelectionContext: validateSelectionContext
+                validateSelectionContext: validateSelectionContext,
+
+                clickConsumed: clickConsumed
             };
         }]);
 }());