ONOS-248 Added ability to visualize counts of device flows along egress links.

Change-Id: I4587c4a285025fb12e616391cdae91966d976c97
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewMessages.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewMessages.java
index 2c3f018..fdf6caa 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewMessages.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewMessages.java
@@ -35,10 +35,14 @@
 import org.onlab.onos.net.HostId;
 import org.onlab.onos.net.HostLocation;
 import org.onlab.onos.net.Link;
+import org.onlab.onos.net.PortNumber;
 import org.onlab.onos.net.device.DeviceEvent;
 import org.onlab.onos.net.device.DeviceService;
 import org.onlab.onos.net.flow.FlowEntry;
 import org.onlab.onos.net.flow.FlowRuleService;
+import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.flow.instructions.Instruction;
+import org.onlab.onos.net.flow.instructions.Instructions.OutputInstruction;
 import org.onlab.onos.net.host.HostEvent;
 import org.onlab.onos.net.host.HostService;
 import org.onlab.onos.net.intent.Intent;
@@ -58,6 +62,8 @@
 import org.slf4j.LoggerFactory;
 
 import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -72,6 +78,7 @@
 import static org.onlab.onos.cluster.ControllerNode.State.ACTIVE;
 import static org.onlab.onos.net.DeviceId.deviceId;
 import static org.onlab.onos.net.HostId.hostId;
+import static org.onlab.onos.net.PortNumber.P0;
 import static org.onlab.onos.net.PortNumber.portNumber;
 import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_ADDED;
 import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_REMOVED;
@@ -446,6 +453,49 @@
         return count;
     }
 
+    // Counts all entries that egress on the given device links.
+    protected Map<Link, Integer> getFlowCounts(DeviceId deviceId) {
+        List<FlowEntry> entries = new ArrayList<>();
+        Set<Link> links = new HashSet<>(linkService.getDeviceEgressLinks(deviceId));
+        Set<Host> hosts = hostService.getConnectedHosts(deviceId);
+        Iterator<FlowEntry> it = flowService.getFlowEntries(deviceId).iterator();
+        while (it.hasNext()) {
+            entries.add(it.next());
+        }
+
+        // Add all edge links to the set
+        if (hosts != null) {
+            for (Host host : hosts) {
+                links.add(new DefaultEdgeLink(host.providerId(),
+                                              new ConnectPoint(host.id(), P0),
+                                              host.location(), false));
+            }
+        }
+
+        Map<Link, Integer> counts = new HashMap<>();
+        for (Link link : links) {
+            counts.put(link, getEgressFlows(link, entries));
+        }
+        return counts;
+    }
+
+    // Counts all entries that egress on the link source port.
+    private Integer getEgressFlows(Link link, List<FlowEntry> entries) {
+        int count = 0;
+        PortNumber out = link.src().port();
+        for (FlowEntry entry : entries) {
+            TrafficTreatment treatment = entry.treatment();
+            for (Instruction instruction : treatment.instructions()) {
+                if (instruction.type() == Instruction.Type.OUTPUT &&
+                        ((OutputInstruction) instruction).port().equals(out)) {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
+
     // Returns host details response.
     protected ObjectNode hostDetails(HostId hostId, long sid) {
         Host host = hostService.getHost(hostId);
@@ -478,6 +528,34 @@
         return envelope("showTraffic", sid, payload);
     }
 
+    // Produces JSON message to trigger flow overview visualization
+    protected ObjectNode flowSummaryMessage(long sid, Set<Device> devices) {
+        ObjectNode payload = mapper.createObjectNode();
+        ArrayNode paths = mapper.createArrayNode();
+        payload.set("paths", paths);
+
+        for (Device device : devices) {
+            Map<Link, Integer> counts = getFlowCounts(device.id());
+            for (Link link : counts.keySet()) {
+                addLinkFlows(link, paths, counts.get(link));
+            }
+        }
+        return envelope("showTraffic", sid, payload);
+    }
+
+    private void addLinkFlows(Link link, ArrayNode paths, Integer count) {
+        ObjectNode pathNode = mapper.createObjectNode();
+        ArrayNode linksNode = mapper.createArrayNode();
+        ArrayNode labels = mapper.createArrayNode();
+        boolean noFlows = count == null || count == 0;
+        pathNode.put("class", noFlows ? "secondary" : "primary");
+        pathNode.put("traffic", false);
+        pathNode.set("links", linksNode.add(compactLinkString(link)));
+        pathNode.set("labels", labels.add(noFlows ? "" : (count.toString() +
+                (count == 1 ? " flow" : " flows"))));
+        paths.add(pathNode);
+    }
+
 
     // Produces JSON message to trigger traffic visualization
     protected ObjectNode trafficMessage(long sid, TrafficClass... trafficClasses) {
@@ -560,7 +638,7 @@
             unit = B_UNIT;
         }
         DecimalFormat format = new DecimalFormat("#,###.##");
-        return format.format(value) +  " " + unit;
+        return format.format(value) + " " + unit;
     }
 
     private boolean isInfrastructureEgress(Link link) {
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 99e9aeb..104fa95 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
@@ -42,6 +42,7 @@
 import org.onlab.osgi.ServiceDirectory;
 
 import java.io.IOException;
+import java.util.HashSet;
 import java.util.Set;
 import java.util.Timer;
 import java.util.TimerTask;
@@ -167,6 +168,7 @@
             processMessage((ObjectNode) mapper.reader().readTree(data));
         } catch (Exception e) {
             log.warn("Unable to parse GUI request {} due to {}", data, e);
+            log.warn("Boom!!!!", e);
         }
     }
 
@@ -183,6 +185,8 @@
             requestTraffic(event);
         } else if (type.equals("requestAllTraffic")) {
             requestAllTraffic(event);
+        } else if (type.equals("requestDeviceLinkFlows")) {
+            requestDeviceLinkFlows(event);
         } else if (type.equals("cancelTraffic")) {
             cancelTraffic(event);
         }
@@ -263,6 +267,26 @@
         sendMessage(trafficSummaryMessage(sid));
     }
 
