ONOS-1479 -- GUI - augmenting topology view for extensibility: WIP.
- Major refactoring of TopologyViewMessageHandler and related classes.

Change-Id: I920f7f9f7317f3987a9a8da35ac086e9f8cab8d3
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/AbstractHighlight.java b/core/api/src/main/java/org/onosproject/ui/topo/AbstractHighlight.java
new file mode 100644
index 0000000..23cd7d8
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/AbstractHighlight.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * Partial implementation of the types of highlight to apply to topology
+ * elements.
+ */
+public abstract class AbstractHighlight {
+    private final TopoElementType type;
+    private final String elementId;
+
+    public AbstractHighlight(TopoElementType type, String elementId) {
+        this.type = type;
+        this.elementId = elementId;
+    }
+
+    public TopoElementType type() {
+        return type;
+    }
+
+    public String elementId() {
+        return elementId;
+    }
+}
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
new file mode 100644
index 0000000..1b721b8
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/DeviceHighlight.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * Denotes the types of highlight to apply to a link.
+ */
+public class DeviceHighlight extends AbstractHighlight {
+
+    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
new file mode 100644
index 0000000..107fdd3
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/Highlights.java
@@ -0,0 +1,66 @@
+/*
+ * 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;
+
+import java.text.DecimalFormat;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Encapsulates highlights to be applied to the topology view, such as
+ * highlighting links, displaying link labels, perhaps even decorating
+ * nodes with badges, etc.
+ */
+public class Highlights {
+
+    private static final DecimalFormat DF0 = new DecimalFormat("#,###");
+
+    private final Set<DeviceHighlight> devices = new HashSet<>();
+    private final Set<HostHighlight> hosts = new HashSet<>();
+    private final Set<LinkHighlight> links = new HashSet<>();
+
+
+    public Highlights add(DeviceHighlight d) {
+        devices.add(d);
+        return this;
+    }
+
+    public Highlights add(HostHighlight h) {
+        hosts.add(h);
+        return this;
+    }
+
+    public Highlights add(LinkHighlight lh) {
+        links.add(lh);
+        return this;
+    }
+
+
+    public Set<DeviceHighlight> devices() {
+        return Collections.unmodifiableSet(devices);
+    }
+
+    public Set<HostHighlight> hosts() {
+        return Collections.unmodifiableSet(hosts);
+    }
+
+    public Set<LinkHighlight> links() {
+        return Collections.unmodifiableSet(links);
+    }
+}
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
new file mode 100644
index 0000000..ff8b3be
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/HostHighlight.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * Denotes the types of highlight to apply to a link.
+ */
+public class HostHighlight extends AbstractHighlight {
+
+    public HostHighlight(String hostId) {
+        super(TopoElementType.HOST, hostId);
+    }
+
+
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/LinkHighlight.java b/core/api/src/main/java/org/onosproject/ui/topo/LinkHighlight.java
new file mode 100644
index 0000000..d8e4279
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/LinkHighlight.java
@@ -0,0 +1,189 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.TreeSet;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Denotes the highlighting to be applied to a link.
+ * {@link Flavor} is a closed set of NO-, PRIMARY-, or SECONDARY- highlighting.
+ * {@link Mod} is an open ended set of additional modifications (CSS classes)
+ * to apply. Note that {@link #MOD_OPTICAL} and {@link #MOD_ANIMATED} are
+ * pre-defined mods.
+ * Label text may be set, which will also be displayed on the link.
+ */
+public class LinkHighlight extends AbstractHighlight {
+
+    private static final String PLAIN = "plain";
+    private static final String PRIMARY = "primary";
+    private static final String SECONDARY = "secondary";
+    private static final String EMPTY = "";
+    private static final String SPACE = " ";
+
+    private final Flavor flavor;
+    private final Set<Mod> mods = new TreeSet<>();
+    private String label = EMPTY;
+
+    /**
+     * Constructs a link highlight entity.
+     *
+     * @param linkId the link identifier
+     * @param flavor the highlight flavor
+     */
+    public LinkHighlight(String linkId, Flavor flavor) {
+        super(TopoElementType.LINK, linkId);
+        this.flavor = checkNotNull(flavor);
+    }
+
+    /**
+     * Adds a highlighting modification to this link highlight.
+     *
+     * @param mod mod to be added
+     * @return self, for chaining
+     */
+    public LinkHighlight addMod(Mod mod) {
+        mods.add(checkNotNull(mod));
+        return this;
+    }
+
+    /**
+     * Adds a label to be displayed on the link.
+     *
+     * @param label the label text
+     * @return self, for chaining
+     */
+    public LinkHighlight setLabel(String label) {
+        this.label = label == null ? EMPTY : label;
+        return this;
+    }
+
+    /**
+     * Returns the highlight flavor.
+     *
+     * @return highlight flavor
+     */
+    public Flavor flavor() {
+        return flavor;
+    }
+
+    /**
+     * Returns the highlight modifications.
+     *
+     * @return highlight modifications
+     */
+    public Set<Mod> mods() {
+        return Collections.unmodifiableSet(mods);
+    }
+
+    /**
+     * Generates the CSS classes string from the {@link #flavor} and
+     * any optional {@link #mods}.
+     *
+     * @return CSS classes string
+     */
+    public String cssClasses() {
+        StringBuilder sb = new StringBuilder(flavor.toString());
+        mods.forEach(m -> sb.append(SPACE).append(m));
+        return sb.toString();
+    }
+
+    /**
+     * Returns the label text.
+     *
+     * @return label text
+     */
+    public String label() {
+        return label;
+    }
+
+    /**
+     * Link highlighting flavor.
+     */
+    public enum Flavor {
+        NO_HIGHLIGHT(PLAIN),
+        PRIMARY_HIGHLIGHT(PRIMARY),
+        SECONDARY_HIGHLIGHT(SECONDARY);
+
+        private String cssName;
+
+        Flavor(String s) {
+            cssName = s;
+        }
+
+        @Override
+        public String toString() {
+            return cssName;
+        }
+    }
+
+    /**
+     * Link highlighting modification.
+     * <p>
+     * Note that this translates to a CSS class name that is applied to
+     * the link in the Topology UI.
+     */
+    public static final class Mod implements Comparable<Mod> {
+        private final String modId;
+
+        public Mod(String modId) {
+            this.modId = checkNotNull(modId);
+        }
+
+        @Override
+        public String toString() {
+            return modId;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            Mod mod = (Mod) o;
+            return modId.equals(mod.modId);
+        }
+
+        @Override
+        public int hashCode() {
+            return modId.hashCode();
+        }
+
+
+        @Override
+        public int compareTo(Mod o) {
+            return this.modId.compareTo(o.modId);
+        }
+    }
+
+    /**
+     * Denotes a link to be tagged as an optical link.
+     */
+    public static final Mod MOD_OPTICAL = new Mod("optical");
+
+    /**
+     * Denotes a link to be tagged with animated traffic ("marching ants").
+     */
+    public static final Mod MOD_ANIMATED = new Mod("animated");
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/TopoElementType.java b/core/api/src/main/java/org/onosproject/ui/topo/TopoElementType.java
new file mode 100644
index 0000000..dc32746
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/TopoElementType.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+/**
+ * The topology element types to which a highlight can be applied.
+ */
+public enum TopoElementType {
+    DEVICE, HOST, LINK
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/LinkViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/LinkViewMessageHandler.java
index a311b2d..18a5acd 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/LinkViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/LinkViewMessageHandler.java
@@ -24,7 +24,8 @@
 import org.onosproject.net.link.LinkService;
 import org.onosproject.ui.RequestHandler;
 import org.onosproject.ui.UiMessageHandler;
-import org.onosproject.ui.impl.TopologyViewMessageHandlerBase.BiLink;
+import org.onosproject.ui.impl.topo.BiLink;
+import org.onosproject.ui.impl.topo.TopoUtils;
 import org.onosproject.ui.table.TableModel;
 import org.onosproject.ui.table.TableRequestHandler;
 import org.onosproject.ui.table.cell.ConnectPointFormatter;
@@ -33,13 +34,14 @@
 import java.util.Collection;
 import java.util.Map;
 
-import static org.onosproject.ui.impl.TopologyViewMessageHandlerBase.addLink;
-
 /**
  * Message handler for link view related messages.
  */
 public class LinkViewMessageHandler extends UiMessageHandler {
 
+    private static final String A_BOTH_B = "A &harr; B";
+    private static final String A_SINGLE_B = "A &rarr; B";
+
     private static final String LINK_DATA_REQ = "linkDataRequest";
     private static final String LINK_DATA_RESP = "linkDataResponse";
     private static final String LINKS = "links";
@@ -94,38 +96,39 @@
 
             // First consolidate all uni-directional links into two-directional ones.
             Map<LinkKey, BiLink> biLinks = Maps.newHashMap();
-            ls.getLinks().forEach(link -> addLink(biLinks, link));
+            ls.getLinks().forEach(link -> TopoUtils.addLink(biLinks, link));
 
             // Now scan over all bi-links and produce table rows from them.
             biLinks.values().forEach(biLink -> populateRow(tm.addRow(), biLink));
         }
 
         private void populateRow(TableModel.Row row, BiLink biLink) {
-            row.cell(ONE, biLink.one.src())
-                .cell(TWO, biLink.one.dst())
+            row.cell(ONE, biLink.one().src())
+                .cell(TWO, biLink.one().dst())
                 .cell(TYPE, linkType(biLink))
                 .cell(STATE, linkState(biLink))
                 .cell(DIRECTION, linkDir(biLink))
-                .cell(DURABLE, biLink.one.isDurable());
+                .cell(DURABLE, biLink.one().isDurable());
         }
 
         private String linkType(BiLink link) {
             StringBuilder sb = new StringBuilder();
-            sb.append(link.one.type());
-            if (link.two != null && link.two.type() != link.one.type()) {
-                sb.append(" / ").append(link.two.type());
+            sb.append(link.one().type());
+            if (link.two() != null && link.two().type() != link.one().type()) {
+                sb.append(" / ").append(link.two().type());
             }
             return sb.toString();
         }
 
         private String linkState(BiLink link) {
-            return (link.one.state() == Link.State.ACTIVE ||
-                    link.two.state() == Link.State.ACTIVE) ?
+            return (link.one().state() == Link.State.ACTIVE ||
+                    link.two().state() == Link.State.ACTIVE) ?
                     ICON_ID_ONLINE : ICON_ID_OFFLINE;
         }
 
         private String linkDir(BiLink link) {
-            return link.two != null ? "A &harr; B" : "A &rarr; B";
+            return link.two() != null ? A_BOTH_B : A_SINGLE_B;
         }
     }
+
 }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java
index b88ee4f..c2f54e4 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java
@@ -48,7 +48,6 @@
 import org.onosproject.net.host.HostEvent;
 import org.onosproject.net.host.HostListener;
 import org.onosproject.net.intent.HostToHostIntent;
-import org.onosproject.net.intent.Intent;
 import org.onosproject.net.intent.IntentEvent;
 import org.onosproject.net.intent.IntentListener;
 import org.onosproject.net.intent.MultiPointToSinglePointIntent;
@@ -57,6 +56,9 @@
 import org.onosproject.ui.JsonUtils;
 import org.onosproject.ui.RequestHandler;
 import org.onosproject.ui.UiConnection;
+import org.onosproject.ui.impl.TrafficMonitorObject.Mode;
+import org.onosproject.ui.impl.topo.NodeSelection;
+import org.onosproject.ui.topo.Highlights;
 import org.onosproject.ui.topo.PropertyPanel;
 
 import java.util.ArrayList;
@@ -70,7 +72,6 @@
 import java.util.TimerTask;
 import java.util.concurrent.ExecutorService;
 
-import static com.google.common.base.Strings.isNullOrEmpty;
 import static java.util.concurrent.Executors.newSingleThreadExecutor;
 import static org.onlab.util.Tools.groupedThreads;
 import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_ADDED;
@@ -117,8 +118,6 @@
 
     // fields
     private static final String ID = "id";
-    private static final String IDS = "ids";
-    private static final String HOVER = "hover";
     private static final String DEVICE = "device";
     private static final String HOST = "host";
     private static final String CLASS = "class";
@@ -132,14 +131,12 @@
     private static final String NAMES = "names";
     private static final String ACTIVATE = "activate";
     private static final String DEACTIVATE = "deactivate";
-    private static final String PRIMARY = "primary";
-    private static final String SECONDARY = "secondary";
 
 
     private static final String APP_ID = "org.onosproject.gui";
 
-    private static final long TRAFFIC_FREQUENCY = 5000;
-    private static final long SUMMARY_FREQUENCY = 30000;
+    private static final long TRAFFIC_PERIOD = 5000;
+    private static final long SUMMARY_PERIOD = 30000;
 
     private static final Comparator<? super ControllerNode> NODE_COMPARATOR =
             (o1, o2) -> o1.id().toString().compareTo(o2.id().toString());
@@ -165,31 +162,21 @@
     private final ExecutorService msgSender =
             newSingleThreadExecutor(groupedThreads("onos/gui", "msg-sender"));
 
-    private TopoOverlayCache overlayCache;
+    private TrafficMonitorObject tmo;
 
-    private TimerTask trafficTask = null;
-    private TrafficEvent trafficEvent = null;
+    private TopoOverlayCache overlayCache;
 
     private TimerTask summaryTask = null;
     private boolean summaryRunning = false;
 
     private boolean listenersRemoved = false;
 
-    private TopologyViewIntentFilter intentFilter;
-
-    // Current selection context
-    private Set<Host> selectedHosts;
-    private Set<Device> selectedDevices;
-    private List<Intent> selectedIntents;
-    private int currentIntentIndex = -1;
-
 
     @Override
     public void init(UiConnection connection, ServiceDirectory directory) {
         super.init(connection, directory);
-        intentFilter = new TopologyViewIntentFilter(intentService, deviceService,
-                                                    hostService, linkService);
         appId = directory.get(CoreService.class).registerApplication(APP_ID);
+        tmo = new TrafficMonitorObject(TRAFFIC_PERIOD, servicesBundle, this);
     }
 
     @Override
@@ -214,18 +201,18 @@
                 new UpdateMeta(),
                 new EqMasters(),
 
-                // TODO: implement "showHighlights" event (replaces "showTraffic")
-
                 // TODO: migrate traffic related to separate app
                 new AddHostIntent(),
                 new AddMultiSourceIntent(),
+
+                new ReqAllFlowTraffic(),
+                new ReqAllPortTraffic(),
+                new ReqDevLinkFlows(),
                 new ReqRelatedIntents(),
                 new ReqNextIntent(),
                 new ReqPrevIntent(),
                 new ReqSelectedIntentTraffic(),
-                new ReqAllFlowTraffic(),
-                new ReqAllPortTraffic(),
-                new ReqDevLinkFlows(),
+
                 new CancelTraffic()
         );
     }
@@ -288,7 +275,7 @@
         @Override
         public void process(long sid, ObjectNode payload) {
             stopSummaryMonitoring();
-            stopTrafficMonitoring();
+            tmo.stop();
         }
     }
 
@@ -390,6 +377,9 @@
         }
     }
 
