mapcomplete/Logic/Osm/Actions/RelationSplitHandler.ts

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,
},
},
]
}
}