ONOS-1479 -- GUI - augmenting topology view for extensibility: WIP.

Change-Id: I11820a9ff8f446c0d10a0311cee5ce448c15f402
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/ButtonDescriptor.java b/core/api/src/main/java/org/onosproject/ui/topo/ButtonDescriptor.java
deleted file mode 100644
index e797130..0000000
--- a/core/api/src/main/java/org/onosproject/ui/topo/ButtonDescriptor.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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;
-
-/**
- * Designates a descriptor for a button on the topology view panels.
- */
-public class ButtonDescriptor {
-
-    private final String id;
-    private final String glyphId;
-    private final String tooltip;
-
-    /**
-     * Creates a button descriptor with the given identifier, glyph ID, and
-     * tooltip text. To reference a custom glyph defined in the overlay itself,
-     * prefix its ID with an asterisk, (e.g. {@code "*myGlyph"}). Alternatively,
-     * use one of the {@link TopoConstants.Glyphs predefined constant}.
-     *
-     * @param id identifier for the button
-     * @param glyphId identifier for the glyph
-     * @param tooltip tooltip text
-     */
-    public ButtonDescriptor(String id, String glyphId, String tooltip) {
-        this.id = id;
-        this.glyphId = glyphId;
-        this.tooltip = tooltip;
-    }
-
-    /**
-     * Returns the identifier for this button.
-     *
-     * @return identifier
-     */
-    public String id() {
-        return id;
-    }
-
-    /**
-     * Returns the glyph identifier for this button.
-     *
-     * @return glyph identifier
-     */
-    public String glyphId() {
-        return glyphId;
-    }
-
-    /**
-     * Returns the tooltip text for this button.
-     *
-     * @return tooltip text
-     */
-    public String tooltip() {
-        return tooltip;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-
-        ButtonDescriptor that = (ButtonDescriptor) o;
-        return id.equals(that.id);
-
-    }
-
-    @Override
-    public int hashCode() {
-        return id.hashCode();
-    }
-}
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/ButtonId.java b/core/api/src/main/java/org/onosproject/ui/topo/ButtonId.java
new file mode 100644
index 0000000..ca2eccc
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/ButtonId.java
@@ -0,0 +1,70 @@
+/*
+ * 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 com.google.common.base.MoreObjects;
+
+/**
+ * Designates the identity of a button on the topology view panels.
+ */
+public class ButtonId {
+
+    private final String id;
+
+    /**
+     * Creates a button ID with the given identifier.
+     *
+     * @param id identifier for the button
+     */
+    public ButtonId(String id) {
+        this.id = id;
+    }
+
+    /**
+     * Returns the identifier for this button.
+     *
+     * @return identifier
+     */
+    public String id() {
+        return id;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("id", id()).toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        ButtonId that = (ButtonId) o;
+        return id.equals(that.id);
+    }
+
+    @Override
+    public int hashCode() {
+        return id.hashCode();
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/PropertyPanel.java b/core/api/src/main/java/org/onosproject/ui/topo/PropertyPanel.java
index 43364ea..121e083 100644
--- a/core/api/src/main/java/org/onosproject/ui/topo/PropertyPanel.java
+++ b/core/api/src/main/java/org/onosproject/ui/topo/PropertyPanel.java
@@ -35,7 +35,7 @@
     private String typeId;
     private String id;
     private List<Prop> properties = new ArrayList<>();
-    private List<ButtonDescriptor> buttons = new ArrayList<>();
+    private List<ButtonId> buttons = new ArrayList<>();
 
     /**
      * Constructs a property panel model with the given title and
@@ -181,7 +181,7 @@
      * @return the button list
      */
     // TODO: consider protecting this?
-    public List<ButtonDescriptor> buttons() {
+    public List<ButtonId> buttons() {
         return buttons;
     }
 
@@ -243,7 +243,7 @@
      * @param button button descriptor
      * @return self, for chaining
      */
-    public PropertyPanel addButton(ButtonDescriptor button) {
+    public PropertyPanel addButton(ButtonId button) {
         buttons.add(button);
         return this;
     }
@@ -254,10 +254,10 @@
      * @param descriptors descriptors to remove
      * @return self, for chaining
      */
-    public PropertyPanel removeButtons(ButtonDescriptor... descriptors) {
-        Set<ButtonDescriptor> forRemoval = Sets.newHashSet(descriptors);
-        List<ButtonDescriptor> toKeep = new ArrayList<>();
-        for (ButtonDescriptor bd: buttons) {
+    public PropertyPanel removeButtons(ButtonId... descriptors) {
+        Set<ButtonId> forRemoval = Sets.newHashSet(descriptors);
+        List<ButtonId> toKeep = new ArrayList<>();
+        for (ButtonId bd: buttons) {
             if (!forRemoval.contains(bd)) {
                 toKeep.add(bd);
             }
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/TopoConstants.java b/core/api/src/main/java/org/onosproject/ui/topo/TopoConstants.java
index a48c253..38a8f03 100644
--- a/core/api/src/main/java/org/onosproject/ui/topo/TopoConstants.java
+++ b/core/api/src/main/java/org/onosproject/ui/topo/TopoConstants.java
@@ -108,30 +108,22 @@
         public static final String VLAN = "VLAN";
     }
 
-    private static final class CoreButton extends ButtonDescriptor {
-        private CoreButton(String tag, String glyphId, boolean extra) {
-            super("show" + tag + "View",
-                  glyphId,
-                  "Show " + tag + " View" + (extra ? " for this Device" : ""));
-        }
-    }
-
     /**
-     * Defines constants for core buttons that appear on the topology
+     * Defines identities of core buttons that appear on the topology
      * details panel.
      */
     public static final class CoreButtons {
-        public static final ButtonDescriptor SHOW_DEVICE_VIEW =
-                new CoreButton("Device", Glyphs.SWITCH, false);
+        public static final ButtonId SHOW_DEVICE_VIEW =
+                new ButtonId("showDeviceView");
 
-        public static final ButtonDescriptor SHOW_FLOW_VIEW =
-                new CoreButton("Flow", Glyphs.FLOW_TABLE, true);
+        public static final ButtonId SHOW_FLOW_VIEW =
+                new ButtonId("showFlowView");
 
-        public static final ButtonDescriptor SHOW_PORT_VIEW =
-                new CoreButton("Port", Glyphs.PORT_TABLE, true);
+        public static final ButtonId SHOW_PORT_VIEW =
+                new ButtonId("showPortView");
 
-        public static final ButtonDescriptor SHOW_GROUP_VIEW =
-                new CoreButton("Group", Glyphs.GROUP_TABLE, true);
+        public static final ButtonId SHOW_GROUP_VIEW =
+                new ButtonId("showGroupView");
     }
 
 }
diff --git a/core/api/src/test/java/org/onosproject/ui/topo/ButtonDescriptorTest.java b/core/api/src/test/java/org/onosproject/ui/topo/ButtonDescriptorTest.java
deleted file mode 100644
index 1f47a09..0000000
--- a/core/api/src/test/java/org/onosproject/ui/topo/ButtonDescriptorTest.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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 org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-
-/**
- * Unit tests for {@link ButtonDescriptor}.
- */
-public class ButtonDescriptorTest {
-
-    private static final String ID = "my-id";
-    private static final String GID = "my-glyphId";
-    private static final String TT = "my-tewltyp";
-
-    private ButtonDescriptor bd;
-
-
-    @Test
-    public void basic() {
-        bd = new ButtonDescriptor(ID, GID, TT);
-
-        assertEquals("bad id", ID, bd.id());
-        assertEquals("bad gid", GID, bd.glyphId());
-        assertEquals("bad tt", TT, bd.tooltip());
-    }
-
-}
diff --git a/core/api/src/test/java/org/onosproject/ui/topo/ButtonIdTest.java b/core/api/src/test/java/org/onosproject/ui/topo/ButtonIdTest.java
new file mode 100644
index 0000000..04c6dc1
--- /dev/null
+++ b/core/api/src/test/java/org/onosproject/ui/topo/ButtonIdTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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 org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit tests for {@link ButtonId}.
+ */
+public class ButtonIdTest {
+
+    private static final String ID1 = "id-1";
+    private static final String ID2 = "id-2";
+
+    private ButtonId b1, b2;
+
+
+    @Test
+    public void basic() {
+        b1 = new ButtonId(ID1);
+    }
+
+    @Test
+    public void same() {
+        b1 = new ButtonId(ID1);
+        b2 = new ButtonId(ID1);
+        assertFalse("same ref?", b1 == b2);
+        assertTrue("not equal?", b1.equals(b2));
+    }
+
+    @Test
+    public void notSame() {
+        b1 = new ButtonId(ID1);
+        b2 = new ButtonId(ID2);
+        assertFalse("same ref?", b1 == b2);
+        assertFalse("equal?", b1.equals(b2));
+    }
+}
diff --git a/core/api/src/test/java/org/onosproject/ui/topo/PropertyPanelTest.java b/core/api/src/test/java/org/onosproject/ui/topo/PropertyPanelTest.java
index e97c037..b08ee4d 100644
--- a/core/api/src/test/java/org/onosproject/ui/topo/PropertyPanelTest.java
+++ b/core/api/src/test/java/org/onosproject/ui/topo/PropertyPanelTest.java
@@ -47,14 +47,6 @@
     private static final String VALUE_B = "Bee";
     private static final String VALUE_C = "Sea";
     private static final String VALUE_Z = "Zed";
-    private static final String GID_A = "gid-A";
-    private static final String GID_B = "gid-B";
-    private static final String GID_C = "gid-C";
-    private static final String GID_Z = "gid-Z";
-    private static final String TT_A = "toolTip-A";
-    private static final String TT_B = "toolTip-B";
-    private static final String TT_C = "toolTip-C";
-    private static final String TT_Z = "toolTip-Z";
 
     private static final Map<String, Prop> PROP_MAP = new HashMap<>();
 
@@ -211,17 +203,13 @@
         validateProp(KEY_B, ">byyy<");
     }
 
-    private static final ButtonDescriptor BD_A =
-            new ButtonDescriptor(KEY_A, GID_A, TT_A);
-    private static final ButtonDescriptor BD_B =
-            new ButtonDescriptor(KEY_B, GID_B, TT_B);
-    private static final ButtonDescriptor BD_C =
-            new ButtonDescriptor(KEY_C, GID_C, TT_C);
-    private static final ButtonDescriptor BD_Z =
-            new ButtonDescriptor(KEY_Z, GID_Z, TT_Z);
+    private static final ButtonId BD_A = new ButtonId(KEY_A);
+    private static final ButtonId BD_B = new ButtonId(KEY_B);
+    private static final ButtonId BD_C = new ButtonId(KEY_C);
+    private static final ButtonId BD_Z = new ButtonId(KEY_Z);
 
     private void verifyButtons(String... keys) {
-        Iterator<ButtonDescriptor> iter = pp.buttons().iterator();
+        Iterator<ButtonId> iter = pp.buttons().iterator();
         for (String k: keys) {
             assertEquals("wrong button", k, iter.next().id());
         }
diff --git a/utils/misc/src/main/java/org/onlab/util/AbstractAccumulator.java b/utils/misc/src/main/java/org/onlab/util/AbstractAccumulator.java
index a7b50cb..ef692c7 100644
--- a/utils/misc/src/main/java/org/onlab/util/AbstractAccumulator.java
+++ b/utils/misc/src/main/java/org/onlab/util/AbstractAccumulator.java
@@ -112,7 +112,10 @@
             if (isReady()) {
                 try {
                     maxTask = cancelIfActive(maxTask);
-                    processItems(finalizeCurrentBatch());
+                    List<T> items = finalizeCurrentBatch();
+                    if (!items.isEmpty()) {
+                        processItems(items);
+                    }
                 } catch (Exception e) {
                     log.warn("Unable to process batch due to {}", e);
                 }
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 9a360bf..b88ee4f 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
@@ -214,6 +214,8 @@
                 new UpdateMeta(),
                 new EqMasters(),
 
+                // TODO: implement "showHighlights" event (replaces "showTraffic")
+
                 // TODO: migrate traffic related to separate app
                 new AddHostIntent(),
                 new AddMultiSourceIntent(),
@@ -965,6 +967,14 @@
 
         @Override
         public void processItems(List<Event> items) {
+            // Start-of-Debugging
+            long now = System.currentTimeMillis();
+            String me = this.toString();
+            String miniMe = me.replaceAll("^.*@", "me@");
+            log.debug("Time: {}; this: {}, processing items ({} events)",
+                      now, miniMe, items.size());
+            // End-of-Debugging
+
             try {
                 if (summaryRunning) {
                     msgSender.execute(() -> requestSummary(0));
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 8dbb111..9265e5f 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
@@ -72,7 +72,7 @@
 import org.onosproject.ui.JsonUtils;
 import org.onosproject.ui.UiConnection;
 import org.onosproject.ui.UiMessageHandler;
-import org.onosproject.ui.topo.ButtonDescriptor;
+import org.onosproject.ui.topo.ButtonId;
 import org.onosproject.ui.topo.PropertyPanel;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -123,6 +123,8 @@
             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;
@@ -642,7 +644,7 @@
                 labelsN.add("");
             }
         }
-        return JsonUtils.envelope("showTraffic", 0, payload);
+        return JsonUtils.envelope(SHOW_HIGHLIGHTS, 0, payload);
     }
 
     private Load getLinkLoad(Link link) {
@@ -679,7 +681,7 @@
                 addLinkFlows(link, paths, counts.get(link));
             }
         }
-        return JsonUtils.envelope("showTraffic", 0, payload);
+        return JsonUtils.envelope(SHOW_HIGHLIGHTS, 0, payload);
     }
 
     private void addLinkFlows(Link link, ArrayNode paths, Integer count) {
@@ -723,7 +725,7 @@
             ((ArrayNode) pathNode.path("labels")).add(hasTraffic ? formatBytes(biLink.bytes) : "");
         }
 
-        return JsonUtils.envelope("showTraffic", 0, payload);
+        return JsonUtils.envelope(SHOW_HIGHLIGHTS, 0, payload);
     }
 
     // Classifies the link traffic according to the specified classes.
@@ -870,21 +872,13 @@
         result.set("props", pnode);
 
         ArrayNode buttons = arrayNode();
-        for (ButtonDescriptor b : pp.buttons()) {
-            buttons.add(json(b));
+        for (ButtonId b : pp.buttons()) {
+            buttons.add(b.id());
         }
         result.set("buttons", buttons);
         return result;
     }
 
-    // translates the button descriptor into JSON
-    private ObjectNode json(ButtonDescriptor bdesc) {
-        return objectNode()
-                .put("id", bdesc.id())
-                .put("gid", bdesc.glyphId())
-                .put("tt", bdesc.tooltip());
-    }
-
 
     // Produces canonical link key, i.e. one that will match link and its inverse.
     static LinkKey canonicalLinkKey(Link link) {
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficOverlay.java b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficOverlay.java
index 3c09bb4..6eec92f 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficOverlay.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficOverlay.java
@@ -18,6 +18,8 @@
 package org.onosproject.ui.impl;
 
 import org.onosproject.ui.UiTopoOverlay;
+import org.onosproject.ui.topo.ButtonId;
+import org.onosproject.ui.topo.PropertyPanel;
 
 /**
  * Topology Overlay for network traffic.
@@ -25,12 +27,21 @@
 public class TrafficOverlay extends UiTopoOverlay {
     private static final String TRAFFIC_ID = "traffic";
 
-    /**
-     * Constructs the traffic overlay.
-     */
+    private static final String SDF_ID = "showDeviceFlows";
+    private static final String SRT_ID = "showRelatedTraffic";
+
+    private static final ButtonId SHOW_DEVICE_FLOWS = new ButtonId(SDF_ID);
+    private static final ButtonId SHOW_RELATED_TRAFFIC = new ButtonId(SRT_ID);
+
+
     public TrafficOverlay() {
         super(TRAFFIC_ID);
     }
 
-    // TODO : override init(), activate(), deactivate(), destroy()
+    @Override
+    public void modifyDeviceDetails(PropertyPanel pp) {
+        pp.addButton(SHOW_DEVICE_FLOWS)
+            .addButton(SHOW_RELATED_TRAFFIC);
+    }
+
 }
diff --git a/web/gui/src/main/webapp/_sdh/overlaywork/AppUiTopoOverlay.java b/web/gui/src/main/webapp/_sdh/overlaywork/AppUiTopoOverlay.java
index f6a8341..c6d00cc 100644
--- a/web/gui/src/main/webapp/_sdh/overlaywork/AppUiTopoOverlay.java
+++ b/web/gui/src/main/webapp/_sdh/overlaywork/AppUiTopoOverlay.java
@@ -18,7 +18,7 @@
 package org.meowster.over;
 
 import org.onosproject.ui.UiTopoOverlay;
-import org.onosproject.ui.topo.ButtonDescriptor;
+import org.onosproject.ui.topo.ButtonId;
 import org.onosproject.ui.topo.PropertyPanel;
 import org.onosproject.ui.topo.TopoConstants.CoreButtons;
 import org.onosproject.ui.topo.TopoConstants.Glyphs;
@@ -36,12 +36,8 @@
     private static final String MY_TITLE = "I changed the title";
     private static final String MY_VERSION = "Beta-1.0.0042";
 
-    private static final ButtonDescriptor FOO_DESCRIPTOR =
-            new ButtonDescriptor("foo", "chain", "A FOO action");
-
-    private static final ButtonDescriptor BAR_DESCRIPTOR =
-            new ButtonDescriptor("bar", "*banner", "A BAR action");
-
+    private static final ButtonId FOO_BUTTON = new ButtonId("foo");
+    private static final ButtonId BAR_BUTTON = new ButtonId("bar");
 
     public AppUiTopoOverlay() {
         super(OVERLAY_ID);
@@ -68,8 +64,8 @@
         pp.title(MY_TITLE);
         pp.removeProps(LATITUDE, LONGITUDE);
 
-        pp.addButton(FOO_DESCRIPTOR)
-                .addButton(BAR_DESCRIPTOR);
+        pp.addButton(FOO_BUTTON)
+                .addButton(BAR_BUTTON);
 
         pp.removeButtons(CoreButtons.SHOW_PORT_VIEW)
                 .removeButtons(CoreButtons.SHOW_GROUP_VIEW);
diff --git a/web/gui/src/main/webapp/_sdh/overlaywork/topov.js b/web/gui/src/main/webapp/_sdh/overlaywork/topov.js
index 0b5eac8..068c30c 100644
--- a/web/gui/src/main/webapp/_sdh/overlaywork/topov.js
+++ b/web/gui/src/main/webapp/_sdh/overlaywork/topov.js
@@ -3,7 +3,10 @@
     'use strict';
 
     // injected refs
-    var $log;
+    var $log, tov;
+
+    // internal state
+    var someStateValue = true;
 
     // our overlay definition
     var overlay = {
@@ -27,40 +30,127 @@
             }
         },
 
-        activate: activateOverlay,
-        deactivate: deactivateOverlay,
+        activate: function () {
+            $log.debug("sample topology overlay ACTIVATED");
+        },
+        deactivate: function () {
+            $log.debug("sample topology overlay DEACTIVATED");
+        },
 
-        // button callbacks matching button identifiers
-        buttonActions: {
-            foo: fooCb,
-            bar: barCb
+
+
+
+
+        // detail panel button definitions
+        buttons: {
+            foo: {
+                gid: 'chain',
+                tt: 'A FOO action',
+                cb: function (data) {
+                    $log.debug('FOO action invoked with data:', data);
+                }
+            },
+            bar: {
+                gid: '*banner',
+                tt: 'A BAR action',
+                cb: function (data) {
+                    $log.debug('BAR action invoked with data:', data);
+                }
+            }
+        },
+
+        // Key bindings for traffic overlay buttons
+        // NOTE: fully qual. button ID is derived from overlay-id and key-name
+        keyBindings: {
+            V: {
+                cb: buttonCallback,
+                tt: 'Uses the V key',
+                gid: '*banner'
+            },
+            F: {
+                cb: buttonCallback,
+                tt: 'Uses the F key',
+                gid: 'chain'
+            },
+            G: {
+                cb: buttonCallback,
+                tt: 'Uses the G key',
+                gid: 'crown'
+            },
+
+            T: {
+                cb: buttonCallback,
+                tt: 'Uses the T key',
+                gid: 'switch'
+            },
+
+            R: {
+                cb: buttonCallback,
+                tt: 'Uses the R key',
+                gid: 'endstation'
+            },
+
+            0: {
+                cb: buttonCallback,
+                tt: 'Uses the ZERO key',
+                gid: 'xMark'
+            },
+
+            _keyOrder: [
+                '0', 'V', 'F', 'G', 'T', 'R'
+            ]
+
+            // NOTE: T and R should be rejected (not installed)
+            //       T is reserved for 'toggle Theme'
+            //       R is reserved for 'Reset pan and zoom'
+        },
+
+        hooks: {
+            // hook for handling escape key
+            // Must return true to consume ESC, false otherwise.
+            escape: cancelState,
+
+            // hooks for when the selection changes...
+            empty: function () {
+                selectionCallback('empty');
+            },
+            single: function (data) {
+                selectionCallback('single', data);
+            },
+            multi: function (selectOrder) {
+                selectionCallback('multi', selectOrder);
+                tov.addDetailButton('foo');
+                tov.addDetailButton('bar');
+            }
         }
+
     };
 
-    function fooCb(data) {
-        $log.debug('FOO callback with data:', data);
+    // invoked when the escape key is pressed
+    function cancelState() {
+        if (someStateValue) {
+            someStateValue = false;
+            // we consumed the ESC event
+            return true;
+        }
+        return false;
     }
 
-    function barCb(data) {
-        $log.debug('BAR callback with data:', data);
+    function buttonCallback(x) {
+        $log.debug('Toolbar-button callback', x);
     }
 
-    // === implementation of overlay API (essentially callbacks)
-    function activateOverlay() {
-        $log.debug("sample topology overlay ACTIVATED");
+    function selectionCallback(x, d) {
+        $log.debug('Selection callback', x, d);
     }
 
-    function deactivateOverlay() {
-        $log.debug("sample topology overlay DEACTIVATED");
-    }
-
-
     // invoke code to register with the overlay service
     angular.module('ovSample')
         .run(['$log', 'TopoOverlayService',
 
-            function (_$log_, tov) {
+            function (_$log_, _tov_) {
                 $log = _$log_;
+                tov = _tov_;
                 tov.register(overlay);
             }]);
 
diff --git a/web/gui/src/main/webapp/app/fw/svg/glyph.js b/web/gui/src/main/webapp/app/fw/svg/glyph.js
index b00f81d..838a2ac 100644
--- a/web/gui/src/main/webapp/app/fw/svg/glyph.js
+++ b/web/gui/src/main/webapp/app/fw/svg/glyph.js
@@ -45,6 +45,10 @@
             "C429.9,285.5,426.7,293.2,427.7,300.4z"
         },
 
+    // TODO: ONOS-2566 glyphs for device types:
+    //          otn, roadm_otn, firewall, balancer, ips, ids,
+    //          controller, virtual, fiber_switch, other
+
         glyphDataSet = {
             _viewbox: "0 0 110 110",
 
diff --git a/web/gui/src/main/webapp/app/fw/widget/toolbar.js b/web/gui/src/main/webapp/app/fw/widget/toolbar.js
index 00086dc..050afd0 100644
--- a/web/gui/src/main/webapp/app/fw/widget/toolbar.js
+++ b/web/gui/src/main/webapp/app/fw/widget/toolbar.js
@@ -18,6 +18,7 @@
  ONOS GUI -- Widget -- Toolbar Service
  */
 // TODO: Augment service to allow toolbars to exist on right edge of screen
+// TODO: also - make toolbar more object aware (rows etc.)
 
 
 (function () {
@@ -80,6 +81,7 @@
             panel = ps.createPanel(tbid, settings),
             arrowDiv = createArrow(panel),
             currentRow = panel.append('div').classed('tbar-row', true),
+            rowButtonIds = [],          // for removable buttons
             tbWidth = arrowSize + 2,    // empty toolbar width
             maxWidth = panel.width();
 
@@ -162,9 +164,43 @@
             } else {
                 panel.append('br');
                 currentRow = panel.append('div').classed('tbar-row', true);
+
+                // return API to allow caller more access to the row
+                return {
+                    clear: rowClear,
+                    setText: rowSetText,
+                    addButton: rowAddButton,
+                    classed: rowClassed
+                };
             }
         }
 
+        function rowClear() {
+            currentRow.selectAll('*').remove();
+            rowButtonIds.forEach(function (bid) {
+                delete items[bid];
+            });
+            rowButtonIds = [];
+        }
+
+        // installs a div with text into the button row
+        function rowSetText(text) {
+            rowClear();
+            currentRow.append('div').classed('tbar-row-text', true)
+                .html(text);
+        }
+
+        function rowAddButton(id, gid, cb, tooltip) {
+            var b = addButton(id, gid, cb, tooltip);
+            if (b) {
+                rowButtonIds.push(id);
+            }
+        }
+
+        function rowClassed(classes, bool) {
+            currentRow.classed(classes, bool);
+        }
+
         function show(cb) {
             rotateArrowLeft(arrowDiv);
             panel.show(cb);
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.css b/web/gui/src/main/webapp/app/view/topo/topo.css
index d1d4b4c..f26f478 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.css
+++ b/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -305,6 +305,21 @@
     filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"yellow-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0  1.0 0 0 0 0  1.0 0 0 0 0  0.3 0 0 0 1  0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#yellow-glow");
 }
 
+
+/* --- Toolbar --- */
+
+#toolbar-topo-tbar .tbar-row.right {
+    width: 100%;
+}
+
+#toolbar-topo-tbar .tbar-row-text {
+    height: 21px;
+    text-align: right;
+    padding: 8px 60px 0 0;
+    font-style: italic;
+}
+
+
 /* --- Topo Nodes --- */
 
 #ov-topo svg .suppressed {
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index f46645e..313673f 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -40,7 +40,7 @@
 
     // --- Short Cut Keys ------------------------------------------------
 
-    function setUpKeys() {
+    function setUpKeys(overlayKeys) {
         // key bindings need to be made after the services have been injected
         // thus, deferred to here...
         actionMap = {
@@ -63,14 +63,6 @@
             R: [resetZoom, 'Reset pan / zoom'],
             dot: [ttbs.toggleToolbar, 'Toggle Toolbar'],
 
-            V: [tts.showRelatedIntentsAction, 'Show all related intents'],
-            rightArrow: [tts.showNextIntentAction, 'Show next related intent'],
-            leftArrow: [tts.showPrevIntentAction, 'Show previous related intent'],
-            W: [tts.showSelectedIntentTrafficAction, 'Monitor traffic of selected intent'],
-            A: [tts.showAllFlowTrafficAction, 'Monitor all traffic using flow stats'],
-            Q: [tts.showAllPortTrafficAction, 'Monitor all traffic using port stats'],
-            F: [tts.showDeviceLinkFlowsAction, 'Show device link flows'],
-
             E: [equalizeMasters, 'Equalize mastership roles'],
 
             esc: handleEscape,
@@ -78,12 +70,16 @@
             _keyListener: ttbs.keyListener,
 
             _helpFormat: [
-                ['I', 'O', 'D', '-', 'H', 'M', 'P', 'dash', 'B' ],
-                ['X', 'Z', 'N', 'L', 'U', 'R', '-', 'dot'],
-                ['V', 'rightArrow', 'leftArrow', 'W', 'A', 'F', '-', 'E' ]
+                ['I', 'O', 'D', 'H', 'M', 'P', 'dash', 'B', 'S' ],
+                ['X', 'Z', 'N', 'L', 'U', 'R', '-', 'E', '-', 'dot'],
+                []   // this column reserved for overlay actions
             ]
         };
 
+        if (fs.isO(overlayKeys)) {
+            mergeKeys(overlayKeys);
+        }
+
         ks.keyBindings(actionMap);
 
         ks.gestureNotes([
@@ -95,6 +91,22 @@
         ]);
     }
 
+    // when a topology overlay is activated, we need to bind their keystrokes
+    // and include them in the quick-help panel
+    function mergeKeys(extra) {
+        var _hf = actionMap._helpFormat[2];
+        extra._keyOrder.forEach(function (k) {
+            var d = extra[k],
+                cb = d && d.cb,
+                tt = d && d.tt;
+            // NOTE: ignore keys that are already defined
+            if (d && !actionMap[k]) {
+                actionMap[k] = [cb, tt];
+                _hf.push(k);
+            }
+        });
+    }
+
     // --- Keystroke functions -------------------------------------------
 
     function toggleInstances(x) {
@@ -153,6 +165,10 @@
             // if an instance is selected, cancel the affinity mapping
             tis.cancelAffinity()
 
+        } else if (tov.hooks.escape()) {
+            // else if the overlay consumed the ESC event...
+            // (work already done)
+
         } else if (tss.deselectAll()) {
             // else if we have node selections, deselect them all
             // (work already done)
@@ -169,19 +185,15 @@
         } else if (tps.summaryVisible()) {
             // else if the Summary Panel is visible, hide it
             tps.hideSummaryPanel();
-
-        } else {
-            // TODO: set hover mode to hoverModeNone
-            // talk to Thomas about this: shouldn't it be done
-            // when we deselect the node (if tss.haveDetails()...)
         }
     }
 
     // --- Toolbar Functions ---------------------------------------------
 
     function notValid(what) {
-        $log.warn('Topo.js getActionEntry(): Not a valid ' + what);
+        $log.warn('topo.js getActionEntry(): Not a valid ' + what);
     }
+
     function getActionEntry(key) {
         var entry;
 
@@ -201,7 +213,8 @@
 
     function setUpToolbar() {
         ttbs.init({
-            getActionEntry: getActionEntry
+            getActionEntry: getActionEntry,
+            setUpKeys: setUpKeys
         });
         ttbs.createToolbar();
     }
@@ -503,7 +516,6 @@
             restoreConfigFromPrefs();
 
             $log.debug('registered overlays...', tov.list());
-
             $log.log('OvTopoCtrl has been created');
         }]);
 }());
diff --git a/web/gui/src/main/webapp/app/view/topo/topoEvent.js b/web/gui/src/main/webapp/app/view/topo/topoEvent.js
index e6c943d..5fd38bf 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoEvent.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoEvent.js
@@ -27,14 +27,14 @@
     'use strict';
 
     // injected refs
-    var $log, $interval, wss, tps, tis, tfs, tss, tts, tspr;
+    var $log, $interval, wss, tps, tis, tfs, tss, tov, tspr;
 
     // internal state
     var handlerMap,
         openListener,
         heartbeatTimer;
 
-    var heartbeatPeriod = 5000; // 5 seconds
+    var heartbeatPeriod = 9000; // 9 seconds
 
     // ==========================
 
@@ -44,7 +44,7 @@
 
             showDetails: tss,
 
-            showTraffic: tts,
+            showHighlights: tov,
 
             addInstance: tis,
             updateInstance: tis,
@@ -90,10 +90,10 @@
     .factory('TopoEventService',
         ['$log', '$interval', 'WebSocketService',
             'TopoPanelService', 'TopoInstService', 'TopoForceService',
-            'TopoSelectService', 'TopoTrafficService', 'TopoSpriteService',
+            'TopoSelectService', 'TopoOverlayService', 'TopoSpriteService',
 
         function (_$log_,  _$interval_, _wss_,
-                  _tps_, _tis_, _tfs_, _tss_, _tts_, _tspr_) {
+                  _tps_, _tis_, _tfs_, _tss_, _tov_, _tspr_) {
             $log = _$log_;
             $interval = _$interval_;
             wss = _wss_;
@@ -101,7 +101,7 @@
             tis = _tis_;
             tfs = _tfs_;
             tss = _tss_;
-            tts = _tts_;
+            tov = _tov_;
             tspr = _tspr_;
 
             createHandlerMap();
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 963a370..0595393 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -23,7 +23,7 @@
     'use strict';
 
     // injected refs
-    var $log, $timeout, fs, sus, is, ts, flash, wss,
+    var $log, $timeout, fs, sus, ts, flash, wss, tov,
         tis, tms, td3, tss, tts, tos, fltr, tls, uplink, svg;
 
     // configuration
@@ -797,9 +797,10 @@
         return true;
     }
 
-    // ==========================
-    // function entry points for traffic module
+    // =============================================
+    // function entry points for overlay module
 
+    // TODO: find an automatic way of tracking via the "showHighlights" events
     var allTrafficClasses = 'primary secondary optical animated ' +
         'port-traffic-Kbps port-traffic-Mbps port-traffic-Gbps ' +
         'port-traffic-Gbps-choked';
@@ -845,7 +846,7 @@
         };
     }
 
-    function mkD3Api(uplink) {
+    function mkD3Api() {
         return {
             node: function () { return node; },
             link: function () { return link; },
@@ -859,7 +860,7 @@
         };
     }
 
-    function mkSelectApi(uplink) {
+    function mkSelectApi() {
         return {
             node: function () { return node; },
             zoomingOrPanning: zoomingOrPanning,
@@ -868,15 +869,20 @@
         };
     }
 
-    function mkTrafficApi(uplink) {
+    function mkTrafficApi() {
+        return {
+            hovered: tss.hovered,
+            somethingSelected: tss.somethingSelected,
+            selectOrder: tss.selectOrder
+        };
+    }
+
+    function mkOverlayApi() {
         return {
             clearLinkTrafficStyle: clearLinkTrafficStyle,
             removeLinkLabels: removeLinkLabels,
             updateLinks: updateLinks,
-            findLinkById: tms.findLinkById,
-            hovered: tss.hovered,
-            validateSelectionContext: tss.validateSelectionContext,
-            selectOrder: tss.selectOrder
+            findLinkById: tms.findLinkById
         };
     }
 
@@ -904,7 +910,7 @@
         };
     }
 
-    function mkFilterApi(uplink) {
+    function mkFilterApi() {
         return {
             node: function () { return node; },
             link: function () { return link; }
@@ -925,11 +931,11 @@
     .factory('TopoForceService',
         ['$log', '$timeout', 'FnService', 'SvgUtilService',
             'ThemeService', 'FlashService', 'WebSocketService',
-            'TopoInstService', 'TopoModelService',
+            'TopoOverlayService', 'TopoInstService', 'TopoModelService',
             'TopoD3Service', 'TopoSelectService', 'TopoTrafficService',
             'TopoObliqueService', 'TopoFilterService', 'TopoLinkService',
 
-        function (_$log_, _$timeout_, _fs_, _sus_, _ts_, _flash_, _wss_,
+        function (_$log_, _$timeout_, _fs_, _sus_, _ts_, _flash_, _wss_, _tov_,
                   _tis_, _tms_, _td3_, _tss_, _tts_, _tos_, _fltr_, _tls_) {
             $log = _$log_;
             $timeout = _$timeout_;
@@ -938,6 +944,7 @@
             ts = _ts_;
             flash = _flash_;
             wss = _wss_;
+            tov = _tov_;
             tis = _tis_;
             tms = _tms_;
             td3 = _td3_;
@@ -966,12 +973,13 @@
 
                 $log.debug('initForce().. dim = ' + dim);
 
+                tov.setApi(mkOverlayApi(), tss);
                 tms.initModel(mkModelApi(uplink), dim);
-                td3.initD3(mkD3Api(uplink));
-                tss.initSelect(mkSelectApi(uplink));
-                tts.initTraffic(mkTrafficApi(uplink));
+                td3.initD3(mkD3Api());
+                tss.initSelect(mkSelectApi());
+                tts.initTraffic(mkTrafficApi());
                 tos.initOblique(mkObliqueApi(uplink, fltr));
-                fltr.initFilter(mkFilterApi(uplink));
+                fltr.initFilter(mkFilterApi());
                 tls.initLink(mkLinkApi(svg, uplink), td3);
 
                 settings = angular.extend({}, defaultSettings, opts);
@@ -1016,6 +1024,7 @@
                 tss.destroySelect();
                 td3.destroyD3();
                 tms.destroyModel();
+                // note: no need to destroy overlay service
                 ts.removeListener(themeListener);
                 themeListener = null;
 
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 f2b81f5..41c8e1e 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
@@ -30,7 +30,7 @@
     var tos = 'TopoOverlayService: ';
 
     // injected refs
-    var $log, fs, gs, wss, ns;
+    var $log, fs, gs, wss, ns, tss, tps, api;
 
     // internal state
     var overlays = {},
@@ -80,6 +80,7 @@
     function register(overlay) {
         var r = 'register',
             over = fs.isO(overlay),
+            kb = over ? fs.isO(overlay.keyBindings) : null,
             id = over ? over.overlayId : '';
 
         if (!id) {
@@ -90,11 +91,26 @@
         }
         overlays[id] = overlay;
         handleGlyphs(overlay);
+
+        if (kb) {
+            if (!fs.isA(kb._keyOrder)) {
+                warn(r, 'no _keyOrder array defined on keyBindings');
+            } else {
+                kb._keyOrder.forEach(function (k) {
+                    if (k !== '-' && !kb[k]) {
+                        warn(r, 'no "' + k + '" property defined on keyBindings');
+                    }
+                });
+            }
+        }
+
         $log.debug(tos + 'registered overlay: ' + id, overlay);
     }
 
+    // TODO: remove this redundant code.......
     // NOTE: unregister needs to be called if an app is ever
     //       deactivated/uninstalled via the applications view
+/*
     function unregister(overlay) {
         var u = 'unregister',
             over = fs.isO(overlay),
@@ -108,21 +124,33 @@
         }
         delete overlays[id];
         $log.debug(tos + 'unregistered overlay: ' + id);
-        // TODO: rebuild the toolbar overlay radio button set
     }
+*/
 
+
+    // returns the list of overlay identifiers
     function list() {
         return d3.map(overlays).keys();
     }
 
-    function overlay(id) {
-        return overlays[id];
+    // add a radio button for each registered overlay
+    function augmentRbset(rset, switchFn) {
+        angular.forEach(overlays, function (ov) {
+            rset.push({
+                gid: ov._glyphId,
+                tooltip: (ov.tooltip || '(no tooltip)'),
+                cb: function () {
+                    tbSelection(ov.overlayId, switchFn);
+                }
+            });
+        });
     }
 
     // an overlay was selected via toolbar radio button press from user
-    function tbSelection(id) {
+    function tbSelection(id, switchFn) {
         var same = current && current.overlayId === id,
-            payload = {};
+            payload = {},
+            actions;
 
         function doop(op) {
             var oid = current.overlayId;
@@ -133,70 +161,211 @@
 
         if (!same) {
             current && doop('deactivate');
-            current = overlay(id);
+            current = overlays[id];
             current && doop('activate');
+            actions = current && fs.isO(current.keyBindings);
+            switchFn(id, actions);
+
             wss.sendEvent('topoSelectOverlay', payload);
 
-            // TODO: refactor to emit "flush on overlay change" messages
+            // Ensure summary and details panels are updated immediately..
             wss.sendEvent('requestSummary');
+            tss.updateDetail();
         }
     }
 
-    var coreButtonPath = {
-        showDeviceView: 'device',
-        showFlowView: 'flow',
-        showPortView: 'port',
-        showGroupView: 'group'
+    var coreButtons = {
+        showDeviceView: {
+            gid: 'switch',
+            tt: 'Show Device View',
+            path: 'device'
+        },
+        showFlowView: {
+            gid: 'flowTable',
+            tt: 'Show Flow View for this Device',
+            path: 'flow'
+        },
+        showPortView: {
+            gid: 'portTable',
+            tt: 'Show Port View for this Device',
+            path: 'port'
+        },
+        showGroupView: {
+            gid: 'groupTable',
+            tt: 'Show Group View for this Device',
+            path: 'group'
+        }
     };
 
+    // retrieves a button definition from the current overlay and generates
+    //  a button descriptor to be added to the panel, with the data baked in
+    function _getButtonDef(id, data) {
+        var btns = current && current.buttons,
+            b = btns && btns[id],
+            cb = fs.isF(b.cb),
+            f = cb ? function () { cb(data); } : function () {};
+
+        return b ? {
+            id: current.mkId(id),
+            gid: current.mkGid(b.gid),
+            tt: b.tt,
+            cb: f
+        } : null;
+    }
+
     // install core buttons, and include any additional from the current overlay
-    function installButtons(buttons, addFn, data, devId) {
+    function installButtons(buttons, data, devId) {
+        buttons.forEach(function (id) {
+            var btn = coreButtons[id],
+                gid = btn && btn.gid,
+                tt = btn && btn.tt,
+                path = btn && btn.path;
 
-        angular.forEach(buttons, function (btn) {
-            var path = coreButtonPath[btn.id],
-                _id,
-                _gid,
-                _cb,
-                action;
-
-            if (path) {
-                // core callback function
-                _id = btn.id;
-                _gid = btn.gid;
-                action = function () {
-                    ns.navTo(path, { devId: devId });
-                };
-            } else if (current) {
-                _id = current.mkId(btn.id);
-                _gid = current.mkGid(btn.gid);
-                action = current.buttonActions[btn.id] || function () {};
+            if (btn) {
+                tps.addAction({
+                    id: 'core-' + id,
+                    gid: gid,
+                    tt: tt,
+                    cb: function () { ns.navTo(path, {devId: devId }); }
+                });
+            } else if (btn = _getButtonDef(id, data)) {
+                tps.addAction(btn);
             }
+        });
+    }
 
-            _cb = function () { action(data); };
+    function addDetailButton(id) {
+        var b = _getButtonDef(id);
+        if (b) {
+            tps.addAction({
+                id: current.mkId(id),
+                gid: current.mkGid(b.gid),
+                cb: b.cb,
+                tt: b.tt
+            });
+        }
+    }
 
-            addFn({ id: _id, gid: _gid, cb: _cb, tt: btn.tt});
+
+    // === -----------------------------------------------------
+    //  Hooks for overlays
+
+    function _hook(x) {
+        var h = current && current.hooks;
+        return h && fs.isF(h[x]);
+    }
+
+    function escapeHook() {
+        var eh = _hook('escape');
+        return eh ? eh() : false;
+    }
+
+    function emptySelectHook() {
+        var cb = _hook('empty');
+        cb && cb();
+    }
+
+    function singleSelectHook(data) {
+        var cb = _hook('single');
+        cb && cb(data);
+    }
+
+    function multiSelectHook(selectOrder) {
+        var cb = _hook('multi');
+        cb && cb(selectOrder);
+    }
+
+    // === -----------------------------------------------------
+    //  Event (from server) Handlers
+
+    function setApi(_api_, _tss_) {
+        api = _api_;
+        tss = _tss_;
+    }
+
+    // TODO: refactor this (currently using showTraffic data structure)
+    function showHighlights(data) {
+        /*
+           API to topoForce
+             clearLinkTrafficStyle()
+             removeLinkLabels()
+             updateLinks()
+             findLinkById( id )
+         */
+
+        var paths = data.paths;
+
+        api.clearLinkTrafficStyle();
+        api.removeLinkLabels();
+
+        // Now highlight all links in the paths payload, and attach
+        //  labels to them, if they are defined.
+        paths.forEach(function (p) {
+            var n = p.links.length,
+                i, ldata, lab, units, magnitude, portcls;
+
+            for (i=0; i<n; i++) {
+                ldata = api.findLinkById(p.links[i]);
+                lab = p.labels[i];
+
+                if (ldata && !ldata.el.empty()) {
+                    ldata.el.classed(p.class, true);
+                    ldata.label = lab;
+
+                    if (fs.endsWith(lab, 'bps')) {
+                        // inject additional styling for port-based traffic
+                        units = lab.substring(lab.length-4);
+                        portcls = 'port-traffic-' + units;
+
+                        // for GBps
+                        if (units.substring(0,1) === 'G') {
+                            magnitude = fs.parseBitRate(lab);
+                            if (magnitude >= 9) {
+                                portcls += '-choked'
+                            }
+                        }
+                        ldata.el.classed(portcls, true);
+                    }
+                }
+            }
         });
 
+        api.updateLinks();
     }
 
+    // ========================================================================
+
     angular.module('ovTopo')
     .factory('TopoOverlayService',
         ['$log', 'FnService', 'GlyphService', 'WebSocketService', 'NavService',
+            'TopoPanelService',
 
-        function (_$log_, _fs_, _gs_, _wss_, _ns_) {
+        function (_$log_, _fs_, _gs_, _wss_, _ns_, _tps_) {
             $log = _$log_;
             fs = _fs_;
             gs = _gs_;
             wss = _wss_;
             ns = _ns_;
+            tps = _tps_;
 
             return {
                 register: register,
-                unregister: unregister,
+                //unregister: unregister,
+                setApi: setApi,
                 list: list,
-                overlay: overlay,
+                augmentRbset: augmentRbset,
+                mkGlyphId: mkGlyphId,
                 tbSelection: tbSelection,
-                installButtons: installButtons
+                installButtons: installButtons,
+                addDetailButton: addDetailButton,
+                hooks: {
+                    escape: escapeHook,
+                    emptySelect: emptySelectHook,
+                    singleSelect: singleSelectHook,
+                    multiSelect: multiSelectHook
+                },
+
+                showHighlights: showHighlights
             }
         }]);
 
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 fe2f7a1..2e73ea2 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoSelect.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoSelect.js
@@ -40,12 +40,6 @@
         selectOrder = [],       // the order in which we made selections
         consumeClick = false;   // used to coordinate with SVG click handler
 
-    // constants
-    var devPath = 'device',
-        flowPath = 'flow',
-        portPath ='port',
-        groupPath = 'group';
-
     // ==========================
 
     function nSel() {
@@ -157,8 +151,7 @@
 
     // === -----------------------------------------------------
 
-    function requestDetails() {
-        var data = getSel(0).obj;
+    function requestDetails(data) {
         wss.sendEvent('requestDetails', {
             id: data.id,
             class: data.class
@@ -179,91 +172,62 @@
     }
 
     function emptySelect() {
-        tts.cancelTraffic();
+        tov.hooks.emptySelect();
         tps.displayNothing();
     }
 
     function singleSelect() {
-        // NOTE: detail is shown from 'showDetails' event callback
-        requestDetails();
-        tts.cancelTraffic();
-        tts.requestTrafficForMode();
+        var data = getSel(0).obj;
+        requestDetails(data);
+        // NOTE: detail panel is shown as a response to receiving
+        //       a 'showDetails' event from the server. See 'showDetails'
+        //       callback function below...
     }
 
     function multiSelect() {
         // display the selected nodes in the detail panel
         tps.displayMulti(selectOrder);
-
-        // always add the 'show traffic' action
-        tps.addAction({
-            id: '-mult-rel-traf-btn',
-            gid: 'allTraffic',
-            cb:  tts.showRelatedIntentsAction,
-            tt: 'Show Related Traffic'
-        });
-
-        // add other actions, based on what is selected...
-        if (nSel() === 2 && allSelectionsClass('host')) {
-            tps.addAction({
-                id: 'host-flow-btn',
-                gid: 'endstation',
-                cb: tts.addHostIntentAction,
-                tt: 'Create Host-to-Host Flow'
-            });
-        } else if (nSel() >= 2 && allSelectionsClass('host')) {
-            tps.addAction({
-                id: 'mult-src-flow-btn',
-                gid: 'flows',
-                cb: tts.addMultiSourceIntentAction,
-                tt: 'Create Multi-Source Flow'
-            });
-        }
-
-        tts.cancelTraffic();
-        tts.requestTrafficForMode();
+        addHostSelectionActions();
+        tov.hooks.multiSelect(selectOrder);
         tps.displaySomething();
     }
 
+    function addHostSelectionActions() {
+        if (allSelectionsClass('host')) {
+            if (nSel() === 2) {
+                tps.addAction({
+                    id: 'host-flow-btn',
+                    gid: 'endstation',
+                    cb: tts.addHostIntent,
+                    tt: 'Create Host-to-Host Flow'
+                });
+            } else if (nSel() >= 2) {
+                tps.addAction({
+                    id: 'mult-src-flow-btn',
+                    gid: 'flows',
+                    cb: tts.addMultiSourceIntent,
+                    tt: 'Create Multi-Source Flow'
+                });
+            }
+        }
+    }
+
 
     // === -----------------------------------------------------
     //  Event Handlers
 
+    // display the data for the single selected node
     function showDetails(data) {
         var buttons = fs.isA(data.buttons) || [];
-
-        // display the data for the single selected node
         tps.displaySingle(data);
-
-        tov.installButtons(buttons, tps.addAction, data, data.props['URI']);
-
-        // TODO: MOVE traffic buttons to the traffic overlay
-        // always add the 'show traffic' action
-        tps.addAction({
-            id: '-sin-rel-traf-btn',
-            gid: 'intentTraffic',
-            cb: tts.showRelatedIntentsAction,
-            tt: 'Show Related Traffic'
-        });
-
-        // add other actions, based on what is selected...
-        if (data.type === 'switch') {
-            tps.addAction({
-                id: 'sin-dev-flows-btn',
-                gid: 'flows',
-                cb: tts.showDeviceLinkFlowsAction,
-                tt: 'Show Device Flows'
-            });
-        }
-
+        tov.installButtons(buttons, data, data.props['URI']);
+        tov.hooks.singleSelect(data);
         tps.displaySomething();
     }
 
-    function validateSelectionContext() {
-        if (!hovered && !nSel()) {
-            tts.cancelTraffic();
-            return false;
-        }
-        return true;
+    // returns true if we are hovering over a node, or any nodes are selected
+    function somethingSelected() {
+        return hovered || nSel();
     }
 
     function clickConsumed(x) {
@@ -306,10 +270,11 @@
                 selectObject: selectObject,
                 deselectObject: deselectObject,
                 deselectAll: deselectAll,
+                updateDetail: updateDetail,
 
                 hovered: function () { return hovered; },
                 selectOrder: function () { return selectOrder; },
-                validateSelectionContext: validateSelectionContext,
+                somethingSelected: somethingSelected,
 
                 clickConsumed: clickConsumed
             };
diff --git a/web/gui/src/main/webapp/app/view/topo/topoToolbar.js b/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
index cbf443a..84de261 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
@@ -25,19 +25,25 @@
     // injected references
     var $log, fs, tbs, ps, tov, api;
 
+    // API:
+    //  getActionEntry
+    //  setUpKeys
+
     // internal state
-    var toolbar, keyData, cachedState;
+    var toolbar, keyData, cachedState, thirdRow;
 
     // constants
     var name = 'topo-tbar',
-        cooktag = 'topo_prefs';
+        cooktag = 'topo_prefs',
+        soa = 'switchOverlayActions: ',
+        selOver = 'Select overlay here &#x21e7;';
+
 
     // key to button mapping data
     var k2b = {
         O: { id: 'summary-tog', gid: 'summary', isel: true},
         I: { id: 'instance-tog', gid: 'uiAttached', isel: true },
         D: { id: 'details-tog', gid: 'details', isel: true },
-
         H: { id: 'hosts-tog', gid: 'endstation', isel: false },
         M: { id: 'offline-tog', gid: 'switch', isel: true },
         P: { id: 'ports-tog', gid: 'ports', isel: true },
@@ -50,16 +56,16 @@
         L: { id: 'cycleLabels-btn', gid: 'cycleLabels' },
         R: { id: 'resetZoom-btn', gid: 'resetZoom' },
 
-        E: { id: 'eqMaster-btn', gid: 'eqMaster' },
-
-        V: { id: 'relatedIntents-btn', gid: 'relatedIntents' },
-        leftArrow: { id: 'prevIntent-btn', gid: 'prevIntent' },
-        rightArrow: { id: 'nextIntent-btn', gid: 'nextIntent' },
-        W: { id: 'intentTraffic-btn', gid: 'intentTraffic' },
-        A: { id: 'allTraffic-btn', gid: 'allTraffic' },
-        F: { id: 'flows-btn', gid: 'flows' }
+        E: { id: 'eqMaster-btn', gid: 'eqMaster' }
     };
 
+    var prohibited = [
+        'T', 'backSlash', 'slash',
+        'X' // needed until we re-instate X above.
+    ];
+    prohibited = prohibited.concat(d3.map(k2b).keys());
+
+
     // initial toggle state: default settings and tag to key mapping
     var defaultPrefsState = {
             summary: 1,
@@ -112,6 +118,7 @@
     }
 
     function initKeyData() {
+        // TODO: use angular forEach instead of d3.map
         keyData = d3.map(k2b);
         keyData.forEach(function(key, value) {
             var data = api.getActionEntry(key);
@@ -124,6 +131,7 @@
         var v = keyData.get(key);
         v.btn = toolbar.addButton(v.id, v.gid, v.cb, v.tt);
     }
+
     function addToggle(key, suppressIfMobile) {
         var v = keyData.get(key);
         if (suppressIfMobile && fs.isMobile()) { return; }
@@ -158,36 +166,60 @@
 
         // generate radio button set for overlays; start with 'none'
         var rset = [{
-                gid: 'unknown',
+                gid: 'topo',
                 tooltip: 'No Overlay',
                 cb: function () {
-                    tov.tbSelection(null);
+                    tov.tbSelection(null, switchOverlayActions);
                 }
             }];
-
-        tov.list().forEach(function (key) {
-            var ov = tov.overlay(key);
-            rset.push({
-                gid: ov._glyphId,
-                tooltip: (ov.tooltip || '(no tooltip)'),
-                cb: function () {
-                    tov.tbSelection(ov.overlayId);
-                }
-            });
-        });
-
+        tov.augmentRbset(rset, switchOverlayActions);
         toolbar.addRadioSet('topo-overlays', rset);
     }
 
-    // TODO: 3rd row needs to be swapped in/out based on selected overlay
-    // NOTE: This particular row of buttons is for the traffic overlay
-    function addThirdRow() {
-        addButton('V');
-        addButton('leftArrow');
-        addButton('rightArrow');
-        addButton('W');
-        addButton('A');
-        addButton('F');
+    // invoked by overlay service to switch out old buttons and switch in new
+    function switchOverlayActions(oid, keyBindings) {
+        var prohibits = [],
+            kb = fs.isO(keyBindings) || {},
+            order = fs.isA(kb._keyOrder) || [];
+
+        if (keyBindings && !keyBindings._keyOrder) {
+            $log.warn(soa + 'no _keyOrder property defined');
+        } else {
+            // sanity removal of reserved property names
+            ['esc', '_keyListener', '_helpFormat'].forEach(function (k) {
+                fs.removeFromArray(k, order);
+            });
+        }
+
+        thirdRow.clear();
+
+        if (!order.length) {
+            thirdRow.setText(selOver);
+            thirdRow.classed('right', true);
+            api.setUpKeys(); // clear previous overlay key bindings
+
+        } else {
+            thirdRow.classed('right', false);
+            angular.forEach(order, function (key) {
+                var value, bid, gid, tt;
+
+                if (prohibited.indexOf(key) > -1) {
+                    prohibits.push(key);
+
+                } else {
+                    value = keyBindings[key];
+                    bid = oid + '-' + key;
+                    gid = tov.mkGlyphId(oid, value.gid);
+                    tt = value.tt + ' (' + key + ')';
+                    thirdRow.addButton(bid, gid, value.cb, tt);
+                }
+            });
+            api.setUpKeys(keyBindings); // add overlay key bindings
+        }
+
+        if (prohibits.length) {
+            $log.warn(soa + 'Prohibited key bindings ignored:', prohibits);
+        }
     }
 
     function createToolbar() {
@@ -197,8 +229,9 @@
         toolbar.addRow();
         addSecondRow();
         addOverlays();
-        toolbar.addRow();
-        addThirdRow();
+        thirdRow = toolbar.addRow();
+        thirdRow.setText(selOver);
+        thirdRow.classed('right', true);
 
         if (cachedState.toolbar) {
             toolbar.show();
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 7332ad0..27ec979 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoTraffic.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoTraffic.js
@@ -23,85 +23,44 @@
     'use strict';
 
     // injected refs
-    var $log, fs, flash, wss;
+    var $log, fs, flash, wss, api;
 
-    // api to topoForce
-    var api;
     /*
-     clearLinkTrafficStyle()
-     removeLinkLabels()
-     updateLinks()
-     findLinkById( id )
-     hovered()
-     validateSelectionContext()
+       API to topoForce
+         hovered()
+         somethingSelected()
+         selectOrder()
      */
 
-    // constants
-    var hoverModeNone = 0,
-        hoverModeAll = 1,
-        hoverModeFlows = 2,
-        hoverModeIntents = 3;
-
     // internal state
-    var hoverMode = hoverModeNone;
+    var trafficMode = null,
+        hoverMode = null;
 
 
     // === -----------------------------------------------------
-    //  Event Handlers
-
-    function showTraffic(data) {
-        var paths = data.paths;
-
-        api.clearLinkTrafficStyle();
-        api.removeLinkLabels();
-
-        // Now highlight all links in the paths payload, and attach
-        //  labels to them, if they are defined.
-        paths.forEach(function (p) {
-            var n = p.links.length,
-                i, ldata, lab, units, magnitude, portcls;
-
-            for (i=0; i<n; i++) {
-                ldata = api.findLinkById(p.links[i]);
-                lab = p.labels[i];
-
-                if (ldata && !ldata.el.empty()) {
-                    ldata.el.classed(p.class, true);
-                    ldata.label = lab;
-
-                    if (fs.endsWith(lab, 'bps')) {
-                        // inject additional styling for port-based traffic
-                        units = lab.substring(lab.length-4);
-                        portcls = 'port-traffic-' + units;
-
-                        // for GBps
-                        if (units.substring(0,1) === 'G') {
-                            magnitude = fs.parseBitRate(lab);
-                            if (magnitude >= 9) {
-                                portcls += '-choked'
-                            }
-                        }
-                        ldata.el.classed(portcls, true);
-                    }
-                }
-            }
-        });
-
-        api.updateLinks();
-    }
-
-    // === -----------------------------------------------------
     //  Helper functions
 
+    // invoked in response to change in selection and/or mouseover/out:
+    function requestTrafficForMode() {
+        if (hoverMode === 'flows') {
+            requestDeviceLinkFlows();
+        } else if (hoverMode === 'intents') {
+            requestRelatedIntents();
+        } else {
+            cancelTraffic();
+        }
+    }
+
     function requestDeviceLinkFlows() {
+        // generates payload based on current hover-state
         var hov = api.hovered();
 
         function hoverValid() {
-            return hoverMode === hoverModeFlows &&
+            return hoverMode === 'flows' &&
                 hov && (hov.class === 'device');
         }
 
-        if (api.validateSelectionContext()) {
+        if (api.somethingSelected()) {
             wss.sendEvent('requestDeviceLinkFlows', {
                 ids: api.selectOrder(),
                 hover: hoverValid() ? hov.id : ''
@@ -110,14 +69,15 @@
     }
 
     function requestRelatedIntents() {
+        // generates payload based on current hover-state
         var hov = api.hovered();
 
         function hoverValid() {
-            return hoverMode === hoverModeIntents &&
+            return hoverMode === 'intents' &&
                 hov && (hov.class === 'host' || hov.class === 'device');
         }
 
-        if (api.validateSelectionContext()) {
+        if (api.somethingSelected()) {
             wss.sendEvent('requestRelatedIntents', {
                 ids: api.selectOrder(),
                 hover: hoverValid() ? hov.id : ''
@@ -126,71 +86,75 @@
     }
 
 
-    // === -----------------------------------------------------
-    //  Traffic requests
+    // === -------------------------------------------------------------
+    //  Traffic requests invoked from keystrokes or toolbar buttons...
 
     function cancelTraffic() {
-        wss.sendEvent('cancelTraffic');
-    }
-
-    // invoked in response to change in selection and/or mouseover/out:
-    function requestTrafficForMode() {
-        if (hoverMode === hoverModeFlows) {
-            requestDeviceLinkFlows();
-        } else if (hoverMode === hoverModeIntents) {
-            requestRelatedIntents();
+        if (!trafficMode) {
+            return false;
         }
+
+        trafficMode = hoverMode = null;
+        wss.sendEvent('cancelTraffic');
+        flash.flash('Traffic monitoring canceled');
+        return true;
     }
 
-    // === -----------------------------
-    // keystroke commands
-
-    // keystroke-right-arrow (see topo.js)
-    function showNextIntentAction() {
-        hoverMode = hoverModeNone;
-        wss.sendEvent('requestNextRelatedIntent');
-        flash.flash('Next related intent');
-    }
-
-    // keystroke-left-arrow (see topo.js)
-    function showPrevIntentAction() {
-        hoverMode = hoverModeNone;
-        wss.sendEvent('requestPrevRelatedIntent');
-        flash.flash('Previous related intent');
-    }
-
-    // keystroke-W (see topo.js)
-    function showSelectedIntentTrafficAction() {
-        hoverMode = hoverModeNone;
-        wss.sendEvent('requestSelectedIntentTraffic');
-        flash.flash('Traffic on Selected Path');
-    }
-
-    // keystroke-A (see topo.js)
-    function showAllFlowTrafficAction() {
-        hoverMode = hoverModeAll;
+    function showAllFlowTraffic() {
+        trafficMode = 'allFlow';
+        hoverMode = 'all';
         wss.sendEvent('requestAllFlowTraffic');
         flash.flash('All Flow Traffic');
     }
 
-    // keystroke-A (see topo.js)
-    function showAllPortTrafficAction() {
-        hoverMode = hoverModeAll;
+    function showAllPortTraffic() {
+        trafficMode = 'allPort';
+        hoverMode = 'all';
         wss.sendEvent('requestAllPortTraffic');
         flash.flash('All Port Traffic');
     }
 
-    // === -----------------------------
-    // action buttons on detail panel
+    function showDeviceLinkFlows () {
+        trafficMode = hoverMode = 'flows';
+        requestDeviceLinkFlows();
+        flash.flash('Device Flows');
+    }
 
-    // also, keystroke-V (see topo.js)
-    function showRelatedIntentsAction () {
-        hoverMode = hoverModeIntents;
+    function showRelatedIntents () {
+        trafficMode = hoverMode = 'intents';
         requestRelatedIntents();
         flash.flash('Related Paths');
     }
 
-    function addHostIntentAction () {
+    function showPrevIntent() {
+        if (trafficMode === 'intents') {
+            hoverMode = null;
+            wss.sendEvent('requestPrevRelatedIntent');
+            flash.flash('Previous related intent');
+        }
+    }
+
+    function showNextIntent() {
+        if (trafficMode === 'intents') {
+            hoverMode = null;
+            wss.sendEvent('requestNextRelatedIntent');
+            flash.flash('Next related intent');
+        }
+    }
+
+    function showSelectedIntentTraffic() {
+        if (trafficMode === 'intents') {
+            hoverMode = null;
+            wss.sendEvent('requestSelectedIntentTraffic');
+            flash.flash('Traffic on Selected Path');
+        }
+    }
+
+
+    // === ------------------------------------------------------
+    // action buttons on detail panel (multiple selection)
+
+    function addHostIntent () {
         var so = api.selectOrder();
         wss.sendEvent('addHostIntent', {
             one: so[0],
@@ -200,7 +164,7 @@
         flash.flash('Host-to-Host flow added');
     }
 
-    function addMultiSourceIntentAction () {
+    function addMultiSourceIntent () {
         var so = api.selectOrder();
         wss.sendEvent('addMultiSourceIntent', {
             src: so.slice(0, so.length - 1),
@@ -210,12 +174,6 @@
         flash.flash('Multi-Source flow added');
     }
 
-    // also, keystroke-F (see topo.js)
-    function showDeviceLinkFlowsAction () {
-        hoverMode = hoverModeFlows;
-        requestDeviceLinkFlows();
-        flash.flash('Device Flows');
-    }
 
 
     // === -----------------------------------------------------
@@ -231,29 +189,26 @@
             flash = _flash_;
             wss = _wss_;
 
-            function initTraffic(_api_) {
-                api = _api_;
-            }
-
-            function destroyTraffic() { }
-
             return {
-                initTraffic: initTraffic,
-                destroyTraffic: destroyTraffic,
+                initTraffic: function (_api_) { api = _api_; },
+                destroyTraffic: function () { },
 
-                showTraffic: showTraffic,
-
+                // invoked from toolbar overlay buttons or keystrokes
                 cancelTraffic: cancelTraffic,
+                showAllFlowTraffic: showAllFlowTraffic,
+                showAllPortTraffic: showAllPortTraffic,
+                showDeviceLinkFlows: showDeviceLinkFlows,
+                showRelatedIntents: showRelatedIntents,
+                showPrevIntent: showPrevIntent,
+                showNextIntent: showNextIntent,
+                showSelectedIntentTraffic: showSelectedIntentTraffic,
+
+                // invoked from mouseover/mouseout and selection change
                 requestTrafficForMode: requestTrafficForMode,
-                showRelatedIntentsAction: showRelatedIntentsAction,
-                addHostIntentAction: addHostIntentAction,
-                addMultiSourceIntentAction: addMultiSourceIntentAction,
-                showDeviceLinkFlowsAction: showDeviceLinkFlowsAction,
-                showNextIntentAction: showNextIntentAction,
-                showPrevIntentAction: showPrevIntentAction,
-                showSelectedIntentTrafficAction: showSelectedIntentTrafficAction,
-                showAllFlowTrafficAction: showAllFlowTrafficAction,
-                showAllPortTrafficAction: showAllPortTrafficAction
+
+                // invoked from buttons on detail (multi-select) panel
+                addHostIntent: addHostIntent,
+                addMultiSourceIntent: addMultiSourceIntent
             };
         }]);
 }());
diff --git a/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js b/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js
index 71cb94c..be91f0c 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js
@@ -16,7 +16,7 @@
  */
 
 /*
- ONOS GUI -- Topology Traffic Module.
+ ONOS GUI -- Topology Traffic Overlay Module.
  Defines behavior for viewing different traffic modes.
  Installed as a Topology Overlay.
  */
@@ -24,7 +24,13 @@
     'use strict';
 
     // injected refs
-    var $log;
+    var $log, tov, tts;
+
+    // NOTE: no internal state here -- see TopoTrafficService for that
+
+    // NOTE: providing button disabling requires too big a refactoring of
+    //       the button factory etc. Will have to be done another time.
+
 
     // traffic overlay definition
     var overlay = {
@@ -32,26 +38,112 @@
         glyphId: 'allTraffic',
         tooltip: 'Traffic Overlay',
 
-        activate: activateTraffic,
-        deactivate: deactivateTraffic
+        // NOTE: Traffic glyphs already installed as part of the base ONOS set.
+
+        activate: function () {
+            $log.debug("Traffic overlay ACTIVATED");
+        },
+
+        deactivate: function () {
+            tts.cancelTraffic();
+            $log.debug("Traffic overlay DEACTIVATED");
+        },
+
+        // detail panel button definitions
+        // (keys match button identifiers, also defined in TrafficOverlay.java)
+        buttons: {
+            showDeviceFlows: {
+                gid: 'flows',
+                tt: 'Show Device Flows',
+                cb: function (data) { tts.showDeviceLinkFlows(); }
+            },
+
+            showRelatedTraffic: {
+                gid: 'relatedIntents',
+                tt: 'Show Related Traffic',
+                cb: function (data) { tts.showRelatedIntents(); }
+            }
+        },
+
+        // key bindings for traffic overlay toolbar buttons
+        // NOTE: fully qual. button ID is derived from overlay-id and key-name
+        keyBindings: {
+            0: {
+                cb: function () { tts.cancelTraffic(); },
+                tt: 'Cancel traffic monitoring',
+                gid: 'xMark'
+            },
+
+            A: {
+                cb: function () { tts.showAllFlowTraffic(); },
+                tt: 'Monitor all traffic using flow stats',
+                gid: 'allTraffic'
+            },
+            Q: {
+                cb: function () { tts.showAllPortTraffic(); },
+                tt: 'Monitor all traffic using port stats',
+                gid: 'allTraffic'
+            },
+            F: {
+                cb: function () { tts.showDeviceLinkFlows(); },
+                tt: 'Show device link flows',
+                gid: 'flows'
+            },
+            V: {
+                cb: function () { tts.showRelatedIntents(); },
+                tt: 'Show all related intents',
+                gid: 'relatedIntents'
+            },
+            leftArrow: {
+                cb: function () { tts.showPrevIntent(); },
+                tt: 'Show previous related intent',
+                gid: 'prevIntent'
+            },
+            rightArrow: {
+                cb: function () { tts.showNextIntent(); },
+                tt: 'Show next related intent',
+                gid: 'nextIntent'
+            },
+            W: {
+                cb: function () { tts.showSelectedIntentTraffic(); },
+                tt: 'Monitor traffic of selected intent',
+                gid: 'intentTraffic'
+            },
+
+            _keyOrder: [
+                '0', 'A', 'Q', 'F', 'V', 'leftArrow', 'rightArrow', 'W'
+            ]
+        },
+
+        hooks: {
+            // hook for handling escape key
+            escape: function () {
+                // Must return true to consume ESC, false otherwise.
+                return tts.cancelTraffic();
+            },
+
+            // hooks for when the selection changes...
+            empty: function () {
+                tts.cancelTraffic();
+            },
+            single: function (data) {
+                tts.requestTrafficForMode();
+            },
+            multi: function (selectOrder) {
+                tts.requestTrafficForMode();
+                tov.addDetailButton('showRelatedTraffic');
+            }
+        }
     };
 
-    // === implementation of overlay API (essentially callbacks)
-    function activateTraffic() {
-        $log.debug("Topology traffic overlay ACTIVATED");
-    }
-
-    function deactivateTraffic() {
-        $log.debug("Topology traffic overlay DEACTIVATED");
-    }
-
-
     // invoke code to register with the overlay service
     angular.module('ovTopo')
-        .run(['$log', 'TopoOverlayService',
+        .run(['$log', 'TopoOverlayService', 'TopoTrafficService',
 
-        function (_$log_, tov) {
+        function (_$log_, _tov_, _tts_) {
             $log = _$log_;
+            tov = _tov_;
+            tts = _tts_;
             tov.register(overlay);
         }]);