ONOS-303 Added ability to add mult-source intent from GUI.
Fixed treatment of selection & hover modes.

Change-Id: Idf47b6a15b56ea96b9edaeeb034fad0f205af6e3
diff --git a/tools/package/debian/onos.conf b/tools/package/debian/onos.conf
index 888c02b..9891bd1 100644
--- a/tools/package/debian/onos.conf
+++ b/tools/package/debian/onos.conf
@@ -1,4 +1,4 @@
-description  "Open Networking Operating System"
+description  "Open Network Operating System"
 author       "ON.Lab"
 
 start on (net-device-up
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewWebSocket.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewWebSocket.java
index 9562040..f65232e 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewWebSocket.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewWebSocket.java
@@ -15,6 +15,7 @@
  */
 package org.onlab.onos.gui;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.eclipse.jetty.websocket.WebSocket;
@@ -23,20 +24,25 @@
 import org.onlab.onos.cluster.ControllerNode;
 import org.onlab.onos.core.ApplicationId;
 import org.onlab.onos.core.CoreService;
+import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.Device;
 import org.onlab.onos.net.Host;
 import org.onlab.onos.net.HostId;
+import org.onlab.onos.net.HostLocation;
 import org.onlab.onos.net.Link;
 import org.onlab.onos.net.device.DeviceEvent;
 import org.onlab.onos.net.device.DeviceListener;
 import org.onlab.onos.net.flow.DefaultTrafficSelector;
 import org.onlab.onos.net.flow.DefaultTrafficTreatment;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
 import org.onlab.onos.net.host.HostEvent;
 import org.onlab.onos.net.host.HostListener;
 import org.onlab.onos.net.intent.HostToHostIntent;
 import org.onlab.onos.net.intent.Intent;
 import org.onlab.onos.net.intent.IntentEvent;
 import org.onlab.onos.net.intent.IntentListener;
+import org.onlab.onos.net.intent.MultiPointToSinglePointIntent;
 import org.onlab.onos.net.link.LinkEvent;
 import org.onlab.onos.net.link.LinkListener;
 import org.onlab.osgi.ServiceDirectory;
@@ -119,7 +125,7 @@
         super(directory);
 
         intentFilter = new TopologyViewIntentFilter(intentService, deviceService,
-                                                   hostService, linkService);
+                                                    hostService, linkService);
         appId = directory.get(CoreService.class).registerApplication(APP_ID);
     }
 
@@ -195,8 +201,11 @@
             requestDetails(event);
         } else if (type.equals("updateMeta")) {
             updateMetaUi(event);
+
         } else if (type.equals("addHostIntent")) {
             createHostIntent(event);
+        } else if (type.equals("addMultiSourceIntent")) {
+            createMultiSourceIntent(event);
 
         } else if (type.equals("requestTraffic")) {
             requestTraffic(event);
@@ -268,6 +277,7 @@
         }
     }
 
+
     // Creates host-to-host intent.
     private void createHostIntent(ObjectNode event) {
         ObjectNode payload = payload(event);
@@ -276,19 +286,66 @@
         HostId one = hostId(string(payload, "one"));
         HostId two = hostId(string(payload, "two"));
 
-        HostToHostIntent hostIntent = new HostToHostIntent(appId, one, two,
-                                                           DefaultTrafficSelector.builder().build(),
-                                                           DefaultTrafficTreatment.builder().build());
-        trafficEvent = event;
-        intentService.submit(hostIntent);
+        HostToHostIntent intent =
+                new HostToHostIntent(appId, one, two,
+                                     DefaultTrafficSelector.builder().build(),
+                                     DefaultTrafficTreatment.builder().build());
+        startMonitoring(event);
+        intentService.submit(intent);
     }
 
