forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			218 lines
		
	
	
	
		
			7.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			218 lines
		
	
	
	
		
			7.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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<Feature[]> = new UIEventSource(undefined)
 | 
						|
 | 
						|
    public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false)
 | 
						|
    public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0)
 | 
						|
 | 
						|
    private readonly retries: UIEventSource<number> = new UIEventSource<number>(0)
 | 
						|
 | 
						|
    private readonly state: {
 | 
						|
        readonly zoom: Store<number>
 | 
						|
        readonly layers: LayerConfig[]
 | 
						|
        readonly widenFactor: number
 | 
						|
        readonly overpassUrl: Store<string[]>
 | 
						|
        readonly overpassTimeout: Store<number>
 | 
						|
        readonly bounds: Store<BBox>
 | 
						|
    }
 | 
						|
    private readonly _isActive: Store<boolean>
 | 
						|
    private readonly padToZoomLevel?: Store<number>
 | 
						|
    private _lastQueryBBox: BBox
 | 
						|
 | 
						|
    constructor(
 | 
						|
        state: {
 | 
						|
            readonly layers: LayerConfig[]
 | 
						|
            readonly widenFactor: number
 | 
						|
            readonly zoom: Store<number>
 | 
						|
            readonly overpassUrl: Store<string[]>
 | 
						|
            readonly overpassTimeout: Store<number>
 | 
						|
            readonly overpassMaxZoom: Store<number>
 | 
						|
            readonly bounds: Store<BBox>
 | 
						|
        },
 | 
						|
        options?: {
 | 
						|
            padToTiles?: Store<number>
 | 
						|
            isActive?: Store<boolean>
 | 
						|
        }
 | 
						|
    ) {
 | 
						|
        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<void> {
 | 
						|
        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)
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |