diff --git a/src/app/map/map.component.html b/src/app/map/map.component.html index 328608e..15f1cf1 100644 --- a/src/app/map/map.component.html +++ b/src/app/map/map.component.html @@ -1,9 +1,11 @@
+
+
+
+ {{ showLoader }}
-
{{ showLoader }}
diff --git a/src/app/map/map.component.ts b/src/app/map/map.component.ts index fa1149f..1016e00 100644 --- a/src/app/map/map.component.ts +++ b/src/app/map/map.component.ts @@ -1,9 +1,28 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { control, latLng, Layer, LeafletEvent, Map as LeafletMap, MapOptions } from 'leaflet'; +import { + CircleMarker, + control, + GeoJSON, + latLng, + Layer, + LayerGroup, + LeafletEvent, + Map as LeafletMap, + MapOptions +} from 'leaflet'; import { MapService } from './map.service'; import { NGXLogger } from 'ngx-logger'; -import { centerOfEurope, defaultZoom } from './map.constants'; -import { Subscription } from 'rxjs'; +import { + attributionOptions, + centerOfEurope, + defaultCapitolsStyle, + defaultZoom, + defaultZoomPanOptions, + euBorderStyle, + LayerIndices, + noDrawStyle +} from './map.constants'; +import { Subject, takeUntil } from 'rxjs'; @Component({ selector: 'app-map', @@ -24,53 +43,71 @@ export class MapComponent implements OnInit, OnDestroy { attributionControl: false }; - layers: Layer[] = []; + layers: Promise; showLoader: boolean = false; - private map?: LeafletMap; + private theMap?: LeafletMap; private theZoom?: number; - private onReset?: Subscription; - private onToMyLocation?: Subscription; + + private onUnsubscribe$: Subject = new Subject(); constructor( private log: NGXLogger, private mapService: MapService - ) {} + ) { + this.layers = this.loadAllLayers(); + } ngOnInit(): void { - this.onReset = this.mapService.resetMap.subscribe(() => { + this.mapService.resetMap$.pipe(takeUntil(this.onUnsubscribe$)).subscribe(() => { this.resetMap(); }); - this.onToMyLocation = this.mapService.toMyLocation.subscribe(() => { + this.mapService.toMyLocation$.pipe(takeUntil(this.onUnsubscribe$)).subscribe(() => { this.myLocation(); }); + + this.mapService.showEuBorders$.pipe(takeUntil(this.onUnsubscribe$)).subscribe(value => { + this.showEuBorders(value); + }); + + this.mapService.showCapitols$.pipe(takeUntil(this.onUnsubscribe$)).subscribe(value => { + this.showCapitols(value); + }); } ngOnDestroy(): void { - this.onReset!.unsubscribe(); - this.onToMyLocation!.unsubscribe(); + this.unsubscribeAll(); + this.disposeMap(); + } - this.log.info('Disposing map'); + private unsubscribeAll() { + this.onUnsubscribe$.next(true); + this.onUnsubscribe$.complete(); + } - this.map!.clearAllEventListeners(); - this.map!.remove(); + private disposeMap() { + this.theMap!.clearAllEventListeners(); + this.theMap!.remove(); + this.log.info('Disposed theMap'); } onMapReady(map: LeafletMap) { - this.map = map; - - this.map.addControl( - control.attribution({ - position: 'bottomleft', - prefix: '' - }) - ); + this.theMap = map; + this.log.info('Leaflet theMap ready'); + this.theMap.addControl(control.attribution(attributionOptions)); + this.theZoom = map.getZoom(); + } - this.layers.push(this.mapService.getBaseLayer()); - this.mapService.load(this.layers); + private async loadAllLayers() { + return this.mapService.loadAllLayers().then(([baseLayer, euBorderLayer, capitolsLayer]) => { + const layers = new Array(3); + layers[LayerIndices.BASE_LAYER_INDEX] = baseLayer; + layers[LayerIndices.EU_LAYER_INDEX] = euBorderLayer; + layers[LayerIndices.CAPITOLS_LAYER_INDEX] = capitolsLayer; - this.theZoom = map.getZoom(); + return layers; + }); } onMapZoomStart() { @@ -85,17 +122,18 @@ export class MapComponent implements OnInit, OnDestroy { } resetMap() { - this.map!.flyTo(centerOfEurope, defaultZoom); + this.theMap!.flyTo(centerOfEurope, defaultZoom); // TODO clear my location marker } myLocation() { navigator.geolocation.getCurrentPosition( position => { - this.map!.flyTo(latLng(position.coords.latitude, position.coords.longitude), defaultZoom + 2, { - animate: true, - duration: 1 - }); + this.theMap!.flyTo( + latLng(position.coords.latitude, position.coords.longitude), + defaultZoom + 2, + defaultZoomPanOptions + ); }, err => { this.log.error('Could not get geolocation', err); @@ -104,4 +142,26 @@ export class MapComponent implements OnInit, OnDestroy { } ); } + + private showEuBorders(value: boolean) { + this.layers + .then(layer => { + return layer[LayerIndices.EU_LAYER_INDEX] as GeoJSON; + }) + .then(l => l.setStyle(value ? euBorderStyle : noDrawStyle)); + } + + private showCapitols(value: boolean) { + this.theMap!.closePopup(); + + this.layers + .then(layer => { + return layer[LayerIndices.CAPITOLS_LAYER_INDEX] as LayerGroup; + }) + .then(capitols => capitols.getLayers().forEach(capitol => this.markerVisible(capitol, value))); + } + + private markerVisible(layer: Layer, visible: boolean) { + (layer as CircleMarker).setStyle(visible ? defaultCapitolsStyle : noDrawStyle); + } } diff --git a/src/app/map/map.constants.ts b/src/app/map/map.constants.ts index 64275f9..1bc8d25 100644 --- a/src/app/map/map.constants.ts +++ b/src/app/map/map.constants.ts @@ -1,13 +1,37 @@ // TODO find a way to configure color -import { circleMarker, latLng, layerGroup, tileLayer } from 'leaflet'; +import { + CircleMarker, + circleMarker, + Control, + latLng, + LayerGroup, + layerGroup, + tileLayer, + ZoomPanOptions +} from 'leaflet'; import { environment } from '../../environments/environment'; +import AttributionOptions = Control.AttributionOptions; + +export const attributionOptions: AttributionOptions = { + position: 'bottomleft', + prefix: '' +}; + +export const defaultZoomPanOptions: ZoomPanOptions = { + animate: true, + duration: 1 +}; + +export const noDrawStyle = { radius: 0, opacity: 0, fill: false, stroke: false, popup: false }; export const euBorderStyle = { color: '#fff', fillColor: '#fff', opacity: 0.4, weight: 0.6, - fillOpacity: 0.06 + fillOpacity: 0.06, + fill: true, + stroke: true }; export const darkMatterNoLabelsLayer = tileLayer(environment.tileServerUrl, { @@ -18,14 +42,24 @@ export const darkMatterNoLabelsLayer = tileLayer(environment.tileServerUrl, { export const centerOfEurope = latLng(54.526, 15.2551); // TODO find a way to configure color -const berlin = circleMarker([52.52, 13.405], { +export const defaultCapitolsStyle = { radius: 8, weight: 2, - color: '#ffd617' -}).bindPopup('Berlin', { - closeButton: false -}); + color: '#ffd617', + opacity: 1.0, + fill: true, + stroke: true, + popup: true +}; -export const capitols = layerGroup([berlin]); +const berlin = circleMarker([52.52, 13.405], defaultCapitolsStyle).bindPopup('Berlin', { closeButton: false }); + +export const capitols: LayerGroup = layerGroup([berlin]); export const defaultZoom = 5; + +export enum LayerIndices { + BASE_LAYER_INDEX = 0, + EU_LAYER_INDEX = 1, + CAPITOLS_LAYER_INDEX = 2 +} diff --git a/src/app/map/map.service.ts b/src/app/map/map.service.ts index 78f7024..00fc64f 100644 --- a/src/app/map/map.service.ts +++ b/src/app/map/map.service.ts @@ -1,44 +1,78 @@ -import { EventEmitter, Injectable, Output } from '@angular/core'; -import { geoJson, Layer } from 'leaflet'; +import { EventEmitter, Injectable } from '@angular/core'; +import { GeoJSON, geoJson, Layer } from 'leaflet'; import { HttpClient } from '@angular/common/http'; import { NGXLogger } from 'ngx-logger'; import { GeoJsonObject } from 'geojson'; import { capitols, centerOfEurope, darkMatterNoLabelsLayer, euBorderStyle } from './map.constants'; +import { firstValueFrom } from 'rxjs'; @Injectable() export class MapService { - @Output() resetMap = new EventEmitter(); - @Output() toMyLocation = new EventEmitter(); + resetMap$ = new EventEmitter(); + toMyLocation$ = new EventEmitter(); + showEuBorders$ = new EventEmitter(); + showCapitols$ = new EventEmitter(); + + private readonly euBordersLayer: Promise; constructor( private http: HttpClient, private log: NGXLogger - ) {} + ) { + this.euBordersLayer = this.initEuBordersLayer(); + } - getBaseLayer() { - return darkMatterNoLabelsLayer; + private async initEuBordersLayer() { + return firstValueFrom(this.http.get('./assets/geo-data/european-borders.json')) + .then(jsonResponse => { + this.log.debug('Loaded borders json', jsonResponse); + return geoJson(jsonResponse, { style: euBorderStyle }); + }) + .catch(err => { + this.log.error('Error loading EU borders geo json', err); + // TODO error notification + return geoJson(); + }); } getCenterOfEurope() { return centerOfEurope; } - load(layers: Layer[]) { - this.http.get('./assets/geo-data/european-borders.json').subscribe((json: GeoJsonObject) => { - this.log.info('Loaded borders json', json); + async loadAllLayers(): Promise { + return Promise.all([this.getBaseLayer(), this.getEuBordersLayer(), this.getCapitolsLayer()]); + } + + private async getBaseLayer() { + return darkMatterNoLabelsLayer; + } - layers.push(geoJson(json, { style: euBorderStyle })); - layers.push(capitols); - }); + private async getEuBordersLayer() { + return this.euBordersLayer; } - resetMapToEuropeanCenter() { + // TODO load capitols via HTTP + private async getCapitolsLayer() { + return capitols; + } + + async resetMapToEuropeanCenter() { this.log.info('Reset map'); - this.resetMap.emit(); + this.resetMap$.emit(); } - moveMapToMyLocation() { + async moveMapToMyLocation() { this.log.info('Move map to my location'); - this.toMyLocation.emit(); + this.toMyLocation$.emit(); + } + + async doShowEuBorders(value: boolean) { + this.log.trace('Show EU borders', value); + this.showEuBorders$.emit(value); + } + + async doShowCapitols(value: boolean) { + this.log.trace('Show capitols', value); + this.showCapitols$.emit(value); } } diff --git a/src/app/option-pane/option-pane.component.html b/src/app/option-pane/option-pane.component.html index 197ba1f..a3d0c7a 100644 --- a/src/app/option-pane/option-pane.component.html +++ b/src/app/option-pane/option-pane.component.html @@ -17,15 +17,15 @@
+ (change)="euBordersChanged($event)"> {{ 'option-pane.show-eu-borders' | transloco }}
- {{ 'option-pane.show-berlin' | transloco }} + [checked]="showCapitols" + (change)="capitolsChanged($event)"> + {{ 'option-pane.show-capitols' | transloco }}
diff --git a/src/app/option-pane/option-pane.component.ts b/src/app/option-pane/option-pane.component.ts index c6205af..c41b308 100644 --- a/src/app/option-pane/option-pane.component.ts +++ b/src/app/option-pane/option-pane.component.ts @@ -1,5 +1,6 @@ -import { Component } from '@angular/core'; -import { NGXLogger } from 'ngx-logger'; +import { Component, Input } from '@angular/core'; +import { MapService } from '../map/map.service'; +import { MatSlideToggleChange } from '@angular/material/slide-toggle'; @Component({ selector: 'app-option-pane', @@ -7,11 +8,12 @@ import { NGXLogger } from 'ngx-logger'; styleUrl: 'option-pane.component.scss' }) export class OptionPaneComponent { - expanded: boolean = true; - showEuBorders: boolean = true; - showBerlin: boolean = true; + expanded: boolean = false; - constructor(private log: NGXLogger) {} + @Input() showEuBorders: boolean = true; + @Input() showCapitols: boolean = true; + + constructor(private mapService: MapService) {} protected expand() { this.expanded = true; @@ -21,11 +23,11 @@ export class OptionPaneComponent { this.expanded = false; } - euBordersChanged() { - this.log.debug('EU border changed'); + euBordersChanged(event: MatSlideToggleChange) { + this.mapService.doShowEuBorders(event.checked).then(() => (this.showEuBorders = event.checked)); } - berlinChanged() { - this.log.debug('Berlin changed'); + capitolsChanged(event: MatSlideToggleChange) { + this.mapService.doShowCapitols(event.checked).then(() => (this.showCapitols = event.checked)); } } diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 8faeaf2..2239488 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -3,11 +3,11 @@ "title": "Europas Städte" }, "toolbar": { - "reset": "Zurücksetzen", + "reset": "Karte zurücksetzen", "my-location": "Wo bin ich" }, "option-pane":{ "show-eu-borders": "EU Grenzen (UN)", - "show-berlin": "Berlin anzeigen" + "show-capitols": "Hauptstädte anzeigen" } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index fdc4180..e46cd9f 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -3,11 +3,11 @@ "title": "Cities of Europe" }, "toolbar": { - "reset": "Reset", + "reset": "Reset map", "my-location": "My location" }, "option-pane": { "show-eu-borders": "European borders (UN)", - "show-berlin": "Show Berlin" + "show-capitols": "Show capitols" } }