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