Added native Bazel build to GUI2. Reduced a lot of the unused Angular CLI structures

Reviewers should look at the changes in WORKSPACE, BUILD, BUILD.bazel, README.md files
This is only possible now as rules_nodejs went to 1.0.0 on December 20
gui2 has now been made the entry point (rather than gui2-fw-lib)
No tests or linting are functional yet for Typescript
Each NgModule now has its own BUILD.bazel file with ng_module
gui2-fw-lib is all one module and has been refactored to simplify the directory structure
gui2-topo-lib is also all one module - its directory structure has had 3 layers removed
The big bash script in web/gui2/BUILD has been removed - all is done through ng_module rules
in web/gui2/src/main/webapp/BUILD.bazel and web/gui2/src/main/webapp/app/BUILD.bazel

Change-Id: Ifcfcc23a87be39fe6d6c8324046cc8ebadb90551
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/draggable/draggable.directive.spec.ts b/web/gui2-topo-lib/lib/layer/forcesvg/draggable/draggable.directive.spec.ts
new file mode 100644
index 0000000..bb7dfbf
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/draggable/draggable.directive.spec.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { DraggableDirective } from './draggable.directive';
+import {inject, TestBed} from '@angular/core/testing';
+import {ElementRef} from '@angular/core';
+import {LogService} from '../../../../../gui2-fw-lib/public_api';
+
+export class MockElementRef extends ElementRef {
+    nativeElement = {};
+}
+
+describe('DraggableDirective', () => {
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let mockWindow: Window;
+
+    beforeEach(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        mockWindow = <any>{
+            navigator: {
+                userAgent: 'HeadlessChrome',
+                vendor: 'Google Inc.'
+            }
+        };
+
+        TestBed.configureTestingModule({
+            providers: [DraggableDirective,
+                { provide: LogService, useValue: logSpy },
+                { provide: 'Window', useFactory: (() => mockWindow ) },
+                { provide: ElementRef, useValue: mockWindow }
+            ]
+        });
+        logServiceSpy = TestBed.get(LogService);
+    });
+
+    it('should create an instance', inject([DraggableDirective], (directive: DraggableDirective) => {
+
+        expect(directive).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/draggable/draggable.directive.ts b/web/gui2-topo-lib/lib/layer/forcesvg/draggable/draggable.directive.ts
new file mode 100644
index 0000000..f476e46
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/draggable/draggable.directive.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 {
+    Directive,
+    ElementRef,
+    EventEmitter,
+    Input,
+    OnChanges, Output
+} from '@angular/core';
+import {ForceDirectedGraph, Node} from '../models';
+import * as d3 from 'd3';
+import {LogService, MetaUi, ZoomUtils} from '../../../../../gui2-fw-lib/public_api';
+import {BackgroundSvgComponent} from '../../backgroundsvg/backgroundsvg.component';
+
+@Directive({
+  selector: '[onosDraggableNode]'
+})
+export class DraggableDirective implements OnChanges {
+    @Input() draggableNode: Node;
+    @Input() draggableInGraph: ForceDirectedGraph;
+    @Output() newLocation = new EventEmitter<MetaUi>();
+
+    constructor(
+        private _element: ElementRef,
+        private log: LogService
+    ) {
+        this.log.debug('DraggableDirective constructed');
+    }
+
+    ngOnChanges() {
+        this.applyDraggableBehaviour(
+            this._element.nativeElement,
+            this.draggableNode,
+            this.draggableInGraph,
+            this.newLocation);
+    }
+
+    /**
+     * A method to bind a draggable behaviour to an svg element
+     */
+    applyDraggableBehaviour(element, node: Node, graph: ForceDirectedGraph, newLocation: EventEmitter<MetaUi>) {
+        const d3element = d3.select(element);
+
+        function started() {
+            /** Preventing propagation of dragstart to parent elements */
+            d3.event.sourceEvent.stopPropagation();
+
+            if (!d3.event.active) {
+                graph.simulation.alphaTarget(0.3).restart();
+            }
+
+            d3.event.on('drag', () => dragged()).on('end', () => ended());
+
+            function dragged() {
+                node.fx = d3.event.x;
+                node.fy = d3.event.y;
+            }
+
+            function ended() {
+                if (!d3.event.active) {
+                    graph.simulation.alphaTarget(0);
+                }
+                newLocation.emit(ZoomUtils.convertXYtoGeo(node.fx, node.fy));
+
+                // node.fx = null;
+                // node.fy = null;
+            }
+        }
+
+        d3element.call(d3.drag()
+            .on('start', started));
+    }
+}
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/forcesvg.component.css b/web/gui2-topo-lib/lib/layer/forcesvg/forcesvg.component.css
new file mode 100644
index 0000000..addd41c
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/forcesvg.component.css
@@ -0,0 +1,20 @@
+/*
+ * 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) -- CSS file
+ */
\ No newline at end of file
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/forcesvg.component.html b/web/gui2-topo-lib/lib/layer/forcesvg/forcesvg.component.html
new file mode 100644
index 0000000..026ef87
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/forcesvg.component.html
@@ -0,0 +1,104 @@
+<!--
+~ Copyright 2019-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:desc xmlns:svg="http://www.w3.org/2000/svg">The force layout layer. This is
+    an SVG component that displays Nodes (Devices, Hosts and SubRegions) and
+    Links. Positions of each are driven by a forces computation engine</svg:desc>
+<svg:g xmlns:svg="http://www.w3.org/2000/svg" class="topo2-links">
+    <svg:desc>Topology links</svg:desc>
+    <!-- Template explanation: Creates an SVG Group and in
+        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.
+        line 3) feed the highlightPorts of this (forcesvg) component in to
+         the highlightsEnabled of the link component
+        line 5) when the onos-linksvg component emits the selectedEvent,
+         call the updateSelected() method of this (forcesvg) component
+        line 6) feed the scale of this (forcesvg) component in to the scale
+         of the link
+    -->
+    <svg:g onos-linksvg [link]="link"
+           *ngFor="let link of filteredLinks()"
+           [highlightsEnabled]="highlightPorts"
+           (selectedEvent)="updateSelected($event)"
+           [scale]="scale">
+    </svg:g>
+</svg:g>
+<svg:g xmlns:svg="http://www.w3.org/2000/svg" class="topo2-nodes">
+    <svg:desc>Topology nodes</svg:desc>
+    <!-- Template explanation - create an SVG Group and
+        line 1) use the svg component onos-devicenodesvg, setting it's device
+         Input parameter to the device item from the next line
+        line 2) Use the built in NgFor directive to iterate through all
+         of the devices in the chosen layer index. The current iteration
+         is in the device variable
+        line 3) Use the onosDraggable directive and pass this device in to
+         its draggableNode Input parameter and setting the draggableInGraph
+         Input parameter to 'graph'
+        line 4) event handler of the draggable directive - causes the new location
+         to be written back to the server
+        line 5) when the onos-devicenodesvg component emits the selectedEvent,
+         call the updateSelected() method of this (forcesvg) component
+        line 6) feed the devicelabeltoggle of this (forcesvg) component in to
+         the labelToggle of the device
+        line 7) feed the scale of this (forcesvg) component in to the scale
+         of the device
+    -->
+    <svg:g onos-devicenodesvg [device]="device"
+           *ngFor="let device of regionData.devices[visibleLayerIdx()]"
+           onosDraggableNode [draggableNode]="device" [draggableInGraph]="graph"
+               (newLocation)="nodeMoved('device', device.id, $event)"
+           (selectedEvent)="updateSelected($event)"
+            [labelToggle]="deviceLabelToggle"
+            [scale]="scale">
+        <svg:desc>Device nodes</svg:desc>
+    </svg:g>
+    <!-- Template explanation - only display the hosts if 'showHosts' is set true -->
+    <svg:g *ngIf="showHosts">
+        <!-- Template explanation - create an SVG Group and
+            line 1) use the svg component onos-hostnodesvg, setting it's host
+             Input parameter to the host item from the next line
+            line 2) Use the built in NgFor directive to iterate through all
+             of the hosts in the chosen layer index. The current iteration
+             is in the 'host' variable
+            line 3) Use the onosDraggable directive and pass this host in to
+             its draggableNode Input parameter and setting the draggableInGraph
+             Input parameter to 'graph'
+            line 4) event handler of the draggable directive - causes the new location
+             to be written back to the server
+            line 5) when the onos-hostnodesvg component emits the selectedEvent
+             call the updateSelected() method of this (forcesvg) component
+            line 6) feed the hostLabelToggle of this (forcesvg) component in to
+             the labelToggle of the host
+            line 7) feed the scale of this (forcesvg) component in to the scale
+             of the host
+        -->
+        <svg:g onos-hostnodesvg [host]="host"
+               *ngFor="let host of regionData.hosts[visibleLayerIdx()]"
+               onosDraggableNode [draggableNode]="host" [draggableInGraph]="graph"
+                   (newLocation)="nodeMoved('host', host.id, $event)"
+               (selectedEvent)="updateSelected($event)"
+               [labelToggle]="hostLabelToggle"
+               [scale]="scale">
+            <svg:desc>Host nodes</svg:desc>
+        </svg:g>
+    </svg:g>
+    <svg:g onos-subregionnodesvg [subRegion]="subRegion"
+           *ngFor="let subRegion of regionData.subregions"
+           onosDraggableNode [draggableNode]="subRegion" [draggableInGraph]="graph">
+        <svg:desc>Subregion nodes</svg:desc>
+    </svg:g>
+</svg:g>
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/forcesvg.component.spec.ts b/web/gui2-topo-lib/lib/layer/forcesvg/forcesvg.component.spec.ts
new file mode 100644
index 0000000..6ee354a
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/forcesvg.component.spec.ts
@@ -0,0 +1,298 @@
+/*
+ * 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 {ForceSvgComponent} from './forcesvg.component';
+import {
+    FnService, IconService,
+    LionService,
+    LogService, SvgUtilService,
+    UrlFnService,
+    WebSocketService
+} from '../../../gui2-fw-lib/public_api';
+import {DraggableDirective} from './draggable/draggable.directive';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {DeviceNodeSvgComponent} from './visuals/devicenodesvg/devicenodesvg.component';
+import {SubRegionNodeSvgComponent} from './visuals/subregionnodesvg/subregionnodesvg.component';
+import {HostNodeSvgComponent} from './visuals/hostnodesvg/hostnodesvg.component';
+import {LinkSvgComponent} from './visuals/linksvg/linksvg.component';
+import {Device, Host, Link, LinkType, Region} from './models';
+import {ChangeDetectorRef, SimpleChange} from '@angular/core';
+import {TopologyService} from '../../topology.service';
+import {BadgeSvgComponent} from './visuals/badgesvg/badgesvg.component';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockIconService {
+    loadIconDef() { }
+}
+
+class MockSvgUtilService {
+
+    cat7() {
+        const tcid = 'd3utilTestCard';
+
+        function getColor(id, muted, theme) {
+            // NOTE: since we are lazily assigning domain ids, we need to
+            //       get the color from all 4 scales, to keep the domains
+            //       in sync.
+            const ln = '#5b99d2';
+            const lm = '#9ebedf';
+            const dn = '#5b99d2';
+            const dm = '#9ebedf';
+            if (theme === 'dark') {
+                return muted ? dm : dn;
+            } else {
+                return muted ? lm : ln;
+            }
+        }
+
+        return {
+            // testCard: testCard,
+            getColor: getColor,
+        };
+    }
+}
+
+class MockUrlFnService { }
+
+class MockWebSocketService {
+    createWebSocket() { }
+    isConnected() { return false; }
+    unbindHandlers() { }
+    bindHandlers() { }
+}
+
+class MockTopologyService {
+    public instancesIndex: Map<string, number>;
+    constructor() {
+        this.instancesIndex = new Map();
+    }
+}
+
+describe('ForceSvgComponent', () => {
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: ForceSvgComponent;
+    let fixture: ComponentFixture<ForceSvgComponent>;
+    const openflowSampleData = require('./tests/test-module-topo2CurrentRegion.json');
+    const openflowRegionData: Region = <Region><unknown>(openflowSampleData.payload);
+
+    const odtnSampleData = require('./tests/test-OdtnConfig-topo2CurrentRegion.json');
+    const odtnRegionData: Region = <Region><unknown>(odtnSampleData.payload);
+
+    const emptyRegion: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
+
+    beforeEach(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+        windowMock = <any>{
+            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'
+            }
+        };
+
+        const bundleObj = {
+            'core.view.Topo': {
+                test: 'test1'
+            }
+        };
+        const mockLion = (key) => {
+            return bundleObj[key] || '%' + key + '%';
+        };
+
+        fs = new FnService(ar, logSpy, windowMock);
+
+        TestBed.configureTestingModule({
+            declarations: [
+                ForceSvgComponent,
+                DeviceNodeSvgComponent,
+                HostNodeSvgComponent,
+                SubRegionNodeSvgComponent,
+                LinkSvgComponent,
+                DraggableDirective,
+                BadgeSvgComponent
+            ],
+            providers: [
+                { provide: LogService, useValue: logSpy },
+                { provide: ActivatedRoute, useValue: ar },
+                { provide: FnService, useValue: fs },
+                { provide: ChangeDetectorRef, useClass: ChangeDetectorRef },
+                { provide: UrlFnService, useClass: MockUrlFnService },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+                { provide: LionService, useFactory: (() => {
+                        return {
+                            bundle: ((bundleId) => mockLion),
+                            ubercache: new Array(),
+                            loadCbs: new Map<string, () => void>([])
+                        };
+                    })
+                },
+                { provide: IconService, useClass: MockIconService },
+                { provide: SvgUtilService, useClass: MockSvgUtilService },
+                { provide: TopologyService, useClass: MockTopologyService },
+                { provide: 'Window', useValue: windowMock },
+            ]
+        })
+        .compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+
+        fixture = TestBed.createComponent(ForceSvgComponent);
+        component = fixture.debugElement.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('load sample files', () => {
+        expect(openflowSampleData).toBeTruthy();
+        expect(openflowSampleData.payload).toBeTruthy();
+        expect(openflowSampleData.payload.id).toBe('(root)');
+
+        expect(odtnSampleData).toBeTruthy();
+        expect(odtnSampleData.payload).toBeTruthy();
+        expect(odtnSampleData.payload.id).toBe('(root)');
+    });
+
+    it('should read sample data payload as Region', () => {
+        expect(openflowRegionData).toBeTruthy();
+        // console.log(regionData);
+        expect(openflowRegionData.id).toBe('(root)');
+        expect(openflowRegionData.devices).toBeTruthy();
+        expect(openflowRegionData.devices.length).toBe(3);
+        expect(openflowRegionData.devices[2].length).toBe(10);
+        expect(openflowRegionData.hosts.length).toBe(3);
+        expect(openflowRegionData.hosts[2].length).toBe(20);
+        expect(openflowRegionData.links.length).toBe(44);
+    });
+
+    it('should read device246 correctly', () => {
+        const device246: Device = openflowRegionData.devices[2][0];
+        expect(device246.id).toBe('of:0000000000000246');
+        expect(device246.nodeType).toBe('device');
+        expect(device246.type).toBe('switch');
+        expect(device246.online).toBe(true);
+        expect(device246.master).toBe('10.192.19.68');
+        expect(device246.layer).toBe('def');
+
+        expect(device246.props.managementAddress).toBe('10.192.19.69');
+        expect(device246.props.protocol).toBe('OF_13');
+        expect(device246.props.driver).toBe('ofdpa-ovs');
+        expect(device246.props.latitude).toBe('40.15');
+        expect(device246.props.name).toBe('s246');
+        expect(device246.props.locType).toBe('geo');
+        expect(device246.props.channelId).toBe('10.192.19.69:59980');
+        expect(device246.props.longitude).toBe('-121.679');
+
+        expect(device246.location.locType).toBe('geo');
+        expect(device246.location.latOrY).toBe(40.15);
+        expect(device246.location.longOrX).toBe(-121.679);
+    });
+
+    it('should read host 3 correctly', () => {
+        const host3: Host = openflowRegionData.hosts[2][0];
+        expect(host3.id).toBe('00:88:00:00:00:03/110');
+        expect(host3.nodeType).toBe('host');
+        expect(host3.layer).toBe('def');
+        expect(host3.configured).toBe(false);
+        expect(host3.ips.length).toBe(3);
+        expect(host3.ips[0]).toBe('fe80::288:ff:fe00:3');
+        expect(host3.ips[1]).toBe('2000::102');
+        expect(host3.ips[2]).toBe('10.0.1.2');
+    });
+
+    it('should read link 3-205 correctly', () => {
+        const link3_205: Link = openflowRegionData.links[0];
+        expect(link3_205.id).toBe('00:AA:00:00:00:03/None~of:0000000000000205/6');
+        expect(link3_205.epA).toBe('00:AA:00:00:00:03/None');
+        expect(link3_205.epB).toBe('of:0000000000000205');
+        expect(String(LinkType[link3_205.type])).toBe('2');
+        expect(link3_205.portA).toBe(undefined);
+        expect(link3_205.portB).toBe('6');
+
+        expect(link3_205.rollup).toBeTruthy();
+        expect(link3_205.rollup.length).toBe(1);
+        expect(link3_205.rollup[0].id).toBe('00:AA:00:00:00:03/None~of:0000000000000205/6');
+        expect(link3_205.rollup[0].epA).toBe('00:AA:00:00:00:03/None');
+        expect(link3_205.rollup[0].epB).toBe('of:0000000000000205');
+        expect(String(LinkType[link3_205.rollup[0].type])).toBe('2');
+        expect(link3_205.rollup[0].portA).toBe(undefined);
+        expect(link3_205.rollup[0].portB).toBe('6');
+
+    });
+
+    it('should handle regionData change - empty Region', () => {
+        component.ngOnChanges(
+            {'regionData' : new SimpleChange(<Region>{}, emptyRegion, true)});
+
+        expect(component.graph.nodes.length).toBe(0);
+    });
+
+    it('should know how to format names', () => {
+        expect(ForceSvgComponent.extractNodeName('00:AA:00:00:00:03/None', undefined))
+            .toEqual('00:AA:00:00:00:03/None');
+
+        expect(ForceSvgComponent.extractNodeName('00:AA:00:00:00:03/161', '161'))
+            .toEqual('00:AA:00:00:00:03');
+
+        // Like epB of first example in sampleData file - endPtStr contains port number
+        expect(ForceSvgComponent.extractNodeName('of:0000000000000206/6', '6'))
+            .toEqual('of:0000000000000206');
+
+        // Like epB of second example in sampleData file - endPtStr does not contain port number
+        expect(ForceSvgComponent.extractNodeName('of:0000000000000206', '6'))
+            .toEqual('of:0000000000000206');
+    });
+
+    it('should handle openflow regionData change - sample Region', () => {
+        component.regionData = openflowRegionData;
+        component.ngOnChanges(
+            {'regionData' : new SimpleChange(<Region>{}, openflowRegionData, true)});
+
+        expect(component.graph.nodes.length).toBe(30);
+
+        expect(component.graph.links.length).toBe(44);
+
+    });
+
+    it('should handle odtn regionData change - sample odtn Region', () => {
+        component.regionData = odtnRegionData;
+        component.ngOnChanges(
+            {'regionData' : new SimpleChange(<Region>{}, odtnRegionData, true)});
+
+        expect(component.graph.nodes.length).toBe(2);
+
+        expect(component.graph.links.length).toBe(6);
+
+    });
+});
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/forcesvg.component.ts b/web/gui2-topo-lib/lib/layer/forcesvg/forcesvg.component.ts
new file mode 100644
index 0000000..ec3e57d
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/forcesvg.component.ts
@@ -0,0 +1,715 @@
+/*
+ * Copyright 2019-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 {
+    ChangeDetectionStrategy,
+    ChangeDetectorRef,
+    Component,
+    EventEmitter,
+    HostListener,
+    Input,
+    OnChanges, OnDestroy,
+    OnInit,
+    Output,
+    QueryList,
+    SimpleChange,
+    SimpleChanges,
+    ViewChildren
+} from '@angular/core';
+import {LocMeta, LogService, MetaUi, WebSocketService, ZoomUtils} from '../../../../gui2-fw-lib/public_api';
+import {
+    Badge,
+    Device, DeviceHighlight,
+    DeviceProps,
+    ForceDirectedGraph,
+    Host, HostHighlight,
+    HostLabelToggle,
+    LabelToggle,
+    LayerType,
+    Link,
+    LinkHighlight,
+    Location,
+    ModelEventMemo,
+    ModelEventType,
+    Node,
+    Options,
+    Region,
+    RegionLink,
+    SubRegion,
+    UiElement
+} from './models';
+import {LocationType} from '../backgroundsvg/backgroundsvg.component';
+import {DeviceNodeSvgComponent} from './visuals/devicenodesvg/devicenodesvg.component';
+import {HostNodeSvgComponent} from './visuals/hostnodesvg/hostnodesvg.component';
+import {LinkSvgComponent} from './visuals/linksvg/linksvg.component';
+import {SelectedEvent} from './visuals/nodevisual';
+
+interface UpdateMeta {
+    id: string;
+    class: string;
+    memento: MetaUi;
+}
+
+const SVGCANVAS = <Options>{
+    width: 1000,
+    height: 1000
+};
+
+interface ChangeSummary {
+    numChanges: number;
+    locationChanged: boolean;
+}
+
+/**
+ * ONOS GUI -- Topology Forces Graph Layer View.
+ *
+ * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
+ * This drives the whole Force graph
+ */
+@Component({
+    selector: '[onos-forcesvg]',
+    templateUrl: './forcesvg.component.html',
+    styleUrls: ['./forcesvg.component.css'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ForceSvgComponent implements OnInit, OnDestroy, OnChanges {
+    @Input() deviceLabelToggle: LabelToggle.Enum = LabelToggle.Enum.NONE;
+    @Input() hostLabelToggle: HostLabelToggle.Enum = HostLabelToggle.Enum.NONE;
+    @Input() showHosts: boolean = false;
+    @Input() showAlarms: boolean = false;
+    @Input() highlightPorts: boolean = true;
+    @Input() onosInstMastership: string = '';
+    @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
+    @Input() selectedLink: RegionLink = null;
+    @Input() scale: number = 1;
+    @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
+    @Output() linkSelected = new EventEmitter<RegionLink>();
+    @Output() selectedNodeEvent = new EventEmitter<UiElement[]>();
+    public graph: ForceDirectedGraph;
+    private selectedNodes: UiElement[] = [];
+    viewInitialized: boolean = false;
+
+    // References to the children of this component - these are created in the
+    // 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,
+        private ref: ChangeDetectorRef,
+        protected wss: WebSocketService
+    ) {
+        this.selectedLink = null;
+        this.log.debug('ForceSvgComponent constructed');
+    }
+
+    /**
+     * Utility for extracting a node name from an endpoint string
+     * In some cases - have to remove the port number from the end of a device
+     * name
+     * @param endPtStr The end point name
+     */
+    static extractNodeName(endPtStr: string, portStr: string): string {
+        if (portStr === undefined || endPtStr === undefined) {
+            return endPtStr;
+        } else if (endPtStr.includes('/')) {
+            return endPtStr.substr(0, endPtStr.length - portStr.length - 1);
+        }
+        return endPtStr;
+    }
+
+    /**
+     * Recursive method to compare 2 objects attribute by attribute and update
+     * the first where a change is detected
+     * @param existingNode 1st object
+     * @param updatedNode 2nd object
+     */
+    private static updateObject(existingNode: Object, updatedNode: Object): ChangeSummary {
+        const changed = <ChangeSummary>{numChanges: 0, locationChanged: false};
+        for (const key of Object.keys(updatedNode)) {
+            const o = updatedNode[key];
+            if (['id', 'x', 'y', 'fx', 'fy', 'vx', 'vy', 'index'].some(k => k === key)) {
+                continue;
+            } else if (o && typeof o === 'object' && o.constructor === Object) {
+                const subChanged = ForceSvgComponent.updateObject(existingNode[key], updatedNode[key]);
+                changed.numChanges += subChanged.numChanges;
+                changed.locationChanged = subChanged.locationChanged ? true : changed.locationChanged;
+            } else if (existingNode === undefined) {
+                // Copy the whole object
+                existingNode = updatedNode;
+                changed.locationChanged = true;
+                changed.numChanges++;
+            } else if (existingNode[key] !== updatedNode[key]) {
+                if (['locType', 'latOrY', 'longOrX', 'latitude', 'longitude', 'gridX', 'gridY'].some(k => k === key)) {
+                    changed.locationChanged = true;
+                }
+                changed.numChanges++;
+                existingNode[key] = updatedNode[key];
+            }
+        }
+        return changed;
+    }
+
+    @HostListener('window:resize', ['$event'])
+    onResize(event) {
+        this.graph.restartSimulation();
+        this.log.debug('Simulation restart after resize', event);
+    }
+
+    /**
+     * After the component is initialized create the Force simulation
+     * The list of devices, hosts and links will not have been receieved back
+     * from the WebSocket yet as this time - they will be updated later through
+     * ngOnChanges()
+     */
+    ngOnInit() {
+        // Receiving an initialized simulated graph from our custom d3 service
+        this.graph = new ForceDirectedGraph(SVGCANVAS, this.log);
+
+        /** Binding change detection check on each tick
+         * This along with an onPush change detection strategy should enforce
+         * checking only when relevant! This improves scripting computation
+         * duration in a couple of tests I've made, consistently. Also, it makes
+         * sense to avoid unnecessary checks when we are dealing only with
+         * simulations data binding.
+         */
+        this.graph.ticker.subscribe((simulation) => {
+            // this.log.debug("Force simulation has ticked. Alpha",
+            //     Math.round(simulation.alpha() * 1000) / 1000);
+            this.ref.markForCheck();
+        });
+
+        this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
+
+    }
+
+    /**
+     * When any one of the inputs get changed by a containing component, this
+     * gets called automatically. In addition this is called manually by
+     * topology.service when a response is received from the WebSocket from the
+     * server
+     *
+     * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
+     * The Links are added to the Link list of the simulation.
+     * Before they are added the Links are associated with Nodes based on their endPt
+     *
+     * @param changes - a list of changed @Input(s)
+     */
+    ngOnChanges(changes: SimpleChanges) {
+        if (changes['regionData']) {
+            const devices: Device[] =
+                changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
+            const hosts: Host[] =
+                changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
+            const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
+            this.graph.nodes = [];
+            if (devices) {
+                this.graph.nodes = devices;
+            }
+            if (hosts) {
+                this.graph.nodes = this.graph.nodes.concat(hosts);
+            }
+            if (subRegions) {
+                this.graph.nodes = this.graph.nodes.concat(subRegions);
+            }
+
+            this.graph.nodes.forEach((n) => this.fixPosition(n));
+
+            // Associate the endpoints of each link with a real node
+            this.graph.links = [];
+            for (const linkIdx of Object.keys(this.regionData.links)) {
+                const link = this.regionData.links[linkIdx];
+                const epA = ForceSvgComponent.extractNodeName(link.epA, link.portA);
+                if (!this.graph.nodes.find((node) => node.id === epA)) {
+                    this.log.error('ngOnChange Could not find endpoint A', epA, 'for', link);
+                    continue;
+                }
+                const epB = ForceSvgComponent.extractNodeName(
+                    link.epB, link.portB);
+                if (!this.graph.nodes.find((node) => node.id === epB)) {
+                    this.log.error('ngOnChange Could not find endpoint B', epB, 'for', link);
+                    continue;
+                }
+                this.regionData.links[linkIdx].source =
+                    this.graph.nodes.find((node) =>
+                        node.id === epA);
+                this.regionData.links[linkIdx].target =
+                    this.graph.nodes.find((node) =>
+                        node.id === epB);
+                this.regionData.links[linkIdx].index = Number(linkIdx);
+            }
+
+            this.graph.links = this.regionData.links;
+            if (this.graph.nodes.length > 0) {
+                this.graph.reinitSimulation();
+            }
+            this.log.debug('ForceSvgComponent input changed',
+                this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
+            if (!this.viewInitialized) {
+                this.viewInitialized = true;
+                if (this.showAlarms) {
+                    this.wss.sendEvent('alarmTopovDisplayStart', {});
+                }
+            }
+        }
+
+        if (changes['showAlarms'] && this.viewInitialized) {
+            if (this.showAlarms) {
+                this.wss.sendEvent('alarmTopovDisplayStart', {});
+            } else {
+                this.wss.sendEvent('alarmTopovDisplayStop', {});
+                this.cancelAllDeviceHighlightsNow();
+            }
+        }
+    }
+
+    ngOnDestroy(): void {
+        if (this.showAlarms) {
+            this.wss.sendEvent('alarmTopovDisplayStop', {});
+            this.cancelAllDeviceHighlightsNow();
+        }
+        this.viewInitialized = false;
+    }
+
+    /**
+     * If instance has a value then mute colors of devices not connected to it
+     * Otherwise if instance does not have a value unmute all
+     * @param instanceName name of the selected instance
+     */
+    changeInstSelection(instanceName: string) {
+        this.log.debug('Mastership changed', instanceName);
+        this.devices.filter((d) => d.device.master !== instanceName)
+            .forEach((d) => {
+                const isMuted = Boolean(instanceName);
+                d.ngOnChanges({'colorMuted': new SimpleChange(!isMuted, isMuted, true)});
+            }
+        );
+    }
+
+    /**
+     * If a node has a fixed location then assign it to fx and fy so
+     * that it doesn't get affected by forces
+     * @param graphNode The node whose location should be processed
+     */
+    private fixPosition(graphNode: Node): void {
+        const loc: Location = <Location>graphNode['location'];
+        const props: DeviceProps = <DeviceProps>graphNode['props'];
+        const metaUi = <MetaUi>graphNode['metaUi'];
+        if (loc && loc.locType === LocationType.GEO) {
+            const position: MetaUi =
+                ZoomUtils.convertGeoToCanvas(
+                    <LocMeta>{lng: loc.longOrX, lat: loc.latOrY});
+            graphNode.fx = position.x;
+            graphNode.fy = position.y;
+            this.log.debug('Found node', graphNode.id, 'with', loc.locType);
+        } else if (loc && loc.locType === LocationType.GRID) {
+            graphNode.fx = loc.longOrX;
+            graphNode.fy = loc.latOrY;
+            this.log.debug('Found node', graphNode.id, 'with', loc.locType);
+        } else if (props && props.locType === LocationType.NONE && metaUi) {
+            graphNode.fx = metaUi.x;
+            graphNode.fy = metaUi.y;
+            this.log.debug('Found node', graphNode.id, 'with locType=none and metaUi');
+        } else {
+            graphNode.fx = null;
+            graphNode.fy = null;
+        }
+    }
+
+    /**
+     * Get the index of LayerType so it can drive the visibility of nodes and
+     * hosts on layers
+     */
+    visibleLayerIdx(): number {
+        const layerKeys: string[] = Object.keys(LayerType);
+        for (const idx in layerKeys) {
+            if (LayerType[layerKeys[idx]] === this.visibleLayer) {
+                return Number(idx);
+            }
+        }
+        return -1;
+    }
+
+    selectLink(link: RegionLink): void {
+        this.selectedLink = link;
+        this.linkSelected.emit(link);
+    }
+
+    /**
+     * Iterate through all hosts and devices and links to deselect the previously selected
+     * node. The emit an event to the parent that lets it know the selection has
+     * changed.
+     *
+     * This function collates all of the nodes that have been selected and passes
+     * a collection of nodes up to the topology component
+     *
+     * @param selectedNode the newly selected node
+     */
+    updateSelected(selectedNode: SelectedEvent): void {
+        this.log.debug('Node or link ',
+            selectedNode.uiElement ? selectedNode.uiElement.id : '--',
+            selectedNode.deselecting ? 'deselected' : 'selected',
+            selectedNode.isShift ? 'Multiple' : '');
+
+        if (selectedNode.isShift && selectedNode.deselecting) {
+            const idx = this.selectedNodes.findIndex((n) =>
+                n.id === selectedNode.uiElement.id
+            );
+            this.selectedNodes.splice(idx, 1);
+            this.log.debug('Removed node', idx);
+
+        } else if (selectedNode.isShift) {
+            this.selectedNodes.push(selectedNode.uiElement);
+
+        } else if (selectedNode.deselecting) {
+            this.devices
+                .forEach((d) => d.deselect());
+            this.hosts
+                .forEach((h) => h.deselect());
+            this.links
+                .forEach((l) => l.deselect());
+            this.selectedNodes = [];
+
+        } else {
+            const selNodeId = selectedNode.uiElement.id;
+            // Otherwise if shift was not pressed deselect previous
+            this.devices
+                .filter((d) => d.device.id !== selNodeId)
+                .forEach((d) => d.deselect());
+            this.hosts
+                .filter((h) => h.host.id !== selNodeId)
+                .forEach((h) => h.deselect());
+
+            this.links
+                .filter((l) => l.link.id !== selNodeId)
+                .forEach((l) => l.deselect());
+
+            this.selectedNodes = [selectedNode.uiElement];
+        }
+        // Push the changes back up to parent (Topology Component)
+        this.selectedNodeEvent.emit(this.selectedNodes);
+    }
+
+    /**
+     * We want to filter links to show only those not related to hosts if the
+     * 'showHosts' flag has been switched off. If 'showHosts' is true, then
+     * display all links.
+     */
+    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.fixPosition(<Device>data);
+                    this.graph.nodes.push(<Device>data);
+                    this.regionData.devices[this.visibleLayerIdx()].push(<Device>data);
+                    this.log.debug('Device added', (<Device>data).id);
+                } else if (memo === ModelEventMemo.UPDATED) {
+                    const oldDevice: Device =
+                        this.regionData.devices[this.visibleLayerIdx()]
+                            .find((d) => d.id === subject);
+                    const changes = ForceSvgComponent.updateObject(oldDevice, <Device>data);
+                    if (changes.numChanges > 0) {
+                        this.log.debug('Device ', oldDevice.id, memo, ' - ', changes, 'changes');
+                        if (changes.locationChanged) {
+                            this.fixPosition(oldDevice);
+                        }
+                        const svgDevice: DeviceNodeSvgComponent =
+                            this.devices.find((svgdevice) => svgdevice.device.id === subject);
+                        svgDevice.ngOnChanges({'device':
+                                new SimpleChange(<Device>{}, oldDevice, true)
+                        });
+                    }
+                } else {
+                    this.log.warn('Device ', memo, ' - not yet implemented', data);
+                }
+                break;
+            case ModelEventType.HOST_ADDED_OR_UPDATED:
+                if (memo === ModelEventMemo.ADDED) {
+                    this.fixPosition(<Host>data);
+                    this.graph.nodes.push(<Host>data);
+                    this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
+                    this.log.debug('Host added', (<Host>data).id);
+                } else if (memo === ModelEventMemo.UPDATED) {
+                    const oldHost: Host = this.regionData.hosts[this.visibleLayerIdx()]
+                        .find((h) => h.id === subject);
+                    const changes = ForceSvgComponent.updateObject(oldHost, <Host>data);
+                    if (changes.numChanges > 0) {
+                        this.log.debug('Host ', oldHost.id, memo, ' - ', changes, 'changes');
+                        if (changes.locationChanged) {
+                            this.fixPosition(oldHost);
+                        }
+                    }
+                } 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);
+                    this.regionData.devices[this.visibleLayerIdx()].splice(removeIdx, 1);
+                    this.removeRelatedLinks(subject);
+                    this.log.debug('Device ', subject, 'removed. Links', this.regionData.links);
+                } 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);
+                    this.regionData.hosts[this.visibleLayerIdx()].splice(removeIdx, 1);
+                    this.removeRelatedLinks(subject);
+                    this.log.debug('Host ', subject, 'removed');
+                } else {
+                    this.log.warn('Host removed - unexpected memo', memo);
+                }
+                break;
+            case ModelEventType.LINK_ADDED_OR_UPDATED:
+                if (memo === ModelEventMemo.ADDED &&
+                    this.regionData.links.findIndex((l) => l.id === subject) === -1) {
+                    const newLink = <RegionLink>data;
+
+
+                    const epA = ForceSvgComponent.extractNodeName(
+                        newLink.epA, newLink.portA);
+                    if (!this.graph.nodes.find((node) => node.id === epA)) {
+                        this.log.error('Could not find endpoint A', epA, 'of', newLink);
+                        break;
+                    }
+                    const epB = ForceSvgComponent.extractNodeName(
+                        newLink.epB, newLink.portB);
+                    if (!this.graph.nodes.find((node) => node.id === epB)) {
+                        this.log.error('Could not find endpoint B', epB, 'of link', newLink);
+                        break;
+                    }
+
+                    const listLen = this.regionData.links.push(<RegionLink>data);
+                    this.regionData.links[listLen - 1].source =
+                        this.graph.nodes.find((node) => node.id === epA);
+                    this.regionData.links[listLen - 1].target =
+                        this.graph.nodes.find((node) => node.id === epB);
+                    this.log.debug('Link added', subject);
+                } else if (memo === ModelEventMemo.UPDATED) {
+                    const oldLink = this.regionData.links.find((l) => l.id === subject);
+                    const changes = ForceSvgComponent.updateObject(oldLink, <RegionLink>data);
+                    this.log.debug('Link ', subject, '. Updated', changes, 'items');
+                } else {
+                    this.log.warn('Link event ignored', subject, data);
+                }
+                break;
+            case ModelEventType.LINK_REMOVED:
+                if (memo === ModelEventMemo.REMOVED) {
+                    const removeIdx = this.regionData.links.findIndex((l) => l.id === subject);
+                    this.regionData.links.splice(removeIdx, 1);
+                    this.log.debug('Link ', subject, 'removed');
+                }
+                break;
+            default:
+                this.log.error('Unexpected model event', type, 'for', subject, 'Data', data);
+        }
+        this.graph.links = this.regionData.links;
+        this.graph.reinitSimulation();
+    }
+
+    private removeRelatedLinks(subject: string) {
+        const len = this.regionData.links.length;
+        for (let i = 0; i < len; i++) {
+            const linkIdx = this.regionData.links.findIndex((l) =>
+                (ForceSvgComponent.extractNodeName(l.epA, l.portA) === subject ||
+                    ForceSvgComponent.extractNodeName(l.epB, l.portB) === subject));
+            if (linkIdx >= 0) {
+                this.regionData.links.splice(linkIdx, 1);
+                this.log.debug('Link ', linkIdx, 'removed on attempt', i);
+            }
+        }
+    }
+
+    /**
+     * When traffic monitoring is turned on (A key) highlights will be sent back
+     * from the WebSocket through the Traffic Service
+     * Also handles Intent highlights in case one is selected
+     * @param devices - an array of device highlights
+     * @param hosts - an array of host highlights
+     * @param links - an array of link highlights
+     */
+    handleHighlights(devices: DeviceHighlight[], hosts: HostHighlight[], links: LinkHighlight[], fadeMs: number = 0): void {
+
+        if (devices.length > 0) {
+            this.log.debug(devices.length, 'Devices highlighted');
+            devices.forEach((dh: DeviceHighlight) => {
+                this.devices.forEach((d: DeviceNodeSvgComponent) => {
+                    if (d.device.id === dh.id) {
+                        d.badge = dh.badge;
+                        this.ref.markForCheck(); // Forces ngOnChange in the DeviceSvgComponent
+                        this.log.debug('Highlighting device', dh.id);
+                    }
+                });
+            });
+        }
+        if (hosts.length > 0) {
+            this.log.debug(hosts.length, 'Hosts highlighted');
+            hosts.forEach((hh: HostHighlight) => {
+                this.hosts.forEach((h) => {
+                    if (h.host.id === hh.id) {
+                        h.badge = hh.badge;
+                        this.ref.markForCheck(); // Forces ngOnChange in the HostSvgComponent
+                        this.log.debug('Highlighting host', hh.id);
+                    }
+                });
+            });
+        }
+        if (links.length > 0) {
+            this.log.debug(links.length, 'Links highlighted');
+            links.forEach((lh: LinkHighlight) => {
+                if (fadeMs > 0) {
+                    lh.fadems = fadeMs;
+                }
+                // Don't user .filter() above as it will create a copy of the component which will be discarded
+                this.links.forEach((l) => {
+                    if (l.link.id === Link.linkIdFromShowHighlights(lh.id)) {
+                        l.linkHighlight = lh;
+                        this.ref.markForCheck(); // Forces ngOnChange in the LinkSvgComponent
+                    }
+                });
+            });
+        }
+    }
+
+    cancelAllHostHighlightsNow() {
+        this.hosts.forEach((host: HostNodeSvgComponent) => {
+            host.badge = undefined;
+            this.ref.markForCheck(); // Forces ngOnChange in the HostSvgComponent
+        });
+    }
+
+    cancelAllDeviceHighlightsNow() {
+        this.devices.forEach((device: DeviceNodeSvgComponent) => {
+            device.badge = undefined;
+            this.ref.markForCheck(); // Forces ngOnChange in the DeviceSvgComponent
+        });
+    }
+
+    cancelAllLinkHighlightsNow() {
+        this.links.forEach((link: LinkSvgComponent) => {
+            link.linkHighlight = <LinkHighlight>{};
+            this.ref.markForCheck(); // Forces ngOnChange in the LinkSvgComponent
+        });
+    }
+
+    /**
+     * As nodes are dragged around the graph, their new location should be sent
+     * back to server
+     * @param klass The class of node e.g. 'host' or 'device'
+     * @param id - the ID of the node
+     * @param newLocation - the new Location of the node
+     */
+    nodeMoved(klass: string, id: string, newLocation: MetaUi) {
+        this.wss.sendEvent('updateMeta2', <UpdateMeta>{
+            id: id,
+            class: klass,
+            memento: newLocation
+        });
+        this.log.debug(klass, id, 'has been moved to', newLocation);
+    }
+
+    /**
+     * If any nodes with fixed positions had been dragged out of place
+     * then put back where they belong
+     * If there are some devices selected reset only these
+     */
+    resetNodeLocations(): number {
+        let numbernodes = 0;
+        if (this.selectedNodes.length > 0) {
+            this.devices
+                .filter((d) => this.selectedNodes.some((s) => s.id === d.device.id))
+                .forEach((dev) => {
+                    Node.resetNodeLocation(<Node>dev.device);
+                    numbernodes++;
+                });
+            this.hosts
+                .filter((h) => this.selectedNodes.some((s) => s.id === h.host.id))
+                .forEach((h) => {
+                    Host.resetNodeLocation(<Host>h.host);
+                    numbernodes++;
+                });
+        } else {
+            this.devices.forEach((dev) => {
+                Node.resetNodeLocation(<Node>dev.device);
+                numbernodes++;
+            });
+            this.hosts.forEach((h) => {
+                Host.resetNodeLocation(<Host>h.host);
+                numbernodes++;
+            });
+        }
+        this.graph.reinitSimulation();
+        return numbernodes;
+    }
+
+    /**
+     * Toggle floating nodes between unpinned and frozen
+     * There may be frozen and unpinned in the selection
+     *
+     * If there are nodes selected toggle only these
+     */
+    unpinOrFreezeNodes(freeze: boolean): number {
+        let numbernodes = 0;
+        if (this.selectedNodes.length > 0) {
+            this.devices
+                .filter((d) => this.selectedNodes.some((s) => s.id === d.device.id))
+                .forEach((d) => {
+                    Node.unpinOrFreezeNode(<Node>d.device, freeze);
+                    numbernodes++;
+                });
+            this.hosts
+                .filter((h) => this.selectedNodes.some((s) => s.id === h.host.id))
+                .forEach((h) => {
+                    Node.unpinOrFreezeNode(<Node>h.host, freeze);
+                    numbernodes++;
+                });
+        } else {
+            this.devices.forEach((d) => {
+                Node.unpinOrFreezeNode(<Node>d.device, freeze);
+                numbernodes++;
+            });
+            this.hosts.forEach((h) => {
+                Node.unpinOrFreezeNode(<Node>h.host, freeze);
+                numbernodes++;
+            });
+        }
+        this.graph.reinitSimulation();
+        return numbernodes;
+    }
+}
+
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/models/force-directed-graph.spec.ts b/web/gui2-topo-lib/lib/layer/forcesvg/models/force-directed-graph.spec.ts
new file mode 100644
index 0000000..2fb9155
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/models/force-directed-graph.spec.ts
@@ -0,0 +1,101 @@
+/*
+ * 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 {ForceDirectedGraph, Options} from './force-directed-graph';
+import {Node} from './node';
+import {Link} from './link';
+import {LogService} from '../../../../../gui2-fw-lib/public_api';
+import {TestBed} from '@angular/core/testing';
+
+export class TestNode extends Node {
+    constructor(id: string) {
+        super(id);
+    }
+}
+
+export class TestLink extends Link {
+    constructor(source: Node, target: Node) {
+        super(source, target);
+    }
+}
+
+/**
+ * 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, logSpy);
+
+        for (let i = 0; i < 10; i++) {
+            const newNode: TestNode = new TestNode('id' + i);
+            nodes.push(newNode);
+        }
+        for (let j = 1; j < 10; j++) {
+            const newLink = new TestLink(nodes[0], nodes[j]);
+            links.push(newLink);
+        }
+        fdg.nodes = nodes;
+        fdg.links = links;
+        fdg.reinitSimulation();
+        logServiceSpy = TestBed.get(LogService);
+    });
+
+    afterEach(() => {
+        fdg.stopSimulation();
+        fdg.nodes = [];
+        fdg.links = [];
+        fdg.reinitSimulation();
+    });
+
+    it('should be created', () => {
+        expect(fdg).toBeTruthy();
+    });
+
+    it('should have simulation', () => {
+        expect(fdg.simulation).toBeTruthy();
+    });
+
+    it('should have 10 nodes', () => {
+        expect(fdg.nodes.length).toEqual(10);
+    });
+
+    it('should have 10 links', () => {
+        expect(fdg.links.length).toEqual(9);
+    });
+
+    // TODO fix these up to listen for tick
+    // it('nodes should not be at zero', () => {
+    //     expect(nodes[0].x).toBeGreaterThan(0);
+    // });
+    // it('ticker should emit', () => {
+    //     let tickMe = jasmine.createSpy("tickMe() spy");
+    //     fdg.ticker.subscribe((simulation) => tickMe());
+    //     expect(tickMe).toHaveBeenCalled();
+    // });
+
+    // it('init links chould be called ', () => {
+    //     spyOn(fdg, 'initLinks');
+    //     // expect(fdg).toBeTruthy();
+    //     fdg.reinitSimulation(options);
+    //     expect(fdg.initLinks).toHaveBeenCalled();
+    // });
+});
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/models/force-directed-graph.ts b/web/gui2-topo-lib/lib/layer/forcesvg/models/force-directed-graph.ts
new file mode 100644
index 0000000..7bf44af
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/models/force-directed-graph.ts
@@ -0,0 +1,126 @@
+/*
+ * 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 { EventEmitter } from '@angular/core';
+import { Link } from './link';
+import { Node } from './node';
+import * as d3 from 'd3-force';
+import {LogService} from '../../../../../gui2-fw-lib/public_api';
+
+const FORCES = {
+    COLLISION: 1,
+    GRAVITY: 0.4,
+    FRICTION: 0.7
+};
+
+const CHARGES = {
+    device: -800,
+    host: -2000,
+    region: -800,
+    _def_: -1200
+};
+
+const LINK_DISTANCE = {
+    // note: key is link.type
+    direct: 100,
+    optical: 120,
+    UiEdgeLink: 3,
+    UiDeviceLink: 100,
+    _def_: 50,
+};
+
+/**
+ * note: key is link.type
+ * range: {0.0 ... 1.0}
+ */
+const LINK_STRENGTH = {
+    _def_: 0.5
+};
+
+export interface Options {
+    width: number;
+    height: number;
+}
+
+/**
+ * The inspiration for this approach comes from
+ * https://medium.com/netscape/visualizing-data-with-angular-and-d3-209dde784aeb
+ *
+ * Do yourself a favour and read https://d3indepth.com/force-layout/
+ */
+export class ForceDirectedGraph {
+    public ticker: EventEmitter<d3.Simulation<Node, Link>> = new EventEmitter();
+    public simulation: d3.Simulation<any, any>;
+    public canvasOptions: Options;
+    public nodes: Node[] = [];
+    public links: Link[] = [];
+
+    constructor(options: Options, public log: LogService) {
+        this.canvasOptions = options;
+        const ticker = this.ticker;
+
+        // Creating the force simulation and defining the charges
+        this.simulation = d3.forceSimulation()
+            .force('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))
+            .force('center',
+                d3.forceCenter(this.canvasOptions.width / 2, this.canvasOptions.height / 2))
+            .force('x', d3.forceX())
+            .force('y', d3.forceY())
+            .on('tick', () => {
+                ticker.emit(this.simulation); // ForceSvgComponent.ngOnInit listens
+            });
+
+    }
+
+    /**
+     * Assigning updated node and restarting the simulation
+     * Setting alpha to 0.3 and it will count down to alphaTarget=0
+     */
+    public reinitSimulation() {
+        this.simulation.nodes(this.nodes);
+        this.simulation.force('link',
+            d3.forceLink(this.links)
+                .strength(LINK_STRENGTH._def_)
+                .distance(this.distance.bind(this))
+        );
+        this.simulation.alpha(0.3).restart();
+    }
+
+    charges(node: Node) {
+        const nodeType = node.nodeType;
+        return CHARGES[nodeType] || CHARGES._def_;
+    }
+
+    distance(link: Link) {
+        const linkType = link.type;
+        return LINK_DISTANCE[linkType] || LINK_DISTANCE._def_;
+    }
+
+    stopSimulation() {
+        this.simulation.stop();
+        this.log.debug('Simulation stopped');
+    }
+
+    public restartSimulation(alpha: number = 0.3) {
+        this.simulation.alpha(alpha).restart();
+        this.log.debug('Simulation restarted. Alpha:', alpha);
+    }
+}
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/models/index.ts b/web/gui2-topo-lib/lib/layer/forcesvg/models/index.ts
new file mode 100644
index 0000000..36fd2e7
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/models/index.ts
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+export * from './node';
+export * from './link';
+export * from './regions';
+
+export * from './force-directed-graph';
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/models/link.ts b/web/gui2-topo-lib/lib/layer/forcesvg/models/link.ts
new file mode 100644
index 0000000..d5ff2a7
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/models/link.ts
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2019-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 {Node, UiElement} from './node';
+import * as d3 from 'd3';
+
+export enum LinkType {
+    UiRegionLink,
+    UiDeviceLink,
+    UiEdgeLink
+}
+
+/**
+ * model of the topo2CurrentRegion region rollup from Region below
+ *
+ */
+export interface RegionRollup {
+    id: string;
+    epA: string;
+    epB: string;
+    portA: string;
+    portB: string;
+    type: LinkType;
+}
+
+/**
+ * Implementing SimulationLinkDatum interface into our custom Link class
+ */
+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;
+    rollup: RegionRollup[]; // Links in sub regions represented by this one link
+
+    // Must - defining enforced implementation properties
+    source: Node;
+    target: Node;
+
+    public static deviceNameFromEp(ep: string): string {
+        if (ep !== undefined && ep.lastIndexOf('/') > 0) {
+            return ep.substr(0, ep.lastIndexOf('/'));
+        }
+        return ep;
+    }
+
+    /**
+     * The WSS event showHighlights is sent up with a slightly different
+     * name format on the link id using the "-" separator rather than the "~"
+     * @param linkId The id of the link in either format
+     */
+    public static linkIdFromShowHighlights(linkId: string) {
+        if (linkId.includes('-')) {
+            const parts: string[] = linkId.split('-');
+            const part0 = Link.removeHostPortNum(parts[0]);
+            const part1 = Link.removeHostPortNum(parts[1]);
+            return part0 + '~' + part1;
+        }
+        return linkId;
+    }
+
+    private static removeHostPortNum(hostStr: string) {
+        if (hostStr.includes('/None/')) {
+            const subparts = hostStr.split('/');
+            return subparts[0] + '/' + subparts[1];
+        }
+        return hostStr;
+    }
+
+    constructor(source, target) {
+        this.source = source;
+        this.target = target;
+    }
+
+    linkTypeStr(): string {
+        return LinkType[this.type];
+    }
+}
+
+/**
+ * model of the topo2CurrentRegion region link from Region
+ */
+export class RegionLink extends Link {
+
+    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;
+    fadems: number;
+}
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/models/node.ts b/web/gui2-topo-lib/lib/layer/forcesvg/models/node.ts
new file mode 100644
index 0000000..5bcba8c
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/models/node.ts
@@ -0,0 +1,309 @@
+/*
+ * 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 * as d3 from 'd3';
+import {LocationType} from '../../backgroundsvg/backgroundsvg.component';
+import {LayerType, Location, NodeType, RegionProps} from './regions';
+import {LocMeta, MetaUi, ZoomUtils} from '../../../../../gui2-fw-lib/public_api';
+
+export interface UiElement {
+    index?: number;
+    id: string;
+}
+
+export namespace LabelToggle {
+    /**
+     * Toggle state for how device labels should be displayed
+     */
+    export enum Enum {
+        NONE,
+        ID,
+        NAME
+    }
+
+    /**
+     * Add the method 'next()' to the LabelToggle enum above
+     */
+    export function next(current: Enum) {
+        if (current === Enum.NONE) {
+            return Enum.ID;
+        } else if (current === Enum.ID) {
+            return Enum.NAME;
+        } else if (current === Enum.NAME) {
+            return Enum.NONE;
+        }
+    }
+}
+
+export namespace HostLabelToggle {
+    /**
+     * Toggle state for how host labels should be displayed
+     */
+    export enum Enum {
+        NONE,
+        NAME,
+        IP,
+        MAC
+    }
+
+    /**
+     * Add the method 'next()' to the HostLabelToggle enum above
+     */
+    export function next(current: Enum) {
+        if (current === Enum.NONE) {
+            return Enum.NAME;
+        } else if (current === Enum.NAME) {
+            return Enum.IP;
+        } else if (current === Enum.IP) {
+            return Enum.MAC;
+        } else if (current === Enum.MAC) {
+            return Enum.NONE;
+        }
+    }
+}
+
+export namespace GridDisplayToggle {
+    /**
+     * Toggle state for how the grid should be displayed
+     */
+    export enum Enum {
+        GRIDNONE,
+        GRID1000,
+        GRIDGEO,
+        GRIDBOTH
+    }
+
+    /**
+     * Add the method 'next()' to the GridDisplayToggle enum above
+     */
+    export function next(current: Enum) {
+        if (current === Enum.GRIDNONE) {
+            return Enum.GRID1000;
+        } else if (current === Enum.GRID1000) {
+            return Enum.GRIDGEO;
+        } else if (current === Enum.GRIDGEO) {
+            return Enum.GRIDBOTH;
+        } else if (current === Enum.GRIDBOTH) {
+            return Enum.GRIDNONE;
+        }
+    }
+}
+
+/**
+ * model of the topo2CurrentRegion device props from Device below
+ */
+export interface DeviceProps {
+    latitude: string;
+    longitude: string;
+    gridX: number;
+    gridY: number;
+    name: string;
+    locType: LocationType;
+    uiType: string;
+    channelId: string;
+    managementAddress: string;
+    protocol: string;
+    driver: string;
+}
+
+export interface HostProps {
+    gridX: number;
+    gridY: number;
+    latitude: number;
+    longitude: number;
+    locType: LocationType;
+    name: string;
+}
+
+/**
+ * Implementing SimulationNodeDatum interface into our custom Node class
+ */
+export class Node implements UiElement, d3.SimulationNodeDatum {
+    // Optional - defining optional implementation properties - required for relevant typing assistance
+    index?: number;
+    x: number;
+    y: number;
+    vx?: number;
+    vy?: number;
+    fx?: number | null;
+    fy?: number | null;
+    nodeType: NodeType;
+    location: Location;
+    id: string;
+
+    protected constructor(id) {
+        this.id = id;
+        this.x = 0;
+        this.y = 0;
+    }
+
+    /**
+     * Static method to reset the node's position to that specified in its
+     * coordinates
+     * This is overridden for host
+     * @param node The node to reset
+     */
+    static resetNodeLocation(node: Node): void {
+        let origLoc: MetaUi;
+
+        if (!node.location || node.location.locType === LocationType.NONE) {
+            // No location - nothing to do
+            return;
+        } else if (node.location.locType === LocationType.GEO) {
+            origLoc = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+                lng: node.location.longOrX,
+                lat: node.location.latOrY
+            });
+        } else if (node.location.locType === LocationType.GRID) {
+            origLoc = ZoomUtils.convertXYtoGeo(
+                node.location.longOrX, node.location.latOrY);
+        }
+        Node.moveNodeTo(node, origLoc);
+    }
+
+    protected static moveNodeTo(node: Node, origLoc: MetaUi) {
+        const currentX = node.fx;
+        const currentY = node.fy;
+        const distX = origLoc.x - node.fx;
+        const distY = origLoc.y - node.fy;
+        let count = 0;
+        const task = setInterval(() => {
+            count++;
+            if (count >= 10) {
+                clearInterval(task);
+            }
+            node.fx = currentX + count * distX / 10;
+            node.fy = currentY + count * distY / 10;
+        }, 50);
+    }
+
+    static unpinOrFreezeNode(node: Node, freeze: boolean): void {
+        if (!node.location || node.location.locType === LocationType.NONE) {
+            if (freeze) {
+                node.fx = node.x;
+                node.fy = node.y;
+            } else {
+                node.fx = null;
+                node.fy = null;
+            }
+        }
+    }
+}
+
+export interface Badge {
+    status: string;
+    isGlyph: boolean;
+    txt: string;
+    msg: string;
+}
+
+/**
+ * model of the topo2CurrentRegion device from Region below
+ */
+export class Device extends Node {
+    id: string;
+    layer: LayerType;
+    metaUi: MetaUi;
+    master: string;
+    online: boolean;
+    props: DeviceProps;
+    type: string;
+
+    constructor(id: string) {
+        super(id);
+    }
+}
+
+export interface DeviceHighlight {
+    id: string;
+    badge: Badge;
+}
+
+export interface HostHighlight {
+    id: string;
+    badge: Badge;
+}
+
+/**
+ * Model of the ONOS Host element in the topology
+ */
+export class Host extends Node {
+    configured: boolean;
+    id: string;
+    ips: string[];
+    layer: LayerType;
+    props: HostProps;
+
+    constructor(id: string) {
+        super(id);
+    }
+
+    static resetNodeLocation(host: Host): void {
+        let origLoc: MetaUi;
+
+        if (!host.props || host.props.locType === LocationType.NONE) {
+            // No location - nothing to do
+            return;
+        } else if (host.props.locType === LocationType.GEO) {
+            origLoc = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+                lng: host.props.longitude,
+                lat: host.props.latitude
+            });
+        } else if (host.props.locType === LocationType.GRID) {
+            origLoc = ZoomUtils.convertXYtoGeo(
+                host.props.gridX, host.props.gridY);
+        }
+        Node.moveNodeTo(host, origLoc);
+    }
+}
+
+
+/**
+ * model of the topo2CurrentRegion subregion from Region below
+ */
+export class SubRegion extends Node {
+    id: string;
+    location: Location;
+    nDevs: number;
+    nHosts: number;
+    name: string;
+    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,
+    LINK_REMOVED,
+}
+
+/**
+ * Enumerated values for topology update event memo field
+ */
+export enum ModelEventMemo {
+    ADDED = 'added',
+    REMOVED = 'removed',
+    UPDATED = 'updated'
+}
+
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/models/regions.ts b/web/gui2-topo-lib/lib/layer/forcesvg/models/regions.ts
new file mode 100644
index 0000000..3c1894b
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/models/regions.ts
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+/**
+ * Enum of the topo2CurrentRegion node type from SubRegion below
+ */
+import {LocationType} from '../../backgroundsvg/backgroundsvg.component';
+import {Device, Host, SubRegion} from './node';
+import {RegionLink} from './link';
+
+export enum NodeType {
+    REGION = 'region',
+    DEVICE = 'device',
+    HOST = 'host',
+}
+
+/**
+ * Enum of the topo2CurrentRegion layerOrder from Region below
+ */
+export enum LayerType {
+    LAYER_OPTICAL = 'opt',
+    LAYER_PACKET = 'pkt',
+    LAYER_DEFAULT = 'def'
+}
+
+/**
+ * model of the topo2CurrentRegion location from SubRegion below
+ */
+export interface Location {
+    locType: LocationType;
+    latOrY: number;
+    longOrX: number;
+}
+
+/**
+ * model of the topo2CurrentRegion props from SubRegion below
+ */
+export interface RegionProps {
+    latitude: number;
+    longitude: number;
+    name: string;
+    peerLocations: string;
+}
+
+/**
+ * model of the topo2CurrentRegion WebSocket response
+ *
+ * The Devices are in a 2D array - 1st order is layer type, 2nd order is
+ * devices in that layer
+ */
+export interface Region {
+    note?: string;
+    id: string;
+    devices: Device[][];
+    hosts: Host[][];
+    links: RegionLink[];
+    layerOrder: LayerType[];
+    peerLocations?: Location[];
+    subregions: SubRegion[];
+}
+
+/**
+ * model of the topo2PeerRegions WebSocket response
+ */
+export interface Peer {
+    peers: SubRegion[];
+}
+
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/tests/test-OdtnConfig-topo2CurrentRegion.json b/web/gui2-topo-lib/lib/layer/forcesvg/tests/test-OdtnConfig-topo2CurrentRegion.json
new file mode 100644
index 0000000..2087940
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/tests/test-OdtnConfig-topo2CurrentRegion.json
@@ -0,0 +1,165 @@
+{
+    "event": "topo2CurrentRegion",
+    "payload": {
+        "id": "(root)",
+        "subregions": [],
+        "links": [
+            {
+                "id": "netconf:127.0.0.1:11002/201~netconf:127.0.0.1:11003/201",
+                "epA": "netconf:127.0.0.1:11002/201",
+                "epB": "netconf:127.0.0.1:11003/201",
+                "type": "UiDeviceLink",
+                "portA": "201",
+                "portB": "201",
+                "rollup": [
+                    {
+                        "id": "netconf:127.0.0.1:11002/201~netconf:127.0.0.1:11003/201",
+                        "epA": "netconf:127.0.0.1:11002/201",
+                        "epB": "netconf:127.0.0.1:11003/201",
+                        "type": "UiDeviceLink",
+                        "portA": "201",
+                        "portB": "201"
+                    }
+                ]
+            },
+            {
+                "id": "netconf:127.0.0.1:11002/202~netconf:127.0.0.1:11003/202",
+                "epA": "netconf:127.0.0.1:11002/202",
+                "epB": "netconf:127.0.0.1:11003/202",
+                "type": "UiDeviceLink",
+                "portA": "202",
+                "portB": "202",
+                "rollup": [
+                    {
+                        "id": "netconf:127.0.0.1:11002/202~netconf:127.0.0.1:11003/202",
+                        "epA": "netconf:127.0.0.1:11002/202",
+                        "epB": "netconf:127.0.0.1:11003/202",
+                        "type": "UiDeviceLink",
+                        "portA": "202",
+                        "portB": "202"
+                    }
+                ]
+            },
+            {
+                "id": "netconf:127.0.0.1:11002/203~netconf:127.0.0.1:11003/203",
+                "epA": "netconf:127.0.0.1:11002/203",
+                "epB": "netconf:127.0.0.1:11003/203",
+                "type": "UiDeviceLink",
+                "portA": "203",
+                "portB": "203",
+                "rollup": [
+                    {
+                        "id": "netconf:127.0.0.1:11002/203~netconf:127.0.0.1:11003/203",
+                        "epA": "netconf:127.0.0.1:11002/203",
+                        "epB": "netconf:127.0.0.1:11003/203",
+                        "type": "UiDeviceLink",
+                        "portA": "203",
+                        "portB": "203"
+                    }
+                ]
+            },
+            {
+                "id": "netconf:127.0.0.1:11002/204~netconf:127.0.0.1:11003/204",
+                "epA": "netconf:127.0.0.1:11002/204",
+                "epB": "netconf:127.0.0.1:11003/204",
+                "type": "UiDeviceLink",
+                "portA": "204",
+                "portB": "204",
+                "rollup": [
+                    {
+                        "id": "netconf:127.0.0.1:11002/204~netconf:127.0.0.1:11003/204",
+                        "epA": "netconf:127.0.0.1:11002/204",
+                        "epB": "netconf:127.0.0.1:11003/204",
+                        "type": "UiDeviceLink",
+                        "portA": "204",
+                        "portB": "204"
+                    }
+                ]
+            },
+            {
+                "id": "netconf:127.0.0.1:11002/205~netconf:127.0.0.1:11003/205",
+                "epA": "netconf:127.0.0.1:11002/205",
+                "epB": "netconf:127.0.0.1:11003/205",
+                "type": "UiDeviceLink",
+                "portA": "205",
+                "portB": "205",
+                "rollup": [
+                    {
+                        "id": "netconf:127.0.0.1:11002/205~netconf:127.0.0.1:11003/205",
+                        "epA": "netconf:127.0.0.1:11002/205",
+                        "epB": "netconf:127.0.0.1:11003/205",
+                        "type": "UiDeviceLink",
+                        "portA": "205",
+                        "portB": "205"
+                    }
+                ]
+            },
+            {
+                "id": "netconf:127.0.0.1:11002/206~netconf:127.0.0.1:11003/206",
+                "epA": "netconf:127.0.0.1:11002/206",
+                "epB": "netconf:127.0.0.1:11003/206",
+                "type": "UiDeviceLink",
+                "portA": "206",
+                "portB": "206",
+                "rollup": [
+                    {
+                        "id": "netconf:127.0.0.1:11002/206~netconf:127.0.0.1:11003/206",
+                        "epA": "netconf:127.0.0.1:11002/206",
+                        "epB": "netconf:127.0.0.1:11003/206",
+                        "type": "UiDeviceLink",
+                        "portA": "206",
+                        "portB": "206"
+                    }
+                ]
+            }
+        ],
+        "devices": [
+            [],
+            [],
+            [
+                {
+                    "id": "netconf:127.0.0.1:11002",
+                    "nodeType": "device",
+                    "type": "terminal_device",
+                    "online": true,
+                    "master": "127.0.0.1",
+                    "layer": "def",
+                    "props": {
+                        "ipaddress": "127.0.0.1",
+                        "protocol": "NETCONF",
+                        "driver": "cassini-ocnos",
+                        "port": "11002",
+                        "name": "cassini2",
+                        "locType": "none"
+                    }
+                },
+                {
+                    "id": "netconf:127.0.0.1:11003",
+                    "nodeType": "device",
+                    "type": "terminal_device",
+                    "online": true,
+                    "master": "127.0.0.1",
+                    "layer": "def",
+                    "props": {
+                        "ipaddress": "127.0.0.1",
+                        "protocol": "NETCONF",
+                        "driver": "cassini-ocnos",
+                        "port": "11003",
+                        "name": "cassini1",
+                        "locType": "none"
+                    }
+                }
+            ]
+        ],
+        "hosts": [
+            [],
+            [],
+            []
+        ],
+        "layerOrder": [
+            "opt",
+            "pkt",
+            "def"
+        ]
+    }
+}
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/tests/test-module-topo2CurrentRegion.json b/web/gui2-topo-lib/lib/layer/forcesvg/tests/test-module-topo2CurrentRegion.json
new file mode 100644
index 0000000..e8af22f
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/tests/test-module-topo2CurrentRegion.json
@@ -0,0 +1,1204 @@
+{
+  "event": "topo2CurrentRegion",
+  "payload": {
+    "id": "(root)",
+    "subregions": [],
+    "links": [
+      {
+        "id": "00:AA:00:00:00:03/None~of:0000000000000205/6",
+        "epA": "00:AA:00:00:00:03/None",
+        "epB": "of:0000000000000205",
+        "type": "UiEdgeLink",
+        "portB": "6",
+        "rollup": [
+          {
+            "id": "00:AA:00:00:00:03/None~of:0000000000000205/6",
+            "epA": "00:AA:00:00:00:03/None",
+            "epB": "of:0000000000000205",
+            "type": "UiEdgeLink",
+            "portB": "6"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000205/3~of:0000000000000227/5",
+        "epA": "of:0000000000000205/3",
+        "epB": "of:0000000000000227/5",
+        "type": "UiDeviceLink",
+        "portA": "3",
+        "portB": "5",
+        "rollup": [
+          {
+            "id": "of:0000000000000205/3~of:0000000000000227/5",
+            "epA": "of:0000000000000205/3",
+            "epB": "of:0000000000000227/5",
+            "type": "UiDeviceLink",
+            "portA": "3",
+            "portB": "5"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000206/2~of:0000000000000226/8",
+        "epA": "of:0000000000000206/2",
+        "epB": "of:0000000000000226/8",
+        "type": "UiDeviceLink",
+        "portA": "2",
+        "portB": "8",
+        "rollup": [
+          {
+            "id": "of:0000000000000206/2~of:0000000000000226/8",
+            "epA": "of:0000000000000206/2",
+            "epB": "of:0000000000000226/8",
+            "type": "UiDeviceLink",
+            "portA": "2",
+            "portB": "8"
+          }
+        ]
+      },
+      {
+        "id": "00:BB:00:00:00:05/None~of:0000000000000203/7",
+        "epA": "00:BB:00:00:00:05/None",
+        "epB": "of:0000000000000203",
+        "type": "UiEdgeLink",
+        "portB": "7",
+        "rollup": [
+          {
+            "id": "00:BB:00:00:00:05/None~of:0000000000000203/7",
+            "epA": "00:BB:00:00:00:05/None",
+            "epB": "of:0000000000000203",
+            "type": "UiEdgeLink",
+            "portB": "7"
+          }
+        ]
+      },
+      {
+        "id": "00:DD:00:00:00:01/None~of:0000000000000207/3",
+        "epA": "00:DD:00:00:00:01/None",
+        "epB": "of:0000000000000207",
+        "type": "UiEdgeLink",
+        "portB": "3",
+        "rollup": [
+          {
+            "id": "00:DD:00:00:00:01/None~of:0000000000000207/3",
+            "epA": "00:DD:00:00:00:01/None",
+            "epB": "of:0000000000000207",
+            "type": "UiEdgeLink",
+            "portB": "3"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000203/1~of:0000000000000226/1",
+        "epA": "of:0000000000000203/1",
+        "epB": "of:0000000000000226/1",
+        "type": "UiDeviceLink",
+        "portA": "1",
+        "portB": "1",
+        "rollup": [
+          {
+            "id": "of:0000000000000203/1~of:0000000000000226/1",
+            "epA": "of:0000000000000203/1",
+            "epB": "of:0000000000000226/1",
+            "type": "UiDeviceLink",
+            "portA": "1",
+            "portB": "1"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000207/2~of:0000000000000247/1",
+        "epA": "of:0000000000000207/2",
+        "epB": "of:0000000000000247/1",
+        "type": "UiDeviceLink",
+        "portA": "2",
+        "portB": "1",
+        "rollup": [
+          {
+            "id": "of:0000000000000207/2~of:0000000000000247/1",
+            "epA": "of:0000000000000207/2",
+            "epB": "of:0000000000000247/1",
+            "type": "UiDeviceLink",
+            "portA": "2",
+            "portB": "1"
+          }
+        ]
+      },
+      {
+        "id": "00:99:66:00:00:01/None~of:0000000000000205/10",
+        "epA": "00:99:66:00:00:01/None",
+        "epB": "of:0000000000000205",
+        "type": "UiEdgeLink",
+        "portB": "10",
+        "rollup": [
+          {
+            "id": "00:99:66:00:00:01/None~of:0000000000000205/10",
+            "epA": "00:99:66:00:00:01/None",
+            "epB": "of:0000000000000205",
+            "type": "UiEdgeLink",
+            "portB": "10"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000208/1~of:0000000000000246/2",
+        "epA": "of:0000000000000208/1",
+        "epB": "of:0000000000000246/2",
+        "type": "UiDeviceLink",
+        "portA": "1",
+        "portB": "2",
+        "rollup": [
+          {
+            "id": "of:0000000000000208/1~of:0000000000000246/2",
+            "epA": "of:0000000000000208/1",
+            "epB": "of:0000000000000246/2",
+            "type": "UiDeviceLink",
+            "portA": "1",
+            "portB": "2"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000206/1~of:0000000000000226/7",
+        "epA": "of:0000000000000206/1",
+        "epB": "of:0000000000000226/7",
+        "type": "UiDeviceLink",
+        "portA": "1",
+        "portB": "7",
+        "rollup": [
+          {
+            "id": "of:0000000000000206/1~of:0000000000000226/7",
+            "epA": "of:0000000000000206/1",
+            "epB": "of:0000000000000226/7",
+            "type": "UiDeviceLink",
+            "portA": "1",
+            "portB": "7"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000226/9~of:0000000000000246/3",
+        "epA": "of:0000000000000226/9",
+        "epB": "of:0000000000000246/3",
+        "type": "UiDeviceLink",
+        "portA": "9",
+        "portB": "3",
+        "rollup": [
+          {
+            "id": "of:0000000000000226/9~of:0000000000000246/3",
+            "epA": "of:0000000000000226/9",
+            "epB": "of:0000000000000246/3",
+            "type": "UiDeviceLink",
+            "portA": "9",
+            "portB": "3"
+          }
+        ]
+      },
+      {
+        "id": "00:AA:00:00:00:04/None~of:0000000000000205/7",
+        "epA": "00:AA:00:00:00:04/None",
+        "epB": "of:0000000000000205",
+        "type": "UiEdgeLink",
+        "portB": "7",
+        "rollup": [
+          {
+            "id": "00:AA:00:00:00:04/None~of:0000000000000205/7",
+            "epA": "00:AA:00:00:00:04/None",
+            "epB": "of:0000000000000205",
+            "type": "UiEdgeLink",
+            "portB": "7"
+          }
+        ]
+      },
+      {
+        "id": "00:88:00:00:00:03/110~of:0000000000000205/11",
+        "epA": "00:88:00:00:00:03/110",
+        "epB": "of:0000000000000205",
+        "type": "UiEdgeLink",
+        "portB": "11",
+        "rollup": [
+          {
+            "id": "00:88:00:00:00:03/110~of:0000000000000205/11",
+            "epA": "00:88:00:00:00:03/110",
+            "epB": "of:0000000000000205",
+            "type": "UiEdgeLink",
+            "portB": "11"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000204/1~of:0000000000000226/3",
+        "epA": "of:0000000000000204/1",
+        "epB": "of:0000000000000226/3",
+        "type": "UiDeviceLink",
+        "portA": "1",
+        "portB": "3",
+        "rollup": [
+          {
+            "id": "of:0000000000000204/1~of:0000000000000226/3",
+            "epA": "of:0000000000000204/1",
+            "epB": "of:0000000000000226/3",
+            "type": "UiDeviceLink",
+            "portA": "1",
+            "portB": "3"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000203/2~of:0000000000000226/2",
+        "epA": "of:0000000000000203/2",
+        "epB": "of:0000000000000226/2",
+        "type": "UiDeviceLink",
+        "portA": "2",
+        "portB": "2",
+        "rollup": [
+          {
+            "id": "of:0000000000000203/2~of:0000000000000226/2",
+            "epA": "of:0000000000000203/2",
+            "epB": "of:0000000000000226/2",
+            "type": "UiDeviceLink",
+            "portA": "2",
+            "portB": "2"
+          }
+        ]
+      },
+      {
+        "id": "00:88:00:00:00:01/None~of:0000000000000205/12",
+        "epA": "00:88:00:00:00:01/None",
+        "epB": "of:0000000000000205",
+        "type": "UiEdgeLink",
+        "portB": "12",
+        "rollup": [
+          {
+            "id": "00:88:00:00:00:01/None~of:0000000000000205/12",
+            "epA": "00:88:00:00:00:01/None",
+            "epB": "of:0000000000000205",
+            "type": "UiEdgeLink",
+            "portB": "12"
+          }
+        ]
+      },
+      {
+        "id": "00:88:00:00:00:04/160~of:0000000000000206/6",
+        "epA": "00:88:00:00:00:04/160",
+        "epB": "of:0000000000000206",
+        "type": "UiEdgeLink",
+        "portB": "6",
+        "rollup": [
+          {
+            "id": "00:88:00:00:00:04/160~of:0000000000000206/6",
+            "epA": "00:88:00:00:00:04/160",
+            "epB": "of:0000000000000206",
+            "type": "UiEdgeLink",
+            "portB": "6"
+          }
+        ]
+      },
+      {
+        "id": "00:DD:00:00:00:02/None~of:0000000000000208/3",
+        "epA": "00:DD:00:00:00:02/None",
+        "epB": "of:0000000000000208",
+        "type": "UiEdgeLink",
+        "portB": "3",
+        "rollup": [
+          {
+            "id": "00:DD:00:00:00:02/None~of:0000000000000208/3",
+            "epA": "00:DD:00:00:00:02/None",
+            "epB": "of:0000000000000208",
+            "type": "UiEdgeLink",
+            "portB": "3"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000203/3~of:0000000000000227/1",
+        "epA": "of:0000000000000203/3",
+        "epB": "of:0000000000000227/1",
+        "type": "UiDeviceLink",
+        "portA": "3",
+        "portB": "1",
+        "rollup": [
+          {
+            "id": "of:0000000000000203/3~of:0000000000000227/1",
+            "epA": "of:0000000000000203/3",
+            "epB": "of:0000000000000227/1",
+            "type": "UiDeviceLink",
+            "portA": "3",
+            "portB": "1"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000208/2~of:0000000000000247/2",
+        "epA": "of:0000000000000208/2",
+        "epB": "of:0000000000000247/2",
+        "type": "UiDeviceLink",
+        "portA": "2",
+        "portB": "2",
+        "rollup": [
+          {
+            "id": "of:0000000000000208/2~of:0000000000000247/2",
+            "epA": "of:0000000000000208/2",
+            "epB": "of:0000000000000247/2",
+            "type": "UiDeviceLink",
+            "portA": "2",
+            "portB": "2"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000205/1~of:0000000000000226/5",
+        "epA": "of:0000000000000205/1",
+        "epB": "of:0000000000000226/5",
+        "type": "UiDeviceLink",
+        "portA": "1",
+        "portB": "5",
+        "rollup": [
+          {
+            "id": "of:0000000000000205/1~of:0000000000000226/5",
+            "epA": "of:0000000000000205/1",
+            "epB": "of:0000000000000226/5",
+            "type": "UiDeviceLink",
+            "portA": "1",
+            "portB": "5"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000204/2~of:0000000000000226/4",
+        "epA": "of:0000000000000204/2",
+        "epB": "of:0000000000000226/4",
+        "type": "UiDeviceLink",
+        "portA": "2",
+        "portB": "4",
+        "rollup": [
+          {
+            "id": "of:0000000000000204/2~of:0000000000000226/4",
+            "epA": "of:0000000000000204/2",
+            "epB": "of:0000000000000226/4",
+            "type": "UiDeviceLink",
+            "portA": "2",
+            "portB": "4"
+          }
+        ]
+      },
+      {
+        "id": "00:AA:00:00:00:01/None~of:0000000000000204/6",
+        "epA": "00:AA:00:00:00:01/None",
+        "epB": "of:0000000000000204",
+        "type": "UiEdgeLink",
+        "portB": "6",
+        "rollup": [
+          {
+            "id": "00:AA:00:00:00:01/None~of:0000000000000204/6",
+            "epA": "00:AA:00:00:00:01/None",
+            "epB": "of:0000000000000204",
+            "type": "UiEdgeLink",
+            "portB": "6"
+          }
+        ]
+      },
+      {
+        "id": "00:BB:00:00:00:03/None~of:0000000000000205/8",
+        "epA": "00:BB:00:00:00:03/None",
+        "epB": "of:0000000000000205",
+        "type": "UiEdgeLink",
+        "portB": "8",
+        "rollup": [
+          {
+            "id": "00:BB:00:00:00:03/None~of:0000000000000205/8",
+            "epA": "00:BB:00:00:00:03/None",
+            "epB": "of:0000000000000205",
+            "type": "UiEdgeLink",
+            "portB": "8"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000206/4~of:0000000000000227/8",
+        "epA": "of:0000000000000206/4",
+        "epB": "of:0000000000000227/8",
+        "type": "UiDeviceLink",
+        "portA": "4",
+        "portB": "8",
+        "rollup": [
+          {
+            "id": "of:0000000000000206/4~of:0000000000000227/8",
+            "epA": "of:0000000000000206/4",
+            "epB": "of:0000000000000227/8",
+            "type": "UiDeviceLink",
+            "portA": "4",
+            "portB": "8"
+          }
+        ]
+      },
+      {
+        "id": "00:AA:00:00:00:05/None~of:0000000000000203/6",
+        "epA": "00:AA:00:00:00:05/None",
+        "epB": "of:0000000000000203",
+        "type": "UiEdgeLink",
+        "portB": "6",
+        "rollup": [
+          {
+            "id": "00:AA:00:00:00:05/None~of:0000000000000203/6",
+            "epA": "00:AA:00:00:00:05/None",
+            "epB": "of:0000000000000203",
+            "type": "UiEdgeLink",
+            "portB": "6"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000205/5~of:0000000000000206/5",
+        "epA": "of:0000000000000205/5",
+        "epB": "of:0000000000000206/5",
+        "type": "UiDeviceLink",
+        "portA": "5",
+        "portB": "5",
+        "rollup": [
+          {
+            "id": "of:0000000000000205/5~of:0000000000000206/5",
+            "epA": "of:0000000000000205/5",
+            "epB": "of:0000000000000206/5",
+            "type": "UiDeviceLink",
+            "portA": "5",
+            "portB": "5"
+          }
+        ]
+      },
+      {
+        "id": "00:BB:00:00:00:02/None~of:0000000000000204/9",
+        "epA": "00:BB:00:00:00:02/None",
+        "epB": "of:0000000000000204",
+        "type": "UiEdgeLink",
+        "portB": "9",
+        "rollup": [
+          {
+            "id": "00:BB:00:00:00:02/None~of:0000000000000204/9",
+            "epA": "00:BB:00:00:00:02/None",
+            "epB": "of:0000000000000204",
+            "type": "UiEdgeLink",
+            "portB": "9"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000204/3~of:0000000000000227/3",
+        "epA": "of:0000000000000204/3",
+        "epB": "of:0000000000000227/3",
+        "type": "UiDeviceLink",
+        "portA": "3",
+        "portB": "3",
+        "rollup": [
+          {
+            "id": "of:0000000000000204/3~of:0000000000000227/3",
+            "epA": "of:0000000000000204/3",
+            "epB": "of:0000000000000227/3",
+            "type": "UiDeviceLink",
+            "portA": "3",
+            "portB": "3"
+          }
+        ]
+      },
+      {
+        "id": "00:EE:00:00:00:01/None~of:0000000000000207/4",
+        "epA": "00:EE:00:00:00:01/None",
+        "epB": "of:0000000000000207",
+        "type": "UiEdgeLink",
+        "portB": "4",
+        "rollup": [
+          {
+            "id": "00:EE:00:00:00:01/None~of:0000000000000207/4",
+            "epA": "00:EE:00:00:00:01/None",
+            "epB": "of:0000000000000207",
+            "type": "UiEdgeLink",
+            "portB": "4"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000203/4~of:0000000000000227/2",
+        "epA": "of:0000000000000203/4",
+        "epB": "of:0000000000000227/2",
+        "type": "UiDeviceLink",
+        "portA": "4",
+        "portB": "2",
+        "rollup": [
+          {
+            "id": "of:0000000000000203/4~of:0000000000000227/2",
+            "epA": "of:0000000000000203/4",
+            "epB": "of:0000000000000227/2",
+            "type": "UiDeviceLink",
+            "portA": "4",
+            "portB": "2"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000205/2~of:0000000000000226/6",
+        "epA": "of:0000000000000205/2",
+        "epB": "of:0000000000000226/6",
+        "type": "UiDeviceLink",
+        "portA": "2",
+        "portB": "6",
+        "rollup": [
+          {
+            "id": "of:0000000000000205/2~of:0000000000000226/6",
+            "epA": "of:0000000000000205/2",
+            "epB": "of:0000000000000226/6",
+            "type": "UiDeviceLink",
+            "portA": "2",
+            "portB": "6"
+          }
+        ]
+      },
+      {
+        "id": "00:99:00:00:00:01/None~of:0000000000000205/10",
+        "epA": "00:99:00:00:00:01/None",
+        "epB": "of:0000000000000205",
+        "type": "UiEdgeLink",
+        "portB": "10",
+        "rollup": [
+          {
+            "id": "00:99:00:00:00:01/None~of:0000000000000205/10",
+            "epA": "00:99:00:00:00:01/None",
+            "epB": "of:0000000000000205",
+            "type": "UiEdgeLink",
+            "portB": "10"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000205/4~of:0000000000000227/6",
+        "epA": "of:0000000000000205/4",
+        "epB": "of:0000000000000227/6",
+        "type": "UiDeviceLink",
+        "portA": "4",
+        "portB": "6",
+        "rollup": [
+          {
+            "id": "of:0000000000000205/4~of:0000000000000227/6",
+            "epA": "of:0000000000000205/4",
+            "epB": "of:0000000000000227/6",
+            "type": "UiDeviceLink",
+            "portA": "4",
+            "portB": "6"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000206/3~of:0000000000000227/7",
+        "epA": "of:0000000000000206/3",
+        "epB": "of:0000000000000227/7",
+        "type": "UiDeviceLink",
+        "portA": "3",
+        "portB": "7",
+        "rollup": [
+          {
+            "id": "of:0000000000000206/3~of:0000000000000227/7",
+            "epA": "of:0000000000000206/3",
+            "epB": "of:0000000000000227/7",
+            "type": "UiDeviceLink",
+            "portA": "3",
+            "portB": "7"
+          }
+        ]
+      },
+      {
+        "id": "00:BB:00:00:00:04/None~of:0000000000000205/9",
+        "epA": "00:BB:00:00:00:04/None",
+        "epB": "of:0000000000000205",
+        "type": "UiEdgeLink",
+        "portB": "9",
+        "rollup": [
+          {
+            "id": "00:BB:00:00:00:04/None~of:0000000000000205/9",
+            "epA": "00:BB:00:00:00:04/None",
+            "epB": "of:0000000000000205",
+            "type": "UiEdgeLink",
+            "portB": "9"
+          }
+        ]
+      },
+      {
+        "id": "00:AA:00:00:00:02/None~of:0000000000000204/7",
+        "epA": "00:AA:00:00:00:02/None",
+        "epB": "of:0000000000000204",
+        "type": "UiEdgeLink",
+        "portB": "7",
+        "rollup": [
+          {
+            "id": "00:AA:00:00:00:02/None~of:0000000000000204/7",
+            "epA": "00:AA:00:00:00:02/None",
+            "epB": "of:0000000000000204",
+            "type": "UiEdgeLink",
+            "portB": "7"
+          }
+        ]
+      },
+      {
+        "id": "00:BB:00:00:00:01/None~of:0000000000000204/8",
+        "epA": "00:BB:00:00:00:01/None",
+        "epB": "of:0000000000000204",
+        "type": "UiEdgeLink",
+        "portB": "8",
+        "rollup": [
+          {
+            "id": "00:BB:00:00:00:01/None~of:0000000000000204/8",
+            "epA": "00:BB:00:00:00:01/None",
+            "epB": "of:0000000000000204",
+            "type": "UiEdgeLink",
+            "portB": "8"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000207/1~of:0000000000000246/1",
+        "epA": "of:0000000000000207/1",
+        "epB": "of:0000000000000246/1",
+        "type": "UiDeviceLink",
+        "portA": "1",
+        "portB": "1",
+        "rollup": [
+          {
+            "id": "of:0000000000000207/1~of:0000000000000246/1",
+            "epA": "of:0000000000000207/1",
+            "epB": "of:0000000000000246/1",
+            "type": "UiDeviceLink",
+            "portA": "1",
+            "portB": "1"
+          }
+        ]
+      },
+      {
+        "id": "00:88:00:00:00:02/None~of:0000000000000206/7",
+        "epA": "00:88:00:00:00:02/None",
+        "epB": "of:0000000000000206",
+        "type": "UiEdgeLink",
+        "portB": "7",
+        "rollup": [
+          {
+            "id": "00:88:00:00:00:02/None~of:0000000000000206/7",
+            "epA": "00:88:00:00:00:02/None",
+            "epB": "of:0000000000000206",
+            "type": "UiEdgeLink",
+            "portB": "7"
+          }
+        ]
+      },
+      {
+        "id": "00:EE:00:00:00:02/None~of:0000000000000208/4",
+        "epA": "00:EE:00:00:00:02/None",
+        "epB": "of:0000000000000208",
+        "type": "UiEdgeLink",
+        "portB": "4",
+        "rollup": [
+          {
+            "id": "00:EE:00:00:00:02/None~of:0000000000000208/4",
+            "epA": "00:EE:00:00:00:02/None",
+            "epB": "of:0000000000000208",
+            "type": "UiEdgeLink",
+            "portB": "4"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000204/4~of:0000000000000227/4",
+        "epA": "of:0000000000000204/4",
+        "epB": "of:0000000000000227/4",
+        "type": "UiDeviceLink",
+        "portA": "4",
+        "portB": "4",
+        "rollup": [
+          {
+            "id": "of:0000000000000204/4~of:0000000000000227/4",
+            "epA": "of:0000000000000204/4",
+            "epB": "of:0000000000000227/4",
+            "type": "UiDeviceLink",
+            "portA": "4",
+            "portB": "4"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000203/5~of:0000000000000204/5",
+        "epA": "of:0000000000000203/5",
+        "epB": "of:0000000000000204/5",
+        "type": "UiDeviceLink",
+        "portA": "5",
+        "portB": "5",
+        "rollup": [
+          {
+            "id": "of:0000000000000203/5~of:0000000000000204/5",
+            "epA": "of:0000000000000203/5",
+            "epB": "of:0000000000000204/5",
+            "type": "UiDeviceLink",
+            "portA": "5",
+            "portB": "5"
+          }
+        ]
+      },
+      {
+        "id": "of:0000000000000227/9~of:0000000000000247/3",
+        "epA": "of:0000000000000227/9",
+        "epB": "of:0000000000000247/3",
+        "type": "UiDeviceLink",
+        "portA": "9",
+        "portB": "3",
+        "rollup": [
+          {
+            "id": "of:0000000000000227/9~of:0000000000000247/3",
+            "epA": "of:0000000000000227/9",
+            "epB": "of:0000000000000247/3",
+            "type": "UiDeviceLink",
+            "portA": "9",
+            "portB": "3"
+          }
+        ]
+      }
+    ],
+    "devices": [
+      [],
+      [],
+      [
+        {
+          "id": "of:0000000000000246",
+          "nodeType": "device",
+          "type": "switch",
+          "online": true,
+          "master": "10.192.19.68",
+          "layer": "def",
+          "props": {
+            "managementAddress": "10.192.19.69",
+            "protocol": "OF_13",
+            "driver": "ofdpa-ovs",
+            "latitude": "40.15",
+            "name": "s246",
+            "locType": "geo",
+            "channelId": "10.192.19.69:59980",
+            "longitude": "-121.679"
+          },
+          "location": {
+            "locType": "geo",
+            "latOrY": 40.15,
+            "longOrX": -121.679
+          }
+        },
+        {
+          "id": "of:0000000000000206",
+          "nodeType": "device",
+          "type": "switch",
+          "online": true,
+          "master": "10.192.19.68",
+          "layer": "def",
+          "props": {
+            "managementAddress": "10.192.19.69",
+            "protocol": "OF_13",
+            "driver": "ofdpa-ovs",
+            "latitude": "36.766",
+            "name": "s206",
+            "locType": "geo",
+            "channelId": "10.192.19.69:59975",
+            "longitude": "-92.029"
+          },
+          "location": {
+            "locType": "geo",
+            "latOrY": 36.766,
+            "longOrX": -92.029
+          }
+        },
+        {
+          "id": "of:0000000000000227",
+          "nodeType": "device",
+          "type": "switch",
+          "online": true,
+          "master": "10.192.19.68",
+          "layer": "def",
+          "props": {
+            "managementAddress": "10.192.19.69",
+            "protocol": "OF_13",
+            "driver": "ofdpa-ovs",
+            "latitude": "44.205",
+            "name": "s227",
+            "locType": "geo",
+            "channelId": "10.192.19.69:59979",
+            "longitude": "-96.359"
+          },
+          "location": {
+            "locType": "geo",
+            "latOrY": 44.205,
+            "longOrX": -96.359
+          }
+        },
+        {
+          "id": "of:0000000000000208",
+          "nodeType": "device",
+          "type": "switch",
+          "online": true,
+          "master": "10.192.19.68",
+          "layer": "def",
+          "props": {
+            "managementAddress": "10.192.19.69",
+            "protocol": "OF_13",
+            "driver": "ofdpa-ovs",
+            "latitude": "36.766",
+            "name": "s208",
+            "locType": "geo",
+            "channelId": "10.192.19.69:59977",
+            "longitude": "-116.029"
+          },
+          "location": {
+            "locType": "geo",
+            "latOrY": 36.766,
+            "longOrX": -116.029
+          }
+        },
+        {
+          "id": "of:0000000000000205",
+          "nodeType": "device",
+          "type": "switch",
+          "online": true,
+          "master": "10.192.19.68",
+          "layer": "def",
+          "props": {
+            "managementAddress": "10.192.19.69",
+            "protocol": "OF_13",
+            "driver": "ofdpa-ovs",
+            "latitude": "36.766",
+            "name": "s205",
+            "locType": "geo",
+            "channelId": "10.192.19.69:59974",
+            "longitude": "-96.89"
+          },
+          "location": {
+            "locType": "geo",
+            "latOrY": 36.766,
+            "longOrX": -96.89
+          }
+        },
+        {
+          "id": "of:0000000000000247",
+          "nodeType": "device",
+          "type": "switch",
+          "online": true,
+          "master": "10.192.19.68",
+          "layer": "def",
+          "props": {
+            "managementAddress": "10.192.19.69",
+            "protocol": "OF_13",
+            "driver": "ofdpa-ovs",
+            "latitude": "40.205",
+            "name": "s247",
+            "locType": "geo",
+            "channelId": "10.192.19.69:59981",
+            "longitude": "-117.359"
+          },
+          "location": {
+            "locType": "geo",
+            "latOrY": 40.205,
+            "longOrX": -117.359
+          }
+        },
+        {
+          "id": "of:0000000000000226",
+          "nodeType": "device",
+          "type": "switch",
+          "online": true,
+          "master": "10.192.19.68",
+          "layer": "def",
+          "props": {
+            "managementAddress": "10.192.19.69",
+            "protocol": "OF_13",
+            "driver": "ofdpa-ovs",
+            "latitude": "44.15",
+            "name": "s226",
+            "locType": "geo",
+            "channelId": "10.192.19.69:59978",
+            "longitude": "-107.679"
+          },
+          "location": {
+            "locType": "geo",
+            "latOrY": 44.15,
+            "longOrX": -107.679
+          }
+        },
+        {
+          "id": "of:0000000000000203",
+          "nodeType": "device",
+          "type": "switch",
+          "online": true,
+          "master": "10.192.19.68",
+          "layer": "def",
+          "props": {
+            "managementAddress": "10.192.19.69",
+            "protocol": "OF_13",
+            "driver": "ofdpa-ovs",
+            "latitude": "36.766",
+            "name": "s203",
+            "locType": "geo",
+            "channelId": "10.192.19.69:59972",
+            "longitude": "-111.359"
+          },
+          "location": {
+            "locType": "geo",
+            "latOrY": 36.766,
+            "longOrX": -111.359
+          }
+        },
+        {
+          "id": "of:0000000000000204",
+          "nodeType": "device",
+          "type": "switch",
+          "online": true,
+          "master": "10.192.19.68",
+          "layer": "def",
+          "props": {
+            "managementAddress": "10.192.19.69",
+            "protocol": "OF_13",
+            "driver": "ofdpa-ovs",
+            "latitude": "36.766",
+            "name": "s204",
+            "locType": "geo",
+            "channelId": "10.192.19.69:59973",
+            "longitude": "-106.359"
+          },
+          "location": {
+            "locType": "geo",
+            "latOrY": 36.766,
+            "longOrX": -106.359
+          }
+        },
+        {
+          "id": "of:0000000000000207",
+          "nodeType": "device",
+          "type": "switch",
+          "online": true,
+          "master": "10.192.19.68",
+          "layer": "def",
+          "props": {
+            "managementAddress": "10.192.19.69",
+            "protocol": "OF_13",
+            "driver": "ofdpa-ovs",
+            "latitude": "36.766",
+            "name": "s207",
+            "locType": "geo",
+            "channelId": "10.192.19.69:59976",
+            "longitude": "-122.359"
+          },
+          "location": {
+            "locType": "geo",
+            "latOrY": 36.766,
+            "longOrX": -122.359
+          }
+        }
+      ]
+    ],
+    "hosts": [
+      [],
+      [],
+      [
+        {
+          "id": "00:88:00:00:00:03/110",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "fe80::288:ff:fe00:3",
+            "2000::102",
+            "10.0.1.2"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:DD:00:00:00:01/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:88:00:00:00:04/160",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "fe80::288:ff:fe00:4",
+            "10.0.6.2",
+            "2000::602"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:BB:00:00:00:02/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "fe80::2bb:ff:fe00:2"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:AA:00:00:00:05/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:88:00:00:00:01/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "fe80::288:ff:fe00:1",
+            "2000::101",
+            "10.0.1.1"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:AA:00:00:00:01/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:AA:00:00:00:03/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:BB:00:00:00:04/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "fe80::2bb:ff:fe00:4"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:EE:00:00:00:02/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "fe80::2ee:ff:fe00:2"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:99:00:00:00:01/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "10.0.3.253",
+            "fe80::299:ff:fe00:1"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:99:66:00:00:01/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "fe80::299:66ff:fe00:1",
+            "2000::3fd"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:EE:00:00:00:01/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "fe80::2ee:ff:fe00:1"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:BB:00:00:00:01/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "fe80::2bb:ff:fe00:1"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:BB:00:00:00:03/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "fe80::2bb:ff:fe00:3"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:AA:00:00:00:04/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:BB:00:00:00:05/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "fe80::2bb:ff:fe00:5"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:88:00:00:00:02/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [
+            "fe80::288:ff:fe00:2",
+            "2000::601",
+            "10.0.6.1"
+          ],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:AA:00:00:00:02/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [],
+          "props": {},
+          "configured": false
+        },
+        {
+          "id": "00:DD:00:00:00:02/None",
+          "nodeType": "host",
+          "layer": "def",
+          "ips": [],
+          "props": {},
+          "configured": false
+        }
+      ]
+    ],
+    "layerOrder": [
+      "opt",
+      "pkt",
+      "def"
+    ]
+  }
+}
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/badgesvg/badgesvg.component.css b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/badgesvg/badgesvg.component.css
new file mode 100644
index 0000000..373b57b
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/badgesvg/badgesvg.component.css
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+
+.status.e {
+    fill: #c72930;
+}
+
+.text.e {
+    fill: white;
+    font-weight: bold;
+}
+
+.status.w {
+    fill: #db7773;
+}
+
+.text.w {
+    fill: white;
+    font-weight: bold;
+}
+
+.status.i {
+    fill: #007dc4;
+}
+
+.text.i {
+    fill: white;
+}
+
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/badgesvg/badgesvg.component.html b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/badgesvg/badgesvg.component.html
new file mode 100644
index 0000000..e8dfcb9
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/badgesvg/badgesvg.component.html
@@ -0,0 +1,19 @@
+<!--
+~ Copyright 2019-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:g xmlns:svg="http://www.w3.org/2000/svg" [title]="badge.msg">
+    <svg:circle r="12" [ngClass]="['status', badge.status ? badge.status : '']" cx="-18" cy="-18"></svg:circle>
+    <svg:text x="-18" y="-11" text-anchor="middle" [ngClass]="['text', badge.status ? badge.status : '']">{{ badge.txt }}</svg:text>
+</svg:g>
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/badgesvg/badgesvg.component.spec.ts b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/badgesvg/badgesvg.component.spec.ts
new file mode 100644
index 0000000..2b457b4
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/badgesvg/badgesvg.component.spec.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 {BadgeSvgComponent} from './badgesvg.component';
+
+describe('BadgeSvgComponent', () => {
+    let component: BadgeSvgComponent;
+    let fixture: ComponentFixture<BadgeSvgComponent>;
+
+    beforeEach(async(() => {
+        TestBed.configureTestingModule({
+            declarations: [BadgeSvgComponent]
+        })
+        .compileComponents();
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(BadgeSvgComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/badgesvg/badgesvg.component.ts b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/badgesvg/badgesvg.component.ts
new file mode 100644
index 0000000..f0bbc7a
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/badgesvg/badgesvg.component.ts
@@ -0,0 +1,34 @@
+/*
+ * 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, OnInit} from '@angular/core';
+import {Badge} from '../../models';
+
+@Component({
+    selector: '[onos-badgesvg]',
+    templateUrl: './badgesvg.component.html',
+    styleUrls: ['./badgesvg.component.css']
+})
+export class BadgeSvgComponent implements OnInit {
+    @Input() badge: Badge = <Badge>{};
+
+    constructor() {
+    }
+
+    ngOnInit() {
+    }
+
+}
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css
new file mode 100644
index 0000000..e7ce209
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css
@@ -0,0 +1,57 @@
+/*
+ * 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 device visual) -- CSS file
+ */
+g.node.device rect {
+    fill: #f0f0f070;
+}
+g.node.device text {
+    fill: #bbb;
+}
+g.node.device use {
+    fill: #777;
+}
+
+
+g.node.device.online rect {
+    fill: #fafafad0;
+}
+g.node.device.online text {
+    fill: #3c3a3a;
+}
+g.node.device.online use {
+    /* NOTE: this gets overridden programatically */
+    fill: #ffffff;
+}
+
+g.node.selected .node-container {
+    stroke-width: 2.0;
+    stroke: #009fdb;
+}
+
+g.node.hovered .node-container {
+    stroke-width: 2.0;
+    stroke: #454545;
+}
+
+path.bracket {
+    stroke: white;
+    stroke-width: 1;
+    fill: none
+}
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
new file mode 100644
index 0000000..c4f5621
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
@@ -0,0 +1,100 @@
+<!--
+~ Copyright 2019-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">
+    <!-- Template explanation: Define an SVG Filter that in
+        line 0) creates a box big enough to accommodate the drop shadow
+        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" x="-25%" y="-25%" width="200%" height="200%">
+        <svg:feGaussianBlur in="SourceAlpha" stdDeviation="4" 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>
+    <!-- Template explanation: Define a colour gradient that can be used in icons -->
+    <svg:linearGradient id="diagonal_blue" x1="0%" y1="0%" x2="100%" y2="100%">
+        <svg:stop offset= "0%" style="stop-color: #7fabdb;" />
+        <svg:stop offset= "100%" style="stop-color: #5b99d2;" />
+    </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
+            and scale it inversely to the zoom level
+    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, $event)">
+    <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.1)+'px' }}"
+            filter= "url(#drop-shadow)">
+    </svg:rect>
+    <!-- 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" [ngStyle]="{'fill': panelColor}">
+    </svg:rect>
+    <!-- Create an L shaped bracket on bottom left of icon if it has either grid or geo location-->
+    <svg:path *ngIf="device.location && device.location.locType != 'none'"
+              d="M-15 12 v3 h3" class="bracket">
+    </svg:path>
+    <!-- Create an L shaped bracket on top right of icon if it has been pinned or has fixed location-->
+    <svg:path *ngIf="device.fx != null"
+              d="M15 -12 v-3 h-3" class="bracket">
+    </svg:path>
+    <!-- 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"
+            [@deviceLabelToggleTxt]="labelToggle">
+        {{ labelToggle == 0 ? '': labelToggle == 1 ? device.id:device.props.name }}
+    </svg:text>
+    <svg:use [attr.xlink:href]="'#' + deviceIcon()" width="36" height="36" x="-18" y="-18">
+    </svg:use>
+    <svg:g *ngIf="badge" onos-badgesvg [badge]="badge"></svg:g>
+</svg:g>
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts
new file mode 100644
index 0000000..deb175e
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DeviceNodeSvgComponent } from './devicenodesvg.component';
+import {FnService, IconService, LogService, SvgUtilService} from '../../../../../../gui2-fw-lib/public_api';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {ChangeDetectorRef} from '@angular/core';
+import {Device} from '../../models';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {TopologyService} from '../../../../topology.service';
+import {BadgeSvgComponent} from '../badgesvg/badgesvg.component';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockIconService {
+    loadIconDef() { }
+}
+
+class MockSvgUtilService {
+
+    cat7() {
+        const tcid = 'd3utilTestCard';
+
+        function getColor(id, muted, theme) {
+            // NOTE: since we are lazily assigning domain ids, we need to
+            //       get the color from all 4 scales, to keep the domains
+            //       in sync.
+            const ln = '#5b99d2';
+            const lm = '#9ebedf';
+            const dn = '#5b99d2';
+            const dm = '#9ebedf';
+            if (theme === 'dark') {
+                return muted ? dm : dn;
+            } else {
+                return muted ? lm : ln;
+            }
+        }
+
+        return {
+            // testCard: testCard,
+            getColor: getColor,
+        };
+    }
+}
+
+class MockTopologyService {
+    public instancesIndex: Map<string, number>;
+    constructor() {
+        this.instancesIndex = new Map();
+    }
+}
+
+describe('DeviceNodeSvgComponent', () => {
+    let fs: FnService;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: DeviceNodeSvgComponent;
+    let fixture: ComponentFixture<DeviceNodeSvgComponent>;
+    let windowMock: Window;
+    let ar: MockActivatedRoute;
+    let testDevice: Device;
+
+
+    beforeEach(async(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({ 'debug': 'txrx' });
+        testDevice = new Device('test:1');
+        testDevice.online = true;
+
+        windowMock = <any>{
+            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, windowMock);
+
+        TestBed.configureTestingModule({
+            imports: [ BrowserAnimationsModule ],
+            declarations: [ DeviceNodeSvgComponent, BadgeSvgComponent ],
+            providers: [
+                { provide: LogService, useValue: logSpy },
+                { provide: ActivatedRoute, useValue: ar },
+                { provide: ChangeDetectorRef, useClass: ChangeDetectorRef },
+                { provide: IconService, useClass: MockIconService },
+                { provide: SvgUtilService, useClass: MockSvgUtilService },
+                { provide: TopologyService, useClass: MockTopologyService },
+                { provide: 'Window', useValue: windowMock },
+            ]
+        })
+        .compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(DeviceNodeSvgComponent);
+        component = fixture.componentInstance;
+        component.device = testDevice;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
new file mode 100644
index 0000000..2243e20
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
@@ -0,0 +1,168 @@
+/*
+ * 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 {
+    ChangeDetectionStrategy,
+    ChangeDetectorRef,
+    Component,
+    EventEmitter,
+    Input,
+    OnChanges, OnInit, Output,
+    SimpleChanges,
+} from '@angular/core';
+import {
+    Badge,
+    Device,
+    LabelToggle,
+} from '../../models';
+import {IconService, LogService, SvgUtilService} from '../../../../../../gui2-fw-lib/public_api';
+import {NodeVisual, SelectedEvent} from '../nodevisual';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+import {TopologyService} from '../../../../topology.service';
+
+/**
+ * The Device node in the force graph
+ *
+ * Note: here the selector is given square brackets [] so that it can be
+ * inserted in SVG element like a directive
+ */
+@Component({
+    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 => 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 OnInit, OnChanges {
+    @Input() device: Device;
+    @Input() scale: number = 1.0;
+    @Input() labelToggle: LabelToggle.Enum = LabelToggle.Enum.NONE;
+    @Input() colorMuted: boolean = false;
+    @Input() colorTheme: string = 'light';
+    @Input() badge: Badge;
+    @Output() selectedEvent = new EventEmitter<SelectedEvent>();
+    textWidth: number = 36;
+    panelColor: string = '#9ebedf';
+
+    constructor(
+        protected log: LogService,
+        private is: IconService,
+        protected sus: SvgUtilService,
+        protected ts: TopologyService,
+        private ref: ChangeDetectorRef
+    ) {
+        super();
+    }
+
+    ngOnInit(): void {
+        this.panelColor = this.panelColour();
+    }
+
+    /**
+     * Called by parent (forcesvg) when a change happens
+     *
+     * There is a difficulty in passing the SVG text object to the animation
+     * directly, to get its width, so we capture it here and update textWidth
+     * local variable here and use it in the animation
+     */
+    ngOnChanges(changes: SimpleChanges) {
+        if (changes['device']) {
+            if (!this.device.x) {
+                this.device.x = 0;
+                this.device.y = 0;
+            }
+            // The master might have changed - recalculate color
+            this.panelColor = this.panelColour();
+        }
+
+        if (changes['colorMuted']) {
+            this.colorMuted = changes['colorMuted'].currentValue;
+            this.panelColor = this.panelColour();
+        }
+
+        if (changes['badge']) {
+            this.badge = changes['badge'].currentValue;
+        }
+    }
+
+    /**
+     * 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;
+        } 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;
+        } else {
+            return 0;
+        }
+    }
+
+    deviceIcon(): string {
+        if (this.device.props && this.device.props.uiType) {
+            this.is.loadIconDef(this.device.props.uiType);
+            return this.device.props.uiType;
+        } else {
+            return 'm_' + this.device.type;
+        }
+    }
+
+    /**
+     * Get a colour for the banner of the nth panel
+     * @param idx The index of the panel (0-6)
+     */
+    panelColour(): string {
+        const idx = this.ts.instancesIndex.get(this.device.master);
+        return this.sus.cat7().getColor(idx, this.colorMuted, this.colorTheme);
+    }
+}
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.css b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.css
new file mode 100644
index 0000000..92a114f
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.css
@@ -0,0 +1,43 @@
+/*
+ * 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 host visual) -- CSS file
+ */
+.node.host text {
+    stroke: none;
+    font-size: 13px;
+    fill: #846;
+}
+
+.node.host circle {
+    stroke: #a3a596;
+    fill: #e0dfd6;
+}
+
+.node.host.selected > circle {
+    stroke-width: 2.0;
+    stroke: #009fdb;
+}
+
+.node.host use {
+    fill: #3c3a3a;
+}
+
+.node.host rect {
+    fill: #ffffff;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html
new file mode 100644
index 0000000..bdaf54a
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html
@@ -0,0 +1,71 @@
+<!--
+~ 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">
+    <!-- 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" x="-25%" y="-25%" width="200%" height="200%">
+        <svg:feGaussianBlur in="SourceAlpha" stdDeviation="4" 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: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, $event)">
+    <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">
+    </svg:use>
+    <!-- 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"
+        >{{hostName()}}</svg:text>
+    <svg:g *ngIf="badge" onos-badgesvg [badge]="badge"></svg:g>
+</svg:g>
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts
new file mode 100644
index 0000000..cf697bb
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts
@@ -0,0 +1,70 @@
+/*
+ * 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/public_api';
+import {Host} from '../../models';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {ChangeDetectorRef} from '@angular/core';
+import {BadgeSvgComponent} from '../badgesvg/badgesvg.component';
+
+class MockActivatedRoute extends ActivatedRoute {
+  constructor(params: Params) {
+    super();
+    this.queryParams = of(params);
+  }
+}
+
+describe('HostNodeSvgComponent', () => {
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: HostNodeSvgComponent;
+    let fixture: ComponentFixture<HostNodeSvgComponent>;
+    let ar: MockActivatedRoute;
+    let testHost: Host;
+
+    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'];
+
+        TestBed.configureTestingModule({
+            imports: [ BrowserAnimationsModule ],
+            declarations: [ HostNodeSvgComponent, BadgeSvgComponent ],
+            providers: [
+              { provide: LogService, useValue: logSpy },
+              { provide: ActivatedRoute, useValue: ar },
+              { provide: ChangeDetectorRef, useClass: ChangeDetectorRef }
+            ]
+        })
+        .compileComponents();
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(HostNodeSvgComponent);
+        component = fixture.componentInstance;
+        component.host = testHost;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.ts b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.ts
new file mode 100644
index 0000000..decd099
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.ts
@@ -0,0 +1,77 @@
+/*
+ * 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,
+    EventEmitter,
+    Input,
+    OnChanges,
+    Output,
+    SimpleChanges
+} from '@angular/core';
+import {Badge, Host, HostLabelToggle, Node} from '../../models';
+import {LogService} from '../../../../../../gui2-fw-lib/public_api';
+import {NodeVisual, SelectedEvent} from '../nodevisual';
+
+/**
+ * The Host node in the force graph
+ *
+ * Note: here the selector is given square brackets [] so that it can be
+ * inserted in SVG element like a directive
+ */
+@Component({
+    selector: '[onos-hostnodesvg]',
+    templateUrl: './hostnodesvg.component.html',
+    styleUrls: ['./hostnodesvg.component.css']
+})
+export class HostNodeSvgComponent extends NodeVisual implements OnChanges {
+    @Input() host: Host;
+    @Input() scale: number = 1.0;
+    @Input() labelToggle: HostLabelToggle.Enum = HostLabelToggle.Enum.IP;
+    @Input() badge: Badge;
+    @Output() selectedEvent = new EventEmitter<SelectedEvent>();
+
+    constructor(
+        protected log: LogService
+    ) {
+        super();
+    }
+
+    ngOnChanges(changes: SimpleChanges) {
+        if (changes['host']) {
+            if (!this.host.x) {
+                this.host.x = 0;
+                this.host.y = 0;
+            }
+        }
+
+        if (changes['badge']) {
+            this.badge = changes['badge'].currentValue;
+        }
+    }
+
+    hostName(): string {
+        if (this.host === undefined) {
+            return undefined;
+        } else if (this.labelToggle === HostLabelToggle.Enum.IP) {
+            return this.host.ips.join(',');
+        } else if (this.labelToggle === HostLabelToggle.Enum.MAC) {
+            return this.host.id;
+        } else {
+            return this.host.id; // Todo - replace with a friendly name
+        }
+
+    }
+}
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/linksvg/linksvg.component.css b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/linksvg/linksvg.component.css
new file mode 100644
index 0000000..e5f12ae
--- /dev/null
+++ b/web/gui2-topo-lib/lib/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;
+    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-topo-lib/lib/layer/forcesvg/visuals/linksvg/linksvg.component.html b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/linksvg/linksvg.component.html
new file mode 100644
index 0000000..ec3afae
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/linksvg/linksvg.component.html
@@ -0,0 +1,101 @@
+<!--
+~ 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
+    ling 4) Change the line width depending on the scale
+    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':'', highlightAsString()]"
+        [ngStyle]="{'stroke-width': (enhanced ? 4 : 2) * scale + 'px'}"
+        (click)="toggleSelected(link, $event)"
+        (mouseover)="enhance()">
+<!--        [attr.filter]="highlighted?'url(#glow)':'none'">-->
+    <svg:desc>{{link.id}} {{linkHighlight?.css}} {{isHighlighted}}</svg:desc>
+</svg:line>
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+       [ngClass]="['linkLabel']"
+       [attr.transform]="'scale(' + scale + ')'">
+    <!-- Template explanation: Creates SVG Text in the middle of the link to
+          show traffic 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"
+    >{{ linkHighlight?.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"
+       [attr.transform]="'translate(' + labelPosSrc.x + ',' + labelPosSrc.y + '),scale(' + scale + ')'">
+    <!-- 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]="2 - textLength(link.portA)/2" 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 y="2" 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"
+       [attr.transform]="'translate(' + labelPosTgt.x + ',' + labelPosTgt.y + '),scale(' + scale + ')'">
+    <svg:rect
+            [attr.x]="2 - textLength(link.portB)/2" y="-8"
+            [attr.width]="4 + textLength(link.portB)" height="16">
+    </svg:rect>
+    <svg:text x="2" y="2" text-anchor="middle"
+            [attr.textLength]= "textLength(link.portB)" lengthAdjust="spacing"
+    >{{ link.portB }}</svg:text>
+</svg:g>
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/linksvg/linksvg.component.spec.ts b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/linksvg/linksvg.component.spec.ts
new file mode 100644
index 0000000..6418fb4
--- /dev/null
+++ b/web/gui2-topo-lib/lib/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/public_api';
+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-topo-lib/lib/layer/forcesvg/visuals/linksvg/linksvg.component.ts b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/linksvg/linksvg.component.ts
new file mode 100644
index 0000000..9997897
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/linksvg/linksvg.component.ts
@@ -0,0 +1,142 @@
+/*
+ * 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/public_api';
+import {NodeVisual, SelectedEvent} from '../nodevisual';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+
+interface Point {
+    x: number;
+    y: number;
+}
+
+/*
+ * LinkSvgComponent gets its data from 2 sources - the force SVG regionData (which
+ * gives the Link below), and other state data here.
+ */
+@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() linkHighlight: LinkHighlight;
+    @Input() highlightsEnabled: boolean = true;
+    @Input() scale = 1.0;
+    isHighlighted: boolean = false;
+    @Output() selectedEvent = new EventEmitter<SelectedEvent>();
+    @Output() enhancedEvent = new EventEmitter<Link>();
+    enhanced: boolean = false;
+    labelPosSrc: Point = {x: 0, y: 0};
+    labelPosTgt: Point = {x: 0, y: 0};
+    lastTimer: any;
+
+    constructor(
+        protected log: LogService,
+        private ref: ChangeDetectorRef
+    ) {
+        super();
+    }
+
+    ngOnChanges(changes: SimpleChanges) {
+        if (changes['linkHighlight']) {
+            const hl: LinkHighlight = changes['linkHighlight'].currentValue;
+            clearTimeout(this.lastTimer);
+            this.isHighlighted = true;
+            this.log.debug('Link highlighted', this.link.id);
+
+            if (hl.fadems > 0) {
+                this.lastTimer = setTimeout(() => {
+                    this.isHighlighted = false;
+                    this.linkHighlight = <LinkHighlight>{};
+                    this.ref.markForCheck();
+                }, this.linkHighlight.fadems); // Disappear slightly before next one comes in
+            }
+        }
+
+        this.ref.markForCheck();
+    }
+
+    highlightAsString(): string {
+        if (this.linkHighlight && this.linkHighlight.css) {
+            return this.linkHighlight.css;
+        }
+        return '';
+    }
+
+    enhance() {
+        if (!this.highlightsEnabled) {
+            return;
+        }
+        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-topo-lib/lib/layer/forcesvg/visuals/nodevisual.ts b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/nodevisual.ts
new file mode 100644
index 0000000..10bf3d8
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/nodevisual.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 {EventEmitter} from '@angular/core';
+import {UiElement} from '../models';
+
+export interface SelectedEvent {
+    uiElement: UiElement;
+    deselecting: boolean;
+    isShift: boolean;
+    isCtrl: boolean;
+    isAlt: boolean;
+}
+
+/**
+ * A base class for the Host and Device components
+ */
+export abstract class NodeVisual {
+    selected: boolean;
+    selectedEvent = new EventEmitter<SelectedEvent>();
+
+    toggleSelected(uiElement: UiElement, event: MouseEvent) {
+        this.selected = !this.selected;
+        this.selectedEvent.emit(<SelectedEvent>{
+            uiElement: uiElement,
+            deselecting: !this.selected,
+            isShift: event.shiftKey,
+            isCtrl: event.ctrlKey,
+            isAlt: event.altKey
+        });
+    }
+
+    deselect() {
+        this.selected = false;
+    }
+}
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.css b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.css
new file mode 100644
index 0000000..87c23bd
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.css
@@ -0,0 +1,20 @@
+/*
+ * 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 subRegion visual) -- CSS file
+ */
\ No newline at end of file
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.html b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.html
new file mode 100644
index 0000000..5760634
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.html
@@ -0,0 +1,23 @@
+<!--
+~ 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:g  xmlns:svg="http://www.w3.org/2000/svg" [attr.transform]="'translate(' + subRegion?.x + ',' + subRegion?.y + ')'">>
+  <svg:circle
+          cx="0"
+          cy="0"
+          r="5">
+  </svg:circle>
+  <svg:text>{{subRegion?.id}}</svg:text>
+</svg:g>
\ No newline at end of file
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.spec.ts b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.spec.ts
new file mode 100644
index 0000000..d6f6446
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.spec.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { SubRegionNodeSvgComponent } from './subregionnodesvg.component';
+import {SubRegion} from '../../models';
+
+describe('SubRegionNodeSvgComponent', () => {
+    let component: SubRegionNodeSvgComponent;
+    let fixture: ComponentFixture<SubRegionNodeSvgComponent>;
+
+    beforeEach(async(() => {
+        TestBed.configureTestingModule({
+            declarations: [ SubRegionNodeSvgComponent ]
+        })
+        .compileComponents();
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(SubRegionNodeSvgComponent);
+        component = fixture.debugElement.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should create with an input', () => {
+        component.subRegion = new SubRegion('testId');
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.ts b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.ts
new file mode 100644
index 0000000..5dd4736
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.ts
@@ -0,0 +1,42 @@
+/*
+ * 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, OnChanges, SimpleChanges} from '@angular/core';
+import {SubRegion} from '../../models';
+
+/**
+ * The SubRegion node in the force graph
+ *
+ * Note 1: here the selector is given square brackets [] so that it can be
+ * inserted in SVG element like a directive
+ * Note 2: the selector is exactly the same as the @Input alias to make this
+ * directive trick work
+ */
+@Component({
+    selector: '[onos-subregionnodesvg]',
+    templateUrl: './subregionnodesvg.component.html',
+    styleUrls: ['./subregionnodesvg.component.css']
+})
+export class SubRegionNodeSvgComponent implements OnChanges {
+    @Input() subRegion: SubRegion;
+
+    ngOnChanges(changes: SimpleChanges) {
+        if (!this.subRegion.x) {
+            this.subRegion.x = 0;
+            this.subRegion.y = 0;
+        }
+    }
+
+}