GUI -- Implemented Show/Hide Offline devices & Show/Hide Hosts (also used Flash Service).
- added 'toggle(cb)' to panel API.
- deferred keybindings to allow direct reference to sub-API functions.
- re-implemented tick() function.
- added 'list scenarios' command to mockserver.

Change-Id: I1cc0009266e1015747b1d8106bd1f088adb2feb5
diff --git a/web/gui/src/main/webapp/app/fw/layer/panel.js b/web/gui/src/main/webapp/app/fw/layer/panel.js
index 4df0d72..a29c175 100644
--- a/web/gui/src/main/webapp/app/fw/layer/panel.js
+++ b/web/gui/src/main/webapp/app/fw/layer/panel.js
@@ -71,6 +71,7 @@
             api = {
                 show: showPanel,
                 hide: hidePanel,
+                toggle: togglePanel,
                 empty: emptyPanel,
                 append: appendPanel,
                 width: panelWidth,
@@ -111,6 +112,14 @@
                 .style('opacity', 0);
         }
 
+        function togglePanel(cb) {
+            if (p.on) {
+                hidePanel(cb);
+            } else {
+                showPanel(cb);
+            }
+        }
+
         function emptyPanel() {
             return p.el.html('');
         }
diff --git a/web/gui/src/main/webapp/app/fw/svg/svgUtil.js b/web/gui/src/main/webapp/app/fw/svg/svgUtil.js
index 3a35e9f..e99da3c 100644
--- a/web/gui/src/main/webapp/app/fw/svg/svgUtil.js
+++ b/web/gui/src/main/webapp/app/fw/svg/svgUtil.js
@@ -141,7 +141,7 @@
                 lightMute = ['#A8B8CC', '#CCB3A8', '#FFC2BD', '#96D6BF', '#D19FCE', '#8FCCCA', '#CAEAA4'],
 
                 darkNorm  = ['#304860', '#664631', '#A8391B', '#00754B', '#77206D', '#005959', '#428700'],
-                darkMute  = ['#16203A', '#281810', '#4F1206', '#00331C', '#3D063A', '#002D2D', '#1B4400'];
+                darkMute  = ['#304860', '#664631', '#A8391B', '#00754B', '#77206D', '#005959', '#428700'];
 
             var colors= {
                 light: {
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 4a2ae8a..f7568f5 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -36,75 +36,61 @@
     // Internal state
     var zoomer;
 
-    // Note: "exported" state should be properties on 'self' variable
-
     // --- Short Cut Keys ------------------------------------------------
 
-    var keyBindings = {
-        //O: [toggleSummary, 'Toggle ONOS summary pane'],
-        I: [toggleInstances, 'Toggle ONOS instances pane'],
-        //D: [toggleDetails, 'Disable / enable details pane'],
+    function setUpKeys() {
+        // key bindings need to be made after the services have been injected
+        // thus, deferred to here...
+        ks.keyBindings({
+            //O: [toggleSummary, 'Toggle ONOS summary pane'],
+            I: [toggleInstances, 'Toggle ONOS instances pane'],
+            //D: [toggleDetails, 'Disable / enable details pane'],
 
-        //H: [toggleHosts, 'Toggle host visibility'],
-        //M: [toggleOffline, 'Toggle offline visibility'],
-        //B: [toggleBg, 'Toggle background image'],
-        //P: togglePorts,
+            H: [tfs.toggleHosts, 'Toggle host visibility'],
+            M: [tfs.toggleOffline, 'Toggle offline visibility'],
+            //B: [toggleBg, 'Toggle background image'],
+            //P: togglePorts,
 
-        //X: [toggleNodeLock, 'Lock / unlock node positions'],
-        //Z: [toggleOblique, 'Toggle oblique view (Experimental)'],
-        L: [cycleLabels, 'Cycle device labels'],
-        //U: [unpin, 'Unpin node (hover mouse over)'],
-        R: [resetZoom, 'Reset pan / zoom'],
+            //X: [toggleNodeLock, 'Lock / unlock node positions'],
+            //Z: [toggleOblique, 'Toggle oblique view (Experimental)'],
+            L: [tfs.cycleDeviceLabels, 'Cycle device labels'],
+            //U: [unpin, 'Unpin node (hover mouse over)'],
+            R: [resetZoom, 'Reset pan / zoom'],
 
-        //V: [showRelatedIntentsAction, 'Show all related intents'],
-        //rightArrow: [showNextIntentAction, 'Show next related intent'],
-        //leftArrow: [showPrevIntentAction, 'Show previous related intent'],
-        //W: [showSelectedIntentTrafficAction, 'Monitor traffic of selected intent'],
-        //A: [showAllTrafficAction, 'Monitor all traffic'],
-        //F: [showDeviceLinkFlowsAction, 'Show device link flows'],
+            //V: [showRelatedIntentsAction, 'Show all related intents'],
+            //rightArrow: [showNextIntentAction, 'Show next related intent'],
+            //leftArrow: [showPrevIntentAction, 'Show previous related intent'],
+            //W: [showSelectedIntentTrafficAction, 'Monitor traffic of selected intent'],
+            //A: [showAllTrafficAction, 'Monitor all traffic'],
+            //F: [showDeviceLinkFlowsAction, 'Show device link flows'],
 
-        //E: [equalizeMasters, 'Equalize mastership roles'],
+            //E: [equalizeMasters, 'Equalize mastership roles'],
 
-        //esc: handleEscape,
+            //esc: handleEscape,
 
-        _helpFormat: [
-            ['O', 'I', 'D', '-', 'H', 'M', 'B', 'P' ],
-            ['X', 'Z', 'L', 'U', 'R' ],
-            ['V', 'rightArrow', 'leftArrow', 'W', 'A', 'F', '-', 'E' ]
-        ]
+            _helpFormat: [
+                ['O', 'I', 'D', '-', 'H', 'M', 'B', 'P' ],
+                ['X', 'Z', 'L', 'U', 'R' ],
+                ['V', 'rightArrow', 'leftArrow', 'W', 'A', 'F', '-', 'E' ]
+            ]
+        });
 
-    };
+        // TODO:         // mouse gestures
+        var gestures = [
+            ['click', 'Select the item and show details'],
+            ['shift-click', 'Toggle selection state'],
+            ['drag', 'Reposition (and pin) device / host'],
+            ['cmd-scroll', 'Zoom in / out'],
+            ['cmd-drag', 'Pan']
+        ];
+    }
 
-    // mouse gestures
-    var gestures = [
-        ['click', 'Select the item and show details'],
-        ['shift-click', 'Toggle selection state'],
-        ['drag', 'Reposition (and pin) device / host'],
-        ['cmd-scroll', 'Zoom in / out'],
-        ['cmd-drag', 'Pan']
-    ];
 
     function toggleInstances() {
-        if (tis.isVisible()) {
-            tis.hide();
-        } else {
-            tis.show();
-        }
+        tis.toggle();
         tfs.updateDeviceColors();
     }
 
-    function cycleLabels() {
-        $log.debug('Cycle Labels.....');
-    }
-
-    function resetZoom() {
-        zoomer.reset();
-    }
-
-    function setUpKeys() {
-        ks.keyBindings(keyBindings);
-    }
-
 
     // --- Glyphs, Icons, and the like -----------------------------------
 
@@ -122,8 +108,7 @@
     }
 
     function zoomCallback() {
-        var tr = zoomer.translate(),
-            sc = zoomer.scale();
+        var sc = zoomer.scale();
 
         // keep the map lines constant width while zooming
         mapG.style('stroke-width', (2.0 / sc) + 'px');
@@ -139,6 +124,10 @@
         });
     }
 
