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