GUI -- TopoView - added node selection logic.
- added inArray() and removeFromArray() functions to FnService.

Change-Id: I0e9631fa9e5865cb171e8d505f45c1963a1903dc
diff --git a/web/gui/src/main/webapp/app/fw/util/fn.js b/web/gui/src/main/webapp/app/fw/util/fn.js
index 690c8b2..dcc2725 100644
--- a/web/gui/src/main/webapp/app/fw/util/fn.js
+++ b/web/gui/src/main/webapp/app/fw/util/fn.js
@@ -117,6 +117,32 @@
         return -1;
     }
 
+    // search through array to find (the first occurrence of) item,
+    // returning its index if found; otherwise returning -1.
+    function inArray(item, array) {
+        var i;
+        if (isA(array)) {
+            for (i=0; i<array.length; i++) {
+                if (array[i] === item) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    // remove (the first occurrence of) the specified item from the given
+    // array, if any. Return true if the removal was made; false otherwise.
+    function removeFromArray(item, array) {
+        var found = false,
+            i = inArray(item, array);
+        if (i >= 0) {
+            array.splice(i, 1);
+            found = true;
+        }
+        return found;
+    }
+
     angular.module('onosUtil')
         .factory('FnService', ['$window', function (_$window_) {
             $window = _$window_;
@@ -130,7 +156,9 @@
                 areFunctions: areFunctions,
                 areFunctionsNonStrict: areFunctionsNonStrict,
                 windowSize: windowSize,
-                find: find
+                find: find,
+                inArray: inArray,
+                removeFromArray: removeFromArray
             };
     }]);
 
diff --git a/web/gui/src/main/webapp/app/index.html b/web/gui/src/main/webapp/app/index.html
index 7bd640d..c0e9228 100644
--- a/web/gui/src/main/webapp/app/index.html
+++ b/web/gui/src/main/webapp/app/index.html
@@ -111,8 +111,7 @@
         <div id="quickhelp"></div>
         <div id="veil"
              resize
-             ng-style="resizeWithOffset(0, 0)"
-                ></div>
+             ng-style="resizeWithOffset(0, 0)"></div>
     </div>
 </body>
 </html>
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 e5fc263..e189ef1 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -77,7 +77,9 @@
         oblique = false,        // whether we are in the oblique view
         nodeLock = false,       // whether nodes can be dragged or not (locked)
         width, height,          // the width and height of the force layout
-        hovered;                // the node over which the mouse is hovering
+        hovered,                // the node over which the mouse is hovering
+        selections = {},        // what is currently selected
+        selectOrder = [];       // the order in which we made selections
 
     // SVG elements;
     var linkG, linkLabelG, nodeG;
@@ -1323,15 +1325,77 @@
     }
 
 
+    function updateDetailPanel() {
+        // TODO update detail panel
+        $log.debug("TODO: updateDetailPanel() ...");
+    }
+
+
+    // ==========================
+    // === SELECTION / DESELECTION
+
+    function selectObject(obj) {
+        var el = this,
+            ev = d3.event.sourceEvent,
+            n;
+
+        if (zoomingOrPanning(ev)) {
+            return;
+        }
+
+        if (el) {
+            n = d3.select(el);
+        } else {
+            node.each(function (d) {
+                if (d == obj) {
+                    n = d3.select(el = this);
+                }
+            });
+        }
+        if (!n) return;
+
+        if (ev.shiftKey && n.classed('selected')) {
+            deselectObject(obj.id);
+            updateDetailPanel();
+            return;
+        }
+
+        if (!ev.shiftKey) {
+            deselectAll();
+        }
+
+        selections[obj.id] = { obj: obj, el: el };
+        selectOrder.push(obj.id);
+
+        n.classed('selected', true);
+        updateDeviceColors(obj);
+        updateDetailPanel();
+    }
+
+    function deselectObject(id) {
+        var obj = selections[id];
+        if (obj) {
+            d3.select(obj.el).classed('selected', false);
+            delete selections[id];
+            fs.removeFromArray(id, selectOrder);
+            updateDeviceColors(obj.obj);
+        }
+    }
+
+    function deselectAll() {
+        // deselect all nodes in the network...
+        node.classed('selected', false);
+        selections = {};
+        selectOrder = [];
+        updateDeviceColors();
+        updateDetailPanel();
+    }
+
     // ==========================
     // === MOUSE GESTURE HANDLERS
 
-    function selectCb(d) {
-        // this is the selected node
-        $log.debug("\n\n\nSelect Object: ");
-        $log.debug("d is ", d);
-        $log.debug("this is ", this);
-        $log.debug('\n\n');
+    function zoomingOrPanning(ev) {
+        return ev.metaKey || ev.altKey;
     }
 
     function atDragEnd(d) {
@@ -1345,8 +1409,7 @@
     function dragEnabled() {
         var ev = d3.event.sourceEvent;
         // nodeLock means we aren't allowing nodes to be dragged...
-        // meta or alt key pressed means we are zooming/panning...
-        return !nodeLock && !(ev.metaKey || ev.altKey);
+        return !nodeLock && !zoomingOrPanning(ev);
     }
 
     // predicate that indicates when clicking is active
@@ -1406,7 +1469,7 @@
                     .on('tick', tick);
 
                 drag = sus.createDragBehavior(force,
-                    selectCb, atDragEnd, dragEnabled, clickEnabled);
+                    selectObject, atDragEnd, dragEnabled, clickEnabled);
             }
 
             function resize(dim) {
diff --git a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
index 1727aee..27b6ba3 100644
--- a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
@@ -201,7 +201,8 @@
     it('should define api functions', function () {
         expect(fs.areFunctions(fs, [
             'isF', 'isA', 'isS', 'isO', 'contains',
-            'areFunctions', 'areFunctionsNonStrict', 'windowSize', 'find'
+            'areFunctions', 'areFunctionsNonStrict', 'windowSize', 'find',
+            'inArray', 'removeFromArray'
         ])).toBeTruthy();
     });
 
@@ -260,4 +261,68 @@
     it('should find Zevvv', function () {
         expect(fs.find('Zevvv', dataset, 'name')).toEqual(4);
     });
+
+
+    // === Tests for inArray()
+    var objRef = { x:1, y:2 },
+        array = [1, 3.14, 'hey', objRef, 'there', true],
+        array2 = ['b', 'a', 'd', 'a', 's', 's'];
+
+    it('should return -1 on non-arrays', function () {
+        expect(fs.inArray(1, {x:1})).toEqual(-1);
+    });
+    it('should not find HOO', function () {
+        expect(fs.inArray('HOO', array)).toEqual(-1);
+    });
+    it('should find 1', function () {
+        expect(fs.inArray(1, array)).toEqual(0);
+    });
+    it('should find pi', function () {
+        expect(fs.inArray(3.14, array)).toEqual(1);
+    });
+    it('should find hey', function () {
+        expect(fs.inArray('hey', array)).toEqual(2);
+    });
+    it('should find the object', function () {
+        expect(fs.inArray(objRef, array)).toEqual(3);
+    });
+    it('should find there', function () {
+        expect(fs.inArray('there', array)).toEqual(4);
+    });
+    it('should find true', function () {
+        expect(fs.inArray(true, array)).toEqual(5);
+    });
+
+    it('should find the first occurrence A', function () {
+        expect(fs.inArray('a', array2)).toEqual(1);
+    });
+    it('should find the first occurrence S', function () {
+        expect(fs.inArray('s', array2)).toEqual(4);
+    });
+    it('should not find X', function () {
+        expect(fs.inArray('x', array2)).toEqual(-1);
+    });
+
+    // === Tests for removeFromArray()
+    it('should ignore non-arrays', function () {
+        expect(fs.removeFromArray(1, {x:1})).toBe(false);
+    });
+    it('should keep the array the same, for non-match', function () {
+        var array = [1, 2, 3];
+        expect(fs.removeFromArray(4, array)).toBe(false);
+        expect(array).toEqual([1, 2, 3]);
+    });
+    it('should remove a value', function () {
+        var array = [1, 2, 3];
+        expect(fs.removeFromArray(2, array)).toBe(true);
+        expect(array).toEqual([1, 3]);
+    });
+    it('should remove the first occurrence', function () {
+        var array = ['x', 'y', 'z', 'z', 'y'];
+        expect(fs.removeFromArray('y', array)).toBe(true);
+        expect(array).toEqual(['x', 'z', 'z', 'y']);
+        expect(fs.removeFromArray('x', array)).toBe(true);
+        expect(array).toEqual(['z', 'z', 'y']);
+    });
+
 });