+    private void requestDeviceLinkFlows(ObjectNode event) {
+        ObjectNode payload = payload(event);
+        long sid = number(event, "sid");
+        monitorRequest = event;
+
+        // Get the set of selected hosts and their intents.
+        ArrayNode ids = (ArrayNode) payload.path("ids");
+        Set<Host> hosts = new HashSet<>();
+        Set<Device> devices = getDevices(ids);
+
+        // If there is a hover node, include it in the hosts and find intents.
+        String hover = string(payload, "hover");
+        Set<Intent> hoverIntents;
+        if (!isNullOrEmpty(hover)) {
+            addHover(hosts, devices, hover);
+        }
+        sendMessage(flowSummaryMessage(sid, devices));
+    }
+
+
     // Subscribes for host traffic messages.
     private synchronized void requestTraffic(ObjectNode event) {
         ObjectNode payload = payload(event);
@@ -374,6 +398,8 @@
                 String type = string(monitorRequest, "event", "unknown");
                 if (type.equals("requestAllTraffic")) {
                     requestAllTraffic(monitorRequest);
+                } else if (type.equals("requestDeviceLinkFlows")) {
+                    requestDeviceLinkFlows(monitorRequest);
                 } else {
                     requestTraffic(monitorRequest);
                 }
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index e9a8c1e..82f7194 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -143,6 +143,7 @@
         H: toggleHover,
         V: showTrafficAction,
         A: showAllTrafficAction,
+        F: showDeviceLinkFlowsAction,
         esc: handleEscape
     };
 
@@ -179,7 +180,8 @@
         onosOrder = [],
         oiBox,
         oiShowMaster = false,
-        hoverEnabled = false,
+        hoverModes = [ 'none', 'intents', 'flows'],
+        hoverMode = 0,
         portLabelsOn = false;
 
     // D3 selections
@@ -314,7 +316,11 @@
     }
 
     function toggleHover(view) {
-        hoverEnabled = !hoverEnabled;
+        hoverMode++;
+        if (hoverMode === hoverModes.length) {
+            hoverMode = 0;
+        }
+        console.log('Hover Mode:' + hoverMode + ': ' + hoverModes[hoverMode]);
     }
 
     function togglePorts(view) {
@@ -827,6 +833,12 @@
     }
 
     function showTrafficAction() {
+        // force intents hover mode
+        hoverMode = 1;
+        showSelectTraffic();
+    }
+
+    function showSelectTraffic() {
         // if nothing is hovered over, and nothing selected, send cancel request
         if (!hovered && nSel() === 0) {
             sendMessage('cancelTraffic', {});
@@ -848,6 +860,25 @@
         sendMessage('requestAllTraffic', {});
     }
 
+    function showDeviceLinkFlowsAction() {
+        // force intents hover mode
+        hoverMode = 2;
+        showDeviceLinkFlows();
+    }
+
+    function showDeviceLinkFlows() {
+        // if nothing is hovered over, and nothing selected, send cancel request
+        if (!hovered && nSel() === 0) {
+            sendMessage('cancelTraffic', {});
+            return;
+        }
+        var hoverId = (flowsHover() && hovered && hovered.class === 'device') ?
+            hovered.id : '';
+        sendMessage('requestDeviceLinkFlows', {
+            ids: selectOrder,
+            hover: hoverId
+        });
+    }
 
     // ==============================
     // onos instance panel functions
@@ -1375,14 +1406,18 @@
     function nodeMouseOver(d) {
         hovered = d;
         if (trafficHover() && (d.class === 'host' || d.class === 'device')) {
-            showTrafficAction();
+            showSelectTraffic();
+        } else if (flowsHover() && (d.class === 'device')) {
+            showDeviceLinkFlows();
         }
     }
 
     function nodeMouseOut(d) {
         hovered = null;
         if (trafficHover() && (d.class === 'host' || d.class === 'device')) {
-            showTrafficAction();
+            showSelectTraffic();
+        } else if (flowsHover() && (d.class === 'device')) {
+            showDeviceLinkFlows();
         }
     }
 
@@ -2020,8 +2055,13 @@
     }
 
     function trafficHover() {
-        return hoverEnabled;
+        return hoverModes[hoverMode] === 'intents';
     }
+
+    function flowsHover() {
+        return hoverModes[hoverMode] === 'flows';
+    }
+
     function toggleTrafficHover() {
         showTrafficOnHover.classed('active', !trafficHover());
     }