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 readonly isUploading: Store } private readonly backend: string private historyCache = new Map>() constructor( backend: string = "https://www.openstreetmap.org", changes?: { readonly pendingChanges: UIEventSource readonly isUploading: Store } ) { 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 async DownloadObjectAsync(id: WayId, maxCacheAgeInSecs?: number): Promise async DownloadObjectAsync( id: RelationId, maxCacheAgeInSecs?: number ): Promise async DownloadObjectAsync(id: OsmId, maxCacheAgeInSecs?: number): Promise async DownloadObjectAsync( id: string, maxCacheAgeInSecs?: number ): Promise 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 public DownloadHistory(id: WayId): UIEventSource public DownloadHistory(id: RelationId): UIEventSource public DownloadHistory(id: OsmId): UIEventSource public DownloadHistory(id: string): UIEventSource { 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([]) 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 { 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 { 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 { 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(object, pendingChange.changes) break case "way": this.applyWayChange(object, pendingChange.changes) break case "relation": this.applyRelationChange(object, 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 { 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" } }