GUI2 added in the layout topo overlay

Change-Id: I9960f95ae726a5af9950771ed67bcfc9d172e267
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/gui2-topo-lib.module.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/gui2-topo-lib.module.ts
index 643c3d7..dcb6321 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/gui2-topo-lib.module.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/gui2-topo-lib.module.ts
@@ -37,6 +37,7 @@
 import {FormsModule, ReactiveFormsModule} from '@angular/forms';
 import { GridsvgComponent } from './layer/gridsvg/gridsvg.component';
 import {TrafficService} from './traffic.service';
+import {LayoutService} from './layout.service';
 
 /**
  * ONOS GUI -- Topology View Module
@@ -75,7 +76,8 @@
     ],
     providers: [
         TopologyService,
-        TrafficService
+        TrafficService,
+        LayoutService
     ],
     exports: [
         BackgroundSvgComponent,
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
index 6910353..843c19c 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
@@ -36,7 +36,7 @@
     ZoomUtils
 } from 'gui2-fw-lib';
 import {
-    Device,
+    Device, DeviceProps,
     ForceDirectedGraph,
     Host,
     HostLabelToggle,
@@ -47,6 +47,7 @@
     Location,
     ModelEventMemo,
     ModelEventType,
+    Node,
     Region,
     RegionLink,
     SubRegion,
@@ -56,6 +57,7 @@
 import {DeviceNodeSvgComponent} from './visuals/devicenodesvg/devicenodesvg.component';
 import { HostNodeSvgComponent} from './visuals/hostnodesvg/hostnodesvg.component';
 import { LinkSvgComponent} from './visuals/linksvg/linksvg.component';
+import { Options } from './models/force-directed-graph';
 
 interface UpdateMeta {
     id: string;
@@ -63,6 +65,16 @@
     memento: MetaUi;
 }
 
+const SVGCANVAS = <Options>{
+    width: 1000,
+    height: 1000
+};
+
+interface ChangeSummary {
+    numChanges: number;
+    locationChanged: boolean;
+}
+
 /**
  * ONOS GUI -- Topology Forces Graph Layer View.
  *
@@ -88,7 +100,6 @@
     @Output() linkSelected = new EventEmitter<RegionLink>();
     @Output() selectedNodeEvent = new EventEmitter<UiElement>();
     public graph: ForceDirectedGraph;
-    private _options: { width, height } = { width: 800, height: 600 };
 
     // 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
@@ -131,16 +142,26 @@
      * @param existingNode 1st object
      * @param updatedNode 2nd object
      */
-    private static updateObject(existingNode: Object, updatedNode: Object): number {
-        let changed: number = 0;
+    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 (key === 'id') {
+            if (['id', 'x', 'y', 'fx', 'fy', 'vx', 'vy', 'index'].some(k => k === key)) {
                 continue;
             } else if (o && typeof o === 'object' && o.constructor === Object) {
-                changed += ForceSvgComponent.updateObject(existingNode[key], updatedNode[key]);
+                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]) {
-                changed++;
+                if (['locType', 'latOrY', 'longOrX', 'latitude', 'longitude', 'gridX', 'gridY'].some(k => k === key)) {
+                    changed.locationChanged = true;
+                }
+                changed.numChanges++;
                 existingNode[key] = updatedNode[key];
             }
         }
