GUI -- Added updateDevice event handling.
 - Display offline devices as grey.
 - Tracing web socket messages (for now, in console; in future, to trace view).
 - Captured sample events for use with test scenarios - both from and to the server.
 - Added description to scenario file.

Change-Id: I7825b32d63496ebea2ab5789519fb0c6af6c5257
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 55e463c..f6a8456 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -24,7 +24,8 @@
     'use strict';
 
     // shorter names for library APIs
-    var d3u = onos.lib.d3util;
+    var d3u = onos.lib.d3util,
+        trace;
 
     // configuration data
     var config = {
@@ -241,8 +242,8 @@
     }
 
     function handleUiEvent(data) {
-        testDebug('handleUiEvent(): ' + data.event);
-        // TODO:
+        scenario.view.alert('UI Tx: ' + data.event + '\n\n' +
+            JSON.stringify(data));
     }
 
     function injectStartupEvents(view) {
@@ -259,32 +260,44 @@
         bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
     }
 
+    function updateDeviceLabel(d) {
+        var label = niceLabel(deviceLabel(d)),
+            node = d.el,
+            box;
+
+        node.select('text')
+            .text(label)
+            .style('opacity', 0)
+            .transition()
+            .style('opacity', 1);
+
+        box = adjustRectToFitText(node);
+
+        node.select('rect')
+            .transition()
+            .attr(box);
+
+        node.select('image')
+            .transition()
+            .attr('x', box.x + config.icons.xoff)
+            .attr('y', box.y + config.icons.yoff);
+    }
+
+    function updateHostLabel(d) {
+        var label = hostLabel(d),
+            host = d.el;
+
+        host.select('text').text(label);
+    }
+
     function cycleLabels() {
-        deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1) ? 0 : deviceLabelIndex + 1;
+        deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1)
+            ? 0 : deviceLabelIndex + 1;
 
         network.nodes.forEach(function (d) {
-            if (d.class !== 'device') { return; }
-
-            var label = niceLabel(deviceLabel(d)),
-                node = d.el,
-                box;
-
-            node.select('text')
-                .text(label)
-                .style('opacity', 0)
-                .transition()
-                .style('opacity', 1);
-
-            box = adjustRectToFitText(node);
-
-            node.select('rect')
-                .transition()
-                .attr(box);
-
-            node.select('image')
-                .transition()
-                .attr('x', box.x + config.icons.xoff)
-                .attr('y', box.y + config.icons.yoff);
+            if (d.class === 'device') {
+                updateDeviceLabel(d);
+            }
         });
     }
 
@@ -348,15 +361,20 @@
     // ==============================
     // Event handlers for server-pushed events
 
+    function logicError(msg) {
+        // TODO, report logic error to server, via websock, so it can be logged
+        network.view.alert('Logic Error:\n\n' + msg);
+    }
+
     var eventDispatch = {
         addDevice: addDevice,
-        updateDevice: stillToImplement,
-        removeDevice: stillToImplement,
         addLink: addLink,
-        updateLink: stillToImplement,
-        removeLink: stillToImplement,
         addHost: addHost,
+        updateDevice: updateDevice,
+        updateLink: stillToImplement,
         updateHost: updateHost,
+        removeDevice: stillToImplement,
+        removeLink: stillToImplement,
         removeHost: stillToImplement,
         showPath: showPath
     };
