Adding multi-selection to the GUI and sketching out GUI/Server interactions.
Added persistent meta-data; including node coordinates.
Added ability to request path and return one.

Change-Id: I3edbdf44bbb8d8133a5e5a1fd0660a3fa5a2d6a1
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
index e82303b..31c3f84 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
@@ -23,7 +23,9 @@
 import org.onlab.onos.event.Event;
 import org.onlab.onos.net.Annotations;
 import org.onlab.onos.net.Device;
+import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.Link;
+import org.onlab.onos.net.Path;
 import org.onlab.onos.net.device.DeviceEvent;
 import org.onlab.onos.net.device.DeviceService;
 import org.onlab.onos.net.link.LinkEvent;
@@ -37,7 +39,11 @@
 import org.onlab.osgi.ServiceDirectory;
 
 import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
 
+import static org.onlab.onos.net.DeviceId.deviceId;
 import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_ADDED;
 import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_REMOVED;
 import static org.onlab.onos.net.link.LinkEvent.Type.LINK_ADDED;
@@ -56,6 +62,12 @@
 
     private Connection connection;
 
+    // TODO: extract into an external & durable state; good enough for now and demo
+    private static Map<String, ObjectNode> metaUi = new HashMap<>();
+
+    private static final String COMPACT = "%s/%s-%s/%s";
+
+
     /**
      * Creates a new web-socket for serving data to GUI topology view.
      *
@@ -101,9 +113,56 @@
 
     @Override
     public void onMessage(String data) {
-        System.out.println("Received: " + data);
+        try {
+            ObjectNode event = (ObjectNode) mapper.reader().readTree(data);
+            String type = event.path("event").asText("unknown");
+            ObjectNode payload = (ObjectNode) event.path("payload");
+
+            switch (type) {
+                case "updateMeta":
+                    metaUi.put(payload.path("id").asText(), payload);
+                    break;
+                case "requestPath":
+                    findPath(deviceId(payload.path("one").asText()),
+                             deviceId(payload.path("two").asText()));
+                default:
+                    break;
+            }
+        } catch (IOException e) {
+            System.out.println("Received: " + data);
+        }
     }
 
+    private void findPath(DeviceId one, DeviceId two) {
+        Set<Path> paths = topologyService.getPaths(topologyService.currentTopology(),
+                                                   one, two);
+        if (!paths.isEmpty()) {
+            ObjectNode payload = mapper.createObjectNode();
+            ArrayNode links = mapper.createArrayNode();
+
+            Path path = paths.iterator().next();
+            for (Link link : path.links()) {
+                links.add(compactLinkString(link));
+            }
+
+            payload.set("links", links);
+            sendMessage(envelope("showPath", payload));
+        }
+        // TODO: when no path, send a message to the client
+    }
+
+    /**
+     * Returns a compact string representing the given link.
+     *
+     * @param link infrastructure link
+     * @return formatted link string
+     */
+    public static String compactLinkString(Link link) {
+        return String.format(COMPACT, link.src().deviceId(), link.src().port(),
+                             link.dst().deviceId(), link.dst().port());
+    }
+
+
     private void sendMessage(String data) {
         try {
             connection.sendMessage(data);
@@ -130,7 +189,11 @@
         // Add labels, props and stuff the payload into envelope.
         payload.set("labels", labels);
         payload.set("props", props(device.annotations()));
-        payload.set("metaUi", mapper.createObjectNode());
+
+        ObjectNode meta = metaUi.get(device.id().toString());
+        if (meta != null) {
+            payload.set("metaUi", meta);
+        }
 
         String type = (event.type() == DEVICE_ADDED) ? "addDevice" :
                 ((event.type() == DEVICE_REMOVED) ? "removeDevice" : "updateDevice");
diff --git a/web/gui/src/main/webapp/json/intent/ev_1_ui.json b/web/gui/src/main/webapp/json/intent/ev_1_ui.json
new file mode 100644
index 0000000..962fcaa
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_1_ui.json
@@ -0,0 +1,8 @@
+{
+  "event": "addHostIntent",
+  "sid": 1,
+  "payload": {
+    "one": "hostOne",
+    "two": "hostTwo"
+  }
+}
diff --git a/web/gui/src/main/webapp/json/intent/ev_2_onos.json b/web/gui/src/main/webapp/json/intent/ev_2_onos.json
new file mode 100644
index 0000000..2b3bbe5
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_2_onos.json
@@ -0,0 +1,11 @@
+{
+  "event": "showPath",
+  "sid": 1,
+  "payload": {
+    "intentId": "0x1234",
+    "path": {
+      "links": [ "1-2", "2-3" ],
+      "traffic": false
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/intent/ev_3_ui.json b/web/gui/src/main/webapp/json/intent/ev_3_ui.json
new file mode 100644
index 0000000..3151c8d
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_3_ui.json
@@ -0,0 +1,7 @@
+{
+  "event": "monitorIntent",
+  "sid": 2,
+  "payload": {
+    "intentId": "0x1234"
+  }
+}
diff --git a/web/gui/src/main/webapp/json/intent/ev_4_onos.json b/web/gui/src/main/webapp/json/intent/ev_4_onos.json
new file mode 100644
index 0000000..1a6632e
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_4_onos.json
@@ -0,0 +1,13 @@
+{
+  "event": "showPath",
+  "sid": 2,
+  "payload": {
+    "intentId": "0x1234",
+    "path": {
+      "links": [ "1-2", "2-3" ],
+      "traffic": true,
+      "srcLabel": "567 Mb",
+      "dstLabel": "6 Mb"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/intent/ev_5_onos.json b/web/gui/src/main/webapp/json/intent/ev_5_onos.json
new file mode 100644
index 0000000..7fd0ee4
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_5_onos.json
@@ -0,0 +1,13 @@
+{
+  "event": "showPath",
+  "sid": 2,
+  "payload": {
+    "intentId": "0x1234",
+    "path": {
+      "links": [ "1-2", "2-3" ],
+      "traffic": true,
+      "srcLabel": "967 Mb",
+      "dstLabel": "65 Mb"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/intent/ev_6_onos.json b/web/gui/src/main/webapp/json/intent/ev_6_onos.json
new file mode 100644
index 0000000..be3925e
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_6_onos.json
@@ -0,0 +1,11 @@
+{
+  "event": "showPath",
+  "sid": 2,
+  "payload": {
+    "intentId": "0x1234",
+    "path": {
+      "links": [ "1-2", "2-3" ],
+      "traffic": false
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/intent/ev_7_ui.json b/web/gui/src/main/webapp/json/intent/ev_7_ui.json
new file mode 100644
index 0000000..158476e
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_7_ui.json
@@ -0,0 +1,7 @@
+{
+  "event": "cancelMonitorIntent",
+  "sid": 3,
+  "payload": {
+    "intentId": "0x1234"
+  }
+}
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index b1901ba..fc7c35a 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -28,7 +28,7 @@
 
     // configuration data
     var config = {
-        useLiveData: false,
+        useLiveData: true,
         debugOn: false,
         debug: {
             showNodeXY: true,
@@ -120,7 +120,9 @@
         B: toggleBg,
         L: cycleLabels,
         P: togglePorts,
-        U: unpin
+        U: unpin,
+
+        X: requestPath
     };
 
     // state variables
@@ -132,7 +134,11 @@
         },
         webSock,
         labelIdx = 0,
-        selected = {},
+
+        //selected = {},
+        selectOrder = [],
+        selections = {},
+
         highlighted = null,
         hovered = null,
         viewMode = 'showAll',
@@ -239,6 +245,14 @@
         view.alert('unpin() callback')
     }
 
+    function requestPath(view) {
+        var payload = {
+            one: selections[selectOrder[0]].obj.id,
+            two: selections[selectOrder[1]].obj.id
+        }
+        sendMessage('requestPath', payload);
+    }
+
     // ==============================
     // Radio Button Callbacks
 
@@ -287,7 +301,8 @@
         addDevice: addDevice,
         updateDevice: updateDevice,
         removeDevice: removeDevice,
-        addLink: addLink
+        addLink: addLink,
+        showPath: showPath
     };
 
     function addDevice(data) {
@@ -326,6 +341,10 @@
         }
     }
 
+    function showPath(data) {
+        network.view.alert(data.event + "\n" + data.payload.links.length);
+    }
+
     // ....
 
     function unknownEvent(data) {
@@ -611,7 +630,7 @@
         },
 
         send : function(text) {
-            if (text != null && text.length > 0) {
+            if (text != null) {
                 webSock._send(text);
             }
         },
@@ -619,11 +638,93 @@
         _send : function(message) {
             if (webSock.ws) {
                 webSock.ws.send(message);
+            } else {
+                network.view.alert('no web socket open');
             }
         }
 
     };
 
+    var sid = 0;
+
+    function sendMessage(evType, payload) {
+        var toSend = {
+            event: evType,
+            sid: ++sid,
+            payload: payload
+        };
+        webSock.send(JSON.stringify(toSend));
+    }
+
+
+    // ==============================
+    // Selection stuff
+
+    function selectObject(obj, el) {
+        var n,
+            meta = d3.event.sourceEvent.metaKey;
+
+        if (el) {
+            n = d3.select(el);
+        } else {
+            node.each(function(d) {
+                if (d == obj) {
+                    n = d3.select(el = this);
+                }
+            });
+        }
+        if (!n) return;
+
+        if (meta && n.classed('selected')) {
+            deselectObject(obj.id);
+            //flyinPane(null);
+            return;
+        }
+
+        if (!meta) {
+            deselectAll();
+        }
+
+        // TODO: allow for mutli selections
+        var selected = {
+            obj : obj,
+            el  : el
+        };
+
+        selections[obj.id] = selected;
+        selectOrder.push(obj.id);
+
+        n.classed('selected', true);
+        //flyinPane(obj);
+    }
+
+    function deselectObject(id) {
+        var obj = selections[id];
+        if (obj) {
+            d3.select(obj.el).classed('selected', false);
+            selections[id] = null;
+            // TODO: use splice to remove element
+        }
+        //flyinPane(null);
+    }
+
+    function deselectAll() {
+        // deselect all nodes in the network...
+        node.classed('selected', false);
+        selections = {};
+        selectOrder = [];
+        //flyinPane(null);
+    }
+
+
+    $('#view').on('click', function(e) {
+        if (!$(e.target).closest('.node').length) {
+            if (!e.metaKey) {
+                deselectAll();
+            }
+        }
+    });
+
     // ==============================
     // View life-cycle callbacks
 
@@ -678,7 +779,7 @@
         }
 
         function selectCb(d, self) {
-            // TODO: selectObject(d, self);
+            selectObject(d, self);
         }
 
         function atDragEnd(d, self) {
@@ -686,11 +787,21 @@
             // if it is a device (not a host)
             if (d.class === 'device') {
                 d.fixed = true;
-                d3.select(self).classed('fixed', true)
+                d3.select(self).classed('fixed', true);
+                tellServerCoords(d);
                 // TODO: send new [x,y] back to server, via websocket.
             }
         }
 
+        function tellServerCoords(d) {
+            sendMessage('updateMeta', {
+                id: d.id,
+                'class': d.class,
+                x: d.x,
+                y: d.y
+            });
+        }
+
         // set up the force layout
         network.force = d3.layout.force()
             .size(forceDim)