forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			243 lines
		
	
	
	
		
			8.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			243 lines
		
	
	
	
		
			8.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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(
 | |
|         backend: string = "https://www.openstreetmap.org",
 | |
|         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">
 | |
| 
 | |
|     async DownloadObjectAsync(id: WayId, maxCacheAgeInSecs?: number): Promise<OsmWay | "deleted">
 | |
| 
 | |
|     async DownloadObjectAsync(
 | |
|         id: RelationId,
 | |
|         maxCacheAgeInSecs?: number
 | |
|     ): Promise<OsmRelation | undefined>
 | |
| 
 | |
|     async DownloadObjectAsync(id: OsmId, maxCacheAgeInSecs?: number): Promise<OsmObject | "deleted">
 | |
| 
 | |
|     async DownloadObjectAsync(
 | |
|         id: string,
 | |
|         maxCacheAgeInSecs?: number
 | |
|     ): Promise<OsmObject | "deleted">
 | |
| 
 | |
|     async DownloadObjectAsync(id: string, maxCacheAgeInSecs?: number) {
 | |
|         // Wait until uploading is done
 | |
|         if (this._changes) {
 | |
|             await this._changes.isUploading.AsPromise((o) => o === false)
 | |
|         }
 | |
| 
 | |
|         const splitted = id.split("/")
 | |
|         const type = splitted[0]
 | |
|         const idN = Number(splitted[1])
 | |
|         let obj: OsmObject | "deleted"
 | |
|         if (idN < 0) {
 | |
|             obj = this.constructObject(<"node" | "way" | "relation">type, idN)
 | |
|         } else {
 | |
|             obj = await this.RawDownloadObjectAsync(type, idN, maxCacheAgeInSecs)
 | |
|         }
 | |
|         if (obj === "deleted") {
 | |
|             return obj
 | |
|         }
 | |
|         return await this.applyPendingChanges(obj)
 | |
|     }
 | |
| 
 | |
|     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
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     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"
 | |
|     }
 | |
| }
 |