gisaf-frontend/src/app/map/gisaf-mapbox/gisaf-mapbox.component.ts
2024-02-27 11:52:00 +05:30

1276 lines
46 KiB
TypeScript

import { Component, OnInit, OnDestroy, Input, ViewChild, NgZone,
ChangeDetectionStrategy, ChangeDetectorRef, ElementRef } from '@angular/core'
import { ActivatedRoute, Params, Router } from '@angular/router'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
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 Point from '@mapbox/point-geometry'
import * as bbox from '@turf/bbox'
import { GeoJsonService, LiveGeoJsonService, MapboxDataAndStyle } from '../../_services/geojson.service'
import { ConfigService } from '../../config.service'
import { MapControlService } from '../map-control.service'
import { LayerNode } from '../models'
import { InfoDataService, Feature, TaggedLayer, FeatureWithField, TaggedFeature } from '../../info/info-data.service'
import { Store, MapDataService, BaseStyle } from '../map-data.service'
export class LayerWithMetaData {
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,
],
})
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
// XXX: temporary, for keeping map-controls.component.ts happy
map: Map
zoom: number = this.configService.conf.value.map['zoom']
pitch: number = this.configService.conf.value.map['pitch']
lat: number = this.configService.conf.value.map['lat']
lng: number = this.configService.conf.value.map['lng']
bearing: number = this.configService.conf.value.map['bearing']
globalAttribution: string = this.configService.conf.value.map['attribution']
baseStyle: BaseStyle = new BaseStyle('None')
protected _baseStyleName: string = this.configService.conf.value.map['style']
protected _bottom: number = 0
protected canvas: HTMLElement
protected selStart: Point
protected selAppend: boolean
protected selBox: HTMLDivElement
geolocateTrackUserLocation = true
geolocateShowUserLocation = true
geolocatePositionOptions = {
"enableHighAccuracy": true
}
geolocateFitBoundsOptions: FitBoundsOptions = {
"maxZoom": 12,
}
popupOnFeature: string | null | number
layerDefs: object = {}
layers: object = {}
highlightedLayers: object = {}
originalBaseStyle: BaseStyle
pendingLayers: LayerWithMetaData[] = []
subscribedLiveLayers: LayerNode[] = []
protected _baseStyleOpacity: number
private _pendingBaseStyleOpacity: number
protected wss: object = {} // WebSocketSubject<object>
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,
) {}
@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 = (<any>this.map.getSource(store)).serialize()
let highlightedSource = (<any>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 = (<any>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()
}
)
}
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]))
}
return baseUrl + ret.join('&')
}
protected getLayersForUrl(): string[] {
return Object.keys(this.layers)
}
@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 {
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'] = {}
}
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 ('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(',').map((store: string) => new Store(store))
)
}
)
}
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)
}
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<any> {
/*
* 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 = <LayerSpecification>{
id: layerDef.store,
type: layerDef.type,
source: <GeoJSONSourceSpecification>{
type: "geojson",
data: data
},
attribution: resp.style?.attribution
}
this.layers[layerDef.store] = layer
let highlightedLayer: LayerSpecification = <LayerSpecification>{
id: layerDef.store + '-highlighted',
type: layerDef.type,
source: <GeoJSONSourceSpecification>{
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 && <number>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 = [(<any>layerX.layer).source.data['features'].find(
(f: object) => f['id'] == id || f['properties']['id'] == id
)]
}
else {
featureData = (<any>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']
}
// 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
)
}
}
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)
if (!!this.map.getLayer(layerX.layerNode.store)) {
console.debug('Layer already on the map', layerX.layerNode)
return
}
this.ngZone.runOutsideAngular(
() => {
this.map.addLayer(<LayerSpecification>layerX.layer, this.getLayerNameBefore(layerX.layerNode))
this.map.addLayer(<LayerSpecification>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)
}
this.setAttributions()
}
addLiveLayerListener(layerDef: LayerNode) {
let ws: WebSocketSubject<object> = 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
}
(<GeoJSONSource>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<object> = 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
}
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 = []
}
// 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])
}
}
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 = <any>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(<LayerSpecification>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]
}
}
}
)
}
},
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(<LayerSpecification>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 = <LayerNode>this.layerDefs[layerName]
if (!layerNode.custom && !layerNode.live) {
this.map.setFilter(layerName, ['in', 'status', ...status])
}
}
}
/*
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 = (<any>this.layerDefs)[(<any>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)
}
}
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 = (<any>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 = (<any>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 = (<string[]>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)
}
}
}