@@ -149,8 +170,8 @@
 
     @HostListener('window:resize', ['$event'])
     onResize(event) {
-        this.graph.initSimulation(this.options);
-        this.log.debug('Simulation reinit after resize', event);
+        this.graph.restartSimulation();
+        this.log.debug('Simulation restart after resize', event);
     }
 
     /**
@@ -161,7 +182,7 @@
      */
     ngOnInit() {
         // Receiving an initialized simulated graph from our custom d3 service
-        this.graph = new ForceDirectedGraph(this.options, this.log);
+        this.graph = new ForceDirectedGraph(SVGCANVAS, this.log);
 
         /** Binding change detection check on each tick
          * This along with an onPush change detection strategy should enforce
@@ -171,9 +192,11 @@
          * simulations data binding.
          */
         this.graph.ticker.subscribe((simulation) => {
-            // this.log.debug("Force simulation has ticked", 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');
 
     }
@@ -208,20 +231,7 @@
                 this.graph.nodes = this.graph.nodes.concat(subRegions);
             }
 
-            // If a node has a fixed location then assign it to fx and fy so
-            // that it doesn't get affected by forces
-            this.graph.nodes
-            .forEach((n) => {
-                const loc: Location = <Location>n['location'];
-                if (loc && loc.locType === LocationType.GEO) {
-                    const position: MetaUi =
-                        ZoomUtils.convertGeoToCanvas(
-                            <LocMeta>{lng: loc.longOrX, lat: loc.latOrY});
-                    n.fx = position.x;
-                    n.fy = position.y;
-                    this.log.debug('Found node', n.id, 'with', loc.locType);
-                }
-            });
+            this.graph.nodes.forEach((n) => this.fixPosition(n));
 
             // Associate the endpoints of each link with a real node
             this.graph.links = [];
@@ -240,15 +250,42 @@
             }
 
             this.graph.links = this.regionData.links;
-
-            this.graph.initSimulation(this.options);
-            this.graph.initNodes();
-            this.graph.initLinks();
+            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');
         }
+    }
 
-        this.ref.markForCheck();
+    /**
+     * 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;
+        }
     }
 
     /**
@@ -270,13 +307,6 @@
         this.linkSelected.emit(link);
     }
 
-    get options() {
-        return this._options = {
-            width: window.innerWidth,
-            height: window.innerHeight
-        };
-    }
-
     /**
      * Iterate through all hosts and devices to deselect the previously selected
      * node. The emit an event to the parent that lets it know the selection has
@@ -326,23 +356,7 @@
         switch (type) {
             case ModelEventType.DEVICE_ADDED_OR_UPDATED:
                 if (memo === ModelEventMemo.ADDED) {
-                    const loc = (<Device>data).location;
-                    if (loc && loc.locType === LocationType.GEO) {
-                        const position =
-                            ZoomUtils.convertGeoToCanvas(<LocMeta>{ lng: loc.longOrX, lat: loc.latOrY});
-                        (<Device>data).fx = position.x;
-                        (<Device>data).fy = position.y;
-                        this.log.debug('Using long', loc.longOrX, 'lat', loc.latOrY, '(', position.x, position.y, ')');
-                    } else if (loc && loc.locType === LocationType.GRID) {
-                        (<Device>data).fx = loc.longOrX;
-                        (<Device>data).fy = loc.latOrY;
-                        this.log.debug('Using grid', loc.longOrX, loc.latOrY);
-                    } else {
-                        (<Device>data).fx = null;
-                        (<Device>data).fy = null;
-                        // (<Device>data).x = 500;
-                        // (<Device>data).y = 500;
-                    }
+                    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);
@@ -351,8 +365,11 @@
                         this.regionData.devices[this.visibleLayerIdx()]
                             .find((d) => d.id === subject);
                     const changes = ForceSvgComponent.updateObject(oldDevice, <Device>data);
-                    if (changes > 0) {
+                    if (changes.numChanges > 0) {
                         this.log.debug('Device ', oldDevice.id, memo, ' - ', changes, 'changes');
+                        if (changes.locationChanged) {
+                            this.fixPosition(oldDevice);
+                        }
                     }
                 } else {
                     this.log.warn('Device ', memo, ' - not yet implemented', data);
@@ -360,15 +377,19 @@
                 break;
             case ModelEventType.HOST_ADDED_OR_UPDATED:
                 if (memo === ModelEventMemo.ADDED) {
-                    this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
+                    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 > 0) {
+                    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');
@@ -424,10 +445,8 @@
             default:
                 this.log.error('Unexpected model event', type, 'for', subject);
         }
-        this.ref.markForCheck();
-        this.graph.initSimulation(this.options);
-        this.graph.initNodes();
-        this.graph.initLinks();
+        this.graph.links = this.regionData.links;
+        this.graph.reinitSimulation();
     }
 
     private removeRelatedLinks(subject: string) {
@@ -500,7 +519,7 @@
      * @param newLocation - the new Location of the node
      */
     nodeMoved(klass: string, id: string, newLocation: MetaUi) {
-        this.wss.sendEvent('updateMeta', <UpdateMeta>{
+        this.wss.sendEvent('updateMeta2', <UpdateMeta>{
             id: id,
             class: klass,
             memento: newLocation
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.spec.ts
index bdcd5c5..90c12e1 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.spec.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.spec.ts
@@ -55,8 +55,7 @@
         }
         fdg.nodes = nodes;
         fdg.links = links;
-        fdg.initSimulation(options);
-        fdg.initNodes();
+        fdg.reinitSimulation();
         logServiceSpy = TestBed.get(LogService);
     });
 
@@ -64,7 +63,7 @@
         fdg.stopSimulation();
         fdg.nodes = [];
         fdg.links = [];
-        fdg.initSimulation(options);
+        fdg.reinitSimulation();
     });
 
     it('should be created', () => {
@@ -96,14 +95,7 @@
     // it('init links chould be called ', () => {
     //     spyOn(fdg, 'initLinks');
     //     // expect(fdg).toBeTruthy();
-    //     fdg.initSimulation(options);
+    //     fdg.reinitSimulation(options);
     //     expect(fdg.initLinks).toHaveBeenCalled();
     // });
-
-    it ('throws error on no options', () => {
-        expect(fdg.initSimulation).toThrowError('missing options when initializing simulation');
-    });
-
-
-
 });
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.ts
index 24dd029..b4de906 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.ts
@@ -20,17 +20,16 @@
 import {LogService} from 'gui2-fw-lib';
 
 const FORCES = {
-    LINKS: 1 / 50,
     COLLISION: 1,
     GRAVITY: 0.4,
     FRICTION: 0.7
 };
 
 const CHARGES = {
-    device: -80,
-    host: -200,
-    region: -80,
-    _def_: -120
+    device: -800,
+    host: -2000,
+    region: -800,
+    _def_: -1200
 };
 
 const LINK_DISTANCE = {
@@ -41,10 +40,12 @@
     _def_: 50,
 };
 
