forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			369 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			369 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { OsmCreateAction } from "./OsmChangeAction"
 | |
| import { Tag } from "../../Tags/Tag"
 | |
| import { Changes } from "../Changes"
 | |
| import { ChangeDescription } from "./ChangeDescription"
 | |
| import FeaturePipelineState from "../../State/FeaturePipelineState"
 | |
| import { BBox } from "../../BBox"
 | |
| import { TagsFilter } from "../../Tags/TagsFilter"
 | |
| import { GeoOperations } from "../../GeoOperations"
 | |
| import FeatureSource from "../../FeatureSource/FeatureSource"
 | |
| import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
 | |
| import CreateNewNodeAction from "./CreateNewNodeAction"
 | |
| import CreateNewWayAction from "./CreateNewWayAction"
 | |
| 
 | |
| export interface MergePointConfig {
 | |
|     withinRangeOfM: number
 | |
|     ifMatches: TagsFilter
 | |
|     mode: "reuse_osm_point" | "move_osm_point"
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * CreateWayWithPointreuse will create a 'CoordinateInfo' for _every_ point in the way to be created.
 | |
|  *
 | |
|  * The CoordinateInfo indicates the action to take, e.g.:
 | |
|  *
 | |
|  * - Create a new point
 | |
|  * - Reuse an existing OSM point (and don't move it)
 | |
|  * - Reuse an existing OSM point (and leave it where it is)
 | |
|  * - Reuse another Coordinate info (and don't do anything else with it)
 | |
|  *
 | |
|  */
 | |
| interface CoordinateInfo {
 | |
|     /**
 | |
|      * The new coordinate
 | |
|      */
 | |
|     lngLat: [number, number]
 | |
|     /**
 | |
|      * If set: indicates that this point is identical to an earlier point in the way and that that point should be used.
 | |
|      * This is especially needed in closed ways, where the last CoordinateInfo will have '0' as identicalTo
 | |
|      */
 | |
|     identicalTo?: number
 | |
|     /**
 | |
|      * Information about the closebyNode which might be reused
 | |
|      */
 | |
|     closebyNodes?: {
 | |
|         /**
 | |
|          * Distance in meters between the target coordinate and this candidate coordinate
 | |
|          */
 | |
|         d: number
 | |
|         node: any
 | |
|         config: MergePointConfig
 | |
|     }[]
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
 | |
|  */
 | |
| export default class CreateWayWithPointReuseAction extends OsmCreateAction {
 | |
|     public newElementId: string = undefined
 | |
|     public newElementIdNumber: number = undefined
 | |
|     private readonly _tags: Tag[]
 | |
|     /**
 | |
|      * lngLat-coordinates
 | |
|      * @private
 | |
|      */
 | |
|     private _coordinateInfo: CoordinateInfo[]
 | |
|     private _state: FeaturePipelineState
 | |
|     private _config: MergePointConfig[]
 | |
| 
 | |
|     constructor(
 | |
|         tags: Tag[],
 | |
|         coordinates: [number, number][],
 | |
|         state: FeaturePipelineState,
 | |
|         config: MergePointConfig[]
 | |
|     ) {
 | |
|         super(null, true)
 | |
|         this._tags = tags
 | |
|         this._state = state
 | |
|         this._config = config
 | |
| 
 | |
|         // The main logic of this class: the coordinateInfo contains all the changes
 | |
|         this._coordinateInfo = this.CalculateClosebyNodes(coordinates)
 | |
|     }
 | |
| 
 | |
|     public async getPreview(): Promise<FeatureSource> {
 | |
|         const features = []
 | |
|         let geometryMoved = false
 | |
|         for (let i = 0; i < this._coordinateInfo.length; i++) {
 | |
|             const coordinateInfo = this._coordinateInfo[i]
 | |
|             if (coordinateInfo.identicalTo !== undefined) {
 | |
|                 continue
 | |
|             }
 | |
|             if (
 | |
|                 coordinateInfo.closebyNodes === undefined ||
 | |
|                 coordinateInfo.closebyNodes.length === 0
 | |
|             ) {
 | |
|                 const newPoint = {
 | |
|                     type: "Feature",
 | |
|                     properties: {
 | |
|                         newpoint: "yes",
 | |
|                         id: "new-geometry-with-reuse-" + i,
 | |
|                     },
 | |
|                     geometry: {
 | |
|                         type: "Point",
 | |
|                         coordinates: coordinateInfo.lngLat,
 | |
|                     },
 | |
|                 }
 | |
|                 features.push(newPoint)
 | |
|                 continue
 | |
|             }
 | |
| 
 | |
|             const reusedPoint = coordinateInfo.closebyNodes[0]
 | |
|             if (reusedPoint.config.mode === "move_osm_point") {
 | |
|                 const moveDescription = {
 | |
|                     type: "Feature",
 | |
|                     properties: {
 | |
|                         move: "yes",
 | |
|                         "osm-id": reusedPoint.node.properties.id,
 | |
|                         id: "new-geometry-move-existing" + i,
 | |
|                         distance: GeoOperations.distanceBetween(
 | |
|                             coordinateInfo.lngLat,
 | |
|                             reusedPoint.node.geometry.coordinates
 | |
|                         ),
 | |
|                     },
 | |
|                     geometry: {
 | |
|                         type: "LineString",
 | |
|                         coordinates: [reusedPoint.node.geometry.coordinates, coordinateInfo.lngLat],
 | |
|                     },
 | |
|                 }
 | |
|                 features.push(moveDescription)
 | |
|             } else {
 | |
|                 // The geometry is moved, the point is reused
 | |
|                 geometryMoved = true
 | |
| 
 | |
|                 const reuseDescription = {
 | |
|                     type: "Feature",
 | |
|                     properties: {
 | |
|                         move: "no",
 | |
|                         "osm-id": reusedPoint.node.properties.id,
 | |
|                         id: "new-geometry-reuse-existing" + i,
 | |
|                         distance: GeoOperations.distanceBetween(
 | |
|                             coordinateInfo.lngLat,
 | |
|                             reusedPoint.node.geometry.coordinates
 | |
|                         ),
 | |
|                     },
 | |
|                     geometry: {
 | |
|                         type: "LineString",
 | |
|                         coordinates: [coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates],
 | |
|                     },
 | |
|                 }
 | |
|                 features.push(reuseDescription)
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (geometryMoved) {
 | |
|             const coords: [number, number][] = []
 | |
|             for (const info of this._coordinateInfo) {
 | |
|                 if (info.identicalTo !== undefined) {
 | |
|                     coords.push(coords[info.identicalTo])
 | |
|                     continue
 | |
|                 }
 | |
| 
 | |
|                 if (info.closebyNodes === undefined || info.closebyNodes.length === 0) {
 | |
|                     coords.push(coords[info.identicalTo])
 | |
|                     continue
 | |
|                 }
 | |
| 
 | |
|                 const closest = info.closebyNodes[0]
 | |
|                 if (closest.config.mode === "reuse_osm_point") {
 | |
|                     coords.push(closest.node.geometry.coordinates)
 | |
|                 } else {
 | |
|                     coords.push(info.lngLat)
 | |
|                 }
 | |
|             }
 | |
|             const newGeometry = {
 | |
|                 type: "Feature",
 | |
|                 properties: {
 | |
|                     "resulting-geometry": "yes",
 | |
|                     id: "new-geometry",
 | |
|                 },
 | |
|                 geometry: {
 | |
|                     type: "LineString",
 | |
|                     coordinates: coords,
 | |
|                 },
 | |
|             }
 | |
|             features.push(newGeometry)
 | |
|         }
 | |
|         return StaticFeatureSource.fromGeojson(features)
 | |
|     }
 | |
| 
 | |
|     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
 | |
|         const theme = this._state?.layoutToUse?.id
 | |
|         const allChanges: ChangeDescription[] = []
 | |
|         const nodeIdsToUse: { lat: number; lon: number; nodeId?: number }[] = []
 | |
|         for (let i = 0; i < this._coordinateInfo.length; i++) {
 | |
|             const info = this._coordinateInfo[i]
 | |
|             const lat = info.lngLat[1]
 | |
|             const lon = info.lngLat[0]
 | |
| 
 | |
|             if (info.identicalTo !== undefined) {
 | |
|                 nodeIdsToUse.push(nodeIdsToUse[info.identicalTo])
 | |
|                 continue
 | |
|             }
 | |
|             if (info.closebyNodes === undefined || info.closebyNodes[0] === undefined) {
 | |
|                 const newNodeAction = new CreateNewNodeAction([], lat, lon, {
 | |
|                     allowReuseOfPreviouslyCreatedPoints: true,
 | |
|                     changeType: null,
 | |
|                     theme,
 | |
|                 })
 | |
| 
 | |
|                 allChanges.push(...(await newNodeAction.CreateChangeDescriptions(changes)))
 | |
| 
 | |
|                 nodeIdsToUse.push({
 | |
|                     lat,
 | |
|                     lon,
 | |
|                     nodeId: newNodeAction.newElementIdNumber,
 | |
|                 })
 | |
|                 continue
 | |
|             }
 | |
| 
 | |
|             const closestPoint = info.closebyNodes[0]
 | |
|             const id = Number(closestPoint.node.properties.id.split("/")[1])
 | |
|             if (closestPoint.config.mode === "move_osm_point") {
 | |
|                 allChanges.push({
 | |
|                     type: "node",
 | |
|                     id,
 | |
|                     changes: {
 | |
|                         lat,
 | |
|                         lon,
 | |
|                     },
 | |
|                     meta: {
 | |
|                         theme,
 | |
|                         changeType: null,
 | |
|                     },
 | |
|                 })
 | |
|             }
 | |
|             nodeIdsToUse.push({ lat, lon, nodeId: id })
 | |
|         }
 | |
| 
 | |
|         const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, {
 | |
|             theme,
 | |
|         })
 | |
| 
 | |
|         allChanges.push(...(await newWay.CreateChangeDescriptions(changes)))
 | |
|         this.newElementId = newWay.newElementId
 | |
|         this.newElementIdNumber = newWay.newElementIdNumber
 | |
|         return allChanges
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Calculates the main changes.
 | |
|      */
 | |
|     private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] {
 | |
|         const bbox = new BBox(coordinates)
 | |
|         const state = this._state
 | |
|         const allNodes = [].concat(
 | |
|             ...(state?.featurePipeline?.GetFeaturesWithin("type_node", bbox.pad(1.2)) ?? [])
 | |
|         )
 | |
|         const maxDistance = Math.max(...this._config.map((c) => c.withinRangeOfM))
 | |
| 
 | |
|         // Init coordianteinfo with undefined but the same length as coordinates
 | |
|         const coordinateInfo: {
 | |
|             lngLat: [number, number]
 | |
|             identicalTo?: number
 | |
|             closebyNodes?: {
 | |
|                 d: number
 | |
|                 node: any
 | |
|                 config: MergePointConfig
 | |
|             }[]
 | |
|         }[] = coordinates.map((_) => undefined)
 | |
| 
 | |
|         // First loop: gather all information...
 | |
|         for (let i = 0; i < coordinates.length; i++) {
 | |
|             if (coordinateInfo[i] !== undefined) {
 | |
|                 // Already seen, probably a duplicate coordinate
 | |
|                 continue
 | |
|             }
 | |
|             const coor = coordinates[i]
 | |
|             // Check closeby (and probably identical) points further in the coordinate list, mark them as duplicate
 | |
|             for (let j = i + 1; j < coordinates.length; j++) {
 | |
|                 // We look into the 'future' of the way and mark those 'future' locations as being the same as this location
 | |
|                 // The continue just above will make sure they get ignored
 | |
|                 // This code is important to 'close' ways
 | |
|                 if (GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) {
 | |
|                     coordinateInfo[j] = {
 | |
|                         lngLat: coor,
 | |
|                         identicalTo: i,
 | |
|                     }
 | |
|                     break
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             // Gather the actual info for this point
 | |
| 
 | |
|             // Lets search applicable points and determine the merge mode
 | |
|             const closebyNodes: {
 | |
|                 d: number
 | |
|                 node: any
 | |
|                 config: MergePointConfig
 | |
|             }[] = []
 | |
|             for (const node of allNodes) {
 | |
|                 const center = node.geometry.coordinates
 | |
|                 const d = GeoOperations.distanceBetween(coor, center)
 | |
|                 if (d > maxDistance) {
 | |
|                     continue
 | |
|                 }
 | |
| 
 | |
|                 for (const config of this._config) {
 | |
|                     if (d > config.withinRangeOfM) {
 | |
|                         continue
 | |
|                     }
 | |
|                     if (!config.ifMatches.matchesProperties(node.properties)) {
 | |
|                         continue
 | |
|                     }
 | |
|                     closebyNodes.push({ node, d, config })
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             // Sort by distance, closest first
 | |
|             closebyNodes.sort((n0, n1) => {
 | |
|                 return n0.d - n1.d
 | |
|             })
 | |
| 
 | |
|             coordinateInfo[i] = {
 | |
|                 identicalTo: undefined,
 | |
|                 lngLat: coor,
 | |
|                 closebyNodes,
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Second loop: figure out which point moves where without creating conflicts
 | |
|         let conflictFree = true
 | |
|         do {
 | |
|             conflictFree = true
 | |
|             for (let i = 0; i < coordinateInfo.length; i++) {
 | |
|                 const coorInfo = coordinateInfo[i]
 | |
|                 if (coorInfo.identicalTo !== undefined) {
 | |
|                     continue
 | |
|                 }
 | |
|                 if (coorInfo.closebyNodes === undefined || coorInfo.closebyNodes[0] === undefined) {
 | |
|                     continue
 | |
|                 }
 | |
| 
 | |
|                 for (let j = i + 1; j < coordinates.length; j++) {
 | |
|                     const other = coordinateInfo[j]
 | |
|                     if (other.closebyNodes === undefined || other.closebyNodes[0] === undefined) {
 | |
|                         continue
 | |
|                     }
 | |
| 
 | |
|                     if (coorInfo.closebyNodes[0] === undefined) {
 | |
|                         continue
 | |
|                     }
 | |
| 
 | |
|                     if (other.closebyNodes[0].node === coorInfo.closebyNodes[0].node) {
 | |
|                         conflictFree = false
 | |
|                         // We have found a conflict!
 | |
|                         // We only keep the closest point
 | |
|                         if (other.closebyNodes[0].d > coorInfo.closebyNodes[0].d) {
 | |
|                             other.closebyNodes.shift()
 | |
|                         } else {
 | |
|                             coorInfo.closebyNodes.shift()
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         } while (!conflictFree)
 | |
| 
 | |
|         return coordinateInfo
 | |
|     }
 | |
| }
 |