Enable link functionality in GUI2 Topology View

Change-Id: I1b88080ecdf8c9b6f8a60af4832a12441186d508
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html
index 1f1d757..af33384 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html
@@ -17,13 +17,14 @@
     <svg:g id="new-zoom-layer">
         <svg:g class="topo2-links">
             <!-- Template explanation: Creates an SVG Group and in
-                line 1) use the svg component onos-linkvisual, setting it's link
+                line 1) use the svg component onos-linksvg, setting it's link
                  Input parameter to the link item from the next line
                 line 2) Use the built in NgFor directive to iterate through the
                  set of links filtered by the filteredLinks() function.
             -->
-            <svg:g onos-linkvisual [link]="link"
-                   *ngFor="let link of filteredLinks()">
+            <svg:g onos-linksvg [link]="link"
+                   *ngFor="let link of filteredLinks()"
+                   (selectedEvent)="updateSelected($event)">
             </svg:g>
         </svg:g>
         <svg:g class="topo2-linkLabels" />
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.spec.ts
index ffbe667..6b1980a 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.spec.ts
@@ -19,7 +19,7 @@
 import {IconService, LogService} from 'gui2-fw-lib';
 import {
     DeviceNodeSvgComponent,
-    HostNodeSvgComponent, LinkVisualComponent,
+    HostNodeSvgComponent, LinkSvgComponent,
     SubRegionNodeSvgComponent
 } from './visuals';
 import {DraggableDirective} from './draggable/draggable.directive';
@@ -66,7 +66,7 @@
                 DeviceNodeSvgComponent,
                 HostNodeSvgComponent,
                 SubRegionNodeSvgComponent,
-                LinkVisualComponent,
+                LinkSvgComponent,
                 DraggableDirective
             ],
             providers: [
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
index 1d6ebf4..25e52ab 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
@@ -24,6 +24,7 @@
     OnInit,
     Output,
     QueryList,
+    SimpleChange,
     SimpleChanges,
     ViewChildren
 } from '@angular/core';
@@ -31,15 +32,24 @@
 import {
     Device,
     ForceDirectedGraph,
-    Host, HostLabelToggle,
+    Host,
+    HostLabelToggle,
     LabelToggle,
     LayerType,
-    Node,
+    Link,
+    LinkHighlight,
+    ModelEventMemo,
+    ModelEventType,
     Region,
     RegionLink,
-    SubRegion
+    SubRegion,
+    UiElement
 } from './models';
