GUI2 added method to zoom to map size

Change-Id: I3aa578b78ebe2ab26f72a7535b8b5e9e0a822cb6
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.spec.ts
deleted file mode 100644
index fe25860..0000000
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.spec.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Copyright 2018-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
-*
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { TestBed, inject } from '@angular/core/testing';
-import { ActivatedRoute, Params } from '@angular/router';
-import { of } from 'rxjs';
-import * as d3 from 'd3';
-
-import { LogService } from '../log.service';
-import { FnService } from '../util/fn.service';
-
-import { ZoomService, CZ, D3S, ZoomOpts, Zoomer } from './zoom.service';
-
-class MockActivatedRoute extends ActivatedRoute {
-    constructor(params: Params) {
-        super();
-        this.queryParams = of(params);
-    }
-}
-
-
-/**
- * ONOS GUI -- SVG -- Zoom Service - Unit Tests
- */
-describe('ZoomService', () => {
-    let zs: ZoomService;
-    let ar: ActivatedRoute;
-    let fs: FnService;
-    let mockWindow: Window;
-    let logServiceSpy: jasmine.SpyObj<LogService>;
-
-    const svg = d3.select('body').append('svg').attr('id', 'mySvg');
-    const zoomLayer = svg.append('g').attr('id', 'myZoomlayer');
-
-    beforeEach(() => {
-        const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn', 'error']);
-        ar = new MockActivatedRoute({'debug': 'TestService'});
-        mockWindow = <any>{
-            innerWidth: 400,
-            innerHeight: 200,
-            navigator: {
-                userAgent: 'defaultUA'
-            }
-        };
-        fs = new FnService(ar, logSpy, mockWindow);
-
-        TestBed.configureTestingModule({
-            providers: [ ZoomService,
-                { provide: FnService, useValue: fs },
-                { provide: LogService, useValue: logSpy },
-                { provide: ActivatedRoute, useValue: ar },
-                { provide: 'Window', useFactory: (() => mockWindow ) }
-            ]
-        });
-
-        zs = TestBed.get(ZoomService);
-        logServiceSpy = TestBed.get(LogService);
-    });
-
-    it('should be created', () => {
-        expect(zs).toBeTruthy();
-    });
-
-    it('should define ZoomService', function () {
-        expect(zs).toBeDefined();
-    });
-
-    it('should define api functions', function () {
-        expect(fs.areFunctions(zs, [
-            'createZoomer',
-            'zoomed',
-            'adjustZoomLayer'
-        ])).toBeTruthy();
-    });
-
-    function verifyZoomerApi() {
-        expect(fs.areFunctions(zs.zoomer, [
-            'panZoom', 'reset', 'translate', 'scale', 'scaleExtent'
-        ])).toBeTruthy();
-    }
-
-    it('should fail gracefully with no option object', function () {
-        expect(() => zs.createZoomer(<ZoomOpts>{}))
-            .toThrow(new Error(CZ + 'No "svg" (svg tag)' + D3S));
-        expect(logServiceSpy.error)
-            .toHaveBeenCalledWith(CZ + 'No "svg" (svg tag)' + D3S);
-    });
-
-    it('should complain if we miss required options', function () {
-        expect(() => zs.createZoomer(<ZoomOpts>{svg: svg}))
-            .toThrow(new Error(CZ + 'No "zoomLayer" (g tag)' + D3S));
-        expect(logServiceSpy.error).toHaveBeenCalledWith(CZ + 'No "zoomLayer" (g tag)' + D3S);
-    });
-
-    it('should work with minimal parameters', function () {
-        const zoomer = zs.createZoomer(<ZoomOpts>{
-            svg: svg,
-            zoomLayer: zoomLayer
-        });
-        expect(logServiceSpy.error).not.toHaveBeenCalled();
-        verifyZoomerApi();
-    });
-
-    it('should start at scale 1 and translate 0,0', function () {
-        const zoomer = zs.createZoomer(<ZoomOpts>{
-            svg: svg,
-            zoomLayer: zoomLayer
-        });
-        verifyZoomerApi();
-        expect(zoomer.translate()).toEqual([0, 0]);
-        expect(zoomer.scale()).toEqual(1);
-    });
-
-    it('should allow programmatic pan/zoom', function () {
-        const zoomer: Zoomer = zs.createZoomer(<ZoomOpts>{
-            svg: svg,
-            zoomLayer: zoomLayer
-        });
-        verifyZoomerApi();
-
-        expect(zoomer.translate()).toEqual([0, 0]);
-        expect(zoomer.scale()).toEqual(1);
-
-        zoomer.panZoom([20, 30], 1);
-        expect(zoomer.translate()).toEqual([20, 30]);
-        expect(zoomer.scale()).toEqual(1);
-
-        zoomer.reset();
-        expect(zoomer.translate()).toEqual([0, 0]);
-        expect(zoomer.scale()).toEqual(1);
-
-
-    });
-
-    it('should provide default scale extent', function () {
-        const zoomer = zs.createZoomer(<ZoomOpts>{
-            svg: svg,
-            zoomLayer: zoomLayer
-        });
-        expect(zoomer.scaleExtent()).toEqual([0.05, 50]);
-    });
-
-    it('should allow us to override the minimum zoom', function () {
-        const zoomer = zs.createZoomer(<ZoomOpts>{
-            svg: svg,
-            zoomLayer: zoomLayer,
-            zoomMin: 1.23
-        });
-        expect(zoomer.scaleExtent()).toEqual([1.23, 50]);
-    });
-
-    it('should allow us to override the maximum zoom', function () {
-        const zoomer = zs.createZoomer(<ZoomOpts>{
-            svg: svg,
-            zoomLayer: zoomLayer,
-            zoomMax: 13
-        });
-        expect(zoomer.scaleExtent()).toEqual([0.05, 13]);
-    });
-
-    // TODO: test zoomed() where we fake out the d3.event.sourceEvent etc...
-    //  need to check default enabled (true) and custom enabled predicate
-    //  need to check that the callback is invoked also
-
-    it('should invoke the callback on programmatic pan/zoom', function () {
-        const foo = { cb() { return; } };
-        spyOn(foo, 'cb');
-
-        const zoomer = zs.createZoomer(<ZoomOpts>{
-            svg: svg,
-            zoomMin: 0.25,
-            zoomMax: 10,
-            zoomLayer: zoomLayer,
-            zoomEnabled: (ev) => true,
-            zoomCallback: foo.cb,
-        });
-
-        zoomer.panZoom([0, 0], 2);
-        expect(foo.cb).toHaveBeenCalled();
-    });
-});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.ts
deleted file mode 100644
index 8cbca5a..0000000
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright 2018-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { Injectable } from '@angular/core';
-import * as d3 from 'd3';
-import { LogService } from '../log.service';
-
-export interface ZoomOpts {
-    svg: any;                         // D3 selection of <svg> element
-    zoomLayer: any;                   // D3 selection of <g> element
-    zoomMin: number;                  // Min zoom level - usually 0.25
-    zoomMax: number;                  // Max zoom level - usually 10
-    zoomEnabled(ev: any): boolean;   // Function that takes event and returns boolean
-    zoomCallback(translate: number[], scale: number): void; // Function that is called on zoom
-}
-
-export interface Zoomer {
-    panZoom(translate: number[], scale: number, transition?: number): void;
-    reset(): void;
-    translate(): number[];
-    scale(): number;
-    scaleExtent(): number[];
-}
-
-export const CZ: string = 'ZoomService.createZoomer(): ';
-export const D3S: string = ' (D3 selection) property defined';
-
-/**
- * ONOS GUI -- Topology Zoom Service Module.
- */
-@Injectable({
-    providedIn: 'root',
-})
-export class ZoomService {
-    defaultSettings: ZoomOpts;
-
-    zoom: any;
-    public zoomer: Zoomer;
-    settings: ZoomOpts;
-
-    constructor(
-        protected log: LogService,
-    ) {
-        this.defaultSettings = <ZoomOpts>{
-            zoomMin: 0.05,
-            zoomMax: 50,
-            zoomEnabled: (ev) => true,
-            zoomCallback: (t, s) => { return; }
-        };
-
-        this.log.debug('ZoomService constructed');
-    }
-
-    createZoomer(opts: ZoomOpts): Zoomer {
-        this.settings = Object.assign(this.defaultSettings, opts);
-
-        if (!this.settings.svg) {
-            this.log.error(CZ + 'No "svg" (svg tag)' + D3S);
-            throw new Error(CZ + 'No "svg" (svg tag)' + D3S);
-        }
-        if (!this.settings.zoomLayer) {
-            this.log.error(CZ + 'No "zoomLayer" (g tag)' + D3S);
-            throw new Error(CZ + 'No "zoomLayer" (g tag)' + D3S);
-        }
-
-        this.zoom = d3.zoom()
-            .scaleExtent([this.settings.zoomMin, this.settings.zoomMax])
-            .extent([[0, 0], [1000, 1000]])
-            .on('zoom', () => this.zoomed);
-
-
-        this.zoomer = <Zoomer>{
-            panZoom: (translate: number[], scale: number, transition?: number) => {
-                this.settings.svg.call(this.zoom.translateBy, translate[0], translate[1]);
-                this.settings.svg.call(this.zoom.scaleTo, scale);
-                this.adjustZoomLayer(translate, scale, transition);
-            },
-
-            reset: () => {
-                this.settings.svg.call(this.zoom.translateTo, 500, 500);
-                this.settings.svg.call(this.zoom.scaleTo, 1);
-                this.adjustZoomLayer([0, 0], 1, 0);
-            },
-
-            translate: () => {
-                const trans = d3.zoomTransform(this.settings.svg.node());
-                return [trans.x, trans.y];
-            },
-
-            scale: () => {
-                const trans = d3.zoomTransform(this.settings.svg.node());
-                return trans.k;
-            },
-
-            scaleExtent: () => {
-                return this.zoom.scaleExtent();
-            },
-        };
-
-        // apply the zoom behavior to the SVG element
-/*
-        if (this.settings.svg ) {
-            this.settings.svg.call(this.zoom);
-        }
-*/
-        // Remove zoom on double click (prevents a
-        // false zoom navigating regions)
-        // this.settings.svg.on('dblclick.zoom', null);
-
-        return this.zoomer;
-    }
-
-    /**
-     * zoom events from mouse gestures...
-     */
-    zoomed() {
-        const ev = d3.event.sourceEvent;
-        if (this.settings.zoomEnabled(ev)) {
-            this.adjustZoomLayer(d3.event.translate, d3.event.scale);
-        }
-    }
-
-    /**
-     * Adjust the zoom layer
-     */
-    adjustZoomLayer(translate: number[], scale: number, transition?: any): void {
-
-        this.settings.zoomLayer.transition()
-            .duration(transition || 0)
-            .attr('transform',
-                'translate(' + translate + ') scale(' + scale + ')');
-
-        this.settings.zoomCallback(translate, scale);
-    }
-
-}
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoomutils.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoomutils.spec.ts
new file mode 100644
index 0000000..c3b8cae
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoomutils.spec.ts
@@ -0,0 +1,119 @@
+/*
+ * 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 {LocMeta, MapBounds, ZoomUtils} from './zoomutils';
+
+describe('ZoomUtils', () => {
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+
+        });
+    });
+
+    it('should be created', () => {
+        const zu = new ZoomUtils();
+        expect(zu).toBeTruthy();
+    });
+
+    it('should covert GEO origin to Canvas', () => {
+        const canvas = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+            lng: 0, lat: 0
+        });
+
+        expect(canvas).not.toBeNull();
+        expect(canvas.x).toEqual(500);
+        expect(canvas.y).toEqual(500);
+    });
+
+    it('should covert GEO positive to Canvas', () => {
+        const canvas = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+            lng: 30, lat: 30
+        });
+
+        expect(canvas).not.toBeNull();
+        expect(Math.round(canvas.x)).toEqual(667);
+        expect(canvas.y).toEqual(300);
+    });
+
+    it('should covert GEO negative to Canvas', () => {
+        const canvas = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+            lng: -30, lat: -30
+        });
+
+        expect(canvas).not.toBeNull();
+        expect(Math.round(canvas.x)).toEqual(333);
+        expect(canvas.y).toEqual(700);
+    });
+
+    it('should convert XY origin to GEO', () => {
+        const geo = ZoomUtils.convertXYtoGeo(0, 0);
+        expect(geo.equivLoc.lng).toEqual(-90);
+        expect(geo.equivLoc.lat).toEqual(75);
+    });
+
+    it('should convert XY centre to GEO', () => {
+        const geo = ZoomUtils.convertXYtoGeo(500, 500);
+        expect(geo.equivLoc.lng).toEqual(0);
+        expect(geo.equivLoc.lat).toEqual(0);
+    });
+
+    it('should convert XY 1000 to GEO', () => {
+        const geo = ZoomUtils.convertXYtoGeo(1000, 1000);
+        expect(geo.equivLoc.lng).toEqual(90);
+        expect(geo.equivLoc.lat).toEqual(-75);
+    });
+
+    it('should convert XY leftmost to GEO', () => {
+        const geo = ZoomUtils.convertXYtoGeo(-500, 500);
+        expect(geo.equivLoc.lng).toEqual(-180);
+        expect(geo.equivLoc.lat).toEqual(0);
+    });
+
+    it('should convert XY rightmost to GEO', () => {
+        const geo = ZoomUtils.convertXYtoGeo(1500, 500);
+        expect(geo.equivLoc.lng).toEqual(+180);
+        expect(geo.equivLoc.lat).toEqual(0);
+    });
+
+    it('should convert MapBounds in upper left quadrant to Zoom level', () => {
+        const zoomParams = ZoomUtils.convertBoundsToZoomLevel(
+            <MapBounds>{ latMin: 40, lngMin: -40, latMax: 50, lngMax: -30 });
+
+        expect(zoomParams.sc).toEqual(11.18);
+        expect(Math.round(zoomParams.tx)).toEqual(-2916);
+        expect(Math.round(zoomParams.ty)).toEqual(-1736);
+    });
+
+    it('should convert MapBounds in lower right quadrant to Zoom level', () => {
+        const zoomParams = ZoomUtils.convertBoundsToZoomLevel(
+            <MapBounds>{ latMin: -50, lngMin: 30, latMax: -40, lngMax: 40 });
+
+        expect(zoomParams.sc).toEqual(11.18);
+        expect(Math.round(zoomParams.tx)).toEqual(-7264);
+        expect(Math.round(zoomParams.ty)).toEqual(-8444);
+    });
+
+    it('should convert MapBounds around equator to Zoom level', () => {
+        const zoomParams = ZoomUtils.convertBoundsToZoomLevel(
+            <MapBounds>{ latMin: -10, lngMin: -10, latMax: 10, lngMax: 10 });
+
+        expect(Math.round(zoomParams.sc * 100)).toEqual(644);
+        expect(Math.round(zoomParams.tx)).toEqual(-2721);
+        expect(Math.round(zoomParams.ty)).toEqual(-2721);
+    });
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoomutils.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoomutils.ts
new file mode 100644
index 0000000..6835056
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoomutils.ts
@@ -0,0 +1,138 @@
+/*
+ * 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 {LogService} from '../log.service';
+
+const LONGITUDE_EXTENT = 180;
+const LATITUDE_EXTENT = 75;
+const GRID_EXTENT_X = 2000;
+const GRID_EXTENT_Y = 1000;
+const GRID_DIAGONAL = 2236; // 2236 is the length of the diagonal of the 2000x1000 box
+const GRID_CENTRE_X = 500;
+const GRID_CENTRE_Y = 500;
+
+
+/**
+ * A model of the map bounds bottom left to top right in lat and long
+ */
+export interface MapBounds {
+    lngMin: number;
+    latMin: number;
+    lngMax: number;
+    latMax: number;
+}
+
+/**
+ * model of the topo2CurrentRegion Loc part of the MetaUi below
+ */
+export interface LocMeta {
+    lng: number;
+    lat: number;
+}
+
+/**
+ * model of the topo2CurrentRegion MetaUi from Device below
+ */
+export interface MetaUi {
+    equivLoc: LocMeta;
+    x: number;
+    y: number;
+}
+
+/**
+ * Model of the Zoom preferences
+ */
+export interface TopoZoomPrefs {
+    tx: number;
+    ty: number;
+    sc: number;
+}
+
+/**
+ * Utility class with static functions for scaling maps
+ *
+ * This is left as a class, so that the functions are loaded only as needed
+ */
+export class ZoomUtils {
+    static convertGeoToCanvas(location: LocMeta ): MetaUi {
+        const calcX = (LONGITUDE_EXTENT + location.lng) / (LONGITUDE_EXTENT * 2) * GRID_EXTENT_X - GRID_CENTRE_X;
+        const calcY = (LATITUDE_EXTENT - location.lat) / (LATITUDE_EXTENT * 2) * GRID_EXTENT_Y;
+        return <MetaUi>{
+            x: calcX,
+            y: calcY,
+            equivLoc: {
+                lat: location.lat,
+                lng: location.lng
+            }
+        };
+    }
+
+    static convertXYtoGeo(x: number, y: number): MetaUi {
+        const calcLong: number = (x + GRID_CENTRE_X) * 2 * LONGITUDE_EXTENT / GRID_EXTENT_X - LONGITUDE_EXTENT;
+        const calcLat: number = -(y * 2 * LATITUDE_EXTENT / GRID_EXTENT_Y - LATITUDE_EXTENT);
+        return <MetaUi>{
+            x: x,
+            y: y,
+            equivLoc: <LocMeta>{
+                lat: (calcLat === -0) ? 0 : calcLat,
+                lng: calcLong
+            }
+        };
+    }
+
+    /**
+     * This converts the bounds of a map loaded from a TopoGson file that has been
+     * converted in to a GEOJson format by d3
+     *
+     * The bounds are in latitude and longitude from bottom left (min) to top right (max)
+     *
+     * First they are converted in to SVG viewbox coordinates 0,0 top left 1000x1000
+     *
+     * The the zoom level is calculated by scaling to the grid diagonal
+     *
+     * Finally the translation is calculated by applying the zoom first, and then
+     * translating on the zoomed coordinate system
+     * @param mapBounds - the bounding box of the chosen map in lat and long
+     * @param log The LogService
+     */
+static convertBoundsToZoomLevel(mapBounds: MapBounds, log?: LogService): TopoZoomPrefs {
+
+        const min: MetaUi = this.convertGeoToCanvas(<LocMeta>{
+            lng: mapBounds.lngMin,
+            lat: mapBounds.latMin
+        });
+
+        const max: MetaUi = this.convertGeoToCanvas(<LocMeta>{
+            lng: mapBounds.lngMax,
+            lat: mapBounds.latMax
+        });
+
+        const diagonal = Math.sqrt(Math.pow(max.x - min.x, 2) + Math.pow(max.y - min.y, 2));
+        const centreX = (max.x - min.x) / 2 + min.x;
+        const centreY = (max.y - min.y) / 2 + min.y;
+        // Zoom works from the top left of the 1000x1000 viewbox
+        // The scale is applied first and then the translate is on the scaled coordinates
+        const zoomscale = 0.5 * GRID_DIAGONAL / ((diagonal < 100) ? 100 : diagonal); // Don't divide by zero
+        const zoomx = -centreX * zoomscale + GRID_CENTRE_X;
+        const zoomy = -centreY * zoomscale + GRID_CENTRE_Y;
+
+        // log.debug('MapBounds', mapBounds, 'XYMin', min, 'XYMax', max, 'Diag', diagonal,
+        //     'Centre', centreX, centreY, 'translate', zoomx, zoomy, 'Scale', zoomscale);
+
+        return <TopoZoomPrefs>{tx: zoomx, ty: zoomy, sc: zoomscale};
+    }
+}
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/public_api.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/public_api.ts
index 3c6d141..fd1e9c1 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/public_api.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/public_api.ts
@@ -36,7 +36,7 @@
 export * from './lib/svg/svgutil.service';
 export * from './lib/svg/glyphdata.service';
 export * from './lib/svg/glyph.service';
