From 8edcf1c9f4a4c25776e2c99424bec1e6e0ca3670 Mon Sep 17 00:00:00 2001 From: phil Date: Sun, 22 Dec 2024 02:45:41 +0100 Subject: [PATCH] Update openapi, fixing display of map basestyle with static_url --- .../gisaf-mapbox/gisaf-mapbox.component.html | 2 +- .../gisaf-mapbox/gisaf-mapbox.component.ts | 2322 +++++++++-------- src/app/map/map-data.service.ts | 228 +- src/app/openapi/core/OpenAPI.ts | 2 +- src/app/openapi/schemas.gen.ts | 131 +- src/app/openapi/types.gen.ts | 113 +- 6 files changed, 1360 insertions(+), 1438 deletions(-) diff --git a/src/app/map/gisaf-mapbox/gisaf-mapbox.component.html b/src/app/map/gisaf-mapbox/gisaf-mapbox.component.html index aa9ac3d..1ce9b2d 100644 --- a/src/app/map/gisaf-mapbox/gisaf-mapbox.component.html +++ b/src/app/map/gisaf-mapbox/gisaf-mapbox.component.html @@ -94,4 +94,4 @@
- \ No newline at end of file + diff --git a/src/app/map/gisaf-mapbox/gisaf-mapbox.component.ts b/src/app/map/gisaf-mapbox/gisaf-mapbox.component.ts index 2413141..0ede25d 100644 --- a/src/app/map/gisaf-mapbox/gisaf-mapbox.component.ts +++ b/src/app/map/gisaf-mapbox/gisaf-mapbox.component.ts @@ -1,5 +1,7 @@ -import { Component, OnInit, OnDestroy, Input, ViewChild, NgZone, - ChangeDetectionStrategy, ChangeDetectorRef, ElementRef } from '@angular/core' +import { + Component, OnInit, OnDestroy, Input, ViewChild, NgZone, + ChangeDetectionStrategy, ChangeDetectorRef, ElementRef +} from '@angular/core' import { ActivatedRoute, Params, Router } from '@angular/router' import { Observable } from 'rxjs' @@ -8,9 +10,11 @@ import { WebSocketSubject } from 'rxjs/webSocket' import { MatSnackBar } from '@angular/material/snack-bar' -import { Map, MapMouseEvent, LayerSpecification, - GeoJSONSourceSpecification, FitBoundsOptions, - ExpressionSpecification, Source, GeoJSONSource } from 'maplibre-gl' +import { + Map, MapMouseEvent, LayerSpecification, + GeoJSONSourceSpecification, FitBoundsOptions, + ExpressionSpecification, Source, GeoJSONSource +} from 'maplibre-gl' import Point from '@mapbox/point-geometry' import * as bbox from '@turf/bbox' @@ -23,1255 +27,1255 @@ import { InfoDataService, Feature, TaggedLayer, FeatureWithField, TaggedFeature import { MapDataService, BaseStyle } from '../map-data.service' export class LayerWithMetaData { - constructor( - public layer: LayerSpecification, - public highlightedLayer: LayerSpecification, - public layerNode: LayerNode, - ) {} + constructor( + public layer: LayerSpecification, + public highlightedLayer: LayerSpecification, + public layerNode: LayerNode, + ) { } } let normalize = (s: string) => s ? s.trim().toLowerCase() : '' @Component({ - selector: 'gisaf-mapbox', - templateUrl: 'gisaf-mapbox.component.html', - styleUrls: [ 'gisaf-mapbox.component.css' ], - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [ - GeoJsonService, - LiveGeoJsonService, - ], + selector: 'gisaf-mapbox', + templateUrl: 'gisaf-mapbox.component.html', + styleUrls: ['gisaf-mapbox.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + GeoJsonService, + LiveGeoJsonService, + ], }) export class GisafMapboxComponent implements OnInit, OnDestroy { - @ViewChild('mglMap', { static: true }) mglMap: Map - @ViewChild('featureInfo', { static: true }) featureInfo: ElementRef - @ViewChild('mouseLngLat', { static: true }) mouseLngLat: ElementRef - @ViewChild('customAttribution', { static: true }) customAttributionElement: ElementRef + @ViewChild('mglMap', { static: true }) mglMap: Map + @ViewChild('featureInfo', { static: true }) featureInfo: ElementRef + @ViewChild('mouseLngLat', { static: true }) mouseLngLat: ElementRef + @ViewChild('customAttribution', { static: true }) customAttributionElement: ElementRef - // XXX: temporary, for keeping map-controls.component.ts happy - map: Map + // XXX: temporary, for keeping map-controls.component.ts happy + map: Map - zoom: number = this.configService.conf.value.bsData?.map['zoom'] - pitch: number = this.configService.conf.value.bsData?.map['pitch'] - lat: number = this.configService.conf.value.bsData?.map['lat'] - lng: number = this.configService.conf.value.bsData?.map['lng'] - bearing: number = this.configService.conf.value.bsData?.map['bearing'] - globalAttribution: string = this.configService.conf.value.bsData?.map['attribution'] + zoom: number = this.configService.conf.value.bsData?.map['zoom'] + pitch: number = this.configService.conf.value.bsData?.map['pitch'] + lat: number = this.configService.conf.value.bsData?.map['lat'] + lng: number = this.configService.conf.value.bsData?.map['lng'] + bearing: number = this.configService.conf.value.bsData?.map['bearing'] + globalAttribution: string = this.configService.conf.value.bsData?.map['attribution'] - baseStyle: BaseStyle = new BaseStyle('None') - protected _baseStyleName: string = this.configService.conf.value.bsData?.map['style'] - protected _bottom: number = 0 + baseStyle: BaseStyle = new BaseStyle('None') + protected _baseStyleName: string = this.configService.conf.value.bsData?.map['style'] + protected _bottom: number = 0 - protected canvas: HTMLElement + protected canvas: HTMLElement - protected selStart: Point - protected selAppend: boolean - protected selBox: HTMLDivElement + protected selStart: Point + protected selAppend: boolean + protected selBox: HTMLDivElement - geolocateTrackUserLocation = true - geolocateShowUserLocation = true - geolocatePositionOptions = { - "enableHighAccuracy": true - } - geolocateFitBoundsOptions: FitBoundsOptions = { - "maxZoom": 12, - } + geolocateTrackUserLocation = true + geolocateShowUserLocation = true + geolocatePositionOptions = { + "enableHighAccuracy": true + } + geolocateFitBoundsOptions: FitBoundsOptions = { + "maxZoom": 12, + } - popupOnFeature: string | null | number + popupOnFeature: string | null | number - layerDefs: object = {} - layers: object = {} - highlightedLayers: object = {} - originalBaseStyle: BaseStyle - pendingLayers: LayerWithMetaData[] = [] - subscribedLiveLayers: LayerNode[] = [] + layerDefs: object = {} + layers: object = {} + highlightedLayers: object = {} + originalBaseStyle: BaseStyle + pendingLayers: LayerWithMetaData[] = [] + subscribedLiveLayers: LayerNode[] = [] - protected _baseStyleOpacity: number - private _pendingBaseStyleOpacity: number - protected wss: object = {} // WebSocketSubject + protected _baseStyleOpacity: number + private _pendingBaseStyleOpacity: number + protected wss: object = {} // WebSocketSubject - symbolTextSize: ExpressionSpecification = [ - 'interpolate', ["linear"], ['zoom'], 14, 13, 20, 24 - ] + symbolTextSize: ExpressionSpecification = [ + 'interpolate', ["linear"], ['zoom'], 14, 13, 20, 24 + ] - constructor( - public configService: ConfigService, - protected route: ActivatedRoute, - protected router: Router, - protected ngZone: NgZone, - private cdr: ChangeDetectorRef, - public snackBar: MatSnackBar, - protected _geoJsonService: GeoJsonService, - protected _liveGeoJsonService: LiveGeoJsonService, - protected mapDataService: MapDataService, - public mapControlService: MapControlService, - protected infoDataService: InfoDataService, - ) {} + constructor( + public configService: ConfigService, + protected route: ActivatedRoute, + protected router: Router, + protected ngZone: NgZone, + private cdr: ChangeDetectorRef, + public snackBar: MatSnackBar, + protected _geoJsonService: GeoJsonService, + protected _liveGeoJsonService: LiveGeoJsonService, + protected mapDataService: MapDataService, + public mapControlService: MapControlService, + protected infoDataService: InfoDataService, + ) { } - @Input() - set baseStyleName(styleName: string) { - this._baseStyleName = styleName - this.mapDataService.getBaseStyle(styleName).subscribe( - (baseStyle: BaseStyle) => { - // XXX: this assumes mapbox map is loaded - this.originalBaseStyle = baseStyle - if (this.map) { - // Copy the base style for reference (eg. apply opacities) - let oldStyle = this.map.getStyle() - // Copy the layers to the new source - for (let store in this.layers) { - let highlightedStore = store + '-highlighted' - let tagsStore = store + '-tags' - let layer = oldStyle.layers.find(l => l.id == store) - let highlightedLayer = oldStyle.layers.find(l => l.id == highlightedStore) - let source = (this.map.getSource(store)).serialize() - let highlightedSource = (this.map.getSource(highlightedStore)).serialize() - baseStyle.style['sources'][store] = source - baseStyle.style['sources'][highlightedStore] = highlightedSource - //layer['source'] = source - //highlightedLayer['source'] = source - baseStyle.style['layers'].push(layer) - baseStyle.style['layers'].push(highlightedLayer) - let tagsSourceRaw = (this.map.getSource(tagsStore)) - if (tagsSourceRaw) { - let tagsLayer = oldStyle.layers.find(l => l.id == tagsStore) - baseStyle.style['sources'][tagsStore] = tagsSourceRaw.serialize() - baseStyle.style['layers'].push(tagsLayer) - } - } - setTimeout(() => this.applyBaseStyleOpacity(), 0) - } - this.baseStyle = baseStyle - this.cdr.markForCheck() + @Input() + set baseStyleName(styleName: string) { + this._baseStyleName = styleName + this.mapDataService.getBaseStyle(styleName).subscribe( + (baseStyle: BaseStyle) => { + // XXX: this assumes mapbox map is loaded + this.originalBaseStyle = baseStyle + if (this.map) { + // Copy the base style for reference (eg. apply opacities) + let oldStyle = this.map.getStyle() + // Copy the layers to the new source + for (let store in this.layers) { + let highlightedStore = store + '-highlighted' + let tagsStore = store + '-tags' + let layer = oldStyle.layers.find(l => l.id == store) + let highlightedLayer = oldStyle.layers.find(l => l.id == highlightedStore) + let source = (this.map.getSource(store)).serialize() + let highlightedSource = (this.map.getSource(highlightedStore)).serialize() + baseStyle.style['sources'][store] = source + baseStyle.style['sources'][highlightedStore] = highlightedSource + //layer['source'] = source + //highlightedLayer['source'] = source + baseStyle.style['layers'].push(layer) + baseStyle.style['layers'].push(highlightedLayer) + let tagsSourceRaw = (this.map.getSource(tagsStore)) + if (tagsSourceRaw) { + let tagsLayer = oldStyle.layers.find(l => l.id == tagsStore) + baseStyle.style['sources'][tagsStore] = tagsSourceRaw.serialize() + baseStyle.style['layers'].push(tagsLayer) } - ) - } - get baseStyleName() { - return this._baseStyleName - } - - @Input() - get permaLink() { - if (!this.map) return - let baseUrl = window.location.origin + '/' + 'map' + '?' - let center = this.map.getCenter() - let data = {} - let ret = [] - data['lat'] = center['lat'].toFixed(6) - data['lng'] = center['lng'].toFixed(6) - data['zoom'] = this.map.getZoom().toFixed(2) - data['pitch'] = this.map.getPitch().toFixed(1) - data['bearing'] = this.map.getBearing().toFixed(1) - data['baseStyleName'] = this.baseStyleName - data['baseStyleOpacity'] = this.baseStyleOpacity.toFixed(2) - data['layers'] = this.getLayersForUrl().join(',') - for (let d in data) { - ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d])) + } + setTimeout(() => this.applyBaseStyleOpacity(), 0) } - return baseUrl + ret.join('&') - } + this.baseStyle = baseStyle + this.cdr.markForCheck() + } + ) + } + get baseStyleName() { + return this._baseStyleName + } - protected getLayersForUrl(): string[] { - return Object.keys(this.layers) + @Input() + get permaLink() { + if (!this.map) return + let baseUrl = window.location.origin + '/' + 'map' + '?' + let center = this.map.getCenter() + let data = {} + let ret = [] + data['lat'] = center['lat'].toFixed(6) + data['lng'] = center['lng'].toFixed(6) + data['zoom'] = this.map.getZoom().toFixed(2) + data['pitch'] = this.map.getPitch().toFixed(1) + data['bearing'] = this.map.getBearing().toFixed(1) + data['baseStyleName'] = this.baseStyleName + data['baseStyleOpacity'] = this.baseStyleOpacity.toFixed(2) + data['layers'] = this.getLayersForUrl().join(',') + for (let d in data) { + ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d])) } + return baseUrl + ret.join('&') + } - @Input() - set baseStyleOpacity(opacity: number) { - if (opacity == this._baseStyleOpacity) return - if (!this.map) { - if (!this._pendingBaseStyleOpacity) { - this._pendingBaseStyleOpacity = opacity - } - return - } - this._baseStyleOpacity = opacity - this.applyBaseStyleOpacity() - } - get baseStyleOpacity(): number { - return this._baseStyleOpacity - } + protected getLayersForUrl(): string[] { + return Object.keys(this.layers) + } - _getNewSingleOpacity(layerId: string, originalOpacity: number | object): number | object { - if (typeof(originalOpacity) === 'number') { - return originalOpacity * this._baseStyleOpacity + @Input() + set baseStyleOpacity(opacity: number) { + if (opacity == this._baseStyleOpacity) return + if (!this.map) { + if (!this._pendingBaseStyleOpacity) { + this._pendingBaseStyleOpacity = opacity + } + return + } + this._baseStyleOpacity = opacity + this.applyBaseStyleOpacity() + } + get baseStyleOpacity(): number { + return this._baseStyleOpacity + } + + _getNewSingleOpacity(layerId: string, originalOpacity: number | object): number | object { + if (typeof (originalOpacity) === 'number') { + return originalOpacity * this._baseStyleOpacity + } + else { + let newOpacity = {} + for (const k in originalOpacity) { + let v = originalOpacity[k] + if (k == 'stops') { + newOpacity[k] = v.map(stop => [stop[0], stop[1] * this._baseStyleOpacity]) } else { - let newOpacity = {} - for (const k in originalOpacity) { - let v = originalOpacity[k] - if (k == 'stops') { - newOpacity[k] = v.map(stop => [stop[0], stop[1] * this._baseStyleOpacity]) - } - else { - newOpacity[k] = v - } - } - return newOpacity + newOpacity[k] = v } + } + return newOpacity } + } - _getNewOpacity(layer: object): object { - let originalStyle = this.originalBaseStyle.style['layers'].find( - (_layer: object) => layer['id'] == _layer['id'] - ) - if (!('paint' in originalStyle)) { - originalStyle['paint'] = {} + _getNewOpacity(layer: object): object { + let originalStyle = this.originalBaseStyle.style['layers'].find( + (_layer: object) => layer['id'] == _layer['id'] + ) + if (!('paint' in originalStyle)) { + originalStyle['paint'] = {} + } + if (['raster', 'background', 'fill', 'line'].indexOf(layer['type']) != -1) { + let prop = layer['type'] + '-opacity' + return { + [prop]: this._getNewSingleOpacity(layer['id'], originalStyle['paint'][prop] || 1.0) + } + } + else { + let prop1 = 'text-opacity' + let prop2 = 'icon-opacity' + let newOpacity1 = this._getNewSingleOpacity(layer['id'], originalStyle['paint'][prop1] || 1.0) + let newOpacity2 = this._getNewSingleOpacity(layer['id'], originalStyle['paint'][prop2] || 1.0) + return { + [prop1]: newOpacity1, + [prop2]: newOpacity2, + } + } + } + + applyBaseStyleOpacity() { + for (let bsLayer of this.baseStyle.style['layers']) { + // Skip our layers + if (this.layers[bsLayer['id']] || bsLayer['id'].endsWith('-highlighted') || bsLayer['id'].endsWith('-tags') || bsLayer['id'].endsWith('-labels')) { + continue + } + for (const [key, value] of Object.entries(this._getNewOpacity(bsLayer))) { + this.map.setPaintProperty(bsLayer.id, key, value) + } + } + } + + /* + * For reference: + routeQueryParamSetter = ['lat', 'lng', 'zoom', 'pitch', 'baseStyleName', 'bearing', 'baseStyleOpacity', 'layers'] + */ + + ngOnInit() { + this.route.queryParams.subscribe( + (params: Params) => { + if ('lat' in params) { + this.lat = parseFloat(params['lat']) } - if (['raster', 'background', 'fill', 'line'].indexOf(layer['type']) != -1) { - let prop = layer['type'] + '-opacity' - return { - [prop]: this._getNewSingleOpacity(layer['id'], originalStyle['paint'][prop] || 1.0) - } + if ('lng' in params) { + this.lng = parseFloat(params['lng']) + } + if ('zoom' in params) { + this.zoom = parseFloat(params['zoom']) + } + if ('pitch' in params) { + this.pitch = parseFloat(params['pitch']) + } + if ('bearing' in params) { + this.bearing = parseFloat(params['bearing']) + } + if ('baseStyleOpacity' in params) { + this._pendingBaseStyleOpacity = parseFloat(params['baseStyleOpacity']) + } + if ('baseStyleName' in params) { + this.baseStyleName = params['baseStyleName'] } else { - let prop1 = 'text-opacity' - let prop2 = 'icon-opacity' - let newOpacity1 = this._getNewSingleOpacity(layer['id'], originalStyle['paint'][prop1] || 1.0) - let newOpacity2 = this._getNewSingleOpacity(layer['id'], originalStyle['paint'][prop2] || 1.0) - return { - [prop1]: newOpacity1, - [prop2]: newOpacity2, - } + this.baseStyleName = this._baseStyleName } + if ('layers' in params) { + this.mapControlService.mapReady$.subscribe( + _ => { + this.mapControlService.addBaseMapLayers( + params['layers'].split(',') + ) + } + ) + } + if ('show' in params) { + let featureRef: string[] = params['show'].split(':') + let store = featureRef[0] + let field = featureRef[1] + let value = featureRef[2] + this.mapControlService.mapReady$.subscribe( + _ => this.mapControlService.showFeature.next( + new FeatureWithField(store, field, value) + ) + ) + } + this.cdr.markForCheck() + } + ) + + /*** Subscriptions ***/ + + this.mapControlService.baseStyleOpacity$.subscribe( + baseStyleOpacity => { + this.baseStyleOpacity = baseStyleOpacity + } + ) + + this.mapControlService.baseStyleName$.subscribe( + baseStyleName => { + this.baseStyleName = baseStyleName + } + ) + + this.mapControlService.layerAdd$.subscribe( + (layerNode: LayerNode) => { + layerNode.mapSubscription = this.addLayer(layerNode).subscribe( + _ => { + this.setAttributions() + this.mapControlService.layerLoaded.next(layerNode) + } + ) + } + ) + + this.mapControlService.layerRemove$.subscribe( + (layerNode: LayerNode) => { + this.removeLayer(layerNode) + layerNode.cancelDownload() + } + ) + + this.mapControlService.filter$.subscribe( + ([searchText, taggedFeatures, taggedFeatureText]) => { + this.filter(searchText, taggedFeatures, taggedFeatureText) + } + ) + + this.mapControlService.resize$.subscribe( + flag => { + this.map && this.map.resize() + } + ) + + this.mapControlService.zoomToFeatures$.subscribe( + flag => { + this.zoomToFeatures() + } + ) + + this.mapControlService.hasLabels$.subscribe( + flag => { + if (flag) { + this.addLabelsLayers(this.mapControlService.selectedLayers) + } + else { + this.removeLabelsLayers() + } + } + ) + this.mapControlService.hasTags$.subscribe( + flag => { + if (flag) { + this.infoDataService.getTaggedLayers( + this.mapControlService.selectedLayers, + true + ).subscribe( + (taggedLayers: TaggedLayer[]) => this.addTagsLayers(taggedLayers) + ) + } + else { + this.removeTaggedLayers() + } + } + ) + + this.mapControlService.status$.subscribe( + status => { + this.filterByStatus(status) + } + ) + + this.mapControlService.tempSelection$.subscribe( + features => { + for (let layerName in this.highlightedLayers) { + this.highlightFeatures(this.layers[layerName], features[layerName]) + } + } + ) + + this.mapControlService.selection$.subscribe( + features => { + // Verse tempSelection to selection + this.mapControlService.tempSelection.next(features) + } + ) + + // Got tags, add layers + this.infoDataService.taggedLayers$.subscribe( + taggedLayers => this.addTagsLayers(taggedLayers) + ) + } + + onLoad(map: Map) { + this.map = map + if (!this.map) { + console.error('Something quite wrong with the map init') + return + } + this.map.on('mousemove', evt => this.onMove(evt)) + this.map.on('mouseleave', evt => this.onLeave(evt)) + this.map.getCanvasContainer().addEventListener('keydown', evt => this.onKeyDown(evt)) + if (this._pendingBaseStyleOpacity) { + this.mapControlService.baseStyleOpacitySlider.next(this._pendingBaseStyleOpacity) + this.baseStyleOpacity = this._pendingBaseStyleOpacity + this._pendingBaseStyleOpacity = undefined + } + else { + this.applyBaseStyleOpacity() + } + // Set up a trigger for adding more layers + this.map.on('style.load', _ => this.addLayers()) + this.pendingLayers.forEach(layerAndDef => this.addLayerOnMap(layerAndDef)) + this.canvas = map.getCanvasContainer() + + // Trigger the event / observable + this.mapControlService.mapLoaded.complete() + } + + /* + * Feature selection (with alt key down) + */ + + private _mousePos(evt): Point { + var rect = this.canvas.getBoundingClientRect(); + return new Point( + evt.clientX - rect.left - this.canvas.clientLeft, + evt.clientY - rect.top - this.canvas.clientTop + ) + } + + onMouseDown(evt) { + // Continue the rest of the function if the altKey is pressed. + if (!(evt.originalEvent.altKey && evt.originalEvent.button === 0 && this.canvas)) return + + // Disable default drag zooming when the altKey is held down. + this.map.dragPan.disable() + + // Capture the first xy coordinates + this.selStart = this._mousePos(evt.originalEvent) + + // Make sure that the selBox is initiated (takes care of case of a single click) + this.onMouseMove(evt) + + // Remember if the new selBox should append to the current selection + this.selAppend = evt.originalEvent.shiftKey + } + + onMouseMove(evt) { + // Capture the ongoing xy coordinates + if (!this.inSelectionMode()) return + + // Append the selBox element if it doesnt exist + if (!this.selBox) { + this.selBox = document.createElement('div') + this.selBox.classList.add('boxdraw') + this.canvas.appendChild(this.selBox) } - applyBaseStyleOpacity() { - for (let bsLayer of this.baseStyle.style['layers']) { - // Skip our layers - if (this.layers[bsLayer['id']] || bsLayer['id'].endsWith('-highlighted') || bsLayer['id'].endsWith('-tags') || bsLayer['id'].endsWith('-labels')) { - continue - } - for (const [key, value] of Object.entries(this._getNewOpacity(bsLayer))) { - this.map.setPaintProperty(bsLayer.id, key, value) - } - } + let selCurrent: Point = this._mousePos(evt.originalEvent) + var minX = Math.min(this.selStart.x, selCurrent.x), + maxX = Math.max(this.selStart.x, selCurrent.x), + minY = Math.min(this.selStart.y, selCurrent.y), + maxY = Math.max(this.selStart.y, selCurrent.y) + + // Adjust width and xy position of the selBox element ongoing + var pos = 'translate(' + minX + 'px,' + minY + 'px)' + this.selBox.style.transform = pos + this.selBox.style.webkitTransform = pos + this.selBox.style.width = maxX - minX + 'px' + this.selBox.style.height = maxY - minY + 'px' + this.selectFeaturesInBbox(this.selStart, this._mousePos(evt.originalEvent)) + } + + onMouseUp(evt) { + // Capture xy coordinates + if (this.inSelectionMode()) { + this.selFinish() + } + } + + inSelectionMode() { + return !!this.selStart + } + + onKeyDown(evt) { + // If the ESC key is pressed + if (evt.keyCode === 27) { + this.mapControlService.selectFeatures([]) + this.mapControlService.selectFeaturesTemp([]) + if (this.inSelectionMode()) { + this.selFinish() + } + } + } + + selectFeaturesInBbox(point1: Point, point2: Point) { + let featureList = this.map.queryRenderedFeatures( + [point1, point2], + { layers: Object.keys(this.layers) } + ) + + // XXX: there should be a better method to build the dict of selected features, eg. map-reduce + let features: object = {} + for (let feature of featureList) { + if (!features.hasOwnProperty(feature.layer.id)) { + features[feature.layer.id] = new Set() + } + features[feature.layer.id].add(feature.id || feature.properties['id']) } + if (this.selAppend) { + let previousSel = this.mapControlService.selection.getValue() + for (let layerName in previousSel) { + if (features.hasOwnProperty(layerName)) { + features[layerName] = new Set([ + ...features[layerName], + ...previousSel[layerName] || [] + ]) + } + else { + features[layerName] = previousSel[layerName] + } + } + } + // Emit an event with the features, for the info panel and highlight layers + this.mapControlService.selectFeaturesTemp(features) + } + + selFinish() { + this.mapControlService.selectFeatures(this.mapControlService.tempSelection.getValue()) + if (this.selBox) { + this.selBox.parentNode.removeChild(this.selBox); + this.selBox = null; + this.selStart = null; + } + this.map.dragPan.enable(); + } + + ngOnDestroy() { + //this.map.removeHash() + // Remove tagged layers + this.infoDataService.taggedLayers.next([]) + } + + disconnectLive() { + this._liveGeoJsonService.disconnect() + } + + addLayers() { + // Add the layers already selected + for (let layerName in this.layers) { + let layer: LayerSpecification = this.layers[layerName] + this.ngZone.runOutsideAngular( + () => { + this.map.addLayer(layer, this.getLayerNameBefore(this.layerDefs[layerName])) + } + ) + } + } + + getLayerNameBefore(layerDef) { + if (!layerDef.zIndex) { + return + } + let layersAfter = Object.keys(this.layerDefs) + .map(k => this.layerDefs[k]) + .filter(l => l.zIndex && (l.zIndex > layerDef.zIndex)) + .sort((l1, l2) => l1.zIndex - l2.zIndex) + if (layersAfter.length > 0) { + return layersAfter[0].store + } + } + + setAttributions() { + const attributions = Object.values(this.layers).map(l => l['attribution']) + const uniques = [...new Set(attributions)] + this.customAttributionElement.nativeElement.innerHTML = uniques.join(', ') + } + + addLayer(layerNode: LayerNode, simplify: number = 0.2): Observable { /* - * For reference: - routeQueryParamSetter = ['lat', 'lng', 'zoom', 'pitch', 'baseStyleName', 'bearing', 'baseStyleOpacity', 'layers'] + * All the hard work formatting the data and style to MapBox */ - - ngOnInit() { - this.route.queryParams.subscribe( - (params: Params) => { - if ('lat' in params) { - this.lat = parseFloat(params['lat']) - } - if ('lng' in params) { - this.lng = parseFloat(params['lng']) - } - if ('zoom' in params) { - this.zoom = parseFloat(params['zoom']) - } - if ('pitch' in params) { - this.pitch = parseFloat(params['pitch']) - } - if ('bearing' in params) { - this.bearing = parseFloat(params['bearing']) - } - if ('baseStyleOpacity' in params) { - this._pendingBaseStyleOpacity = parseFloat(params['baseStyleOpacity']) - } - if ('baseStyleName' in params) { - this.baseStyleName = params['baseStyleName'] - } - else { - this.baseStyleName = this._baseStyleName - } - if ('layers' in params) { - this.mapControlService.mapReady$.subscribe( - _ => { - this.mapControlService.addBaseMapLayers( - params['layers'].split(',') - ) - } - ) - } - if ('show' in params) { - let featureRef: string[] = params['show'].split(':') - let store = featureRef[0] - let field = featureRef[1] - let value = featureRef[2] - this.mapControlService.mapReady$.subscribe( - _ => this.mapControlService.showFeature.next( - new FeatureWithField(store, field, value) - ) - ) - } - this.cdr.markForCheck() - } - ) - - /*** Subscriptions ***/ - - this.mapControlService.baseStyleOpacity$.subscribe( - baseStyleOpacity => { - this.baseStyleOpacity = baseStyleOpacity - } - ) - - this.mapControlService.baseStyleName$.subscribe( - baseStyleName => { - this.baseStyleName = baseStyleName - } - ) - - this.mapControlService.layerAdd$.subscribe( - (layerNode: LayerNode) => { - layerNode.mapSubscription = this.addLayer(layerNode).subscribe( - _ => { - this.setAttributions() - this.mapControlService.layerLoaded.next(layerNode) - } - ) - } - ) - - this.mapControlService.layerRemove$.subscribe( - (layerNode: LayerNode) => { - this.removeLayer(layerNode) - layerNode.cancelDownload() - } - ) - - this.mapControlService.filter$.subscribe( - ([searchText, taggedFeatures, taggedFeatureText]) => { - this.filter(searchText, taggedFeatures, taggedFeatureText) - } - ) - - this.mapControlService.resize$.subscribe( - flag => { - this.map && this.map.resize() - } - ) - - this.mapControlService.zoomToFeatures$.subscribe( - flag => { - this.zoomToFeatures() - } - ) - - this.mapControlService.hasLabels$.subscribe( - flag => { - if (flag) { - this.addLabelsLayers(this.mapControlService.selectedLayers) - } - else { - this.removeLabelsLayers() - } - } - ) - this.mapControlService.hasTags$.subscribe( - flag => { - if (flag) { - this.infoDataService.getTaggedLayers( - this.mapControlService.selectedLayers, - true - ).subscribe( - (taggedLayers: TaggedLayer[]) => this.addTagsLayers(taggedLayers) - ) - } - else { - this.removeTaggedLayers() - } - } - ) - - this.mapControlService.status$.subscribe( - status => { - this.filterByStatus(status) - } - ) - - this.mapControlService.tempSelection$.subscribe( - features => { - for (let layerName in this.highlightedLayers) { - this.highlightFeatures(this.layers[layerName], features[layerName]) - } - } - ) - - this.mapControlService.selection$.subscribe( - features => { - // Verse tempSelection to selection - this.mapControlService.tempSelection.next(features) - } - ) - - // Got tags, add layers - this.infoDataService.taggedLayers$.subscribe( - taggedLayers => this.addTagsLayers(taggedLayers) - ) - } - - onLoad(map: Map) { - this.map = map - if (!this.map) { - console.error('Something quite wrong with the map init') - return + let layerDef = layerNode + return this._geoJsonService.getAll(layerNode.getUrl(), layerNode.store, { + simplify: String(simplify) + }).pipe(map( + (resp: MapboxDataAndStyle) => { + let data = resp.data + if (!data['features'] || data['features'].length == 0) { + console.log('Empty layer', layerDef.store) + return } - this.map.on('mousemove', evt => this.onMove(evt)) - this.map.on('mouseleave', evt => this.onLeave(evt)) - this.map.getCanvasContainer().addEventListener('keydown', evt => this.onKeyDown(evt)) - if (this._pendingBaseStyleOpacity) { - this.mapControlService.baseStyleOpacitySlider.next(this._pendingBaseStyleOpacity) - this.baseStyleOpacity = this._pendingBaseStyleOpacity - this._pendingBaseStyleOpacity = undefined + let mapboxPaint = resp.style?.paint + let mapboxLayout = resp.style?.layout + this.layerDefs[layerDef.store] = layerDef + + // Make sure ids are not in properties (like postGis to_GeoJson) + // because Mapbox doesn't like strings as ids + if (!data['features'][0]['id'] && data['features'][0]['properties']['id']) { + /* + data['features'].forEach( + (feature:Object) => { + feature['id'] = feature['properties']['id'] + } + ) + */ } else { - this.applyBaseStyleOpacity() - } - // Set up a trigger for adding more layers - this.map.on('style.load', _ => this.addLayers()) - this.pendingLayers.forEach(layerAndDef => this.addLayerOnMap(layerAndDef)) - this.canvas = map.getCanvasContainer() - - // Trigger the event / observable - this.mapControlService.mapLoaded.complete() - } - - /* - * Feature selection (with alt key down) - */ - - private _mousePos(evt): Point { - var rect = this.canvas.getBoundingClientRect(); - return new Point( - evt.clientX - rect.left - this.canvas.clientLeft, - evt.clientY - rect.top - this.canvas.clientTop - ) - } - - onMouseDown(evt) { - // Continue the rest of the function if the altKey is pressed. - if (!(evt.originalEvent.altKey && evt.originalEvent.button === 0 && this.canvas)) return - - // Disable default drag zooming when the altKey is held down. - this.map.dragPan.disable() - - // Capture the first xy coordinates - this.selStart = this._mousePos(evt.originalEvent) - - // Make sure that the selBox is initiated (takes care of case of a single click) - this.onMouseMove(evt) - - // Remember if the new selBox should append to the current selection - this.selAppend = evt.originalEvent.shiftKey - } - - onMouseMove(evt) { - // Capture the ongoing xy coordinates - if (!this.inSelectionMode()) return - - // Append the selBox element if it doesnt exist - if (!this.selBox) { - this.selBox = document.createElement('div') - this.selBox.classList.add('boxdraw') - this.canvas.appendChild(this.selBox) - } - - let selCurrent: Point = this._mousePos(evt.originalEvent) - var minX = Math.min(this.selStart.x, selCurrent.x), - maxX = Math.max(this.selStart.x, selCurrent.x), - minY = Math.min(this.selStart.y, selCurrent.y), - maxY = Math.max(this.selStart.y, selCurrent.y) - - // Adjust width and xy position of the selBox element ongoing - var pos = 'translate(' + minX + 'px,' + minY + 'px)' - this.selBox.style.transform = pos - this.selBox.style.webkitTransform = pos - this.selBox.style.width = maxX - minX + 'px' - this.selBox.style.height = maxY - minY + 'px' - this.selectFeaturesInBbox(this.selStart, this._mousePos(evt.originalEvent)) - } - - onMouseUp(evt) { - // Capture xy coordinates - if (this.inSelectionMode()) { - this.selFinish() - } - } - - inSelectionMode() { - return !!this.selStart - } - - onKeyDown(evt) { - // If the ESC key is pressed - if (evt.keyCode === 27) { - this.mapControlService.selectFeatures([]) - this.mapControlService.selectFeaturesTemp([]) - if (this.inSelectionMode()) { - this.selFinish() + data['features'].forEach( + (feature: object) => { + feature['properties']['id'] = feature['id'] } + ) } - } - selectFeaturesInBbox(point1: Point, point2: Point) { - let featureList = this.map.queryRenderedFeatures( - [point1, point2], - {layers: Object.keys(this.layers)} + // Add a property to each feature, keeping track of selection + data['features'].forEach( + (feature: object) => feature['properties']['_selected'] = false ) - // XXX: there should be a better method to build the dict of selected features, eg. map-reduce - let features: object = {} - for (let feature of featureList) { - if (!features.hasOwnProperty(feature.layer.id)) { - features[feature.layer.id] = new Set() - } - features[feature.layer.id].add(feature.id || feature.properties['id']) + let layer: LayerSpecification = { + id: layerDef.store, + type: layerDef.type, + source: { + type: "geojson", + data: data + }, + attribution: resp.style?.attribution + } + this.layers[layerDef.store] = layer + + let highlightedLayer: LayerSpecification = { + id: layerDef.store + '-highlighted', + type: layerDef.type, + source: { + type: "geojson", + data: data + }, + filter: ["==", "_selected", true] + } + this.highlightedLayers[layerDef.store] = highlightedLayer + + let layerX = new LayerWithMetaData(layer, highlightedLayer, layerNode) + + if (layerDef.type == 'symbol') { + layer['layout'] = { + 'text-line-height': 1, + 'text-padding': 0, + 'text-allow-overlap': true, + 'text-field': layerDef.symbol || '\ue32b', + 'icon-optional': true, + 'text-font': ['GisafSymbols'], + 'text-size': this.symbolTextSize, + } + layer['paint'] = { + 'text-translate-anchor': 'viewport', + 'text-color': '#000000' + } + highlightedLayer['layout'] = { + 'text-line-height': 1, + 'text-padding': 0, + 'text-allow-overlap': true, + 'text-field': layerDef.symbol || '\ue32b', + 'icon-optional': true, + 'text-font': ['GisafSymbols'], + 'text-size': this.symbolTextSize, + } + highlightedLayer['paint'] = { + 'text-translate-anchor': 'viewport', + 'text-color': 'red' + } + } + else if (layerDef.type == 'fill') { + layer['paint'] = { + 'fill-color': 'blue', + 'fill-opacity': 0.50, + } + highlightedLayer['paint'] = { + 'fill-color': 'red', + 'fill-opacity': 0.70, + } + } + else if (layerDef.type == 'line') { + layer['paint'] = { + 'line-color': 'red', + 'line-opacity': 0.70, + 'line-width': 2, + 'line-blur': 0.5, + } + highlightedLayer['paint'] = { + 'line-color': 'red', + 'line-opacity': 0.90, + 'line-width': 2, + 'line-blur': 0.5, + } + } + else if (layerDef.type == 'circle') { + layer['paint'] = { + 'circle-blur': 0.5, + 'circle-opacity': 0.7, + 'circle-color': '#404', + 'circle-radius': 10, + } + highlightedLayer['paint'] = { + 'circle-blur': 0.5, + 'circle-opacity': 0.9, + 'circle-color': 'red', + 'circle-radius': 10, + } + } + else if (layerDef.type == 'fill-extrusion') { + layer['paint'] = { + 'fill-extrusion-height': 5, + 'fill-extrusion-opacity': 0.7, + } + highlightedLayer['paint'] = { + 'fill-extrusion-height': (mapboxPaint && mapboxPaint['fill-extrusion-height']) || 5, + 'fill-extrusion-opacity': 0.9, + 'fill-extrusion-color': 'red', + } } - if (this.selAppend) { - let previousSel = this.mapControlService.selection.getValue() - for (let layerName in previousSel) { - if (features.hasOwnProperty(layerName)) { - features[layerName] = new Set([ - ...features[layerName], - ...previousSel[layerName] || [] - ]) - } - else { - features[layerName] = previousSel[layerName] - } - } + if (mapboxPaint) { + layer['paint'] = mapboxPaint } - // Emit an event with the features, for the info panel and highlight layers - this.mapControlService.selectFeaturesTemp(features) - } - - selFinish() { - this.mapControlService.selectFeatures(this.mapControlService.tempSelection.getValue()) - if (this.selBox) { - this.selBox.parentNode.removeChild(this.selBox); - this.selBox = null; - this.selStart = null; + if (mapboxLayout) { + layer['layout'] = mapboxLayout } - this.map.dragPan.enable(); - } - ngOnDestroy() { - //this.map.removeHash() - // Remove tagged layers - this.infoDataService.taggedLayers.next([]) - } - - disconnectLive() { - this._liveGeoJsonService.disconnect() - } - - addLayers() { - // Add the layers already selected - for (let layerName in this.layers) { - let layer: LayerSpecification = this.layers[layerName] - this.ngZone.runOutsideAngular( - () => { - this.map.addLayer(layer, this.getLayerNameBefore(this.layerDefs[layerName])) - } - ) - } - } - - getLayerNameBefore(layerDef) { - if (!layerDef.zIndex) { - return - } - let layersAfter = Object.keys(this.layerDefs) - .map(k => this.layerDefs[k]) - .filter(l => l.zIndex && (l.zIndex > layerDef.zIndex)) - .sort((l1, l2) => l1.zIndex - l2.zIndex) - if (layersAfter.length > 0) { - return layersAfter[0].store - } - } - - setAttributions() { - const attributions = Object.values(this.layers).map(l => l['attribution']) - const uniques = [...new Set(attributions)] - this.customAttributionElement.nativeElement.innerHTML = uniques.join(', ') - } - - addLayer(layerNode: LayerNode, simplify: number = 0.2): Observable { - /* - * All the hard work formatting the data and style to MapBox - */ - let layerDef = layerNode - return this._geoJsonService.getAll(layerNode.getUrl(), layerNode.store, { - simplify: String(simplify) - }).pipe(map( - (resp: MapboxDataAndStyle) => { - let data = resp.data - if (!data['features'] || data['features'].length == 0) { - console.log('Empty layer', layerDef.store) - return - } - let mapboxPaint = resp.style?.paint - let mapboxLayout = resp.style?.layout - this.layerDefs[layerDef.store] = layerDef - - // Make sure ids are not in properties (like postGis to_GeoJson) - // because Mapbox doesn't like strings as ids - if (!data['features'][0]['id'] && data['features'][0]['properties']['id']) { - /* - data['features'].forEach( - (feature:Object) => { - feature['id'] = feature['properties']['id'] - } - ) - */ - } - else { - data['features'].forEach( - (feature:object) => { - feature['properties']['id'] = feature['id'] - } - ) - } - - // Add a property to each feature, keeping track of selection - data['features'].forEach( - (feature:object) => feature['properties']['_selected'] = false - ) - - let layer: LayerSpecification = { - id: layerDef.store, - type: layerDef.type, - source: { - type: "geojson", - data: data - }, - attribution: resp.style?.attribution - } - this.layers[layerDef.store] = layer - - let highlightedLayer: LayerSpecification = { - id: layerDef.store + '-highlighted', - type: layerDef.type, - source: { - type: "geojson", - data: data - }, - filter: ["==", "_selected", true] - } - this.highlightedLayers[layerDef.store] = highlightedLayer - - let layerX = new LayerWithMetaData(layer, highlightedLayer, layerNode) - - if (layerDef.type == 'symbol') { - layer['layout'] = { - 'text-line-height': 1, - 'text-padding': 0, - 'text-allow-overlap': true, - 'text-field': layerDef.symbol || '\ue32b', - 'icon-optional': true, - 'text-font': ['GisafSymbols'], - 'text-size': this.symbolTextSize, - } - layer['paint'] = { - 'text-translate-anchor': 'viewport', - 'text-color': '#000000' - } - highlightedLayer['layout'] = { - 'text-line-height': 1, - 'text-padding': 0, - 'text-allow-overlap': true, - 'text-field': layerDef.symbol || '\ue32b', - 'icon-optional': true, - 'text-font': ['GisafSymbols'], - 'text-size': this.symbolTextSize, - } - highlightedLayer['paint'] = { - 'text-translate-anchor': 'viewport', - 'text-color': 'red' - } - } - else if (layerDef.type == 'fill') { - layer['paint'] = { - 'fill-color': 'blue', - 'fill-opacity': 0.50, - } - highlightedLayer['paint'] = { - 'fill-color': 'red', - 'fill-opacity': 0.70, - } - } - else if (layerDef.type == 'line') { - layer['paint'] = { - 'line-color': 'red', - 'line-opacity': 0.70, - 'line-width': 2, - 'line-blur': 0.5, - } - highlightedLayer['paint'] = { - 'line-color': 'red', - 'line-opacity': 0.90, - 'line-width': 2, - 'line-blur': 0.5, - } - } - else if (layerDef.type == 'circle') { - layer['paint'] = { - 'circle-blur': 0.5, - 'circle-opacity': 0.7, - 'circle-color': '#404', - 'circle-radius': 10, - } - highlightedLayer['paint'] = { - 'circle-blur': 0.5, - 'circle-opacity': 0.9, - 'circle-color': 'red', - 'circle-radius': 10, - } - } - else if (layerDef.type == 'fill-extrusion') { - layer['paint'] = { - 'fill-extrusion-height': 5, - 'fill-extrusion-opacity': 0.7, - } - highlightedLayer['paint'] = { - 'fill-extrusion-height': (mapboxPaint && mapboxPaint['fill-extrusion-height']) || 5, - 'fill-extrusion-opacity': 0.9, - 'fill-extrusion-color': 'red', - } - } - - if (mapboxPaint) { - layer['paint'] = mapboxPaint - } - if (mapboxLayout) { - layer['layout'] = mapboxLayout - } - - if (this.map) { - this.addLayerOnMap(layerX) - } - else { - this.pendingLayers.push(layerX) - } - - if (this.mapControlService.hasLabels.value) { - this.addLabelsLayer(layerDef) - } - - if (this.mapControlService.hasTags.value) { - this.getAndAddTagsLayer(layerDef) - } - } - )) - } - - zoomToAndInfoFeature(layerX: LayerWithMetaData) { - // Zoom to the feature, if it's in the layer - let featuretoShow = this.mapControlService.showFeature.value - if (!featuretoShow) return - if (featuretoShow.store != layerX.layerNode.store) return - let id: string - let featureData: object[] - if (featuretoShow.field === 'id') { - id = featuretoShow.value - // XXX: remove typings (maplibre 2.x) - featureData = [(layerX.layer).source.data['features'].find( - (f: object) => f['id'] == id || f['properties']['id'] == id - )] + if (this.map) { + this.addLayerOnMap(layerX) } else { - featureData = (layerX.layer).source.data['features'].filter( - (f: object) => f['properties'][featuretoShow.field] == featuretoShow.value || f[featuretoShow.field] == featuretoShow.value - ) - // Select only the first matching id - id = featureData[0]['id'] + this.pendingLayers.push(layerX) } - // Zoom - if (featureData.length > 0 && !!featureData[0]) { - // Show info - this.mapControlService.featureClicked.next(new Feature(featuretoShow.store, id)) - - // Clear to prevent zooming again - this.mapControlService.showFeature.next(undefined) - - // XXX: zoomToFeatures should actually be called when the layer is loaded - // Using setTimeout as workaround - setTimeout( - () => this.zoomToFeatures(featureData), - 700 - ) + if (this.mapControlService.hasLabels.value) { + this.addLabelsLayer(layerDef) } + + if (this.mapControlService.hasTags.value) { + this.getAndAddTagsLayer(layerDef) + } + } + )) + } + + zoomToAndInfoFeature(layerX: LayerWithMetaData) { + // Zoom to the feature, if it's in the layer + let featuretoShow = this.mapControlService.showFeature.value + if (!featuretoShow) return + if (featuretoShow.store != layerX.layerNode.store) return + let id: string + let featureData: object[] + if (featuretoShow.field === 'id') { + id = featuretoShow.value + // XXX: remove typings (maplibre 2.x) + featureData = [(layerX.layer).source.data['features'].find( + (f: object) => f['id'] == id || f['properties']['id'] == id + )] + } + else { + featureData = (layerX.layer).source.data['features'].filter( + (f: object) => f['properties'][featuretoShow.field] == featuretoShow.value || f[featuretoShow.field] == featuretoShow.value + ) + // Select only the first matching id + id = featureData[0]['id'] } - addLayerOnMap(layerX: LayerWithMetaData) { - // Zoom and info of the feature to show (as per the router query), - // if that layer matches the store of that feature - this.zoomToAndInfoFeature(layerX) + // Zoom + if (featureData.length > 0 && !!featureData[0]) { + // Show info + this.mapControlService.featureClicked.next(new Feature(featuretoShow.store, id)) - if (!!this.map.getLayer(layerX.layerNode.store)) { - console.debug('Layer already on the map', layerX.layerNode) - return - } - this.ngZone.runOutsideAngular( - () => { - this.map.addLayer(layerX.layer, this.getLayerNameBefore(layerX.layerNode)) - this.map.addLayer(layerX.highlightedLayer) - } - ) - this.filterByStatus(this.mapControlService.status.value, layerX.layerNode.store, layerX.layerNode) + // Clear to prevent zooming again + this.mapControlService.showFeature.next(undefined) - // Register events for that layer - this.map.on('mouseenter', layerX.layer['id'], evt => this.onEnter(evt)) - this.map.on('mousemove', layerX.layer['id'], evt => this.onMoveOnLayer(evt)) - this.map.on('mouseleave', layerX.layer['id'], evt => this.onLeaveOnLayer(evt)) + // XXX: zoomToFeatures should actually be called when the layer is loaded + // Using setTimeout as workaround + setTimeout( + () => this.zoomToFeatures(featureData), + 700 + ) + } + } - // Live layer - if (layerX.layerNode.live) { - this.subscribedLiveLayers.push(layerX.layerNode) - this.addLiveLayerListener(layerX.layerNode) - } + addLayerOnMap(layerX: LayerWithMetaData) { + // Zoom and info of the feature to show (as per the router query), + // if that layer matches the store of that feature + this.zoomToAndInfoFeature(layerX) - this.setAttributions() + if (!!this.map.getLayer(layerX.layerNode.store)) { + console.debug('Layer already on the map', layerX.layerNode) + return + } + this.ngZone.runOutsideAngular( + () => { + this.map.addLayer(layerX.layer, this.getLayerNameBefore(layerX.layerNode)) + this.map.addLayer(layerX.highlightedLayer) + } + ) + this.filterByStatus(this.mapControlService.status.value, layerX.layerNode.store, layerX.layerNode) + + // Register events for that layer + this.map.on('mouseenter', layerX.layer['id'], evt => this.onEnter(evt)) + this.map.on('mousemove', layerX.layer['id'], evt => this.onMoveOnLayer(evt)) + this.map.on('mouseleave', layerX.layer['id'], evt => this.onLeaveOnLayer(evt)) + + // Live layer + if (layerX.layerNode.live) { + this.subscribedLiveLayers.push(layerX.layerNode) + this.addLiveLayerListener(layerX.layerNode) } - addLiveLayerListener(layerDef: LayerNode) { - let ws: WebSocketSubject = this._liveGeoJsonService.connect(layerDef.store) - this.wss[layerDef.store] = ws - ws.next({ - 'message': 'subscribeLiveLayer', - }) - ws.subscribe({ - next: (data: GeoJSON.FeatureCollection) => { - let source: Source = this.map.getSource(layerDef.store) - if (!source) { - // Unsubscribe from channel on server - console.warn('Live WS: cannot getSource for WS message: ', layerDef.store, data) - ws.next({ - 'message': 'unsubscribeLiveLayer', - }) - return - } - (source).setData(data) - }, - error: err => { - console.error('Websocket', layerDef.store, err) - //this.snackBar.open('Lost contact with Gisaf live server', 'OK') - } - }) - } + this.setAttributions() + } - removeLiveLayerListener(layerDef: LayerNode) { - let ws: WebSocketSubject = this.wss[layerDef.store] - if (!ws) { - return - } - ws.next({ + addLiveLayerListener(layerDef: LayerNode) { + let ws: WebSocketSubject = this._liveGeoJsonService.connect(layerDef.store) + this.wss[layerDef.store] = ws + ws.next({ + 'message': 'subscribeLiveLayer', + }) + ws.subscribe({ + next: (data: GeoJSON.FeatureCollection) => { + let source: Source = this.map.getSource(layerDef.store) + if (!source) { + // Unsubscribe from channel on server + console.warn('Live WS: cannot getSource for WS message: ', layerDef.store, data) + ws.next({ 'message': 'unsubscribeLiveLayer', - }) - delete this.wss[layerDef.store] + }) + return + } + (source).setData(data) + }, + error: err => { + console.error('Websocket', layerDef.store, err) + //this.snackBar.open('Lost contact with Gisaf live server', 'OK') + } + }) + } + + removeLiveLayerListener(layerDef: LayerNode) { + let ws: WebSocketSubject = this.wss[layerDef.store] + if (!ws) { + return + } + ws.next({ + 'message': 'unsubscribeLiveLayer', + }) + delete this.wss[layerDef.store] + } + + removeLayer(layerDef: LayerNode) { + // See https://github.com/mapbox/mapbox-gl-js/issues/4466 + this.removeLayersByIds([ + layerDef.store, + layerDef.store + '-tags', + layerDef.store + '-labels', + ]) + if (layerDef.store in this.highlightedLayers) { + this.removeLayersByIds([this.highlightedLayers[layerDef.store]['id']]) + } + delete this.layerDefs[layerDef.store] + delete this.layers[layerDef.store] + delete this.highlightedLayers[layerDef.store] + if (layerDef.live) { + this.removeLiveLayerListener(layerDef) + } + this.setAttributions() + } + + filter(searchText: string, taggedFeatures: TaggedLayer[], taggedFeatureText: string): void { + for (let layerName in this.layers) { + this.filterLayer(layerName, normalize(searchText), taggedFeatures, normalize(taggedFeatureText)) + } + } + + filterLayer(layerName: string, searchText: string, taggedFeatures: TaggedLayer[] = [], taggedFeatureText: string) { + let layer = this.layers[layerName] + let tagsLayerName = layerName + '-tags' + let labelsLayerName = layerName + '-labels' + let hasTagsLayer = !!this.map.getLayer(tagsLayerName) + let hasLabelsLayer = !!this.map.getLayer(labelsLayerName) + let filteredIds: any[] + + if (!searchText && !taggedFeatureText) { + // Clear all filters + this.map.setFilter(layerName, null) + hasTagsLayer && this.map.setFilter(tagsLayerName, null) + hasLabelsLayer && this.map.setFilter(labelsLayerName, null) + return } - removeLayer(layerDef: LayerNode) { - // See https://github.com/mapbox/mapbox-gl-js/issues/4466 - this.removeLayersByIds([ - layerDef.store, - layerDef.store + '-tags', - layerDef.store + '-labels', - ]) - if (layerDef.store in this.highlightedLayers) { - this.removeLayersByIds([this.highlightedLayers[layerDef.store]['id']]) - } - delete this.layerDefs[layerDef.store] - delete this.layers[layerDef.store] - delete this.highlightedLayers[layerDef.store] - if (layerDef.live) { - this.removeLiveLayerListener(layerDef) - } - this.setAttributions() + if (searchText) { + // TODO: regex matching (waiting for https://github.com/mapbox/mapbox-gl-js/pull/6228) + //this.map.setFilter(layerName, ['regex-test', 'popup', searchText]) + //this.map.setFilter(layerName, ['==', 'popup', searchText]) + // Layer from JSON + let filtered = layer.source.data.features.filter( + feature => normalize(feature.properties.popup).indexOf(searchText) > -1 + ) + // Feature ids are converted to int in mapbox for some reason (https://github.com/mapbox/geojson-vt/pull/60) + filteredIds = filtered.map(feature => feature.id || feature.properties.id) + } + else { + filteredIds = [] } - filter(searchText: string, taggedFeatures: TaggedLayer[], taggedFeatureText: string): void { - for (let layerName in this.layers) { - this.filterLayer(layerName, normalize(searchText), taggedFeatures, normalize(taggedFeatureText)) - } - } - - filterLayer(layerName: string, searchText: string, taggedFeatures: TaggedLayer[]=[], taggedFeatureText: string) { - let layer = this.layers[layerName] - let tagsLayerName = layerName + '-tags' - let labelsLayerName = layerName + '-labels' - let hasTagsLayer = !!this.map.getLayer(tagsLayerName) - let hasLabelsLayer = !!this.map.getLayer(labelsLayerName) - let filteredIds: any[] - - if (!searchText && !taggedFeatureText) { - // Clear all filters - this.map.setFilter(layerName, null) - hasTagsLayer && this.map.setFilter(tagsLayerName, null) - hasLabelsLayer && this.map.setFilter(labelsLayerName, null) - return - } - - if (searchText) { - // TODO: regex matching (waiting for https://github.com/mapbox/mapbox-gl-js/pull/6228) - //this.map.setFilter(layerName, ['regex-test', 'popup', searchText]) - //this.map.setFilter(layerName, ['==', 'popup', searchText]) - // Layer from JSON - let filtered = layer.source.data.features.filter( - feature => normalize(feature.properties.popup).indexOf(searchText) > -1 + // Features with matching tags + let taggedLayerFeatures = taggedFeatures.find(t => t.store == layerName) + if (taggedLayerFeatures) { + filteredIds.push(...taggedLayerFeatures.features.filter( + feature => feature.tags.filter( + tag => ( + normalize(tag.key).indexOf(taggedFeatureText) > -1) + || (normalize(tag.value).indexOf(taggedFeatureText) > -1 ) - // Feature ids are converted to int in mapbox for some reason (https://github.com/mapbox/geojson-vt/pull/60) - filteredIds = filtered.map(feature => feature.id || feature.properties.id) - } - else { - filteredIds = [] - } - - // Features with matching tags - let taggedLayerFeatures = taggedFeatures.find(t => t.store == layerName) - if (taggedLayerFeatures) { - filteredIds.push(...taggedLayerFeatures.features.filter( - feature => feature.tags.filter( - tag => ( - normalize(tag.key).indexOf(taggedFeatureText) > -1) - || (normalize(tag.value).indexOf(taggedFeatureText) > -1 - ) - ).length > 0 - ).map(feature => feature.id)) - } - - if (filteredIds.length > 0) { - // Filter the feature layers - this.map.setFilter( - layerName, - // Filter expressions using 'match' seem not support $id, using 'in' - ['in', 'id', ...filteredIds] - ) - // Filter the tags layers - hasTagsLayer && this.map.setFilter(tagsLayerName, ['in', 'id', ...filteredIds]) - // Filter the labels layers - hasLabelsLayer && this.map.setFilter(labelsLayerName, ['in', 'id', ...filteredIds]) - // TODO: filter the selection layers - } - else { - // Nothing found, set filters as-is (should be none) - this.map.setFilter(layerName, ['==', 'popup', searchText]) - hasTagsLayer && this.map.setFilter(tagsLayerName, ['==', 'popup', taggedFeatureText]) - hasLabelsLayer && this.map.setFilter(labelsLayerName, ['==', 'popup', taggedFeatureText]) - } + ).length > 0 + ).map(feature => feature.id)) } - highlightFeatures(layer, ids) { - // Filter the features matching the ids in the related highlighted layer - if (!layer) { - console.debug('TODO: remove layer from highlightedLayers') - return - } - let highlightedLayer = this.highlightedLayers[layer['id']] - let idsString = new Set(Array.from(ids || []).map(id => id.toString())) - - let allFeatures = layer['source']['data']['features'] - allFeatures.forEach( - feature => feature['properties']['_selected'] = idsString.has(feature['id'] || feature['properties']['id']) - ) - - // XXX: remove typings (maplibre 2.x) - let hlayer = this.map.getSource(highlightedLayer.id) - if (hlayer) { - hlayer.setData({ - type: 'FeatureCollection', - features: allFeatures - }) - } - else { - console.log(highlightedLayer) - } + if (filteredIds.length > 0) { + // Filter the feature layers + this.map.setFilter( + layerName, + // Filter expressions using 'match' seem not support $id, using 'in' + ['in', 'id', ...filteredIds] + ) + // Filter the tags layers + hasTagsLayer && this.map.setFilter(tagsLayerName, ['in', 'id', ...filteredIds]) + // Filter the labels layers + hasLabelsLayer && this.map.setFilter(labelsLayerName, ['in', 'id', ...filteredIds]) + // TODO: filter the selection layers } - - getAndAddTagsLayer(tagLayerNode: LayerNode) { - this.infoDataService.getTaggedLayers([tagLayerNode], true).subscribe( - (taggedLayers: TaggedLayer[]) => Object.values(taggedLayers).forEach( - taggedLayer => this.addTagsLayer(taggedLayer) - ) - ) + else { + // Nothing found, set filters as-is (should be none) + this.map.setFilter(layerName, ['==', 'popup', searchText]) + hasTagsLayer && this.map.setFilter(tagsLayerName, ['==', 'popup', taggedFeatureText]) + hasLabelsLayer && this.map.setFilter(labelsLayerName, ['==', 'popup', taggedFeatureText]) } + } - addLabelsLayer(layerNode: LayerNode) { - let layerName = layerNode.store + '-labels' - let layer = this.layers[layerNode.store] - let source = layer['source']['data'] - let features = source['features'] - let labelsFeatures = features.map( + highlightFeatures(layer, ids) { + // Filter the features matching the ids in the related highlighted layer + if (!layer) { + console.debug('TODO: remove layer from highlightedLayers') + return + } + let highlightedLayer = this.highlightedLayers[layer['id']] + let idsString = new Set(Array.from(ids || []).map(id => id.toString())) + + let allFeatures = layer['source']['data']['features'] + allFeatures.forEach( + feature => feature['properties']['_selected'] = idsString.has(feature['id'] || feature['properties']['id']) + ) + + // XXX: remove typings (maplibre 2.x) + let hlayer = this.map.getSource(highlightedLayer.id) + if (hlayer) { + hlayer.setData({ + type: 'FeatureCollection', + features: allFeatures + }) + } + else { + console.log(highlightedLayer) + } + } + + getAndAddTagsLayer(tagLayerNode: LayerNode) { + this.infoDataService.getTaggedLayers([tagLayerNode], true).subscribe( + (taggedLayers: TaggedLayer[]) => Object.values(taggedLayers).forEach( + taggedLayer => this.addTagsLayer(taggedLayer) + ) + ) + } + + addLabelsLayer(layerNode: LayerNode) { + let layerName = layerNode.store + '-labels' + let layer = this.layers[layerNode.store] + let source = layer['source']['data'] + let features = source['features'] + let labelsFeatures = features.map( + feature => { + return { + 'id': feature['id'], + 'type': 'Feature', + 'properties': { + 'id': feature['id'], + 'label': feature['properties']['label'] || feature['properties']['popup'] + }, + 'geometry': feature['geometry'] + } + } + ) + //this.map.addSource(layerName, { + // 'type': 'geojson', + // 'data': { + // 'type': 'FeatureCollection', + // 'features': labelsFeatures, + // } + //}) + let labelsLayer = { + 'id': layerName, + 'type': 'symbol', + 'minzoom': 13, + 'source': { + 'type': 'geojson', + 'data': { + 'type': 'FeatureCollection', + 'features': labelsFeatures, + } + }, + 'layout': { + 'text-field': ['get', 'label'], + 'text-line-height': 1, + 'text-padding': 0, + 'text-anchor': 'bottom', + 'text-offset': [0, 0], + 'text-allow-overlap': false, + 'text-font': ['Noto Sans Regular'], + 'text-size': { + stops: [[13, 9], [20, 18]] + }, + 'symbol-placement': layerNode.type == 'line' ? 'line' : 'point', + }, + 'paint': { + 'text-translate-anchor': 'viewport', + 'text-color': '#222266', + 'text-halo-color': 'white', + 'text-halo-width': 1, + 'text-halo-blur': 0, + } + } + this.ngZone.runOutsideAngular( + () => this.map.addLayer(labelsLayer) + ) + } + + addTagsLayer(taggedLayer: TaggedLayer) { + let layerName = taggedLayer.store + '-tags' + if (!!this.map.getLayer(layerName)) { + this.removeTaggedLayer(taggedLayer.store) + } + // Filter only features really in the layer + let taggedFeatureIds = taggedLayer.features.map(ff => ff.id) + let features = this.layers[taggedLayer.store].source.data.features.filter( + (f: object) => + taggedFeatureIds.findIndex( + fff => fff == f['id'] || fff == f['properties']['id'] + ) !== -1 + ) + let taggedLayerFeatures = taggedLayer.features.filter( + (f: TaggedFeature) => features.findIndex( + (ff: object) => ff['id'] === f.id || ff['properties']['id'] === f.id) != -1 + ) + let tagsLayer = { + id: layerName, + type: 'symbol', + minzoom: 13, + source: { + type: "geojson", + data: { + type: 'FeatureCollection', + features: taggedLayerFeatures.map( feature => { - return { - 'id': feature['id'], - 'type': 'Feature', - 'properties': { - 'id': feature['id'], - 'label': feature['properties']['label'] || feature['properties']['popup'] - }, - 'geometry': feature['geometry'] - } - } - ) - //this.map.addSource(layerName, { - // 'type': 'geojson', - // 'data': { - // 'type': 'FeatureCollection', - // 'features': labelsFeatures, - // } - //}) - let labelsLayer = { - 'id': layerName, - 'type': 'symbol', - 'minzoom': 13, - 'source': { - 'type': 'geojson', - 'data': { - 'type': 'FeatureCollection', - 'features': labelsFeatures, - } - }, - 'layout': { - 'text-field': ['get', 'label'], - 'text-line-height': 1, - 'text-padding': 0, - 'text-anchor': 'bottom', - 'text-offset': [0, 0], - 'text-allow-overlap': false, - 'text-font': ['Noto Sans Regular'], - 'text-size': { - stops: [[13, 9], [20, 18]] + return { + type: 'Feature', + properties: { + id: feature.id, + tag: feature.tags.map(tag => tag.key + ': ' + tag.value).join(', ') }, - 'symbol-placement': layerNode.type == 'line' ? 'line' : 'point', - }, - 'paint': { - 'text-translate-anchor': 'viewport', - 'text-color': '#222266', - 'text-halo-color': 'white', - 'text-halo-width': 1, - 'text-halo-blur': 0, - } - } - this.ngZone.runOutsideAngular( - () => this.map.addLayer(labelsLayer) - ) - } - - addTagsLayer(taggedLayer: TaggedLayer) { - let layerName = taggedLayer.store + '-tags' - if (!!this.map.getLayer(layerName)) { - this.removeTaggedLayer(taggedLayer.store) - } - // Filter only features really in the layer - let taggedFeatureIds = taggedLayer.features.map(ff => ff.id) - let features = this.layers[taggedLayer.store].source.data.features.filter( - (f: object) => - taggedFeatureIds.findIndex( - fff => fff == f['id'] || fff == f['properties']['id'] - ) !== -1 - ) - let taggedLayerFeatures = taggedLayer.features.filter( - (f: TaggedFeature) => features.findIndex( - (ff: object) => ff['id'] === f.id || ff['properties']['id'] === f.id) != -1 - ) - let tagsLayer = { - id: layerName, - type: 'symbol', - minzoom: 13, - source: { - type: "geojson", - data: { - type: 'FeatureCollection', - features: taggedLayerFeatures.map( - feature => { - return { - type: 'Feature', - properties: { - id: feature.id, - tag: feature.tags.map(tag => tag.key + ': ' + tag.value).join(', ') - }, - geometry: { - type: "Point", - coordinates: [feature.lon, feature.lat] - } - } - } - ) + geometry: { + type: "Point", + coordinates: [feature.lon, feature.lat] } - }, - layout: { - 'text-field': ['get', 'tag'], - 'text-line-height': 1, - 'text-padding': 0, - 'text-anchor': 'bottom', - 'text-offset': [0, -1], - 'text-allow-overlap': false, - 'text-font': ['Noto Sans Regular'], - 'text-size': { - stops: [[13, 9], [20, 18]] - } - }, - paint: { - 'text-translate-anchor': 'viewport', - 'text-color': '#662222', - 'text-halo-color': 'white', - 'text-halo-width': 1, - 'text-halo-blur': 0, + } } + ) } - this.ngZone.runOutsideAngular( - () => { - this.map.addLayer(tagsLayer) - } - ) - } - - addLabelsLayers(layers: LayerNode[]) { - // XXX: taggedLayers in MapControlComponent behaves like an object - Object.values(layers).forEach( - labelsLayer => this.addLabelsLayer(labelsLayer) - ) - } - - addTagsLayers(taggedLayers: TaggedLayer[]) { - // XXX: taggedLayers in MapControlComponent behaves like an object - Object.values(taggedLayers).forEach( - taggedLayer => this.addTagsLayer(taggedLayer) - ) - } - - removeLabelsLayers() { - Object.keys(this.layerDefs).forEach( - layerId => this.removeLabelsLayer(layerId) - ) - } - - removeTaggedLayers() { - Object.keys(this.layerDefs).forEach( - layerId => this.removeTaggedLayer(layerId) - ) - } - - removeLabelsLayer(layerId: string) { - this.removeLayersByIds([layerId + '-labels']) - } - - removeTaggedLayer(layerId: string) { - this.removeLayersByIds([layerId + '-tags']) - } - - protected removeLayersByIds(layerIds: string[]) { - layerIds.forEach( - layerId => { - if (!!this.map.getLayer(layerId)) - this.map.removeLayer(layerId) - if (!!this.map.getSource(layerId)) - this.map.removeSource(layerId) - } - ) - } - - filterByStatus(status: string[], layer?: string, layerNode?: LayerNode) { - for (let layerName of layer ? [layer] : Object.keys(this.layers)) { - let layerNode = this.layerDefs[layerName] - if (!layerNode.custom && !layerNode.live) { - this.map.setFilter(layerName, ['in', 'status', ...status]) - } + }, + layout: { + 'text-field': ['get', 'tag'], + 'text-line-height': 1, + 'text-padding': 0, + 'text-anchor': 'bottom', + 'text-offset': [0, -1], + 'text-allow-overlap': false, + 'text-font': ['Noto Sans Regular'], + 'text-size': { + stops: [[13, 9], [20, 18]] } + }, + paint: { + 'text-translate-anchor': 'viewport', + 'text-color': '#662222', + 'text-halo-color': 'white', + 'text-halo-width': 1, + 'text-halo-blur': 0, + } } + this.ngZone.runOutsideAngular( + () => { + this.map.addLayer(tagsLayer) + } + ) + } - /* - toggleInfoDrawer(): void { - this.mapControlService.toggleInfo() + addLabelsLayers(layers: LayerNode[]) { + // XXX: taggedLayers in MapControlComponent behaves like an object + Object.values(layers).forEach( + labelsLayer => this.addLabelsLayer(labelsLayer) + ) + } + + addTagsLayers(taggedLayers: TaggedLayer[]) { + // XXX: taggedLayers in MapControlComponent behaves like an object + Object.values(taggedLayers).forEach( + taggedLayer => this.addTagsLayer(taggedLayer) + ) + } + + removeLabelsLayers() { + Object.keys(this.layerDefs).forEach( + layerId => this.removeLabelsLayer(layerId) + ) + } + + removeTaggedLayers() { + Object.keys(this.layerDefs).forEach( + layerId => this.removeTaggedLayer(layerId) + ) + } + + removeLabelsLayer(layerId: string) { + this.removeLayersByIds([layerId + '-labels']) + } + + removeTaggedLayer(layerId: string) { + this.removeLayersByIds([layerId + '-tags']) + } + + protected removeLayersByIds(layerIds: string[]) { + layerIds.forEach( + layerId => { + if (!!this.map.getLayer(layerId)) + this.map.removeLayer(layerId) + if (!!this.map.getSource(layerId)) + this.map.removeSource(layerId) + } + ) + } + + filterByStatus(status: string[], layer?: string, layerNode?: LayerNode) { + for (let layerName of layer ? [layer] : Object.keys(this.layers)) { + let layerNode = this.layerDefs[layerName] + if (!layerNode.custom && !layerNode.live) { + this.map.setFilter(layerName, ['in', 'status', ...status]) + } } - */ + } - toggleControlDrawer(value?:boolean): void { - this.mapControlService.toggleControls(value) + /* + toggleInfoDrawer(): void { + this.mapControlService.toggleInfo() + } + */ + + toggleControlDrawer(value?: boolean): void { + this.mapControlService.toggleControls(value) + } + + onClick(evt: MapMouseEvent): void { + if (!(evt.point && this.map)) { + return } + let features = this.map.queryRenderedFeatures(evt.point, { layers: Object.keys(this.layers) }) + let feature = features[0] + if (feature) { + let layer = (this.layerDefs)[(feature).layer.id] + let store: string = layer.live && feature['properties']['store'] ? + feature['properties']['store'] : + layer.store + this.mapControlService.featureClicked.next( + new Feature(store, feature['properties']['id'] || feature.id) + ) + } + else { + this.mapControlService.featureClicked.next(undefined) + } + } - onClick(evt: MapMouseEvent): void { - if (!(evt.point && this.map)) { - return - } - let features = this.map.queryRenderedFeatures(evt.point, {layers:Object.keys(this.layers)}) - let feature = features[0] - if (feature) { - let layer = (this.layerDefs)[(feature).layer.id] - let store: string = layer.live && feature['properties']['store'] ? - feature['properties']['store'] : - layer.store - this.mapControlService.featureClicked.next( - new Feature(store, feature['properties']['id'] || feature.id) - ) + onMove(evt: MapMouseEvent): void { + this.mouseLngLat.nativeElement.innerHTML = evt.lngLat.lng.toFixed(7) + ', ' + evt.lngLat.lat.toFixed(7) + } + + onLeave(evt: MapMouseEvent): void { + this.mouseLngLat.nativeElement.innerHTML = null + } + + onMoveOnLayer(evt: MapMouseEvent): void { + let feature = (evt).features[0] + var rfeature = this.map.queryRenderedFeatures(evt.point)[0] + if (rfeature.id !== this.popupOnFeature) { + this.popupOnFeature = rfeature.id + this.featureInfo.nativeElement.innerHTML = feature.properties.popup + } + } + + onEnter(evt: MapMouseEvent): void { + let feature = (evt).features[0] + this.popupOnFeature = feature.id + let info = feature.properties.popup + this.featureInfo.nativeElement.innerHTML = info + } + + onLeaveOnLayer(evt: MapMouseEvent): void { + this.popupOnFeature = null + this.featureInfo.nativeElement.innerHTML = null + } + + zoomToFeatures(features?): void { + // Zoom to the given features or all the visible ones + const fitBoundsOptions = { + padding: 40, + maxZoom: 17, + } + if (!features) { + // Zoom to visible features + // Collect all the features + features = [] + for (let layerName in this.layers) { + let filter = this.map.getFilter(layerName) + // Eventually re-fit with the filter + if (filter && filter[0] == 'in' && filter[1] == 'id') { + // NOTE: this.map.querySourceLayer works only for features in the viewport, so + // the source data is used + // Assuming that filter is in the form ['in', 'id', ...] + let ids = (filter).splice(2) + features.push(...this.layers[layerName].source.data.features.filter(f => ids.indexOf(f.id) != -1)) } else { - this.mapControlService.featureClicked.next(undefined) + features.push(...this.layers[layerName].source.data.features) } + } } - onMove(evt: MapMouseEvent): void { - this.mouseLngLat.nativeElement.innerHTML = evt.lngLat.lng.toFixed(7) + ', ' + evt.lngLat.lat.toFixed(7) - } - - onLeave(evt: MapMouseEvent): void { - this.mouseLngLat.nativeElement.innerHTML = null - } - - onMoveOnLayer(evt: MapMouseEvent): void { - let feature = (evt).features[0] - var rfeature = this.map.queryRenderedFeatures(evt.point)[0] - if (rfeature.id !== this.popupOnFeature) { - this.popupOnFeature = rfeature.id - this.featureInfo.nativeElement.innerHTML = feature.properties.popup - } - } - - onEnter(evt: MapMouseEvent): void { - let feature = (evt).features[0] - this.popupOnFeature = feature.id - let info = feature.properties.popup - this.featureInfo.nativeElement.innerHTML = info - } - - onLeaveOnLayer(evt: MapMouseEvent): void { - this.popupOnFeature = null - this.featureInfo.nativeElement.innerHTML = null - } - - zoomToFeatures(features?): void { - // Zoom to the given features or all the visible ones - const fitBoundsOptions = { - padding: 40, - maxZoom: 17, - } - if (!features) { - // Zoom to visible features - // Collect all the features - features = [] - for (let layerName in this.layers) { - let filter = this.map.getFilter(layerName) - // Eventually re-fit with the filter - if (filter && filter[0]=='in' && filter[1]=='id') { - // NOTE: this.map.querySourceLayer works only for features in the viewport, so - // the source data is used - // Assuming that filter is in the form ['in', 'id', ...] - let ids = (filter).splice(2) - features.push(...this.layers[layerName].source.data.features.filter(f=>ids.indexOf(f.id)!=-1)) - } - else { - features.push(...this.layers[layerName].source.data.features) - } - } - } - - // XXX: bug in turf: https://github.com/Turfjs/turf/issues/1428 - let bb = bbox.default({ - type: 'FeatureCollection', - features: features - }) - if (!bb.some(b => b == Infinity)) { - this.map.fitBounds([[bb[0], bb[1]], [bb[2], bb[3]]], fitBoundsOptions) - } + // XXX: bug in turf: https://github.com/Turfjs/turf/issues/1428 + let bb = bbox.default({ + type: 'FeatureCollection', + features: features + }) + if (!bb.some(b => b == Infinity)) { + this.map.fitBounds([[bb[0], bb[1]], [bb[2], bb[3]]], fitBoundsOptions) } + } } diff --git a/src/app/map/map-data.service.ts b/src/app/map/map-data.service.ts index 35f3aa8..c72e010 100644 --- a/src/app/map/map-data.service.ts +++ b/src/app/map/map-data.service.ts @@ -8,130 +8,130 @@ import { MapControlService } from './map-control.service' import { MapService, MapInitData, BaseMapWithStores, BaseStyle as OApiBaseStyle } from '../openapi' export class BaseStyle { - constructor( - public name: string, - public style?: StyleSpecification, - ) {} + constructor( + public name: string, + public style?: StyleSpecification, + ) { } } @Injectable() export class MapDataService { - constructor( - // private apollo: Apollo, - protected mapControlService: MapControlService, - public mapService: MapService, - ) { - // Get the data on init, as it is (most probably) immutable - this.getMapInitData().subscribe( - data => { - this.mapInitDataLoaded.next(true) - this.mapInitDataLoaded.complete() - } - ) - } + constructor( + // private apollo: Apollo, + protected mapControlService: MapControlService, + public mapService: MapService, + ) { + // Get the data on init, as it is (most probably) immutable + this.getMapInitData().subscribe( + data => { + this.mapInitDataLoaded.next(true) + this.mapInitDataLoaded.complete() + } + ) + } - public mapInitDataLoaded = new BehaviorSubject(false) - mapInitDataLoaded$ = this.mapInitDataLoaded.asObservable() + public mapInitDataLoaded = new BehaviorSubject(false) + mapInitDataLoaded$ = this.mapInitDataLoaded.asObservable() - mapInitData: MapInitData = {} + mapInitData: MapInitData = {} - getMapInitData(): Observable { - return this.mapService.getInitDataApiMapInitDataGet().pipe(map( - data => this.mapInitData = data - )) - } + getMapInitData(): Observable { + return this.mapService.getInitDataApiMapInitDataGet().pipe(map( + data => this.mapInitData = data + )) + } - // getBaseMaps(): Observable { - // return this.apollo.query({ - // query: baseMapQuery, - // }).pipe(map( - // res => { - // let bms: object = res['data']['baseMap'] - // return bms['map']( - // (bm: object) => new BaseMap( - // bm['name'], - // bm['stores'].map( - // (store: object) => new Store(store['name']) - // ) - // ) - // ) - // } - // )) - // } + // getBaseMaps(): Observable { + // return this.apollo.query({ + // query: baseMapQuery, + // }).pipe(map( + // res => { + // let bms: object = res['data']['baseMap'] + // return bms['map']( + // (bm: object) => new BaseMap( + // bm['name'], + // bm['stores'].map( + // (store: object) => new Store(store['name']) + // ) + // ) + // ) + // } + // )) + // } - public createBaseMap(baseMapName: string, stores: string[]): Observable { - console.log('TODO: Migrate Graphql createBaseMap') - return of({name: '', stores:[]}) - // return this.apollo.mutate({ - // mutation: createBaseMapQuery, - // variables: { - // baseMapName: baseMapName, - // stores: stores - // } - // }).pipe(map( - // res => { - // let bm: object = res['data']['createBaseMap']['baseMap'] - // return new BaseMap( - // bm['name'], - // bm['stores'].map((store: object) => new Store(store['name'])) - // ) - // } - // )) - } - - // XXX: unused - // getBaseStyleList(): Observable { - // return this.apollo.query({query: baseStyleListQuery}).pipe(map( - // data => data['data']['base_style_list'].map( - // (baseStyle: string) => new BaseStyle(baseStyle['name']) + public createBaseMap(baseMapName: string, stores: string[]): Observable { + console.log('TODO: Migrate Graphql createBaseMap') + return of({ name: '', stores: [] }) + // return this.apollo.mutate({ + // mutation: createBaseMapQuery, + // variables: { + // baseMapName: baseMapName, + // stores: stores + // } + // }).pipe(map( + // res => { + // let bm: object = res['data']['createBaseMap']['baseMap'] + // return new BaseMap( + // bm['name'], + // bm['stores'].map((store: object) => new Store(store['name'])) // ) - // )) - // } + // } + // )) + } - getBaseStyle(styleName: string): Observable { - return this.mapService.getBaseStyleApiMapBaseStyleNameGet({name: styleName}).pipe(map( - data => new BaseStyle(data.name, data.style) - )) - } + // XXX: unused + // getBaseStyleList(): Observable { + // return this.apollo.query({query: baseStyleListQuery}).pipe(map( + // data => data['data']['base_style_list'].map( + // (baseStyle: string) => new BaseStyle(baseStyle['name']) + // ) + // )) + // } - // getStores(): Observable { - // return this.apollo.query({ - // query: storeQuery, - // }).pipe(map( - // res => res['data']['stores'].map( - // (layer: Object) => new LayerNode( - // layer['name'], - // layer['group'], - // layer['icon'], - // layer['symbol'] || gisTypeSymbolMap[layer['gisType']], - // layer['store'], - // layer['rawSurveyStore'], - // layer['type'], - // layer['gisType'], - // layer['style'], - // layer['zIndex'], - // layer['count'], - // layer['description'], - // layer['live'], - // layer['custom'], - // layer['tagPlugins'], - // layer['viewableRole'] - // ) - // ) - // )) - // } + getBaseStyle(styleName: string): Observable { + return this.mapService.getBaseStyleApiMapBaseStyleNameGet({ name: styleName }).pipe(map( + (data: OApiBaseStyle) => new BaseStyle(data.name, data.style) + )) + } - // getPrimaryGroups(): Observable { - // return this.apollo.query({ - // query: geomGroupQuery, - // }).pipe(map( - // res => res['data']['geomGroup'] && res['data']['geomGroup'].map( - // (gg: Object) => new PrimaryGroupNode( - // gg['name'], - // gg['title'], - // gg['description'] - // ) - // ) - // )) - // } -} \ No newline at end of file + // getStores(): Observable { + // return this.apollo.query({ + // query: storeQuery, + // }).pipe(map( + // res => res['data']['stores'].map( + // (layer: Object) => new LayerNode( + // layer['name'], + // layer['group'], + // layer['icon'], + // layer['symbol'] || gisTypeSymbolMap[layer['gisType']], + // layer['store'], + // layer['rawSurveyStore'], + // layer['type'], + // layer['gisType'], + // layer['style'], + // layer['zIndex'], + // layer['count'], + // layer['description'], + // layer['live'], + // layer['custom'], + // layer['tagPlugins'], + // layer['viewableRole'] + // ) + // ) + // )) + // } + + // getPrimaryGroups(): Observable { + // return this.apollo.query({ + // query: geomGroupQuery, + // }).pipe(map( + // res => res['data']['geomGroup'] && res['data']['geomGroup'].map( + // (gg: Object) => new PrimaryGroupNode( + // gg['name'], + // gg['title'], + // gg['description'] + // ) + // ) + // )) + // } +} diff --git a/src/app/openapi/core/OpenAPI.ts b/src/app/openapi/core/OpenAPI.ts index 3879553..b118fbe 100644 --- a/src/app/openapi/core/OpenAPI.ts +++ b/src/app/openapi/core/OpenAPI.ts @@ -47,7 +47,7 @@ export const OpenAPI: OpenAPIConfig = { PASSWORD: undefined, TOKEN: undefined, USERNAME: undefined, - VERSION: '0.1.dev85+g41e92fa.d20240509', + VERSION: '0.6.0a0', WITH_CREDENTIALS: false, interceptors: { response: new Interceptors(), diff --git a/src/app/openapi/schemas.gen.ts b/src/app/openapi/schemas.gen.ts index d95c8fa..5790f33 100644 --- a/src/app/openapi/schemas.gen.ts +++ b/src/app/openapi/schemas.gen.ts @@ -259,12 +259,26 @@ export const $BaseStyle = { title: 'Style' }, mbtiles: { - type: 'string', + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], title: 'Mbtiles' }, - static_tiles_url: { - type: 'string', - title: 'Static Tiles Url' + static_url: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Static Url' }, enabled: { type: 'boolean', @@ -273,7 +287,7 @@ export const $BaseStyle = { } }, type: 'object', - required: ['name', 'style', 'mbtiles', 'static_tiles_url'], + required: ['name'], title: 'BaseStyle' } as const; @@ -311,7 +325,6 @@ export const $BasketDefault = { title: 'Store' } }, - additionalProperties: false, type: 'object', title: 'BasketDefault' } as const; @@ -514,52 +527,43 @@ export const $BootstrapData = { version: { type: 'string', title: 'Version', - default: '0.1.dev85+g41e92fa.d20240509' + default: '0.6.0a0' }, title: { type: 'string', title: 'Title', - default: 'Auroville Geomatics Studio (Me)' + default: 'Gisaf' }, windowTitle: { type: 'string', title: 'Windowtitle', - default: 'AV Geomatics Studio (Me)' + default: 'Gisaf' }, map: { - allOf: [ - { - '$ref': '#/components/schemas/Map' - } - ], + '$ref': '#/components/schemas/Map', default: { - attribution: '© Auroville CSR Geomatics', - bearing: 0, - defaultStatus: ['E'], - lat: 12.007, - lng: 79.8098, - opacity: 0.4, - pitch: 0, - status: ['E', 'F', 'D'], - style: 'No base map', - tagKeys: ['source'], tileServer: { - baseDir: '/home/phil/gisaf_misc/map', - openMapTilesKey: 'cS3lrAfYXoM4MDooT6aS', - spriteBaseDir: '/home/phil/gisaf_misc/map/sprite', - spriteBaseUrl: 'https://gis.auroville.org.in', + baseDir: '/home/phil/.local/share/gisaf/mbtiles_files_dir', + spriteBaseDir: '/home/phil/.local/share/gisaf/mbtiles_sprites_dir', + spriteBaseUrl: 'https://gisaf.example.org', spriteUrl: '/tiles/sprite/sprite', - useRequestUrl: true + useRequestUrl: false }, - zoom: 14 + zoom: 14, + pitch: 45, + lat: 12, + lng: 79.8106, + bearing: 0, + style: 'OpenFreeMap', + opacity: 1, + attribution: '', + status: ['E', 'F', 'D'], + defaultStatus: ['E'], + tagKeys: ['source'] } }, geo: { - allOf: [ - { - '$ref': '#/components/schemas/Geo' - } - ], + '$ref': '#/components/schemas/Geo', default: { raw_survey: { spatial_sys_ref: { @@ -584,19 +588,13 @@ export const $BootstrapData = { } }, measures: { - allOf: [ - { - '$ref': '#/components/schemas/Measures' - } - ], - default: { - defaultStore: 'avsm_water.well' - } + '$ref': '#/components/schemas/Measures', + default: {} }, redirect: { type: 'string', title: 'Redirect', - default: 'http://gis.auroville.org.in' + default: '' }, user: { anyOf: [ @@ -1442,11 +1440,7 @@ export const $FormFieldInput = { export const $Geo = { properties: { raw_survey: { - allOf: [ - { - '$ref': '#/components/schemas/RawSurvey' - } - ], + '$ref': '#/components/schemas/RawSurvey', default: { spatial_sys_ref: { author: 'AVSM', @@ -1485,7 +1479,6 @@ export const $Geo = { default: 32644 } }, - additionalProperties: false, type: 'object', title: 'Geo' } as const; @@ -1568,17 +1561,13 @@ export const $LegendItem = { export const $Map = { properties: { tileServer: { - allOf: [ - { - '$ref': '#/components/schemas/TileServer' - } - ], + '$ref': '#/components/schemas/TileServer', default: { - baseDir: '/path/to/mbtiles_files_dir', - spriteBaseDir: '/path/to/mbtiles_sprites_dir', - spriteBaseUrl: 'https://gisaf.example.org', + baseDir: '/home/phil/.local/share/gisaf/mbtiles_files_dir', + useRequestUrl: false, + spriteBaseDir: '/home/phil/.local/share/gisaf/mbtiles_sprites_dir', spriteUrl: '/tiles/sprite/sprite', - useRequestUrl: false + spriteBaseUrl: 'https://gisaf.example.org' } }, zoom: { @@ -1609,7 +1598,7 @@ export const $Map = { style: { type: 'string', title: 'Style', - default: 'OSM (vector)' + default: 'OpenFreeMap' }, opacity: { type: 'number', @@ -1646,7 +1635,6 @@ export const $Map = { default: ['source'] } }, - additionalProperties: false, type: 'object', title: 'Map' } as const; @@ -1790,7 +1778,6 @@ export const $Measures = { title: 'Defaultstore' } }, - additionalProperties: false, type: 'object', title: 'Measures' } as const; @@ -2109,11 +2096,7 @@ export const $Project = { export const $RawSurvey = { properties: { spatial_sys_ref: { - allOf: [ - { - '$ref': '#/components/schemas/SpatialSysRef' - } - ], + '$ref': '#/components/schemas/SpatialSysRef', default: { author: 'AVSM', ellps: 'WGS84', @@ -2134,7 +2117,6 @@ export const $RawSurvey = { default: 910001 } }, - additionalProperties: false, type: 'object', title: 'RawSurvey' } as const; @@ -2237,7 +2219,6 @@ export const $SpatialSysRef = { default: 1328608.994 } }, - additionalProperties: false, type: 'object', title: 'SpatialSysRef' } as const; @@ -2386,11 +2367,12 @@ export const $Store = { }, z_index: { type: 'integer', - title: 'Z Index' + title: 'Z Index', + default: 500 } }, type: 'object', - required: ['name', 'auto_import', 'custom', 'description', 'gis_type', 'group', 'in_menu', 'is_db', 'is_line_work', 'is_live', 'long_name', 'type', 'minor_group_1', 'minor_group_2', 'status', 'style', 'symbol', 'title', 'viewable_role', 'z_index'], + required: ['name', 'auto_import', 'custom', 'description', 'gis_type', 'group', 'in_menu', 'is_db', 'is_line_work', 'is_live', 'long_name', 'type', 'minor_group_1', 'minor_group_2', 'status', 'style', 'symbol', 'title'], title: 'Store' } as const; @@ -2648,8 +2630,9 @@ export const $TileServer = { properties: { baseDir: { type: 'string', + format: 'path', title: 'Basedir', - default: '/path/to/mbtiles_files_dir' + default: '/home/phil/.local/share/gisaf/mbtiles_files_dir' }, useRequestUrl: { type: 'boolean', @@ -2658,8 +2641,9 @@ export const $TileServer = { }, spriteBaseDir: { type: 'string', + format: 'path', title: 'Spritebasedir', - default: '/path/to/mbtiles_sprites_dir' + default: '/home/phil/.local/share/gisaf/mbtiles_sprites_dir' }, spriteUrl: { type: 'string', @@ -2683,7 +2667,6 @@ export const $TileServer = { title: 'Openmaptileskey' } }, - additionalProperties: false, type: 'object', title: 'TileServer' } as const; diff --git a/src/app/openapi/types.gen.ts b/src/app/openapi/types.gen.ts index 1e81860..ca92eab 100644 --- a/src/app/openapi/types.gen.ts +++ b/src/app/openapi/types.gen.ts @@ -54,11 +54,11 @@ export type BaseMapWithStores = { export type BaseStyle = { id?: number | null; name: string; - style: { + style?: { [key: string]: unknown; } | null; - mbtiles: string; - static_tiles_url: string; + mbtiles?: string | null; + static_url?: string | null; enabled?: boolean; }; @@ -457,8 +457,8 @@ export type Store = { style: string | null; symbol: string | null; title: string; - viewable_role: string | null; - z_index: number; + viewable_role?: string | null; + z_index?: number; }; export type StoreNameOnly = { @@ -751,9 +751,7 @@ export type $OpenApiTs = { }; '/api/token': { post: { - req: { - formData: Body_login_for_access_token_api_token_post; - }; + req: LoginForAccessTokenApiTokenPostData; res: { /** * Successful Response @@ -870,9 +868,7 @@ export type $OpenApiTs = { }; '/api/data-provider/{store}': { get: { - req: { - store: string; - }; + req: GetModelListApiDataProviderStoreGetData; res: { /** * Successful Response @@ -891,12 +887,7 @@ export type $OpenApiTs = { }; '/api/{store_name}/values/{value}': { get: { - req: { - resample?: string | null; - storeName: string; - value: string; - where: string; - }; + req: GetModelValuesApiStoreNameValuesValueGetData; res: { /** * Successful Response @@ -957,10 +948,7 @@ export type $OpenApiTs = { }; '/api/feature-info/{store}/{id}': { get: { - req: { - id: string; - store: string; - }; + req: GetFeatureInfoApiFeatureInfoStoreIdGetData; res: { /** * Successful Response @@ -979,9 +967,7 @@ export type $OpenApiTs = { }; '/api/model-info/{store}': { get: { - req: { - store: string; - }; + req: GetModelInfoApiModelInfoStoreGetData; res: { /** * Successful Response @@ -1000,11 +986,7 @@ export type $OpenApiTs = { }; '/api/plot-params/{store}': { get: { - req: { - id: string; - store: string; - value: string; - }; + req: GetPlotParamsApiPlotParamsStoreGetData; res: { /** * Successful Response @@ -1037,9 +1019,7 @@ export type $OpenApiTs = { }; '/api/execTagActions': { post: { - req: { - requestBody: Body_execute_tag_action_api_execTagActions_post; - }; + req: ExecuteTagActionApiExecTagActionsPostData; res: { /** * Successful Response @@ -1058,12 +1038,7 @@ export type $OpenApiTs = { }; '/api/gj/{store_name}': { get: { - req: { - ifNoneMatch?: string | null; - preserveTopology?: boolean | null; - simplify?: number | null; - storeName: unknown; - }; + req: GetGeojsonApiGjStoreNameGetData; res: { /** * Successful Response @@ -1096,9 +1071,7 @@ export type $OpenApiTs = { }; '/api/admin/basket/{name}': { get: { - req: { - name: string; - }; + req: GetBasketApiAdminBasketNameGetData; res: { /** * Successful Response @@ -1117,14 +1090,7 @@ export type $OpenApiTs = { }; '/api/admin/basket/upload/{name}': { post: { - req: { - autoImport?: boolean; - equipmentId?: number | null; - formData: Body_upload_basket_file_api_admin_basket_upload__name__post; - name: string; - projectId?: number | null; - surveyorId?: number | null; - }; + req: UploadBasketFileApiAdminBasketUploadNamePostData; res: { /** * Successful Response @@ -1143,11 +1109,7 @@ export type $OpenApiTs = { }; '/api/admin/basket/download/{name}/{file_id}/{file_name}': { get: { - req: { - fileId: number; - fileName: string; - name: string; - }; + req: DownloadBasketFileApiAdminBasketDownloadNameFileIdFileNameGetData; res: { /** * Successful Response @@ -1166,11 +1128,7 @@ export type $OpenApiTs = { }; '/api/admin/basket/import/{basket}/{file_id}': { get: { - req: { - basket: string; - dryRun?: boolean; - fileId: number; - }; + req: ImportBasketFileApiAdminBasketImportBasketFileIdGetData; res: { /** * Successful Response @@ -1189,10 +1147,7 @@ export type $OpenApiTs = { }; '/api/admin/basket/delete/{basket}/{file_id}': { get: { - req: { - basket: string; - fileId: number; - }; + req: DeleteBasketFileApiAdminBasketDeleteBasketFileIdGetData; res: { /** * Successful Response @@ -1239,10 +1194,7 @@ export type $OpenApiTs = { }; '/api/dashboard/page/{group}/{name}': { get: { - req: { - group: string; - name: string; - }; + req: GetDashboardPageApiDashboardPageGroupNameGetData; res: { /** * Successful Response @@ -1271,9 +1223,7 @@ export type $OpenApiTs = { }; '/api/map/base_style/{name}': { get: { - req: { - name: string; - }; + req: GetBaseStyleApiMapBaseStyleNameGetData; res: { /** * Successful Response @@ -1288,9 +1238,7 @@ export type $OpenApiTs = { }; '/api/map/layer_style/{store}': { get: { - req: { - store: string; - }; + req: GetLayerStyleApiMapLayerStyleStoreGetData; res: { /** * Successful Response @@ -1305,12 +1253,7 @@ export type $OpenApiTs = { }; '/api/download/csv/{store}/{model_id}/{value}/{resample}': { get: { - req: { - modelId: number; - resample: string; - store: string; - value: string; - }; + req: DownloadCsvApiDownloadCsvStoreModelIdValueResampleGetData; res: { /** * Successful Response @@ -1329,11 +1272,7 @@ export type $OpenApiTs = { }; '/api/download/geodata/{stores}': { get: { - req: { - format?: string; - reproject?: boolean; - stores: string; - }; + req: DownloadGeodataApiDownloadGeodataStoresGetData; res: { /** * Successful Response @@ -1352,11 +1291,7 @@ export type $OpenApiTs = { }; '/api/download/plugin/{name}/{store}/{id}': { get: { - req: { - id: number; - name: string; - store: string; - }; + req: ExecuteActionApiDownloadPluginNameStoreIdGetData; res: { /** * Successful Response