forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			238 lines
		
	
	
	
		
			8.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			238 lines
		
	
	
	
		
			8.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import {OsmRelation, OsmWay} from "../OsmObject";
 | |
| import {Changes} from "../Changes";
 | |
| import {GeoOperations} from "../../GeoOperations";
 | |
| import OsmChangeAction from "./OsmChangeAction";
 | |
| import {ChangeDescription} from "./ChangeDescription";
 | |
| import RelationSplitlHandler from "./RelationSplitlHandler";
 | |
| 
 | |
| interface SplitInfo {
 | |
|     originalIndex?: number, // or negative for new elements
 | |
|     lngLat: [number, number],
 | |
|     doSplit: boolean
 | |
| }
 | |
| 
 | |
| export default class SplitAction extends OsmChangeAction {
 | |
|     private readonly roadObject: any;
 | |
|     private readonly osmWay: OsmWay;
 | |
|     private _partOf: OsmRelation[];
 | |
|     private readonly _splitPoints: any[];
 | |
| 
 | |
|     constructor(osmWay: OsmWay, wayGeoJson: any, partOf: OsmRelation[], splitPoints: any[]) {
 | |
|         super()
 | |
|         this.osmWay = osmWay;
 | |
|         this.roadObject = wayGeoJson;
 | |
|         this._partOf = partOf;
 | |
|         this._splitPoints = splitPoints;
 | |
|     }
 | |
| 
 | |
|     private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] {
 | |
|         const wayParts = []
 | |
|         let currentPart = []
 | |
|         for (const splitInfoElement of splitInfo) {
 | |
|             currentPart.push(splitInfoElement)
 | |
| 
 | |
|             if (splitInfoElement.doSplit) {
 | |
|                 // We have to do a split!
 | |
|                 // We add the current index to the currentParts, flush it and add it again
 | |
|                 wayParts.push(currentPart)
 | |
|                 currentPart = [splitInfoElement]
 | |
|             }
 | |
|         }
 | |
|         wayParts.push(currentPart)
 | |
|         return wayParts.filter(wp => wp.length > 0)
 | |
|     }
 | |
| 
 | |
|     CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
 | |
|         const splitPoints = this._splitPoints
 | |
|         // We mark the new split points with a new id
 | |
|         console.log(splitPoints)
 | |
|         for (const splitPoint of splitPoints) {
 | |
|             splitPoint.properties["_is_split_point"] = true
 | |
|         }
 | |
| 
 | |
| 
 | |
|         const self = this;
 | |
|         const partOf = this._partOf
 | |
|         const originalElement = this.osmWay
 | |
|         const originalNodes = this.osmWay.nodes;
 | |
| 
 | |
|         // First, calculate splitpoints and remove points close to one another
 | |
|         const splitInfo = self.CalculateSplitCoordinates(splitPoints)
 | |
|         // Now we have a list with e.g. 
 | |
|         // [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}]
 | |
| 
 | |
|         // Lets change 'originalIndex' to the actual node id first:
 | |