-export * from './lib/svg/zoom.service';
+export * from './lib/svg/zoomutils';
 
 export * from './lib/util/prefs.service';
 export * from './lib/util/fn.service';
diff --git a/web/gui2/package-lock.json b/web/gui2/package-lock.json
index 763b0c4..846cc0f 100644
--- a/web/gui2/package-lock.json
+++ b/web/gui2/package-lock.json
@@ -5560,7 +5560,7 @@
     },
     "gui2-fw-lib": {
       "version": "file:../gui2-fw-lib/dist/gui2-fw-lib/gui2-fw-lib-2.0.0.tgz",
-      "integrity": "sha512-+ueL0HHFkVkLJYVDNVFu2sZ19uSYT/dSOTyN0faPh6Xwdlsfz58JLSdL5L8wXMjI2QYPg8Ff5hjBhjtO7cJ7kA==",
+      "integrity": "sha512-3UMUj7LSENJ75R5yVAZM/KmvZAA/fCG+qJl7/1VnvI/0wJGjVPp4eTEHDH2hMOkJ/yw8b0VgQARdZR49zEc2iQ==",
       "requires": {
         "tslib": "1.9.3"
       }
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.html
index b6290b6..9f8faf8 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.html
@@ -25,5 +25,5 @@
      countries in the northern and southern extremities, and so
      the map is squashed horizontally slightly here to compensate
      (with no squashing the width would be 2400)-->
-<svg:g xmlns:svg="http://www.w3.org/2000/svg" onos-mapsvg [map]="map"
+<svg:g xmlns:svg="http://www.w3.org/2000/svg" onos-mapsvg [map]="map" (mapBounds)="updatedBounds($event)"
        transform="translate(500,500), scale(5.5555,6.666666)"/>
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.spec.ts
index 0732018..7fb0b3f 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.spec.ts
@@ -19,9 +19,8 @@
 import {MapSvgComponent} from '../mapsvg/mapsvg.component';
 import {from} from 'rxjs';
 import {HttpClient} from '@angular/common/http';
-import {LogService} from 'gui2-fw-lib';
+import {LocMeta, LogService, ZoomUtils} from 'gui2-fw-lib';
 import {MapObject} from '../maputils';
-import {LocMeta} from '../forcesvg/models';
 import {ForceSvgComponent} from '../forcesvg/forcesvg.component';
 import {
     DeviceNodeSvgComponent,
@@ -83,31 +82,8 @@
     });
 
     it('should convert latlong to xy', () => {
-        const result = BackgroundSvgComponent.convertGeoToCanvas(<LocMeta>{lat: 52, lng: -8});
+        const result = ZoomUtils.convertGeoToCanvas(<LocMeta>{lat: 52, lng: -8});
         expect(Math.round(result.x * 100)).toEqual(45556);
         expect(Math.round(result.y * 100)).toEqual(15333);
     });
-
-    /**
-     * For some reason including the following causes "ForceSvgComponent should create error
-     * TODO: Investigate
-     */
-
-    // it('should convert xy random extents to latlong', () => {
-    //     const result = BackgroundSvgComponent.convertXYtoGeo(455.556, 153.33);
-    //     expect(Math.round(result.equivLoc.lng)).toEqual(-8);
-    //     expect(Math.round(result.equivLoc.lat)).toEqual(52);
-    // });
-
-    // it('should convert xy min extents to latlong', () => {
-    //     const result = BackgroundSvgComponent.convertXYtoGeo(-500, 0);
-    //     expect(Math.round(result.equivLoc.lng)).toEqual(-180);
-    //     expect(Math.round(result.equivLoc.lat)).toEqual(75);
-    // });
-
-    // it('should convert xy full extents to latlong', () => {
-    //     const result = BackgroundSvgComponent.convertXYtoGeo(1500, 1000);
-    //     expect(Math.round(result.equivLoc.lng)).toEqual(180);
-    //     expect(Math.round(result.equivLoc.lat)).toEqual(-75);
-    // });
 });
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.ts
index cc41fb6..daa4fcb 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.ts
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {Component, Input, OnInit} from '@angular/core';
+import {Component, EventEmitter, Input, Output} from '@angular/core';
 import {MapObject} from '../maputils';
