1276 lines
46 KiB
TypeScript
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)
|
|
}
|
|
}
|
|
}
|