GUI -- augmented hash parsing to include flags (after '?'), which are passed into view callbacks as a boolean map.
 - moved event test files into sub directories
 - prepared topo2.js for scenario choice via hash context and 'local' (and 'debug') flag.
 - added 'simple' scenario: 2 switches, 1 link, and 2 hosts.
 - augmented topo event dispatch for yet-to-be-implemented event handlers.
 - implemented addHost() event handler.

Change-Id: I06b032684fd4d5f85262d13d58ad10edae23b3ed
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index acd2464..75fb99b 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -132,8 +132,21 @@
             links: [],
             lookup: {}
         },
+        scenario = {
+            evDir: 'json/ev/',
+            evScenario: '/scenario.json',
+            evPrefix: '/ev_',
+            evOnos: '_onos.json',
+            evUi: '_ui.json',
+            ctx: null,
+            params: {},
+            evNumber: 0,
+            view: null,
+            debug: false
+        },
         webSock,
-        labelIdx = 0,
+        deviceLabelIndex = 0,
+        hostLabelIndex = 0,
 
         selectOrder = [],
         selections = {},
@@ -155,10 +168,6 @@
     // ==============================
     // For Debugging / Development
 
-    var eventPrefix = 'json/eventTest_',
-        eventNumber = 0,
-        alertNumber = 0;
-
     function note(label, msg) {
         console.log('NOTE: ' + label + ': ' + msg);
     }
@@ -175,32 +184,71 @@
         view.alert('test');
     }
 
-    function injectTestEvent(view) {
+    function abortIfLive() {
         if (config.useLiveData) {
-            view.alert("Sorry, currently using live data..");
-            return;
+            scenario.view.alert("Sorry, currently using live data..");
+            return true;
         }
+        return false;
+    }
 
-        eventNumber++;
-        var eventUrl = eventPrefix + eventNumber + '.json';
+    function testDebug(msg) {
+        if (scenario.debug) {
+            scenario.view.alert(msg);
+        }
+    }
 
-        d3.json(eventUrl, function(err, data) {
+    function injectTestEvent(view) {
+        if (abortIfLive()) { return; }
+        var sc = scenario,
+            evn = ++sc.evNumber,
+            pfx = sc.evDir + sc.ctx + sc.evPrefix + evn,
+            onosUrl = pfx + sc.evOnos,
+            uiUrl = pfx + sc.evUi;
+
+        tryOnosEvent(onosUrl, uiUrl);
+    }
+
+    // TODO: tryOnosEvent/tryUiEvent folded into recursive function.
+    function tryOnosEvent(onosUrl, uiUrl) {
+        var v = scenario.view;
+        d3.json(onosUrl, function(err, data) {
             if (err) {
-                view.dataLoadError(err, eventUrl);
+                if (err.status === 404) {
+                    tryUiEvent(uiUrl);
+                } else {
+                    v.alert('non-404 error:\n\n' + onosUrl + '\n\n' + err);
+                }
             } else {
+                testDebug('loaded: ' + onosUrl);
                 handleServerEvent(data);
             }
         });
     }
 
-    function injectStartupEvents(view) {
-        if (config.useLiveData) {
-            view.alert("Sorry, currently using live data..");
-            return;
-        }
+    function tryUiEvent(uiUrl) {
+        var v = scenario.view;
+        d3.json(uiUrl, function(err, data) {
+            if (err) {
+                v.alert('Error:\n\n' + uiUrl + '\n\n' +
+                        err.status + ': ' + err.statusText);
+            } else {
+                testDebug('loaded: ' + uiUrl);
+                handleUiEvent(data);
+            }
+        });
+    }
 
-        var lastStartupEvent = 32;
-        while (eventNumber < lastStartupEvent) {
+    function handleUiEvent(data) {
+        testDebug('handleUiEvent(): ' + data.event);
+        // TODO:
+    }
+
+    function injectStartupEvents(view) {
+        var last = scenario.params.lastAuto || 0;
+        if (abortIfLive()) { return; }
+
+        while (scenario.evNumber < last) {
             injectTestEvent(view);
         }
     }
@@ -211,14 +259,16 @@
     }
 
     function cycleLabels() {
-        labelIdx = (labelIdx === network.deviceLabelCount - 1) ? 0 : labelIdx + 1;
+        deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1) ? 0 : deviceLabelIndex + 1;
 
         function niceLabel(label) {
             return (label && label.trim()) ? label : '.';
         }
 
         network.nodes.forEach(function (d) {
-            var idx = (labelIdx < d.labels.length) ? labelIdx : 0,
+            if (d.class !== 'device') { return; }
+
+            var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0,
                 node = d3.select('#' + safeId(d.id)),
                 box;
 
@@ -303,9 +353,14 @@
 
     var eventDispatch = {
         addDevice: addDevice,
-        updateDevice: updateDevice,
-        removeDevice: removeDevice,
+        updateDevice: stillToImplement,
+        removeDevice: stillToImplement,
         addLink: addLink,
+        updateLink: stillToImplement,
+        removeLink: stillToImplement,
+        addHost: addHost,
+        updateHost: stillToImplement,
+        removeHost: stillToImplement,
         showPath: showPath
     };
 
@@ -320,18 +375,6 @@
         network.force.start();
     }
 
-    function updateDevice(data) {
-        var device = data.payload;
-        note('updateDevice', device.id);
-
-    }
-
-    function removeDevice(data) {
-        var device = data.payload;
-        note('removeDevice', device.id);
-
-    }
-
     function addLink(data) {
         var link = data.payload,
             lnk = createLink(link);
@@ -345,11 +388,35 @@
         }
     }
 
+    function addHost(data) {
+        var host = data.payload,
+            node = createHostNode(host),
+            lnk;
+
+        note('addHost', node.id);
+        network.nodes.push(node);
+        network.lookup[host.id] = node;
+        updateNodes();
+
+        lnk = createHostLink(host);
+        if (lnk) {
+            network.links.push(lnk);
+            updateLinks();
+        }
+        network.force.start();
+    }
+
     function showPath(data) {
         network.view.alert(data.event + "\n" + data.payload.links.length);
     }
 
-    // ....
+    // ...............................
+
+    function stillToImplement(data) {
+        var p = data.payload;
+        note(data.event, p.id);
+        network.view.alert('Not yet implemented: "' + data.event + '"');
+    }
 
     function unknownEvent(data) {
         network.view.alert('Unknown event type: "' + data.event + '"');
@@ -367,6 +434,35 @@
         return 'translate(' + x + ',' + y + ')';
     }
 
+    function createHostLink(host) {
+        var src = host.id,
+            dst = host.cp.device,
+            srcNode = network.lookup[src],
+            dstNode = network.lookup[dst],
+            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);
+            return null;
+        }
+
+        lnk = {
+            id: safeId(src) + '~' + safeId(dst),
+            source: srcNode,
+            target: dstNode,
+            class: 'link',
+            svgClass: 'link hostLink',
+            x1: srcNode.x,
+            y1: srcNode.y,
+            x2: dstNode.x,
+            y2: dstNode.y,
+            width: 1
+        };
+        return lnk;
+    }
+
     function createLink(link) {
         var type = link.type,
             src = link.src,
@@ -451,6 +547,22 @@
         return node;
     }
 
+    function createHostNode(host) {
+        // start with the object as is
+        var node = host;
+
+        // Augment as needed...
+        node.class = 'host';
+        node.svgClass = 'node host';
+        // TODO: consider placing near its switch, if [x,y] not defined
+        positionNode(node);
+
+        // cache label array length
+        network.hostLabelCount = host.labels.length;
+
+        return node;
+    }
+
     function positionNode(node) {
         var meta = node.metaUi,
             x = 0,
@@ -525,7 +637,7 @@
         entering.filter('.device').each(function (d) {
             var node = d3.select(this),
                 icon = iconUrl(d),
-                idx = (labelIdx < d.labels.length) ? labelIdx : 0,
+                idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0,
                 box;
 
             node.append('rect')
@@ -568,6 +680,32 @@
             }
         });
 
+        // augment host nodes...
+        entering.filter('.host').each(function (d) {
+            var node = d3.select(this),
+                idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0,
+                box;
+
+            node.append('circle')
+                .attr('r', 8);     // TODO: define host circle radius
+
+            // TODO: are we attaching labels to hosts?
+            node.append('text')
+                .text(d.labels[idx])
+                .attr('dy', '1.1em');
+
+            // debug function to show the modelled x,y coordinates of nodes...
+            if (debug('showNodeXY')) {
+                node.select('circle').attr('fill-opacity', 0.5);
+                node.append('circle')
+                    .attr({
+                        class: 'debug',
+                        cx: 0,
+                        cy: 0,
+                        r: '3px'
+                    });
+            }
+        });
 
         // operate on both existing and new nodes, if necessary
         //node .foo() .bar() ...
@@ -643,7 +781,7 @@
             if (webSock.ws) {
                 webSock.ws.send(message);
             } else {
-                network.view.alert('no web socket open');
+                network.view.alert('no web socket open\n\n' + message);
             }
         }
 
@@ -714,7 +852,7 @@
         //flyinPane(null);
     }
 
