Feature: allow to move and snap to a layer, fix #2120

This commit is contained in:
Pieter Vander Vennet 2024-09-04 00:07:23 +02:00
parent eb89427bfc
commit fdedb75954
34 changed files with 824 additions and 301 deletions

View file

@ -1,10 +1,15 @@
import { ChangeDescription } from "./ChangeDescription"
import OsmChangeAction from "./OsmChangeAction"
import { WayId } from "../../../Models/OsmFeature"
import InsertPointIntoWayAction from "./InsertPointIntoWayAction"
import { SpecialVisualizationState } from "../../../UI/SpecialVisualization"
export default class ChangeLocationAction extends OsmChangeAction {
private readonly _id: number
private readonly _newLonLat: [number, number]
private readonly _meta: { theme: string; reason: string }
private readonly state: SpecialVisualizationState
private snapTo: WayId | undefined
static metatags: {
readonly key?: string
readonly value?: string
@ -21,28 +26,30 @@ export default class ChangeLocationAction extends OsmChangeAction {
]
constructor(
state: SpecialVisualizationState,
id: string,
newLonLat: [number, number],
snapTo: WayId | undefined,
meta: {
theme: string
reason: string
}
},
) {
super(id, true)
this.state = state
if (!id.startsWith("node/")) {
throw "Invalid ID: only 'node/number' is accepted"
}
this._id = Number(id.substring("node/".length))
this._newLonLat = newLonLat
this.snapTo = snapTo
this._meta = meta
}
protected async CreateChangeDescriptions(): Promise<ChangeDescription[]> {
const [lon, lat] = this._newLonLat
const d: ChangeDescription = {
changes: {
lat: this._newLonLat[1],
lon: this._newLonLat[0],
},
changes: { lon, lat },
type: "node",
id: this._id,
meta: {
@ -51,7 +58,21 @@ export default class ChangeLocationAction extends OsmChangeAction {
specialMotivation: this._meta.reason,
},
}
if (!this.snapTo) {
return [d]
}
const snapToWay = await this.state.osmObjectDownloader.DownloadObjectAsync(this.snapTo, 0)
if (snapToWay === "deleted") {
return [d]
}
return [d]
const insertIntoWay = new InsertPointIntoWayAction(
lat, lon, this._id, snapToWay, {
allowReuseOfPreviouslyCreatedPoints: false,
reusePointWithinMeters: 0.25,
},
).prepareChangeDescription()
return [d, { ...insertIntoWay, meta: d.meta }]
}
}

View file

@ -5,6 +5,7 @@ import { ChangeDescription } from "./ChangeDescription"
import { And } from "../../Tags/And"
import { OsmWay } from "../OsmObject"
import { GeoOperations } from "../../GeoOperations"
import InsertPointIntoWayAction from "./InsertPointIntoWayAction"
export default class CreateNewNodeAction extends OsmCreateAction {
/**
@ -37,7 +38,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
theme: string
changeType: "create" | "import" | null
specialMotivation?: string
}
},
) {
super(null, basicTags !== undefined && basicTags.length > 0)
this._basicTags = basicTags
@ -101,72 +102,20 @@ export default class CreateNewNodeAction extends OsmCreateAction {
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(GeoOperations.outerRing(geojson), [
this._lon,
const change = new InsertPointIntoWayAction(
this._lat,
])
const projectedCoor = <[number, number]>projected.geometry.coordinates
const index = projected.properties.index
console.log("Attempting to snap:", { geojson, projected, projectedCoor, index })
// We check that it isn't close to an already existing point
let reusedPointId = undefined
let reusedPointCoordinates: [number, number] = undefined
let outerring: [number, number][]
this._lon,
id,
this._snapOnto,
{
reusePointWithinMeters: this._reusePointDistance,
allowReuseOfPreviouslyCreatedPoints: this._reusePreviouslyCreatedPoint,
},
).prepareChangeDescription()
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]
reusedPointCoordinates = this._snapOnto.coordinates[index]
}
const next = outerring[index + 1]
if (GeoOperations.distanceBetween(next, projectedCoor) < this._reusePointDistance) {
// We reuse this point instead!
reusedPointId = this._snapOnto.nodes[index + 1]
reusedPointCoordinates = this._snapOnto.coordinates[index + 1]
}
if (reusedPointId !== undefined) {
this.setElementId(reusedPointId)
return [
{
tags: new And(this._basicTags).asChange(properties),
type: "node",
id: reusedPointId,
meta: this.meta,
changes: { lat: reusedPointCoordinates[0], lon: reusedPointCoordinates[1] },
},
]
}
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,
},
{ ...change, meta: this.meta },
]
}

View file

@ -0,0 +1,96 @@
import { ChangeDescription } from "./ChangeDescription"
import { GeoOperations } from "../../GeoOperations"
import { OsmWay } from "../OsmObject"
export default class InsertPointIntoWayAction {
private readonly _lat: number
private readonly _lon: number
private readonly _idToInsert: number
private readonly _snapOnto: OsmWay
private readonly _options: {
allowReuseOfPreviouslyCreatedPoints?: boolean
reusePointWithinMeters?: number
}
constructor(
lat: number,
lon: number,
idToInsert: number,
snapOnto: OsmWay,
options: {
allowReuseOfPreviouslyCreatedPoints?: boolean
reusePointWithinMeters?: number
}
){
this._lat = lat
this._lon = lon
this._idToInsert = idToInsert
this._snapOnto = snapOnto
this._options = options
}
/**
* Tries to create the changedescription of the way where the point is inserted
* Returns `undefined` if inserting failed
*/
public prepareChangeDescription(): Omit<ChangeDescription, "meta"> | undefined {
// Project the point onto the way
console.log("Snapping a node onto an existing way...")
const geojson = this._snapOnto.asGeoJson()
const projected = GeoOperations.nearestPoint(GeoOperations.outerRing(geojson), [
this._lon,
this._lat,
])
const projectedCoor = <[number, number]>projected.geometry.coordinates
const index = projected.properties.index
console.log("Attempting to snap:", { geojson, projected, projectedCoor, index })
// We check that it isn't close to an already existing point
let reusedPointId = undefined
let reusedPointCoordinates: [number, number] = 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._options.reusePointWithinMeters) {
// We reuse this point instead!
reusedPointId = this._snapOnto.nodes[index]
reusedPointCoordinates = this._snapOnto.coordinates[index]
}
const next = outerring[index + 1]
if (GeoOperations.distanceBetween(next, projectedCoor) < this._options.reusePointWithinMeters) {
// We reuse this point instead!
reusedPointId = this._snapOnto.nodes[index + 1]
reusedPointCoordinates = this._snapOnto.coordinates[index + 1]
}
if (reusedPointId !== undefined) {
return undefined
}
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, this._idToInsert)
return {
type: "way",
id: this._snapOnto.id,
changes: {
coordinates: locations,
nodes: ids,
}
}
}
}