forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			565 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			565 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import OsmChangeAction from "./OsmChangeAction"
 | 
						|
import { Changes } from "../Changes"
 | 
						|
import { ChangeDescription } from "./ChangeDescription"
 | 
						|
import { Tag } from "../../Tags/Tag"
 | 
						|
import FeatureSource from "../../FeatureSource/FeatureSource"
 | 
						|
import { OsmNode, OsmObject, OsmWay } from "../OsmObject"
 | 
						|
import { GeoOperations } from "../../GeoOperations"
 | 
						|
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
 | 
						|
import CreateNewNodeAction from "./CreateNewNodeAction"
 | 
						|
import ChangeTagAction from "./ChangeTagAction"
 | 
						|
import { And } from "../../Tags/And"
 | 
						|
import { Utils } from "../../../Utils"
 | 
						|
import { OsmConnection } from "../OsmConnection"
 | 
						|
import { Feature } from "@turf/turf"
 | 
						|
import FeaturePipeline from "../../FeatureSource/FeaturePipeline"
 | 
						|
import { Geometry, LineString, Point, Polygon } from "geojson"
 | 
						|
 | 
						|
export default class ReplaceGeometryAction extends OsmChangeAction {
 | 
						|
    /**
 | 
						|
     * The target feature - mostly used for the metadata
 | 
						|
     */
 | 
						|
    private readonly feature: any
 | 
						|
    private readonly state: {
 | 
						|
        osmConnection: OsmConnection
 | 
						|
        featurePipeline: FeaturePipeline
 | 
						|
    }
 | 
						|
    private readonly wayToReplaceId: string
 | 
						|
    private readonly theme: string
 | 
						|
    /**
 | 
						|
     * The target coordinates that should end up in OpenStreetMap.
 | 
						|
     * This is identical to either this.feature.geometry.coordinates or -in case of a polygon- feature.geometry.coordinates[0]
 | 
						|
     * Format: [lon, lat]
 | 
						|
     */
 | 
						|
    private readonly targetCoordinates: [number, number][]
 | 
						|
    /**
 | 
						|
     * If a target coordinate is close to another target coordinate, 'identicalTo' will point to the first index.
 | 
						|
     */
 | 
						|
    private readonly identicalTo: number[]
 | 
						|
    private readonly newTags: Tag[] | undefined
 | 
						|
 | 
						|
    constructor(
 | 
						|
        state: {
 | 
						|
            osmConnection: OsmConnection
 | 
						|
            featurePipeline: FeaturePipeline
 | 
						|
        },
 | 
						|
        feature: any,
 | 
						|
        wayToReplaceId: string,
 | 
						|
        options: {
 | 
						|
            theme: string
 | 
						|
            newTags?: Tag[]
 | 
						|
        }
 | 
						|
    ) {
 | 
						|
        super(wayToReplaceId, false)
 | 
						|
        this.state = state
 | 
						|
        this.feature = feature
 | 
						|
        this.wayToReplaceId = wayToReplaceId
 | 
						|
        this.theme = options.theme
 | 
						|
 | 
						|
        const geom = this.feature.geometry
 | 
						|
        let coordinates: [number, number][]
 | 
						|
        if (geom.type === "LineString") {
 | 
						|
            coordinates = geom.coordinates
 | 
						|
        } else if (geom.type === "Polygon") {
 | 
						|
            coordinates = geom.coordinates[0]
 | 
						|
        }
 | 
						|
        this.targetCoordinates = coordinates
 | 
						|
 | 
						|
        this.identicalTo = coordinates.map((_) => undefined)
 | 
						|
 | 
						|
        for (let i = 0; i < coordinates.length; i++) {
 | 
						|
            if (this.identicalTo[i] !== undefined) {
 | 
						|
                continue
 | 
						|
            }
 | 
						|
            for (let j = i + 1; j < coordinates.length; j++) {
 | 
						|
                const d = GeoOperations.distanceBetween(coordinates[i], coordinates[j])
 | 
						|
                if (d < 0.1) {
 | 
						|
                    this.identicalTo[j] = i
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        this.newTags = options.newTags
 | 
						|
    }
 | 
						|
 | 
						|
    // noinspection JSUnusedGlobalSymbols
 | 
						|
    public async getPreview(): Promise<FeatureSource> {
 | 
						|
        const { closestIds, allNodesById, detachedNodes, reprojectedNodes } =
 | 
						|
            await this.GetClosestIds()
 | 
						|
        const preview: Feature<Geometry>[] = closestIds.map((newId, i) => {
 | 
						|
            if (this.identicalTo[i] !== undefined) {
 | 
						|
                return undefined
 | 
						|
            }
 | 
						|
 | 
						|
            if (newId === undefined) {
 | 
						|
                return {
 | 
						|
                    type: "Feature",
 | 
						|
                    properties: {
 | 
						|
                        newpoint: "yes",
 | 
						|
                        id: "replace-geometry-move-" + i,
 | 
						|
                    },
 | 
						|
                    geometry: {
 | 
						|
                        type: "Point",
 | 
						|
                        coordinates: this.targetCoordinates[i],
 | 
						|
                    },
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            const origNode = allNodesById.get(newId)
 | 
						|
            return {
 | 
						|
                type: "Feature",
 | 
						|
                properties: {
 | 
						|
                    move: "yes",
 | 
						|
                    "osm-id": newId,
 | 
						|
                    id: "replace-geometry-move-" + i,
 | 
						|
                    "original-node-tags": JSON.stringify(origNode.tags),
 | 
						|
                },
 | 
						|
                geometry: {
 | 
						|
                    type: "LineString",
 | 
						|
                    coordinates: [[origNode.lon, origNode.lat], this.targetCoordinates[i]],
 | 
						|
                },
 | 
						|
            }
 | 
						|
        })
 | 
						|
 | 
						|
        reprojectedNodes.forEach(({ newLat, newLon, nodeId }) => {
 | 
						|
            const origNode = allNodesById.get(nodeId)
 | 
						|
            const feature: Feature<LineString> = {
 | 
						|
                type: "Feature",
 | 
						|
                properties: {
 | 
						|
                    move: "yes",
 | 
						|
                    reprojection: "yes",
 | 
						|
                    "osm-id": nodeId,
 | 
						|
                    id: "replace-geometry-reproject-" + nodeId,
 | 
						|
                    "original-node-tags": JSON.stringify(origNode.tags),
 | 
						|
                },
 | 
						|
                geometry: {
 | 
						|
                    type: "LineString",
 | 
						|
                    coordinates: [
 | 
						|
                        [origNode.lon, origNode.lat],
 | 
						|
                        [newLon, newLat],
 | 
						|
                    ],
 | 
						|
                },
 | 
						|
            }
 | 
						|
            preview.push(feature)
 | 
						|
        })
 | 
						|
 | 
						|
        detachedNodes.forEach(({ reason }, id) => {
 | 
						|
            const origNode = allNodesById.get(id)
 | 
						|
            const feature: Feature<Point> = {
 | 
						|
                type: "Feature",
 | 
						|
                properties: {
 | 
						|
                    detach: "yes",
 | 
						|
                    id: "replace-geometry-detach-" + id,
 | 
						|
                    "detach-reason": reason,
 | 
						|
                    "original-node-tags": JSON.stringify(origNode.tags),
 | 
						|
                },
 | 
						|
                geometry: {
 | 
						|
                    type: "Point",
 | 
						|
                    coordinates: [origNode.lon, origNode.lat],
 | 
						|
                },
 | 
						|
            }
 | 
						|
            preview.push(feature)
 | 
						|
        })
 | 
						|
 | 
						|
        return StaticFeatureSource.fromGeojson(Utils.NoNull(preview))
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * For 'this.feature`, gets a corresponding closest node that alreay exsists.
 | 
						|
     *
 | 
						|
     * This method contains the main logic for this module, as it decides which node gets moved where.
 | 
						|
     *
 | 
						|
     */
 | 
						|
    public async GetClosestIds(): Promise<{
 | 
						|
        // A list of the same length as targetCoordinates, containing which OSM-point to move. If undefined, a new point will be created
 | 
						|
        closestIds: number[]
 | 
						|
        allNodesById: Map<number, OsmNode>
 | 
						|
        osmWay: OsmWay
 | 
						|
        detachedNodes: Map<
 | 
						|
            number,
 | 
						|
            {
 | 
						|
                reason: string
 | 
						|
                hasTags: boolean
 | 
						|
            }
 | 
						|
        >
 | 
						|
        reprojectedNodes: Map<
 | 
						|
            number,
 | 
						|
            {
 | 
						|
                /*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
 | 
						|
                projectAfterIndex: number
 | 
						|
                distance: number
 | 
						|
                newLat: number
 | 
						|
                newLon: number
 | 
						|
                nodeId: number
 | 
						|
            }
 | 
						|
        >
 | 
						|
    }> {
 | 
						|
        // TODO FIXME: if a new point has to be created, snap to already existing ways
 | 
						|
 | 
						|
        const nodeDb = this.state.featurePipeline.fullNodeDatabase
 | 
						|
        if (nodeDb === undefined) {
 | 
						|
            throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
 | 
						|
        }
 | 
						|
        const self = this
 | 
						|
        let parsed: OsmObject[]
 | 
						|
        {
 | 
						|
            // Gather the needed OsmObjects
 | 
						|
            const splitted = this.wayToReplaceId.split("/")
 | 
						|
            const type = splitted[0]
 | 
						|
            const idN = Number(splitted[1])
 | 
						|
            if (idN < 0 || type !== "way") {
 | 
						|
                throw "Invalid ID to conflate: " + this.wayToReplaceId
 | 
						|
            }
 | 
						|
            const url = `${
 | 
						|
                this.state.osmConnection?._oauth_config?.url ?? "https://openstreetmap.org"
 | 
						|
            }/api/0.6/${this.wayToReplaceId}/full`
 | 
						|
            const rawData = await Utils.downloadJsonCached(url, 1000)
 | 
						|
            parsed = OsmObject.ParseObjects(rawData.elements)
 | 
						|
        }
 | 
						|
        const allNodes = parsed.filter((o) => o.type === "node")
 | 
						|
        const osmWay = <OsmWay>parsed[parsed.length - 1]
 | 
						|
        if (osmWay.type !== "way") {
 | 
						|
            throw "WEIRD: expected an OSM-way as last element here!"
 | 
						|
        }
 | 
						|
        const allNodesById = new Map<number, OsmNode>()
 | 
						|
        for (const node of allNodes) {
 | 
						|
            allNodesById.set(node.id, <OsmNode>node)
 | 
						|
        }
 | 
						|
        /**
 | 
						|
         * For every already existing OSM-point, we calculate:
 | 
						|
         *
 | 
						|
         * - the distance to every target point.
 | 
						|
         * - Wether this node has (other) parent ways, which might restrict movement
 | 
						|
         * - Wether this node has tags set
 | 
						|
         *
 | 
						|
         * Having tags and/or being connected to another way indicate that there is some _relation_ with objects in the neighbourhood.
 | 
						|
         *
 | 
						|
         * The Replace-geometry action should try its best to honour these. Some 'wiggling' is allowed (e.g. moving an entrance a bit), but these relations should not be broken.l
 | 
						|
         */
 | 
						|
        const distances = new Map<
 | 
						|
            number /* osmId*/,
 | 
						|
            /** target coordinate index --> distance (or undefined if a duplicate)*/
 | 
						|
            number[]
 | 
						|
        >()
 | 
						|
 | 
						|
        const nodeInfo = new Map<
 | 
						|
            number /* osmId*/,
 | 
						|
            {
 | 
						|
                distances: number[]
 | 
						|
                // Part of some other way then the one that should be replaced
 | 
						|
                partOfWay: boolean
 | 
						|
                hasTags: boolean
 | 
						|
            }
 | 
						|
        >()
 | 
						|
 | 
						|
        for (const node of allNodes) {
 | 
						|
            const parentWays = nodeDb.GetParentWays(node.id)
 | 
						|
            if (parentWays === undefined) {
 | 
						|
                throw "PANIC: the way to replace has a node which has no parents at all. Is it deleted in the meantime?"
 | 
						|
            }
 | 
						|
            const parentWayIds = parentWays.data.map((w) => w.type + "/" + w.id)
 | 
						|
            const idIndex = parentWayIds.indexOf(this.wayToReplaceId)
 | 
						|
            if (idIndex < 0) {
 | 
						|
                throw "PANIC: the way to replace has a node, which is _not_ part of this was according to the node..."
 | 
						|
            }
 | 
						|
            parentWayIds.splice(idIndex, 1)
 | 
						|
            const partOfSomeWay = parentWayIds.length > 0
 | 
						|
            const hasTags = Object.keys(node.tags).length > 1
 | 
						|
 | 
						|
            const nodeDistances = this.targetCoordinates.map((_) => undefined)
 | 
						|
            for (let i = 0; i < this.targetCoordinates.length; i++) {
 | 
						|
                if (this.identicalTo[i] !== undefined) {
 | 
						|
                    continue
 | 
						|
                }
 | 
						|
                const targetCoordinate = this.targetCoordinates[i]
 | 
						|
                const cp = node.centerpoint()
 | 
						|
                const d = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]])
 | 
						|
                if (d > 25) {
 | 
						|
                    // This is too much to move
 | 
						|
                    continue
 | 
						|
                }
 | 
						|
                if (d < 3 || !(hasTags || partOfSomeWay)) {
 | 
						|
                    // If there is some relation: cap the move distance to 3m
 | 
						|
                    nodeDistances[i] = d
 | 
						|
                }
 | 
						|
            }
 | 
						|
            distances.set(node.id, nodeDistances)
 | 
						|
            nodeInfo.set(node.id, {
 | 
						|
                distances: nodeDistances,
 | 
						|
                partOfWay: partOfSomeWay,
 | 
						|
                hasTags,
 | 
						|
            })
 | 
						|
        }
 | 
						|
 | 
						|
        const closestIds = this.targetCoordinates.map((_) => undefined)
 | 
						|
        const unusedIds = new Map<
 | 
						|
            number,
 | 
						|
            {
 | 
						|
                reason: string
 | 
						|
                hasTags: boolean
 | 
						|
            }
 | 
						|
        >()
 | 
						|
        {
 | 
						|
            // Search best merge candidate
 | 
						|
            /**
 | 
						|
             * Then, we search the node that has to move the least distance and add this as mapping.
 | 
						|
             * We do this until no points are left
 | 
						|
             */
 | 
						|
            let candidate: number
 | 
						|
            let moveDistance: number
 | 
						|
            /**
 | 
						|
             * The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates
 | 
						|
             */
 | 
						|
            do {
 | 
						|
                candidate = undefined
 | 
						|
                moveDistance = Infinity
 | 
						|
                distances.forEach((distances, nodeId) => {
 | 
						|
                    const minDist = Math.min(...Utils.NoNull(distances))
 | 
						|
                    if (moveDistance > minDist) {
 | 
						|
                        // We have found a candidate to move
 | 
						|
                        candidate = nodeId
 | 
						|
                        moveDistance = minDist
 | 
						|
                    }
 | 
						|
                })
 | 
						|
 | 
						|
                if (candidate !== undefined) {
 | 
						|
                    // We found a candidate... Search the corresponding target id:
 | 
						|
                    let targetId: number = undefined
 | 
						|
                    let lowestDistance = Number.MAX_VALUE
 | 
						|
                    let nodeDistances = distances.get(candidate)
 | 
						|
                    for (let i = 0; i < nodeDistances.length; i++) {
 | 
						|
                        const d = nodeDistances[i]
 | 
						|
                        if (d !== undefined && d < lowestDistance) {
 | 
						|
                            lowestDistance = d
 | 
						|
                            targetId = i
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
 | 
						|
                    // This candidates role is done, it can be removed from the distance matrix
 | 
						|
                    distances.delete(candidate)
 | 
						|
 | 
						|
                    if (targetId !== undefined) {
 | 
						|
                        // At this point, we have our target coordinate index: targetId!
 | 
						|
                        // Lets map it...
 | 
						|
                        closestIds[targetId] = candidate
 | 
						|
 | 
						|
                        // To indicate that this targetCoordinate is taken, we remove them from the distances matrix
 | 
						|
                        distances.forEach((dists) => {
 | 
						|
                            dists[targetId] = undefined
 | 
						|
                        })
 | 
						|
                    } else {
 | 
						|
                        // Seems like all the targetCoordinates have found a source point
 | 
						|
                        unusedIds.set(candidate, {
 | 
						|
                            reason: "Unused by new way",
 | 
						|
                            hasTags: nodeInfo.get(candidate).hasTags,
 | 
						|
                        })
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            } while (candidate !== undefined)
 | 
						|
        }
 | 
						|
 | 
						|
        // If there are still unused values in 'distances', they are definitively unused
 | 
						|
        distances.forEach((_, nodeId) => {
 | 
						|
            unusedIds.set(nodeId, {
 | 
						|
                reason: "Unused by new way",
 | 
						|
                hasTags: nodeInfo.get(nodeId).hasTags,
 | 
						|
            })
 | 
						|
        })
 | 
						|
 | 
						|
        const reprojectedNodes = new Map<
 | 
						|
            number,
 | 
						|
            {
 | 
						|
                /*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
 | 
						|
                projectAfterIndex: number
 | 
						|
                distance: number
 | 
						|
                newLat: number
 | 
						|
                newLon: number
 | 
						|
                nodeId: number
 | 
						|
            }
 | 
						|
        >()
 | 
						|
        {
 | 
						|
            // Lets check the unused ids: can they be detached or do they signify some relation with the object?
 | 
						|
            unusedIds.forEach(({}, id) => {
 | 
						|
                const info = nodeInfo.get(id)
 | 
						|
                if (!(info.hasTags || info.partOfWay)) {
 | 
						|
                    // Nothing special here, we detach
 | 
						|
                    return
 | 
						|
                }
 | 
						|
 | 
						|
                // The current node has tags and/or has an attached other building.
 | 
						|
                // We should project them and move them onto the building on an appropriate place
 | 
						|
                const node = allNodesById.get(id)
 | 
						|
 | 
						|
                // Project the node onto the target way to calculate the new coordinates
 | 
						|
                const way = <Feature<LineString>>{
 | 
						|
                    type: "Feature",
 | 
						|
                    properties: {},
 | 
						|
                    geometry: {
 | 
						|
                        type: "LineString",
 | 
						|
                        coordinates: self.targetCoordinates,
 | 
						|
                    },
 | 
						|
                }
 | 
						|
                const projected = GeoOperations.nearestPoint(way, [node.lon, node.lat])
 | 
						|
                reprojectedNodes.set(id, {
 | 
						|
                    newLon: projected.geometry.coordinates[0],
 | 
						|
                    newLat: projected.geometry.coordinates[1],
 | 
						|
                    projectAfterIndex: projected.properties.index,
 | 
						|
                    distance: projected.properties.dist,
 | 
						|
                    nodeId: id,
 | 
						|
                })
 | 
						|
            })
 | 
						|
 | 
						|
            reprojectedNodes.forEach((_, nodeId) => unusedIds.delete(nodeId))
 | 
						|
        }
 | 
						|
 | 
						|
        return { closestIds, allNodesById, osmWay, detachedNodes: unusedIds, reprojectedNodes }
 | 
						|
    }
 | 
						|
 | 
						|
    protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
 | 
						|
        const nodeDb = this.state.featurePipeline.fullNodeDatabase
 | 
						|
        if (nodeDb === undefined) {
 | 
						|
            throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
 | 
						|
        }
 | 
						|
 | 
						|
        const { closestIds, osmWay, detachedNodes, reprojectedNodes } = await this.GetClosestIds()
 | 
						|
        const allChanges: ChangeDescription[] = []
 | 
						|
        const actualIdsToUse: number[] = []
 | 
						|
        for (let i = 0; i < closestIds.length; i++) {
 | 
						|
            if (this.identicalTo[i] !== undefined) {
 | 
						|
                const j = this.identicalTo[i]
 | 
						|
                actualIdsToUse.push(actualIdsToUse[j])
 | 
						|
                continue
 | 
						|
            }
 | 
						|
            const closestId = closestIds[i]
 | 
						|
            const [lon, lat] = this.targetCoordinates[i]
 | 
						|
            if (closestId === undefined) {
 | 
						|
                const newNodeAction = new CreateNewNodeAction([], lat, lon, {
 | 
						|
                    allowReuseOfPreviouslyCreatedPoints: true,
 | 
						|
                    theme: this.theme,
 | 
						|
                    changeType: null,
 | 
						|
                })
 | 
						|
                const changeDescr = await newNodeAction.CreateChangeDescriptions(changes)
 | 
						|
                allChanges.push(...changeDescr)
 | 
						|
                actualIdsToUse.push(newNodeAction.newElementIdNumber)
 | 
						|
            } else {
 | 
						|
                const change = <ChangeDescription>{
 | 
						|
                    id: closestId,
 | 
						|
                    type: "node",
 | 
						|
                    meta: {
 | 
						|
                        theme: this.theme,
 | 
						|
                        changeType: "move",
 | 
						|
                    },
 | 
						|
                    changes: { lon, lat },
 | 
						|
                }
 | 
						|
                actualIdsToUse.push(closestId)
 | 
						|
                allChanges.push(change)
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (this.newTags !== undefined && this.newTags.length > 0) {
 | 
						|
            const addExtraTags = new ChangeTagAction(
 | 
						|
                this.wayToReplaceId,
 | 
						|
                new And(this.newTags),
 | 
						|
                osmWay.tags,
 | 
						|
                {
 | 
						|
                    theme: this.theme,
 | 
						|
                    changeType: "conflation",
 | 
						|
                }
 | 
						|
            )
 | 
						|
            allChanges.push(...(await addExtraTags.CreateChangeDescriptions(changes)))
 | 
						|
        }
 | 
						|
 | 
						|
        const newCoordinates = [...this.targetCoordinates]
 | 
						|
 | 
						|
        {
 | 
						|
            // Add reprojected nodes to the way
 | 
						|
 | 
						|
            const proj = Array.from(reprojectedNodes.values())
 | 
						|
            proj.sort((a, b) => {
 | 
						|
                // Sort descending
 | 
						|
                const diff = b.projectAfterIndex - a.projectAfterIndex
 | 
						|
                if (diff !== 0) {
 | 
						|
                    return diff
 | 
						|
                }
 | 
						|
                return b.distance - a.distance
 | 
						|
            })
 | 
						|
 | 
						|
            for (const reprojectedNode of proj) {
 | 
						|
                const change = <ChangeDescription>{
 | 
						|
                    id: reprojectedNode.nodeId,
 | 
						|
                    type: "node",
 | 
						|
                    meta: {
 | 
						|
                        theme: this.theme,
 | 
						|
                        changeType: "move",
 | 
						|
                    },
 | 
						|
                    changes: { lon: reprojectedNode.newLon, lat: reprojectedNode.newLat },
 | 
						|
                }
 | 
						|
                allChanges.push(change)
 | 
						|
                actualIdsToUse.splice(
 | 
						|
                    reprojectedNode.projectAfterIndex + 1,
 | 
						|
                    0,
 | 
						|
                    reprojectedNode.nodeId
 | 
						|
                )
 | 
						|
                newCoordinates.splice(reprojectedNode.projectAfterIndex + 1, 0, [
 | 
						|
                    reprojectedNode.newLon,
 | 
						|
                    reprojectedNode.newLat,
 | 
						|
                ])
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // Actually change the nodes of the way!
 | 
						|
        allChanges.push({
 | 
						|
            type: "way",
 | 
						|
            id: osmWay.id,
 | 
						|
            changes: {
 | 
						|
                nodes: actualIdsToUse,
 | 
						|
                coordinates: newCoordinates,
 | 
						|
            },
 | 
						|
            meta: {
 | 
						|
                theme: this.theme,
 | 
						|
                changeType: "conflation",
 | 
						|
            },
 | 
						|
        })
 | 
						|
 | 
						|
        // Some nodes might need to be deleted
 | 
						|
        if (detachedNodes.size > 0) {
 | 
						|
            detachedNodes.forEach(({ hasTags, reason }, nodeId) => {
 | 
						|
                const parentWays = nodeDb.GetParentWays(nodeId)
 | 
						|
                const index = parentWays.data.map((w) => w.id).indexOf(osmWay.id)
 | 
						|
                if (index < 0) {
 | 
						|
                    console.error(
 | 
						|
                        "ReplaceGeometryAction is trying to detach node " +
 | 
						|
                            nodeId +
 | 
						|
                            ", but it isn't listed as being part of way " +
 | 
						|
                            osmWay.id
 | 
						|
                    )
 | 
						|
                    return
 | 
						|
                }
 | 
						|
                // We detachted this node - so we unregister
 | 
						|
                parentWays.data.splice(index, 1)
 | 
						|
                parentWays.ping()
 | 
						|
 | 
						|
                if (hasTags) {
 | 
						|
                    // Has tags: we leave this node alone
 | 
						|
                    return
 | 
						|
                }
 | 
						|
                if (parentWays.data.length != 0) {
 | 
						|
                    // Still part of other ways: we leave this node alone!
 | 
						|
                    return
 | 
						|
                }
 | 
						|
 | 
						|
                console.log("Removing node " + nodeId, "as it isn't needed anymore by any way")
 | 
						|
                allChanges.push({
 | 
						|
                    meta: {
 | 
						|
                        theme: this.theme,
 | 
						|
                        changeType: "delete",
 | 
						|
                    },
 | 
						|
                    doDelete: true,
 | 
						|
                    type: "node",
 | 
						|
                    id: nodeId,
 | 
						|
                })
 | 
						|
            })
 | 
						|
        }
 | 
						|
 | 
						|
        return allChanges
 | 
						|
    }
 | 
						|
}
 |