+/**
+ * note: key is link.type
+ * range: {0.0 ... 1.0}
+ */
 const LINK_STRENGTH = {
-    // note: key is link.type
-    // range: {0.0 ... 1.0}
-    _def_: 0.1
+    _def_: 0.5
 };
 
 export interface Options {
@@ -55,87 +56,68 @@
 /**
  * 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.initSimulation(options);
+        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
+            });
+
     }
 
-    initNodes() {
-        if (!this.simulation) {
-            throw new Error('simulation was not initialized yet');
-        }
-
+    /**
+     * 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);
-    }
-
-    initLinks() {
-        if (!this.simulation) {
-            throw new Error('simulation was not initialized yet');
-        }
-
-        // Initializing the links force simulation
-        this.simulation.force('links',
+        this.simulation.force('link',
             d3.forceLink(this.links)
                 .strength(this.strength.bind(this))
                 .distance(this.distance.bind(this))
         );
+        this.simulation.alpha(0.3).restart();
     }
 
-    charges(node) {
+    charges(node: Node) {
         const nodeType = node.nodeType;
         return CHARGES[nodeType] || CHARGES._def_;
     }
 
-    distance(node) {
-        const nodeType = node.nodeType;
-        return LINK_DISTANCE[nodeType] || LINK_DISTANCE._def_;
+    distance(link: Link) {
+        const linkType = link.type;
+        this.log.debug('Link type', linkType, LINK_DISTANCE[linkType]);
+        return LINK_DISTANCE[linkType] || LINK_DISTANCE._def_;
     }
 
-    strength(node) {
-        const nodeType = node.nodeType;
-        return LINK_STRENGTH[nodeType] || LINK_STRENGTH._def_;
-    }
-
-    initSimulation(options: Options) {
-        if (!options || !options.width || !options.height) {
-            throw new Error('missing options when initializing simulation');
-        }
-
-        /** Creating the simulation */
-        if (!this.simulation) {
-            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));
-
-            // Connecting the d3 ticker to an angular event emitter
-            this.simulation.on('tick', function () {
-                ticker.emit(this);
-            });
-
-            this.initNodes();
-            // this.initLinks();
-        }
-
-        /** Updating the central force of the simulation */
-        this.simulation.force('centers', d3.forceCenter(options.width / 2, options.height / 2));
-
-        /** Restarting the simulation internal timer */
-        this.simulation.restart();
+    strength(link: Link) {
+        const linkType = link.type;
+        this.log.debug('Link type', linkType, LINK_STRENGTH[linkType]);
+        return LINK_STRENGTH[linkType] || LINK_STRENGTH._def_;
     }
 
     stopSimulation() {
@@ -143,8 +125,8 @@
         this.log.debug('Simulation stopped');
     }
 