|         for (const element of splitInfo) {
 | |
|             if (element.originalIndex >= 0) {
 | |
|                 element.originalIndex = originalElement.nodes[element.originalIndex]
 | |
|             } else {
 | |
|                 element.originalIndex = changes.getNewID();
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Next up is creating actual parts from this
 | |
|         const wayParts: SplitInfo[][] = SplitAction.SegmentSplitInfo(splitInfo);
 | |
|         // Allright! At this point, we have our new ways!
 | |
|         // Which one is the longest of them (and can keep the id)?
 | |
| 
 | |
|         let longest = undefined;
 | |
|         for (const wayPart of wayParts) {
 | |
|             if (longest === undefined) {
 | |
|                 longest = wayPart;
 | |
|                 continue
 | |
|             }
 | |
|             if (wayPart.length > longest.length) {
 | |
|                 longest = wayPart
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         const changeDescription: ChangeDescription[] = []
 | |
|         // Let's create the new points as needed
 | |
|         for (const element of splitInfo) {
 | |
|             if (element.originalIndex >= 0) {
 | |
|                 continue;
 | |
|             }
 | |
|             changeDescription.push({
 | |
|                 type: "node",
 | |
|                 id: element.originalIndex,
 | |
|                 changes: {
 | |
|                     lon: element.lngLat[0],
 | |
|                     lat: element.lngLat[1]
 | |
|                 }
 | |
|             })
 | |
|         }
 | |
| 
 | |
|         const newWayIds: number[] = []
 | |
|         // Lets create OsmWays based on them
 | |
|         for (const wayPart of wayParts) {
 | |
| 
 | |
|             let isOriginal = wayPart === longest
 | |
|             if (isOriginal) {
 | |
|                 // We change the actual element!
 | |
|                 changeDescription.push({
 | |
|                     type: "way",
 | |
|                     id: originalElement.id,
 | |
|                     changes: {
 | |
|                         locations: wayPart.map(p => p.lngLat),
 | |
|                         nodes: wayPart.map(p => p.originalIndex)
 | |
|                     }
 | |
|                 })
 | |
|             } else {
 | |
|                 let id = changes.getNewID();
 | |
|                 newWayIds.push(id)
 | |
| 
 | |
|                 const kv = []
 | |
|                 for (const k in originalElement.tags) {
 | |
|                     if (!originalElement.tags.hasOwnProperty(k)) {
 | |
|                         continue
 | |
|                     }
 | |
|                     if (k.startsWith("_") || k === "id") {
 | |
|                         continue;
 | |
|                     }
 | |
|                     kv.push({k: k, v: originalElement.tags[k]})
 | |
|                 }
 | |
|                 changeDescription.push({
 | |
|                     type: "way",
 | |
|                     id: id,
 | |
|                     tags: kv,
 | |
|                     changes: {
 | |
|                         locations: wayPart.map(p => p.lngLat),
 | |
|                         nodes: wayPart.map(p => p.originalIndex)
 | |
|                     }
 | |
|                 })
 | |
|             }
 | |
| 
 | |
|         }
 | |
| 
 | |
|         // At last, we still have to check that we aren't part of a relation...
 | |
|         // At least, the order of the ways is identical, so we can keep the same roles
 | |
|         changeDescription.push(...new RelationSplitlHandler(partOf, newWayIds, originalNodes).CreateChangeDescriptions(changes))
 | |
| 
 | |
|         // And we have our objects!
 | |
|         // Time to upload
 | |
| 
 | |
|         return changeDescription
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Calculates the actual points to split
 | |
|      * If another point is closer then ~5m, we reuse that point
 | |
|      */
 | |
|     private CalculateSplitCoordinates(
 | |
|         splitPoints: any[],
 | |
|         toleranceInM = 5): SplitInfo[] {
 | |
| 
 | |
|         const allPoints = [...splitPoints];
 | |
|         // We have a bunch of coordinates here: [ [lat, lon], [lat, lon], ...] ...
 | |
|         const originalPoints: [number, number][] = this.roadObject.geometry.coordinates
 | |
|         // We project them onto the line (which should yield pretty much the same point
 | |
|         for (let i = 0; i < originalPoints.length; i++) {
 | |
|             let originalPoint = originalPoints[i];
 | |
|             let projected = GeoOperations.nearestPoint(this.roadObject, originalPoint)
 | |
|             projected.properties["_is_split_point"] = false
 | |
|             projected.properties["_original_index"] = i
 | |
|             allPoints.push(projected)
 | |
|         }
 | |
|         // At this point, we have a list of both the split point and the old points, with some properties to discriminate between them
 | |
|         // We sort this list so that the new points are at the same location
 | |
|         allPoints.sort((a, b) => a.properties.location - b.properties.location)
 | |
| 
 | |
|         // When this is done, we check that no now point is too close to an already existing point and no very small segments get created
 | |
| 
 | |
|         /*   for (let i = allPoints.length - 1; i > 0; i--) {
 | |
|    
 | |
|                const point = allPoints[i];
 | |
|                if (point.properties._original_index !== undefined) {
 | |
|                    // This point is already in OSM - we have to keep it!
 | |
|                    continue;
 | |
|                }
 | |
|    
 | |
|                if (i != allPoints.length - 1) {
 | |
|                    const prevPoint = allPoints[i + 1]
 | |
|                    const diff = Math.abs(point.properties.location - prevPoint.properties.location) * 1000
 | |
|                    if (diff <= toleranceInM) {
 | |
|                        // To close to the previous point! We delete this point...
 | |
|                        allPoints.splice(i, 1)
 | |
|                        // ... and mark the previous point as a split point
 | |
|                        prevPoint.properties._is_split_point = true
 | |
|                        continue;
 | |
|                    }
 | |
|                }
 | |
|    
 | |
|                if (i > 0) {
 | |
|                    const nextPoint = allPoints[i - 1]
 | |
|                    const diff = Math.abs(point.properties.location - nextPoint.properties.location) * 1000
 | |
|                    if (diff <= toleranceInM) {
 | |
|                        // To close to the next point! We delete this point...
 | |
|                        allPoints.splice(i, 1)
 | |
|                        // ... and mark the next point as a split point
 | |
|                        nextPoint.properties._is_split_point = true
 | |
|                        // noinspection UnnecessaryContinueJS
 | |
|                        continue;
 | |
|                    }
 | |
|                }
 | |
|                // We don't have to remove this point...
 | |
|            }*/
 | |
| 
 | |
|         const splitInfo: SplitInfo[] = []
 | |
|         let nextId = -1
 | |
| 
 | |
|         for (const p of allPoints) {
 | |
|             let index = p.properties._original_index
 | |
|             if (index === undefined) {
 | |
|                 index = nextId;
 | |
|                 nextId--;
 | |
|             }
 | |
|             const splitInfoElement = {
 | |
|                 originalIndex: index,
 | |
|                 lngLat: p.geometry.coordinates,
 | |
|                 doSplit: p.properties._is_split_point
 | |
|             }
 | |
|             splitInfo.push(splitInfoElement)
 | |
|         }
 | |
| 
 | |
|         return splitInfo
 | |
|     }
 | |
| 
 | |
| 
 | |
| }
 |