-import {LocMeta, MetaUi} from '../forcesvg/models';
+import {MapBounds, TopoZoomPrefs, LogService, ZoomUtils} from 'gui2-fw-lib';
 
 /**
  * model of the topo2CurrentLayout attrs from BgZoom below
@@ -69,53 +69,39 @@
     regionName: string;
 }
 
-const LONGITUDE_EXTENT = 180;
-const LATITUDE_EXTENT = 75;
-
 /**
  * ONOS GUI -- Topology Background Layer View.
+ *
+ * TODO: consider that this layer has only one component the MapSvg and hence
+ * might be able to be eliminated
  */
 @Component({
     selector: '[onos-backgroundsvg]',
     templateUrl: './backgroundsvg.component.html',
     styleUrls: ['./backgroundsvg.component.css']
 })
-export class BackgroundSvgComponent implements OnInit {
+export class BackgroundSvgComponent {
     @Input() map: MapObject;
+    @Output() zoomlevel = new EventEmitter<TopoZoomPrefs>();
 
     layoutData: Layout = <Layout>{};
 
-    static convertGeoToCanvas(location: LocMeta): MetaUi {
-        const calcX = (LONGITUDE_EXTENT + location.lng) / ( LONGITUDE_EXTENT * 2 ) * 2000 - 500;
-        const calcY = (LATITUDE_EXTENT - location.lat) / ( LATITUDE_EXTENT * 2 ) * 1000;
-        return <MetaUi>{
-            x: calcX,
-            y: calcY,
-            equivLoc: {
-                lat: location.lat,
-                lng: location.lng
-            }
-        };
+    constructor(
+        private log: LogService
+    ) {
+        this.log.debug('BackgroundSvg constructed');
     }
 
-    static convertXYtoGeo(x: number, y: number): MetaUi {
-        const calcLong: number = (x + 500) * 2 * LONGITUDE_EXTENT / 2000 - LONGITUDE_EXTENT;
-        const calcLat: number = -(y * 2 * LATITUDE_EXTENT / 1000 - LATITUDE_EXTENT);
-        return <MetaUi>{
-            x: x,
-            y: y,
-            equivLoc: <LocMeta>{
-                lat: calcLat,
-                lng: calcLong
-            }
-        };
+    /**
+     * Called when ever the mapBounds event is raised by the MapSvgComponent
+     *
+     * @param bounds - the bounds of the newly loaded map in terms of Lat and Long
+     */
+    updatedBounds(bounds: MapBounds): void {
+        const zoomPrefs: TopoZoomPrefs =
+            ZoomUtils.convertBoundsToZoomLevel(bounds, this.log);
+
+        this.zoomlevel.emit(zoomPrefs);
     }
 
-    constructor() { }
-
-    ngOnInit() {
-    }
-
-
-
 }
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/draggable/draggable.directive.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/draggable/draggable.directive.ts
index ef99728..474d7a8 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/draggable/draggable.directive.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/draggable/draggable.directive.ts
@@ -20,9 +20,9 @@
     Input,
     OnChanges, Output
 } from '@angular/core';
-import {ForceDirectedGraph, LocMeta, MetaUi, Node} from '../models';
+import {ForceDirectedGraph, Node} from '../models';
 import * as d3 from 'd3';
-import {LogService} from 'gui2-fw-lib';
+import {LogService, MetaUi, ZoomUtils} from 'gui2-fw-lib';
 import {BackgroundSvgComponent} from '../../backgroundsvg/backgroundsvg.component';
 
 @Directive({
@@ -73,7 +73,7 @@
                 if (!d3.event.active) {
                     graph.simulation.alphaTarget(0);
                 }
-                newLocation.emit(BackgroundSvgComponent.convertXYtoGeo(node.fx, node.fy));
+                newLocation.emit(ZoomUtils.convertXYtoGeo(node.fx, node.fy));
 
                 // node.fx = null;
                 // node.fy = null;
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html
index 6bbf85b..b673b24 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html
@@ -23,11 +23,18 @@
          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)">
+           (selectedEvent)="updateSelected($event)"
+           [scale]="scale">
     </svg:g>
 </svg:g>
 <svg:g xmlns:svg="http://www.w3.org/2000/svg" class="topo2-nodes">
@@ -43,15 +50,20 @@
          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
+        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">
+            [labelToggle]="deviceLabelToggle"
+            [scale]="scale">
         <svg:desc>Device nodes</svg:desc>
     </svg:g>
     <!-- Template explanation - only display the hosts if 'showHosts' is set true -->
@@ -69,13 +81,18 @@
              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', device.id, $event)"
                (selectedEvent)="updateSelected($event)"
-               [labelToggle]="hostLabelToggle">
+               [labelToggle]="hostLabelToggle"
+               [scale]="scale">
             <svg:desc>Host nodes</svg:desc>
         </svg:g>
     </svg:g>
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
index 7b66f36..ebc70aa 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
@@ -28,7 +28,13 @@
     SimpleChanges,
     ViewChildren
 } from '@angular/core';
-import {LogService, WebSocketService} from 'gui2-fw-lib';
+import {
+    LocMeta,
+    LogService,
+    MetaUi,
+    WebSocketService,
+    ZoomUtils
+} from 'gui2-fw-lib';
 import {
     Device,
     ForceDirectedGraph,
@@ -38,8 +44,7 @@
     LayerType,
     Link,
     LinkHighlight,
-    Location, LocMeta,
-    MetaUi,
+    Location,
     ModelEventMemo,
     ModelEventType,
     Region,
@@ -83,6 +88,7 @@
     @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>();
@@ -192,7 +198,7 @@
                 const loc: Location = <Location>n['location'];
                 if (loc && loc.locType === LocationType.GEO) {
                     const position: MetaUi =
-                        BackgroundSvgComponent.convertGeoToCanvas(
+                        ZoomUtils.convertGeoToCanvas(
                             <LocMeta>{lng: loc.longOrX, lat: loc.latOrY});
                     n.fx = position.x;
                     n.fy = position.y;
@@ -438,5 +444,11 @@
         });
         this.log.debug(klass, id, 'has been moved to', newLocation);
     }
+
+    resetNodeLocations() {
+        this.devices.forEach((d) => {
+            d.resetNodeLocation();
+        });
+    }
 }
 
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts
index 5f8d8c6..ce5441f 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts
@@ -15,12 +15,9 @@
  */
 import * as d3 from 'd3';
 import {LocationType} from '../../backgroundsvg/backgroundsvg.component';
-import {
-    LayerType,
-    Location,
-    NodeType,
-    RegionProps
-} from './regions';
+import {LayerType, Location, NodeType, RegionProps} from './regions';
+import {LocMeta, LogService, MetaUi} from 'gui2-fw-lib';
+import {ZoomUtils} from 'gui2-fw-lib';
 
 export interface UiElement {
     index?: number;
@@ -116,23 +113,6 @@
     uiType: string;
 }
 
-/**
- * model of the topo2CurrentRegion Loc part of the MetaUi below
- */
-export interface LocMeta {
-    lng: number;
-    lat: number;
-}
-
-/**
- * model of the topo2CurrentRegion MetaUi from Device below
- */
-export interface MetaUi {
-    equivLoc: LocMeta;
-    x: number;
-    y: number;
-}
-
 export interface HostProps {
     gridX: number;
     gridY: number;
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css
index 57a2bd3..204b85c 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css
@@ -37,7 +37,7 @@
 }
 g.node.device.online use {
     /* NOTE: this gets overridden programatically */
-    fill: #454545;
+    fill: #ffffff;
 }
 
 g.node.selected .node-container {
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
index 747b1bb..399955c 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
@@ -15,6 +15,7 @@
 -->
 <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
@@ -29,6 +30,7 @@
             <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;" />
@@ -36,6 +38,7 @@
 </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.
@@ -80,7 +83,6 @@
             text-anchor="start" y="0.3em" x="22"
             [attr.textLength]= "labelTextLen()"
             lengthAdjust= "spacing"
-            [ngStyle]="{'transform': 'scale(' + scale + ')'}"
             [@deviceLabelToggleTxt]="labelToggle">
         {{ labelToggle == 0 ? '': labelToggle == 1 ? device.id:device.props.name }}
     </svg:text>
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
index 1cfe7cf..87b7e0a 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
@@ -23,9 +23,10 @@
     SimpleChanges,
 } from '@angular/core';
 import {Device, LabelToggle, UiElement} from '../../models';
-import {IconService, LogService} from 'gui2-fw-lib';
+import {IconService, LocMeta, LogService, MetaUi, ZoomUtils} from 'gui2-fw-lib';
 import {NodeVisual} from '../nodevisual';
 import {animate, state, style, transition, trigger} from '@angular/animations';
+import {LocationType} from '../../../backgroundsvg/backgroundsvg.component';
 
 /**
  * The Device node in the force graph
@@ -113,9 +114,10 @@
      */
     labelTextLen() {
         if (this.labelToggle === 1) {
-            return this.device.id.length * 8 * this.scale;
-        } else if (this.labelToggle === 2 && this.device && this.device.props.name && this.device.props.name.trim().length > 0) {
-            return this.device.props.name.length * 8 * this.scale;
+            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;
         }
@@ -129,4 +131,25 @@
             return 'm_' + this.device.type;
         }
     }
