Topo2: Topo2SelectService now maintains de/selecting nodes and displaying the details panel

Change-Id: I29d2476d8615263d79304636df6ca1664e7dc76b
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 823387c..fadcd93 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
@@ -47,19 +47,23 @@
 
     angular.module('ovTopo2')
     .factory('Topo2DeviceService',
-        ['Topo2Collection', 'Topo2NodeModel', 'Topo2DeviceDetailsPanel',
-            function (_c_, _nm_, detailsPanel) {
+        ['Topo2Collection', 'Topo2NodeModel', 'Topo2DeviceDetailsPanel', 'Topo2SelectService',
+            function (_c_, _nm_, detailsPanel, t2ss) {
 
                 Collection = _c_;
 
                 Model = _nm_.extend({
+
+                    nodeType: 'device',
+                    multiSelectEnabled: true,
+                    events: {
+                        'click': 'onClick'
+                    },
+
                     initialize: function () {
                         this.super = this.constructor.__super__;
                         this.super.initialize.apply(this, arguments);
                     },
-                    events: {
-                        'click': 'onClick'
-                    },
                     onChange: function (change) {
                         if (this.el) {
                             this.el.attr('class', this.svgClassName());
@@ -67,30 +71,15 @@
                             rect.style('fill', this.devGlyphColor());
                         }
                     },
-                    nodeType: 'device',
                     icon: function () {
                         var type = this.get('type');
                         return remappedDeviceTypes[type] || type || 'unknown';
                     },
-                    onClick: function () {
-
-                        if (d3.event.defaultPrevented) return;
-                        var selected = this.select(d3.event);
-
-                        if (_.isArray(selected) && selected.length > 0) {
-                            if (selected.length === 1) {
-                                var model = selected[0],
-                                    id = model.get('id'),
-                                    nodeType = model.get('nodeType');
-                                detailsPanel.updateDetails(id, nodeType);
-                                detailsPanel.show();
-                            } else {
-                                // Multi Panel
-                                detailsPanel.showMulti(selected);
-                            }
-                        } else {
-                            detailsPanel.hide();
-                        }
+                    showDetails: function () {
+                        var id = this.get('id'),
+                            nodeType = this.get('nodeType');
+                        detailsPanel.updateDetails(id, nodeType);
+                        detailsPanel.show();
                     },
                     onExit: function () {
                         var node = this.el;
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 e1e392b..fc3e5b739 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
@@ -46,8 +46,9 @@
         t2bgs.region = t2rs;
         t2ls.init(svg, uplink, dim, zoomer, opts);
         t2bcs.addLayout(t2ls);
-        t2rs.layout = t2ls;
         t2ss.init(svg, zoomer);
+        t2ss.region = t2rs;
+        t2rs.layout = t2ls;
 
         navToBookmarkedRegion($loc.search().regionId);
     }
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 787819b..5688dc5 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
@@ -54,30 +54,25 @@
             Collection = _c_;
 
             Model = NodeModel.extend({
+
+                nodeType: 'host',
+                events: {
+                    'click': 'onClick'
+                },
+
                 initialize: function () {
                     this.super = this.constructor.__super__;
                     this.super.initialize.apply(this, arguments);
                 },
-                events: {
-                    'click': 'onClick'
-                },
                 onChange: function () {
                     // Update class names when the model changes
                     if (this.el) {
                         this.el.attr('class', this.svgClassName());
                     }
                 },
-                onClick: function () {
-                    if (d3.event.defaultPrevented) return;
-                    var selected = this.select(d3.select);
-
-                    if (selected.length > 0) {
-                        t2hds.displayPanel(this);
-                    } else {
-                        t2hds.hide();
-                    }
+                showDetails: function() {
+                    t2hds.displayPanel(this);
                 },
-                nodeType: 'host',
                 icon: function () {
                     var type = this.get('type');
                     return remappedDeviceTypes[type] || type || 'm_endstation';
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 4e21040..473bd3a 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
@@ -158,7 +158,7 @@
                             .data(regionNodes, function (d) { return d.get('id'); });
 
                         this.drag = sus.createDragBehavior(this.force,
-                            t2ss.selectObject,
+                            function () {}, // click event is no longer handled in the drag service
                             this.atDragEnd,
                             this.dragEnabled.bind(this),
                             clickEnabled
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 7c674c3..e94ed6c 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
@@ -217,22 +217,14 @@
                 });
             },
             select: function () {
-
-                // TODO: if single selection clear selected devices, hosts, sub-regions
-                var s = Boolean(this.get('selected'));
-                // Clear all selected Items
-                _.each(this.collection.models, function (m) {
-                    m.set('selected', false);
-                });
-
-                this.set('selected', !s);
-                this.showDetails();
-
+                this.set({ 'selected': true });
                 return this.getSelected();
             },
             deselect: function () {
-                this.set('selected', false);
-                this.set('enhanced', false);
+                this.set({
+                    'selected': false,
+                    'enhanced': false
+                });
             },
             showDetails: function () {
                 var selected = this.getSelected();
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 9d46494..fa3a345 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
@@ -49,8 +49,9 @@
         'Topo2Model', 'FnService', 'Topo2PrefsService',
         'SvgUtilService', 'IconService', 'ThemeService',
         'Topo2MapConfigService', 'Topo2ZoomService', 'Topo2NodePositionService',
+        'Topo2SelectService',
         function (Model, _fn_, _ps_, _sus_, _is_, _ts_,
-            _t2mcs_, zoomService, _t2nps_) {
+            _t2mcs_, zoomService, _t2nps_, t2ss) {
 
             ts = _ts_;
             fn = _fn_;
@@ -69,29 +70,7 @@
                     };
                 },
                 select: function () {
-                    var ev = d3.event;
-
-                    // TODO: if single selection clear selected devices, hosts, sub-regions
-
-                    if (ev.shiftKey) {
-                        // TODO: Multi-Select Details Panel
-                        this.set('selected', true);
-                    } else {
-
-                        var s = Boolean(this.get('selected'));
-                        // Clear all selected Items
-                        _.each(this.collection.models, function (m) {
-                            m.set('selected', false);
-                        });
-
-                        this.set('selected', !s);
-                    }
-
-                    var selected = this.collection.filter(function (m) {
-                        return m.get('selected');
-                    });
-
-                    return selected;
+                    this.set('selected', true);
                 },
                 index: function () {
 
@@ -125,6 +104,12 @@
                 mouseoutHandler: function () {
                     this.set('hovered', false);
                 },
+                onClick: function () {
+                    if (d3.event.defaultPrevented) return;
+
+                    d3.event.preventDefault();
+                    t2ss.selectObject(this, this.multiSelectEnabled);
+                },
                 fix: function (fixed) {
                     this.set({ fixed: fixed });
                     this.fixed = fixed;
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2PeerRegion.js b/web/gui/src/main/webapp/app/view/topo2/topo2PeerRegion.js
index 2ae8e64..64dc954 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2PeerRegion.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2PeerRegion.js
@@ -48,34 +48,30 @@
                 Collection = _c_;
 
                 Model = NodeModel.extend({
-                    initialize: function () {
-                        this.super = this.constructor.__super__;
-                        this.super.initialize.apply(this, arguments);
-                    },
+
+                    nodeType: 'peer-region',
                     events: {
                         'dblclick': 'navigateToRegion',
                         'click': 'onClick'
                     },
+
+                    initialize: function () {
+                        this.super = this.constructor.__super__;
+                        this.super.initialize.apply(this, arguments);
+                    },
                     onChange: function () {
                         // Update class names when the model changes
                         if (this.el) {
                             this.el.attr('class', this.svgClassName());
                         }
                     },
-                    nodeType: 'peer-region',
+                    showDetails: function () {
+                        t2srp.displayPanel(this);
+                    },
                     icon: function () {
                         var type = this.get('type');
                         return remappedDeviceTypes[type] || type || 'm_cloud';
                     },
-                    onClick: function () {
-                        var selected = this.select(d3.event);
-
-                        if (selected.length > 0) {
-                            t2srp.displayPanel(this);
-                        } else {
-                            t2srp.hide();
-                        }
-                    },
                     navigateToRegion: function () {
 
                         if (d3.event.defaultPrevented) return;
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 67b2a7c..d79cea9 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
@@ -148,6 +148,7 @@
                     return false;
                 },
                 deselectLink: function () {
+                    console.log('remove link')
                     var selected = _.filter(this.regionLinks(), function (link) {
                         return link.get('selected', true);
                     });
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 242d55a0..e172e0d 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Select.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Select.js
@@ -21,43 +21,30 @@
 (function () {
     'use strict';
 
-    var t2rs, t2zs;
+    var t2zs, t2ddp;
 
     // internal state
-    var consumeClick,
+    var instance,
+        consumeClick,
         zoomer,
         previousNearestLink;    // previous link to mouse position
 
-    function init(svg) {
-        zoomer = t2zs.getZoomer();
-        svg.on('mousemove', mouseMoveHandler);
-        svg.on('click', mouseClickHandler);
-    }
-
-    function selectObject(obj) {}
-
-    function clickConsumed(x) {
-        var cc = consumeClick;
-        consumeClick = Boolean(x);
-        return cc;
-    }
-
     function mouseClickHandler() {
+        if (d3.event.defaultPrevented) return;
 
         if (!d3.event.shiftKey) {
-            t2rs.deselectLink();
+            this.clearSelection();
         }
 
-        if (!clickConsumed()) {
+        if (!this.clickConsumed()) {
             if (previousNearestLink) {
-                previousNearestLink.select();
+                this.selectObject(previousNearestLink, true);
             }
         }
-
     }
 
     // Select Links
-    function mouseMoveHandler() {
+    function mouseMoveHandler(ev) {
         var mp = getLogicalMousePosition(this),
             link = computeNearestLink(mp);
 
@@ -124,8 +111,8 @@
 
         var links = [];
 
-        if (t2rs.model.get('links')) {
-            links = (t2rs.backgroundRendered) ? t2rs.regionLinks() : [];
+        if (instance.region.model.get('links')) {
+            links = instance.region.regionLinks();
         }
 
         if (links.length) {
@@ -159,19 +146,76 @@
         return nearest;
     }
 
+    var SelectionService = function () {
+        instance = this;
+        this.selectedNodes = [];
+    };
+
+    SelectionService.prototype = {
+        init: function () {
+            zoomer = t2zs.getZoomer();
+
+            var svg = d3.select('#topo2');
+            svg.on('mousemove', mouseMoveHandler);
+            svg.on('click', mouseClickHandler.bind(this));
+        },
+        updateDetails: function () {
+
+            var nodeCount =  this.selectedNodes.length;
+
+            if (nodeCount === 1) {
+                this.selectedNodes[0].showDetails();
+            } else if (nodeCount > 1)  {
+                t2ddp.showMulti(this.selectedNodes);
+            } else {
+                t2ddp.hide();
+            }
+        },
+        selectObject: function (node, multiSelectEnabled) {
+
+            var event = d3.event;
+
+            if (multiSelectEnabled && !event.shiftKey || !multiSelectEnabled) {
+                this.clearSelection();
+            }
+
+            var nodeIndex = _.indexOf(this.selectedNodes, node);
+
+            if (nodeIndex < 0) {
+                this.selectedNodes.push(node);
+                node.select();
+            } else {
+                this.removeNode(node, nodeIndex);
+            }
+
+            this.updateDetails();
+        },
+        removeNode: function (node, index) {
+            this.selectedNodes.splice(index, 1);
+            node.deselect();
+        },
+        clearSelection: function () {
+            _.each(this.selectedNodes, function (node) {
+                node.deselect();
+            });
+
+            this.selectedNodes = [];
+            this.updateDetails();
+        },
+        clickConsumed: function (x) {
+            var cc = consumeClick;
+            consumeClick = Boolean(x);
+            return cc;
+        }
+    };
+
     angular.module('ovTopo2')
     .factory('Topo2SelectService', [
-        'Topo2RegionService', 'Topo2ZoomService',
-        function (_t2rs_, _t2zs_) {
-
-            t2rs = _t2rs_;
+        'Topo2ZoomService', 'Topo2DeviceDetailsPanel',
+        function (_t2zs_, _t2ddp_) {
             t2zs = _t2zs_;
-
-            return {
-                init: init,
-                selectObject: selectObject,
-                clickConsumed: clickConsumed
-            };
+            t2ddp = _t2ddp_;
+            return instance || new SelectionService();
         }
     ]);
 
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 02c8b73..4696050 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
@@ -48,36 +48,30 @@
             Collection = _c_;
 
             Model = NodeModel.extend({
-                initialize: function () {
-                    this.super = this.constructor.__super__;
-                    this.super.initialize.apply(this, arguments);
-                },
+
+                nodeType: 'sub-region',
                 events: {
                     'dblclick': 'navigateToRegion',
                     'click': 'onClick'
                 },
+
+                initialize: function () {
+                    this.super = this.constructor.__super__;
+                    this.super.initialize.apply(this, arguments);
+                },
                 onChange: function () {
                     // Update class names when the model changes
                     if (this.el) {
                         this.el.attr('class', this.svgClassName());
                     }
                 },
-                nodeType: 'sub-region',
+                showDetails: function () {
+                    t2srp.displayPanel(this);
+                },
                 icon: function () {
                     var type = this.get('type');
                     return remappedDeviceTypes[type] || type || 'm_cloud';
                 },
-                onClick: function () {
-                    if (d3.event.defaultPrevented) return;
-
-                    var selected = this.select(d3.event);
-
-                    if (selected.length > 0) {
-                        t2srp.displayPanel(this);
-                    } else {
-                        t2srp.hide();
-                    }
-                },
                 navigateToRegion: function () {
 
                     if (d3.event.defaultPrevented) return;