import { Feature } from "geojson" import { FeatureSource } from "../FeatureSource" import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import { Or } from "../../Tags/Or" import { Overpass } from "../../Osm/Overpass" import { Utils } from "../../../Utils" import { TagsFilter } from "../../Tags/TagsFilter" import { BBox } from "../../BBox" /** * A wrapper around the 'Overpass'-object. * It has more logic and will automatically fetch the data for the right bbox and the active layers */ export default class OverpassFeatureSource implements FeatureSource { /** * The last loaded features, as geojson */ public readonly features: UIEventSource = new UIEventSource(undefined) public readonly runningQuery: UIEventSource = new UIEventSource(false) public readonly timeout: UIEventSource = new UIEventSource(0) private readonly retries: UIEventSource = new UIEventSource(0) private readonly state: { readonly zoom: Store readonly layers: LayerConfig[] readonly widenFactor: number readonly overpassUrl: Store readonly overpassTimeout: Store readonly bounds: Store } private readonly _isActive: Store private readonly padToZoomLevel?: Store private _lastQueryBBox: BBox constructor( state: { readonly layers: LayerConfig[] readonly widenFactor: number readonly zoom: Store readonly overpassUrl: Store readonly overpassTimeout: Store readonly overpassMaxZoom: Store readonly bounds: Store }, options?: { padToTiles?: Store isActive?: Store } ) { this.state = state this._isActive = options?.isActive ?? new ImmutableStore(true) this.padToZoomLevel = options?.padToTiles const self = this state.bounds.addCallbackD((_) => { self.updateAsyncIfNeeded() }) } /** * Creates the 'Overpass'-object for the given layers * @param interpreterUrl * @param layersToDownload * @constructor * @private */ private GetFilter(interpreterUrl: string, layersToDownload: LayerConfig[]): Overpass { let filters: TagsFilter[] = layersToDownload.map((layer) => layer.source.osmTags) filters = Utils.NoNull(filters) if (filters.length === 0) { return undefined } return new Overpass(new Or(filters), [], interpreterUrl, this.state.overpassTimeout) } /** * * @private */ private async updateAsyncIfNeeded(): Promise { if (!this._isActive?.data) { console.log("OverpassFeatureSource: not triggering as not active") return } if (this.runningQuery.data) { console.log("Still running a query, not updating") return undefined } if (this.timeout.data > 0) { console.log("Still in timeout - not updating") return undefined } const requestedBounds = this.state.bounds.data if ( this._lastQueryBBox !== undefined && requestedBounds.isContainedIn(this._lastQueryBBox) ) { return undefined } const result = await this.updateAsync() if (!result) { return } const [bounds, date, updatedLayers] = result this._lastQueryBBox = bounds } /** * Download the relevant data from overpass. Attempt to use a different server; only downloads the relevant layers * @private */ private async updateAsync(): Promise<[BBox, Date, LayerConfig[]]> { let data: any = undefined let date: Date = undefined let lastUsed = 0 const layersToDownload = [] for (const layer of this.state.layers) { if (typeof layer === "string") { throw "A layer was not expanded!" } if (layer.source === undefined) { continue } if (this.state.zoom.data < layer.minzoom) { continue } if (layer.doNotDownload) { continue } if (layer.source === null) { // This is a special layer. Should not have been here console.warn( "OverpassFeatureSource received a layer for which the source is null:", layer.id ) continue } if (layer.source.geojsonSource !== undefined) { // Not our responsibility to download this layer! continue } layersToDownload.push(layer) } if (layersToDownload.length == 0) { return } const self = this const overpassUrls = self.state.overpassUrl.data if (overpassUrls === undefined || overpassUrls.length === 0) { throw "Panic: overpassFeatureSource didn't receive any overpassUrls" } // Note: the bounds are updated between attempts, in case that the user zoomed around let bounds: BBox do { try { bounds = this.state.bounds.data ?.pad(this.state.widenFactor) ?.expandToTileBounds(this.padToZoomLevel?.data) if (bounds === undefined) { return undefined } const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload) if (overpass === undefined) { return undefined } this.runningQuery.setData(true) ;[data, date] = await overpass.queryGeoJson(bounds) } catch (e) { self.retries.data++ self.retries.ping() console.error(`QUERY FAILED due to`, e) await Utils.waitFor(1000) if (lastUsed + 1 < overpassUrls.length) { lastUsed++ console.log("Trying next time with", overpassUrls[lastUsed]) } else { lastUsed = 0 self.timeout.setData(self.retries.data * 5) while (self.timeout.data > 0) { await Utils.waitFor(1000) self.timeout.data-- self.timeout.ping() } } } } while (data === undefined && this._isActive.data) try { if (data === undefined) { return undefined } // Some metatags are delivered by overpass _without_ underscore-prefix; we fix them below // TODO FIXME re-enable this data.features.forEach((f) => SimpleMetaTaggers.objectMetaInfo.applyMetaTagsOnFeature(f)) console.log("Overpass returned", data.features.length, "features") self.features.setData(data.features) return [bounds, date, layersToDownload] } catch (e) { console.error("Got the overpass response, but could not process it: ", e, e.stack) return undefined } finally { self.retries.setData(0) self.runningQuery.setData(false) } } }