blob: a10cf4ccbc62d7be72c0a8c7d95dc200a03040ac [file] [log] [blame]
Sean Condonf4f54a12018-10-10 23:25:46 +01001/*
2 * Copyright 2018-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the 'License');
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an 'AS IS' BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
Sean Condon0d064ec2019-02-04 21:53:53 +000016import {
17 Component,
18 Input,
19 OnChanges,
20 SimpleChanges
21} from '@angular/core';
22import { MapObject } from '../maputils';
23import {LogService} from 'gui2-fw-lib';
24import {HttpClient} from '@angular/common/http';
25import * as d3 from 'd3';
26import * as topojson from 'topojson-client';
27
28const BUNDLED_URL_PREFIX = 'data/map/';
29
30/**
31 * Model of the transform attribute of a topojson file
32 */
33interface TopoDataTransform {
34 scale: number[];
35 translate: number[];
36}
37
38/**
39 * Model of the Generator setting for D3 GEO
40 */
41interface GeneratorSettings {
42 objectTag: string;
43 projection: Object;
44 logicalSize: number;
45 mapFillScale: number;
46}
47
48/**
49 * Model of the Path Generator
50 */
51interface PathGenerator {
52 geodata: FeatureCollection;
53 pathgen: (Feature) => string;
54 settings: GeneratorSettings;
55}
56
57/**
58 * Model of the Feature returned prom topojson library
59 */
60interface Feature {
61 geometry: Object;
62 id: string;
63 properties: Object;
64 type: string;
65}
66
67/**
68 * Model of the Features Collection returned by the topojson.features function
69 */
70interface FeatureCollection {
71 type: string;
72 features: Feature[];
73}
74
75/**
76 * Model of the topojson file
77 */
78interface TopoData {
79 type: string; // Usually "Topology"
80 objects: Object; // Can be a list of countries or individual countries
81 arcs: number[][][]; // Coordinates
82 bbox: number[]; // Bounding box
83 transform: TopoDataTransform; // scale and translate
84}
85
86/**
87 * Default settings for the path generator for TopoJson
88 */
89const DEFAULT_GEN_SETTINGS: GeneratorSettings = <GeneratorSettings>{
90 objectTag: 'states',
91 projection: d3.geoMercator(),
92 logicalSize: 1000,
93 mapFillScale: .95,
94};
Sean Condonf4f54a12018-10-10 23:25:46 +010095
96@Component({
97 selector: '[onos-mapsvg]',
98 templateUrl: './mapsvg.component.html',
99 styleUrls: ['./mapsvg.component.css']
100})
Sean Condon0d064ec2019-02-04 21:53:53 +0000101export class MapSvgComponent implements OnChanges {
102 @Input() map: MapObject = <MapObject>{id: 'none'};
Sean Condonf4f54a12018-10-10 23:25:46 +0100103
Sean Condon0d064ec2019-02-04 21:53:53 +0000104 cache = new Map<string, TopoData>();
105 topodata: TopoData;
106 mapPathGenerator: PathGenerator;
Sean Condonf4f54a12018-10-10 23:25:46 +0100107
Sean Condon0d064ec2019-02-04 21:53:53 +0000108 constructor(
109 private log: LogService,
110 private httpClient: HttpClient,
111 ) {
112 this.log.debug('MapSvgComponent constructed');
Sean Condonf4f54a12018-10-10 23:25:46 +0100113 }
114
Sean Condon0d064ec2019-02-04 21:53:53 +0000115 static getUrl(id: string): string {
116 if (id && id[0] === '*') {
117 return BUNDLED_URL_PREFIX + id.slice(1) + '.topojson';
118 }
119 return id + '.topojson';
120 }
121
122 ngOnChanges(changes: SimpleChanges): void {
123 this.log.debug('Change detected', changes);
124 if (changes['map']) {
125 const map: MapObject = <MapObject>(changes['map'].currentValue);
126 if (map.id) {
127 if (this.cache.get(map.id)) {
128 this.topodata = this.cache.get(map.id);
129 } else {
130 this.httpClient
131 .get(MapSvgComponent.getUrl(map.filePath))
132 .subscribe((topoData: TopoData) => {
133 this.mapPathGenerator = this.handleTopoJson(map, topoData);
134 this.log.debug('Path Generated for', map.id,
135 'from', MapSvgComponent.getUrl(map.filePath));
136 });
137 }
138 }
139 }
140 }
141
142 /**
143 * Wrapper for the path generator function
144 * @param feature The county or state within the map
145 */
146 pathGenerator(feature: Feature): string {
147 return this.mapPathGenerator.pathgen(feature);
148 }
149
150 /**
151 * Handle the topojson file stream as it arrives back from the server
152 *
153 * The topojson library converts the topojson file in to a FeatureCollection
154 * d3.geo then further converts this in to a Path
155 *
156 * @param map The Map chosen in the GUI
157 * @param topoData The data in the TopoJson file
158 */
159 handleTopoJson(map: MapObject, topoData: TopoData): PathGenerator {
160 this.topodata = topoData;
161 this.cache.set(map.id, topoData);
162 this.log.debug('Map retrieved', topoData);
163
164 const topoObject = topoData.objects[map.id];
165 const geoData: FeatureCollection = <FeatureCollection>topojson.feature(topoData, topoObject);
166 this.log.debug('Map retrieved', topoData, geoData);
167
168 const settings: GeneratorSettings = Object.assign({}, DEFAULT_GEN_SETTINGS);
169 const path = d3.geoPath().projection(settings.projection);
170 this.rescaleProjection(
171 settings.projection,
172 settings.mapFillScale,
173 settings.logicalSize,
174 path,
175 geoData);
176 this.log.debug('Scale adjusted');
177
178 return <PathGenerator>{
179 geodata: geoData,
180 pathgen: path,
181 settings: settings
182 };
183 }
184
185 /**
186 * Adjust projection scale and translation to fill the view
187 * with the map
188 * @param proj
189 * @param mfs
190 * @param dim
191 * @param path
192 * @param geoData
193 * @param adjustScale
194 */
195 rescaleProjection(proj: any, mfs: number, dim: number, path: any,
196 geoData: FeatureCollection, adjustScale: number = 1.0) {
197 // start with unit scale, no translation..
198 proj.scale(1).translate([0, 0]);
199
200 // figure out dimensions of map data..
201 const b = path.bounds(geoData);
202 const x1 = b[0][0];
203 const y1 = b[0][1];
204 const x2 = b[1][0];
205 const y2 = b[1][1];
206 const dx = x2 - x1;
207 const dy = y2 - y1;
208 const x = (x1 + x2) / 2;
209 const y = (y1 + y2) / 2;
210
211 // size map to 95% of minimum dimension to fill space..
212 const s = (mfs / Math.min(dx / dim, dy / dim)) * adjustScale;
213 const t = [dim / 2 - s * x, dim / 2 - s * y];
214
215 // set new scale, translation on the projection..
216 proj.scale(s).translate(t);
217 }
Sean Condonf4f54a12018-10-10 23:25:46 +0100218}