ONOS-2186 - GUI Topo Overlay - (WIP)
- Showing traffic on selected intent now subdues other elements.
- Augmented Highlights to allow for retrieval by ID.
- Reparented HostHighlight and DeviceHighlight to NodeHighlight.
- Added a few extra highlight unit tests.

Change-Id: I0de1cefdcfda58a6fec6e90be5fe898d35aa1b37
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/DeviceHighlight.java b/core/api/src/main/java/org/onosproject/ui/topo/DeviceHighlight.java
index fe1ecb2..2985d3d 100644
--- a/core/api/src/main/java/org/onosproject/ui/topo/DeviceHighlight.java
+++ b/core/api/src/main/java/org/onosproject/ui/topo/DeviceHighlight.java
@@ -20,7 +20,7 @@
 /**
  * Denotes the highlighting to apply to a device.
  */
-public class DeviceHighlight extends AbstractHighlight {
+public class DeviceHighlight extends NodeHighlight {
 
     public DeviceHighlight(String deviceId) {
         super(TopoElementType.DEVICE, deviceId);
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/Highlights.java b/core/api/src/main/java/org/onosproject/ui/topo/Highlights.java
index 0700173..be59c26 100644
--- a/core/api/src/main/java/org/onosproject/ui/topo/Highlights.java
+++ b/core/api/src/main/java/org/onosproject/ui/topo/Highlights.java
@@ -17,9 +17,10 @@
 
 package org.onosproject.ui.topo;
 
+import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.HashMap;
+import java.util.Map;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
@@ -53,9 +54,9 @@
         }
     }
 
-    private final Set<DeviceHighlight> devices = new HashSet<>();
-    private final Set<HostHighlight> hosts = new HashSet<>();
-    private final Set<LinkHighlight> links = new HashSet<>();
+    private final Map<String, DeviceHighlight> devices = new HashMap<>();
+    private final Map<String, HostHighlight> hosts = new HashMap<>();
+    private final Map<String, LinkHighlight> links = new HashMap<>();
 
     private Amount subdueLevel = Amount.ZERO;
 
@@ -67,7 +68,7 @@
      * @return self, for chaining
      */
     public Highlights add(DeviceHighlight dh) {
-        devices.add(dh);
+        devices.put(dh.elementId(), dh);
         return this;
     }
 
@@ -78,7 +79,7 @@
      * @return self, for chaining
      */
     public Highlights add(HostHighlight hh) {
-        hosts.add(hh);
+        hosts.put(hh.elementId(), hh);
         return this;
     }
 
@@ -89,7 +90,7 @@
      * @return self, for chaining
      */
     public Highlights add(LinkHighlight lh) {
-        links.add(lh);
+        links.put(lh.elementId(), lh);
         return this;
     }
 
@@ -106,30 +107,30 @@
     }
 
     /**
-     * Returns the set of device highlights.
+     * Returns the collection of device highlights.
      *
      * @return device highlights
      */