-import {DeviceNodeSvgComponent, HostNodeSvgComponent} from './visuals';
+import {
+    DeviceNodeSvgComponent,
+    HostNodeSvgComponent,
+    LinkSvgComponent
+} from './visuals';
 
 
 /**
@@ -58,7 +68,7 @@
     @Input() onosInstMastership: string = '';
     @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
     @Output() linkSelected = new EventEmitter<RegionLink>();
-    @Output() selectedNodeEvent = new EventEmitter<Node>();
+    @Output() selectedNodeEvent = new EventEmitter<UiElement>();
     @Input() selectedLink: RegionLink = null;
     @Input() showHosts: boolean = false;
     @Input() deviceLabelToggle: LabelToggle = LabelToggle.NONE;
@@ -71,6 +81,7 @@
     // template view with the *ngFor and we get them by a query here
     @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
     @ViewChildren(HostNodeSvgComponent) hosts: QueryList<HostNodeSvgComponent>;
+    @ViewChildren(LinkSvgComponent) links: QueryList<LinkSvgComponent>;
 
     constructor(
         protected log: LogService,
@@ -115,7 +126,7 @@
      */
     ngOnInit() {
         // Receiving an initialized simulated graph from our custom d3 service
-        this.graph = new ForceDirectedGraph(this.options);
+        this.graph = new ForceDirectedGraph(this.options, this.log);
 
         /** Binding change detection check on each tick
          * This along with an onPush change detection strategy should enforce
@@ -186,6 +197,7 @@
 
             this.graph.initSimulation(this.options);
             this.graph.initNodes();
+            this.graph.initLinks();
             this.log.debug('ForceSvgComponent input changed',
                 this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
         }
@@ -245,8 +257,8 @@
      * changed.
      * @param selectedNode the newly selected node
      */
-    updateSelected(selectedNode: Node): void {
-        this.log.debug('Device selected', selectedNode);
+    updateSelected(selectedNode: UiElement): void {
+        this.log.debug('Node or link selected', selectedNode);
         this.devices
             .filter((d) =>
                 selectedNode === undefined || d.device.id !== selectedNode.id)
@@ -256,19 +268,156 @@
                 selectedNode === undefined || h.host.id !== selectedNode.id)
             .forEach((h) => h.deselect());
 
+        this.links
+            .filter((l) =>
+                selectedNode === undefined || l.link.id !== selectedNode.id)
+            .forEach((l) => l.deselect());
+
         this.selectedNodeEvent.emit(selectedNode);
     }
 
     /**
      * We want to filter links to show only those not related to hosts if the
-     * 'showHosts' flag has been switched off. If 'shwoHosts' is true, then
+     * 'showHosts' flag has been switched off. If 'showHosts' is true, then
      * display all links.
      */
-    filteredLinks() {
+    filteredLinks(): Link[] {
         return this.regionData.links.filter((h) =>
             this.showHosts ||
             ((<Host>h.source).nodeType !== 'host' &&
             (<Host>h.target).nodeType !== 'host'));
     }
+
+    /**
+     * When changes happen in the model, then model events are sent up through the
+     * Web Socket
+     * @param type - the type of the change
+     * @param memo - a qualifier on the type
+     * @param subject - the item that the update is for
+     * @param data - the new definition of the item
+     */
+    handleModelEvent(type: ModelEventType, memo: ModelEventMemo, subject: string, data: UiElement): void {
+        switch (type) {
+            case ModelEventType.DEVICE_ADDED_OR_UPDATED:
+                if (memo === ModelEventMemo.ADDED) {
+                    this.regionData.devices[this.visibleLayerIdx()].push(<Device>data);
+                } else if (memo === ModelEventMemo.UPDATED) {
+                    const oldDevice: Device =
+                        this.regionData.devices[this.visibleLayerIdx()]
+                            .find((d) => d.id === subject);
+                    this.compareDevice(oldDevice, <Device>data);
+                } else {
+                    this.log.warn('Device ', memo, ' - not yet implemented', data);
+                }
+                this.log.warn('Device ', memo, ' - not yet implemented', data);
+                break;
+            case ModelEventType.HOST_ADDED_OR_UPDATED:
+                if (memo === ModelEventMemo.ADDED) {
+                    this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
+                    this.log.warn('Host added - not yet implemented', data);
+                } else if (memo === ModelEventMemo.UPDATED) {
+                    const oldHost: Host = this.regionData.hosts[this.visibleLayerIdx()]
+                        .find((h) => h.id === subject);
+                    this.compareHost(oldHost, <Host>data);
+                    this.log.warn('Host updated - not yet implemented', data);
+                } else {
+                    this.log.warn('Host change', memo, ' - unexpected');
+                }
+                break;
+            case ModelEventType.DEVICE_REMOVED:
+                if (memo === ModelEventMemo.REMOVED || memo === undefined) {
+                    const removeIdx: number =
+                        this.regionData.devices[this.visibleLayerIdx()]
+                            .findIndex((d) => d.id === subject);
+                    const removeCmpt: DeviceNodeSvgComponent =
+                        this.devices.find((dc) => dc.device.id === subject);
+                    this.log.warn('Device ', subject, 'removed - not yet implemented', removeIdx, removeCmpt.device.id);
+                } else {
+                    this.log.warn('Device removed - unexpected memo', memo);
+                }
+                break;
+            case ModelEventType.HOST_REMOVED:
+                if (memo === ModelEventMemo.REMOVED || memo === undefined) {
+                    const removeIdx: number =
+                        this.regionData.hosts[this.visibleLayerIdx()]
+                            .findIndex((h) => h.id === subject);
+                    const removeCmpt: HostNodeSvgComponent =
+                        this.hosts.find((hc) => hc.host.id === subject);
+                    this.log.warn('Host ', subject, 'removed - not yet implemented', removeIdx, removeCmpt.host.id);
+                } else {
+                    this.log.warn('Host removed - unexpected memo', memo);
+                }
+                break;
+            case ModelEventType.LINK_ADDED_OR_UPDATED:
+                this.log.warn('link added or updated - not yet implemented', subject);
+                break;
+            default:
+                this.log.error('Unexpected model event', type, 'for', subject);
+        }
+    }
+
+    private compareDevice(oldDevice: Device, updatedDevice: Device) {
+        if (oldDevice.master !== updatedDevice.master) {
+            this.log.debug('Mastership has changed for', updatedDevice.id, 'to', updatedDevice.master);
+        }
+        if (oldDevice.online !== updatedDevice.online) {
+            this.log.debug('Status has changed for', updatedDevice.id, 'to', updatedDevice.online);
+        }
+    }
+
+    private compareHost(oldHost: Host, updatedHost: Host) {
+        if (oldHost.configured !== updatedHost.configured) {
+            this.log.debug('Configured has changed for', updatedHost.id, 'to', updatedHost.configured);
+        }
+    }
+
+    /**
+     * When traffic monitoring is turned on (A key) highlights will be sent back
+     * from the WebSocket through the Traffic Service
+     * @param devices - an array of device highlights
+     * @param hosts - an array of host highlights
+     * @param links - an array of link highlights
+     */
+    handleHighlights(devices: Device[], hosts: Host[], links: LinkHighlight[]): void {
+
+        if (devices.length > 0) {
+            this.log.debug(devices.length, 'Devices highlighted');
+            devices.forEach((dh) => {
+                const deviceComponent: DeviceNodeSvgComponent = this.devices.find((d) => d.device.id === dh.id );
+                if (deviceComponent) {
+                    deviceComponent.ngOnChanges(
+                        {'deviceHighlight': new SimpleChange(<Device>{}, dh, true)}
+                    );
+                    this.log.debug('Highlighting device', deviceComponent.device.id);
+                } else {
+                    this.log.warn('Device component not found', dh.id);
+                }
+            });
+        }
+        if (hosts.length > 0) {
+            this.log.debug(hosts.length, 'Hosts highlighted');
+            hosts.forEach((hh) => {
+                const hostComponent: HostNodeSvgComponent = this.hosts.find((h) => h.host.id === hh.id );
+                if (hostComponent) {
+                    hostComponent.ngOnChanges(
+                        {'hostHighlight': new SimpleChange(<Host>{}, hh, true)}
+                    );
+                    this.log.debug('Highlighting host', hostComponent.host.id);
+                }
+            });
+        }
+        if (links.length > 0) {
+            this.log.debug(links.length, 'Links highlighted');
+            links.forEach((lh) => {
+                const linkComponent: LinkSvgComponent = this.links.find((l) => l.link.id === lh.id );
+                if (linkComponent) { // A link might not be present is hosts viewing is switched off
+                    linkComponent.ngOnChanges(
+                        {'linkHighlight': new SimpleChange(<LinkHighlight>{}, lh, true)}
+                    );
+                    // this.log.debug('Highlighting link', linkComponent.link.id, lh.css, lh.label);
+                }
+            });
+        }
+    }
 }
 
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/force-directed-graph.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/force-directed-graph.spec.ts
index 767e094..bdcd5c5 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/force-directed-graph.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/force-directed-graph.spec.ts
@@ -16,6 +16,8 @@
 import {ForceDirectedGraph, Options} from './force-directed-graph';
 import {Node} from './node';
 import {Link} from './link';
+import {LogService} from 'gui2-fw-lib';
+import {TestBed} from '@angular/core/testing';
 
 export class TestNode extends Node {
     constructor(id: string) {
@@ -33,13 +35,15 @@
  * ONOS GUI -- ForceDirectedGraph - Unit Tests
  */
 describe('ForceDirectedGraph', () => {
+    let logServiceSpy: jasmine.SpyObj<LogService>;
     let fdg: ForceDirectedGraph;
     const options: Options = {width: 1000, height: 1000};
 
     beforeEach(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
         const nodes: Node[] = [];
         const links: Link[] = [];
-        fdg = new ForceDirectedGraph(options);
+        fdg = new ForceDirectedGraph(options, logSpy);
 
         for (let i = 0; i < 10; i++) {
             const newNode: TestNode = new TestNode('id' + i);
@@ -53,7 +57,7 @@
         fdg.links = links;
         fdg.initSimulation(options);
         fdg.initNodes();
-
+        logServiceSpy = TestBed.get(LogService);
     });
 
     afterEach(() => {
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/force-directed-graph.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/force-directed-graph.ts
index 46d3ba7..b511886 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/force-directed-graph.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/force-directed-graph.ts
@@ -16,12 +16,35 @@
 import { EventEmitter } from '@angular/core';
 import { Link } from './link';
 import { Node } from './node';
-import * as d3 from 'd3';
+import * as d3 from 'd3-force';
+import {LogService} from 'gui2-fw-lib';
 
 const FORCES = {
     LINKS: 1 / 50,
     COLLISION: 1,
-    CHARGE: -10
+    GRAVITY: 0.4,
+    FRICTION: 0.7
+};
+
+const CHARGES = {
+    device: -80,
+    host: -200,
+    region: -80,
+    _def_: -120
+};
+
+const LINK_DISTANCE = {
+    // note: key is link.type
+    direct: 100,
+    optical: 120,
+    UiEdgeLink: 100,
+    _def_: 50,
+};
+
+const LINK_STRENGTH = {
+    // note: key is link.type
+    // range: {0.0 ... 1.0}
+    _def_: 0.1
 };
 
 export interface Options {
@@ -36,7 +59,7 @@
     public nodes: Node[] = [];
     public links: Link[] = [];
 
-    constructor(options: Options) {
+    constructor(options: Options, public log: LogService) {
         this.initSimulation(options);
     }
 
@@ -56,10 +79,26 @@
         // Initializing the links force simulation
         this.simulation.force('links',
             d3.forceLink(this.links)
-                .strength(FORCES.LINKS)
+                .strength(this.strength.bind(this))
+                .distance(this.distance.bind(this))
         );
     }
 
+    charges(node) {
+        const nodeType = node.nodeType;
+        return CHARGES[nodeType] || CHARGES._def_;
+    }
+
+    distance(node) {
+        const nodeType = node.nodeType;
+        return LINK_DISTANCE[nodeType] || LINK_DISTANCE._def_;
+    }
+
+    strength(node) {
+        const nodeType = node.nodeType;
+        return LINK_STRENGTH[nodeType] || LINK_STRENGTH._def_;
+    }
+
     initSimulation(options: Options) {
         if (!options || !options.width || !options.height) {
             throw new Error('missing options when initializing simulation');
@@ -72,9 +111,12 @@
             // Creating the force simulation and defining the charges
             this.simulation = d3.forceSimulation()
                 .force('charge',
-                    d3.forceManyBody()
-                        .strength(FORCES.CHARGE)
-                );
+                    d3.forceManyBody().strength(this.charges.bind(this)))
+                        // .distanceMin(100).distanceMax(500))
+                .force('gravity',
+                    d3.forceManyBody().strength(FORCES.GRAVITY))
+                .force('friction',
+                    d3.forceManyBody().strength(FORCES.FRICTION));
 
             // Connecting the d3 ticker to an angular event emitter
             this.simulation.on('tick', function () {
@@ -82,7 +124,7 @@
             });
 
             this.initNodes();
-            this.initLinks();
+            // this.initLinks();
         }
 
         /** Updating the central force of the simulation */
@@ -94,5 +136,11 @@
 
     stopSimulation() {
         this.simulation.stop();
+        this.log.debug('Simulation stopped');
+    }
+
+    restartSimulation() {
+        this.simulation.restart();
+        this.log.debug('Simulation restarted');
     }
 }
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/link.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/link.ts
index e4d7768..eb6d340 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/link.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/link.ts
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Node } from './node';
+import {Node, UiElement} from './node';
 import * as d3 from 'd3';
 
 export enum LinkType {
@@ -38,9 +38,15 @@
 /**
  * Implementing SimulationLinkDatum interface into our custom Link class
  */
-export class Link implements d3.SimulationLinkDatum<Node> {
+export class Link implements UiElement, d3.SimulationLinkDatum<Node> {
     // Optional - defining optional implementation properties - required for relevant typing assistance
     index?: number;
+    id: string; // The id of the link in the format epA/portA~epB/portB
+    epA: string; // The name of the device or host at one end
+    epB: string; // The name of the device or host at the other end
+    portA: string; // The number of the port at one end
+    portB: string; // The number of the port at the other end
+    type: LinkType;
 
     // Must - defining enforced implementation properties
     source: Node;
@@ -50,21 +56,29 @@
         this.source = source;
         this.target = target;
     }
+
+    linkTypeStr(): string {
+        return LinkType[this.type];
+    }
 }
 
 /**
- * model of the topo2CurrentRegion region link from Region below
+ * model of the topo2CurrentRegion region link from Region
  */
 export class RegionLink extends Link {
-    id: string; // The id of the link in the format epA/portA~epB/portB
-    epA: string; // The name of the device or host at one end
-    epB: string; // The name of the device or host at the other end
-    portA: string; // The number of the port at one end
-    portB: string; // The number of the port at the other end
     rollup: RegionRollup[]; // Links in sub regions represented by this one link
-    type: LinkType;
 
     constructor(type: LinkType, nodeA: Node, nodeB: Node) {
         super(nodeA, nodeB);
+        this.type = type;
     }
 }
+
+/**
+ * model of the highlights that are sent back from WebSocket when traffic is shown
+ */
+export interface LinkHighlight {
+    id: string;
+    css: string;
+    label: string;
+}
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts
index a7a7cb1..a5b4f3a 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts
@@ -22,6 +22,11 @@
     RegionProps
 } from './regions';
 
+export interface UiElement {
+    index?: number;
+    id: string;
+}
+
 /**
  * Toggle state for how device labels should be displayed
  */
@@ -112,7 +117,7 @@
 /**
  * Implementing SimulationNodeDatum interface into our custom Node class
  */
-export abstract class Node implements d3.SimulationNodeDatum {
+export abstract class Node implements UiElement, d3.SimulationNodeDatum {
     // Optional - defining optional implementation properties - required for relevant typing assistance
     index?: number;
     x: number;
@@ -121,7 +126,7 @@
     vy?: number;
     fx?: number | null;
     fy?: number | null;
-
+    nodeType: NodeType;
     id: string;
 
     protected constructor(id) {
@@ -140,7 +145,6 @@
     location: LocationType;
     metaUi: MetaUi;
     master: string;
-    nodeType: NodeType;
     online: boolean;
     props: DeviceProps;
     type: string;
@@ -150,12 +154,14 @@
     }
 }
 
+/**
+ * Model of the ONOS Host element in the topology
+ */
 export class Host extends Node {
     configured: boolean;
     id: string;
     ips: string[];
     layer: LayerType;
-    nodeType: NodeType;
     props: HostProps;
 
     constructor(id: string) {
@@ -173,10 +179,30 @@
     nDevs: number;
     nHosts: number;
     name: string;
-    nodeType: NodeType;
     props: RegionProps;
 
     constructor(id: string) {
         super(id);
     }
 }
+
+/**
+ * Enumerated values for topology update event types
+ */
+export enum ModelEventType {
+    HOST_ADDED_OR_UPDATED,
+    LINK_ADDED_OR_UPDATED,
+    DEVICE_ADDED_OR_UPDATED,
+    DEVICE_REMOVED,
+    HOST_REMOVED
+}
+
+/**
+ * Enumerated values for topology update event memo field
+ */
+export enum ModelEventMemo {
+    ADDED = 'added',
+    REMOVED = 'removed',
+    UPDATED = 'updated'
+}
+
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css
index cf965ac..57a2bd3 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css
@@ -19,7 +19,7 @@
  ONOS GUI -- Topology View (forces device visual) -- CSS file
  */
 g.node.device rect {
-    fill: #f0f0f0;
+    fill: #f0f0f070;
 }
 g.node.device text {
     fill: #bbb;
@@ -30,7 +30,7 @@
 
 
 g.node.device.online rect {
-    fill: #ffffff;
+    fill: #fafafad0;
 }
 g.node.device.online text {
     fill: #3c3a3a;
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
index e329874..70d4d5b 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
@@ -13,17 +13,75 @@
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
-<svg:g xmlns:svg="http://www.w3.org/2000/svg" #devSvg
+<svg:defs xmlns:svg="http://www.w3.org/2000/svg">
+    <!-- Template explanation: Define an SVG Filter that in
+        line 1) render the target object in to a bit map and apply a blur to it
+            based on its alpha channel
+        line 2) take that blurred layer and shift it down and to the right by 4
+        line 3) Merge this blurred and shifted layer and overlay it with the
+            original target object
+    -->
+    <svg:filter id="drop-shadow">
+        <svg:feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur" />
+        <svg:feOffset in="blur" dx="4" dy="4" result="offsetBlur"/>
+        <svg:feMerge >
+            <svg:feMergeNode in="offsetBlur" />
+            <svg:feMergeNode in="SourceGraphic" />
+        </svg:feMerge>
+    </svg:filter>
+    <svg:linearGradient id="diagonal_blue" x1="0%" y1="0%" x2="100%" y2="100%">
+        <svg:stop offset= "0%" style="stop-color: #5b99d2;" />
+        <svg:stop offset= "100%" style="stop-color: #3b79b2;" />
+    </svg:linearGradient>
+</svg:defs>
+<!-- Template explanation: Creates an SVG Group and in
+    line 1) transform it to the position calculated by the d3 force graph engine
+    line 2) Give it various CSS styles depending on attributes
+    line 3) When it is clicked, call the method that toggles the selection and
+        emits an event.
+    Other child objects have their own description
+-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
        [attr.transform]="'translate(' + device?.x + ',' + device?.y + '), scale(' + scale + ')'"
         [ngClass]="['node', 'device', device.online?'online':'', selected?'selected':'']"
         (click)="toggleSelected(device)">
-    <svg:rect class="node-container" x="-18" y="-18" width="36" height="36">
-              <!--[attr.width]="devText.getComputedTextLength()+36" -->
-              <!--[@deviceLabelToggle]="{ value: labelToggle, params: {txtWidth: devSvg.getBBox().width+'px' }}">-->
+    <svg:desc>Device {{device.id}}</svg:desc>
+    <!-- Template explanation: Creates an SVG Rectangle and in
+        line 1) set a css style and shift so that it's centred
+        line 2) set the initial width and height - width changes with label
+        line 3) link to the animation 'deviceLabelToggle', pass in to it a width
+            calculated from the width of the text, and additional padding at the end
+        line 4) Apply the filter defined above to this rectangle (even as its
+            width changes
+    -->
+    <svg:rect
+            class="node-container" x="-18" y="-18"
+            width="36" height="36"
+            [@deviceLabelToggle]="{ value: labelToggle, params: {txtWidth: (36 + labelTextLen() * 1.05)+'px' }}"
+            filter= "url(#drop-shadow)">
     </svg:rect>
-    <svg:rect x="-16" y="-16" width="32" height="32" [ngStyle]="{'fill': 'rgb(91, 153, 210)'}">
+    <!-- Template explanation: Creates an SVG Rectangle slightly smaller and
+        overlaid on the above. This is the blue box, and its width and height does
+        not change
+    -->
+    <svg:rect x="-16" y="-16" width="32" height="32" style="fill: url(#diagonal_blue)">
     </svg:rect>
-    <svg:text #devText text-anchor="start" y="0.3em" x="22" [ngStyle]="{'transform': 'scale(' + scale + ')'}">
+    <!-- Template explanation: Creates an SVG Text element and in
+        line 1) make it left aligned and slightly down and to the right of the last rect
+        line 2) set its text length to be the calculated value - see that function
+        line 3) because of kerning the actual text might be shorter or longer than
+            the pre-calculated value - if so change the spacing between the letters
+            (and not the letter width to compensate)
+        line 4) link to the animation deviceLabelToggleTxt, so that the text appears
+            in gently
+        line 5) The text will be one of 3 values - blank, the id or the name
+    -->
+    <svg:text
+            text-anchor="start" y="0.3em" x="22"
+            [attr.textLength]= "labelTextLen()"
+            lengthAdjust= "spacing"
+            [ngStyle]="{'transform': 'scale(' + scale + ')'}"
+            [@deviceLabelToggleTxt]="labelToggle">
         {{ labelToggle == 0 ? '': labelToggle == 1 ? device.id:device.props.name }}
     </svg:text>
     <!-- It's not possible to drive the following xref dynamically -->
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts
index 0f4feee..32ed2ef 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts
@@ -21,6 +21,7 @@
 import {of} from 'rxjs';
 import {ChangeDetectorRef} from '@angular/core';
 import {Device} from '../../models';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 
 class MockActivatedRoute extends ActivatedRoute {
     constructor(params: Params) {
@@ -44,6 +45,7 @@
         testDevice.online = true;
 
         TestBed.configureTestingModule({
+            imports: [ BrowserAnimationsModule ],
             declarations: [ DeviceNodeSvgComponent ],
             providers: [
                 { provide: LogService, useValue: logSpy },
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
index 036fc08..756769d 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
@@ -14,17 +14,18 @@
  * limitations under the License.
  */
 import {
+    ChangeDetectionStrategy,
     ChangeDetectorRef,
     Component,
-    ElementRef, EventEmitter,
+    EventEmitter,
     Input,
     OnChanges, Output,
     SimpleChanges,
-    ViewChild
 } from '@angular/core';
-import {Node, Device, LabelToggle} from '../../models';
+import {Device, LabelToggle, UiElement} from '../../models';
 import {LogService} from 'gui2-fw-lib';
 import {NodeVisual} from '../nodevisual';
+import {animate, state, style, transition, trigger} from '@angular/animations';
 
 /**
  * The Device node in the force graph
@@ -36,28 +37,38 @@
     selector: '[onos-devicenodesvg]',
     templateUrl: './devicenodesvg.component.html',
     styleUrls: ['./devicenodesvg.component.css'],
-    // changeDetection: ChangeDetectionStrategy.Default,
-    // animations: [
-    //     trigger('deviceLabelToggle', [
-    //         state('0', style({ // none
-    //             width: '36px',
-    //         })),
-    //         state('1, 2', // id
-    //             style({ width: '{{ txtWidth }}'}),
-    //             { params: {'txtWidth': '36px'}}
-    //         ), // default
-    //         transition('0 => *', animate('1000ms ease-in')),
-    //         transition('* => 0', animate('1000ms ease-out'))
-    //     ])
-    // ]
+    changeDetection: ChangeDetectionStrategy.Default,
+    animations: [
+        trigger('deviceLabelToggle', [
+            state('0', style({ // none
+                width: '36px',
+            })),
+            state('1, 2', // id
+                style({ width: '{{ txtWidth }}'}),
+                { params: {'txtWidth': '36px'}}
+            ), // default
+            transition('0 => 1', animate('250ms ease-in')),
+            transition('1 => 2', animate('250ms ease-in')),
+            transition('* => 0', animate('250ms ease-out'))
+        ]),
+        trigger('deviceLabelToggleTxt', [
+            state('0', style( {
+                opacity: 0,
+            })),
+            state( '1,2', style({
+                opacity: 1.0
+            })),
+            transition('0 => 1', animate('250ms ease-in')),
+            transition('* => 0', animate('250ms ease-out'))
+        ])
+    ]
 })
 export class DeviceNodeSvgComponent extends NodeVisual implements OnChanges {
     @Input() device: Device;
     @Input() scale: number = 1.0;
     @Input() labelToggle: LabelToggle = LabelToggle.NONE;
-    @Output() selectedEvent = new EventEmitter<Node>();
+    @Output() selectedEvent = new EventEmitter<UiElement>();
     textWidth: number = 36;
-    @ViewChild('idTxt') idTxt: ElementRef;
     constructor(
         protected log: LogService,
         private ref: ChangeDetectorRef
@@ -84,4 +95,31 @@
         }
         this.ref.markForCheck();
     }
+
+    /**
+     * Calculate the text length in advance as well as possible
+     *
+     * The length of SVG text cannot be exactly estimated, because depending on
+     * the letters kerning might mean that it is shorter or longer than expected
+     *
+     * This takes the approach of 8px width per letter of this size, that on average
+     * evens out over words. A word like 'ilj' will be much shorter than 'wm0'
+     * because of kerning
+     *
+     *
+     * In addition in the template, the <svg:text> properties
+     * textLength and lengthAdjust ensure that the text becomes long with extra
+     * wide spacing created as necessary.
+     *
+     * Other approaches like getBBox() of the text
+     */
+    labelTextLen() {
+        if (this.labelToggle === 1) {
+            return this.device.id.length * 8 * this.scale;
+        } else if (this.labelToggle === 2 && this.device && this.device.props.name && this.device.props.name.trim().length > 0) {
+            return this.device.props.name.length * 8 * this.scale;
+        } else {
+            return 0;
+        }
+    }
 }
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html
index 9cac8b0..728a8ce 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html
@@ -13,11 +13,57 @@
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
+<svg:defs xmlns:svg="http://www.w3.org/2000/svg">
+    <!-- Template explanation: Define an SVG Filter that in
+        line 1) render the target object in to a bit map and apply a blur to it
+            based on its alpha channel
+        line 2) take that blurred layer and shift it down and to the right by 4
+        line 3) Merge this blurred and shifted layer and overlay it with the
+            original target object
+    -->
+    <svg:filter id="drop-shadow-host">
+        <svg:feGaussianBlur in="SourceAlpha" stdDeviation="1" result="blur" />
+        <svg:feOffset in="blur" dx="2" dy="2" result="offsetBlur"/>
+        <svg:feMerge >
+            <svg:feMergeNode in="offsetBlur" />
+            <svg:feMergeNode in="SourceGraphic" />
+        </svg:feMerge>
+    </svg:filter>
+    <svg:radialGradient id="three_stops_radial">
+        <svg:stop offset= "0%" style="stop-color: #e3e5d6;" />
+        <svg:stop offset= "70%" style="stop-color: #c3c5b6;" />
+        <svg:stop offset="100%" style="stop-color: #a3a596;" />
+    </svg:radialGradient>
+</svg:defs>
+<!-- Template explanation: Creates an SVG Group and in
+    line 1) transform it to the position calculated by the d3 force graph engine
+    line 2) Give it various CSS styles depending on attributes
+    line 3) When it is clicked, call the method that toggles the selection and
+        emits an event.
+    Other child objects have their own description
+-->
 <svg:g  xmlns:svg="http://www.w3.org/2000/svg"
         [attr.transform]="'translate(' + host?.x + ',' + host?.y + '), scale(' + scale + ')'"
         [ngClass]="['node', 'host', 'endstation', 'fixed', selected?'selected':'', 'hovered']"
         (click)="toggleSelected(host)">
