import { ElementRef, Input, ViewChild, Directive, HostListener } from '@angular/core' import { Subscription } from 'rxjs' import { MatButton } from '@angular/material/button' import { Position } from 'geojson' import distance from '@turf/distance' import { Units } from '@turf/helpers' import { MapService } from '@maplibre/ngx-maplibre-gl' import { GeoJSONSource, MapMouseEvent } from 'maplibre-gl' const LAYER_LINE = 'gisaf-ruler-line' const LAYER_SYMBOL = 'gisaf-ruler-symbol' const LAYER_DISTANCE = 'gisaf-ruler-dist' const LAYER_MARKER = 'gisaf-ruler-marker' const SOURCE_LINE = 'gisaf-ruler-line-source' const SOURCE_SYMBOL = 'gisaf-ruler-symbol-source' @Directive({ selector: '[gisafRuler]' }) export class GisafRulerDirective { segments: Position[][] = [] startingPoint: Position labels: string[] distances: number[] // XXX: remove typings (maplibre 2.x) //lineSource: GeoJSONSource //symbolSource: GeoJSONSource lineSource: any symbolSource: any active: boolean = false @Input() color: string = '#4264fb' @Input() units: Units = 'kilometers' @Input() font: string[] = ['Noto Sans Regular'] @Input() fontSize: number = 14 @Input() markerSize: number = 3 @Input() lineWidth: number = 1 @Input() secondaryColor: string = 'white' @Input() fontHalo: number = 2 @ViewChild(MatButton) button: MatButton clickSubscription: Subscription lineDrawn: boolean constructor( public elementRef: ElementRef, private mapService: MapService ) {} @HostListener('click') onClick() { this.active ? this.stop() : this.start() } start() { this.active = true this.elementRef.nativeElement.classList.add('-active') this.segments = [] this.labels = [] this.distances = [] this.lineDrawn = false this.startingPoint = undefined this.addLayers() this.mapService.mapCreated$.subscribe( () => { this.clickSubscription = this.mapService.mapEvents.mapClick.subscribe( evt => this.onMapClick(evt) ) } ) } stop() { this.active = false this.elementRef.nativeElement.classList.remove('-active') this.clickSubscription.unsubscribe() this.removeLayers() } /* updateLabels() { let sum = 0 this.labels = this.segments.map((coordinate, index) => { if (index === 0) return this.labelFormat(0) sum += distance(this.segments[index - 1], coordinate, {units: this.units}) return this.labelFormat(sum) }) } */ /* updateDistances() { this.distances = [] this.segments.map((coordinate, index) => { if (index === 0) return this.distances.push( distance(this.segments[index - 1], coordinate, {units: this.units}) ) }) } */ labelFormat(n) { return n == 0 ? '0' : n < 1 ? `${(n * 1000).toFixed(2)} m`: `${n.toFixed(3)} km` } onMapClick(evt: MapMouseEvent) { const newCoordinate = [evt.lngLat.lng, evt.lngLat.lat] if (this.lineDrawn) { if (evt.originalEvent.ctrlKey) { this.segments.push([this.startingPoint, newCoordinate]) } else { this.startingPoint = newCoordinate this.lineDrawn = false } } else { if (this.startingPoint) { this.segments.push([this.startingPoint, newCoordinate]) this.lineDrawn = true } else { this.startingPoint = newCoordinate } } //else { // if (!this.startingPoint || this.multiLineMode) { // this.startingPoint = newCoordinate // } // else { // this.segments.push([this.startingPoint, newCoordinate]) // this.startingPoint = undefined // } // this.multiLineMode = false //} //this.updateLabels() //this.updateDistances() this.lineSource.setData(this.getLineStringFeatures()) this.symbolSource.setData(this.getPointFeatureCollection()) } addLayers() { const map = this.mapService.mapInstance map.addSource(SOURCE_LINE, { type: 'geojson', data: this.getLineStringFeatures() } ) map.addSource(SOURCE_SYMBOL, { type: 'geojson', data: this.getPointFeatureCollection() }) map.addLayer({ id: LAYER_LINE, type: 'line', source: SOURCE_LINE, paint: { 'line-color': this.color, 'line-width': this.lineWidth, 'line-dasharray': [2, 2] }, }) /* map.addLayer({ id: LAYER_SYMBOL, type: 'symbol', source: SOURCE_SYMBOL, layout: { 'text-field': '{text}', 'text-font': this.font, 'text-anchor': 'top', 'text-size': this.fontSize, 'text-offset': [0, 0.8], 'text-allow-overlap': true, }, paint: { 'text-color': this.color, //'text-halo-color': this.secondaryColor, //'text-halo-width': this.fontHalo, }, }) */ map.addLayer({ id: LAYER_DISTANCE, type: 'symbol', source: SOURCE_LINE, layout: { 'text-field': ['get', 'dist'], 'text-font': this.font, //'text-anchor': 'top', 'text-size': this.fontSize, //'text-offset': [0, 0.8], 'text-allow-overlap': true, 'symbol-placement': 'line-center' }, paint: { 'text-color': this.color, 'text-halo-color': this.secondaryColor, 'text-halo-width': this.fontHalo, }, }) map.addLayer({ id: LAYER_MARKER, type: 'circle', source: SOURCE_SYMBOL, paint: { 'circle-radius': this.markerSize, 'circle-color': this.color, }, }) this.lineSource = map.getSource(SOURCE_LINE) this.symbolSource = map.getSource(SOURCE_SYMBOL) } removeLayers() { const map = this.mapService.mapInstance map.removeLayer(LAYER_LINE) //map.removeLayer(LAYER_SYMBOL) map.removeLayer(LAYER_DISTANCE) map.removeLayer(LAYER_MARKER) map.removeSource(SOURCE_LINE) map.removeSource(SOURCE_SYMBOL) } getLineStringFeatures(): GeoJSON.FeatureCollection { return { type: 'FeatureCollection', features: this.segments.map( segment => ({ type: 'Feature', properties: { dist: this.labelFormat(distance(segment[0], segment[1])) }, geometry: { type: 'LineString', coordinates: segment, } }) ) } } getPointFeatureCollection(): GeoJSON.FeatureCollection { let points: Position[] = [] this.segments.forEach( s => { points.push(s[0], s[1]) } ) if (!!this.startingPoint) { points.push(this.startingPoint) } return { type: 'FeatureCollection', features: points.map( p => ({ type: 'Feature', properties: {}, geometry: { type: 'Point', coordinates: p, } }) ) } } }