-    public Set<DeviceHighlight> devices() {
-        return Collections.unmodifiableSet(devices);
+    public Collection<DeviceHighlight> devices() {
+        return Collections.unmodifiableCollection(devices.values());
     }
 
     /**
-     * Returns the set of host highlights.
+     * Returns the collection of host highlights.
      *
      * @return host highlights
      */
-    public Set<HostHighlight> hosts() {
-        return Collections.unmodifiableSet(hosts);
+    public Collection<HostHighlight> hosts() {
+        return Collections.unmodifiableCollection(hosts.values());
     }
 
     /**
-     * Returns the set of link highlights.
+     * Returns the collection of link highlights.
      *
      * @return link highlights
      */
-    public Set<LinkHighlight> links() {
-        return Collections.unmodifiableSet(links);
+    public Collection<LinkHighlight> links() {
+        return Collections.unmodifiableCollection(links.values());
     }
 
     /**
@@ -141,4 +142,49 @@
     public Amount subdueLevel() {
         return subdueLevel;
     }
+
+    /**
+     * Returns the node highlight (device or host) for the given element
+     * identifier, or null if no match.
+     *
+     * @param id element identifier
+     * @return corresponding node highlight
+     */
+    public NodeHighlight getNode(String id) {
+        NodeHighlight nh = devices.get(id);
+        return nh != null ? nh : hosts.get(id);
+    }
+
+    /**
+     * Returns the device highlight for the given device identifier,
+     * or null if no match.
+     *
+     * @param id device identifier
+     * @return corresponding device highlight
+     */
+    public DeviceHighlight getDevice(String id) {
+        return devices.get(id);
+    }
+
+    /**
+     * Returns the host highlight for the given host identifier,
+     * or null if no match.
+     *
+     * @param id host identifier
+     * @return corresponding host highlight
+     */
+    public HostHighlight getHost(String id) {
+        return hosts.get(id);
+    }
+
+    /**
+     * Returns the link highlight for the given link identifier,
+     * or null if no match.
+     *
+     * @param id link identifier
+     * @return corresponding link highlight
+     */
+    public LinkHighlight getLink(String id) {
+        return links.get(id);
+    }
 }
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/HostHighlight.java b/core/api/src/main/java/org/onosproject/ui/topo/HostHighlight.java
index cb64e07..76669a8 100644
--- a/core/api/src/main/java/org/onosproject/ui/topo/HostHighlight.java
+++ b/core/api/src/main/java/org/onosproject/ui/topo/HostHighlight.java
@@ -20,7 +20,7 @@
 /**
  * Denotes the highlighting to apply to a host.
  */
-public class HostHighlight extends AbstractHighlight {
+public class HostHighlight extends NodeHighlight {
 
     public HostHighlight(String hostId) {
         super(TopoElementType.HOST, hostId);
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/NodeHighlight.java b/core/api/src/main/java/org/onosproject/ui/topo/NodeHighlight.java
new file mode 100644
index 0000000..735f816
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/NodeHighlight.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.onosproject.ui.topo;
+
+/**
+ * Parent class of {@link DeviceHighlight} and {@link HostHighlight}.
+ */
+public abstract class NodeHighlight extends AbstractHighlight {
+    public NodeHighlight(TopoElementType type, String elementId) {
+        super(type, elementId);
+    }
+}
diff --git a/core/api/src/test/java/org/onosproject/ui/topo/HighlightsTest.java b/core/api/src/test/java/org/onosproject/ui/topo/HighlightsTest.java
index ce2d2db..7d6dfe6 100644
--- a/core/api/src/test/java/org/onosproject/ui/topo/HighlightsTest.java
+++ b/core/api/src/test/java/org/onosproject/ui/topo/HighlightsTest.java
@@ -17,28 +17,75 @@
 
 package org.onosproject.ui.topo;
 
+import org.junit.Before;
 import org.junit.Test;
+import org.onosproject.ui.topo.Highlights.Amount;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 /**
  * Unit tests for {@link Highlights}.
  */
 public class HighlightsTest {
 
-    private Highlights hl;
+    private static final String DEV_1 = "dev-1";
+    private static final String DEV_2 = "dev-2";
+    private static final String HOST_A = "Host...A";
+
+    private Highlights highlights;
+    private DeviceHighlight dh1;
+    private DeviceHighlight dh2;
+    private HostHighlight hha;
+
+    @Before
+    public void setUp() {
+        highlights = new Highlights();
+    }
 
     @Test
     public void basic() {
-        hl = new Highlights();
+        assertEquals("devices", 0, highlights.devices().size());
+        assertEquals("hosts", 0, highlights.hosts().size());
+        assertEquals("links", 0, highlights.links().size());
+        assertEquals("sudue", Amount.ZERO, highlights.subdueLevel());
+    }
 
-        assertEquals("devices", 0, hl.devices().size());
-        assertEquals("hosts", 0, hl.hosts().size());
-        assertEquals("links", 0, hl.links().size());
-        assertEquals("sudue", Highlights.Amount.ZERO, hl.subdueLevel());
+    @Test
+    public void coupleOfDevices() {
+        dh1 = new DeviceHighlight(DEV_1);
+        dh2 = new DeviceHighlight(DEV_2);
+
+        highlights.add(dh1);
+        highlights.add(dh2);
+        assertTrue("missing dh1", highlights.devices().contains(dh1));
+        assertTrue("missing dh2", highlights.devices().contains(dh2));
+    }
+
+    @Test
+    public void alternateSubdue() {
+        highlights.subdueAllElse(Amount.MINIMALLY);
+        assertEquals("wrong level", Amount.MINIMALLY, highlights.subdueLevel());
+    }
+
+    @Test
+    public void highlightRetrieval() {
+        dh1 = new DeviceHighlight(DEV_1);
+        hha = new HostHighlight(HOST_A);
+        highlights.add(dh1)
+                .add(hha);
+
+        assertNull("dev as host", highlights.getHost(DEV_1));
+        assertNull("host as dev", highlights.getDevice(HOST_A));
+
+        assertEquals("missed dev as dev", dh1, highlights.getDevice(DEV_1));
+        assertEquals("missed dev as node", dh1, highlights.getNode(DEV_1));
+
+        assertEquals("missed host as host", hha, highlights.getHost(HOST_A));
+        assertEquals("missed host as node", hha, highlights.getNode(HOST_A));
     }
 
     // NOTE: further unit tests involving the Highlights class are done
     //       in TopoJsonTest.
-
 }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java
index 2dc8e44..4dcaeb1 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java
@@ -20,7 +20,9 @@
 import com.google.common.collect.ImmutableList;
 import org.onosproject.net.Device;
 import org.onosproject.net.DeviceId;
+import org.onosproject.net.ElementId;
 import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
 import org.onosproject.net.Link;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.flow.FlowEntry;
@@ -40,8 +42,12 @@
 import org.onosproject.ui.impl.topo.TrafficLink;
 import org.onosproject.ui.impl.topo.TrafficLink.StatsType;
 import org.onosproject.ui.impl.topo.TrafficLinkMap;
+import org.onosproject.ui.topo.DeviceHighlight;
 import org.onosproject.ui.topo.Highlights;
+import org.onosproject.ui.topo.Highlights.Amount;
+import org.onosproject.ui.topo.HostHighlight;
 import org.onosproject.ui.topo.LinkHighlight.Flavor;
+import org.onosproject.ui.topo.NodeHighlight;
 import org.onosproject.ui.topo.NodeSelection;
 import org.onosproject.ui.topo.TopoUtils;
 import org.slf4j.Logger;
@@ -59,9 +65,7 @@
 import java.util.TimerTask;
 
 import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
-import static org.onosproject.ui.impl.TrafficMonitor.Mode.IDLE;
-import static org.onosproject.ui.impl.TrafficMonitor.Mode.RELATED_INTENTS;
-import static org.onosproject.ui.impl.TrafficMonitor.Mode.SELECTED_INTENT;
+import static org.onosproject.ui.impl.TrafficMonitor.Mode.*;
 
 /**
  * Encapsulates the behavior of monitoring specific traffic patterns.
@@ -442,6 +446,7 @@
                       current.id(), selectedIntents.index(), selectedIntents.size());
 
             highlightIntentLinksWithTraffic(highlights, primary);
+            highlights.subdueAllElse(Amount.MINIMALLY);
         }
         return highlights;
     }
@@ -540,19 +545,20 @@
         TrafficLinkMap linkMap = new TrafficLinkMap();
         // NOTE: highlight secondary first, then primary, so that links shared
         //       by intents are colored correctly ("last man wins")
-        createTrafficLinks(linkMap, secondary, Flavor.SECONDARY_HIGHLIGHT, false);
-        createTrafficLinks(linkMap, primary, Flavor.PRIMARY_HIGHLIGHT, false);
+        createTrafficLinks(highlights, linkMap, secondary, Flavor.SECONDARY_HIGHLIGHT, false);
+        createTrafficLinks(highlights, linkMap, primary, Flavor.PRIMARY_HIGHLIGHT, false);
         colorLinks(highlights, linkMap);
     }
 
     private void highlightIntentLinksWithTraffic(Highlights highlights,
                                                  Set<Intent> primary) {
         TrafficLinkMap linkMap = new TrafficLinkMap();
-        createTrafficLinks(linkMap, primary, Flavor.PRIMARY_HIGHLIGHT, true);
+        createTrafficLinks(highlights, linkMap, primary, Flavor.PRIMARY_HIGHLIGHT, true);
         colorLinks(highlights, linkMap);
     }
 
-    private void createTrafficLinks(TrafficLinkMap linkMap, Set<Intent> intents,
+    private void createTrafficLinks(Highlights highlights,
+                                    TrafficLinkMap linkMap, Set<Intent> intents,
                                     Flavor flavor, boolean showTraffic) {
         for (Intent intent : intents) {
             List<Intent> installables = servicesBundle.intentService()
@@ -573,11 +579,33 @@
 
                     boolean isOptical = intent instanceof OpticalConnectivityIntent;
                     processLinks(linkMap, links, flavor, isOptical, showTraffic);
+                    updateHighlights(highlights, links);
                 }
             }
         }
     }
 
+    private void updateHighlights(Highlights highlights, Iterable<Link> links) {
+        for (Link link : links) {
+            ensureNodePresent(highlights, link.src().elementId());
+            ensureNodePresent(highlights, link.dst().elementId());
+        }
+    }
+
+    private void ensureNodePresent(Highlights highlights, ElementId eid) {
+        String id = eid.toString();
+        NodeHighlight nh = highlights.getNode(id);
+        if (nh == null) {
+            if (eid instanceof DeviceId) {
+                nh = new DeviceHighlight(id);
+                highlights.add((DeviceHighlight) nh);
+            } else if (eid instanceof HostId) {
+                nh = new HostHighlight(id);
+                highlights.add((HostHighlight) nh);
+            }
+        }
+    }
+
     // Extracts links from the specified flow rule intent resources
     private Collection<Link> linkResources(Intent installable) {
         ImmutableList.Builder<Link> builder = ImmutableList.builder();
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoJson.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoJson.java
index 0a12e1c..8b4b354 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoJson.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoJson.java
@@ -90,13 +90,13 @@
     }
 
     private static ObjectNode json(DeviceHighlight dh) {
-        // TODO: implement this once we know what a device highlight looks like
-        return objectNode();
+        return objectNode()
+                .put(ID, dh.elementId());
     }
 
     private static ObjectNode json(HostHighlight hh) {
-        // TODO: implement this once we know what a host highlight looks like
-        return objectNode();
+        return objectNode()
+                .put(ID, hh.elementId());
     }
 
     private static ObjectNode json(LinkHighlight lh) {
diff --git a/web/gui/src/main/webapp/app/view/topo/topoForce.js b/web/gui/src/main/webapp/app/view/topo/topoForce.js
index 555fa50..dbe8f9f 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -242,6 +242,10 @@
 
     // ========================
 
+    function nodeById(id) {
+        return lu[id];
+    }
+
     function makeNodeKey(node1, node2) {
         return node1 + '-' + node2;
     }
@@ -515,10 +519,10 @@
         });
     }
 
-    function unsuppressLink(id, less) {
+    function unsuppressLink(key, less) {
         var cls = supAmt(less);
         link.each(function (n) {
-            if (n.id === id) {
+            if (n.key === key) {
                 n.el.classed(cls, false);
             }
         });
@@ -922,6 +926,7 @@
             clearLinkTrafficStyle: clearLinkTrafficStyle,
             removeLinkLabels: removeLinkLabels,
             findLinkById: tms.findLinkById,
+            findNodeById: nodeById,
             updateLinks: updateLinks,
             updateNodes: updateNodes,
             supLayers: suppressLayers,
diff --git a/web/gui/src/main/webapp/app/view/topo/topoOverlay.js b/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
index 4a432e0..6bd7762 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
@@ -301,11 +301,12 @@
              clearLinkTrafficStyle()
              removeLinkLabels()
              findLinkById( id )
+             findNodeById( id )
              updateLinks()
              updateNodes()
              supLayers( bool, [less] )
              unsupNode( id, [less] )
-             unsupLink( id, [less] )
+             unsupLink( key, [less] )
          */
 
         // TODO: clear node highlighting
@@ -322,16 +323,30 @@
             api.supLayers(false, true);
         }
 
-        // TODO: device and host highlights
+        data.hosts.forEach(function (host) {
+            var hdata = api.findNodeById(host.id);
+            if (hdata && !hdata.el.empty()) {
+                api.unsupNode(hdata.id, less);
+                // TODO: further highlighting?
+            }
+        });
 
-        data.links.forEach(function (lnk) {
-            var ldata = api.findLinkById(lnk.id),
-                lab = lnk.label,
+        data.devices.forEach(function (device) {
+            var ddata = api.findNodeById(device.id);
+            if (ddata && !ddata.el.empty()) {
+                api.unsupNode(ddata.id, less);
+                // TODO: further highlighting?
+            }
+        });
+
+        data.links.forEach(function (link) {
+            var ldata = api.findLinkById(link.id),
+                lab = link.label,
                 units, portcls, magnitude;
 
             if (ldata && !ldata.el.empty()) {
-                api.unsupLink(ldata.id, less);
-                ldata.el.classed(lnk.css, true);
+                api.unsupLink(ldata.key, less);
+                ldata.el.classed(link.css, true);
                 ldata.label = lab;
 
                 // inject additional styling for port-based traffic