+
+    resetNodeLocation(): void {
+        this.log.debug('Resetting device', this.device.id, this.device.type);
+        let origLoc: MetaUi;
+
+        if (!this.device.location || this.device.location.locType === LocationType.NONE) {
+            // No location - nothing to do
+            return;
+        } else if (this.device.location.locType === LocationType.GEO) {
+            origLoc = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+                lng: this.device.location.longOrX,
+                lat: this.device.location.latOrY
+            });
+        } else if (this.device.location.locType === LocationType.GRID) {
+            origLoc = ZoomUtils.convertXYtoGeo(
+                this.device.location.longOrX, this.device.location.latOrY);
+        }
+        this.device.metaUi = origLoc;
+        this.device['fx'] = origLoc.x;
+        this.device['fy'] = origLoc.y;
+    }
 }
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html
index 5bab75d..ccce65a 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html
@@ -55,7 +55,8 @@
         filter="url(#drop-shadow-host)"
         style="fill: url(#three_stops_radial)">
     </svg:circle>
-    <svg:use xlink:href="#m_endstation" width="22.5" height="22.5" x="-11.25" y="-11.25" style="transform: scale(1);"></svg:use>
+    <svg: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
@@ -65,5 +66,5 @@
     <svg:text
         *ngIf="labelToggle != 0"
         dy="30" text-anchor="middle"