-
+    // TODO: this click handler does not get unloaded when the view does
     $('#view').on('click', function(e) {
         if (!$(e.target).closest('.node').length) {
             if (!e.metaKey) {
@@ -723,6 +861,33 @@
         }
     });
 
+
+    function prepareScenario(view, ctx, dbg) {
+        var sc = scenario,
+            urlSc = sc.evDir + ctx + sc.evScenario;
+
+        if (!ctx) {
+            view.alert("No scenario specified (null ctx)");
+            return;
+        }
+
+        sc.view = view;
+        sc.ctx = ctx;
+        sc.debug = dbg;
+        sc.evNumber = 0;
+
+        d3.json(urlSc, function(err, data) {
+            var p = data && data.params || {};
+            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);
+            }
+        });
+
+    }
+
     // ==============================
     // View life-cycle callbacks
 
@@ -781,14 +946,11 @@
         }
 
         function atDragEnd(d, self) {
-            // once we've finished moving, pin the node in position,
-            // if it is a device (not a host)
-            if (d.class === 'device') {
-                d.fixed = true;
-                d3.select(self).classed('fixed', true);
-                if (config.useLiveData) {
-                    tellServerCoords(d);
-                }
+            // once we've finished moving, pin the node in position
+            d.fixed = true;
+            d3.select(self).classed('fixed', true);
+            if (config.useLiveData) {
+                tellServerCoords(d);
             }
         }
 
@@ -814,9 +976,14 @@
         network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
     }
 
-    function load(view, ctx) {
+    function load(view, ctx, flags) {
         // cache the view token, so network topo functions can access it
         network.view = view;
+        config.useLiveData = !flags.local;
+
+        if (!config.useLiveData) {
+            prepareScenario(view, ctx, flags.debug);
+        }
 
         // set our radio buttons and key bindings
         view.setRadio(btnSet);