forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			170 lines
		
	
	
	
		
			6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			170 lines
		
	
	
	
		
			6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { Tag } from "../../Tags/Tag"
 | 
						|
import { OsmCreateAction } from "./OsmChangeAction"
 | 
						|
import { Changes } from "../Changes"
 | 
						|
import { ChangeDescription } from "./ChangeDescription"
 | 
						|
import { And } from "../../Tags/And"
 | 
						|
import { OsmWay } from "../OsmObject"
 | 
						|
import { GeoOperations } from "../../GeoOperations"
 | 
						|
 | 
						|
export default class CreateNewNodeAction extends OsmCreateAction {
 | 
						|
    /**
 | 
						|
     * Maps previously created points onto their assigned ID, to reuse the point if uplaoded
 | 
						|
     * "lat,lon" --> id
 | 
						|
     */
 | 
						|
    private static readonly previouslyCreatedPoints = new Map<string, number>()
 | 
						|
    public newElementId: string = undefined
 | 
						|
    public newElementIdNumber: number = undefined
 | 
						|
    private readonly _basicTags: Tag[]
 | 
						|
    private readonly _lat: number
 | 
						|
    private readonly _lon: number
 | 
						|
    private readonly _snapOnto: OsmWay
 | 
						|
    private readonly _reusePointDistance: number
 | 
						|
    private meta: { changeType: "create" | "import"; theme: string; specialMotivation?: string }
 | 
						|
    private readonly _reusePreviouslyCreatedPoint: boolean
 | 
						|
 | 
						|
    constructor(
 | 
						|
        basicTags: Tag[],
 | 
						|
        lat: number,
 | 
						|
        lon: number,
 | 
						|
        options: {
 | 
						|
            allowReuseOfPreviouslyCreatedPoints?: boolean
 | 
						|
            snapOnto?: OsmWay
 | 
						|
            reusePointWithinMeters?: number
 | 
						|
            theme: string
 | 
						|
            changeType: "create" | "import" | null
 | 
						|
            specialMotivation?: string
 | 
						|
        }
 | 
						|
    ) {
 | 
						|
        super(null, basicTags !== undefined && basicTags.length > 0)
 | 
						|
        this._basicTags = basicTags
 | 
						|
        this._lat = lat
 | 
						|
        this._lon = lon
 | 
						|
        if (lat === undefined || lon === undefined) {
 | 
						|
            throw "Lat or lon are undefined!"
 | 
						|
        }
 | 
						|
        this._snapOnto = options?.snapOnto
 | 
						|
        this._reusePointDistance = options?.reusePointWithinMeters ?? 1
 | 
						|
        this._reusePreviouslyCreatedPoint =
 | 
						|
            options?.allowReuseOfPreviouslyCreatedPoints ?? basicTags.length === 0
 | 
						|
        this.meta = {
 | 
						|
            theme: options.theme,
 | 
						|
            changeType: options.changeType,
 | 
						|
            specialMotivation: options.specialMotivation,
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
 | 
						|
        if (this._reusePreviouslyCreatedPoint) {
 | 
						|
            const key = this._lat + "," + this._lon
 | 
						|
            const prev = CreateNewNodeAction.previouslyCreatedPoints
 | 
						|
            if (prev.has(key)) {
 | 
						|
                this.newElementIdNumber = prev.get(key)
 | 
						|
                this.newElementId = "node/" + this.newElementIdNumber
 | 
						|
                return []
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        const id = changes.getNewID()
 | 
						|
        const properties = {
 | 
						|
            id: "node/" + id,
 | 
						|
        }
 | 
						|
        this.setElementId(id)
 | 
						|
        for (const kv of this._basicTags) {
 | 
						|
            if (typeof kv.value !== "string") {
 | 
						|
                throw (
 | 
						|
                    "Invalid value: don't use non-string value in a preset. The tag " +
 | 
						|
                    kv.key +
 | 
						|
                    "=" +
 | 
						|
                    kv.value +
 | 
						|
                    " is not a string, the value is a " +
 | 
						|
                    typeof kv.value
 | 
						|
                )
 | 
						|
            }
 | 
						|
            properties[kv.key] = kv.value
 | 
						|
        }
 | 
						|
 | 
						|
        const newPointChange: ChangeDescription = {
 | 
						|
            tags: new And(this._basicTags).asChange(properties),
 | 
						|
            type: "node",
 | 
						|
            id: id,
 | 
						|
            changes: {
 | 
						|
                lat: this._lat,
 | 
						|
                lon: this._lon,
 | 
						|
            },
 | 
						|
            meta: this.meta,
 | 
						|
        }
 | 
						|
        if (this._snapOnto === undefined) {
 | 
						|
            return [newPointChange]
 | 
						|
        }
 | 
						|
 | 
						|
        // Project the point onto the way
 | 
						|
        console.log("Snapping a node onto an existing way...")
 | 
						|
        const geojson = this._snapOnto.asGeoJson()
 | 
						|
        const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat])
 | 
						|
        const projectedCoor = <[number, number]>projected.geometry.coordinates
 | 
						|
        const index = projected.properties.index
 | 
						|
        // We check that it isn't close to an already existing point
 | 
						|
        let reusedPointId = undefined
 | 
						|
        let outerring: [number, number][]
 | 
						|
 | 
						|
        if (geojson.geometry.type === "LineString") {
 | 
						|
            outerring = <[number, number][]>geojson.geometry.coordinates
 | 
						|
        } else if (geojson.geometry.type === "Polygon") {
 | 
						|
            outerring = <[number, number][]>geojson.geometry.coordinates[0]
 | 
						|
        }
 | 
						|
 | 
						|
        const prev = outerring[index]
 | 
						|
        if (GeoOperations.distanceBetween(prev, projectedCoor) < this._reusePointDistance) {
 | 
						|
            // We reuse this point instead!
 | 
						|
            reusedPointId = this._snapOnto.nodes[index]
 | 
						|
        }
 | 
						|
        const next = outerring[index + 1]
 | 
						|
        if (GeoOperations.distanceBetween(next, projectedCoor) < this._reusePointDistance) {
 | 
						|
            // We reuse this point instead!
 | 
						|
            reusedPointId = this._snapOnto.nodes[index + 1]
 | 
						|
        }
 | 
						|
        if (reusedPointId !== undefined) {
 | 
						|
            this.setElementId(reusedPointId)
 | 
						|
            return [
 | 
						|
                {
 | 
						|
                    tags: new And(this._basicTags).asChange(properties),
 | 
						|
                    type: "node",
 | 
						|
                    id: reusedPointId,
 | 
						|
                    meta: this.meta,
 | 
						|
                },
 | 
						|
            ]
 | 
						|
        }
 | 
						|
 | 
						|
        const locations = [
 | 
						|
            ...this._snapOnto.coordinates.map(([lat, lon]) => <[number, number]>[lon, lat]),
 | 
						|
        ]
 | 
						|
        const ids = [...this._snapOnto.nodes]
 | 
						|
 | 
						|
        locations.splice(index + 1, 0, [this._lon, this._lat])
 | 
						|
        ids.splice(index + 1, 0, id)
 | 
						|
 | 
						|
        // Allright, we have to insert a new point in the way
 | 
						|
        return [
 | 
						|
            newPointChange,
 | 
						|
            {
 | 
						|
                type: "way",
 | 
						|
                id: this._snapOnto.id,
 | 
						|
                changes: {
 | 
						|
                    coordinates: locations,
 | 
						|
                    nodes: ids,
 | 
						|
                },
 | 
						|
                meta: this.meta,
 | 
						|
            },
 | 
						|
        ]
 | 
						|
    }
 | 
						|
 | 
						|
    private setElementId(id: number) {
 | 
						|
        this.newElementIdNumber = id
 | 
						|
        this.newElementId = "node/" + id
 | 
						|
        if (!this._reusePreviouslyCreatedPoint) {
 | 
						|
            return
 | 
						|
        }
 | 
						|
        const key = this._lat + "," + this._lon
 | 
						|
        CreateNewNodeAction.previouslyCreatedPoints.set(key, id)
 | 
						|
    }
 | 
						|
}
 |