+    function resetZoom() {
+        zoomer.reset();
+    }
+
 
     // callback invoked when the SVG view has been resized..
     function svgResized(dim) {
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 01b719e..b3c8eaa 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -23,9 +23,7 @@
     'use strict';
 
     // injected refs
-    var $log, fs, sus, is, ts, tis, uplink;
-
-    var icfg;
+    var $log, fs, sus, is, ts, flash, tis, icfg, uplink;
 
     // configuration
     var labelConfig = {
@@ -53,9 +51,9 @@
             outColor: '#f00',
         },
         dark: {
-            baseColor: '#666',
+            baseColor: '#aaa',
             inColor: '#66f',
-            outColor: '#f00',
+            outColor: '#f66'
         },
         inWidth: 12,
         outWidth: 10
@@ -74,7 +72,9 @@
         lu = network.lookup,    // shorthand
         deviceLabelIndex = 0,   // for device label cycling
         hostLabelIndex = 0,     // for host label cycling
-        showHosts = 1,          // whether hosts are displayed
+        showHosts = true,       // whether hosts are displayed
+        showOffline = true,     // whether offline devices are displayed
+        oblique = false,        // whether we are in the oblique view
         width, height;
 
     // SVG elements;
@@ -150,9 +150,8 @@
             }
             updateNodes();
             if (wasOnline !== d.online) {
-                // TODO: re-instate link update, and offline visibility
-                //findAttachedLinks(d.id).forEach(restyleLinkElement);
-                //updateOfflineVisibility(d);
+                findAttachedLinks(d.id).forEach(restyleLinkElement);
+                updateOfflineVisibility(d);
             }
         } else {
             // TODO: decide whether we want to capture logic errors
@@ -338,9 +337,8 @@
         linkScale = d3.scale.linear()
             .domain([1, 12])
             .range([widthRatio, 12 * widthRatio])
-            .clamp(true);
-
-    var allLinkTypes = 'direct indirect optical tunnel',
+            .clamp(true),
+        allLinkTypes = 'direct indirect optical tunnel',
         defaultLinkType = 'direct';
 
     function restyleLinkElement(ldata) {
@@ -364,6 +362,12 @@
             .attr('stroke', linkConfig[th].baseColor);
     }
 
+    function findLinkById(id) {
+        // check to see if this is a reverse lookup, else default to given id
+        var key = network.revLinkToKey[id] || id;
+        return key && lu[key];
+    }
+
     function findLink(linkData, op) {
         var key = makeLinkKey(linkData),
             keyrev = makeLinkKey(linkData, 1),
@@ -436,6 +440,15 @@
         return result;
     }
 
+    function findOfflineNodes() {
+        var a = [];
+        network.nodes.forEach(function (d) {
+            if (d.class === 'device' && !d.online) {
+                a.push(d);
+            }
+        });
+        return a;
+    }
 
     function findAttachedHosts(devId) {
         var hosts = [];
@@ -513,6 +526,36 @@
         fResume();
     }
 
+    function updateHostVisibility() {
+        sus.makeVisible(nodeG.selectAll('.host'), showHosts);
+        sus.makeVisible(linkG.selectAll('.hostLink'), showHosts);
+    }
+
+    function updateOfflineVisibility(dev) {
+        function updDev(d, show) {
+            sus.makeVisible(d.el, show);
+
+            findAttachedLinks(d.id).forEach(function (link) {
+                b = show && ((link.type() !== 'hostLink') || showHosts);
+                sus.makeVisible(link.el, b);
+            });
+            findAttachedHosts(d.id).forEach(function (host) {
+                b = show && showHosts;
+                sus.makeVisible(host.el, b);
+            });
+        }
+
+        if (dev) {
+            // updating a specific device that just toggled off/on-line
+            updDev(dev, dev.online || showOffline);
+        } else {
+            // updating all offline devices
+            findOfflineNodes().forEach(function (d) {
+                updDev(d, showOffline);
+            });
+        }
+    }
+
 
     function sendUpdateMeta(d, store) {
         var metaUi = {},
@@ -536,16 +579,6 @@
     }
 
 
-    function fStart() {
-        $log.debug('TODO fStart()...');
-        // TODO...
-    }
-
-    function fResume() {
-        $log.debug('TODO fResume()...');
-        // TODO...
-    }
-
     // ==========================
     // === Devices and hosts - helper functions
 
@@ -817,6 +850,28 @@
         }
     }
 
+    function vis(b) {
+        return b ? 'visible' : 'hidden';
+    }
+
+    function toggleHosts() {
+        showHosts = !showHosts;
+        updateHostVisibility();
+        flash.flash('Hosts ' + vis(showHosts));
+    }
+
+    function toggleOffline() {
+        showOffline = !showOffline;
+        updateOfflineVisibility();
+        flash.flash('Offline devices ' + vis(showOffline));
+    }
+
+    function cycleDeviceLabels() {
+        // TODO cycle device labels
+    }
+
+    // ==========================================
+
     var dCol = {
         black: '#000',
         paleblue: '#acf',
@@ -1070,12 +1125,12 @@
         // operate on exiting links:
         link.exit()
             .attr('stroke-dasharray', '3 3')
+            .attr('stroke', linkConfig[th].outColor)
             .style('opacity', 0.5)
             .transition()
             .duration(1500)
             .attr({
                 'stroke-dasharray': '3 12',
-                stroke: linkConfig[th].outColor,
                 'stroke-width': linkConfig.outWidth
             })
             .style('opacity', 0.0)
@@ -1084,7 +1139,7 @@
         // NOTE: invoke a single tick to force the labels to position
         //        onto their links.
         tick();
-        // FIXME: this is a bug when in oblique view
+        // TODO: this causes undesirable behavior when in oblique view
         // It causes the nodes to jump into "overhead" view positions, even
         //  though the oblique planes are still showing...
     }
