From f77c1efdf5b99e1f7295e3b2fc49c229711fd8b9 Mon Sep 17 00:00:00 2001 From: Arno Deceuninck Date: Wed, 14 Jul 2021 15:28:02 +0200 Subject: [PATCH] SplitAction logic, not yet pushing changes to osm, pieter will take over --- Logic/GeoOperations.ts | 2 +- Logic/Osm/Changes.ts | 84 ++++++++++++++++--- Logic/Osm/OsmObject.ts | 2 +- Logic/Osm/SplitAction.ts | 162 ++++++++++++++++++++++++++++++++++++ UI/Popup/SplitRoadWizard.ts | 4 +- test.ts | 37 +++++--- 6 files changed, 262 insertions(+), 29 deletions(-) create mode 100644 Logic/Osm/SplitAction.ts diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index e4ee3234c3..4550dabd46 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -280,7 +280,7 @@ export class GeoOperations { * @param point Point defined as [lon, lat] */ public static nearestPoint(way, point: [number, number]){ - return turf.nearestPointOnLine(way, point); + return turf.nearestPointOnLine(way, point, {units: "kilometers"}); } } diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index 4a4b00d356..d26dec6a62 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -1,4 +1,4 @@ -import {OsmNode, OsmObject} from "./OsmObject"; +import {OsmNode, OsmObject, OsmWay} from "./OsmObject"; import State from "../../State"; import {Utils} from "../../Utils"; import {UIEventSource} from "../UIEventSource"; @@ -86,6 +86,14 @@ export class Changes implements FeatureSource{ this.uploadAll([], this.pending.data); this.pending.setData([]); } + + /** + * Returns a new ID and updates the value for the next ID + */ + public getNewID(){ + return Changes._nextId--; + } + /** * Create a new node element at the given lat/long. * An internal OsmObject is created to upload later on, a geojson represention is returned. @@ -93,8 +101,7 @@ export class Changes implements FeatureSource{ */ public createElement(basicTags: Tag[], lat: number, lon: number) { console.log("Creating a new element with ", basicTags) - const osmNode = new OsmNode(Changes._nextId); - Changes._nextId--; + const osmNode = new OsmNode(this.getNewID()); const id = "node/" + osmNode.id; osmNode.lat = lat; @@ -114,16 +121,7 @@ export class Changes implements FeatureSource{ } } - // The basictags are COPIED, the id is included in the properties - // The tags are not yet written into the OsmObject, but this is applied onto a - const changes = []; - for (const kv of basicTags) { - properties[kv.key] = kv.value; - if (typeof kv.value !== "string") { - throw "Invalid value: don't use a regex in a preset" - } - changes.push({elementId: id, key: kv.key, value: kv.value}) - } + const changes = this.createTagChangeList(basicTags, properties, id); console.log("New feature added and pinged") this.features.data.push({feature:geojson, freshness: new Date()}); @@ -135,6 +133,57 @@ export class Changes implements FeatureSource{ return geojson; } + /** + * Creates a new road with given tags that consist of the points corresponding to given nodeIDs + * @param basicTags The tags to add to the road + * @param nodeIDs IDs of nodes of which the road consists. Those nodes must already exist in osm or already be added to the changeset. + * @param coordinates The coordinates correspoinding to the nodeID at the same index. Each coordinate is a [lon, lat] point + * @return geojson A geojson representation of the created road + */ + public createRoad(basicTags: Tag[], nodeIDs, coordinates) { + const osmWay = new OsmWay(this.getNewID()); + + const id = "way/" + osmWay.id; + osmWay.nodes = nodeIDs; + const properties = {id: id}; + + const geojson = { + "type": "Feature", + "properties": properties, + "id": id, + "geometry": { + "type": "LineString", + "coordinates": coordinates + } + } + + const changes = this.createTagChangeList(basicTags, properties, id); + + console.log("New feature added and pinged") + this.features.data.push({feature:geojson, freshness: new Date()}); + this.features.ping(); + + State.state.allElements.addOrGetElement(geojson).ping(); + + this.uploadAll([osmWay], changes); + return geojson; + } + + + private createTagChangeList(basicTags: Tag[], properties: { id: string }, id: string) { + // The basictags are COPIED, the id is included in the properties + // The tags are not yet written into the OsmObject, but this is applied onto a + const changes = []; + for (const kv of basicTags) { + properties[kv.key] = kv.value; + if (typeof kv.value !== "string") { + throw "Invalid value: don't use a regex in a preset" + } + changes.push({elementId: id, key: kv.key, value: kv.value}) + } + return changes; + } + private uploadChangesWithLatestVersions( knownElements: OsmObject[], newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) { const knownById = new Map(); @@ -244,4 +293,13 @@ export class Changes implements FeatureSource{ }) } + + /** + * Changes the nodes of road with given id to the given nodes + * @param roadID The ID of the road to update + * @param newNodes The node id's the road consists of (should already be added to the changeset or in osm) + */ + public updateRoadCoordinates(roadID: string, newNodes: number[]) { + // TODO + } } \ No newline at end of file diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts index 6f9ec95b3e..2fe1ef50bf 100644 --- a/Logic/Osm/OsmObject.ts +++ b/Logic/Osm/OsmObject.ts @@ -444,7 +444,7 @@ export class OsmWay extends OsmObject { this.nodes = element.nodes; } - asGeoJson() { + public asGeoJson() { return { "type": "Feature", "properties": this.tags, diff --git a/Logic/Osm/SplitAction.ts b/Logic/Osm/SplitAction.ts new file mode 100644 index 0000000000..4405fd1994 --- /dev/null +++ b/Logic/Osm/SplitAction.ts @@ -0,0 +1,162 @@ +import {UIEventSource} from "../UIEventSource"; +import {OsmNode, OsmObject, OsmWay} from "./OsmObject"; +import State from "../../State"; +import {distance} from "@turf/turf"; +import {GeoOperations} from "../GeoOperations"; +import {Changes} from "./Changes"; + +/** + * Splits a road in different segments, each splitted at one of the given points (or a point on the road close to it) + * @param roadID The id of the road you want to split + * @param points The points on the road where you want the split to occur (geojson point list) + */ +export async function splitRoad(roadID, points) { + if (points.length != 1) { + // TODO: more than one point + console.log(points) + window.alert("Warning, currently only tested on one point, you selected " + points.length + " points") + } + + let road = State.state.allElements.ContainingFeatures.get(roadID); + + /** + * Compares two points based on the starting point of the road, can be used in sort function + * @param point1 [lon, lat] point + * @param point2 [lon, lat] point + */ + function comparePointDistance(point1, point2) { + let distFromStart1 = GeoOperations.nearestPoint(road, point1).properties.location; + let distFromStart2 = GeoOperations.nearestPoint(road, point2).properties.location; + return distFromStart1 - distFromStart2; // Sort requires a number to return instead of a bool + } + + /** + * Eliminates split points close (<4m) to existing points on the road, so you can split on these points instead + * @param road The road geojson object + * @param points The points on the road where you want the split to occur (geojson point list) + * @return realSplitPoints List containing all new locations where you should split + */ + function getSplitPoints(road, points) { + // Copy the list + let roadPoints = [...road.geometry.coordinates]; + + // Get the coordinates of all geojson points + let splitPointsCoordinates = points.map((point) => point.geometry.coordinates); + + roadPoints.push(...splitPointsCoordinates); + + // Sort all points on the road based on the distance from the start + roadPoints.sort(comparePointDistance) + + // Remove points close to existing points on road + let realSplitPoints = [...splitPointsCoordinates]; + for (let index = roadPoints.length - 1; index > 0; index--) { + // Iterate backwards to prevent problems when removing elements + let dist = distance(roadPoints[index - 1], roadPoints[index], {units: "kilometers"}); + // Remove all cutpoints closer than 4m to their previous point + if ((dist < 0.004) && (splitPointsCoordinates.includes(roadPoints[index]))) { + console.log("Removed a splitpoint, using a closer point to the road instead") + realSplitPoints.splice(index, 1) + realSplitPoints.push(roadPoints[index - 1]) + } + } + return realSplitPoints; + } + + let realSplitPoints = getSplitPoints(road, points); + + // Create a sorted list containing all points + let allPoints = [...road.geometry.coordinates]; + allPoints.push(...realSplitPoints); + allPoints.sort(comparePointDistance); + + // The changeset that will contain the operations to split the road + let changes = new Changes(); + + // Download the data of the current road from Osm to get the ID's of the coordinates + let osmRoad: UIEventSource = OsmObject.DownloadObject(roadID); + + // TODO: Remove delay, use a callback on odmRoad instead and execute all code below in callback function + function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + await delay(3000); + + // Dict to quickly convert a coordinate to a nodeID + let coordToIDMap = {}; + + /** + * Converts a coordinate to a string, so it's hashable (e.g. for using it in a dict) + * @param coord [lon, lat] point + */ + function getCoordKey(coord: [number, number]) { + return coord[0] + "," + coord[1]; + } + + osmRoad.data.coordinates.forEach((coord, i) => coordToIDMap[getCoordKey([coord[1], coord[0]])] = osmRoad.data.nodes[i]); + + let currentRoadPoints: number[] = []; + let currentRoadCoordinates: [number, number][] = [] + + /** + * Given a coordinate, check whether there is already a node in osm created (on the road or cutpoints) or create + * such point if it doesn't exist yet and return the id of this coordinate + * @param coord [lon, lat] point + * @return pointID The ID of the existing/created node on given coordinates + */ + function getOrCreateNodeID(coord) { + console.log(coordToIDMap) + let poinID = coordToIDMap[getCoordKey(coord)]; + if (poinID == undefined) { + console.log(getCoordKey(coord) + " not in map") + // TODO: Check if lat, lon is correct + let newNode = changes.createElement([], coord[1], coord[0]); + + coordToIDMap[coord] = newNode.id; + poinID = newNode.id; + + console.log("New point created "); + } + return poinID; + } + + /** + * Creates a new road in OSM, while copying the tags from osmRoad and using currentRoadPoints as points + * @param currentRoadPoints List of id's of nodes the road should exist of + * @param osmRoad The road to copy the tags from + */ + function createNewRoadSegment(currentRoadPoints, osmRoad) { + changes.createRoad(osmRoad.data.tags, currentRoadPoints, currentRoadCoordinates); + } + + for (let coord of allPoints) { + console.log("Handling coord") + let pointID = getOrCreateNodeID(coord); + currentRoadPoints.push(pointID); + currentRoadCoordinates.push(coord); + if (realSplitPoints.includes(coord)) { + console.log("Handling split") + // Should split here + // currentRoadPoints contains a list containing all points for this road segment + createNewRoadSegment(currentRoadPoints, osmRoad); + + // Cleanup for next split + currentRoadPoints = [pointID]; + currentRoadCoordinates = [coord]; + console.log("Splitting here...") + } + } + + // Update the road to contain only the points of the last segment + // changes.updateRoadCoordinates(roadID, currentRoadPoints); + + // push the applied changes + changes.flushChanges(); + + return; +} + + +// TODO: Vlakbij bestaand punt geklikt? Bestaand punt hergebruiken +// Nieuw wegobject aanmaken, en oude hergebruiken voor andere helft van de weg +// TODO: CHeck if relation exist to the road -> Delete them when splitted, because they might be outdated after the split diff --git a/UI/Popup/SplitRoadWizard.ts b/UI/Popup/SplitRoadWizard.ts index 55410c6768..8aed42a7ec 100644 --- a/UI/Popup/SplitRoadWizard.ts +++ b/UI/Popup/SplitRoadWizard.ts @@ -11,6 +11,7 @@ import LayerConfig from "../../Customizations/JSON/LayerConfig"; import Combine from "../Base/Combine"; import {Button} from "../Base/Button"; import Translations from "../i18n/Translations"; +import {splitRoad} from "../../Logic/Osm/SplitAction"; export default class SplitRoadWizard extends Toggle { /** @@ -87,7 +88,7 @@ export default class SplitRoadWizard extends Toggle { State.state.osmConnection.isLoggedIn) // Save button - const saveButton = new Button("Split here", () => window.alert("Splitting...")); + const saveButton = new Button("Split here", () => splitRoad(id, splitPositions.data)); saveButton.SetClass("block btn btn-primary"); const disabledSaveButton = new Button("Split here", undefined); disabledSaveButton.SetClass("block btn btn-disabled"); @@ -98,6 +99,7 @@ export default class SplitRoadWizard extends Toggle { splitClicked.setData(false); splitPositions.setData([]); + // Only keep showing the road, the cutpoints must be removed from the map roadEventSource.setData([roadEventSource.data[0]]) }); diff --git a/test.ts b/test.ts index b20f90323e..3357bf6455 100644 --- a/test.ts +++ b/test.ts @@ -5,32 +5,43 @@ import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; const way = { "type": "Feature", "properties": { - "id": "way/1234", "highway": "residential", - "cyclestreet": "yes" + "maxweight": "3.5", + "maxweight:conditional": "none @ delivery", + "name": "Silsstraat", + "_last_edit:contributor": "Jorisbo", + "_last_edit:contributor:uid": 1983103, + "_last_edit:changeset": 70963524, + "_last_edit:timestamp": "2019-06-05T18:20:44Z", + "_version_number": 9, + "id": "way/23583625" }, "geometry": { "type": "LineString", "coordinates": [ [ - 4.488961100578308, - 51.204971024401374 + 4.4889691, + 51.2049831 ], [ - 4.4896745681762695, - 51.204712226516435 + 4.4895496, + 51.2047718 ], [ - 4.489814043045044, - 51.20459459063348 + 4.48966, + 51.2047147 ], [ - 4.48991060256958, - 51.204439983016115 + 4.4897439, + 51.2046548 ], [ - 4.490291476249695, - 51.203845074952376 + 4.4898162, + 51.2045921 + ], + [ + 4.4902997, + 51.2038418 ] ] } @@ -39,4 +50,4 @@ const way = { State.state = new State(AllKnownLayouts.allKnownLayouts.get("fietsstraten")); // add road to state State.state.allElements.addOrGetElement(way); -new SplitRoadWizard("way/1234").AttachTo("maindiv") \ No newline at end of file +new SplitRoadWizard("way/23583625").AttachTo("maindiv") \ No newline at end of file