forked from MapComplete/MapComplete
Feature: allow to move and snap to a layer, fix #2120
This commit is contained in:
parent
eb89427bfc
commit
fdedb75954
34 changed files with 824 additions and 301 deletions
|
@ -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 }]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
]
|
||||
}
|
||||
|
||||
|
|
96
src/Logic/Osm/Actions/InsertPointIntoWayAction.ts
Normal file
96
src/Logic/Osm/Actions/InsertPointIntoWayAction.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue