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)
 | |
|         }
 | |
|     }
 | |
| }
 |