First part of migrating Topo2 to GUI2

Change-Id: I316dd34cba161688e01dfb7b340bff5f2c3c57d4
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyph.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyph.service.spec.ts
new file mode 100644
index 0000000..5b4669d
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyph.service.spec.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { GlyphService } from './glyph.service';
+import { FnService } from '../util/fn.service';
+
+class MockFnService {}
+
+/**
+ * ONOS GUI -- SVG -- Glyph Service - Unit Tests
+ */
+describe('GlyphService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [GlyphService,
+                { provide: FnService, useClass: MockFnService },
+                { provide: LogService, useValue: log },
+            ]
+        });
+    });
+
+    it('should be created', inject([GlyphService], (service: GlyphService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyphdata.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyphdata.service.spec.ts
new file mode 100644
index 0000000..ab770d5
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyphdata.service.spec.ts
@@ -0,0 +1,41 @@
+/*
+ *  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 { LogService } from '..//log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { GlyphDataService } from './glyphdata.service';
+
+/**
+ * ONOS GUI -- SVG -- Glyph Data Service - Unit Tests
+ */
+describe('GlyphDataService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [GlyphDataService,
+                { provide: LogService, useValue: log },
+            ]
+        });
+    });
+
+    it('should be created', inject([GlyphDataService], (service: GlyphDataService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.spec.ts
new file mode 100644
index 0000000..094baef
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.spec.ts
@@ -0,0 +1,50 @@
+/*
+ *  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 { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { IconService } from './icon.service';
+import { GlyphService } from './glyph.service';
+import { SvgUtilService } from './svgutil.service';
+
+class MockGlyphService {}
+
+class MockSvgUtilService {}
+
+/**
+ * ONOS GUI -- SVG -- Icon Service - Unit Tests
+ */
+describe('IconService', () => {
+
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [IconService,
+                { provide: LogService, useValue: log },
+                { provide: GlyphService, useClass: MockGlyphService },
+                { provide: SvgUtilService, useClass: MockSvgUtilService },
+            ]
+        });
+    });
+
+    it('should be created', inject([IconService], (service: IconService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.ts
index 6ced162..73480ed 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.ts
@@ -43,6 +43,7 @@
     ['m_ports', 'm_ports'],
 
     ['topo', 'topo'],
+    ['bird', 'bird'],
 
     ['refresh', 'refresh'],
     ['query', 'query'],
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon/icon.component.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon/icon.component.spec.ts
new file mode 100644
index 0000000..8234551
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon/icon.component.spec.ts
@@ -0,0 +1,30 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LogService } from '../../log.service';
+import { ConsoleLoggerService } from '../../consolelogger.service';
+import { IconComponent } from './icon.component';
+import { IconService } from '../icon.service';
+
+class MockIconService {}
+
+describe('IconComponent', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            declarations: [ IconComponent ],
+            providers: [
+                { provide: LogService, useValue: log },
+                { provide: IconService, useClass: MockIconService },
+            ]
+        });
+    });
+
+    it('should create', () => {
+        const fixture = TestBed.createComponent(IconComponent);
+        const component = fixture.componentInstance;
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.spec.ts
new file mode 100644
index 0000000..7165b33
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.spec.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { SvgUtilService } from './svgutil.service';
+import { FnService } from '../util/fn.service';
+
+class MockFnService {}
+
+/**
+ * ONOS GUI -- SVG -- Svg Util Service - Unit Tests
+ */
+describe('SvgUtilService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [SvgUtilService,
+                { provide: LogService, useValue: log },
+                { provide: FnService, useClass: MockFnService },
+            ]
+        });
+    });
+
+    it('should be created', inject([SvgUtilService], (service: SvgUtilService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.ts
index 13327fe..6107d16 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.ts
@@ -16,6 +16,7 @@
 import { Injectable } from '@angular/core';
 import { FnService } from '../util/fn.service';
 import { LogService } from '../log.service';
+import * as d3 from 'd3';
 
 /**
  * ONOS GUI -- SVG -- Util Service
@@ -26,11 +27,45 @@
     providedIn: 'root',
 })
 export class SvgUtilService {
+    lightNorm: string[];
+    lightMute: string[];
+    darkNorm: string[];
+    darkMute: string[];
+    colors: any;
 
     constructor(
         private fs: FnService,
         private log: LogService
     ) {
+
+        // --- Ordinal scales for 7 values.
+        // TODO: migrate these colors to the theme service.
+
+        // Colors per Mojo-Design's color palette.. (version one)
+        //               blue       red        dk grey    steel      lt blue    lt red     lt grey
+        // var lightNorm = ['#5b99d2', '#d05a55', '#716b6b', '#7e9aa8', '#66cef6', '#db7773', '#aeada8' ],
+        //     lightMute = ['#a8cceb', '#f1a7a7', '#b9b5b5', '#bdcdd5', '#a8e9fd', '#f8c9c9', '#d7d6d4' ],
+
+        // Colors per Mojo-Design's color palette.. (version two)
+        //               blue       lt blue    red        green      brown      teal       lime
+        this.lightNorm = ['#5b99d2', '#66cef6', '#d05a55', '#0f9d58', '#ba7941', '#3dc0bf', '#56af00'];
+        this.lightMute = ['#9ebedf', '#abdef5', '#d79a96', '#7cbe99', '#cdab8d', '#96d5d5', '#a0c96d'];
+
+        this.darkNorm = ['#5b99d2', '#66cef6', '#d05a55', '#0f9d58', '#ba7941', '#3dc0bf', '#56af00'];
+        this.darkMute = ['#9ebedf', '#abdef5', '#d79a96', '#7cbe99', '#cdab8d', '#96d5d5', '#a0c96d'];
+
+
+        this.colors = {
+            light: {
+                norm: d3.scaleOrdinal().range(this.lightNorm),
+                mute: d3.scaleOrdinal().range(this.lightMute),
+            },
+            dark: {
+                norm: d3.scaleOrdinal().range(this.darkNorm),
+                mute: d3.scaleOrdinal().range(this.darkMute),
+            },
+        };
+
         this.log.debug('SvgUtilService constructed');
     }
 
@@ -40,4 +75,91 @@
         }
         return 'translate(' + x + ',' + y + ')';
     }
+
+    scale(x, y) {
+        return 'scale(' + x + ',' + y + ')';
+    }
+
+    skewX(x) {
+        return 'skewX(' + x + ')';
+    }
+
+    rotate(deg) {
+        return 'rotate(' + deg + ')';
+    }
+
+    cat7() {
+        const tcid = 'd3utilTestCard';
+
+        function getColor(id, muted, theme) {
+            // NOTE: since we are lazily assigning domain ids, we need to
+            //       get the color from all 4 scales, to keep the domains
+            //       in sync.
+            const ln = this.colors.light.norm(id);
+            const lm = this.colors.light.mute(id);
+            const dn = this.colors.dark.norm(id);
+            const dm = this.colors.dark.mute(id);
+            if (theme === 'dark') {
+                return muted ? dm : dn;
+            } else {
+                return muted ? lm : ln;
+            }
+        }
+
+        function testCard(svg) {
+            let g = svg.select('g#' + tcid);
+            const dom = d3.range(7);
+            let k;
+            let muted;
+            let theme;
+            let what;
+
+            if (!g.empty()) {
+                g.remove();
+
+            } else {
+                g = svg.append('g')
+                    .attr('id', tcid)
+                    .attr('transform', 'scale(4)translate(20,20)');
+
+                for (k = 0; k < 4; k++) {
+                    muted = k % 2;
+                    what = muted ? ' muted' : ' normal';
+                    theme = k < 2 ? 'light' : 'dark';
+                    dom.forEach(function (id, i) {
+                        const x = i * 20;
+                        const y = k * 20;
+                        const f = getColor(id, muted, theme);
+                        g.append('circle').attr({
+                            cx: x,
+                            cy: y,
+                            r: 5,
+                            fill: f,
+                        });
+                    });
+                    g.append('rect').attr({
+                        x: 140,
+                        y: k * 20 - 5,
+                        width: 32,
+                        height: 10,
+                        rx: 2,
+                        fill: '#888',
+                    });
+                    g.append('text').text(theme + what)
+                        .attr({
+                            x: 142,
+                            y: k * 20 + 2,
+                            fill: 'white',
+                        })
+                        .style('font-size', '4pt');
+                }
+            }
+        }
+
+        return {
+            testCard: testCard,
+            getColor: getColor,
+        };
+    }
+
 }
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
new file mode 100644
index 0000000..fe25860
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.spec.ts
@@ -0,0 +1,193 @@
+/*
+ * 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
new file mode 100644
index 0000000..fdf08f9
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.ts
@@ -0,0 +1,148 @@
+/*
+ * 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);
+    }
+
+}