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