import OsmChangeAction from "./OsmChangeAction"; import {Changes} from "../Changes"; import {ChangeDescription} from "./ChangeDescription"; import {OsmObject, OsmRelation, OsmWay} from "../OsmObject"; 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; constructor(input: RelationSplitInput, theme: string) { super("relation/" + input.relation.id, false) this._input = input; this._theme = theme; } /** * 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 = await OsmObject.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) { super(input, theme) } async CreateChangeDescriptions(changes: Changes): Promise { if (this._input.relation.tags["type"] === "restriction") { // This is a turn restriction return new TurnRestrictionRSH(this._input, this._theme).CreateChangeDescriptions(changes) } return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(changes) } } export class TurnRestrictionRSH extends AbstractRelationSplitHandler { constructor(input: RelationSplitInput, theme: string) { super(input, theme); } public async CreateChangeDescriptions(changes: Changes): Promise { 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).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) { super(input, theme); } async CreateChangeDescriptions(changes: Changes): Promise { 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 } }]; } }