+
+    // ========= -----------------------------------------------------------------
+
     // === TODO: move traffic related classes to traffic app
 
     private final class AddHostIntent extends RequestHandler {
@@ -410,7 +400,7 @@
                     .build();
 
             intentService.submit(intent);
-            startMonitoringIntent(intent);
+            tmo.monitor(intent);
         }
     }
 
@@ -443,82 +433,11 @@
                             .build();
 
             intentService.submit(intent);
-            startMonitoringIntent(intent);
+            tmo.monitor(intent);
         }
     }
 
-    private final class ReqRelatedIntents extends RequestHandler {
-        private ReqRelatedIntents() {
-            super(REQ_RELATED_INTENTS);
-        }
-
-        @Override
-        public void process(long sid, ObjectNode payload) {
-            // Cancel any other traffic monitoring mode.
-            stopTrafficMonitoring();
-
-            if (!payload.has(IDS)) {
-                return;
-            }
-
-            // Get the set of selected hosts and their intents.
-            ArrayNode ids = (ArrayNode) payload.path(IDS);
-            selectedHosts = getHosts(ids);
-            selectedDevices = getDevices(ids);
-            selectedIntents = intentFilter.findPathIntents(
-                    selectedHosts, selectedDevices, intentService.getIntents());
-            currentIntentIndex = -1;
-
-            if (haveSelectedIntents()) {
-                // Send a message to highlight all links of all monitored intents.
-                sendMessage(trafficMessage(new TrafficClass(PRIMARY, selectedIntents)));
-            }
-
-            // TODO: Re-introduce once the client click vs hover gesture stuff is sorted out.
-//        String hover = string(payload, "hover");
-//        if (!isNullOrEmpty(hover)) {
-//            // If there is a hover node, include it in the selection and find intents.
-//            processHoverExtendedSelection(sid, hover);
-//        }
-        }
-    }
-
-    private final class ReqNextIntent extends RequestHandler {
-        private ReqNextIntent() {
-            super(REQ_NEXT_INTENT);
-        }
-
-        @Override
-        public void process(long sid, ObjectNode payload) {
-            stopTrafficMonitoring();
-            requestAnotherRelatedIntent(+1);
-        }
-    }
-
-    private final class ReqPrevIntent extends RequestHandler {
-        private ReqPrevIntent() {
-            super(REQ_PREV_INTENT);
-        }
-
-        @Override
-        public void process(long sid, ObjectNode payload) {
-            stopTrafficMonitoring();
-            requestAnotherRelatedIntent(-1);
-        }
-    }
-
-    private final class ReqSelectedIntentTraffic extends RequestHandler {
-        private ReqSelectedIntentTraffic() {
-            super(REQ_SEL_INTENT_TRAFFIC);
-        }
-
-        @Override
-        public void process(long sid, ObjectNode payload) {
-            trafficEvent = new TrafficEvent(TrafficEvent.Type.SEL_INTENT, payload);
-            requestSelectedIntentTraffic();
-            startTrafficMonitoring();
-        }
-    }
+    // ========= -----------------------------------------------------------------
 
     private final class ReqAllFlowTraffic extends RequestHandler {
         private ReqAllFlowTraffic() {
@@ -527,8 +446,7 @@
 
         @Override
         public void process(long sid, ObjectNode payload) {
-            trafficEvent = new TrafficEvent(TrafficEvent.Type.ALL_FLOW_TRAFFIC, payload);
-            requestAllFlowTraffic();
+            tmo.monitor(Mode.ALL_FLOW_TRAFFIC);
         }
     }
 
@@ -539,8 +457,7 @@
 
         @Override
         public void process(long sid, ObjectNode payload) {
-            trafficEvent = new TrafficEvent(TrafficEvent.Type.ALL_PORT_TRAFFIC, payload);
-            requestAllPortTraffic();
+            tmo.monitor(Mode.ALL_PORT_TRAFFIC);
         }
     }
 
@@ -551,8 +468,55 @@
 
         @Override
         public void process(long sid, ObjectNode payload) {
-            trafficEvent = new TrafficEvent(TrafficEvent.Type.DEV_LINK_FLOWS, payload);
-            requestDeviceLinkFlows(payload);
+            NodeSelection nodeSelection =
+                    new NodeSelection(payload, deviceService, hostService);
+            tmo.monitor(Mode.DEV_LINK_FLOWS, nodeSelection);
+        }
+    }
+
+    private final class ReqRelatedIntents extends RequestHandler {
+        private ReqRelatedIntents() {
+            super(REQ_RELATED_INTENTS);
+        }
+
+        @Override
+        public void process(long sid, ObjectNode payload) {
+            NodeSelection nodeSelection =
+                    new NodeSelection(payload, deviceService, hostService);
+            tmo.monitor(Mode.RELATED_INTENTS, nodeSelection);
+        }
+    }
+
+    private final class ReqNextIntent extends RequestHandler {
+        private ReqNextIntent() {
+            super(REQ_NEXT_INTENT);
+        }
+
+        @Override
+        public void process(long sid, ObjectNode payload) {
+            tmo.selectNextIntent();
+        }
+    }
+
+    private final class ReqPrevIntent extends RequestHandler {
+        private ReqPrevIntent() {
+            super(REQ_PREV_INTENT);
+        }
+
+        @Override
+        public void process(long sid, ObjectNode payload) {
+            tmo.selectPreviousIntent();
+        }
+    }
+
+    private final class ReqSelectedIntentTraffic extends RequestHandler {
+        private ReqSelectedIntentTraffic() {
+            super(REQ_SEL_INTENT_TRAFFIC);
+        }
+
+        @Override
+        public void process(long sid, ObjectNode payload) {
+            tmo.monitor(Mode.SEL_INTENT);
         }
     }
 
@@ -563,14 +527,16 @@
 
         @Override
         public void process(long sid, ObjectNode payload) {
-            selectedIntents = null;
-            sendMessage(trafficMessage());
-            stopTrafficMonitoring();
+            tmo.stop();
         }
     }
 
     //=======================================================================
 
+    // Converts highlights to JSON format and sends the message to the client
+    protected void sendHighlights(Highlights highlights) {
+        sendMessage(JsonUtils.envelope(SHOW_HIGHLIGHTS, json(highlights)));
+    }
 
     // Sends the specified data to the client.
     protected synchronized void sendMessage(ObjectNode data) {
@@ -591,7 +557,7 @@
 
     private void cancelAllRequests() {
         stopSummaryMonitoring();
-        stopTrafficMonitoring();
+        tmo.stop();
     }
 
     // Sends all controller nodes to the client as node-added messages.
@@ -641,18 +607,6 @@
         }
     }
 
-
-    private synchronized void startMonitoringIntent(Intent intent) {
-        selectedHosts = new HashSet<>();
-        selectedDevices = new HashSet<>();
-        selectedIntents = new ArrayList<>();
-        selectedIntents.add(intent);
-        currentIntentIndex = -1;
-        requestAnotherRelatedIntent(+1);
-        requestSelectedIntentTraffic();
-    }
-
-
     private Set<ConnectPoint> getHostLocations(Set<HostId> hostIds) {
         Set<ConnectPoint> points = new HashSet<>();
         for (HostId hostId : hostIds) {
@@ -675,121 +629,10 @@
     }
 
 
-    private synchronized void startTrafficMonitoring() {
-        stopTrafficMonitoring();
-        trafficTask = new TrafficMonitor();
-        timer.schedule(trafficTask, TRAFFIC_FREQUENCY, TRAFFIC_FREQUENCY);
-    }
-
-    private synchronized void stopTrafficMonitoring() {
-        if (trafficTask != null) {
-            trafficTask.cancel();
-            trafficTask = null;
-        }
-    }
-
-    // Subscribes for flow traffic messages.
-    private synchronized void requestAllFlowTraffic() {
-        startTrafficMonitoring();
-        sendMessage(trafficSummaryMessage(StatsType.FLOW));
-    }
-
-    // Subscribes for port traffic messages.
-    private synchronized void requestAllPortTraffic() {
-        startTrafficMonitoring();
-        sendMessage(trafficSummaryMessage(StatsType.PORT));
-    }
-
-    private void requestDeviceLinkFlows(ObjectNode payload) {
-        startTrafficMonitoring();
-
-        // 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 = JsonUtils.string(payload, HOVER);
-        if (!isNullOrEmpty(hover)) {
-            addHover(hosts, devices, hover);
-        }
-        sendMessage(flowSummaryMessage(devices));
-    }
-
-
-    private boolean haveSelectedIntents() {
-        return selectedIntents != null && !selectedIntents.isEmpty();
-    }
-
-    // Processes the selection extended with hovered item to segregate items
-    // into primary (those including the hover) vs secondary highlights.
-    private void processHoverExtendedSelection(long sid, String hover) {
-        Set<Host> hoverSelHosts = new HashSet<>(selectedHosts);
-        Set<Device> hoverSelDevices = new HashSet<>(selectedDevices);
-        addHover(hoverSelHosts, hoverSelDevices, hover);
-
-        List<Intent> primary = selectedIntents == null ? new ArrayList<>() :
-                intentFilter.findPathIntents(hoverSelHosts, hoverSelDevices,
-                                             selectedIntents);
-        Set<Intent> secondary = new HashSet<>(selectedIntents);
-        secondary.removeAll(primary);
-
-        // Send a message to highlight all links of all monitored intents.
-        sendMessage(trafficMessage(new TrafficClass(PRIMARY, primary),
-                                   new TrafficClass(SECONDARY, secondary)));
-    }
-
-    // Requests next or previous related intent.
-    private void requestAnotherRelatedIntent(int offset) {
-        if (haveSelectedIntents()) {
-            currentIntentIndex = currentIntentIndex + offset;
-            if (currentIntentIndex < 0) {
-                currentIntentIndex = selectedIntents.size() - 1;
-            } else if (currentIntentIndex >= selectedIntents.size()) {
-                currentIntentIndex = 0;
-            }
-            sendSelectedIntent();
-        }
-    }
-
-    // Sends traffic information on the related intents with the currently
-    // selected intent highlighted.
-    private void sendSelectedIntent() {
-        Intent selectedIntent = selectedIntents.get(currentIntentIndex);
-        log.debug("Requested next intent {}", selectedIntent.id());
-
-        Set<Intent> primary = new HashSet<>();
-        primary.add(selectedIntent);
-
-        Set<Intent> secondary = new HashSet<>(selectedIntents);
-        secondary.remove(selectedIntent);
-
-        // Send a message to highlight all links of the selected intent.
-        sendMessage(trafficMessage(new TrafficClass(PRIMARY, primary),
-                                   new TrafficClass(SECONDARY, secondary)));
-    }
-
-    // Requests monitoring of traffic for the selected intent.
-    private void requestSelectedIntentTraffic() {
-        if (haveSelectedIntents()) {
-            if (currentIntentIndex < 0) {
-                currentIntentIndex = 0;
-            }
-            Intent selectedIntent = selectedIntents.get(currentIntentIndex);
-            log.debug("Requested traffic for selected {}", selectedIntent.id());
-
-            Set<Intent> primary = new HashSet<>();
-            primary.add(selectedIntent);
-
-            // Send a message to highlight all links of the selected intent.
-            sendMessage(trafficMessage(new TrafficClass(PRIMARY, primary, true)));
-        }
-    }
-
     private synchronized void startSummaryMonitoring() {
         stopSummaryMonitoring();
         summaryTask = new SummaryMonitor();
-        timer.schedule(summaryTask, SUMMARY_FREQUENCY, SUMMARY_FREQUENCY);
+        timer.schedule(summaryTask, SUMMARY_PERIOD, SUMMARY_PERIOD);
         summaryRunning = true;
     }
 
@@ -883,9 +726,7 @@
     private class InternalIntentListener implements IntentListener {
         @Override
         public void event(IntentEvent event) {
-            if (trafficTask != null) {
-                msgSender.execute(TopologyViewMessageHandler.this::requestSelectedIntentTraffic);
-            }
+            msgSender.execute(tmo::pokeIntent);
             eventAccummulator.add(event);
         }
     }
@@ -898,51 +739,8 @@
         }
     }
 