-    restartSimulation() {
-        this.simulation.restart();
-        this.log.debug('Simulation restarted');
+    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/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/node.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/node.ts
index ce22e99..b4e62cc 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/node.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/node.ts
@@ -107,6 +107,8 @@
 export interface DeviceProps {
     latitude: number;
     longitude: number;
+    gridX: number;
+    gridY: number;
     name: string;
     locType: LocationType;
     uiType: string;
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/zoomable.directive.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/zoomable.directive.ts
index 9564444..8c3707b 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/zoomable.directive.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/zoomable.directive.ts
@@ -54,8 +54,10 @@
 
         const zoomed = () => {
             const transform = d3.event.transform;
-            container.attr('transform', 'translate(' + transform.x + ',' + transform.y + ') scale(' + transform.k + ')');
-            this.updateZoomState(<TopoZoomPrefs>{tx: transform.x, ty: transform.y, sc: transform.k});
+            if (transform) {
+                container.attr('transform', 'translate(' + transform.x + ',' + transform.y + ') scale(' + transform.k + ')');
+                this.updateZoomState(<TopoZoomPrefs>{tx: transform.x, ty: transform.y, sc: transform.k});
+            }
         };
 
         this.zoom = d3.zoom().on('zoom', zoomed);
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layout.service.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layout.service.spec.ts
new file mode 100644
index 0000000..d970993
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layout.service.spec.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 { TestBed } from '@angular/core/testing';
+
+import { LayoutService } from './layout.service';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {FnService, LogService} from 'gui2-fw-lib';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+describe('LayoutService', () => {
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let ar: ActivatedRoute;
+    let fs: FnService;
+    let mockWindow: Window;
+
+    beforeEach(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn', 'info']);
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        mockWindow = <any>{
+            innerWidth: 400,
+            innerHeight: 200,
+            navigator: {
+                userAgent: 'defaultUA'
+            },
+            location: <any>{
+                hostname: 'foo',
+                host: 'foo',
+                port: '80',
+                protocol: 'http',
+                search: { debug: 'true' },
+                href: 'ws://foo:123/onos/ui/websock/path',
+                absUrl: 'ws://foo:123/onos/ui/websock/path'
+            }
+        };
+        fs = new FnService(ar, logSpy, mockWindow);
+
+        TestBed.configureTestingModule({
+            providers: [LayoutService,
+                { provide: FnService, useValue: fs},
+                { provide: LogService, useValue: logSpy },
+                { provide: ActivatedRoute, useValue: ar },
+                { provide: 'Window', useFactory: (() => mockWindow ) }
+            ]
+        });
+        logServiceSpy = TestBed.get(LogService);
+    });
+
+    it('should be created', () => {
+        const service: LayoutService = TestBed.get(LayoutService);
+        expect(service).toBeTruthy();
+    });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layout.service.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layout.service.ts
new file mode 100644
index 0000000..2ed795a
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layout.service.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import {LogService, WebSocketService} from 'gui2-fw-lib';
+
+export enum LayoutType {
+    LAYOUT_DEFAULT = 'default',
+    LAYOUT_ACCESS = 'access'
+}
+
+/**
+ * ONOS GUI - Layout service - connects to the Layout UI Extension app
+ */
+@Injectable()
+export class LayoutService {
+
+    constructor(
+        protected log: LogService,
+        protected wss: WebSocketService
+    ) {
+        this.log.debug('LayoutService constructed');
+    }
+
+    /**
+     * tell the server we want a new layout
+     * @param type The type of layout we want
+     */
+    changeLayout(type: LayoutType): void {
+        this.wss.sendEvent('doLayout', {
+            type: type
+        });
+        this.log.debug('Layout changed to', type);
+    }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.html
index 623c425..69d4557 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.html
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.html
@@ -74,4 +74,12 @@
             <onos-icon [iconSize]="25" iconId="m_cycleGridDisplay" [toolTip]="lionFn('tbtt_cyc_grid_display')" classes="button"></onos-icon>
         </div>
     </div>
-</div>
\ No newline at end of file
+    <div class="tbar-row">
+        <div class="button" id="toolbar-topo2-toolbar-topo2-layout-default" (click)="buttonClicked('layout-default-btn')">
+            <onos-icon iconSize="25" iconId="m_fiberSwitch" toolTip="Default (force-based) layout" classes="button"></onos-icon>
+        </div>
+        <div class="button" id="toolbar-topo2-toolbar-topo2-layout-access" (click)="buttonClicked('layout-access-btn')">
+            <onos-icon iconSize="25" iconId="m_disjointPaths" toolTip="Access layout - separate service leafs" classes="button"></onos-icon>
+        </div>
+    </div>
+</div>
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.ts
index 0615530..ce7b90b 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.ts
@@ -39,6 +39,8 @@
 export const CANCEL_TRAFFIC = 'cancel-traffic';
 export const ALL_TRAFFIC = 'all-traffic';
 export const QUICKHELP_BTN = 'quickhelp-btn';
+export const LAYOUT_DEFAULT_BTN = 'layout-default-btn';
+export const LAYOUT_ACCESS_BTN = 'layout-access-btn';
 
 
 /*
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.ts
index 2c5d777..509a468 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.ts
@@ -75,7 +75,7 @@
                         <ModelEventType><unknown>(ModelEventType[event.type]), // Number based enum
                         <ModelEventMemo>(event.memo), // String based enum
                         event.subject, event.data);
-                    this.log.debug('Region Data updated from WSS as topo2UiModelEvent', force.regionData);
+                    this.log.debug('Region Data updated from WSS as topo2UiModelEvent', event.subject, event.data);
                 }
             ],
             // topo2Highlights is handled by TrafficService
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.spec.ts
index b4d579d..c7b6c15 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.spec.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.spec.ts
@@ -50,6 +50,7 @@
 import {DeviceNodeSvgComponent} from '../layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component';
 import {SubRegionNodeSvgComponent} from '../layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component';
 import {HostNodeSvgComponent} from '../layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component';
+import {LayoutService} from '../layout.service';
 
 
 class MockActivatedRoute extends ActivatedRoute {
@@ -105,6 +106,8 @@
 
 class MockTrafficService {}
 
+class MockLayoutService {}
+
 class MockPrefsService {
     listeners: ((data) => void)[] = [];
 
@@ -200,6 +203,7 @@
                 { provide: HttpClient, useClass: MockHttpClient },
                 { provide: TopologyService, useClass: MockTopologyService },
                 { provide: TrafficService, useClass: MockTrafficService },
+                { provide: LayoutService, useClass: MockLayoutService },
                 { provide: IconService, useClass: MockIconService },
                 { provide: PrefsService, useClass: MockPrefsService },
                 { provide: KeysService, useClass: MockKeysService },
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.ts
index d7055a4..9f2a08b 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.ts
@@ -22,14 +22,17 @@
 } from '@angular/core';
 import * as d3 from 'd3';
 import {
-    FnService, IconService,
+    FnService,
+    IconService,
     KeysService,
-    KeysToken, LionService,
+    KeysToken,
+    LionService,
     LogService,
     PrefsService,
     SvgUtilService,
+    TopoZoomPrefs,
     WebSocketService,
-    TopoZoomPrefs, ZoomUtils
+    ZoomUtils
 } from 'gui2-fw-lib';
 import {InstanceComponent} from '../panel/instance/instance.component';
 import {DetailsComponent} from '../panel/details/details.component';
@@ -43,15 +46,29 @@
     UiElement
 } from '../layer/forcesvg/models';
 import {
-    INSTANCE_TOGGLE, SUMMARY_TOGGLE, DETAILS_TOGGLE,
-    HOSTS_TOGGLE, OFFLINE_TOGGLE, PORTS_TOGGLE,
-    BKGRND_TOGGLE, CYCLELABELS_BTN, CYCLEHOSTLABEL_BTN,
-    CYCLEGRIDDISPLAY_BTN, RESETZOOM_BTN, EQMASTER_BTN,
-    CANCEL_TRAFFIC, ALL_TRAFFIC, QUICKHELP_BTN, BKGRND_SELECT
+    ALL_TRAFFIC,
+    BKGRND_SELECT,
+    BKGRND_TOGGLE,
+    CANCEL_TRAFFIC,
+    CYCLEGRIDDISPLAY_BTN,
+    CYCLEHOSTLABEL_BTN,
+    CYCLELABELS_BTN,
+    DETAILS_TOGGLE,
+    EQMASTER_BTN,
+    HOSTS_TOGGLE,
+    INSTANCE_TOGGLE,
+    LAYOUT_ACCESS_BTN,
+    LAYOUT_DEFAULT_BTN,
+    OFFLINE_TOGGLE,
+    PORTS_TOGGLE,
+    QUICKHELP_BTN,
+    RESETZOOM_BTN,
+    SUMMARY_TOGGLE
 } from '../panel/toolbar/toolbar.component';
 import {TrafficService} from '../traffic.service';
 import {ZoomableDirective} from '../layer/zoomable.directive';
 import {MapObject} from '../layer/maputils';
+import {LayoutService, LayoutType} from '../layout.service';
 
 const TOPO2_PREFS = 'topo2_prefs';
 const TOPO_MAPID_PREFS = 'topo_mapid';
@@ -165,6 +182,7 @@
         protected trs: TrafficService,
         protected is: IconService,
         private lion: LionService,
+        private layout: LayoutService,
         @Inject('Window') public window: any,
     ) {
         if (this.lion.ubercache.length === 0) {
@@ -199,6 +217,8 @@
         this.is.loadIconDef('groupTable');
         this.is.loadIconDef('meterTable');
         this.is.loadIconDef('triangleUp');
+        this.is.loadIconDef('m_disjointPaths');
+        this.is.loadIconDef('m_fiberSwitch');
         this.log.debug('Topology component constructed');
     }
 
@@ -350,6 +370,12 @@
             case QUICKHELP_BTN:
                 this.ks.quickHelpShown = true;
                 break;
+            case LAYOUT_DEFAULT_BTN:
+                this.layout.changeLayout(LayoutType.LAYOUT_DEFAULT);
+                break;
+            case LAYOUT_ACCESS_BTN:
+                this.layout.changeLayout(LayoutType.LAYOUT_ACCESS);
+                break;
             default:
                 this.log.warn('Unhandled Toolbar action', name);
         }
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.spec.ts
index 8b2a736..6029cca 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.spec.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.spec.ts
@@ -19,7 +19,6 @@
 import {FnService, LogService} from 'gui2-fw-lib';
 import {ActivatedRoute, Params} from '@angular/router';
 import {of} from 'rxjs';
-import {TopologyService} from './topology.service';
 
 class MockActivatedRoute extends ActivatedRoute {
     constructor(params: Params) {
@@ -56,7 +55,7 @@
         fs = new FnService(ar, logSpy, mockWindow);
 
         TestBed.configureTestingModule({
-            providers: [TopologyService,
+            providers: [TrafficService,
                 { provide: FnService, useValue: fs},
                 { provide: LogService, useValue: logSpy },
                 { provide: ActivatedRoute, useValue: ar },
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.ts
index b6d2b85..9aff0c7 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.ts
@@ -39,9 +39,7 @@
 /**
  * ONOS GUI -- Traffic Service Module.
  */
-@Injectable({
-    providedIn: 'root'
-})
+@Injectable()
 export class TrafficService {
     private handlers: string[] = [];
     private openListener: any;