@@ -1191,14 +1246,55 @@
 
     // ==========================
     // force layout tick function
-    function tick() {
 
+    function fResume() {
+        if (!oblique) {
+            force.resume();
+        }
+    }
+
+    function fStart() {
+        if (!oblique) {
+            force.start();
+        }
+    }
+
+    var tickStuff = {
+        nodeAttr: {
+            transform: function (d) { return sus.translate(d.x, d.y); }
+        },
+        linkAttr: {
+            x1: function (d) { return d.source.x; },
+            y1: function (d) { return d.source.y; },
+            x2: function (d) { return d.target.x; },
+            y2: function (d) { return d.target.y; }
+        },
+        linkLabelAttr: {
+            transform: function (d) {
+                var lnk = findLinkById(d.key);
+                if (lnk) {
+                    return transformLabel({
+                        x1: lnk.source.x,
+                        y1: lnk.source.y,
+                        x2: lnk.target.x,
+                        y2: lnk.target.y
+                    });
+                }
+            }
+        }
+    };
+
+    function tick() {
+        node.attr(tickStuff.nodeAttr);
+        link.attr(tickStuff.linkAttr);
+        linkLabel.attr(tickStuff.linkLabelAttr);
     }
 
 
     // ==========================
     // === MOUSE GESTURE HANDLERS
 
+    // FIXME:
     function selectCb() { }
     function atDragEnd() {}
     function dragEnabled() {}
@@ -1211,14 +1307,15 @@
     angular.module('ovTopo')
     .factory('TopoForceService',
         ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
-            'TopoInstService',
+            'FlashService', 'TopoInstService',
 
-        function (_$log_, _fs_, _sus_, _is_, _ts_, _tis_) {
+        function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_) {
             $log = _$log_;
             fs = _fs_;
             sus = _sus_;
             is = _is_;
             ts = _ts_;
+            flash = _flash_;
             tis = _tis_;
 
             icfg = is.iconConfig();
@@ -1270,6 +1367,9 @@
                 resize: resize,
 
                 updateDeviceColors: updateDeviceColors,
+                toggleHosts: toggleHosts,
+                toggleOffline: toggleOffline,
+                cycleDeviceLabels: cycleDeviceLabels,
 
                 addDevice: addDevice,
                 updateDevice: updateDevice,
diff --git a/web/gui/src/main/webapp/app/view/topo/topoInst.js b/web/gui/src/main/webapp/app/view/topo/topoInst.js
index ee5c96c..49e129c 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoInst.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoInst.js
@@ -55,7 +55,6 @@
 
 
     // ==========================
-    // *** ADD INSTANCE ***
 
     function addInstance(data) {
         var id = data.id;
@@ -330,7 +329,8 @@
 
                 isVisible: function () { return oiBox.isVisible(); },
                 show: function () { oiBox.show(); },
-                hide: function () { oiBox.hide(); }
+                hide: function () { oiBox.hide(); },
+                toggle: function () { oiBox.toggle(); }
             };
         }]);
 }());
diff --git a/web/gui/src/main/webapp/tests/app/fw/layer/panel-spec.js b/web/gui/src/main/webapp/tests/app/fw/layer/panel-spec.js
index 64c54f1..c6c63c3 100644
--- a/web/gui/src/main/webapp/tests/app/fw/layer/panel-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/layer/panel-spec.js
@@ -87,7 +87,8 @@
     it('should provide an api of panel functions', function () {
         var p = ps.createPanel('foo');
         expect(fs.areFunctions(p, [
-            'show', 'hide', 'empty', 'append', 'width', 'height', 'isVisible', 'el'
+            'show', 'hide', 'toggle', 'empty', 'append',
+            'width', 'height', 'isVisible', 'el'
         ])).toBeTruthy();
     });
 