@@ -364,8 +382,6 @@
     function addDevice(data) {
         var device = data.payload,
             nodeData = createDeviceNode(device);
-        note('addDevice', device.id);
-
         network.nodes.push(nodeData);
         network.lookup[nodeData.id] = nodeData;
         updateNodes();
@@ -375,10 +391,7 @@
     function addLink(data) {
         var link = data.payload,
             lnk = createLink(link);
-
         if (lnk) {
-            note('addLink', link.id);
-
             network.links.push(lnk);
             network.lookup[lnk.id] = lnk;
             updateLinks();
@@ -390,8 +403,6 @@
         var host = data.payload,
             node = createHostNode(host),
             lnk;
-        note('addHost', node.id);
-
         network.nodes.push(node);
         network.lookup[host.id] = node;
         updateNodes();
@@ -406,13 +417,28 @@
         network.force.start();
     }
 
+    function updateDevice(data) {
+        var device = data.payload,
+            id = device.id,
+            nodeData = network.lookup[id];
+        if (nodeData) {
+            $.extend(nodeData, device);
+            updateDeviceState(nodeData);
+        } else {
+            logicError('updateDevice lookup fail. ID = "' + id + '"');
+        }
+    }
+
     function updateHost(data) {
         var host = data.payload,
-            hostData = network.lookup[host.id];
-        note('updateHost', host.id);
-
-        $.extend(hostData, host);
-        updateNodes();
+            id = host.id,
+            hostData = network.lookup[id];
+        if (hostData) {
+            $.extend(hostData, host);
+            updateHostState(hostData);
+        } else {
+            logicError('updateHost lookup fail. ID = "' + id + '"');
+        }
     }
 
     function showPath(data) {
@@ -466,9 +492,8 @@
             lnk;
 
         if (!dstNode) {
-            // TODO: send warning message back to server on websocket
-            network.view.alert('switch not on map for link\n\n' +
-            'src = ' + src + '\ndst = ' + dst);
+            logicError('switch not on map for link\n\n' +
+                        'src = ' + src + '\ndst = ' + dst);
             return null;
         }
 
@@ -500,9 +525,8 @@
             dstNode = network.lookup[dst];
 
         if (!(srcNode && dstNode)) {
-            // TODO: send warning message back to server on websocket
-            network.view.alert('nodes not on map for link\n\n' +
-                'src = ' + src + '\ndst = ' + dst);
+            logicError('nodes not on map for link\n\n' +
+            'src = ' + src + '\ndst = ' + dst);
             return null;
         }
 
@@ -578,11 +602,12 @@
     function createDeviceNode(device) {
         // start with the object as is
         var node = device,
-            type = device.type;
+            type = device.type,
+            svgCls = type ? 'node device ' + type : 'node device';
 
         // Augment as needed...
         node.class = 'device';
-        node.svgClass = type ? 'node device ' + type : 'node device';
+        node.svgClass = device.online ? svgCls + ' online' : svgCls;
         positionNode(node);
 
         // cache label array length
@@ -669,15 +694,24 @@
         return (label && label.trim()) ? label : '.';
     }
 
+    function updateDeviceState(nodeData) {
+        nodeData.el.classed('online', nodeData.online);
+        updateDeviceLabel(nodeData);
+        // TODO: review what else might need to be updated
+    }
+
+    function updateHostState(hostData) {
+        updateHostLabel(hostData);
+        // TODO: review what else might need to be updated
+    }
+
+
     function updateNodes() {
         node = nodeG.selectAll('.node')
             .data(network.nodes, function (d) { return d.id; });
 
         // operate on existing nodes, if necessary
         //  update host labels
-        node.filter('.host').select('text')
-            .text(hostLabel);
-
         //node .foo() .bar() ...
 
         // operate on entering nodes:
@@ -828,7 +862,7 @@
 
             webSock.ws.onmessage = function(m) {
                 if (m.data) {
-                    console.log(m.data);
+                    wsTraceRx(m.data);
                     handleServerEvent(JSON.parse(m.data));
                 }
             };
@@ -858,11 +892,28 @@
 
     function sendMessage(evType, payload) {
         var toSend = {
-            event: evType,
-            sid: ++sid,
-            payload: payload
-        };
-        webSock.send(JSON.stringify(toSend));
+                event: evType,
+                sid: ++sid,
+                payload: payload
+            },
+            asText = JSON.stringify(toSend);
+        wsTraceTx(asText);
+        webSock.send(asText);
+    }
+
+    function wsTraceTx(msg) {
+        wsTrace('tx', msg);
+    }
+    function wsTraceRx(msg) {
+        wsTrace('rx', msg);
+    }
+    function wsTrace(rxtx, msg) {
+
+        console.log('[' + rxtx + '] ' + msg);
+        // TODO: integrate with trace view
+        //if (trace) {
+        //    trace.output(rxtx, msg);
+        //}
     }
 
 
@@ -944,12 +995,19 @@
         sc.evNumber = 0;
 
         d3.json(urlSc, function(err, data) {
-            var p = data && data.params || {};
+            var p = data && data.params || {},
+                desc = data && data.description || null,
+                intro;
+
             if (err) {
                 view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
             } else {
                 sc.params = p;
-                view.alert("Scenario loaded: " + ctx + '\n\n' + data.title);
+                intro = "Scenario loaded: " + ctx + '\n\n' + data.title;
+                if (desc) {
+                    intro += '\n\n  ' + desc.join('\n  ');
+                }
+                view.alert(intro);
             }
         });
 
@@ -967,6 +1025,9 @@
             fpad = fcfg.pad,
             forceDim = [w - 2*fpad, h - 2*fpad];
 
+        // TODO: set trace api
+        //trace = onos.exported.webSockTrace;
+
         // NOTE: view.$div is a D3 selection of the view's div
         svg = view.$div.append('svg');
         setSize(svg, view);