import {Component, Input, Output, EventEmitter, OnChanges, SimpleChanges} from '@angular/core'; import {IdaiFieldDocument} from '../idai-field-model/idai-field-document'; import {IdaiFieldResource} from '../idai-field-model/idai-field-resource'; import {IdaiFieldPolyline} from './idai-field-polyline'; import {IdaiFieldPolygon} from './idai-field-polygon'; import {IdaiFieldMarker} from './idai-field-marker'; import {CoordinatesUtility} from './coordinates-utility'; import {ConfigLoader} from '../configuration/config-loader'; // no typings for VectorMarkers available declare global { namespace L { module VectorMarkers { function icon(option: any): any; } } } @Component({ moduleId: module.id, selector: 'map', template: '
' }) /** * @author Thomas Kleinke */ export class MapComponent implements OnChanges { @Input() documents: Array; @Input() selectedDocument: IdaiFieldDocument; @Input() parentDocument: IdaiFieldDocument; @Input() projectDocument: IdaiFieldDocument; @Input() update: boolean; @Output() onSelectDocument: EventEmitter = new EventEmitter(); protected ready: Promise; protected map: L.Map; protected polygons: { [resourceId: string]: Array } = {}; protected polylines: { [resourceId: string]: Array } = {}; protected markers: { [resourceId: string]: IdaiFieldMarker } = {}; protected bounds: any[] = []; // in fact L.LatLng[], but leaflet typings are incomplete protected typeColors: { [typeName: string]: string } = {}; constructor(configLoader: ConfigLoader) { this.ready = (configLoader.getProjectConfiguration() as any).then((projectConfiguration: any) => { return projectConfiguration.getTypeColors(); }).then((typeColors: any) => this.typeColors = typeColors); } public ngAfterViewInit() { if (this.map) this.map.invalidateSize(false); } public ngOnChanges(changes: SimpleChanges) { if (!this.map) this.map = this.createMap(); this.ready.then(() => this.updateMap(changes)); } protected createMap(): L.Map { const mapOptions: L.MapOptions = { crs: this.getCoordinateReferenceSystem(), attributionControl: false, minZoom: -20, maxZoom: 30, maxBoundsViscosity: 0.7 }; const map: L.Map = L.map('map-container', mapOptions); let mapComponent = this; map.on('click', function(event: L.MouseEvent) { mapComponent.clickOnMap(event.latlng); }); return map; } protected updateMap(changes: SimpleChanges): Promise { if (!this.update) return Promise.resolve(); this.clearMap(); this.addGeometriesToMap(); this.updateCoordinateReferenceSystem(); return this.setView(); } protected setView(): Promise { this.map.invalidateSize(true); if (this.selectedDocument) { if (this.polygons[this.selectedDocument.resource.id as any]) { this.focusPolygons(this.polygons[this.selectedDocument.resource.id as any]); } else if (this.polylines[this.selectedDocument.resource.id as any]) { this.focusPolylines(this.polylines[this.selectedDocument.resource.id as any]); } else if (this.markers[this.selectedDocument.resource.id as any]) { this.focusMarker(this.markers[this.selectedDocument.resource.id as any]); } } else if (this.bounds.length > 1) { this.map.fitBounds(L.latLngBounds(this.bounds)); } else if (this.bounds.length == 1) { this.map.setView(this.bounds[0], 15); } else { this.map.setView([0, 0], 15); } return Promise.resolve(); } protected clearMap() { for (let i in this.polygons) { for (let polygon of this.polygons[i]) { this.map.removeLayer(polygon); } } for (let i in this.polylines) { for (let polyline of this.polylines[i]) { this.map.removeLayer(polyline); } } for (let i in this.markers) { this.map.removeLayer(this.markers[i]); } this.polygons = {}; this.polylines = {}; this.markers = {}; } protected extendBounds(latLng: L.LatLng) { this.bounds.push(latLng); } private extendBoundsForMultipleLatLngs(latLngs: Array) { // Check if latLngs is an array of LatLng objects or an array of arrays of LatLng objects. // This is necessary because getLatLngs() returns an array of LatLng objects for points and polylines but an // array of arrays of LatLng objects for polygons. if (!latLngs[0].lng) latLngs = latLngs[0]; for (let latLng of latLngs) { this.extendBounds(latLng); } } protected addGeometriesToMap() { this.bounds = []; this.addParentDocumentGeometryToMap(); if (this.documents) { for (let document of this.documents) { if (document.resource.geometry) this.addGeometryToMap(document); } } } protected addParentDocumentGeometryToMap() { if (!this.parentDocument || !this.parentDocument.resource.geometry) return; if (['LineString', 'MultiLineString', 'Polygon', 'MultiPolygon'] .indexOf(this.parentDocument.resource.geometry.type) == -1) { return; } this.addGeometryToMap(this.parentDocument); } protected addGeometryToMap(document: IdaiFieldDocument) { const geometry = document.resource.geometry; if (!geometry) return; if (!geometry.coordinates || geometry.coordinates.length == 0) return; switch(geometry.type) { case 'Point': let marker: IdaiFieldMarker = this.addMarkerToMap(geometry.coordinates, document); this.extendBounds(marker.getLatLng()); break; case 'LineString': let polyline: IdaiFieldPolyline = this.addPolylineToMap(geometry.coordinates, document); this.extendBoundsForMultipleLatLngs(polyline.getLatLngs()); break; case 'MultiLineString': for (let polylineCoordinates of geometry.coordinates) { let polyline: IdaiFieldPolyline = this.addPolylineToMap(polylineCoordinates, document); this.extendBoundsForMultipleLatLngs(polyline.getLatLngs()); } break; case 'Polygon': let polygon: IdaiFieldPolygon = this.addPolygonToMap(geometry.coordinates, document); this.extendBoundsForMultipleLatLngs(polygon.getLatLngs()); break; case 'MultiPolygon': for (let polygonCoordinates of geometry.coordinates) { let polygon: IdaiFieldPolygon = this.addPolygonToMap(polygonCoordinates, document); this.extendBoundsForMultipleLatLngs(polygon.getLatLngs()); } break; } } private addMarkerToMap(coordinates: any, document: IdaiFieldDocument): IdaiFieldMarker { let latLng = L.latLng([coordinates[1], coordinates[0]]); let color = this.typeColors[document.resource.type]; let extraClasses = (this.selectedDocument && this.selectedDocument.resource.id == document.resource.id) ? 'active' : ''; let icon = this.generateMarkerIcon(color, extraClasses); let marker: IdaiFieldMarker = L.marker(latLng, { icon: icon }); marker.document = document; marker.bindTooltip(this.getShortDescription(document.resource), { offset: L.point(0, -40), direction: 'top', opacity: 1.0}); let mapComponent = this; marker.on('click', function() { mapComponent.select(this.document); }); marker.addTo(this.map); this.markers[document.resource.id as any] = marker; return marker; } private addPolylineToMap(coordinates: any, document: IdaiFieldDocument): IdaiFieldPolyline { let polyline: IdaiFieldPolyline = this.getPolylineFromCoordinates(coordinates); polyline.document = document; if (document == this.parentDocument) { this.setPathOptionsForParentDocument(polyline, document); } else { this.setPathOptions(polyline, document, 'polyline'); } let polylines: Array = this.polylines[document.resource.id as any] ? this.polylines[document.resource.id as any] : []; polylines.push(polyline); this.polylines[document.resource.id as any] = polylines; return polyline; } private addPolygonToMap(coordinates: any, document: IdaiFieldDocument): IdaiFieldPolygon { let polygon: IdaiFieldPolygon = this.getPolygonFromCoordinates(coordinates); polygon.document = document; if (document == this.parentDocument) { this.setPathOptionsForParentDocument(polygon, document); } else { this.setPathOptions(polygon, document, 'polygon'); } let polygons: Array = this.polygons[document.resource.id as any] ? this.polygons[document.resource.id as any] : []; polygons.push(polygon); this.polygons[document.resource.id as any] = polygons; return polygon; } private setPathOptions(path: L.Path, document: IdaiFieldDocument, className: string) { if (this.selectedDocument && this.selectedDocument.resource.id == document.resource.id) { className = className + ' active'; } let style = { color: this.typeColors[document.resource.type], className: className }; path.setStyle(style); path.bindTooltip(this.getShortDescription(document.resource), { direction: 'center', opacity: 1.0 }); let mapComponent = this; path.on('click', function (event: L.Event) { if (mapComponent.select(this.document)) L.DomEvent.stop(event); }); path.addTo(this.map); } private setPathOptionsForParentDocument(path: L.Path, document: IdaiFieldDocument) { path.setStyle({ color: this.typeColors[document.resource.type], className: 'parent', interactive: false }); path.addTo(this.map); } private focusMarker(marker: L.Marker) { this.map.panTo(marker.getLatLng(), { animate: true, easeLinearity: 0.3 }); } private focusPolylines(polylines: Array) { let bounds = [] as any; for (let polyline of polylines) { bounds.push(polyline.getLatLngs() as never); } this.map.fitBounds(bounds); } private focusPolygons(polygons: Array) { let bounds = [] as any; for (let polygon of polygons) { bounds.push(polygon.getLatLngs() as never); } this.map.fitBounds(bounds); } private getShortDescription(resource: IdaiFieldResource) { let shortDescription = resource.identifier; if (resource.shortDescription && resource.shortDescription.length > 0) { shortDescription += ' | ' + resource.shortDescription; } return shortDescription; } protected clickOnMap(clickPosition: L.LatLng) { this.deselect(); } protected select(document: IdaiFieldDocument): boolean { this.onSelectDocument.emit(document); return true; } protected deselect() { this.onSelectDocument.emit(null as any); } private getPolylineFromCoordinates(coordinates: Array): L.Polyline { return L.polyline( CoordinatesUtility.convertPolylineCoordinatesFromLngLatToLatLng(coordinates)); } private getPolygonFromCoordinates(coordinates: Array): L.Polygon { return L.polygon( CoordinatesUtility.convertPolygonCoordinatesFromLngLatToLatLng(coordinates)); } protected generateMarkerIcon(color: string, extraClasses: string = ''): L.Icon { return L.VectorMarkers.icon({ prefix: 'mdi', icon: 'checkbox-blank-circle', markerColor: color, extraClasses: extraClasses }); } private updateCoordinateReferenceSystem() { this.map.options.crs = this.getCoordinateReferenceSystem(); } private getCoordinateReferenceSystem(): L.CRS { if (!this.projectDocument) return L.CRS.Simple; switch (this.projectDocument.resource.coordinateReferenceSystem) { case 'EPSG4326 (WGS 84)': return L.CRS.EPSG4326; case 'EPSG3857 (WGS 84 Web Mercator)': return L.CRS.EPSG3857; default: return L.CRS.Simple; } } }