-    // encapsulate
-    private static class TrafficEvent {
-        enum Type {
-            ALL_FLOW_TRAFFIC, ALL_PORT_TRAFFIC, DEV_LINK_FLOWS, SEL_INTENT
-        }
 
-        private final Type type;
-        private final ObjectNode payload;
-
-        TrafficEvent(Type type, ObjectNode payload) {
-            this.type = type;
-            this.payload = payload;
-        }
-    }
-
-    // Periodic update of the traffic information
-    private class TrafficMonitor extends TimerTask {
-        @Override
-        public void run() {
-            try {
-                if (trafficEvent != null) {
-                    switch (trafficEvent.type) {
-                        case ALL_FLOW_TRAFFIC:
-                            requestAllFlowTraffic();
-                            break;
-                        case ALL_PORT_TRAFFIC:
-                            requestAllPortTraffic();
-                            break;
-                        case DEV_LINK_FLOWS:
-                            requestDeviceLinkFlows(trafficEvent.payload);
-                            break;
-                        case SEL_INTENT:
-                            requestSelectedIntentTraffic();
-                            break;
-                        default:
-                            // nothing to do
-                            break;
-                    }
-                }
-            } catch (Exception e) {
-                log.warn("Unable to handle traffic request due to {}", e.getMessage());
-                log.warn("Boom!", e);
-            }
-        }
-    }
+    // === SUMMARY MONITORING
 
     // Periodic update of the summary information
     private class SummaryMonitor extends TimerTask {
@@ -967,7 +765,7 @@
 
         @Override
         public void processItems(List<Event> items) {
-            // Start-of-Debugging
+            // Start-of-Debugging -- Keep in until ONOS-2572 is fixed for reals
             long now = System.currentTimeMillis();
             String me = this.toString();
             String miniMe = me.replaceAll("^.*@", "me@");
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
index 9265e5f..130f88f 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
@@ -18,7 +18,6 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.google.common.collect.ImmutableList;
 import org.onlab.osgi.ServiceDirectory;
 import org.onlab.packet.IpAddress;
 import org.onosproject.cluster.ClusterEvent;
@@ -43,8 +42,6 @@
 import org.onosproject.net.HostId;
 import org.onosproject.net.HostLocation;
 import org.onosproject.net.Link;
-import org.onosproject.net.LinkKey;
-import org.onosproject.net.NetworkResource;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.device.DeviceEvent;
 import org.onosproject.net.device.DeviceService;
@@ -55,29 +52,26 @@
 import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
 import org.onosproject.net.host.HostEvent;
 import org.onosproject.net.host.HostService;
-import org.onosproject.net.intent.FlowRuleIntent;
-import org.onosproject.net.intent.Intent;
 import org.onosproject.net.intent.IntentService;
-import org.onosproject.net.intent.LinkCollectionIntent;
-import org.onosproject.net.intent.OpticalConnectivityIntent;
-import org.onosproject.net.intent.OpticalPathIntent;
-import org.onosproject.net.intent.PathIntent;
 import org.onosproject.net.link.LinkEvent;
 import org.onosproject.net.link.LinkService;
 import org.onosproject.net.provider.ProviderId;
-import org.onosproject.net.statistic.Load;
 import org.onosproject.net.statistic.StatisticService;
 import org.onosproject.net.topology.Topology;
 import org.onosproject.net.topology.TopologyService;
 import org.onosproject.ui.JsonUtils;
 import org.onosproject.ui.UiConnection;
 import org.onosproject.ui.UiMessageHandler;
+import org.onosproject.ui.impl.topo.ServicesBundle;
 import org.onosproject.ui.topo.ButtonId;
+import org.onosproject.ui.topo.DeviceHighlight;
+import org.onosproject.ui.topo.Highlights;
+import org.onosproject.ui.topo.HostHighlight;
+import org.onosproject.ui.topo.LinkHighlight;
 import org.onosproject.ui.topo.PropertyPanel;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -95,10 +89,6 @@
 import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_REMOVED;
 import static org.onosproject.cluster.ControllerNode.State.ACTIVE;
 import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
-import static org.onosproject.net.DeviceId.deviceId;
-import static org.onosproject.net.HostId.hostId;
-import static org.onosproject.net.LinkKey.linkKey;
-import static org.onosproject.net.PortNumber.P0;
 import static org.onosproject.net.PortNumber.portNumber;
 import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED;
 import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_REMOVED;
@@ -106,8 +96,7 @@
 import static org.onosproject.net.host.HostEvent.Type.HOST_REMOVED;
 import static org.onosproject.net.link.LinkEvent.Type.LINK_ADDED;
 import static org.onosproject.net.link.LinkEvent.Type.LINK_REMOVED;
-import static org.onosproject.ui.impl.TopologyViewMessageHandlerBase.StatsType.FLOW;
-import static org.onosproject.ui.impl.TopologyViewMessageHandlerBase.StatsType.PORT;
+import static org.onosproject.ui.impl.topo.TopoUtils.compactLinkString;
 import static org.onosproject.ui.topo.TopoConstants.CoreButtons;
 import static org.onosproject.ui.topo.TopoConstants.Properties;
 
@@ -121,24 +110,8 @@
 
     private static final ProviderId PID =
             new ProviderId("core", "org.onosproject.core", true);
-    private static final String COMPACT = "%s/%s-%s/%s";
 
-    private static final String SHOW_HIGHLIGHTS = "showHighlights";
-
-    private static final double KILO = 1024;
-    private static final double MEGA = 1024 * KILO;
-    private static final double GIGA = 1024 * MEGA;
-
-    private static final String GBITS_UNIT = "Gb";
-    private static final String MBITS_UNIT = "Mb";
-    private static final String KBITS_UNIT = "Kb";
-    private static final String BITS_UNIT = "b";
-    private static final String GBYTES_UNIT = "GB";
-    private static final String MBYTES_UNIT = "MB";
-    private static final String KBYTES_UNIT = "KB";
-    private static final String BYTES_UNIT = "B";
-    //4 Kilo Bytes as threshold
-    private static final double BPS_THRESHOLD = 4 * KILO;
+    protected static final String SHOW_HIGHLIGHTS = "showHighlights";
 
     protected ServiceDirectory directory;
     protected ClusterService clusterService;
@@ -153,9 +126,7 @@
     protected TopologyService topologyService;
     protected TunnelService tunnelService;
 
-    protected enum StatsType {
-        FLOW, PORT
-    }
+    protected ServicesBundle servicesBundle;
 
     private String version;
 
@@ -187,6 +158,11 @@
         topologyService = directory.get(TopologyService.class);
         tunnelService = directory.get(TunnelService.class);
 
+        servicesBundle = new ServicesBundle(intentService, deviceService,
+                                            hostService, linkService,
+                                            flowService,
+                                            flowStatsService, portStatsService);
+
         String ver = directory.get(CoreService.class).version().toString();
         version = ver.replace(".SNAPSHOT", "*").replaceFirst("~.*$", "");
     }
@@ -232,64 +208,6 @@
         return JsonUtils.envelope("message", id, payload);
     }
 
-    // Produces a set of all hosts listed in the specified JSON array.
-    protected Set<Host> getHosts(ArrayNode array) {
-        Set<Host> hosts = new HashSet<>();
-        if (array != null) {
-            for (JsonNode node : array) {
-                try {
-                    addHost(hosts, hostId(node.asText()));
-                } catch (IllegalArgumentException e) {
-                    log.debug("Skipping ID {}", node.asText());
-                }
-            }
-        }
-        return hosts;
-    }
-
-    // Adds the specified host to the set of hosts.
-    private void addHost(Set<Host> hosts, HostId hostId) {
-        Host host = hostService.getHost(hostId);
-        if (host != null) {
-            hosts.add(host);
-        }
-    }
-
-
-    // Produces a set of all devices listed in the specified JSON array.
-    protected Set<Device> getDevices(ArrayNode array) {
-        Set<Device> devices = new HashSet<>();
-        if (array != null) {
-            for (JsonNode node : array) {
-                try {
-                    addDevice(devices, deviceId(node.asText()));
-                } catch (IllegalArgumentException e) {
-                    log.debug("Skipping ID {}", node.asText());
-                }
-            }
-        }
-        return devices;
-    }
-
-    private void addDevice(Set<Device> devices, DeviceId deviceId) {
-        Device device = deviceService.getDevice(deviceId);
-        if (device != null) {
-            devices.add(device);
-        }
-    }
-
-    protected void addHover(Set<Host> hosts, Set<Device> devices, String hover) {
-        try {
-            addHost(hosts, hostId(hover));
-        } catch (IllegalArgumentException e) {
-            try {
-                addDevice(devices, deviceId(hover));
-            } catch (IllegalArgumentException ne) {
-                log.debug("Skipping ID {}", hover);
-            }
-        }
-    }
-
     // Produces a cluster instance message to the client.
     protected ObjectNode instanceMessage(ClusterEvent event, String messageType) {
         ControllerNode node = event.subject();
@@ -445,6 +363,7 @@
                    JsonUtils.node(payload, "memento"));
     }
 
+
     // -----------------------------------------------------------------------
     // Create models of the data to return, that overlays can adjust / augment
 
@@ -527,24 +446,24 @@
         return count;
     }
 
