Further work on the road splitting feature

This commit is contained in:
Pieter Vander Vennet 2021-07-15 00:26:25 +02:00
parent 9348a019d6
commit 1da3f8a332
9 changed files with 351 additions and 274 deletions

View file

@ -1,6 +1,10 @@
import FeatureSource from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
/**
* Merges features from different featureSources
* Uses the freshest feature available in the case multiple sources offer data with the same identifier
*/
export default class FeatureSourceMerger implements FeatureSource {
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);

View file

@ -0,0 +1,7 @@
/**
* An action is a change to the OSM-database
* It will generate some new/modified/deleted objects, which are all bundled by the 'changes'-object
*/
export default interface Action {
}

View file

@ -1,4 +1,4 @@
import {OsmNode, OsmObject, OsmWay} from "./OsmObject";
import {OsmNode, OsmObject} from "./OsmObject";
import State from "../../State";
import {Utils} from "../../Utils";
import {UIEventSource} from "../UIEventSource";
@ -121,7 +121,7 @@ export class Changes implements FeatureSource{
}
}
const changes = this.createTagChangeList(basicTags, properties, id);
const changes = Changes.createTagChangeList(basicTags, properties, id);
console.log("New feature added and pinged")
this.features.data.push({feature:geojson, freshness: new Date()});
@ -133,44 +133,8 @@ 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) {
private static 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 = [];
@ -229,46 +193,46 @@ export class Changes implements FeatureSource{
State.state.osmConnection.UploadChangeset(
State.state.layoutToUse.data,
State.state.allElements,
function (csId) {
let modifications = "";
for (const element of changedElements) {
if (!element.changed) {
continue;
}
modifications += element.ChangesetXML(csId) + "\n";
}
let creations = "";
for (const newElement of newElements) {
creations += newElement.ChangesetXML(csId);
}
let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`;
if (creations.length > 0) {
changes +=
"<create>" +
creations +
"</create>";
}
if (modifications.length > 0) {
changes +=
"<modify>\n" +
modifications +
"\n</modify>";
}
changes += "</osmChange>";
return changes;
});
(csId) => Changes.createChangesetFor(csId,changedElements, newElements )
);
};
public static createChangesetFor(csId: string, changedElements: OsmObject[], newElements: OsmObject[]): string {
let modifications = "";
for (const element of changedElements) {
modifications += element.ChangesetXML(csId) + "\n";
}
let creations = "";
for (const newElement of newElements) {
creations += newElement.ChangesetXML(csId);
}
let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`;
if (creations.length > 0) {
changes +=
"\n<create>\n" +
creations +
"</create>";
}
if (modifications.length > 0) {
changes +=
"\n<modify>\n" +
modifications +
"\n</modify>";
}
changes += "</osmChange>";
return changes;
}
private uploadAll(
newElements: OsmObject[],
pending: { elementId: string; key: string; value: string }[]
@ -293,13 +257,4 @@ 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
}
}

View file

@ -0,0 +1,3 @@
export default class CreateNewNodeAction {
}

View file

@ -60,6 +60,8 @@ export abstract class OsmObject {
case("relation"):
new OsmRelation(idN).Download(newContinuation);
break;
default:
throw "Invalid road type:" + type;
}
return src;
@ -150,7 +152,7 @@ export abstract class OsmObject {
const minlat = bounds[1][0]
const maxlat = bounds[0][0];
const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}`
Utils.downloadJson(url).then( data => {
Utils.downloadJson(url).then(data => {
const elements: any[] = data.elements;
const objects = OsmObject.ParseObjects(elements)
callback(objects);
@ -354,9 +356,9 @@ export class OsmNode extends OsmObject {
ChangesetXML(changesetId: string): string {
let tags = this.TagsXML();
return ' <node id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + ' lat="' + this.lat + '" lon="' + this.lon + '">\n' +
return ' <node id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + ' lat="' + this.lat + '" lon="' + this.lon + '">\n' +
tags +
' </node>\n';
' </node>\n';
}
SaveExtraData(element) {
@ -401,7 +403,6 @@ export class OsmWay extends OsmObject {
constructor(id) {
super("way", id);
}
centerpoint(): [number, number] {
@ -418,7 +419,7 @@ export class OsmWay extends OsmObject {
return ' <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
nds +
tags +
' </way>\n';
' </way>\n';
}
SaveExtraData(element, allNodes: OsmNode[]) {

View file

@ -0,0 +1,11 @@
/**
* The logic to handle relations after a way within
*/
export default class RelationSplitlHandler {
constructor() {
}
}

View file

@ -1,162 +1,222 @@
import {UIEventSource} from "../UIEventSource";
import {OsmNode, OsmObject, OsmWay} from "./OsmObject";
import State from "../../State";
import {distance} from "@turf/turf";
import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject";
import {GeoOperations} from "../GeoOperations";
import State from "../../State";
import {UIEventSource} from "../UIEventSource";
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<OsmWay> = 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;
interface SplitInfo {
originalIndex?: number, // or negative for new elements
lngLat: [number, number],
doSplit: boolean
}
export default class SplitAction {
private readonly roadObject: any;
// 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
/***
*
* @param roadObject: the geojson of the road object. Properties.id must be the corresponding OSM-id
*/
constructor(roadObject: any) {
this.roadObject = roadObject;
}
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
}
public DoSplit(splitPoints: any[]) {
// We mark the new split points with a new id
for (const splitPoint of splitPoints) {
splitPoint.properties["_is_split_point"] = true
}
const self = this;
const id = this.roadObject.properties.id
const osmWay = <UIEventSource<OsmWay>>OsmObject.DownloadObject(id)
const partOf = OsmObject.DownloadReferencingRelations(id)
osmWay.map(originalElement => {
if(originalElement === undefined || partOf === undefined){
return;
}
const changes = State.state?.changes ?? new Changes();
// 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 = 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 newOsmObjects: OsmObject[] = []
const modifiedObjects: OsmObject[] = []
// Let's create the new points as needed
for (const element of splitInfo) {
if (element.originalIndex >= 0) {
continue;
}
const node = new OsmNode(element.originalIndex)
node.lon = element.lngLat[0]
node.lat = element.lngLat[1]
newOsmObjects.push(node)
}
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!
originalElement.nodes = wayPart.map(p => p.originalIndex);
originalElement.changed = true;
modifiedObjects.push(originalElement)
}else{
let id = changes.getNewID();
const way = new OsmWay(id)
way.tags = originalElement.tags;
way.nodes = wayPart.map(p => p.originalIndex);
way.changed = true;
newOsmObjects.push(way)
newWayIds.push(way.id)
}
}
// 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
modifiedObjects.push(...SplitAction.UpdateRelations(partOf.data, newWayIds, originalElement))
// And we have our objects!
// Time to upload
console.log(Changes.createChangesetFor("123", modifiedObjects, newOsmObjects))
}, [partOf])
}
private static UpdateRelations(data: OsmRelation[], newWayIds: number[], originalElement: OsmWay):OsmRelation[]{
// TODO
return []
}
/**
* 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
}
}