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';