| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  | import { Utils } from "../../../Utils" | 
					
						
							| 
									
										
										
										
											2023-01-16 15:05:22 +01:00
										 |  |  | import OsmToGeoJson from "osmtogeojson" | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  | import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  | import { Tiles } from "../../../Models/TileRange" | 
					
						
							|  |  |  | import { BBox } from "../../BBox" | 
					
						
							| 
									
										
										
										
											2021-10-07 18:58:45 +02:00
										 |  |  | import { TagsFilter } from "../../Tags/TagsFilter" | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  | import { Feature } from "geojson" | 
					
						
							|  |  |  | import FeatureSourceMerger from "../Sources/FeatureSourceMerger" | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  | import OsmObjectDownloader from "../../Osm/OsmObjectDownloader" | 
					
						
							| 
									
										
										
										
											2023-06-14 20:39:36 +02:00
										 |  |  | import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource" | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-15 02:44:11 +01:00
										 |  |  | /** | 
					
						
							|  |  |  |  * If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile' | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  | export default class OsmFeatureSource extends FeatureSourceMerger { | 
					
						
							|  |  |  |     private readonly _bounds: Store<BBox> | 
					
						
							|  |  |  |     private readonly isActive: Store<boolean> | 
					
						
							| 
									
										
										
										
											2021-11-07 16:34:51 +01:00
										 |  |  |     private readonly _backend: string | 
					
						
							| 
									
										
										
										
											2021-10-07 18:58:45 +02:00
										 |  |  |     private readonly allowedTags: TagsFilter | 
					
						
							| 
									
										
										
										
											2023-06-01 02:52:21 +02:00
										 |  |  |     private options: { | 
					
						
							|  |  |  |         bounds: Store<BBox> | 
					
						
							|  |  |  |         readonly allowedFeatures: TagsFilter | 
					
						
							|  |  |  |         backend?: "https://openstreetmap.org/" | string | 
					
						
							|  |  |  |         /** | 
					
						
							|  |  |  |          * If given: this featureSwitch will not update if the store contains 'false' | 
					
						
							|  |  |  |          */ | 
					
						
							| 
									
										
										
										
											2023-06-14 20:39:36 +02:00
										 |  |  |         isActive?: Store<boolean> | 
					
						
							|  |  |  |         patchRelations?: true | boolean | 
					
						
							| 
									
										
										
										
											2023-06-01 02:52:21 +02:00
										 |  |  |         fullNodeDatabase?: FullNodeDatabaseSource | 
					
						
							| 
									
										
										
										
											2023-06-14 20:39:36 +02:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |     public readonly isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private readonly _downloadedTiles: Set<number> = new Set<number>() | 
					
						
							|  |  |  |     private readonly _downloadedData: Feature[][] = [] | 
					
						
							| 
									
										
										
										
											2023-06-14 20:39:36 +02:00
										 |  |  |     private readonly _patchRelations: boolean | 
					
						
							| 
									
										
										
										
											2022-06-21 22:57:13 +02:00
										 |  |  |     /** | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |      * Downloads data directly from the OSM-api within the given bounds. | 
					
						
							|  |  |  |      * All features which match the TagsFilter 'allowedFeatures' are kept and converted into geojson | 
					
						
							| 
									
										
										
										
											2022-06-21 22:57:13 +02:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  |     constructor(options: { | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |         bounds: Store<BBox> | 
					
						
							|  |  |  |         readonly allowedFeatures: TagsFilter | 
					
						
							|  |  |  |         backend?: "https://openstreetmap.org/" | string | 
					
						
							|  |  |  |         /** | 
					
						
							|  |  |  |          * If given: this featureSwitch will not update if the store contains 'false' | 
					
						
							|  |  |  |          */ | 
					
						
							| 
									
										
										
										
											2023-06-14 20:39:36 +02:00
										 |  |  |         isActive?: Store<boolean> | 
					
						
							|  |  |  |         patchRelations?: true | boolean | 
					
						
							| 
									
										
										
										
											2023-06-01 02:52:21 +02:00
										 |  |  |         fullNodeDatabase?: FullNodeDatabaseSource | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  |     }) { | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |         super() | 
					
						
							| 
									
										
										
										
											2023-06-14 20:39:36 +02:00
										 |  |  |         this.options = options | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |         this._bounds = options.bounds | 
					
						
							|  |  |  |         this.allowedTags = options.allowedFeatures | 
					
						
							|  |  |  |         this.isActive = options.isActive ?? new ImmutableStore(true) | 
					
						
							|  |  |  |         this._backend = options.backend ?? "https://www.openstreetmap.org" | 
					
						
							|  |  |  |         this._bounds.addCallbackAndRunD((bbox) => this.loadData(bbox)) | 
					
						
							| 
									
										
										
										
											2023-05-24 03:26:33 +02:00
										 |  |  |         this._patchRelations = options?.patchRelations ?? true | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |     private async loadData(bbox: BBox) { | 
					
						
							|  |  |  |         if (this.isActive?.data === false) { | 
					
						
							|  |  |  |             console.log("OsmFeatureSource: not triggering: inactive") | 
					
						
							| 
									
										
										
										
											2022-06-21 22:57:13 +02:00
										 |  |  |             return | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |         const z = 15 | 
					
						
							|  |  |  |         const neededTiles = Tiles.tileRangeFrom(bbox, z) | 
					
						
							| 
									
										
										
										
											2022-06-21 22:57:13 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |         if (neededTiles.total == 0) { | 
					
						
							| 
									
										
										
										
											2022-06-21 22:57:13 +02:00
										 |  |  |             return | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         this.isRunning.setData(true) | 
					
						
							|  |  |  |         try { | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |             const tileNumbers = Tiles.MapRange(neededTiles, (x, y) => { | 
					
						
							|  |  |  |                 return Tiles.tile_index(z, x, y) | 
					
						
							|  |  |  |             }) | 
					
						
							|  |  |  |             await Promise.all(tileNumbers.map((i) => this.LoadTile(...Tiles.tile_from_index(i)))) | 
					
						
							| 
									
										
										
										
											2022-06-21 22:57:13 +02:00
										 |  |  |         } catch (e) { | 
					
						
							|  |  |  |             console.error(e) | 
					
						
							|  |  |  |         } finally { | 
					
						
							|  |  |  |             this.isRunning.setData(false) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |     private registerFeatures(features: Feature[]): void { | 
					
						
							|  |  |  |         this._downloadedData.push(features) | 
					
						
							|  |  |  |         super.addData(this._downloadedData) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * The requested tile might only contain part of the relation. | 
					
						
							|  |  |  |      * | 
					
						
							|  |  |  |      * This method will download the full relation and return it as geojson if it was incomplete. | 
					
						
							| 
									
										
										
										
											2023-05-24 03:26:33 +02:00
										 |  |  |      * If the feature is already complete (or is not a relation), the feature will be returned as is | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |      */ | 
					
						
							|  |  |  |     private async patchIncompleteRelations( | 
					
						
							|  |  |  |         feature: { properties: { id: string } }, | 
					
						
							|  |  |  |         originalJson: { elements: { type: "node" | "way" | "relation"; id: number }[] } | 
					
						
							|  |  |  |     ): Promise<any> { | 
					
						
							| 
									
										
										
										
											2023-05-24 03:26:33 +02:00
										 |  |  |         if (!feature.properties.id.startsWith("relation") || !this._patchRelations) { | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |             return feature | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         const relationSpec = originalJson.elements.find( | 
					
						
							|  |  |  |             (f) => "relation/" + f.id === feature.properties.id | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  |         ) | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |         const members: { type: string; ref: number }[] = relationSpec["members"] | 
					
						
							|  |  |  |         for (const member of members) { | 
					
						
							|  |  |  |             const isFound = originalJson.elements.some( | 
					
						
							|  |  |  |                 (f) => f.id === member.ref && f.type === member.type | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  |             ) | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |             if (isFound) { | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |             // This member is missing. We redownload the entire relation instead
 | 
					
						
							|  |  |  |             console.debug("Fetching incomplete relation " + feature.properties.id) | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  |             const dfeature = await new OsmObjectDownloader(this._backend).DownloadObjectAsync( | 
					
						
							|  |  |  |                 feature.properties.id | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             if (dfeature === "deleted") { | 
					
						
							|  |  |  |                 console.warn( | 
					
						
							|  |  |  |                     "This relation has been deleted in the meantime: ", | 
					
						
							|  |  |  |                     feature.properties.id | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             return dfeature.asGeoJson() | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |         return feature | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-01 02:52:21 +02:00
										 |  |  |     private async LoadTile(z: number, x: number, y: number): Promise<void> { | 
					
						
							| 
									
										
										
										
											2023-04-20 01:52:23 +02:00
										 |  |  |         console.log("OsmFeatureSource: loading ", z, x, y, "from", this._backend) | 
					
						
							| 
									
										
										
										
											2022-06-28 01:37:49 +02:00
										 |  |  |         if (z >= 22) { | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  |             throw "This is an absurd high zoom level" | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2022-01-26 21:40:38 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-26 18:04:42 +02:00
										 |  |  |         if (z < 15) { | 
					
						
							| 
									
										
										
										
											2022-01-17 21:33:03 +01:00
										 |  |  |             throw `Zoom ${z} is too much for OSM to handle! Use a higher zoom level!` | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |         const index = Tiles.tile_index(z, x, y) | 
					
						
							|  |  |  |         if (this._downloadedTiles.has(index)) { | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         this._downloadedTiles.add(index) | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         const bbox = BBox.fromTile(z, x, y) | 
					
						
							|  |  |  |         const url = `${this._backend}/api/0.6/map?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |         let error = undefined | 
					
						
							|  |  |  |         try { | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  |             const osmJson = await Utils.downloadJsonCached(url, 2000) | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  |             try { | 
					
						
							| 
									
										
										
										
											2023-06-01 02:52:21 +02:00
										 |  |  |                 this.options?.fullNodeDatabase?.handleOsmJson(osmJson, z, x, y) | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |                 let features = <Feature<any, { id: string }>[]>OsmToGeoJson( | 
					
						
							| 
									
										
										
										
											2022-07-08 03:14:55 +02:00
										 |  |  |                     osmJson, | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  |                     // @ts-ignore
 | 
					
						
							|  |  |  |                     { | 
					
						
							|  |  |  |                         flatProperties: true, | 
					
						
							|  |  |  |                     } | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |                 ).features | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-07 18:58:45 +02:00
										 |  |  |                 // The geojson contains _all_ features at the given location
 | 
					
						
							|  |  |  |                 // We only keep what is needed
 | 
					
						
							| 
									
										
										
										
											2021-10-08 15:11:20 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |                 features = features.filter((feature) => | 
					
						
							| 
									
										
										
										
											2021-10-07 18:58:45 +02:00
										 |  |  |                     this.allowedTags.matchesProperties(feature.properties) | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  |                 ) | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |                 for (let i = 0; i < features.length; i++) { | 
					
						
							|  |  |  |                     features[i] = await this.patchIncompleteRelations(features[i], osmJson) | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |                 } | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  |                 features = Utils.NoNull(features) | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |                 features.forEach((f) => { | 
					
						
							| 
									
										
										
										
											2021-12-17 19:28:05 +01:00
										 |  |  |                     f.properties["_backend"] = this._backend | 
					
						
							|  |  |  |                 }) | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |                 this.registerFeatures(features) | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |             } catch (e) { | 
					
						
							|  |  |  |                 console.error( | 
					
						
							|  |  |  |                     "PANIC: got the tile from the OSM-api, but something crashed handling this tile" | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  |                 error = e | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |         } catch (e) { | 
					
						
							|  |  |  |             console.error( | 
					
						
							|  |  |  |                 "Could not download tile", | 
					
						
							|  |  |  |                 z, | 
					
						
							|  |  |  |                 x, | 
					
						
							|  |  |  |                 y, | 
					
						
							|  |  |  |                 "due to", | 
					
						
							|  |  |  |                 e, | 
					
						
							| 
									
										
										
										
											2023-06-01 02:52:21 +02:00
										 |  |  |                 e === "rate limited" ? "; stopping now" : "; retrying with smaller bounds" | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |             ) | 
					
						
							|  |  |  |             if (e === "rate limited") { | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  |                 return | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2023-03-26 05:58:28 +02:00
										 |  |  |             await Promise.all([ | 
					
						
							|  |  |  |                 this.LoadTile(z + 1, x * 2, y * 2), | 
					
						
							|  |  |  |                 this.LoadTile(z + 1, 1 + x * 2, y * 2), | 
					
						
							|  |  |  |                 this.LoadTile(z + 1, x * 2, 1 + y * 2), | 
					
						
							|  |  |  |                 this.LoadTile(z + 1, 1 + x * 2, 1 + y * 2), | 
					
						
							|  |  |  |             ]) | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-24 18:12:39 +02:00
										 |  |  |         if (error !== undefined) { | 
					
						
							|  |  |  |             throw error | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-09-28 17:30:48 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | } |