GUI2 added in the layout topo overlay

Change-Id: I9960f95ae726a5af9950771ed67bcfc9d172e267
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiDevice.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiDevice.java
index b0622dd..15a0519 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiDevice.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiDevice.java
@@ -45,6 +45,7 @@
         checkNotNull(device, DEVICE_CANNOT_BE_NULL);
         this.topology = topology;
         this.deviceId = device.id();
+        this.regionId = RegionId.regionId(UiRegion.NULL_NAME);
     }
 
     /**
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiHost.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiHost.java
index 5ca86e3..10dfbf1 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiHost.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiHost.java
@@ -52,6 +52,7 @@
         checkNotNull(host, HOST_CANNOT_BE_NULL);
         this.topology = topology;
         this.hostId = host.id();
+        this.regionId = RegionId.regionId(UiRegion.NULL_NAME);
     }
 
     @Override
@@ -60,6 +61,7 @@
                 .add("id", id())
                 .add("dev", locDevice)
                 .add("port", locPort)
+                .add("Region", regionId)
                 .toString();
     }
 
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java
index de4c816f..4fe24b6 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java
@@ -37,7 +37,7 @@
  */
 public class UiRegion extends UiNode {
 
-    private static final String NULL_NAME = "(root)";
+    public static final String NULL_NAME = "(root)";
     private static final String NO_NAME = "???";
     private static final String MEMO_ADDED = "added";
 
@@ -338,9 +338,11 @@
                 return isRegionRelevant(((UiRegion) event.subject()).id());
 
             case DEVICE_ADDED_OR_UPDATED:
+                final UiDevice uiDevice = (UiDevice) event.subject();
                 if (MEMO_ADDED.equalsIgnoreCase(event.memo()) &&
-                        regionId.toString().equalsIgnoreCase(
-                          ((UiDevice) event.subject()).regionId().toString())) {
+                        uiDevice.regionId() != null &&
+                        regionId.equals(
+                          ((UiDevice) event.subject()).regionId())) {
                     return true;
                 } else {
                     return isDeviceRelevant(((UiDevice) event.subject()).id());
@@ -360,7 +362,7 @@
                                 uiHost.regionId().toString())) {
                     return true;
                 } else {
-                    return isDeviceRelevant(((UiDevice) event.subject()).id());
+                    return isHostRelevant(((UiHost) event.subject()).id());
                 }
             case HOST_MOVED:
             case HOST_REMOVED:
@@ -375,6 +377,10 @@
         return deviceIds.contains(deviceId);
     }
 
+    private boolean isHostRelevant(HostId hostId) {
+        return hostIds.contains(hostId);
+    }
+
     private boolean isLinkRelevant(UiLink uiLink) {
         if (uiLink instanceof UiDeviceLink) {
             UiDeviceLink uiDeviceLink = (UiDeviceLink) uiLink;
diff --git a/web/gui2-topo-lib/package-lock.json b/web/gui2-topo-lib/package-lock.json
index 7854959..e492e32 100644
--- a/web/gui2-topo-lib/package-lock.json
+++ b/web/gui2-topo-lib/package-lock.json
@@ -4873,7 +4873,7 @@
     },
     "gui2-fw-lib": {
       "version": "file:../gui2-fw-lib/dist/gui2-fw-lib/gui2-fw-lib-2.0.0.tgz",
-      "integrity": "sha512-30VYr4XaRNZrIN7h0HhU9UhejhRVtZ498YAgTOH1W5mHrXSWaNPkmstXh5MS8or+PCtVur0/ID1wbbzwboYomQ==",
+      "integrity": "sha512-sAZOR92QXU8IIphWwT16d5T2w4ekVNvs83wPfPxRKkCDoItIywovna7tt6n//sXbF5+wjjTgIqT6zDwedm9saw==",
       "requires": {
         "tslib": "1.9.3"
       }
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;
diff --git a/web/gui2/README.md b/web/gui2/README.md
index b57cb55..512f1f0 100644
--- a/web/gui2/README.md
+++ b/web/gui2/README.md
@@ -1,6 +1,6 @@
 # ONOS GUI 2.0.0
 
-This project is based on __[Angular 6](https://angular.io/docs)__ 
+This project is based on __[Angular 7](https://angular.io/docs)__ 
 and __[ES6](http://www.ecma-international.org/ecma-262/6.0/index.html)__ (aka __ES2015__), 
 as an alternative to the 1.0.0 GUI which was based 
 off __[AngularJS 1.3.5](https://angularjs.org/)__
@@ -9,9 +9,20 @@
 
 To use this new GUI you simply have to start the GUI in a running ONOS at the __onos>__ cli:
 ```
-feature:install onos-gui2
+app activate gui2
 ```
-and the gui will be accessible at [http://localhost:8181/onos/ui2](http://localhost:8181/onos/ui2)
+and the gui will be accessible at [http://localhost:8181/onos/ui](http://localhost:8181/onos/ui)
+
+Gui2 can also be loaded every time ONOS starts by adding it to ONOS_APPS
+```
+export ONOS_APPS=${ONOS_APPS:-drivers,openflow,gui2}
+```
+
+The original legacy GUI is also loadable as an app, and is available at the same web resource onos/ui
+The legacy GUI should be disabled if GUI2 is active and vice versa
+```
+app deactivate gui
+```
 
 As usual with ONOS if you want to run it in a different language set the __ONOS_LOCALE__ environment variable
 to the locale you want before starting onos. e.g.
@@ -25,17 +36,17 @@
  (this is not optimal though since in this mode the browser side code is built in '--prod' mode
  and all debug symbols are stripped and debug statements are not logged and the code is uglified and minimized.
  It is useful for testing "prod" mode works though and saves having to set up direct development) OR
-2. use Angular 6 CLI (__ng__ command) to rebuild on the fly (must faster for development) 
+2. use Angular 7 CLI (__ng__ command) to rebuild on the fly (must faster for development) 
 
 For 1) (this needs to be updated for Bazel commands) if you change the code you can redeploy the application without restarting ONOS with (requires you to be in ~/onos directory):
 ```
-onos-buck build //web/gui2:onos-web-gui2-oar --show-output|grep /app.oar | cut -d\  -f2 | xargs onos-app localhost reinstall!
+bazel build //web/gui2:onos-web-gui2-oar && onos-app localhost reinstall! bazel-bin/web/gui2/onos-web-gui2-oar.oar
 ```
 
 For 2) it's well worth becoming familiar with Angular CLI.
-The project is created with [Angular CLI](https://github.com/angular/angular-cli) v6 to simplify development of the browser side code.
+The project is created with [Angular CLI](https://github.com/angular/angular-cli) v7 to simplify development of the browser side code.
 It is complicated to set up, but is worth the effort if you have more than a day's worth of development to do.
-This allows you to develop the Angular 6 TypeScript code independent of ONOS in a separate container. 
+This allows you to develop the Angular 7 TypeScript code independent of ONOS in a separate container. 
 Since WebSockets have been implemented - there is a requirement to run ONOS in the background.
 
 There is no need to install node, npm or ng again on your system, and indeed if they are already installed, it's best