GUI2 Added selection of background maps

Change-Id: I88ee69fe2ff24bb1b4b3fe633b04f2f1778f3a82
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.css b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.css
index f206973..0dac42a 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.css
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.css
@@ -16,4 +16,11 @@
 
 /**
  * ONOS GUI -- Topology Map Layer -- CSS file
- */
\ No newline at end of file
+ */
+/* --- Topo Map --- */
+
+path.topo-map {
+    stroke-width: 2px;
+    stroke: #f4f4f4;
+    fill: #e5e5e6;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.html
index 157a87b..adc0346 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.html
@@ -13,5 +13,11 @@
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
-<svg:text xmlns:svg="http://www.w3.org/2000/svg">Map of Croatia</svg:text>
-<svg:path xmlns:svg="http://www.w3.org/2000/svg" d="m 187.50819,219.30177 -3.14986,-1.57795 z M 45.764161,56.635237 48.914029,55.022914 Z m 62.997349,-32.313055 6.29973,4.85593 6.29974,11.31816 v 11.300949 h 9.4496 l 12.59947,8.061616 6.29973,12.880483 v 8.039036 l 6.29974,14.448536 -3.14987,9.616908 -6.29973,12.80342 v 14.37787 l 3.14986,7.97587 -9.4496,3.18799 3.14987,9.55592 6.29973,4.77344 v 7.94906 l -6.29973,1.58881 3.14987,15.86987 6.29973,6.33869 v 6.33341 l 6.29974,1.58253 v 20.54314 h 6.29973 18.89921 l 3.14986,4.73289 v 1.57699 6.30469 l -3.14986,4.72511 6.29973,6.29564 v 1.57311 l 3.14987,7.8607 -9.4496,17.26536 h -9.44961 l -6.29973,4.70203 v 14.08899 l -6.29974,3.12741 v 15.61816 l -3.14986,6.23849 -22.04907,3.11737 -3.14987,7.78797 h -6.29974 l 9.44961,17.10623 -3.14987,6.21118 -6.29974,-1.55233 v 10.85988 l -6.29973,7.74786 -18.89921,-6.19768 v -20.17635 l -6.299731,-3.10867 v -6.22107 l -6.299735,-10.8988 -9.449602,-3.11675 v -4.67746 l -9.449602,-1.55978 -6.299735,-6.24224 -9.449602,-3.123 -9.449602,-12.5046 -3.149867,-14.09183 12.599469,12.52734 3.149868,-12.52734 -9.449602,-9.40886 6.299734,1.56894 v -7.84788 l -6.299734,1.57021 -6.299735,-9.4261 3.149867,-4.71738 V 228.7627 l 3.149868,-9.46093 6.299734,-3.15624 v -31.63431 l 6.299735,-7.92914 -3.149867,-6.34927 H 52.063896 V 155.9275 l 9.449602,-9.5519 -9.449602,-7.96914 h 12.599469 v -27.15822 l -12.599469,-6.40444 -6.299735,1.60162 V 82.385126 l 3.149868,-3.213886 6.299734,1.607116 3.149868,-8.039036 -12.59947,-9.658277 6.299735,-9.6708 -3.149867,-1.613022 6.299734,-11.300949 6.299735,-4.848528 v -6.469632 l 6.299735,-3.236933 v -8.098523 l 9.449602,-4.863368 3.149867,3.2426 v 14.574161 l 6.299735,3.234816 12.59947,-3.234816 z"></svg:path>
\ No newline at end of file
+<svg:desc xmlns:svg="http://www.w3.org/2000/svg">Map of {{map.id}} in SVG format</svg:desc>
+<svg:path class="topo-map"
+        *ngFor="let f of mapPathGenerator?.geodata.features"
+        xmlns:svg="http://www.w3.org/2000/svg"
+        [attr.d]="pathGenerator(f)">
+    <svg:title>{{ f.id }}</svg:title>
+</svg:path>
+
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.spec.ts
index e1000c8..ab73820 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.spec.ts
@@ -16,25 +16,45 @@
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { MapSvgComponent } from './mapsvg.component';
+import {HttpClient} from '@angular/common/http';
+import {from} from 'rxjs';
+import {LogService} from 'gui2-fw-lib';
+
+class MockHttpClient {
+    get() {
+        return from(['{"id":"app","icon":"nav_apps","cat":"PLATFORM","label":"Applications"}']);
+    }
+
+    subscribe() {}
+}
 
 describe('MapSvgComponent', () => {
-  let component: MapSvgComponent;
-  let fixture: ComponentFixture<MapSvgComponent>;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+    let component: MapSvgComponent;
+    let fixture: ComponentFixture<MapSvgComponent>;
 
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ MapSvgComponent ]
-    })
-    .compileComponents();
-  }));
+    beforeEach(async(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
 
-  beforeEach(() => {
-    fixture = TestBed.createComponent(MapSvgComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
+        TestBed.configureTestingModule({
+            declarations: [ MapSvgComponent ],
+            providers: [
+                { provide: LogService, useValue: logSpy },
+                { provide: HttpClient, useClass: MockHttpClient },
+            ]
+        })
+        .compileComponents();
 
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
+        logServiceSpy = TestBed.get(LogService);
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(MapSvgComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
 });
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 2de6c12..a10cf4c 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
@@ -13,18 +13,206 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit } from '@angular/core';
+import {
+    Component,
+    Input,
+    OnChanges,
+    SimpleChanges
+} from '@angular/core';
+import { MapObject } from '../maputils';
+import {LogService} from 'gui2-fw-lib';
+import {HttpClient} from '@angular/common/http';
+import * as d3 from 'd3';
+import * as topojson from 'topojson-client';
+
+const BUNDLED_URL_PREFIX = 'data/map/';
+
+/**
+ * Model of the transform attribute of a topojson file
+ */
+interface TopoDataTransform {
+    scale: number[];
+    translate: number[];
+}
+
+/**
+ * Model of the Generator setting for D3 GEO
+ */
+interface GeneratorSettings {
+    objectTag: string;
+    projection: Object;
+    logicalSize: number;
+    mapFillScale: number;
+}
+
+/**
+ * Model of the Path Generator
+ */
+interface PathGenerator {
+    geodata: FeatureCollection;
+    pathgen: (Feature) => string;
+    settings: GeneratorSettings;
+}
+
+/**
+ * Model of the Feature returned prom topojson library
+ */
+interface Feature {
+    geometry: Object;
+    id: string;
+    properties: Object;
+    type: string;
+}
+
+/**
+ * Model of the Features Collection returned by the topojson.features function
+ */
+interface FeatureCollection {
+    type: string;
+    features: Feature[];
+}
+
+/**
+ * Model of the topojson file
+ */
+interface TopoData {
+    type: string; // Usually "Topology"
+    objects: Object; // Can be a list of countries or individual countries
+    arcs: number[][][]; // Coordinates
+    bbox: number[]; // Bounding box
+    transform: TopoDataTransform; // scale and translate
+}
+
+/**
+ * Default settings for the path generator for TopoJson
+ */
+const DEFAULT_GEN_SETTINGS: GeneratorSettings = <GeneratorSettings>{
+    objectTag: 'states',
+    projection: d3.geoMercator(),
+    logicalSize: 1000,
+    mapFillScale: .95,
+};
 
 @Component({
     selector: '[onos-mapsvg]',
     templateUrl: './mapsvg.component.html',
     styleUrls: ['./mapsvg.component.css']
 })
-export class MapSvgComponent implements OnInit {
+export class MapSvgComponent implements  OnChanges {
+    @Input() map: MapObject = <MapObject>{id: 'none'};
 
-    constructor() { }
+    cache = new Map<string, TopoData>();
+    topodata: TopoData;
+    mapPathGenerator: PathGenerator;
 
-    ngOnInit() {
+    constructor(
+        private log: LogService,
+        private httpClient: HttpClient,
+    ) {
+        this.log.debug('MapSvgComponent constructed');
     }
 
+    static getUrl(id: string): string {
+        if (id && id[0] === '*') {
+            return BUNDLED_URL_PREFIX + id.slice(1) + '.topojson';
+        }
+        return id + '.topojson';
+    }
+
+    ngOnChanges(changes: SimpleChanges): void {
+        this.log.debug('Change detected', changes);
+        if (changes['map']) {
+            const map: MapObject = <MapObject>(changes['map'].currentValue);
+            if (map.id) {
+                if (this.cache.get(map.id)) {
+                    this.topodata = this.cache.get(map.id);
+                } else {
+                    this.httpClient
+                        .get(MapSvgComponent.getUrl(map.filePath))
+                        .subscribe((topoData: TopoData) => {
+                            this.mapPathGenerator = this.handleTopoJson(map, topoData);
+                            this.log.debug('Path Generated for', map.id,
+                                'from', MapSvgComponent.getUrl(map.filePath));
+                        });
+                }
+            }
+        }
+    }
+
+    /**
+     * Wrapper for the path generator function
+     * @param feature The county or state within the map
+     */
+    pathGenerator(feature: Feature): string {
+        return this.mapPathGenerator.pathgen(feature);
+    }
+
+    /**
+     * Handle the topojson file stream as it arrives back from the server
+     *
+     * The topojson library converts the topojson file in to a FeatureCollection
+     * d3.geo then further converts this in to a Path
+     *
+     * @param map The Map chosen in the GUI
+     * @param topoData The data in the TopoJson file
+     */
+    handleTopoJson(map: MapObject, topoData: TopoData): PathGenerator {
+        this.topodata = topoData;
+        this.cache.set(map.id, topoData);
+        this.log.debug('Map retrieved', topoData);
+
+        const topoObject = topoData.objects[map.id];
+        const geoData: FeatureCollection = <FeatureCollection>topojson.feature(topoData, topoObject);
+        this.log.debug('Map retrieved', topoData, geoData);
+
+        const settings: GeneratorSettings = Object.assign({}, DEFAULT_GEN_SETTINGS);
+        const path = d3.geoPath().projection(settings.projection);
+        this.rescaleProjection(
+            settings.projection,
+            settings.mapFillScale,
+            settings.logicalSize,
+            path,
+            geoData);
+        this.log.debug('Scale adjusted');
+
+        return <PathGenerator>{
+            geodata: geoData,
+            pathgen: path,
+            settings: settings
+        };
+    }
+
+    /**
+     * Adjust projection scale and translation to fill the view
+     * with the map
+     * @param proj
+     * @param mfs
+     * @param dim
+     * @param path
+     * @param geoData
+     * @param adjustScale
+     */
+    rescaleProjection(proj: any, mfs: number, dim: number, path: any,
+                      geoData: FeatureCollection, adjustScale: number = 1.0) {
+        // start with unit scale, no translation..
+        proj.scale(1).translate([0, 0]);
+
+        // figure out dimensions of map data..
+        const b = path.bounds(geoData);
+        const x1 = b[0][0];
+        const y1 = b[0][1];
+        const x2 = b[1][0];
+        const y2 = b[1][1];
+        const dx = x2 - x1;
+        const dy = y2 - y1;
+        const x = (x1 + x2) / 2;
+        const y = (y1 + y2) / 2;
+
+        // size map to 95% of minimum dimension to fill space..
+        const s = (mfs / Math.min(dx / dim, dy / dim)) * adjustScale;
+        const t = [dim / 2 - s * x, dim / 2 - s * y];
+
+        // set new scale, translation on the projection..
+        proj.scale(s).translate(t);
+    }
 }