import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'; import 'ol/ol.css'; import {Map, View} from 'ol'; import TileLayer from 'ol/layer/Tile'; import {fromLonLat, transform} from 'ol/proj'; import VectorLayer from 'ol/layer/Vector'; import {OSM} from 'ol/source'; import {CdkDragDrop} from "@angular/cdk/drag-drop"; import {MatDialog} from "@angular/material"; import {AddLayerComponent} from "../add-layer-component/add-layer.component"; import {RawLayer} from "../models/RawLayer"; import LayerGroup from 'ol/layer/Group'; import {Collection} from "openlayers"; import {EPSG} from "../enums/EPSG"; import {inAndOut} from 'ol/easing'; import {Feature} from 'ol/Feature'; import Style from 'ol/style/Style'; import Stroke from 'ol/style/Stroke'; import Fill from 'ol/style/Fill'; /** * TODO: add description */ @Component({ selector: 'app-map', templateUrl: './map.component.html', styleUrls: ['./map.component.scss'] }) export class MapComponent implements OnInit { @ViewChild('mapContainer') mapContainer: ElementRef; public map: Map; public baseLayer: TileLayer; /** * Represents the custom layer that were uploaded by user */ private customLayerGroup: LayerGroup; /** * Returns the layer collection of the customLayerGroup */ get customLayerCollection(): Collection { return this.customLayerGroup.getLayers(); } /** * The coordinates in wgs84 according to the cursor position */ public cursorCoordinates: string; /** * The properties of the current feature. The current layer is the layer beneath the cursor. */ public currentFeatureInfo: any; /** * A simple cache to save styles */ private styleCache: { [key: string]: Style } = {}; /** * A simple feature cache */ private featureCache: { [key: string]: Feature } = {}; constructor(public dialog: MatDialog) { } ngOnInit() { this.initCache(); this.initBaseLayer(); this.initLayerGroup(); this.initMap(); this.addMockData(); } /** * Inits the base layer (OSM TileLayer) */ initBaseLayer() { this.baseLayer = new TileLayer({ source: new OSM() }); } /** * Inits the customLayerGroup */ initLayerGroup() { this.customLayerGroup = new LayerGroup({ layers: [] }); } /** * Inits the open layers map instance */ private initMap() { this.map = new Map({ target: this.mapContainer.nativeElement, layers: [ this.baseLayer, this.customLayerGroup ], view: new View({ center: fromLonLat([0, 0]), zoom: 2 }) }); this.addMapEventlistener(); // without this line the map is only visible after resizing window requestAnimationFrame(() => { this.map.updateSize(); }) } /** * Adds all map event listener */ private addMapEventlistener() { // get current cursor coordinates to transform and display them this.map.on('pointermove', (evt) => { const tmpCoords = transform(evt.coordinate, EPSG.openLayersDefault, EPSG.wgs84); this.cursorCoordinates = `Lat: ${tmpCoords[1]} / Long: ${tmpCoords[0]}`; }); // add event to get feature infos this.map.on('pointermove', (evt) => { if (evt.dragging || this.featureCache['selectedFeature']) { return; } const pixel = this.map.getEventPixel(evt.originalEvent); const features = this.map.getFeaturesAtPixel(pixel); if (features && features.length > 0) { this.map.getTargetElement().style.cursor = 'pointer'; this.displayFeatureInfo(features[0]); this.highlightFeature(features[0]); } else { this.map.getTargetElement().style.cursor = ''; // reapply original style to last highlighted feature if (this.featureCache['lastHighlighted']) { this.featureCache['lastHighlighted'].setStyle(this.styleCache['lastHighlightedStyle']); } // reset cache for feature and style this.featureCache['lastHighlighted'] = null; this.styleCache['lastHighlightedStyle'] = null; this.currentFeatureInfo = null; } }); // handle feature selection this.map.on('click', (evt) => { const pixel = this.map.getEventPixel(evt.originalEvent); const features = this.map.getFeaturesAtPixel(pixel); if (this.featureCache['selectedFeature']) { // use lastHighlightedStyle because the original style is saved there this.featureCache['selectedFeature'].setStyle(this.styleCache['lastHighlightedStyle']); // this.styleCache['lastHighlightedStyle'] = null; this.featureCache['selectedFeature'] = null; } else if (features && features.length > 0) { this.map.getTargetElement().style.cursor = ''; this.displayFeatureInfo(features[0]); this.featureCache['selectedFeature'] = features[0]; this.featureCache['lastHighlighted'] = null; features[0].setStyle(this.styleCache['defaultSelected']); } }); } /** * Prepared feature properties and shows them * @param features */ private displayFeatureInfo(feature: Feature) { const properties = feature.getProperties(); delete properties.geometry; this.currentFeatureInfo = properties; }; /** * Highlights a Features * @param feature */ private highlightFeature(feature: Feature) { // reapply saved feature and style if (this.featureCache['lastHighlighted']) { this.featureCache['lastHighlighted'].setStyle(this.styleCache['lastHighlightedStyle']); } // save current feature and style this.featureCache['lastHighlighted'] = feature; this.styleCache['lastHighlightedStyle'] = feature.getStyle(); // set new feature style feature.setStyle(this.styleCache['defaultHighlight']); } /** * Opens a dialog to create a new custom layer */ addLayer(): void { const dialogRef = this.dialog.open(AddLayerComponent); dialogRef.afterClosed().subscribe(rawLayer => { if (rawLayer) { const customLayer = RawLayer.toVectorLayer(rawLayer); this.centerLayerOnMap(customLayer); this.customLayerCollection.push(customLayer); } } ); } /** * Opens a dialog to edit a custom layer */ editLayer(index: number): void { const vectorLayer = this.customLayerCollection.item(index); const rawLayer = RawLayer.fromVectorLayer(vectorLayer); const dialogRef = this.dialog.open(AddLayerComponent, {data: rawLayer}); dialogRef.afterClosed().subscribe(rawLayer => { if (rawLayer) { // TODO // when a layer was edited the information about the file is lost. // That causes that the layer is removed from map. In such a case, just edit color or name. const customLayer = RawLayer.toVectorLayer(rawLayer); this.customLayerCollection.setAt(index, customLayer); } } ); } /** * Updates the customLayer collection according to drag and drop actions. * @param event */ drop(event: CdkDragDrop) { const tmp = this.customLayerCollection.item(event.previousIndex); this.customLayerCollection.removeAt(event.previousIndex); this.customLayerCollection.insertAt(event.currentIndex, tmp); } deleteLayer(layer: VectorLayer) { this.customLayerCollection.remove(layer); } /** * Toggles the visibility of a custom layer * @param index */ toggleLayerVisibility(layer: VectorLayer) { layer.setVisible(!layer.getVisible()); } saveLayers(): RawLayer[] { const rawLayers = []; this.customLayerCollection.forEach(layer => rawLayers.push(RawLayer.fromVectorLayer(layer))); return rawLayers; } /** * Focusses the passed layer on map * @param customLayer */ private centerLayerOnMap(customLayer: VectorLayer) { const options = { size: this.map.getSize(), duration: 888, easing: inAndOut } this.map.getView().fit(customLayer.getSource().getExtent(), options); } /** * Support function that inits cache values */ private initCache() { this.styleCache['defaultSelected'] = new Style({ stroke: new Stroke({ color: 'rgb(245, 64, 41)', width: 3 }), fill: new Fill({ color: 'rgba(245, 64, 41, 0.3)', }) }) this.styleCache['defaultHighlight'] = new Style({ stroke: new Stroke({ color: 'rgb(245, 64, 41)', width: 2 }), fill: new Fill({ color: 'rgba(245, 64, 41, 0.1)', }) }) } /** * DeBUG DEBUG DBEUG */ private addMockData() { const data = { "type": "FeatureCollection", "name": "background", "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, "features": [{ "type": "Feature", "properties": { "name": "Layer 1" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [8.801864395832126, 53.14770721254275], [8.806370506977146, 53.14770721254275], [8.806542168354099, 53.14500456859233], [8.80203605720908, 53.14521049030746] ] ] ] } }, { "type": "Feature", "properties": { "name": "Layer 2" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [8.807787400997881, 53.1477644198588], [8.808044893063311, 53.145010298994144], [8.813452226437335, 53.145010298994144], [8.813194734371905, 53.1477644198588] ] ] ] } }, { "type": "Feature", "properties": { "name": "Layer 3" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [8.80207966021419, 53.14392919393571], [8.802122575558428, 53.138780701270434], [8.80632827929378, 53.13896090893504], [8.806542856014971, 53.14410938000193] ] ] ] } }, { "type": "Feature", "properties": { "name": "Layer 4" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [8.808654874418039, 53.143631496039035], [8.809770673368234, 53.1391523112723], [8.816036313627023, 53.13930677369489], [8.8156071601846, 53.14352853151957] ] ] ] } } ] }; const rawLayer = new RawLayer('Mock Data Layer', JSON.stringify(data)); rawLayer.saveDefaultColor([125,125,125]) const customLayer = RawLayer.toVectorLayer(rawLayer); this.centerLayerOnMap(customLayer); this.customLayerCollection.push(customLayer); } }