forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			257 lines
		
	
	
	
		
			9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			257 lines
		
	
	
	
		
			9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import OsmChangeAction from "./OsmChangeAction"
 | 
						|
import { Changes } from "../Changes"
 | 
						|
import { ChangeDescription } from "./ChangeDescription"
 | 
						|
import { OsmRelation, OsmWay } from "../OsmObject"
 | 
						|
import OsmObjectDownloader from "../OsmObjectDownloader"
 | 
						|
 | 
						|
export interface RelationSplitInput {
 | 
						|
    relation: OsmRelation
 | 
						|
    originalWayId: number
 | 
						|
    allWayIdsInOrder: number[]
 | 
						|
    originalNodes: number[]
 | 
						|
    allWaysNodesInOrder: number[][]
 | 
						|
}
 | 
						|
 | 
						|
abstract class AbstractRelationSplitHandler extends OsmChangeAction {
 | 
						|
    protected readonly _input: RelationSplitInput
 | 
						|
    protected readonly _theme: string
 | 
						|
    protected readonly _objectDownloader: OsmObjectDownloader
 | 
						|
 | 
						|
    constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) {
 | 
						|
        super("relation/" + input.relation.id, false)
 | 
						|
        this._input = input
 | 
						|
        this._theme = theme
 | 
						|
        this._objectDownloader = objectDownloader
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns which node should border the member at the given index
 | 
						|
     */
 | 
						|
    protected async targetNodeAt(i: number, first: boolean) {
 | 
						|
        const member = this._input.relation.members[i]
 | 
						|
        if (member === undefined) {
 | 
						|
            return undefined
 | 
						|
        }
 | 
						|
        if (member.type === "node") {
 | 
						|
            return member.ref
 | 
						|
        }
 | 
						|
        if (member.type === "way") {
 | 
						|
            const osmWay = <OsmWay>(
 | 
						|
                await this._objectDownloader.DownloadObjectAsync("way/" + member.ref)
 | 
						|
            )
 | 
						|
            const nodes = osmWay.nodes
 | 
						|
            if (first) {
 | 
						|
                return nodes[0]
 | 
						|
            } else {
 | 
						|
                return nodes[nodes.length - 1]
 | 
						|
            }
 | 
						|
        }
 | 
						|
        if (member.type === "relation") {
 | 
						|
            return undefined
 | 
						|
        }
 | 
						|
        return undefined
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * When a way is split and this way is part of a relation, the relation should be updated too to have the new segment if relevant.
 | 
						|
 */
 | 
						|
export default class RelationSplitHandler extends AbstractRelationSplitHandler {
 | 
						|
    constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) {
 | 
						|
        super(input, theme, objectDownloader)
 | 
						|
    }
 | 
						|
 | 
						|
    async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
 | 
						|
        if (this._input.relation.tags["type"] === "restriction") {
 | 
						|
            // This is a turn restriction
 | 
						|
            return new TurnRestrictionRSH(
 | 
						|
                this._input,
 | 
						|
                this._theme,
 | 
						|
                this._objectDownloader
 | 
						|
            ).CreateChangeDescriptions(changes)
 | 
						|
        }
 | 
						|
        return new InPlaceReplacedmentRTSH(
 | 
						|
            this._input,
 | 
						|
            this._theme,
 | 
						|
            this._objectDownloader
 | 
						|
        ).CreateChangeDescriptions(changes)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class TurnRestrictionRSH extends AbstractRelationSplitHandler {
 | 
						|
    constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) {
 | 
						|
        super(input, theme, objectDownloader)
 | 
						|
    }
 | 
						|
 | 
						|
    public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
 | 
						|
        const relation = this._input.relation
 | 
						|
        const members = relation.members
 | 
						|
 | 
						|
        const selfMembers = members.filter(
 | 
						|
            (m) => m.type === "way" && m.ref === this._input.originalWayId
 | 
						|
        )
 | 
						|
 | 
						|
        if (selfMembers.length > 1) {
 | 
						|
            console.warn(
 | 
						|
                "Detected a turn restriction where this way has multiple occurances. This is an error"
 | 
						|
            )
 | 
						|
        }
 | 
						|
        const selfMember = selfMembers[0]
 | 
						|
 | 
						|
        if (selfMember.role === "via") {
 | 
						|
            // A via way can be replaced in place
 | 
						|
            return new InPlaceReplacedmentRTSH(
 | 
						|
                this._input,
 | 
						|
                this._theme,
 | 
						|
                this._objectDownloader
 | 
						|
            ).CreateChangeDescriptions(changes)
 | 
						|
        }
 | 
						|
 | 
						|
        // We have to keep only the way with a common point with the rest of the relation
 | 
						|
        // Let's figure out which member is neighbouring our way
 | 
						|
 | 
						|
        let commonStartPoint: number = await this.targetNodeAt(members.indexOf(selfMember), true)
 | 
						|
        let commonEndPoint: number = await this.targetNodeAt(members.indexOf(selfMember), false)
 | 
						|
 | 
						|
        // In normal circumstances, only one of those should be defined
 | 
						|
        let commonPoint = commonStartPoint ?? commonEndPoint
 | 
						|
 | 
						|
        // Let's select the way to keep
 | 
						|
        const idToKeep: { id: number } = this._input.allWaysNodesInOrder
 | 
						|
            .map((nodes, i) => ({
 | 
						|
                nodes: nodes,
 | 
						|
                id: this._input.allWayIdsInOrder[i],
 | 
						|
            }))
 | 
						|
            .filter((nodesId) => {
 | 
						|
                const nds = nodesId.nodes
 | 
						|
                return nds[0] == commonPoint || nds[nds.length - 1] == commonPoint
 | 
						|
            })[0]
 | 
						|
 | 
						|
        if (idToKeep === undefined) {
 | 
						|
            console.error("No common point found, this was a broken turn restriction!", relation.id)
 | 
						|
            return []
 | 
						|
        }
 | 
						|
 | 
						|
        const originalWayId = this._input.originalWayId
 | 
						|
        if (idToKeep.id === originalWayId) {
 | 
						|
            console.log("Turn_restriction fixer: the original ID can be kept, nothing to do")
 | 
						|
            return []
 | 
						|
        }
 | 
						|
 | 
						|
        const newMembers: {
 | 
						|
            ref: number
 | 
						|
            type: "way" | "node" | "relation"
 | 
						|
            role: string
 | 
						|
        }[] = relation.members.map((m) => {
 | 
						|
            if (m.type === "way" && m.ref === originalWayId) {
 | 
						|
                return {
 | 
						|
                    ref: idToKeep.id,
 | 
						|
                    type: "way",
 | 
						|
                    role: m.role,
 | 
						|
                }
 | 
						|
            }
 | 
						|
            return m
 | 
						|
        })
 | 
						|
 | 
						|
        return [
 | 
						|
            {
 | 
						|
                type: "relation",
 | 
						|
                id: relation.id,
 | 
						|
                changes: {
 | 
						|
                    members: newMembers,
 | 
						|
                },
 | 
						|
                meta: {
 | 
						|
                    theme: this._theme,
 | 
						|
                    changeType: "relation-fix:turn_restriction",
 | 
						|
                },
 | 
						|
            },
 | 
						|
        ]
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * A simple strategy to split relations:
 | 
						|
 * -> Download the way members just before and just after the original way
 | 
						|
 * -> Make sure they are still aligned
 | 
						|
 *
 | 
						|
 * Note that the feature might appear multiple times.
 | 
						|
 */
 | 
						|
export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler {
 | 
						|
    constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) {
 | 
						|
        super(input, theme, objectDownloader)
 | 
						|
    }
 | 
						|
 | 
						|
    async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
 | 
						|
        const wayId = this._input.originalWayId
 | 
						|
        const relation = this._input.relation
 | 
						|
        const members = relation.members
 | 
						|
        const originalNodes = this._input.originalNodes
 | 
						|
        const firstNode = originalNodes[0]
 | 
						|
        const lastNode = originalNodes[originalNodes.length - 1]
 | 
						|
        const newMembers: { type: "node" | "way" | "relation"; ref: number; role: string }[] = []
 | 
						|
 | 
						|
        for (let i = 0; i < members.length; i++) {
 | 
						|
            const member = members[i]
 | 
						|
            if (member.type !== "way" || member.ref !== wayId) {
 | 
						|
                newMembers.push(member)
 | 
						|
                continue
 | 
						|
            }
 | 
						|
 | 
						|
            const nodeIdBefore = await this.targetNodeAt(i - 1, false)
 | 
						|
            const nodeIdAfter = await this.targetNodeAt(i + 1, true)
 | 
						|
 | 
						|
            const firstNodeMatches = nodeIdBefore === undefined || nodeIdBefore === firstNode
 | 
						|
            const lastNodeMatches = nodeIdAfter === undefined || nodeIdAfter === lastNode
 | 
						|
 | 
						|
            if (firstNodeMatches && lastNodeMatches) {
 | 
						|
                // We have a classic situation, forward situation
 | 
						|
                for (const wId of this._input.allWayIdsInOrder) {
 | 
						|
                    newMembers.push({
 | 
						|
                        ref: wId,
 | 
						|
                        type: "way",
 | 
						|
                        role: member.role,
 | 
						|
                    })
 | 
						|
                }
 | 
						|
                continue
 | 
						|
            }
 | 
						|
 | 
						|
            const firstNodeMatchesRev = nodeIdBefore === undefined || nodeIdBefore === lastNode
 | 
						|
            const lastNodeMatchesRev = nodeIdAfter === undefined || nodeIdAfter === firstNode
 | 
						|
            if (firstNodeMatchesRev || lastNodeMatchesRev) {
 | 
						|
                // We (probably) have a reversed situation, backward situation
 | 
						|
                for (let i1 = this._input.allWayIdsInOrder.length - 1; i1 >= 0; i1--) {
 | 
						|
                    // Iterate BACKWARDS
 | 
						|
                    const wId = this._input.allWayIdsInOrder[i1]
 | 
						|
                    newMembers.push({
 | 
						|
                        ref: wId,
 | 
						|
                        type: "way",
 | 
						|
                        role: member.role,
 | 
						|
                    })
 | 
						|
                }
 | 
						|
                continue
 | 
						|
            }
 | 
						|
 | 
						|
            // Euhm, allright... Something weird is going on, but let's not care too much
 | 
						|
            // Lets pretend this is forward going
 | 
						|
            for (const wId of this._input.allWayIdsInOrder) {
 | 
						|
                newMembers.push({
 | 
						|
                    ref: wId,
 | 
						|
                    type: "way",
 | 
						|
                    role: member.role,
 | 
						|
                })
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return [
 | 
						|
            {
 | 
						|
                id: relation.id,
 | 
						|
                type: "relation",
 | 
						|
                changes: { members: newMembers },
 | 
						|
                meta: {
 | 
						|
                    changeType: "relation-fix",
 | 
						|
                    theme: this._theme,
 | 
						|
                },
 | 
						|
            },
 | 
						|
        ]
 | 
						|
    }
 | 
						|
}
 |