diff --git a/web/gui/src/main/webapp/tests/app/fw/svg/svgUtil-spec.js b/web/gui/src/main/webapp/tests/app/fw/svg/svgUtil-spec.js
index e1a2107..69fe750 100644
--- a/web/gui/src/main/webapp/tests/app/fw/svg/svgUtil-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/svg/svgUtil-spec.js
@@ -76,7 +76,7 @@
     });
 
     it('should provide an alternate (dark) shade of blue for muted', function () {
-        expect(sus.cat7().getColor('foo', true, 'dark')).toEqual('#16203A');
+        expect(sus.cat7().getColor('foo', true, 'dark')).toEqual('#304860');
     });
 
     it('should iterate across the colors', function () {
diff --git a/web/gui/src/main/webapp/tests/app/view/topo/topoForce-spec.js b/web/gui/src/main/webapp/tests/app/view/topo/topoForce-spec.js
index b85272d..7c65d8c 100644
--- a/web/gui/src/main/webapp/tests/app/view/topo/topoForce-spec.js
+++ b/web/gui/src/main/webapp/tests/app/view/topo/topoForce-spec.js
@@ -35,6 +35,7 @@
     it('should define api functions', function () {
         expect(fs.areFunctions(tfs, [
             'initForce', 'resize', 'updateDeviceColors',
+            'toggleHosts', 'toggleOffline','cycleDeviceLabels',
             'addDevice', 'updateDevice', 'removeDevice',
             'addHost', 'updateHost', 'removeHost',
             'addLink', 'updateLink', 'removeLink'
diff --git a/web/gui/src/main/webapp/tests/app/view/topo/topoInst-spec.js b/web/gui/src/main/webapp/tests/app/view/topo/topoInst-spec.js
index f54d839..8eefc4a 100644
--- a/web/gui/src/main/webapp/tests/app/view/topo/topoInst-spec.js
+++ b/web/gui/src/main/webapp/tests/app/view/topo/topoInst-spec.js
@@ -36,7 +36,7 @@
         expect(fs.areFunctions(tis, [
             'initInst', 'destroyInst',
             'addInstance', 'updateInstance', 'removeInstance',
-            'isVisible', 'show', 'hide'
+            'isVisible', 'show', 'hide', 'toggle'
         ])).toBeTruthy();
     });
 
diff --git a/web/gui/src/test/_karma/ev/simple/ev_8_addHost_03.json b/web/gui/src/test/_karma/ev/simple/ev_8_addHost_03.json
index 35e4572..fba7015 100644
--- a/web/gui/src/test/_karma/ev/simple/ev_8_addHost_03.json
+++ b/web/gui/src/test/_karma/ev/simple/ev_8_addHost_03.json
@@ -13,8 +13,8 @@
       "0E:2A:69:30:13:86"
     ],
     "metaUi": {
-      "x": 800,
-      "y": 180
+      "Xx": 800,
+      "Xy": 180
     },
     "props": {}
   }
diff --git a/web/gui/src/test/_karma/ev/simple/ev_9_addHost_08.json b/web/gui/src/test/_karma/ev/simple/ev_9_addHost_08.json
index 3d368c8..eff767d 100644
--- a/web/gui/src/test/_karma/ev/simple/ev_9_addHost_08.json
+++ b/web/gui/src/test/_karma/ev/simple/ev_9_addHost_08.json
@@ -13,8 +13,8 @@
       "A6:96:E5:03:52:5F"
     ],
     "metaUi": {
-      "x": 520,
-      "y": 250
+      "Xx": 520,
+      "Xy": 250
     },
     "props": {}
   }
diff --git a/web/gui/src/test/_karma/mockserver.js b/web/gui/src/test/_karma/mockserver.js
index 454d686..8ddcb9c 100644
--- a/web/gui/src/test/_karma/mockserver.js
+++ b/web/gui/src/test/_karma/mockserver.js
@@ -25,10 +25,7 @@
 var scFiles = fs.readdirSync(scenarioRoot);
 console.log('Mock Server v1.0');
 console.log('================');
-console.log('Scenarios ...');
-console.log(scFiles.join(', '));
-console.log();
-
+listScenarios();
 
 var rl = readline.createInterface(process.stdin, process.stdout);
 rl.setPrompt('ws> ');
@@ -118,6 +115,7 @@
         }
 
         switch(cmd) {
+            case 'l': listScenarios(); break;
             case 'c': connStatus(); break;
             case 'm': customMessage(str); break;
             case 's': setScenario(str); break;
@@ -137,10 +135,11 @@
 }
 
 var helptext = '\n' +
+        'l        - list scenarios\n' +
         'c        - show connection status\n' +
         'm {text} - send custom message to client\n' +
         's {id}   - load scenario {id}\n' +
-        's        - show scenario staus\n' +
+        's        - show scenario status\n' +
         //'a        - auto-send events\n' +
         'n        - send next event\n' +
         'r        - restart the scenario\n' +
@@ -151,6 +150,12 @@
     console.log(helptext);
 }
 
+function listScenarios() {
+    console.log('Scenarios ...');
+    console.log(scFiles.join(', '));
+    console.log();
+}
+
 function connStatus() {
     if (connection) {
         console.log('Connection from ' + origin + ' established.');