-    private synchronized long startMonitoring(ObjectNode event) {
-        if (trafficTask == null) {
-            trafficEvent = event;
-            trafficTask = new TrafficMonitor();
-            timer.schedule(trafficTask, TRAFFIC_FREQUENCY_SEC, TRAFFIC_FREQUENCY_SEC);
+    // Creates multi-source-to-single-dest intent.
+    private void createMultiSourceIntent(ObjectNode event) {
+        ObjectNode payload = payload(event);
+        long id = number(event, "sid");
+        // TODO: add protection against device ids and non-existent hosts.
+        Set<HostId> src = getHostIds((ArrayNode) payload.path("src"));
+        HostId dst = hostId(string(payload, "dst"));
+        Host dstHost = hostService.getHost(dst);
+
+        Set<ConnectPoint> ingressPoints = getHostLocations(src);
+
+        // FIXME: clearly, this is not enough
+        TrafficSelector selector = DefaultTrafficSelector.builder()
+                .matchEthDst(dstHost.mac()).build();
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder().build();
+
+        MultiPointToSinglePointIntent intent =
+                new MultiPointToSinglePointIntent(appId, selector, treatment,
+                                                  ingressPoints, dstHost.location());
+        trafficEvent = event;
+        intentService.submit(intent);
+    }
+
+    private Set<ConnectPoint> getHostLocations(Set<HostId> hostIds) {
+        Set<ConnectPoint> points = new HashSet<>();
+        for (HostId hostId : hostIds) {
+            points.add(getHostLocation(hostId));
         }
+        return points;
+    }
+
+    private HostLocation getHostLocation(HostId hostId) {
+        return hostService.getHost(hostId).location();
+    }
+
+    // Produces a list of host ids from the specified JSON array.
+    private Set<HostId> getHostIds(ArrayNode ids) {
+        Set<HostId> hostIds = new HashSet<>();
+        for (JsonNode id : ids) {
+            hostIds.add(hostId(id.asText()));
+        }
+        return hostIds;
+    }
+
+
+    private synchronized long startMonitoring(ObjectNode event) {
+        if (trafficTask != null) {
+            stopMonitoring();
+        }
+        trafficEvent = event;
+        trafficTask = new TrafficMonitor();
+        timer.schedule(trafficTask, TRAFFIC_FREQUENCY_SEC, TRAFFIC_FREQUENCY_SEC);
         return number(event, "sid");
     }
 
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index 50b3493..46c0d02 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -132,11 +132,11 @@
 /* LINKS */
 
 #topo svg .link {
-    opacity: .7;
+    opacity: .9;
 }
 
 #topo svg .link.inactive {
-    opacity: .2;
+    opacity: .5;
     stroke-dasharray: 8 4;
 }
 
@@ -285,7 +285,7 @@
     padding: 2px 6px;
     font-size: 9pt;
     cursor: pointer;
-    width: 50%;
+    width: 200px;
     text-align: center;
 
     /* theme specific... */
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index d58de15..cd30334 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -72,9 +72,9 @@
         topo: {
             linkBaseColor: '#666',
             linkInColor: '#66f',
-            linkInWidth: 14,
+            linkInWidth: 12,
             linkOutColor: '#f00',
-            linkOutWidth: 14
+            linkOutWidth: 10
         },
         icons: {
             w: 30,
@@ -148,8 +148,7 @@
         P: togglePorts,
         U: [unpin, 'Unpin node'],
         R: [resetZoomPan, 'Reset zoom/pan'],
-        H: [cycleHoverMode, 'Cycle hover mode'],
-        V: [showTrafficAction, 'Show traffic'],
+        V: [showTrafficAction, 'Show related traffic'],
         A: [showAllTrafficAction, 'Show all traffic'],
         F: [showDeviceLinkFlowsAction, 'Show device link flows'],
         esc: handleEscape
@@ -191,10 +190,13 @@
         onosOrder = [],
         oiBox,
         oiShowMaster = false,
-        hoverModes = [ 'none', 'intents', 'flows'],
-        hoverMode = 0,
         portLabelsOn = false;
 
+    var hoverModeAll = 1,
+        hoverModeFlows = 2,
+        hoverModeIntents = 3,
+        hoverMode = hoverModeFlows;
+
     // D3 selections
     var svg,
         zoomPanContainer,
@@ -327,14 +329,6 @@
         });
     }
 
-    function cycleHoverMode(view) {
-        hoverMode++;
-        if (hoverMode === hoverModes.length) {
-            hoverMode = 0;
-        }
-        view.flash('Mode: ' + hoverModes[hoverMode]);
-    }
-
     function togglePorts(view) {
         view.alert('togglePorts() callback')
     }
@@ -829,6 +823,14 @@
     function getSelId(idx) {
         return getSel(idx).obj.id;
     }
+    function getSelIds(start, endOffset) {
+        var end = selectOrder.length - endOffset;
+        var ids = [];
+        selectOrder.slice(start, end).forEach(function (d) {
+            ids.push(getSelId(d));
+        });
+        return ids;
+    }
     function allSelectionsClass(cls) {
         for (var i=0, n=nSel(); i<n; i++) {
             if (getSel(i).obj.class !== cls) {
@@ -876,69 +878,92 @@
         sendMessage('requestDetails', payload);
     }
 
-    function addIntentAction() {
+    function addHostIntentAction() {
         sendMessage('addHostIntent', {
-            one: getSelId(0),
-            two: getSelId(1),
-            ids: [ getSelId(0), getSelId(1) ]
+            one: selectOrder[0],
+            two: selectOrder[1],
+            ids: selectOrder
         });
-        network.view.flash('Host-to-Host connectivity added');
+        network.view.flash('Host-to-Host flow added');
     }
 
-    function showTrafficAction() {
-        cancelTraffic();
-        hoverMode = 1;
-        showSelectTraffic();
-        network.view.flash('Related Traffic');
+    function addMultiSourceIntentAction() {
+        sendMessage('addMultiSourceIntent', {
+            src: selectOrder.slice(0, selectOrder.length - 1),
+            dst: selectOrder[selectOrder.length - 1],
+            ids: selectOrder
+        });
+        network.view.flash('Multi-Source flow added');
     }
 
+
     function cancelTraffic() {
         sendMessage('cancelTraffic', {});
     }
 
-    function showSelectTraffic() {
-        // if nothing is hovered over, and nothing selected, send cancel request
-        if (!hovered && nSel() === 0) {
-            cancelTraffic();
-            return;
+    function requestTrafficForMode() {
+        if (hoverMode === hoverModeAll) {
+            requestAllTraffic();
+        } else if (hoverMode === hoverModeFlows) {
+            requestDeviceLinkFlows();
+        } else if (hoverMode === hoverModeIntents) {
+            requestSelectTraffic();
         }
+    }
 
-        // NOTE: hover is only populated if "show traffic on hover" is
-        //        toggled on, and the item hovered is a host or a device...
-        var hoverId = (trafficHover() && hovered &&
-                (hovered.class === 'host' || hovered.class === 'device'))
+    function showTrafficAction() {
+        hoverMode = hoverModeIntents;
+        requestSelectTraffic();
+        network.view.flash('Related Traffic');
+    }
+
+    function requestSelectTraffic() {
+        if (validateSelectionContext()) {
+            var hoverId = (hoverMode === hoverModeIntents && hovered &&
+                    (hovered.class === 'host' || hovered.class === 'device'))
                         ? hovered.id : '';
-        sendMessage('requestTraffic', {
-            ids: selectOrder,
-            hover: hoverId
-        });
+            sendMessage('requestTraffic', {
+                ids: selectOrder,
+                hover: hoverId
+            });
+        }
     }
 
-    function showAllTrafficAction() {
-        cancelTraffic();
-        sendMessage('requestAllTraffic', {});
-        network.view.flash('All Traffic');
-    }
 
     function showDeviceLinkFlowsAction() {
-        cancelTraffic();
-        hoverMode = 2;
-        showDeviceLinkFlows();
+        hoverMode = hoverModeFlows;
+        requestDeviceLinkFlows();
         network.view.flash('Device Flows');
     }
 
-    function showDeviceLinkFlows() {
-        // if nothing is hovered over, and nothing selected, send cancel request
+    function requestDeviceLinkFlows() {
+        if (validateSelectionContext()) {
+            var hoverId = (hoverMode === hoverModeFlows && hovered &&
+                    (hovered.class === 'device')) ? hovered.id : '';
+            sendMessage('requestDeviceLinkFlows', {
+                ids: selectOrder,
+                hover: hoverId
+            });
+        }
+    }
+
+
+    function showAllTrafficAction() {
+        hoverMode = hoverModeAll;
+        requestAllTraffic();
+        network.view.flash('All Traffic');
+    }
+
+    function requestAllTraffic() {
+        sendMessage('requestAllTraffic', {});
+    }
+
+    function validateSelectionContext() {
         if (!hovered && nSel() === 0) {
             cancelTraffic();
-            return;
+            return false;
         }
-        var hoverId = (flowsHover() && hovered && hovered.class === 'device') ?
-            hovered.id : '';
-        sendMessage('requestDeviceLinkFlows', {
-            ids: selectOrder,
-            hover: hoverId
-        });
+        return true;
     }
 
     // TODO: these should be moved out to utility module.
@@ -1547,20 +1572,12 @@
 
     function nodeMouseOver(d) {
         hovered = d;
-        if (trafficHover() && (d.class === 'host' || d.class === 'device')) {
-            showSelectTraffic();
-        } else if (flowsHover() && (d.class === 'device')) {
-            showDeviceLinkFlows();
-        }
+        requestTrafficForMode();
     }
 
     function nodeMouseOut(d) {
         hovered = null;
-        if (trafficHover() && (d.class === 'host' || d.class === 'device')) {
-            showSelectTraffic();
-        } else if (flowsHover() && (d.class === 'device')) {
-            showDeviceLinkFlows();
-        }
+        requestTrafficForMode();
     }
 
     function addHostIcon(node, radius, iid) {
@@ -2002,22 +2019,29 @@
     function updateDetailPane() {
         var nSel = selectOrder.length;
         if (!nSel) {
-            detailPane.hide();
-            cancelTraffic();
+            emptySelect();
         } else if (nSel === 1) {
             singleSelect();
+            requestTrafficForMode();
         } else {
             multiSelect();
         }
     }
 
+    function emptySelect() {
+        detailPane.hide();
+        cancelTraffic();
+    }
+
     function singleSelect() {
+        // NOTE: detail is shown from showDetails event callback
         requestDetails();
-        // NOTE: detail pane will be shown from showDetails event callback
+        requestTrafficForMode();
     }
 
     function multiSelect() {
         populateMultiSelect();
+        requestTrafficForMode();
     }
 
     function addSep(tbody) {
@@ -2127,7 +2151,9 @@
         addAction(detailPane, 'Show Related Traffic', showTrafficAction);
         // if exactly two hosts are selected, also want 'add host intent'
         if (nSel() === 2 && allSelectionsClass('host')) {
-            addAction(detailPane, 'Add Host-to-Host Intent', addIntentAction);
+            addAction(detailPane, 'Create Host-to-Host Flow', addHostIntentAction);
+        } else if (nSel() >= 2 && allSelectionsClass('host')) {
+            addAction(detailPane, 'Create Multi-Source Flow', addMultiSourceIntentAction);
         }
     }
 
@@ -2239,14 +2265,6 @@
         return false;
     }
 
-    function trafficHover() {
-        return hoverModes[hoverMode] === 'intents';
-    }
-
-    function flowsHover() {
-        return hoverModes[hoverMode] === 'flows';
-    }
-
     function loadGlyphs(svg) {
         var defs = svg.append('defs');
         gly.defBird(defs);