| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  | import { Utils } from "../../Utils" | 
					
						
							|  |  |  | import { OsmNode, OsmObject, OsmRelation, OsmWay } from "./OsmObject" | 
					
						
							|  |  |  | import { NodeId, OsmId, RelationId, WayId } from "../../Models/OsmFeature" | 
					
						
							|  |  |  | import { Store, UIEventSource } from "../UIEventSource" | 
					
						
							|  |  |  | import { ChangeDescription } from "./Actions/ChangeDescription" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * The OSM-Object downloader downloads the latest version of the object, but applies 'pendingchanges' to them, | 
					
						
							|  |  |  |  * so that we always have a consistent view | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export default class OsmObjectDownloader { | 
					
						
							|  |  |  |     private readonly _changes?: { | 
					
						
							|  |  |  |         readonly pendingChanges: UIEventSource<ChangeDescription[]> | 
					
						
							|  |  |  |         readonly isUploading: Store<boolean> | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     private readonly backend: string | 
					
						
							|  |  |  |     private historyCache = new Map<string, UIEventSource<OsmObject[]>>() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     constructor( | 
					
						
							| 
									
										
										
										
											2023-05-17 13:48:38 +02:00
										 |  |  |         backend: string = "https://www.openstreetmap.org", | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  |         changes?: { | 
					
						
							|  |  |  |             readonly pendingChanges: UIEventSource<ChangeDescription[]> | 
					
						
							|  |  |  |             readonly isUploading: Store<boolean> | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     ) { | 
					
						
							|  |  |  |         this._changes = changes | 
					
						
							|  |  |  |         if (!backend.endsWith("/")) { | 
					
						
							|  |  |  |             backend += "/" | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         if (!backend.startsWith("http")) { | 
					
						
							|  |  |  |             throw "Backend URL must begin with http" | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         this.backend = backend | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async DownloadObjectAsync(id: NodeId, maxCacheAgeInSecs?: number): Promise<OsmNode | "deleted"> | 
					
						
							| 
									
										
										
										
											2023-04-20 17:42:07 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  |     async DownloadObjectAsync(id: WayId, maxCacheAgeInSecs?: number): Promise<OsmWay | "deleted"> | 
					
						
							| 
									
										
										
										
											2023-04-20 17:42:07 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  |     async DownloadObjectAsync( | 
					
						
							|  |  |  |         id: RelationId, | 
					
						
							|  |  |  |         maxCacheAgeInSecs?: number | 
					
						
							|  |  |  |     ): Promise<OsmRelation | undefined> | 
					
						
							| 
									
										
										
										
											2023-04-20 17:42:07 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  |     async DownloadObjectAsync(id: OsmId, maxCacheAgeInSecs?: number): Promise<OsmObject | "deleted"> | 
					
						
							| 
									
										
										
										
											2023-04-20 17:42:07 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  |     async DownloadObjectAsync( | 
					
						
							|  |  |  |         id: string, | 
					
						
							|  |  |  |         maxCacheAgeInSecs?: number | 
					
						
							|  |  |  |     ): Promise<OsmObject | "deleted"> | 
					
						
							| 
									
										
										
										
											2023-04-20 17:42:07 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     async DownloadObjectAsync(id: string, maxCacheAgeInSecs?: number) { | 
					
						
							|  |  |  |         // Wait until uploading is done
 | 
					
						
							|  |  |  |         if (this._changes) { | 
					
						
							|  |  |  |             await this._changes.isUploading.AsPromise((o) => o === false) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  |         const splitted = id.split("/") | 
					
						
							|  |  |  |         const type = splitted[0] | 
					
						
							|  |  |  |         const idN = Number(splitted[1]) | 
					
						
							| 
									
										
										
										
											2023-04-20 17:42:07 +02:00
										 |  |  |         let obj: OsmObject | "deleted" | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  |         if (idN < 0) { | 
					
						
							| 
									
										
										
										
											2023-04-20 17:42:07 +02:00
										 |  |  |             obj = this.constructObject(<"node" | "way" | "relation">type, idN) | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |             obj = await this.RawDownloadObjectAsync(type, idN, maxCacheAgeInSecs) | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-04-20 17:42:07 +02:00
										 |  |  |         if (obj === "deleted") { | 
					
						
							|  |  |  |             return obj | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-04-20 17:42:07 +02:00
										 |  |  |         return await this.applyPendingChanges(obj) | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public DownloadHistory(id: NodeId): UIEventSource<OsmNode[]> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public DownloadHistory(id: WayId): UIEventSource<OsmWay[]> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public DownloadHistory(id: RelationId): UIEventSource<OsmRelation[]> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public DownloadHistory(id: OsmId): UIEventSource<OsmObject[]> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public DownloadHistory(id: string): UIEventSource<OsmObject[]> { | 
					
						
							|  |  |  |         if (this.historyCache.has(id)) { | 
					
						
							|  |  |  |             return this.historyCache.get(id) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         const splitted = id.split("/") | 
					
						
							|  |  |  |         const type = splitted[0] | 
					
						
							|  |  |  |         const idN = Number(splitted[1]) | 
					
						
							|  |  |  |         const src = new UIEventSource<OsmObject[]>([]) | 
					
						
							|  |  |  |         this.historyCache.set(id, src) | 
					
						
							|  |  |  |         Utils.downloadJsonCached( | 
					
						
							|  |  |  |             `${this.backend}api/0.6/${type}/${idN}/history`, | 
					
						
							|  |  |  |             10 * 60 * 1000 | 
					
						
							|  |  |  |         ).then((data) => { | 
					
						
							|  |  |  |             const elements: any[] = data.elements | 
					
						
							|  |  |  |             const osmObjects: OsmObject[] = [] | 
					
						
							|  |  |  |             for (const element of elements) { | 
					
						
							|  |  |  |                 let osmObject: OsmObject = null | 
					
						
							|  |  |  |                 element.nodes = [] | 
					
						
							|  |  |  |                 switch (type) { | 
					
						
							|  |  |  |                     case "node": | 
					
						
							|  |  |  |                         osmObject = new OsmNode(idN, element) | 
					
						
							|  |  |  |                         break | 
					
						
							|  |  |  |                     case "way": | 
					
						
							|  |  |  |                         osmObject = new OsmWay(idN, element) | 
					
						
							|  |  |  |                         break | 
					
						
							|  |  |  |                     case "relation": | 
					
						
							|  |  |  |                         osmObject = new OsmRelation(idN, element) | 
					
						
							|  |  |  |                         break | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 osmObject?.SaveExtraData(element, []) | 
					
						
							|  |  |  |                 osmObjects.push(osmObject) | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             src.setData(osmObjects) | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         return src | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Downloads the ways that are using this node. | 
					
						
							|  |  |  |      * Beware: their geometry will be incomplete! | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public async DownloadReferencingWays(id: string): Promise<OsmWay[]> { | 
					
						
							|  |  |  |         const data = await Utils.downloadJsonCached(`${this.backend}api/0.6/${id}/ways`, 60 * 1000) | 
					
						
							|  |  |  |         return data.elements.map((wayInfo) => new OsmWay(wayInfo.id, wayInfo)) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Downloads the relations that are using this feature. | 
					
						
							|  |  |  |      * Beware: their geometry will be incomplete! | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public async DownloadReferencingRelations(id: string): Promise<OsmRelation[]> { | 
					
						
							|  |  |  |         const data = await Utils.downloadJsonCached( | 
					
						
							|  |  |  |             `${this.backend}api/0.6/${id}/relations`, | 
					
						
							|  |  |  |             60 * 1000 | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         return data.elements.map((wayInfo) => { | 
					
						
							|  |  |  |             const rel = new OsmRelation(wayInfo.id, wayInfo) | 
					
						
							|  |  |  |             rel.SaveExtraData(wayInfo, undefined) | 
					
						
							|  |  |  |             return rel | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-04-20 17:42:07 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     private applyNodeChange(object: OsmNode, change: { lat: number; lon: number }) { | 
					
						
							|  |  |  |         object.lat = change.lat | 
					
						
							|  |  |  |         object.lon = change.lon | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private applyWayChange(object: OsmWay, change: { nodes: number[]; coordinates }) { | 
					
						
							|  |  |  |         object.nodes = change.nodes | 
					
						
							|  |  |  |         object.coordinates = change.coordinates.map(([lat, lon]) => [lon, lat]) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private applyRelationChange( | 
					
						
							|  |  |  |         object: OsmRelation, | 
					
						
							|  |  |  |         change: { members: { type: "node" | "way" | "relation"; ref: number; role: string }[] } | 
					
						
							|  |  |  |     ) { | 
					
						
							|  |  |  |         object.members = change.members | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private async applyPendingChanges(object: OsmObject): Promise<OsmObject | "deleted"> { | 
					
						
							|  |  |  |         if (!this._changes) { | 
					
						
							|  |  |  |             return object | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         const pendingChanges = this._changes.pendingChanges.data | 
					
						
							|  |  |  |         for (const pendingChange of pendingChanges) { | 
					
						
							|  |  |  |             if (object.id !== pendingChange.id || object.type !== pendingChange.type) { | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             if (pendingChange.doDelete) { | 
					
						
							|  |  |  |                 return "deleted" | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             if (pendingChange.tags) { | 
					
						
							|  |  |  |                 for (const { k, v } of pendingChange.tags) { | 
					
						
							|  |  |  |                     if (v === undefined) { | 
					
						
							|  |  |  |                         delete object.tags[k] | 
					
						
							|  |  |  |                     } else { | 
					
						
							|  |  |  |                         object.tags[k] = v | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (pendingChange.changes) { | 
					
						
							|  |  |  |                 switch (pendingChange.type) { | 
					
						
							|  |  |  |                     case "node": | 
					
						
							|  |  |  |                         this.applyNodeChange(<OsmNode>object, <any>pendingChange.changes) | 
					
						
							|  |  |  |                         break | 
					
						
							|  |  |  |                     case "way": | 
					
						
							|  |  |  |                         this.applyWayChange(<OsmWay>object, <any>pendingChange.changes) | 
					
						
							|  |  |  |                         break | 
					
						
							|  |  |  |                     case "relation": | 
					
						
							|  |  |  |                         this.applyRelationChange(<OsmRelation>object, <any>pendingChange.changes) | 
					
						
							|  |  |  |                         break | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return object | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Creates an empty object of the specified type with the specified id. | 
					
						
							|  |  |  |      * We assume that the pending changes will be applied on them, filling in details such as coordinates, tags, ... | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     private constructObject(type: "node" | "way" | "relation", id: number): OsmObject { | 
					
						
							|  |  |  |         switch (type) { | 
					
						
							|  |  |  |             case "node": | 
					
						
							|  |  |  |                 return new OsmNode(id) | 
					
						
							|  |  |  |             case "way": | 
					
						
							|  |  |  |                 return new OsmWay(id) | 
					
						
							|  |  |  |             case "relation": | 
					
						
							|  |  |  |                 return new OsmRelation(id) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private async RawDownloadObjectAsync( | 
					
						
							|  |  |  |         type: string, | 
					
						
							|  |  |  |         idN: number, | 
					
						
							|  |  |  |         maxCacheAgeInSecs?: number | 
					
						
							|  |  |  |     ): Promise<OsmObject | "deleted"> { | 
					
						
							|  |  |  |         const full = type !== "node" ? "/full" : "" | 
					
						
							|  |  |  |         const url = `${this.backend}api/0.6/${type}/${idN}${full}` | 
					
						
							|  |  |  |         const rawData = await Utils.downloadJsonCachedAdvanced( | 
					
						
							|  |  |  |             url, | 
					
						
							|  |  |  |             (maxCacheAgeInSecs ?? 10) * 1000 | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         if (rawData["error"] !== undefined && rawData["statuscode"] === 410) { | 
					
						
							|  |  |  |             return "deleted" | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         // A full query might contain more then just the requested object (e.g. nodes that are part of a way, where we only want the way)
 | 
					
						
							|  |  |  |         const parsed = OsmObject.ParseObjects(rawData["content"].elements) | 
					
						
							|  |  |  |         // Lets fetch the object we need
 | 
					
						
							|  |  |  |         for (const osmObject of parsed) { | 
					
						
							|  |  |  |             if (osmObject.type !== type) { | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             if (osmObject.id !== idN) { | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             // Found the one!
 | 
					
						
							|  |  |  |             return osmObject | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         throw "PANIC: requested object is not part of the response" | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-04-20 03:58:31 +02:00
										 |  |  | } |