-        style="transform: scale(1);">{{hostName()}}</svg:text>
+        >{{hostName()}}</svg:text>
 </svg:g>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.html
index b3a5557..c856763 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.html
@@ -27,6 +27,7 @@
     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
@@ -37,12 +38,16 @@
         [attr.x1]="link.source?.x" [attr.y1]="link.source?.y"
         [attr.x2]="link.target?.x" [attr.y2]="link.target?.y"
         [ngClass]="['link', selected?'selected':'', enhanced?'enhanced':'', highlighted]"
+        [ngStyle]="{'stroke-width': (enhanced ? 4 : 2) * scale + 'px'}"
         (click)="toggleSelected(link)"
         (mouseover)="enhance()"
         [attr.filter]="highlighted?'url(#glow)':'none'">
 </svg:line>
-<svg:g xmlns:svg="http://www.w3.org/2000/svg" [ngClass]="['linkLabel']">
-    <!-- Template explanation: Creates SVG Text and in
+<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
@@ -61,13 +66,14 @@
 -->
 <svg:g xmlns:svg="http://www.w3.org/2000/svg"
        *ngIf="enhanced && link.portA"
-       class="portLabel">
+       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]="labelPosSrc.x - 2 - textLength(link.portA)/2" [attr.y]="labelPosSrc.y - 8"
+            [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
@@ -75,23 +81,20 @@
         line 2) centre aligns it
         line 3) ensures that the text fills the rectangle by adjusting spacing
     -->
-    <svg:text
-            [attr.x]="labelPosSrc.x" [attr.y]="labelPosSrc.y + 6"
-            text-anchor="middle"
+    <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">
+       class="portLabel"
+       [attr.transform]="'translate(' + labelPosTgt.x + ',' + labelPosTgt.y + '),scale(' + scale + ')'">
     <svg:rect
-            [attr.x]="labelPosTgt.x - 2 - textLength(link.portB)/2" [attr.y]="labelPosTgt.y - 8"
+            [attr.x]="2 - textLength(link.portB)/2" y="-8"
             [attr.width]="4 + textLength(link.portB)" height="16">
     </svg:rect>
-    <svg:text
-            [attr.x]="labelPosTgt.x" [attr.y]="labelPosTgt.y + 6"
-            text-anchor="middle"
+    <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/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.ts
index af4d0a9..eab533f 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/visuals/linksvg/linksvg.component.ts
@@ -28,11 +28,6 @@
     y: number;
 }
 
-enum LinkEnd {
-    A,
-    B
-}
-
 @Component({
     selector: '[onos-linksvg]',
     templateUrl: './linksvg.component.html',
@@ -55,6 +50,7 @@
     @Input() highlighted: string = '';
     @Input() highlightsEnabled: boolean = true;
     @Input() label: string;
+    @Input() scale = 1.0;
     isHighlighted: boolean = false;
     @Output() selectedEvent = new EventEmitter<UiElement>();
     @Output() enhancedEvent = new EventEmitter<Link>();
@@ -100,7 +96,7 @@
     }
 
     /**
-     * We want to place the label for the port about 40 px from the node
+     * 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
      */
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.ts
index 23dbeb1..679bfd4 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.ts
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 import {
-    Component,
+    Component, EventEmitter,
     Input,
-    OnChanges,
+    OnChanges, Output,
     SimpleChanges
 } from '@angular/core';
 import { MapObject } from '../maputils';
-import {LogService} from 'gui2-fw-lib';
+import {LogService, MapBounds} from 'gui2-fw-lib';
 import {HttpClient} from '@angular/common/http';
 import * as d3 from 'd3';
 import * as topojson from 'topojson-client';
@@ -36,16 +36,6 @@
 }
 
 /**
- * Model of the Generator setting for D3 GEO
- */
-interface GeneratorSettings {
-    objectTag: string;
-    projection: Object;
-    logicalSize: number;
-    mapFillScale: number;
-}
-
-/**
  * Model of the Feature returned prom topojson library
  */
 interface Feature {
@@ -81,9 +71,10 @@
 })
 export class MapSvgComponent implements  OnChanges {
     @Input() map: MapObject = <MapObject>{id: 'none'};
+    @Output() mapBounds = new EventEmitter<MapBounds>();
 
     geodata: FeatureCollection;
-    pathgen: (Feature) => string;
+    pathgen: any; // (Feature) => string; have to leave it general, as there is the bounds method used below
     // testPath: string;
     // testFeature = <Feature>{
     //     id: 'test',
@@ -102,13 +93,14 @@
         private log: LogService,
         private httpClient: HttpClient,
     ) {
+        // Scale everything to 360 degrees wide and 150 high
+        // See background.component.html for more details
         this.pathgen = d3.geoPath().projection(
-            MapSvgComponent.scale(1, 360, 150));
+            MapSvgComponent.scale());
 
         // this.log.debug('Feature Test',this.testFeature);
         // this.testPath = this.pathgen(this.testFeature);
         // this.log.debug('Feature Path', this.testPath);
-
         this.log.debug('MapSvgComponent constructed');
     }
 
@@ -119,11 +111,10 @@
         return id + '.topojson';
     }
 
-    static scale (scaleFactor: number, width: number, height: number) {
+    static scale () {
         return d3.geoTransform({
             point: function(x, y) {
-                this.stream.point( (x - width / 2) * scaleFactor + width / 2,
-                    (-y - height / 2) * scaleFactor + height / 2);
+                this.stream.point(x, -y); // 1-1 mapping but invert y-axis
             }
         });
     }
@@ -170,6 +161,13 @@
         }
         this.log.debug('Topo obj', topoObject, 'topodata', topoData);
         this.geodata = <FeatureCollection>topojson.feature(topoData, topoObject);
+        const bounds = this.pathgen.bounds(this.geodata);
+        this.mapBounds.emit(<MapBounds>{
+            lngMin: bounds[0][0],
+            latMin: -bounds[0][1], // Y was inverted in the transform
+            lngMax: bounds[1][0],
+            latMax: -bounds[1][1] // Y was inverted in the transform
+        });
         this.log.debug('Map retrieved', topoData, this.geodata);
 
     }
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/zoomable.directive.ts b/web/gui2/src/main/webapp/app/view/topology/layer/zoomable.directive.ts
index 0484384..2020a42 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/zoomable.directive.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/zoomable.directive.ts
@@ -21,19 +21,9 @@
     OnInit,
     SimpleChanges
 } from '@angular/core';
-import {LogService, PrefsService} from 'gui2-fw-lib';
+import {LogService, PrefsService, TopoZoomPrefs} from 'gui2-fw-lib';
 import * as d3 from 'd3';
 
-
-/**
- * Model of the Zoom preferences
- */
-export interface TopoZoomPrefs {
-    tx: number;
-    ty: number;
-    sc: number;
-}
-
 const TOPO_ZOOM_PREFS = 'topo_zoom';
 
 const ZOOM_PREFS_DEFAULT: TopoZoomPrefs = <TopoZoomPrefs>{
@@ -53,6 +43,7 @@
     @Input() zoomableOf: ElementRef;
 
     zoom: any; // The d3 zoom behaviour
+    zoomCached: TopoZoomPrefs = <TopoZoomPrefs>{tx: 0, ty: 0, sc: 1.0};
 
     constructor(
         private _element: ElementRef,
@@ -64,34 +55,31 @@
         const zoomed = () => {
             const transform = d3.event.transform;
             container.attr('transform', 'translate(' + transform.x + ',' + transform.y + ') scale(' + transform.k + ')');
-            this.updateZoomState(transform.x, transform.y, transform.k);
+            this.updateZoomState(<TopoZoomPrefs>{tx: transform.x, ty: transform.y, sc: transform.k});
         };
 
         this.zoom = d3.zoom().on('zoom', zoomed);
     }
 
     ngOnInit() {
-        const zoomState: TopoZoomPrefs = this.ps.getPrefs(TOPO_ZOOM_PREFS, ZOOM_PREFS_DEFAULT);
+        this.zoomCached = this.ps.getPrefs(TOPO_ZOOM_PREFS, ZOOM_PREFS_DEFAULT);
         const svg = d3.select(this.zoomableOf);
 
         svg.call(this.zoom);
 
         svg.transition().call(this.zoom.transform,
-            d3.zoomIdentity.translate(zoomState.tx, zoomState.ty).scale(zoomState.sc));
+            d3.zoomIdentity.translate(this.zoomCached.tx, this.zoomCached.ty).scale(this.zoomCached.sc));
         this.log.debug('Loaded topo_zoom_prefs',
-            zoomState.tx, zoomState.ty, zoomState.sc);
+            this.zoomCached.tx, this.zoomCached.ty, this.zoomCached.sc);
 
     }
 
     /**
      * Updates the cache of zoom preferences locally and onwards to the PrefsService
      */
-    updateZoomState(x: number, y: number, scale: number): void {
-        this.ps.setPrefs(TOPO_ZOOM_PREFS, <TopoZoomPrefs>{
-            tx: x,
-            ty: y,
-            sc: scale
-        });
+    updateZoomState(zoomPrefs: TopoZoomPrefs): void {
+        this.zoomCached = zoomPrefs;
+        this.ps.setPrefs(TOPO_ZOOM_PREFS, zoomPrefs);
     }
 
     /**
@@ -113,8 +101,21 @@
     resetZoom(): void {
         const svg = d3.select(this.zoomableOf);
         svg.transition().duration(750).call(this.zoom.transform, d3.zoomIdentity);
-        this.updateZoomState(0, 0, 1.0);
+        this.updateZoomState(ZOOM_PREFS_DEFAULT);
         this.log.debug('Pan to 0,0 and zoom to 1.0');
     }
 
+    /**
+     * Change the zoom level when a map is chosen in Topology view
+     *
+     * Animated to run over 750ms
+     */
+    changeZoomLevel(zoomState: TopoZoomPrefs): void {
+        const svg = d3.select(this.zoomableOf);
+        svg.transition().duration(750).call(this.zoom.transform,
+            d3.zoomIdentity.translate(zoomState.tx, zoomState.ty).scale(zoomState.sc));
+        this.updateZoomState(zoomState);
+        this.log.debug('Pan to', zoomState.tx, zoomState.ty, 'and zoom to', zoomState.sc);
+    }
+
 }
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
index b6f511a..8644ac1 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
@@ -72,7 +72,8 @@
                    [invertVertical]="true" [fit]="'fit1000high'" [aspectRatio]="0.83333"
                    [gridcolor]="'#bfe7fb'">
             </svg:g>
-            <svg:g *ngIf="prefsState.bg" onos-backgroundsvg [map]="mapIdState">
+            <svg:g *ngIf="prefsState.bg"
+                   onos-backgroundsvg [map]="mapIdState" (zoomlevel)="mapExtentsZoom($event)">
                 <svg:desc>The Background SVG component - contains maps</svg:desc>
             </svg:g>
             <svg:g #force onos-forcesvg
@@ -80,6 +81,7 @@
                    [hostLabelToggle]="prefsState.hlbls"
                    [showHosts]="prefsState.hosts"
                    [highlightPorts]="prefsState.porthl"
+                   [scale]="1 / (2 * zoomDirective.zoomCached.sc)"
                    (selectedNodeEvent)="nodeSelected($event)">
                 <svg:desc>The Force SVG component - contains all the devices, hosts and links</svg:desc>
             </svg:g>
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
index 2de3c8b..441bbc6 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
@@ -28,6 +28,7 @@
     PrefsService,
     SvgUtilService,
     WebSocketService,
+    TopoZoomPrefs
 } from 'gui2-fw-lib';
 import {InstanceComponent} from '../panel/instance/instance.component';
 import {DetailsComponent} from '../panel/details/details.component';
@@ -552,6 +553,7 @@
 
     protected resetNodeLocation() {
         // TODO: Implement reset locations
+        this.force.resetNodeLocations();
         this.flashMsg = this.lionFn('fl_reset_node_locations');
         this.log.debug('resetting node location');
     }
@@ -624,6 +626,12 @@
         this.log.debug('Map has been changed to ', map);
     }
 
+    mapExtentsZoom(zoomMapExtents: TopoZoomPrefs) {
+        // this.zoomDirective.updateZoomState(zoomPrefs.tx, zoomPrefs.ty, zoomPrefs.sc);
+        this.zoomDirective.changeZoomLevel(zoomMapExtents);
+        this.log.debug('Map zoom prefs updated', zoomMapExtents);
+    }
+
     /**
      * Read the LION bundle for Toolbar and set up the lionFn
      */