-    // Counts all entries that egress on the given device links.
-    protected Map<Link, Integer> getFlowCounts(DeviceId deviceId) {
+    // Counts all flow entries that egress on the links of the given device.
+    private Map<Link, Integer> getLinkFlowCounts(DeviceId deviceId) {
+        // get the flows for the device
         List<FlowEntry> entries = new ArrayList<>();
-        Set<Link> links = new HashSet<>(linkService.getDeviceEgressLinks(deviceId));
-        Set<Host> hosts = hostService.getConnectedHosts(deviceId);
         for (FlowEntry flowEntry : flowService.getFlowEntries(deviceId)) {
             entries.add(flowEntry);
         }
 
-        // Add all edge links to the set
+        // get egress links from device, and include edge links
+        Set<Link> links = new HashSet<>(linkService.getDeviceEgressLinks(deviceId));
+        Set<Host> hosts = hostService.getConnectedHosts(deviceId);
         if (hosts != null) {
             for (Host host : hosts) {
-                links.add(new DefaultEdgeLink(host.providerId(),
-                                              new ConnectPoint(host.id(), P0),
-                                              host.location(), false));
+                links.add(createEdgeLink(host, false));
             }
         }
 
+        // compile flow counts per link
         Map<Link, Integer> counts = new HashMap<>();
         for (Link link : links) {
             counts.put(link, getEgressFlows(link, entries));
@@ -553,7 +472,7 @@
     }
 
     // Counts all entries that egress on the link source port.
-    private Integer getEgressFlows(Link link, List<FlowEntry> entries) {
+    private int getEgressFlows(Link link, List<FlowEntry> entries) {
         int count = 0;
         PortNumber out = link.src().port();
         for (FlowEntry entry : entries) {
@@ -568,7 +487,6 @@
         return count;
     }
 
-
     // Returns host details response.
     protected PropertyPanel hostDetails(HostId hostId, long sid) {
         Host host = hostService.getHost(hostId);
@@ -589,270 +507,98 @@
             .addProp(Properties.LATITUDE, annot.value(AnnotationKeys.LATITUDE))
             .addProp(Properties.LONGITUDE, annot.value(AnnotationKeys.LONGITUDE));
 
-        // TODO: add button descriptors
+        // Potentially add button descriptors here
         return pp;
     }
 
 
-    // TODO: migrate to Traffic overlay
-    // Produces JSON message to trigger flow traffic overview visualization
-    protected ObjectNode trafficSummaryMessage(StatsType type) {
+    // ----------------------------------------------------------------------
+
+    /**
+     * Transforms the given highlights model into a JSON message payload.
+     *
+     * @param highlights the model to transform
+     * @return JSON payload
+     */
+    protected ObjectNode json(Highlights highlights) {
         ObjectNode payload = objectNode();
-        ArrayNode paths = arrayNode();
-        payload.set("paths", paths);
 
-        ObjectNode pathNodeN = objectNode();
-        ArrayNode linksNodeN = arrayNode();
-        ArrayNode labelsN = arrayNode();
+        ArrayNode devices = arrayNode();
+        ArrayNode hosts = arrayNode();
+        ArrayNode links = arrayNode();
 
-        pathNodeN.put("class", "plain").put("traffic", false);
-        pathNodeN.set("links", linksNodeN);
-        pathNodeN.set("labels", labelsN);
-        paths.add(pathNodeN);
+        payload.set("devices", devices);
+        payload.set("hosts", hosts);
+        payload.set("links", links);
 
-        ObjectNode pathNodeT = objectNode();
-        ArrayNode linksNodeT = arrayNode();
-        ArrayNode labelsT = arrayNode();
+        highlights.devices().forEach(dh -> devices.add(json(dh)));
+        highlights.hosts().forEach(hh -> hosts.add(json(hh)));
+        jsonifyLinks(links, highlights.links());
 
-        pathNodeT.put("class", "secondary").put("traffic", true);
-        pathNodeT.set("links", linksNodeT);
-        pathNodeT.set("labels", labelsT);
-        paths.add(pathNodeT);
-
-        Map<LinkKey, BiLink> biLinks = consolidateLinks(linkService.getLinks());
-        addEdgeLinks(biLinks);
-        for (BiLink link : biLinks.values()) {
-            boolean bi = link.two != null;
-            if (type == FLOW) {
-                link.addLoad(getLinkLoad(link.one));
-                link.addLoad(bi ? getLinkLoad(link.two) : null);
-            } else if (type == PORT) {
-                //For a bi-directional traffic links, use
-                //the max link rate of either direction
-                link.addLoad(portStatsService.load(link.one.src()),
-                             BPS_THRESHOLD,
-                             portStatsService.load(link.one.dst()),
-                             BPS_THRESHOLD);
-            }
-            if (link.hasTraffic) {
-                linksNodeT.add(compactLinkString(link.one));
-                labelsT.add(type == PORT ?
-                                    formatBitRate(link.rate) + "ps" :
-                                    formatBytes(link.bytes));
-            } else {
-                linksNodeN.add(compactLinkString(link.one));
-                labelsN.add("");
-            }
-        }
-        return JsonUtils.envelope(SHOW_HIGHLIGHTS, 0, payload);
+        return payload;
     }
 
-    private Load getLinkLoad(Link link) {
-        if (link.src().elementId() instanceof DeviceId) {
-            return flowStatsService.load(link);
-        }
-        return null;
-    }
+    private void jsonifyLinks(ArrayNode links, Set<LinkHighlight> hilites) {
+        // a little more complicated than devices or hosts, since we are
+        //  grouping the link highlights by CSS classes
 
-    private void addEdgeLinks(Map<LinkKey, BiLink> biLinks) {
-        hostService.getHosts().forEach(host -> {
-            addLink(biLinks, createEdgeLink(host, true));
-            addLink(biLinks, createEdgeLink(host, false));
-        });
-    }
-
-    private Map<LinkKey, BiLink> consolidateLinks(Iterable<Link> links) {
-        Map<LinkKey, BiLink> biLinks = new HashMap<>();
-        for (Link link : links) {
-            addLink(biLinks, link);
-        }
-        return biLinks;
-    }
-
-    // Produces JSON message to trigger flow overview visualization
-    protected ObjectNode flowSummaryMessage(Set<Device> devices) {
-        ObjectNode payload = objectNode();
-        ArrayNode paths = arrayNode();
-        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 JsonUtils.envelope(SHOW_HIGHLIGHTS, 0, payload);
-    }
-
-    private void addLinkFlows(Link link, ArrayNode paths, Integer count) {
-        ObjectNode pathNode = objectNode();
-        ArrayNode linksNode = arrayNode();
-        ArrayNode labels = arrayNode();
-        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);
-    }
+        // TODO: refactor this method (including client side) to use new format
+        //       as a more compact representation of the data...
+        // * links:
+        //   * "primary animated":
+        //     * "link01" -> "label"
+        //     * "link02" -> "label"
+        //   * "secondary":
+        //     * "link04" -> "label"
+        //     * "link05" -> ""
 
 
-    // Produces JSON message to trigger traffic visualization
-    protected ObjectNode trafficMessage(TrafficClass... trafficClasses) {
-        ObjectNode payload = objectNode();
-        ArrayNode paths = arrayNode();
-        payload.set("paths", paths);
+        Map<String, List<String>> linkIdMap = new HashMap<>();
+        Map<String, List<String>> linkLabelMap = new HashMap<>();
+        List<String> ids;
+        List<String> labels;
 
-        // Classify links based on their traffic traffic first...
-        Map<LinkKey, BiLink> biLinks = classifyLinkTraffic(trafficClasses);
+        for (LinkHighlight lh : hilites) {
+            String cls = lh.cssClasses();
+            ids = linkIdMap.get(cls);
+            labels = linkLabelMap.get(cls);
 
-        // Then separate the links into their respective classes and send them out.
-        Map<String, ObjectNode> pathNodes = new HashMap<>();
-        for (BiLink biLink : biLinks.values()) {
-            boolean hasTraffic = biLink.hasTraffic;
-            String tc = (biLink.classes() + (hasTraffic ? " animated" : "")).trim();
-            ObjectNode pathNode = pathNodes.get(tc);
-            if (pathNode == null) {
-                pathNode = objectNode()
-                        .put("class", tc).put("traffic", hasTraffic);
-                pathNode.set("links", arrayNode());
-                pathNode.set("labels", arrayNode());
-                pathNodes.put(tc, pathNode);
-                paths.add(pathNode);
-            }
-            ((ArrayNode) pathNode.path("links")).add(compactLinkString(biLink.one));
-            ((ArrayNode) pathNode.path("labels")).add(hasTraffic ? formatBytes(biLink.bytes) : "");
-        }
-
-        return JsonUtils.envelope(SHOW_HIGHLIGHTS, 0, payload);
-    }
-
-    // Classifies the link traffic according to the specified classes.
-    private Map<LinkKey, BiLink> classifyLinkTraffic(TrafficClass... trafficClasses) {
-        Map<LinkKey, BiLink> biLinks = new HashMap<>();
-        for (TrafficClass trafficClass : trafficClasses) {
-            for (Intent intent : trafficClass.intents) {
-                boolean isOptical = intent instanceof OpticalConnectivityIntent;
-                List<Intent> installables = intentService.getInstallableIntents(intent.key());
-                if (installables != null) {
-                    for (Intent installable : installables) {
-                        String type = isOptical ? trafficClass.type + " optical" : trafficClass.type;
-                        if (installable instanceof PathIntent) {
-                            classifyLinks(type, biLinks, trafficClass.showTraffic,
-                                          ((PathIntent) installable).path().links());
-                        } else if (installable instanceof FlowRuleIntent) {
-                            classifyLinks(type, biLinks, trafficClass.showTraffic,
-                                          linkResources(installable));
-                        } else if (installable instanceof LinkCollectionIntent) {
-                            classifyLinks(type, biLinks, trafficClass.showTraffic,
-                                          ((LinkCollectionIntent) installable).links());
-                        } else if (installable instanceof OpticalPathIntent) {
-                            classifyLinks(type, biLinks, trafficClass.showTraffic,
-                                          ((OpticalPathIntent) installable).path().links());
-                        }
-                    }
-                }
-            }
-        }
-        return biLinks;
-    }
-
-    // Extracts links from the specified flow rule intent resources
-    private Collection<Link> linkResources(Intent installable) {
-        ImmutableList.Builder<Link> builder = ImmutableList.builder();
-        for (NetworkResource r : installable.resources()) {
-            if (r instanceof Link) {
-                builder.add((Link) r);
-            }
-        }
-        return builder.build();
-    }
-
-
-    // Adds the link segments (path or tree) associated with the specified
-    // connectivity intent
-    private void classifyLinks(String type, Map<LinkKey, BiLink> biLinks,
-                               boolean showTraffic, Iterable<Link> links) {
-        if (links != null) {
-            for (Link link : links) {
-                BiLink biLink = addLink(biLinks, link);
-                if (showTraffic) {
-                    biLink.addLoad(getLinkLoad(link));
-                }
-                biLink.addClass(type);
-            }
-        }
-    }
-
-
-    static BiLink addLink(Map<LinkKey, BiLink> biLinks, Link link) {
-        LinkKey key = canonicalLinkKey(link);
-        BiLink biLink = biLinks.get(key);
-        if (biLink != null) {
-            biLink.setOther(link);
-        } else {
-            biLink = new BiLink(key, link);
-            biLinks.put(key, biLink);
-        }
-        return biLink;
-    }
-
-    // Poor-mans formatting to get the labels with byte counts looking nice.
-    private String formatBytes(long bytes) {
-        String unit;
-        double value;
-        if (bytes > GIGA) {
-            value = bytes / GIGA;
-            unit = GBYTES_UNIT;
-        } else if (bytes > MEGA) {
-            value = bytes / MEGA;
-            unit = MBYTES_UNIT;
-        } else if (bytes > KILO) {
-            value = bytes / KILO;
-            unit = KBYTES_UNIT;
-        } else {
-            value = bytes;
-            unit = BYTES_UNIT;
-        }
-        DecimalFormat format = new DecimalFormat("#,###.##");
-        return format.format(value) + " " + unit;
-    }
-
-    // Poor-mans formatting to get the labels with bit rate looking nice.
-    private String formatBitRate(long bytes) {
-        String unit;
-        double value;
-        //Convert to bits
-        long bits = bytes * 8;
-        if (bits > GIGA) {
-            value = bits / GIGA;
-            unit = GBITS_UNIT;
-
-            // NOTE: temporary hack to clip rate at 10.0 Gbps
-            //  Added for the CORD Fabric demo at ONS 2015
-            if (value > 10.0) {
-                value = 10.0;
+            if (ids == null) {  // labels will be null also
+                ids = new ArrayList<>();
+                linkIdMap.put(cls, ids);
+                labels = new ArrayList<>();
+                linkLabelMap.put(cls, labels);
             }
 
-        } else if (bits > MEGA) {
-            value = bits / MEGA;
-            unit = MBITS_UNIT;
-        } else if (bits > KILO) {
-            value = bits / KILO;
-            unit = KBITS_UNIT;
-        } else {
-            value = bits;
-            unit = BITS_UNIT;
+            ids.add(lh.elementId());
+            labels.add(lh.label());
         }
-        DecimalFormat format = new DecimalFormat("#,###.##");
-        return format.format(value) + " " + unit;
+
+        for (String cls : linkIdMap.keySet()) {
+            ObjectNode group = objectNode();
+            links.add(group);
+
+            group.put("class", cls);
+
+            ArrayNode lnks = arrayNode();
+            ArrayNode labs = arrayNode();
+            group.set("links", lnks);
+            group.set("labels", labs);
+
+            linkIdMap.get(cls).forEach(lnks::add);
+            linkLabelMap.get(cls).forEach(labs::add);
+        }
     }
 
-    // Produces compact string representation of a link.
-    private static String compactLinkString(Link link) {
-        return String.format(COMPACT, link.src().elementId(), link.src().port(),
-                             link.dst().elementId(), link.dst().port());
+
+    protected ObjectNode json(DeviceHighlight dh) {
+        // TODO: implement this once we know what a device highlight looks like
+        return objectNode();
+    }
+
+    protected ObjectNode json(HostHighlight hh) {
+        // TODO: implement this once we know what a host highlight looks like
+        return objectNode();
     }
 
     // translates the property panel into JSON, for returning to the client
@@ -879,95 +625,4 @@
         return result;
     }
 
-
-    // Produces canonical link key, i.e. one that will match link and its inverse.
-    static LinkKey canonicalLinkKey(Link link) {
-        String sn = link.src().elementId().toString();
-        String dn = link.dst().elementId().toString();
-        return sn.compareTo(dn) < 0 ?
-                linkKey(link.src(), link.dst()) : linkKey(link.dst(), link.src());
-    }
-
-    // Representation of link and its inverse and any traffic data.
-    static class BiLink {
-        public final LinkKey key;
-        public final Link one;
-        public Link two;
-        public boolean hasTraffic = false;
-        public long bytes = 0;
-
-        private Set<String> classes = new HashSet<>();
-        private long rate;
-
-        BiLink(LinkKey key, Link link) {
-            this.key = key;
-            this.one = link;
-        }
-
-        void setOther(Link link) {
-            this.two = link;
-        }
-
-        void addLoad(Load load) {
-            addLoad(load, 0);
-        }
-
-        void addLoad(Load load, double threshold) {
-            if (load != null) {
-                this.hasTraffic = hasTraffic || load.rate() > threshold;
-                this.bytes += load.latest();
-                this.rate += load.rate();
-            }
-        }
-
-        void addLoad(Load srcLinkLoad,
-                     double srcLinkThreshold,
-                     Load dstLinkLoad,
-                     double dstLinkThreshold) {
-            //use the max of link load at source or destination
-            if (srcLinkLoad != null) {
-                this.hasTraffic = hasTraffic || srcLinkLoad.rate() > srcLinkThreshold;
-                this.bytes = srcLinkLoad.latest();
-                this.rate = srcLinkLoad.rate();
-            }
-
-            if (dstLinkLoad != null) {
-                if (dstLinkLoad.rate() > this.rate) {
-                    this.bytes = dstLinkLoad.latest();
-                    this.rate = dstLinkLoad.rate();
-                    this.hasTraffic = hasTraffic || dstLinkLoad.rate() > dstLinkThreshold;
-                }
-            }
-        }
-
-        void addClass(String trafficClass) {
-            classes.add(trafficClass);
-        }
-
-        String classes() {
-            StringBuilder sb = new StringBuilder();
-            classes.forEach(c -> sb.append(c).append(" "));
-            return sb.toString().trim();
-        }
-    }
-
-
-    // TODO: move this to traffic overlay component
-    // Auxiliary carrier of data for requesting traffic message.
-    static class TrafficClass {
-        public final boolean showTraffic;
-        public final String type;
-        public final Iterable<Intent> intents;
-
-        TrafficClass(String type, Iterable<Intent> intents) {
-            this(type, intents, false);
-        }
-
-        TrafficClass(String type, Iterable<Intent> intents, boolean showTraffic) {
-            this.type = type;
-            this.intents = intents;
-            this.showTraffic = showTraffic;
-        }
-    }
-
 }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorObject.java b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorObject.java
new file mode 100644
index 0000000..0826655
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorObject.java
@@ -0,0 +1,594 @@
+/*
+ * 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.impl;
+
+import com.google.common.collect.ImmutableList;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
+import org.onosproject.net.intent.FlowRuleIntent;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.LinkCollectionIntent;
+import org.onosproject.net.intent.OpticalConnectivityIntent;
+import org.onosproject.net.intent.OpticalPathIntent;
+import org.onosproject.net.intent.PathIntent;
+import org.onosproject.net.statistic.Load;
+import org.onosproject.ui.impl.topo.BiLink;
+import org.onosproject.ui.impl.topo.IntentSelection;
+import org.onosproject.ui.impl.topo.LinkStatsType;
+import org.onosproject.ui.impl.topo.NodeSelection;
+import org.onosproject.ui.impl.topo.ServicesBundle;
+import org.onosproject.ui.impl.topo.TopoUtils;
+import org.onosproject.ui.impl.topo.TopologyViewIntentFilter;
+import org.onosproject.ui.impl.topo.TrafficClass;
+import org.onosproject.ui.topo.Highlights;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
+import static org.onosproject.ui.impl.TrafficMonitorObject.Mode.IDLE;
+import static org.onosproject.ui.impl.TrafficMonitorObject.Mode.SEL_INTENT;
+import static org.onosproject.ui.topo.LinkHighlight.Flavor.PRIMARY_HIGHLIGHT;
+import static org.onosproject.ui.topo.LinkHighlight.Flavor.SECONDARY_HIGHLIGHT;
+
+/**
+ * Encapsulates the behavior of monitoring specific traffic patterns.
+ */
+public class TrafficMonitorObject {
+
+    // 4 Kilo Bytes as threshold
+    private static final double BPS_THRESHOLD = 4 * TopoUtils.KILO;
+
+    private static final Logger log =
+            LoggerFactory.getLogger(TrafficMonitorObject.class);
+
+    /**
+     * Designates the different modes of operation.
+     */
+    public enum Mode {
+        IDLE,
+        ALL_FLOW_TRAFFIC,
+        ALL_PORT_TRAFFIC,
+        DEV_LINK_FLOWS,
+        RELATED_INTENTS,
+        SEL_INTENT
+    }
+
+    private final long trafficPeriod;
+    private final ServicesBundle servicesBundle;
+    private final TopologyViewMessageHandler messageHandler;
+    private final TopologyViewIntentFilter intentFilter;
+
+    private final Timer timer = new Timer("topo-traffic");
+
+    private TimerTask trafficTask = null;
+    private Mode mode = IDLE;
+    private NodeSelection selectedNodes = null;
+    private IntentSelection selectedIntents = null;
+
+
+    /**
+     * Constructs a traffic monitor.
+     *
+     * @param trafficPeriod   traffic task period in ms
+     * @param servicesBundle  bundle of services
+     * @param messageHandler  our message handler
+     */
+    public TrafficMonitorObject(long trafficPeriod,
+                                ServicesBundle servicesBundle,
+                                TopologyViewMessageHandler messageHandler) {
+        this.trafficPeriod = trafficPeriod;
+        this.servicesBundle = servicesBundle;
+        this.messageHandler = messageHandler;
+
+        intentFilter = new TopologyViewIntentFilter(servicesBundle);
+    }
+
+    // =======================================================================
+    // === API === // TODO: add javadocs
+
+    public synchronized void monitor(Mode mode) {
+        log.debug("monitor: {}", mode);
+        this.mode = mode;
+
+        switch (mode) {
+            case ALL_FLOW_TRAFFIC:
+                clearSelection();
+                scheduleTask();
+                sendAllFlowTraffic();
+                break;
+
+            case ALL_PORT_TRAFFIC:
+                clearSelection();
+                scheduleTask();
+                sendAllPortTraffic();
+                break;
+
+            case SEL_INTENT:
+                scheduleTask();
+                sendSelectedIntentTraffic();
+                break;
+
+            default:
+                log.debug("Unexpected call to monitor({})", mode);
+                clearAll();
+                break;
+        }
+    }
+
+    public synchronized void monitor(Mode mode, NodeSelection nodeSelection) {
+        log.debug("monitor: {} -- {}", mode, nodeSelection);
+        this.mode = mode;
+        this.selectedNodes = nodeSelection;
+
+        switch (mode) {
+            case DEV_LINK_FLOWS:
+                // only care about devices (not hosts)
+                if (selectedNodes.devices().isEmpty()) {
+                    sendClearAll();
+                } else {
+                    scheduleTask();
+                    sendDeviceLinkFlows();
+                }
+                break;
+
+            case RELATED_INTENTS:
+                if (selectedNodes.none()) {
+                    sendClearAll();
+                } else {
+                    selectedIntents = new IntentSelection(selectedNodes, intentFilter);
+                    if (selectedIntents.none()) {
+                        sendClearAll();
+                    } else {
+                        sendSelectedIntents();
+                    }
+                }
+                break;
+
+            default:
+                log.debug("Unexpected call to monitor({}, {})", mode, nodeSelection);
+                clearAll();
+                break;
+        }
+    }
+
+    public synchronized void monitor(Intent intent) {
+        log.debug("monitor intent: {}", intent.id());
+        selectedNodes = null;
+        selectedIntents = new IntentSelection(intent);
+        mode = SEL_INTENT;
+        scheduleTask();
+        sendSelectedIntentTraffic();
+    }
+
+    public synchronized void selectNextIntent() {
+        if (selectedIntents != null) {
+            selectedIntents.next();
+            sendSelectedIntents();
+        }
+    }
+
+    public synchronized void selectPreviousIntent() {
+        if (selectedIntents != null) {
+            selectedIntents.prev();
+            sendSelectedIntents();
+        }
+    }
+
+    public synchronized void pokeIntent() {
+        if (mode == SEL_INTENT) {
+            sendSelectedIntentTraffic();
+        }
+    }
+
+    public synchronized void stop() {
+        log.debug("STOP");
+        if (mode != IDLE) {
+            sendClearAll();
+        }
+    }
+
+
+    // =======================================================================
+    // === Helper methods ===
+
+    private void sendClearAll() {
+        clearAll();
+        sendClearHighlights();
+    }
+
+    private void clearAll() {
+        this.mode = IDLE;
+        clearSelection();
+        cancelTask();
+    }
+
+    private void clearSelection() {
+        selectedNodes = null;
+        selectedIntents = null;
+    }
+
+    private synchronized void  scheduleTask() {
+        if (trafficTask == null) {
+            log.debug("Starting up background traffic task...");
+            trafficTask = new TrafficMonitor();
+            timer.schedule(trafficTask, trafficPeriod, trafficPeriod);
+        } else {
+            // TEMPORARY until we are sure this is working correctly
+            log.debug("(traffic task already running)");
+        }
+    }
+
+    private synchronized void cancelTask() {
+        if (trafficTask != null) {
+            trafficTask.cancel();
+            trafficTask = null;
+        }
+    }
+
+    // ---
+
+    private void sendAllFlowTraffic() {
+        log.debug("sendAllFlowTraffic");
+        sendHighlights(trafficSummary(LinkStatsType.FLOW_STATS));
+    }
+
+    private void sendAllPortTraffic() {
+        log.debug("sendAllPortTraffic");
+        sendHighlights(trafficSummary(LinkStatsType.PORT_STATS));
+    }
+
+    private void sendDeviceLinkFlows() {
+        log.debug("sendDeviceLinkFlows: {}", selectedNodes);
+        sendHighlights(deviceLinkFlows());
+    }
+
+    private void sendSelectedIntents() {
+        log.debug("sendSelectedIntents: {}", selectedIntents);
+        sendHighlights(intentGroup());
+    }
+
+    private void sendSelectedIntentTraffic() {
+        log.debug("sendSelectedIntentTraffic: {}", selectedIntents);
+        sendHighlights(intentTraffic());
+    }
+
+    private void sendClearHighlights() {
+        log.debug("sendClearHighlights");
+        sendHighlights(new Highlights());
+    }
+
+    private void sendHighlights(Highlights highlights) {
+        messageHandler.sendHighlights(highlights);
+    }
+
+
+    // =======================================================================
+    // === Generate messages in JSON object node format
+
+    private Highlights trafficSummary(LinkStatsType type) {
+        Highlights highlights = new Highlights();
+
+        // compile a set of bilinks (combining pairs of unidirectional links)
+        Map<LinkKey, BiLink> linkMap = new HashMap<>();
+        compileLinks(linkMap);
+        addEdgeLinks(linkMap);
+
+        for (BiLink blink : linkMap.values()) {
+            if (type == LinkStatsType.FLOW_STATS) {
+                attachFlowLoad(blink);
+            } else if (type == LinkStatsType.PORT_STATS) {
+                attachPortLoad(blink);
+            }
+
+            // we only want to report on links deemed to have traffic
+            if (blink.hasTraffic()) {
+                highlights.add(blink.generateHighlight(type));
+            }
+        }
+        return highlights;
+    }
+
+    // create highlights for links, showing flows for selected devices.
+    private Highlights deviceLinkFlows() {
+        Highlights highlights = new Highlights();
+
+        if (selectedNodes != null && !selectedNodes.devices().isEmpty()) {
+            // capture flow counts on bilinks
+            Map<LinkKey, BiLink> linkMap = new HashMap<>();
+
+            for (Device device : selectedNodes.devices()) {
+                Map<Link, Integer> counts = getLinkFlowCounts(device.id());
+                for (Link link : counts.keySet()) {
+                    BiLink blink = TopoUtils.addLink(linkMap, link);
+                    blink.addFlows(counts.get(link));
+                }
+            }
+
+            // now report on our collated links
+            for (BiLink blink : linkMap.values()) {
+                highlights.add(blink.generateHighlight(LinkStatsType.FLOW_COUNT));
+            }
+
+        }
+        return highlights;
+    }
+
+    private Highlights intentGroup() {
+        Highlights highlights = new Highlights();
+
+        if (selectedIntents != null && !selectedIntents.none()) {
+            // If 'all' intents are selected, they will all have primary
+            // highlighting; otherwise, the specifically selected intent will
+            // have primary highlighting, and the remainder will have secondary
+            // highlighting.
+            Set<Intent> primary;
+            Set<Intent> secondary;
+            int count = selectedIntents.size();
+
+            Set<Intent> allBut = new HashSet<>(selectedIntents.intents());
+            Intent current;
+
+            if (selectedIntents.all()) {
+                primary = allBut;
+                secondary = Collections.emptySet();
+                log.debug("Highlight all intents ({})", count);
+            } else {
+                current = selectedIntents.current();
+                primary = new HashSet<>();
+                primary.add(current);
+                allBut.remove(current);
+                secondary = allBut;
+                log.debug("Highlight intent: {} ([{}] of {})",
+                          current.id(), selectedIntents.index(), count);
+            }
+            TrafficClass tc1 = new TrafficClass(PRIMARY_HIGHLIGHT, primary);
+            TrafficClass tc2 = new TrafficClass(SECONDARY_HIGHLIGHT, secondary);
+            // classify primary links after secondary (last man wins)
+            highlightIntents(highlights, tc2, tc1);
+        }
+        return highlights;
+    }
+
+    private Highlights intentTraffic() {
+        Highlights highlights = new Highlights();
+
+        if (selectedIntents != null && selectedIntents.single()) {
+            Intent current = selectedIntents.current();
+            Set<Intent> primary = new HashSet<>();
+            primary.add(current);
+            log.debug("Highlight traffic for intent: {} ([{}] of {})",
+                      current.id(), selectedIntents.index(), selectedIntents.size());
+            TrafficClass tc1 = new TrafficClass(PRIMARY_HIGHLIGHT, primary, true);
+            highlightIntents(highlights, tc1);
+        }
+        return highlights;
+    }
+
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    private void compileLinks(Map<LinkKey, BiLink> linkMap) {
+        servicesBundle.linkService().getLinks()
+                .forEach(link -> TopoUtils.addLink(linkMap, link));
+    }
+
+    private void addEdgeLinks(Map<LinkKey, BiLink> biLinks) {
+        servicesBundle.hostService().getHosts().forEach(host -> {
+            TopoUtils.addLink(biLinks, createEdgeLink(host, true));
+            TopoUtils.addLink(biLinks, createEdgeLink(host, false));
+        });
+    }
+
+    private Load getLinkFlowLoad(Link link) {
+        if (link != null && link.src().elementId() instanceof DeviceId) {
+            return servicesBundle.flowStatsService().load(link);
+        }
+        return null;
+    }
+
+    private void attachFlowLoad(BiLink link) {
+        link.addLoad(getLinkFlowLoad(link.one()));
+        link.addLoad(getLinkFlowLoad(link.two()));
+    }
+
+    private void attachPortLoad(BiLink link) {
+        // For bi-directional traffic links, use
+        // the max link rate of either direction
+        // (we choose 'one' since we know that is never null)
+        Link one = link.one();
+        Load egressSrc = servicesBundle.portStatsService().load(one.src());
+        Load egressDst = servicesBundle.portStatsService().load(one.dst());
+//        link.addLoad(maxLoad(egressSrc, egressDst), BPS_THRESHOLD);
+        link.addLoad(maxLoad(egressSrc, egressDst), 10);    // FIXME - debug only
+    }
+
+    private Load maxLoad(Load a, Load b) {
+        if (a == null) {
+            return b;
+        }
+        if (b == null) {
+            return a;
+        }
+        return a.rate() > b.rate() ? a : b;
+    }
+
+    // ---
+
+    // Counts all flow entries that egress on the links of the given device.
+    private Map<Link, Integer> getLinkFlowCounts(DeviceId deviceId) {
+        // get the flows for the device
+        List<FlowEntry> entries = new ArrayList<>();
+        for (FlowEntry flowEntry : servicesBundle.flowService().getFlowEntries(deviceId)) {
+            entries.add(flowEntry);
+        }
+
+        // get egress links from device, and include edge links
+        Set<Link> links = new HashSet<>(servicesBundle.linkService().getDeviceEgressLinks(deviceId));
+        Set<Host> hosts = servicesBundle.hostService().getConnectedHosts(deviceId);
+        if (hosts != null) {
+            for (Host host : hosts) {
+                links.add(createEdgeLink(host, false));
+            }
+        }
+
+        // compile flow counts per link
+        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 int 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.allInstructions()) {
+                if (instruction.type() == Instruction.Type.OUTPUT &&
+                        ((OutputInstruction) instruction).port().equals(out)) {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
+    // ---
+    private void highlightIntents(Highlights highlights,
+                                  TrafficClass... trafficClasses) {
+        Map<LinkKey, BiLink> linkMap = new HashMap<>();
+
+
+        for (TrafficClass trafficClass : trafficClasses) {
+            classifyLinkTraffic(linkMap, trafficClass);
+        }
+
+        for (BiLink blink : linkMap.values()) {
+            highlights.add(blink.generateHighlight(LinkStatsType.TAGGED));
+        }
+    }
+
+    private void classifyLinkTraffic(Map<LinkKey, BiLink> linkMap,
+                                     TrafficClass trafficClass) {
+        for (Intent intent : trafficClass.intents()) {
+            boolean isOptical = intent instanceof OpticalConnectivityIntent;
+            List<Intent> installables = servicesBundle.intentService()
+                    .getInstallableIntents(intent.key());
+            Iterable<Link> links = null;
+
+            if (installables != null) {
+                for (Intent installable : installables) {
+
+                    if (installable instanceof PathIntent) {
+                        links = ((PathIntent) installable).path().links();
+                    } else if (installable instanceof FlowRuleIntent) {
+                        links = linkResources(installable);
+                    } else if (installable instanceof LinkCollectionIntent) {
+                        links = ((LinkCollectionIntent) installable).links();
+                    } else if (installable instanceof OpticalPathIntent) {
+                        links = ((OpticalPathIntent) installable).path().links();
+                    }
+
+                    classifyLinks(trafficClass, isOptical, linkMap, links);
+                }
+            }
+        }
+    }
+
+    private void classifyLinks(TrafficClass trafficClass, boolean isOptical,
+                               Map<LinkKey, BiLink> linkMap,
+                               Iterable<Link> links) {
+        if (links != null) {
+            for (Link link : links) {
+                BiLink blink = TopoUtils.addLink(linkMap, link);
+                if (trafficClass.showTraffic()) {
+                    blink.addLoad(getLinkFlowLoad(link));
+                    blink.setAntMarch(true);
+                }
+                blink.setOptical(isOptical);
+                blink.tagFlavor(trafficClass.flavor());
+            }
+        }
+    }
+
+    // Extracts links from the specified flow rule intent resources
+    private Collection<Link> linkResources(Intent installable) {
+        ImmutableList.Builder<Link> builder = ImmutableList.builder();
+        installable.resources().stream().filter(r -> r instanceof Link)
+                .forEach(r -> builder.add((Link) r));
+        return builder.build();
+    }
+
+    // =======================================================================
+    // === Background Task
+
+    // Provides periodic update of traffic information to the client
+    private class TrafficMonitor extends TimerTask {
+        @Override
+        public void run() {
+            try {
+                switch (mode) {
+                    case ALL_FLOW_TRAFFIC:
+                        sendAllFlowTraffic();
+                        break;
+                    case ALL_PORT_TRAFFIC:
+                        sendAllPortTraffic();
+                        break;
+                    case DEV_LINK_FLOWS:
+                        sendDeviceLinkFlows();
+                        break;
+                    case SEL_INTENT:
+                        sendSelectedIntentTraffic();
+                        break;
+
+                    default:
+                        // RELATED_INTENTS and IDLE modes should never invoke
+                        // the background task, but if they do, they have
+                        // nothing to do
+                        break;
+                }
+
+            } catch (Exception e) {
+                log.warn("Unable to process traffic task due to {}", e.getMessage());
+                log.warn("Boom!", e);
+            }
+        }
+    }
+
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/BiLink.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/BiLink.java
new file mode 100644
index 0000000..5ab4e0e
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/BiLink.java
@@ -0,0 +1,244 @@
+/*
+ * 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.impl.topo;
+
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+import org.onosproject.net.statistic.Load;
+import org.onosproject.ui.topo.LinkHighlight;
+
+import static org.onosproject.ui.topo.LinkHighlight.Flavor.NO_HIGHLIGHT;
+import static org.onosproject.ui.topo.LinkHighlight.Flavor.PRIMARY_HIGHLIGHT;
+import static org.onosproject.ui.topo.LinkHighlight.Flavor.SECONDARY_HIGHLIGHT;
+
+/**
+ * Representation of a link and its inverse, and any associated traffic data.
+ * This class understands how to generate {@link LinkHighlight}s for sending
+ * back to the topology view.
+ */
+public class BiLink {
+
+    private static final String EMPTY = "";
+
+    private final LinkKey key;
+    private final Link one;
+    private Link two;
+
+    private boolean hasTraffic = false;
+    private long bytes = 0;
+    private long rate = 0;
+    private long flows = 0;
+    private boolean isOptical = false;
+    private LinkHighlight.Flavor taggedFlavor = NO_HIGHLIGHT;
+    private boolean antMarch = false;
+
+    /**
+     * Constructs a bilink for the given key and initial link.
+     *
+     * @param key canonical key for this bilink
+     * @param link first link
+     */
+    public BiLink(LinkKey key, Link link) {
+        this.key = key;
+        this.one = link;
+    }
+
+    /**
+     * Sets the second link for this bilink.
+     *
+     * @param link second link
+     */
+    public void setOther(Link link) {
+        this.two = link;
+    }
+
+    /**
+     * Sets the optical flag to the given value.
+     *
+     * @param b true if an optical link
+     */
+    public void setOptical(boolean b) {
+        isOptical = b;
+    }
+
+    /**
+     * Sets the ant march flag to the given value.
+     *
+     * @param b true if marching ants required
+     */
+    public void setAntMarch(boolean b) {
+        antMarch = b;
+    }
+
+    /**
+     * Tags this bilink with a link flavor to be used in visual rendering.
+     *
+     * @param flavor the flavor to tag
+     */
+    public void tagFlavor(LinkHighlight.Flavor flavor) {
+        this.taggedFlavor = flavor;
+    }
+
+    /**
+     * Adds load statistics, marks the bilink as having traffic.
+     *
+     * @param load load to add
+     */
+    public void addLoad(Load load) {
+        addLoad(load, 0);
+    }
+
+    /**
+     * Adds load statistics, marks the bilink as having traffic, if the
+     * load rate is greater than the given threshold.
+     *
+     * @param load load to add
+     * @param threshold threshold to register traffic
+     */
+    public void addLoad(Load load, double threshold) {
+        if (load != null) {
+            this.hasTraffic = hasTraffic || load.rate() > threshold;
+            this.bytes += load.latest();
+            this.rate += load.rate();
+        }
+    }
+
+    /**
+     * Adds the given count of flows to this bilink.
+     *
+     * @param count count of flows
+     */
+    public void addFlows(int count) {
+        this.flows += count;
+    }
+
+    /**
+     * Generates a link highlight entity, based on state of this bilink.
+     *
+     * @param type the type of statistics to use to interpret the data
+     * @return link highlight data for this bilink
+     */
+    public LinkHighlight generateHighlight(LinkStatsType type) {
+        switch (type) {
+            case FLOW_COUNT:
+                return highlightForFlowCount(type);
+
+            case FLOW_STATS:
+            case PORT_STATS:
+                return highlightForStats(type);
+
+            case TAGGED:
+                return highlightForTagging(type);
+
+            default:
+                throw new IllegalStateException("unexpected case: " + type);
+        }
+    }
+
+    private LinkHighlight highlightForStats(LinkStatsType type) {
+        return new LinkHighlight(linkId(), SECONDARY_HIGHLIGHT)
+                .setLabel(generateLabel(type));
+    }
+
+    private LinkHighlight highlightForFlowCount(LinkStatsType type) {
+        LinkHighlight.Flavor flavor = flows() > 0 ?
+                PRIMARY_HIGHLIGHT : SECONDARY_HIGHLIGHT;
+        return new LinkHighlight(linkId(), flavor)
+                .setLabel(generateLabel(type));
+    }
+
+    private LinkHighlight highlightForTagging(LinkStatsType type) {
+        LinkHighlight hlite = new LinkHighlight(linkId(), flavor())
+                .setLabel(generateLabel(type));
+        if (isOptical()) {
+            hlite.addMod(LinkHighlight.MOD_OPTICAL);
+        }
+        if (isAntMarch()) {
+            hlite.addMod(LinkHighlight.MOD_ANIMATED);
+        }
+        return hlite;
+    }
+
+    // Generates a link identifier in the form that the Topology View on the
+    private String linkId() {
+        return TopoUtils.compactLinkString(one);
+    }
+
+    // Generates a string representation of the load, to be used as a label
+    private String generateLabel(LinkStatsType type) {
+        switch (type) {
+            case FLOW_COUNT:
+                return TopoUtils.formatFlows(flows());
+
+            case FLOW_STATS:
+                return TopoUtils.formatBytes(bytes());
+
+            case PORT_STATS:
+                return TopoUtils.formatBitRate(rate());
+
+            case TAGGED:
+                return hasTraffic() ? TopoUtils.formatBytes(bytes()) : EMPTY;
+
+            default:
+                return "?";
+        }
+    }
+
+    // === ----------------------------------------------------------------
+    // accessors
+
+    public LinkKey key() {
+        return key;
+    }
+
+    public Link one() {
+        return one;
+    }
+
+    public Link two() {
+        return two;
+    }
+
+    public boolean hasTraffic() {
+        return hasTraffic;
+    }
+
+    public boolean isOptical() {
+        return isOptical;
+    }
+
+    public boolean isAntMarch() {
+        return antMarch;
+    }
+
+    public LinkHighlight.Flavor flavor() {
+        return taggedFlavor;
+    }
+
+    public long bytes() {
+        return bytes;
+    }
+
+    public long rate() {
+        return rate;
+    }
+
+    public long flows() {
+        return flows;
+    }
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/IntentSelection.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/IntentSelection.java
new file mode 100644
index 0000000..eb959c5
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/IntentSelection.java
@@ -0,0 +1,171 @@
+/*
+ * 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.impl.topo;
+
+import org.onosproject.net.intent.Intent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Encapsulates a selection of intents (paths) inferred from a selection
+ * of devices and/or hosts from the topology view.
+ */
+public class IntentSelection {
+
+    private static final int ALL = -1;
+
+    protected static final Logger log =
+            LoggerFactory.getLogger(IntentSelection.class);
+
+    private final NodeSelection nodes;
+
+    private final List<Intent> intents;
+    private int index = ALL;
+
+    /**
+     * Creates an intent selection group, based on selected nodes.
+     *
+     * @param nodes node selection
+     * @param filter intent filter
+     */
+    public IntentSelection(NodeSelection nodes, TopologyViewIntentFilter filter) {
+        this.nodes = nodes;
+        intents = filter.findPathIntents(nodes.hosts(), nodes.devices());
+    }
+
+    /**
+     * Creates an intent selection group, for a single intent.
+     *
+     * @param intent the intent
+     */
+    public IntentSelection(Intent intent) {
+        nodes = null;
+        intents = new ArrayList<>(1);
+        intents.add(intent);
+        index = 0;
+    }
+
+    /**
+     * Returns true if no intents are selected.
+     *
+     * @return true if nothing selected
+     */
+    public boolean none() {
+        return intents.isEmpty();
+    }
+
+    /**
+     * Returns true if all intents in this select group are currently selected.
+     * This is the initial state, so that all intents are shown on the
+     * topology view with primary highlighting.
+     *
+     * @return true if all selected
+     */
+    public boolean all() {
+        return index == ALL;
+    }
+
+    /**
+     * Returns true if there is a single intent in this select group, or if
+     * a specific intent has been marked (index != ALL).
+     *
+     * @return true if single intent marked
+     */
+    public boolean single() {
+        return !all();
+    }
+
+    /**
+     * Returns the number of intents in this selection group.
+     *
+     * @return number of intents
+     */
+    public int size() {
+        return intents.size();
+    }
+
+    /**
+     * Returns the index of the currently selected intent.
+     *
+     * @return the current index
+     */
+    public int index() {
+        return index;
+    }
+
+    /**
+     * The list of intents in this selection group.
+     *
+     * @return list of intents
+     */
+    public List<Intent> intents() {
+        return Collections.unmodifiableList(intents);
+    }
+
+    /**
+     * Marks and returns the next intent in this group. Note that the
+     * selection wraps around to the beginning again, if necessary.
+     *
+     * @return the next intent in the group
+     */
+    public Intent next() {
+        index += 1;
+        if (index >= intents.size()) {
+            index = 0;
+        }
+        return intents.get(index);
+    }
+
+    /**
+     * Marks and returns the previous intent in this group. Note that the
+     * selection wraps around to the end again, if necessary.
+     *
+     * @return the previous intent in the group
+     */
+    public Intent prev() {
+        index -= 1;
+        if (index < 0) {
+            index = intents.size() - 1;
+        }
+        return intents.get(index);
+    }
+
+    /**
+     * Returns the currently marked intent, or null if "all" intents
+     * are marked.
+     *
+     * @return the currently marked intent
+     */
+    public Intent current() {
+        return all() ? null : intents.get(index);
+    }
+
+    @Override
+    public String toString() {
+        return "IntentSelection{" +
+                "nodes=" + nodes +
+                ", #intents=" + intents.size() +
+                ", index=" + index +
+                '}';
+    }
+
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/LinkStatsType.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/LinkStatsType.java
new file mode 100644
index 0000000..589cddd
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/LinkStatsType.java
@@ -0,0 +1,43 @@
+/*
+ * 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.impl.topo;
+
+/**
+ * Designates type of stats to report on a highlighted link.
+ */
+public enum LinkStatsType {
+    /**
+     * Number of flows.
+     */
+    FLOW_COUNT,
+
+    /**
+     * Number of bytes.
+     */
+    FLOW_STATS,
+
+    /**
+     * Number of bits per second.
+     */
+    PORT_STATS,
+
+    /**
+     * Custom tagged information.
+     */
+    TAGGED
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/NodeSelection.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/NodeSelection.java
new file mode 100644
index 0000000..fa776b3
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/NodeSelection.java
@@ -0,0 +1,187 @@
+/*
+ * 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.impl.topo;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onosproject.net.Device;
+import org.onosproject.net.Host;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.ui.JsonUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.HostId.hostId;
+
+/**
+ * Encapsulates a selection of devices and/or hosts from the topology view.
+ */
+public class NodeSelection {
+
+    protected static final Logger log =
+            LoggerFactory.getLogger(NodeSelection.class);
+
+    private static final String IDS = "ids";
+    private static final String HOVER = "hover";
+
+    private final DeviceService deviceService;
+    private final HostService hostService;
+
+    private final Set<String> ids;
+    private final String hover;
+
+    private final Set<Device> devices = new HashSet<>();
+    private final Set<Host> hosts = new HashSet<>();
+
+    /**
+     * Creates a node selection entity, from the given payload, using the
+     * supplied device and host services.
+     *
+     * @param payload message payload
+     * @param deviceService device service
+     * @param hostService host service
+     */
+    public NodeSelection(ObjectNode payload,
+                         DeviceService deviceService,
+                         HostService hostService) {
+        this.deviceService = deviceService;
+        this.hostService = hostService;
+
+        ids = extractIds(payload);
+        hover = extractHover(payload);
+
+        Set<String> unmatched = findDevices(ids);
+        unmatched = findHosts(unmatched);
+        if (unmatched.size() > 0) {
+            log.debug("Skipping unmatched IDs {}", unmatched);
+        }
+
+        if (!isNullOrEmpty(hover)) {
+            unmatched = new HashSet<>();
+            unmatched.add(hover);
+            unmatched = findDevices(unmatched);
+            unmatched = findHosts(unmatched);
+            if (unmatched.size() > 0) {
+                log.debug("Skipping unmatched HOVER {}", unmatched);
+            }
+        }
+    }
+
+    /**
+     * Returns a view of the selected devices.
+     *
+     * @return selected devices
+     */
+    public Set<Device> devices() {
+        return Collections.unmodifiableSet(devices);
+    }
+
+    /**
+     * Returns a view of the selected hosts.
+     *
+     * @return selected hosts
+     */
+    public Set<Host> hosts() {
+        return Collections.unmodifiableSet(hosts);
+    }
+
+    /**
+     * Returns true if nothing is selected.
+     *
+     * @return true if nothing selected
+     */
+    public boolean none() {
+        return devices().size() == 0 && hosts().size() == 0;
+    }
+
+    @Override
+    public String toString() {
+        return "NodeSelection{" +
+                "ids=" + ids +
+                ", hover='" + hover + '\'' +
+                ", #devices=" + devices.size() +
+                ", #hosts=" + hosts.size() +
+                '}';
+    }
+
+    // == helper methods
+
+    private Set<String> extractIds(ObjectNode payload) {
+        ArrayNode array = (ArrayNode) payload.path(IDS);
+        if (array == null || array.size() == 0) {
+            return Collections.emptySet();
+        }
+
+        Set<String> ids = new HashSet<>();
+        for (JsonNode node : array) {
+            ids.add(node.asText());
+        }
+        return ids;
+    }
+
+    private String extractHover(ObjectNode payload) {
+        return JsonUtils.string(payload, HOVER);
+    }
+
+    private Set<String> findDevices(Set<String> ids) {
+        Set<String> unmatched = new HashSet<>();
+        Device device;
+
+        for (String id : ids) {
+            try {
+                device = deviceService.getDevice(deviceId(id));
+                if (device != null) {
+                    devices.add(device);
+                } else {
+                    log.debug("Device with ID {} not found", id);
+                }
+            } catch (IllegalArgumentException e) {
+                unmatched.add(id);
+            }
+        }
+        return unmatched;
+    }
+
+    private Set<String> findHosts(Set<String> ids) {
+        Set<String> unmatched = new HashSet<>();
+        Host host;
+
+        for (String id : ids) {
+            try {
+                host = hostService.getHost(hostId(id));
+                if (host != null) {
+                    hosts.add(host);
+                } else {
+                    log.debug("Host with ID {} not found", id);
+                }
+            } catch (IllegalArgumentException e) {
+                unmatched.add(id);
+            }
+        }
+        return unmatched;
+    }
+
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/ServicesBundle.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/ServicesBundle.java
new file mode 100644
index 0000000..4282cdc
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/ServicesBundle.java
@@ -0,0 +1,96 @@
+/*
+ * 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.impl.topo;
+
+import org.onosproject.incubator.net.PortStatisticsService;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.intent.IntentService;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.statistic.StatisticService;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A bundle of services that the topology view requires to get its job done.
+ */
+public class ServicesBundle {
+
+    private final IntentService intentService;
+    private final DeviceService deviceService;
+    private final HostService hostService;
+    private final LinkService linkService;
+    private final FlowRuleService flowService;
+    private final StatisticService flowStatsService;
+    private final PortStatisticsService portStatsService;
+
+    /**
+     * Creates the services bundle.
+     * @param intentService     intent service reference
+     * @param deviceService     device service reference
+     * @param hostService       host service reference
+     * @param linkService       link service reference
+     * @param flowService       flow service reference
+     * @param flowStatsService  flow statistics service reference
+     * @param portStatsService  port statistics service reference
+     */
+    public ServicesBundle(IntentService intentService,
+                          DeviceService deviceService,
+                          HostService hostService,
+                          LinkService linkService,
+                          FlowRuleService flowService,
+                          StatisticService flowStatsService,
+                          PortStatisticsService portStatsService) {
+        this.intentService = checkNotNull(intentService);
+        this.deviceService = checkNotNull(deviceService);
+        this.hostService = checkNotNull(hostService);
+        this.linkService = checkNotNull(linkService);
+        this.flowService = checkNotNull(flowService);
+        this.flowStatsService = checkNotNull(flowStatsService);
+        this.portStatsService = checkNotNull(portStatsService);
+    }
+
+    public IntentService intentService() {
+        return intentService;
+    }
+
+    public DeviceService deviceService() {
+        return deviceService;
+    }
+
+    public HostService hostService() {
+        return hostService;
+    }
+
+    public LinkService linkService() {
+        return linkService;
+    }
+
+    public FlowRuleService flowService() {
+        return flowService;
+    }
+
+    public StatisticService flowStatsService() {
+        return flowStatsService;
+    }
+
+    public PortStatisticsService portStatsService() {
+        return portStatsService;
+    }
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoUtils.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoUtils.java
new file mode 100644
index 0000000..8d6b319
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoUtils.java
@@ -0,0 +1,184 @@
+/*
+ * 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.impl.topo;
+
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+
+import java.text.DecimalFormat;
+import java.util.Map;
+
+import static org.onosproject.net.LinkKey.linkKey;
+
+/**
+ * Utility methods for helping out with the topology view.
+ */
+public final class TopoUtils {
+
+    public static final double KILO = 1024;
+    public static final double MEGA = 1024 * KILO;
+    public static final double GIGA = 1024 * MEGA;
+
+    public static final String GBITS_UNIT = "Gb";
+    public static final String MBITS_UNIT = "Mb";
+    public static final String KBITS_UNIT = "Kb";
+    public static final String BITS_UNIT = "b";
+    public static final String GBYTES_UNIT = "GB";
+    public static final String MBYTES_UNIT = "MB";
+    public static final String KBYTES_UNIT = "KB";
+    public static final String BYTES_UNIT = "B";
+
+
+    private static final DecimalFormat DF2 = new DecimalFormat("#,###.##");
+
+    private static final String COMPACT = "%s/%s-%s/%s";
+    private static final String EMPTY = "";
+    private static final String SPACE = " ";
+    private static final String PER_SEC = "ps";
+    private static final String FLOW = "flow";
+    private static final String FLOWS = "flows";
+
+    // non-instantiable
+    private TopoUtils() { }
+
+    /**
+     * Returns a compact identity for the given link, in the form
+     * used to identify links in the Topology View on the client.
+     *
+     * @param link link
+     * @return compact link identity
+     */
+    public static String compactLinkString(Link link) {
+        return String.format(COMPACT, link.src().elementId(), link.src().port(),
+                             link.dst().elementId(), link.dst().port());
+    }
+
+    /**
+     * Produces a canonical link key, that is, one that will match both a link
+     * and its inverse.
+     *
+     * @param link the link
+     * @return canonical key
+     */
+    public static LinkKey canonicalLinkKey(Link link) {
+        String sn = link.src().elementId().toString();
+        String dn = link.dst().elementId().toString();
+        return sn.compareTo(dn) < 0 ?
+                linkKey(link.src(), link.dst()) : linkKey(link.dst(), link.src());
+    }
+
+    /**
+     * Returns human readable count of bytes, to be displayed as a label.
+     *
+     * @param bytes number of bytes
+     * @return formatted byte count
+     */
+    public static String formatBytes(long bytes) {
+        String unit;
+        double value;
+        if (bytes > GIGA) {
+            value = bytes / GIGA;
+            unit = GBYTES_UNIT;
+        } else if (bytes > MEGA) {
+            value = bytes / MEGA;
+            unit = MBYTES_UNIT;
+        } else if (bytes > KILO) {
+            value = bytes / KILO;
+            unit = KBYTES_UNIT;
+        } else {
+            value = bytes;
+            unit = BYTES_UNIT;
+        }
+        return DF2.format(value) + SPACE + unit;
+    }
+
+    /**
+     * Returns human readable bit rate, to be displayed as a label.
+     *
+     * @param bytes bytes per second
+     * @return formatted bits per second
+     */
+    public static String formatBitRate(long bytes) {
+        String unit;
+        double value;
+
+        //Convert to bits
+        long bits = bytes * 8;
+        if (bits > GIGA) {
+            value = bits / GIGA;
+            unit = GBITS_UNIT;
+
+            // NOTE: temporary hack to clip rate at 10.0 Gbps
+            //  Added for the CORD Fabric demo at ONS 2015
+            // TODO: provide a more elegant solution to this issue
+            if (value > 10.0) {
+                value = 10.0;
+            }
+
+        } else if (bits > MEGA) {
+            value = bits / MEGA;
+            unit = MBITS_UNIT;
+        } else if (bits > KILO) {
+            value = bits / KILO;
+            unit = KBITS_UNIT;
+        } else {
+            value = bits;
+            unit = BITS_UNIT;
+        }
+        return DF2.format(value) + SPACE + unit + PER_SEC;
+    }
+
+    /**
+     * Returns human readable flow count, to be displayed as a label.
+     *
+     * @param flows number of flows
+     * @return formatted flow count
+     */
+    public static String formatFlows(long flows) {
+        if (flows < 1) {
+            return EMPTY;
+        }
+        return String.valueOf(flows) + SPACE + (flows > 1 ? FLOWS : FLOW);
+    }
+
+
+    /**
+     * Creates a new biLink with the supplied link (and adds it to the map),
+     * or attaches the link to an existing biLink (which already has the
+     * peer link).
+     *
+     * @param linkMap map of biLinks
+     * @param link the link to add
+     * @return the biLink to which the link was added
+     */
+    // creates a new biLink with supplied link, or attaches link to the
+    //  existing biLink (which already has its peer link)
+    public static BiLink addLink(Map<LinkKey, BiLink> linkMap, Link link) {
+        LinkKey key = TopoUtils.canonicalLinkKey(link);
+        BiLink biLink = linkMap.get(key);
+        if (biLink != null) {
+            biLink.setOther(link);
+        } else {
+            biLink = new BiLink(key, link);
+            linkMap.put(key, biLink);
+        }
+        return biLink;
+    }
+
+
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewIntentFilter.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopologyViewIntentFilter.java
similarity index 92%
rename from web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewIntentFilter.java
rename to web/gui/src/main/java/org/onosproject/ui/impl/topo/TopologyViewIntentFilter.java
index c3f58f7..1bd2b58 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewIntentFilter.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopologyViewIntentFilter.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.onosproject.ui.impl;
+package org.onosproject.ui.impl.topo;
 
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.Device;
@@ -56,32 +56,35 @@
     private final LinkService linkService;
 
     /**
-     * Crreates an intent filter.
+     * Creates an intent filter.
      *
-     * @param intentService intent service reference
-     * @param deviceService device service reference
-     * @param hostService   host service reference
-     * @param linkService   link service reference
+     * @param services service references bundle
      */
-    TopologyViewIntentFilter(IntentService intentService, DeviceService deviceService,
-                             HostService hostService, LinkService linkService) {
-        this.intentService = intentService;
-        this.deviceService = deviceService;
-        this.hostService = hostService;
-        this.linkService = linkService;
+    public TopologyViewIntentFilter(ServicesBundle services) {
+        this.intentService = services.intentService();
+        this.deviceService = services.deviceService();
+        this.hostService = services.hostService();
+        this.linkService = services.linkService();
     }
 
+
+    // TODO: Review - do we need this signature, with sourceIntents??
+//    public List<Intent> findPathIntents(Set<Host> hosts, Set<Device> devices,
+//                                        Iterable<Intent> sourceIntents) {
+//    }
+
     /**
-     * Finds all path (host-to-host or point-to-point) intents that pertains
-     * to the given hosts.
+     * Finds all path (host-to-host or point-to-point) intents that pertain
+     * to the given hosts and devices.
      *
      * @param hosts         set of hosts to query by
      * @param devices       set of devices to query by
-     * @param sourceIntents collection of intents to search
      * @return set of intents that 'match' all hosts and devices given
      */
-    List<Intent> findPathIntents(Set<Host> hosts, Set<Device> devices,
-                                 Iterable<Intent> sourceIntents) {
+    public List<Intent> findPathIntents(Set<Host> hosts, Set<Device> devices) {
+        // start with all intents
+        Iterable<Intent> sourceIntents = intentService.getIntents();
+
         // Derive from this the set of edge connect points.
         Set<ConnectPoint> edgePoints = getEdgePoints(hosts);
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/TrafficClass.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/TrafficClass.java
new file mode 100644
index 0000000..1389aba
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/TrafficClass.java
@@ -0,0 +1,55 @@
+/*
+ * 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.impl.topo;
+
+import org.onosproject.net.intent.Intent;
+import org.onosproject.ui.topo.LinkHighlight;
+
+/**
+ * Auxiliary data carrier for assigning a highlight class to a set of
+ * intents, for visualization in the topology view.
+ */
+public class TrafficClass {
+
+    private final LinkHighlight.Flavor flavor;
+    private final Iterable<Intent> intents;
+    private final boolean showTraffic;
+
+    public TrafficClass(LinkHighlight.Flavor flavor, Iterable<Intent> intents) {
+        this(flavor, intents, false);
+    }
+
+    public TrafficClass(LinkHighlight.Flavor flavor, Iterable<Intent> intents,
+                        boolean showTraffic) {
+        this.flavor = flavor;
+        this.intents = intents;
+        this.showTraffic = showTraffic;
+    }
+
+    public LinkHighlight.Flavor flavor() {
+        return flavor;
+    }
+
+    public Iterable<Intent> intents() {
+        return intents;
+    }
+
+    public boolean showTraffic() {
+        return showTraffic;
+    }
+}
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 0595393..f3c053e 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -58,6 +58,7 @@
         showHosts = false,      // whether hosts are displayed
         showOffline = true,     // whether offline devices are displayed
         nodeLock = false,       // whether nodes can be dragged or not (locked)
+        fTimer,                 // timer for delayed force layout
         fNodesTimer,            // timer for delayed nodes update
         fLinksTimer,            // timer for delayed links update
         dim,                    // the dimensions of the force layout [w,h]
@@ -117,6 +118,7 @@
         network.nodes.push(d);
         lu[id] = d;
         updateNodes();
+        fStart();
     }
 
     function updateDevice(data) {
@@ -170,6 +172,7 @@
             lu[d.egress] = lnk;
             updateLinks();
         }
+        fStart();
     }
 
     function updateHost(data) {
@@ -215,6 +218,7 @@
             aggregateLink(d, data);
             lu[d.key] = d;
             updateLinks();
+            fStart();
         }
     }
 
@@ -322,6 +326,7 @@
             // remove from lookup cache
             delete lu[removed[0].key];
             updateLinks();
+            fResume();
         }
     }
 
@@ -343,6 +348,7 @@
         // NOTE: upd is false if we were called from removeDeviceElement()
         if (upd) {
             updateNodes();
+            fResume();
         }
     }
 
@@ -367,6 +373,7 @@
 
         // remove from SVG
         updateNodes();
+        fResume();
     }
 
     function updateHostVisibility() {
@@ -520,8 +527,9 @@
         fNodesTimer = $timeout(_updateNodes, 150);
     }
 
+    // IMPLEMENTATION NOTE: _updateNodes() should NOT stop, start, or resume
+    //  the force layout; that needs to be determined and implemented elsewhere
     function _updateNodes() {
-        force.stop();
         // select all the nodes in the layout:
         node = nodeG.selectAll('.node')
             .data(network.nodes, function (d) { return d.id; });
@@ -536,7 +544,10 @@
             .attr({
                 id: function (d) { return sus.safeId(d.id); },
                 class: mkSvgClass,
-                transform: function (d) { return sus.translate(d.x, d.y); },
+                transform: function (d) {
+                    // Need to guard against NaN here ??
+                    return sus.translate(d.x, d.y);
+                },
                 opacity: 0
             })
             .call(drag)
@@ -564,7 +575,6 @@
         // exiting node specifics:
         exiting.filter('.host').each(td3.hostExit);
         exiting.filter('.device').each(td3.deviceExit);
-        fStart();
     }
 
     // ==========================
@@ -659,9 +669,10 @@
         fLinksTimer = $timeout(_updateLinks, 150);
     }
 
+    // IMPLEMENTATION NOTE: _updateLinks() should NOT stop, start, or resume
+    //  the force layout; that needs to be determined and implemented elsewhere
     function _updateLinks() {
         var th = ts.theme();
-        force.stop();
 
         link = linkG.selectAll('.link')
             .data(network.links, function (d) { return d.key; });
@@ -714,7 +725,6 @@
             })
             .style('opacity', 0.0)
             .remove();
-        fStart();
     }
 
 
@@ -729,14 +739,23 @@
 
     function fStart() {
         if (!tos.isOblique()) {
-            $log.debug("Starting force-layout");
-            force.start();
+            if (fTimer) {
+                $timeout.cancel(fTimer);
+            }
+            fTimer = $timeout(function () {
+                $log.debug("Starting force-layout");
+                force.start();
+            }, 200);
         }
     }
 
     var tickStuff = {
         nodeAttr: {
-            transform: function (d) { return sus.translate(d.x, d.y); }
+            transform: function (d) {
+                var dx = isNaN(d.x) ? 0 : d.x,
+                    dy = isNaN(d.y) ? 0 : d.y;
+                return sus.translate(dx, dy);
+            }
         },
         linkAttr: {
             x1: function (d) { return d.position.x1; },
@@ -1046,6 +1065,9 @@
                 force = drag = null;
 
                 // clean up $timeout promises
+                if (fTimer) {
+                    $timeout.cancel(fTimer);
+                }
                 if (fNodesTimer) {
                     $timeout.cancel(fNodesTimer);
                 }
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 41c8e1e..a049948 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
@@ -293,7 +293,7 @@
              findLinkById( id )
          */
 
-        var paths = data.paths;
+        var paths = data.links;
 
         api.clearLinkTrafficStyle();
         api.removeLinkLabels();
diff --git a/web/gui/src/main/webapp/app/view/topo/topoSelect.js b/web/gui/src/main/webapp/app/view/topo/topoSelect.js
index 2e73ea2..72a689f 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoSelect.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoSelect.js
@@ -114,7 +114,7 @@
         }
 
         if (!ev.shiftKey) {
-            deselectAll();
+            deselectAll(true);
         }
 
         selections[obj.id] = { obj: obj, el: el };
@@ -135,7 +135,7 @@
         }
     }
 
-    function deselectAll() {
+    function deselectAll(skipUpdate) {
         var something = (selectOrder.length > 0);
 
         // deselect all nodes in the network...
@@ -143,7 +143,9 @@
         selections = {};
         selectOrder = [];
         api.updateDeviceColors();
-        updateDetail();
+        if (!skipUpdate) {
+            updateDetail();
+        }
 
         // return true if something was selected
         return something;
diff --git a/web/gui/src/main/webapp/app/view/topo/topoTraffic.js b/web/gui/src/main/webapp/app/view/topo/topoTraffic.js
index 27ec979..9308542 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoTraffic.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoTraffic.js
@@ -42,9 +42,9 @@
 
     // invoked in response to change in selection and/or mouseover/out:
     function requestTrafficForMode() {
-        if (hoverMode === 'flows') {
+        if (trafficMode === 'flows') {
             requestDeviceLinkFlows();
-        } else if (hoverMode === 'intents') {
+        } else if (trafficMode === 'intents') {
             requestRelatedIntents();
         } else {
             cancelTraffic();
@@ -175,7 +175,6 @@
     }
 
 
-
     // === -----------------------------------------------------
     // === MODULE DEFINITION ===