-    <svg:circle r="15"></svg:circle>
+    <svg:desc>Host {{host.id}}</svg:desc>
+    <!-- Template explanation: Creates an SVG Circle and in
+        line 1) Apply the drop shadow defined above to this circle
+        line 2) Apply the radial gradient defined above to the circle
+    -->
+    <svg:circle r="15"
+        filter="url(#drop-shadow-host)"
+        style="fill: url(#three_stops_radial)">
+    </svg:circle>
     <svg:use xlink:href="#m_endstation" width="22.5" height="22.5" x="-11.25" y="-11.25" style="transform: scale(1);"></svg:use>
-    <svg:text *ngIf="labelToggle != 0" dy="24" text-anchor="middle" style="transform: scale(1);">{{hostName()}}</svg:text>
+    <!-- Template explanation: Creates an SVG Text
+        line 1) if the labelToggle is not 0
+        line 2) shift it below the circle, and have it centred with the circle
+        line 3) apply a scale and call on the hostName(0 method to get the
+            displayed value
+    -->
+    <svg:text
+        *ngIf="labelToggle != 0"
+        dy="30" text-anchor="middle"
+        style="transform: scale(1);">{{hostName()}}</svg:text>
 </svg:g>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts
index bb791b9..e3efb3f 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts
@@ -1,25 +1,69 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { HostNodeSvgComponent } from './hostnodesvg.component';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {LogService} from 'gui2-fw-lib';
+import {Host} from '../../models';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {ChangeDetectorRef} from '@angular/core';
+
+class MockActivatedRoute extends ActivatedRoute {
+  constructor(params: Params) {
+    super();
+    this.queryParams = of(params);
+  }
+}
 
 describe('HostNodeSvgComponent', () => {
-  let component: HostNodeSvgComponent;
-  let fixture: ComponentFixture<HostNodeSvgComponent>;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: HostNodeSvgComponent;
+    let fixture: ComponentFixture<HostNodeSvgComponent>;
+    let ar: MockActivatedRoute;
+    let testHost: Host;
 
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ HostNodeSvgComponent ]
-    })
-    .compileComponents();
-  }));
+    beforeEach(async(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({ 'debug': 'txrx' });
+        testHost = new Host('host:1');
+        testHost.ips = ['10.205.86.123', '192.168.56.10'];
 
-  beforeEach(() => {
-    fixture = TestBed.createComponent(HostNodeSvgComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
+        TestBed.configureTestingModule({
+            imports: [ BrowserAnimationsModule ],
+            declarations: [ HostNodeSvgComponent ],
+            providers: [
+              { provide: LogService, useValue: logSpy },
+              { provide: ActivatedRoute, useValue: ar },
+              { provide: ChangeDetectorRef, useClass: ChangeDetectorRef }
+            ]
+        })
+        .compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
 
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
+    beforeEach(() => {
+        fixture = TestBed.createComponent(HostNodeSvgComponent);
+        component = fixture.componentInstance;
+        component.host = testHost;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
 });
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/index.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/index.ts
index 0723986..60ab90b 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/index.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/index.ts
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-export * from './linkvisual.component';
+export * from './linksvg/linksvg.component';
 export * from './devicenodesvg/devicenodesvg.component';
 export * from './hostnodesvg/hostnodesvg.component';
 export * from './subregionnodesvg/subregionnodesvg.component';
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.css b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.css
new file mode 100644
index 0000000..58548c4
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.css
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+
+/*
+ ONOS GUI -- Topology View (forces link svg) -- CSS file
+ */
+/* --- Topo Links --- */
+line {
+    stroke: #888888;
+    stroke-width: 2px;
+}
+
+.link {
+    opacity: .9;
+}
+
+.link.selected {
+    stroke: #009fdb;
+}
+.link.enhanced {
+    stroke: #009fdb;
+    stroke-width: 4px;
+    cursor: pointer;
+}
+
+.link.inactive {
+    opacity: .5;
+    stroke-dasharray: 4 2;
+}
+/* TODO: Review for not-permitted links */
+.link.not-permitted {
+    stroke: rgb(255,0,0);
+    stroke-dasharray: 8 4;
+}
+
+.link.secondary {
+    stroke: rgba(0,153,51,0.5);
+}
+
+.link.secondary.port-traffic-green {
+    stroke: rgb(0,153,51);
+}
+
+.link.secondary.port-traffic-yellow {
+    stroke: rgb(128,145,27);
+}
+
+.link.secondary.port-traffic-orange {
+    stroke: rgb(255, 137, 3);
+}
+
+.link.secondary.port-traffic-red {
+    stroke: rgb(183, 30, 21);
+}
+
+/* Port traffic color visualization for Kbps, Mbps, and Gbps */
+
+.link.secondary.port-traffic-Kbps {
+    stroke: rgb(0,153,51);
+}
+
+.link.secondary.port-traffic-Mbps {
+    stroke: rgb(128,145,27);
+}
+
+.link.secondary.port-traffic-Gbps {
+    stroke: rgb(255, 137, 3);
+}
+
+.link.secondary.port-traffic-Gbps-choked {
+    stroke: rgb(183, 30, 21);
+}
+
+.link.animated {
+    stroke-dasharray: 8 5;
+    animation: ants 5s infinite linear;
+    /* below line could be added via Javascript, based on path, if we cared
+     * enough about the direction of ant-flow
+     */
+    /*animation-direction: reverse;*/
+}
+@keyframes ants {
+    from {
+        stroke-dashoffset: 0;
+    }
+    to {
+        stroke-dashoffset: 400;
+    }
+}
+
+.link.primary {
+    stroke-width: 4px;
+    stroke: #ffA300;
+}
+
+.link.secondary.optical {
+    stroke-width: 4px;
+    stroke: rgba(128,64,255,0.5);
+}
+
+.link.primary.optical {
+    stroke-width: 6px;
+    stroke: #74f;
+}
+
+/* Link Labels */
+.linkLabel rect {
+    stroke: none;
+    fill: #ffffff;
+}
+
+.linkLabel text {
+    fill: #444;
+    text-anchor: middle;
+}
+
+
+/* Port Labels */
+.portLabel rect {
+    stroke: #a3a596;
+    fill: #ffffff;
+}
+
+.portLabel {
+    fill: #444;
+    alignment-baseline: middle;
+    dominant-baseline: middle;
+}
+
+/* Number of Links Labels */
+
+
+#ov-topo2 text.numLinkText {
+    fill: #444;
+}
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.html
new file mode 100644
index 0000000..b3a5557
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.html
@@ -0,0 +1,97 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ 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.
+-->
+<svg:defs xmlns:svg="http://www.w3.org/2000/svg">
+    <svg:filter id="glow">
+        <svg:feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0.9 0 0 0 0 0.9 0 0 0 0 1 0" />
+        <svg:feGaussianBlur stdDeviation="2.5" result="coloredBlur" />
+        <svg:feMerge>
+            <svg:feMergeNode in="coloredBlur" />
+            <svg:feMergeNode in="SourceGraphic"/>
+        </svg:feMerge>
+    </svg:filter>
+</svg:defs>
+<!-- Template explanation: Creates an SVG Line and in
+    line 1) transform end A to the position calculated by the d3 force graph engine
+    line 2) transform end B to the position calculated by the d3 force graph engine
+    line 3) Give it various CSS styles depending on attributes
+    line 4) When it is clicked, call the method that toggles the selection and
+        emits an event.
+    line 5) When the mouse is moved over call on enhance() function. This will
+        flash up the port labels, and display the link in blue for 1 second
+    Other child objects have their own description
+-->
+<svg:line xmlns:svg="http://www.w3.org/2000/svg"
+        [attr.x1]="link.source?.x" [attr.y1]="link.source?.y"
+        [attr.x2]="link.target?.x" [attr.y2]="link.target?.y"
+        [ngClass]="['link', selected?'selected':'', enhanced?'enhanced':'', highlighted]"
+        (click)="toggleSelected(link)"
+        (mouseover)="enhance()"
+        [attr.filter]="highlighted?'url(#glow)':'none'">
+</svg:line>
+<svg:g xmlns:svg="http://www.w3.org/2000/svg" [ngClass]="['linkLabel']">
+    <!-- Template explanation: Creates SVG Text and in
+        line 1) Performs the animation 'linkLabelVisible' whenever the isHighlighted
+            boolean value changes
+        line 2 & 3) Sets the text at half way between the 2 end points of the line
+        Note: we do not use an *ngIf to enable or disable this, because that would
+        cause the fade out of the text to not work
+    -->
+    <svg:text xmlns:svg="http://www.w3.org/2000/svg"
+              [@linkLabelVisible]="isHighlighted"
+              [attr.x]="link.source?.x + (link.target?.x - link.source?.x)/2"
+              [attr.y]="link.source?.y + (link.target?.y - link.source?.y)/2"
+    >{{ label }}</svg:text>
+</svg:g>
+<!-- Template explanation: Creates an SVG Group if
+    line 1) 'enhanced' is active and port text exists
+    line 2) assigns classes to it
+-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+       *ngIf="enhanced && link.portA"
+       class="portLabel">
+    <!-- Template explanation: Creates an SVG Rectangle and in
+        line 1) transform end A to the position calculated by the d3 force graph engine
+        line 2) assigns classes to it
+    -->
+    <svg:rect
+            [attr.x]="labelPosSrc.x - 2 - textLength(link.portA)/2" [attr.y]="labelPosSrc.y - 8"
+            [attr.width]="4 + textLength(link.portA)" height="16" >
+    </svg:rect>
+    <!-- Template explanation: Creates SVG Text and in
+        line 1) transform it to the position calculated by the method labelPosSrc()
+        line 2) centre aligns it
+        line 3) ensures that the text fills the rectangle by adjusting spacing
+    -->
+    <svg:text
+            [attr.x]="labelPosSrc.x" [attr.y]="labelPosSrc.y + 6"
+            text-anchor="middle"
+            [attr.textLength]= "textLength(link.portA)" lengthAdjust="spacing"
+    >{{ link.portA }}</svg:text>
+</svg:g>
+<!-- A repeat of the above, but for the other end of the line -->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+       *ngIf="enhanced && link.portB"
+       class="portLabel">
+    <svg:rect
+            [attr.x]="labelPosTgt.x - 2 - textLength(link.portB)/2" [attr.y]="labelPosTgt.y - 8"
+            [attr.width]="4 + textLength(link.portB)" height="16">
+    </svg:rect>
+    <svg:text
+            [attr.x]="labelPosTgt.x" [attr.y]="labelPosTgt.y + 6"
+            text-anchor="middle"
+            [attr.textLength]= "textLength(link.portB)" lengthAdjust="spacing"
+    >{{ link.portB }}</svg:text>
+</svg:g>
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.spec.ts
new file mode 100644
index 0000000..7626ff5
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.spec.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LinkSvgComponent } from './linksvg.component';
+import {LogService} from 'gui2-fw-lib';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {Device, Link, RegionLink, LinkType} from '../../models';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+describe('LinkVisualComponent', () => {
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: LinkSvgComponent;
+    let fixture: ComponentFixture<LinkSvgComponent>;
+    let ar: MockActivatedRoute;
+    let testLink: Link;
+    let testDeviceA: Device;
+    let testDeviceB: Device;
+
+    beforeEach(async(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+        testDeviceA = new Device('test:A');
+        testDeviceA.online = true;
+
+        testDeviceB = new Device('test:B');
+        testDeviceB.online = true;
+
+        testLink = new RegionLink(LinkType.UiDeviceLink, testDeviceA, testDeviceB);
+        testLink.id = 'test:A/1-test:B/1';
+
+        TestBed.configureTestingModule({
+            imports: [ BrowserAnimationsModule ],
+            declarations: [ LinkSvgComponent ],
+            providers: [
+                { provide: LogService, useValue: logSpy },
+                { provide: ActivatedRoute, useValue: ar },
+            ]
+        })
+        .compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(LinkSvgComponent);
+        component = fixture.componentInstance;
+        component.link = testLink;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.ts
new file mode 100644
index 0000000..8202c67
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.ts
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import {
+    ChangeDetectorRef,
+    Component, EventEmitter,
+    Input, OnChanges, Output, SimpleChanges,
+} from '@angular/core';
+import {Link, LinkHighlight, UiElement} from '../../models';
+import {LogService} from 'gui2-fw-lib';
+import {NodeVisual} from '../nodevisual';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+
+interface Point {
+    x: number;
+    y: number;
+}
+
+enum LinkEnd {
+    A,
+    B
+}
+
+@Component({
+    selector: '[onos-linksvg]',
+    templateUrl: './linksvg.component.html',
+    styleUrls: ['./linksvg.component.css'],
+    animations: [
+        trigger('linkLabelVisible', [
+            state('true', style( {
+                opacity: 1.0,
+            })),
+            state( 'false', style({
+                opacity: 0
+            })),
+            transition('false => true', animate('500ms ease-in')),
+            transition('true => false', animate('1000ms ease-out'))
+        ])
+    ]
+})
+export class LinkSvgComponent extends NodeVisual implements OnChanges {
+    @Input() link: Link;
+    @Input() highlighted: string = '';
+    @Input() label: string;
+    isHighlighted: boolean = false;
+    @Output() selectedEvent = new EventEmitter<UiElement>();
+    @Output() enhancedEvent = new EventEmitter<Link>();
+    enhanced: boolean = false;
+    labelPosSrc: Point = {x: 0, y: 0};
+    labelPosTgt: Point = {x: 0, y: 0};
+
+    constructor(
+        protected log: LogService,
+        private ref: ChangeDetectorRef
+    ) {
+        super();
+    }
+
+    ngOnChanges(changes: SimpleChanges) {
+        if (changes['linkHighlight']) {
+            const hl: LinkHighlight = changes['linkHighlight'].currentValue;
+            this.highlighted = hl.css;
+            this.label = hl.label;
+            this.isHighlighted = true;
+            setTimeout(() => {
+                this.isHighlighted = false;
+                this.highlighted = '';
+                this.ref.markForCheck();
+            }, 4990);
+
+        }
+
+        this.ref.markForCheck();
+    }
+
+    enhance() {
+        this.enhancedEvent.emit(this.link);
+        this.enhanced = true;
+        this.repositionLabels();
+        setTimeout(() => {
+            this.enhanced = false;
+            this.ref.markForCheck();
+        }, 1000);
+    }
+
+    /**
+     * We want to place the label for the port about 40 px from the node
+     * If the distance between the nodes is less than 100, then just place the
+     * label 1/3 of the way from the node
+     */
+    repositionLabels(): void {
+        const x1: number = this.link.source.x;
+        const y1: number = this.link.source.y;
+        const x2: number = this.link.target.x;
+        const y2: number = this.link.target.y;
+
+        const dist = Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2));
+        const offset = dist > 100 ? 40 : dist / 3;
+        this.labelPosSrc = <Point>{
+            x: x1 + (x2 - x1) * offset / dist,
+            y: y1 + (y2 - y1) * offset / dist
+        };
+
+        this.labelPosTgt = <Point>{
+            x: x2 - (x2 - x1) * offset / dist,
+            y: y2 - (y2 - y1) * offset / dist
+        };
+    }
+
+    /**
+     * For the 14pt font we are using, the average width seems to be about 8px
+     * @param text The string we want to calculate a width for
+     */
+    textLength(text: string) {
+        return text.length * 8;
+    }
+}
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linkvisual.component.css b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linkvisual.component.css
deleted file mode 100644
index 52e5df3..0000000
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linkvisual.component.css
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright 2018-present Open Networking Foundation
- *
- * 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.
- */
-
-
-/*
- ONOS GUI -- Topology View (forces link visual) -- CSS file
- */
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linkvisual.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linkvisual.component.html
deleted file mode 100644
index 6c4a6bd..0000000
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linkvisual.component.html
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-~ Copyright 2018-present Open Networking Foundation
-~
-~ 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.
--->
-<svg:line xmlns:svg="http://www.w3.org/2000/svg"
-        [attr.x1]="(link && link.source) ? link.source.x : 0"
-        [attr.y1]="(link && link.source) ? link.source.y : 0"
-        [attr.x2]="(link && link.target) ? link.target.x : 0"
-        [attr.y2]="(link && link.target) ? link.target.y : 0"
-          [ngStyle]="{'stroke':'black'}"
->
-    <!--<svg:line *ngFor="let link of regionData.links"-->
-    <!--x1="0" y1="0" x2="0" y2="0"-->
-    <!--stroke="#939598" stroke-width="1"-->
-    <!--[ngClass]="['link', 'direct']"-->
-    <!--[ngStyle]="{'stroke-width': 1+'px'}" [attr.click()]="selectLink(link)" />-->
-</svg:line>
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linkvisual.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linkvisual.component.spec.ts
deleted file mode 100644
index c060aab..0000000
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linkvisual.component.spec.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2018-present Open Networking Foundation
- *
- * 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.
- */
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { LinkVisualComponent } from './linkvisual.component';
-
-describe('LinkVisualComponent', () => {
-    let component: LinkVisualComponent;
-    let fixture: ComponentFixture<LinkVisualComponent>;
-
-    beforeEach(async(() => {
-        TestBed.configureTestingModule({
-          declarations: [ LinkVisualComponent ]
-        })
-        .compileComponents();
-    }));
-
-    beforeEach(() => {
-        fixture = TestBed.createComponent(LinkVisualComponent);
-        component = fixture.componentInstance;
-        fixture.detectChanges();
-    });
-
-    it('should create', () => {
-        expect(component).toBeTruthy();
-    });
-});
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linkvisual.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linkvisual.component.ts
deleted file mode 100644
index edde2aa..0000000
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linkvisual.component.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2018-present Open Networking Foundation
- *
- * 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.
- */
-import {
-    Component,
-    Input,
-} from '@angular/core';
-import { Link } from '../models';
-import {LogService} from 'gui2-fw-lib';
-
-@Component({
-    selector: '[onos-linkvisual]',
-    templateUrl: './linkvisual.component.html',
-    styleUrls: ['./linkvisual.component.css']
-})
-export class LinkVisualComponent {
-    @Input() link: Link;
-
-    constructor(
-        protected log: LogService
-    ) {
-    }
-
-}
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/nodevisual.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/nodevisual.ts
index 677745d..a41df62 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/nodevisual.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/nodevisual.ts
@@ -14,16 +14,19 @@
  * limitations under the License.
  */
 import {EventEmitter} from '@angular/core';
-import {Node} from '../models';
+import {UiElement} from '../models';
 
+/**
+ * A base class for the Host and Device components
+ */
 export abstract class NodeVisual {
     selected: boolean;
-    selectedEvent = new EventEmitter<Node>();
+    selectedEvent = new EventEmitter<UiElement>();
 
-    toggleSelected(node: Node) {
+    toggleSelected(uiElement: UiElement) {
         this.selected = !this.selected;
         if (this.selected) {
-            this.selectedEvent.emit(node);
+            this.selectedEvent.emit(uiElement);
         } else {
             this.selectedEvent.emit();
         }
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.html b/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.html
index a5e4b96..ae25507 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.html
@@ -13,7 +13,7 @@
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
-<div id="toolbar-topo2-toolbar" class="floatpanel toolbar"
+<div id="toolbar-topo2-toolbar" class="floatpanel toolbar" [@toolbarState]="on"
      style="opacity: 1; left: 0px; width: 261px; top: auto; bottom: 10px;">
     <div class="tbar-arrow">
         <svg class="embeddedIcon" width="10" height="10" viewBox="0 0 50 50">
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.spec.ts
index 730809c..90a629e 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.spec.ts
@@ -22,6 +22,7 @@
     FnService,
     LogService
 } from 'gui2-fw-lib';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 
 class MockActivatedRoute extends ActivatedRoute {
     constructor(params: Params) {
@@ -58,6 +59,7 @@
         };
         fs = new FnService(ar, logSpy, windowMock);
         TestBed.configureTestingModule({
+            imports: [ BrowserAnimationsModule ],
             declarations: [ ToolbarComponent ],
             providers: [
                 { provide: FnService, useValue: fs },
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.ts b/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.ts
index 208cafe..2addffe 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.ts
@@ -20,6 +20,7 @@
     FnService,
     PanelBaseImpl
 } from 'gui2-fw-lib';
+import {animate, state, style, transition, trigger} from '@angular/animations';
 
 /*
  ONOS GUI -- Topology Toolbar Module.
@@ -32,6 +33,20 @@
         './toolbar.component.css', './toolbar.theme.css',
         '../../topology.common.css',
         '../../../../fw/widget/panel.css', '../../../../fw/widget/panel-theme.css'
+    ],
+    animations: [
+        trigger('toolbarState', [
+            state('true', style({
+                transform: 'translateX(0%)',
+                opacity: '1.0'
+            })),
+            state('false', style({
+                transform: 'translateX(-100%)',
+                opacity: '0.0'
+            })),
+            transition('0 => 1', animate('100ms ease-in')),
+            transition('1 => 0', animate('100ms ease-out'))
+        ])
     ]
 })
 export class ToolbarComponent extends PanelBaseImpl implements OnInit {
@@ -42,6 +57,7 @@
         protected ls: LoadingService,
     ) {
         super(fs, ls, log);
+        this.on = false;
         this.log.debug('ToolbarComponent constructed');
     }
 
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology.module.ts b/web/gui2/src/main/webapp/app/view/topology/topology.module.ts
index d4d2bbf..3d9debc 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology.module.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology.module.ts
@@ -31,10 +31,9 @@
 import { DraggableDirective } from './layer/forcesvg/draggable/draggable.directive';
 import { ZoomableDirective } from './layer/zoomable.directive';
 import {
-    LinkVisualComponent,
     SubRegionNodeSvgComponent,
     DeviceNodeSvgComponent,
-    HostNodeSvgComponent,
+    HostNodeSvgComponent, LinkSvgComponent,
 } from './layer/forcesvg/visuals';
 
 /**
@@ -62,7 +61,7 @@
         MapSvgComponent,
         ZoomableDirective,
         DraggableDirective,
-        LinkVisualComponent,
+        LinkSvgComponent,
         DeviceNodeSvgComponent,
         HostNodeSvgComponent,
         SubRegionNodeSvgComponent
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology.service.ts b/web/gui2/src/main/webapp/app/view/topology/topology.service.ts
index 3572b7c..9422a3f 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology.service.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology.service.ts
@@ -13,14 +13,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {Injectable, SimpleChanges, SimpleChange} from '@angular/core';
+import {Injectable, SimpleChange} from '@angular/core';
 import {
     LogService, WebSocketService,
 } from 'gui2-fw-lib';
 import { InstanceComponent } from './panel/instance/instance.component';
 import { BackgroundSvgComponent } from './layer/backgroundsvg/backgroundsvg.component';
 import { ForceSvgComponent } from './layer/forcesvg/forcesvg.component';
-import {Region} from './layer/forcesvg/models';
+import {
+    ModelEventMemo,
+    ModelEventType,
+    Region
+} from './layer/forcesvg/models';
 
 /**
  * ONOS GUI -- Topology Service Module.
@@ -51,7 +55,9 @@
             ],
             ['topo2CurrentLayout', (data) => {
                     this.log.warn('Add fn for topo2CurrentLayout callback', data);
-                    background.layoutData = data;
+                    if (background) {
+                        background.layoutData = data;
+                    }
                 }
             ],
             ['topo2CurrentRegion', (data) => {
@@ -63,15 +69,22 @@
                 }
             ],
             ['topo2PeerRegions', (data) => { this.log.warn('Add fn for topo2PeerRegions callback', data); } ],
-            ['topo2UiModelEvent', (data) => { this.log.warn('Add fn for topo2UiModelEvent callback', data); } ],
-            ['topo2Highlights', (data) => { this.log.warn('Add fn for topo2Highlights callback', data); } ],
+            ['topo2UiModelEvent', (event) => {
+                    // this.log.debug('Handling', event);
+                    force.handleModelEvent(
+                        <ModelEventType><unknown>(ModelEventType[event.type]),
+                        <ModelEventMemo>(event.memo),
+                        event.subject, event.data);
+                }
+            ],
+            // ['topo2Highlights', (data) => { this.log.warn('Add fn for topo2Highlights callback', data); } ],
         ]));
         this.handlers.push('topo2AllInstances');
         this.handlers.push('topo2CurrentLayout');
         this.handlers.push('topo2CurrentRegion');
         this.handlers.push('topo2PeerRegions');
         this.handlers.push('topo2UiModelEvent');
-        this.handlers.push('topo2Highlights');
+        // this.handlers.push('topo2Highlights');
 
         // in case we fail over to a new server,
         // listen for wsock-open events
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
index d72a2e0..2a654b7 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
@@ -22,9 +22,9 @@
 
 <div id="ov-topo2">
     <svg:svg #svgZoom xmlns:svg="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" id="topo2">
-        <svg:g onos-nodeviceconnected />
+        <svg:g *ngIf="force.regionData?.devices.length === 0" onos-nodeviceconnected />
         <svg:g id="topo-zoomlayer" onosZoomableOf [zoomableOf]="svgZoom">
-            <svg:g #background onos-backgroundsvg/>
+            <svg:g *ngIf="showBackground" onos-backgroundsvg/>
             <svg:g #force onos-forcesvg (selectedNodeEvent)="nodeSelected($event)"/>
         </svg:g>
     </svg:svg>
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
index 433f749..7365310 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
@@ -28,8 +28,6 @@
     PrefsService,
     SvgUtilService,
     WebSocketService,
-    Zoomer,
-    ZoomOpts,
     ZoomService
 } from 'gui2-fw-lib';
 import {InstanceComponent} from '../panel/instance/instance.component';
@@ -39,6 +37,8 @@
 import {ForceSvgComponent} from '../layer/forcesvg/forcesvg.component';
 import {TopologyService} from '../topology.service';
 import {HostLabelToggle, LabelToggle, Node} from '../layer/forcesvg/models';
+import {ToolbarComponent} from '../panel/toolbar/toolbar.component';
+import {TrafficService} from '../traffic.service';
 
 /**
  * ONOS GUI Topology View
@@ -74,15 +74,14 @@
     @ViewChild(InstanceComponent) instance: InstanceComponent;
     @ViewChild(SummaryComponent) summary: SummaryComponent;
     @ViewChild(DetailsComponent) details: DetailsComponent;
+    @ViewChild(ToolbarComponent) toolbar: ToolbarComponent;
     @ViewChild(BackgroundSvgComponent) background: BackgroundSvgComponent;
     @ViewChild(ForceSvgComponent) force: ForceSvgComponent;
 
     flashMsg: string = '';
     prefsState = {};
     hostLabelIdx: number = 1;
-
-    zoomer: Zoomer;
-    zoomEventListeners: any[];
+    showBackground: boolean = false;
 
     constructor(
         protected log: LogService,
@@ -93,6 +92,7 @@
         protected wss: WebSocketService,
         protected zs: ZoomService,
         protected ts: TopologyService,
+        protected trs: TrafficService
     ) {
 
         this.log.debug('Topology component constructed');
@@ -117,15 +117,6 @@
 
     ngOnInit() {
         this.bindCommands();
-        this.zoomer = this.createZoomer(<ZoomOpts>{
-            svg: d3.select('svg#topo2'),
-            zoomLayer: d3.select('g#topo-zoomlayer'),
-            zoomEnabled: () => true,
-            zoomMin: 0.25,
-            zoomMax: 10.0,
-            zoomCallback: (() => { return; })
-        });
-        this.zoomEventListeners = [];
         // The components from the template are handed over to TopologyService here
         // so that WebSocket responses can be passed back in to them
         // The handling of the WebSocket call is delegated out to the Topology
@@ -141,14 +132,13 @@
 
     actionMap() {
         return {
+            A: [() => {this.monitorAllTraffic(); }, 'Monitor all traffic'],
             L: [() => {this.cycleDeviceLabels(); }, 'Cycle device labels'],
             B: [(token) => {this.toggleBackground(token); }, 'Toggle background'],
             D: [(token) => {this.toggleDetails(token); }, 'Toggle details panel'],
             I: [(token) => {this.toggleInstancePanel(token); }, 'Toggle ONOS Instance Panel'],
             O: [() => {this.toggleSummary(); }, 'Toggle the Summary Panel'],
             R: [() => {this.resetZoom(); }, 'Reset pan / zoom'],
-            'shift-Z': [() => {this.panAndZoom([0, 0], this.zoomer.scale() * 2); }, 'Zoom x2'],
-            'alt-Z': [() => {this.panAndZoom([0, 0], this.zoomer.scale() / 2); }, 'Zoom x0.5'],
             P: [(token) => {this.togglePorts(token); }, 'Toggle Port Highlighting'],
             E: [() => {this.equalizeMasters(); }, 'Equalize mastership roles'],
             X: [() => {this.resetNodeLocation(); }, 'Reset Node Location'],
@@ -156,6 +146,7 @@
             H: [() => {this.toggleHosts(); }, 'Toggle host visibility'],
             M: [() => {this.toggleOfflineDevices(); }, 'Toggle offline visibility'],
             dot: [() => {this.toggleToolbar(); }, 'Toggle Toolbar'],
+            0: [() => {this.cancelTraffic(); }, 'Cancel traffic monitoring'],
             'shift-L': [() => {this.cycleHostLabels(); }, 'Cycle host labels'],
 
             // -- instance color palette debug
@@ -189,21 +180,6 @@
         const am = this.actionMap();
         const add = this.fs.isO(additional);
 
-        // TODO: Reimplement when we have a use case
-        // if (add) {
-        //     _.each(add, function (value, key) {
-        //         // filter out meta properties (e.g. _keyOrder)
-        //         if (!(_.startsWith(key, '_'))) {
-        //             // don't allow re-definition of existing key bindings
-        //             if (am[key]) {
-        //                 this.log.warn('keybind: ' + key + ' already exists');
-        //             } else {
-        //                 am[key] = [value.cb, value.tt];
-        //             }
-        //         }
-        //     });
-        // }
-
         this.ks.keyBindings(am);
 
         this.ks.gestureNotes([
@@ -265,6 +241,7 @@
 
     protected toggleBackground(token: KeysToken) {
         this.flashMsg = 'Toggling background';
+        this.showBackground = !this.showBackground;
         this.log.debug('Toggling background', token);
         // TODO: Reinstate with components
         // t2bgs.toggle(x);
@@ -293,7 +270,7 @@
     }
 
     protected resetZoom() {
-        this.zoomer.reset();
+        // this.zoomer.reset();
         this.log.debug('resetting zoom');
         // TODO: Reinstate with components
         // t2bgs.resetZoom();
@@ -331,8 +308,8 @@
 
     protected toggleToolbar() {
         this.log.debug('toggling toolbar');
-        // TODO: Reinstate with components
-        // t2tbs.toggle();
+        this.flashMsg = ('Toggle toolbar');
+        this.toolbar.on = !this.toolbar.on;
     }
 
     protected actionedFlashed(action, message) {
@@ -377,70 +354,22 @@
         return this.fs.isA(entry) || [entry, ''];
     }
 
-
-
-    protected createZoomer(options: ZoomOpts) {
-        // need to wrap the original zoom callback to extend its behavior
-        const origCallback = this.fs.isF(options.zoomCallback) ? options.zoomCallback : () => {};
-
-        options.zoomCallback = () => {
-            origCallback([0, 0], 1);
-
-            this.zoomEventListeners.forEach((ev) => ev(this.zoomer));
-        };
-
-        return this.zs.createZoomer(options);
-    }
-
-    getZoomer() {
-        return this.zoomer;
-    }
-
-    findZoomEventListener(ev) {
-        for (let i = 0, len = this.zoomEventListeners.length; i < len; i++) {
-            if (this.zoomEventListeners[i] === ev) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
-    addZoomEventListener(callback) {
-        this.zoomEventListeners.push(callback);
-    }
-
-    removeZoomEventListener(callback) {
-        const evIndex = this.findZoomEventListener(callback);
-
-        if (evIndex !== -1) {
-            this.zoomEventListeners.splice(evIndex);
-        }
-    }
-
-    adjustmentScale(min: number, max: number): number {
-        let _scale = 1;
-        const size = (min + max) / 2;
-
-        if (size * this.scale() < max) {
-            _scale = min / (size * this.scale());
-        } else if (size * this.scale() > max) {
-            _scale = min / (size * this.scale());
-        }
-
-        return _scale;
-    }
-
-    scale(): number {
-        return this.zoomer.scale();
-    }
-
-    panAndZoom(translate: number[], scale: number, transition?: number) {
-        this.zoomer.panZoom(translate, scale, transition);
-    }
-
     nodeSelected(node: Node) {
         this.details.selectedNode = node;
         this.details.on = Boolean(node);
     }
 
+    /**
+     * Enable traffic monitoring
+     */
+    monitorAllTraffic() {
+        this.trs.init(this.force);
+    }
+
+    /**
+     * Cancel traffic monitoring
+     */
+    cancelTraffic() {
+        this.trs.destroy();
+    }
 }
diff --git a/web/gui2/src/main/webapp/app/view/topology/traffic.service.spec.ts b/web/gui2/src/main/webapp/app/view/topology/traffic.service.spec.ts
new file mode 100644
index 0000000..8b2a736
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/topology/traffic.service.spec.ts
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import { TestBed } from '@angular/core/testing';
+
+import { TrafficService } from './traffic.service';
+import {FnService, LogService} from 'gui2-fw-lib';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {TopologyService} from './topology.service';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+describe('TrafficService', () => {
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let ar: ActivatedRoute;
+    let fs: FnService;
+    let mockWindow: Window;
+
+    beforeEach(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn', 'info']);
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        mockWindow = <any>{
+            innerWidth: 400,
+            innerHeight: 200,
+            navigator: {
+                userAgent: 'defaultUA'
+            },
+            location: <any>{
+                hostname: 'foo',
+                host: 'foo',
+                port: '80',
+                protocol: 'http',
+                search: { debug: 'true' },
+                href: 'ws://foo:123/onos/ui/websock/path',
+                absUrl: 'ws://foo:123/onos/ui/websock/path'
+            }
+        };
+        fs = new FnService(ar, logSpy, mockWindow);
+
+        TestBed.configureTestingModule({
+            providers: [TopologyService,
+                { provide: FnService, useValue: fs},
+                { provide: LogService, useValue: logSpy },
+                { provide: ActivatedRoute, useValue: ar },
+                { provide: 'Window', useFactory: (() => mockWindow ) }
+            ]
+        });
+        logServiceSpy = TestBed.get(LogService);
+    });
+
+    it('should be created', () => {
+        const service: TrafficService = TestBed.get(TrafficService);
+        expect(service).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/topology/traffic.service.ts b/web/gui2/src/main/webapp/app/view/topology/traffic.service.ts
new file mode 100644
index 0000000..b6d2b85
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/topology/traffic.service.ts
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import { Injectable } from '@angular/core';
+import {LogService, WebSocketService} from 'gui2-fw-lib';
+import {ForceSvgComponent} from './layer/forcesvg/forcesvg.component';
+
+export enum TrafficType {
+    IDLE,
+    FLOWSTATSBYTES = 'flowStatsBytes',
+    PORTSTATSBITSEC = 'portStatsBitSec',
+    PORTSTATSPKTSEC = 'portStatsPktSec',
+}
+
+const ALL_TRAFFIC_TYPES = [
+    TrafficType.FLOWSTATSBYTES,
+    TrafficType.PORTSTATSBITSEC,
+    TrafficType.PORTSTATSPKTSEC
+];
+
+const ALL_TRAFFIC_MSGS = [
+    'Flow Stats (bytes)',
+    'Port Stats (bits / second)',
+    'Port Stats (packets / second)',
+];
+
+/**
+ * ONOS GUI -- Traffic Service Module.
+ */
+@Injectable({
+    providedIn: 'root'
+})
+export class TrafficService {
+    private handlers: string[] = [];
+    private openListener: any;
+
+    constructor(
+        protected log: LogService,
+        protected wss: WebSocketService
+    ) {
+        this.log.debug('TrafficService constructed');
+    }
+
+    init(force: ForceSvgComponent) {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            ['topo2Highlights', (data) => {
+                  force.handleHighlights(data.devices, data.hosts, data.links);
+                }
+            ]
+        ]));
+
+        this.handlers.push('topo2Highlights');
+
+        // in case we fail over to a new server,
+        // listen for wsock-open events
+        this.openListener = this.wss.addOpenListener(() => this.wsOpen);
+
+        // tell the server we are ready to receive topology events
+        this.wss.sendEvent('topo2RequestAllTraffic', {
+            trafficType: TrafficType.FLOWSTATSBYTES
+        });
+        this.log.debug('Topo2Traffic: Show All Traffic');
+    }
+
+    destroy() {
+        this.wss.sendEvent('topo2CancelTraffic', {});
+        this.wss.unbindHandlers(this.handlers);
+        this.log.debug('Traffic monitoring canceled');
+    }
+
+    wsOpen(host: string, url: string) {
+        this.log.debug('topo2RequestAllTraffic: WSopen - cluster node:', host, 'URL:', url);
+        // tell the server we are ready to receive topo events
+        this.wss.sendEvent('topo2RequestAllTraffic', {
+            trafficType: TrafficType.FLOWSTATSBYTES
+        });
+    }
+}