forked from MapComplete/MapComplete
Further work on the road splitting feature
This commit is contained in:
parent
9348a019d6
commit
1da3f8a332
9 changed files with 351 additions and 274 deletions
|
@ -1,6 +1,10 @@
|
||||||
import FeatureSource from "./FeatureSource";
|
import FeatureSource from "./FeatureSource";
|
||||||
import {UIEventSource} from "../UIEventSource";
|
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 {
|
export default class FeatureSourceMerger implements FeatureSource {
|
||||||
|
|
||||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import {OsmNode, OsmObject, OsmWay} from "./OsmObject";
|
import {OsmNode, OsmObject} from "./OsmObject";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
import {UIEventSource} from "../UIEventSource";
|
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")
|
console.log("New feature added and pinged")
|
||||||
this.features.data.push({feature:geojson, freshness: new Date()});
|
this.features.data.push({feature:geojson, freshness: new Date()});
|
||||||
|
@ -133,44 +133,8 @@ export class Changes implements FeatureSource{
|
||||||
return geojson;
|
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;
|
private static createTagChangeList(basicTags: Tag[], properties: { id: string }, id: string) {
|
||||||
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 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
|
// The tags are not yet written into the OsmObject, but this is applied onto a
|
||||||
const changes = [];
|
const changes = [];
|
||||||
|
@ -229,46 +193,46 @@ export class Changes implements FeatureSource{
|
||||||
State.state.osmConnection.UploadChangeset(
|
State.state.osmConnection.UploadChangeset(
|
||||||
State.state.layoutToUse.data,
|
State.state.layoutToUse.data,
|
||||||
State.state.allElements,
|
State.state.allElements,
|
||||||
function (csId) {
|
(csId) => Changes.createChangesetFor(csId,changedElements, newElements )
|
||||||
|
);
|
||||||
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;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
private uploadAll(
|
||||||
newElements: OsmObject[],
|
newElements: OsmObject[],
|
||||||
pending: { elementId: string; key: string; value: string }[]
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default class CreateNewNodeAction {
|
||||||
|
|
||||||
|
}
|
|
@ -60,6 +60,8 @@ export abstract class OsmObject {
|
||||||
case("relation"):
|
case("relation"):
|
||||||
new OsmRelation(idN).Download(newContinuation);
|
new OsmRelation(idN).Download(newContinuation);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
throw "Invalid road type:" + type;
|
||||||
|
|
||||||
}
|
}
|
||||||
return src;
|
return src;
|
||||||
|
@ -150,7 +152,7 @@ export abstract class OsmObject {
|
||||||
const minlat = bounds[1][0]
|
const minlat = bounds[1][0]
|
||||||
const maxlat = bounds[0][0];
|
const maxlat = bounds[0][0];
|
||||||
const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}`
|
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 elements: any[] = data.elements;
|
||||||
const objects = OsmObject.ParseObjects(elements)
|
const objects = OsmObject.ParseObjects(elements)
|
||||||
callback(objects);
|
callback(objects);
|
||||||
|
@ -354,9 +356,9 @@ export class OsmNode extends OsmObject {
|
||||||
ChangesetXML(changesetId: string): string {
|
ChangesetXML(changesetId: string): string {
|
||||||
let tags = this.TagsXML();
|
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 +
|
tags +
|
||||||
' </node>\n';
|
' </node>\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveExtraData(element) {
|
SaveExtraData(element) {
|
||||||
|
@ -401,7 +403,6 @@ export class OsmWay extends OsmObject {
|
||||||
|
|
||||||
constructor(id) {
|
constructor(id) {
|
||||||
super("way", id);
|
super("way", id);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
centerpoint(): [number, number] {
|
centerpoint(): [number, number] {
|
||||||
|
@ -418,7 +419,7 @@ export class OsmWay extends OsmObject {
|
||||||
return ' <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
|
return ' <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
|
||||||
nds +
|
nds +
|
||||||
tags +
|
tags +
|
||||||
' </way>\n';
|
' </way>\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveExtraData(element, allNodes: OsmNode[]) {
|
SaveExtraData(element, allNodes: OsmNode[]) {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
/**
|
||||||
|
* The logic to handle relations after a way within
|
||||||
|
*/
|
||||||
|
export default class RelationSplitlHandler {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,162 +1,222 @@
|
||||||
import {UIEventSource} from "../UIEventSource";
|
import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject";
|
||||||
import {OsmNode, OsmObject, OsmWay} from "./OsmObject";
|
|
||||||
import State from "../../State";
|
|
||||||
import {distance} from "@turf/turf";
|
|
||||||
import {GeoOperations} from "../GeoOperations";
|
import {GeoOperations} from "../GeoOperations";
|
||||||
|
import State from "../../State";
|
||||||
|
import {UIEventSource} from "../UIEventSource";
|
||||||
import {Changes} from "./Changes";
|
import {Changes} from "./Changes";
|
||||||
|
|
||||||
/**
|
interface SplitInfo {
|
||||||
* Splits a road in different segments, each splitted at one of the given points (or a point on the road close to it)
|
originalIndex?: number, // or negative for new elements
|
||||||
* @param roadID The id of the road you want to split
|
lngLat: [number, number],
|
||||||
* @param points The points on the road where you want the split to occur (geojson point list)
|
doSplit: boolean
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -7,13 +7,15 @@ import State from "../../State";
|
||||||
import ShowDataLayer from "../ShowDataLayer";
|
import ShowDataLayer from "../ShowDataLayer";
|
||||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||||
import {LeafletMouseEvent} from "leaflet";
|
import {LeafletMouseEvent} from "leaflet";
|
||||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import {Button} from "../Base/Button";
|
import {Button} from "../Base/Button";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import {splitRoad} from "../../Logic/Osm/SplitAction";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
|
import SplitAction from "../../Logic/Osm/SplitAction";
|
||||||
|
|
||||||
export default class SplitRoadWizard extends Toggle {
|
export default class SplitRoadWizard extends Toggle {
|
||||||
|
private static splitLayout = new UIEventSource(SplitRoadWizard.GetSplitLayout())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A UI Element used for splitting roads
|
* A UI Element used for splitting roads
|
||||||
*
|
*
|
||||||
|
@ -23,25 +25,25 @@ export default class SplitRoadWizard extends Toggle {
|
||||||
|
|
||||||
const t = Translations.t.split;
|
const t = Translations.t.split;
|
||||||
|
|
||||||
// Contains the points on the road that are selected to split on
|
// Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring
|
||||||
const splitPositions = new UIEventSource([]);
|
const splitPoints = new UIEventSource<{feature: any, freshness: Date}[]>([]);
|
||||||
|
|
||||||
// Toggle variable between show split button and map
|
// Toggle variable between show split button and map
|
||||||
const splitClicked = new UIEventSource<boolean>(true); // todo: -> false
|
const splitClicked = new UIEventSource<boolean>(false);
|
||||||
|
|
||||||
// Minimap on which you can select the points to be splitted
|
// Minimap on which you can select the points to be splitted
|
||||||
const miniMap = new Minimap({background: State.state.backgroundLayer});
|
const miniMap = new Minimap({background: State.state.backgroundLayer});
|
||||||
miniMap.SetStyle("width: 100%; height: 50rem;");
|
miniMap.SetStyle("width: 100%; height: 50rem;");
|
||||||
|
|
||||||
// Define how a cut is displayed on the map
|
// Define how a cut is displayed on the map
|
||||||
const layoutConfigJson = {id: "splitpositions", source: {osmTags: "_cutposition=yes"}, icon: "./assets/svg/plus.svg"}
|
|
||||||
State.state.layoutToUse.data.layers.push(new LayerConfig(layoutConfigJson,undefined,"Split Road Wizard"))
|
|
||||||
|
|
||||||
// Load the road with given id on the minimap
|
// Load the road with given id on the minimap
|
||||||
const roadElement = State.state.allElements.ContainingFeatures.get(id)
|
const roadElement = State.state.allElements.ContainingFeatures.get(id)
|
||||||
|
const splitAction = new SplitAction(roadElement)
|
||||||
const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]);
|
const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]);
|
||||||
// Datalayer displaying the road and the cut points (if any)
|
// Datalayer displaying the road and the cut points (if any)
|
||||||
const dataLayer = new ShowDataLayer(roadEventSource, miniMap.leafletMap, State.state.layoutToUse, false, true);
|
new ShowDataLayer(roadEventSource, miniMap.leafletMap, State.state.layoutToUse, false, true);
|
||||||
|
new ShowDataLayer(splitPoints, miniMap.leafletMap, SplitRoadWizard.splitLayout, false, false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles a click on the overleaf map.
|
* Handles a click on the overleaf map.
|
||||||
|
@ -54,17 +56,14 @@ export default class SplitRoadWizard extends Toggle {
|
||||||
|
|
||||||
// Update point properties to let it match the layer
|
// Update point properties to let it match the layer
|
||||||
pointOnRoad.properties._cutposition = "yes";
|
pointOnRoad.properties._cutposition = "yes";
|
||||||
pointOnRoad._matching_layer_id = "splitpositions";
|
pointOnRoad["_matching_layer_id"] = "splitpositions";
|
||||||
|
|
||||||
// Add it to the list of all points and notify observers
|
|
||||||
splitPositions.data.push(pointOnRoad);
|
|
||||||
splitPositions.ping();
|
|
||||||
|
|
||||||
// let the state remember the point, to be able to retrieve it later by id
|
// let the state remember the point, to be able to retrieve it later by id
|
||||||
State.state.allElements.addOrGetElement(pointOnRoad);
|
State.state.allElements.addOrGetElement(pointOnRoad);
|
||||||
|
|
||||||
roadEventSource.data.push({feature: pointOnRoad, freshness: new Date()}); // show the point on the data layer
|
// Add it to the list of all points and notify observers
|
||||||
roadEventSource.ping(); // not updated using .setData, so manually ping observers
|
splitPoints.data.push({feature: pointOnRoad, freshness: new Date()}); // show the point on the data layer
|
||||||
|
splitPoints.ping(); // not updated using .setData, so manually ping observers
|
||||||
}
|
}
|
||||||
|
|
||||||
// When clicked, pass clicked location coordinates to onMapClick function
|
// When clicked, pass clicked location coordinates to onMapClick function
|
||||||
|
@ -88,19 +87,16 @@ export default class SplitRoadWizard extends Toggle {
|
||||||
State.state.osmConnection.isLoggedIn)
|
State.state.osmConnection.isLoggedIn)
|
||||||
|
|
||||||
// Save button
|
// Save button
|
||||||
const saveButton = new Button("Split here", () => splitRoad(id, splitPositions.data));
|
const saveButton = new Button("Split here", () => splitAction.DoSplit(splitPoints.data));
|
||||||
saveButton.SetClass("block btn btn-primary");
|
saveButton.SetClass("block btn btn-primary");
|
||||||
const disabledSaveButton = new Button("Split here", undefined);
|
const disabledSaveButton = new Button("Split here", undefined);
|
||||||
disabledSaveButton.SetClass("block btn btn-disabled");
|
disabledSaveButton.SetClass("block btn btn-disabled");
|
||||||
// Only show the save button if there are split points defined
|
// Only show the save button if there are split points defined
|
||||||
const saveToggle = new Toggle(disabledSaveButton, saveButton, splitPositions.map((data) => data.length === 0))
|
const saveToggle = new Toggle(disabledSaveButton, saveButton, splitPoints.map((data) => data.length === 0))
|
||||||
|
|
||||||
const cancelButton = new Button("Cancel", () => {
|
const cancelButton = new Button("Cancel", () => {
|
||||||
splitClicked.setData(false);
|
splitClicked.setData(false);
|
||||||
|
splitPoints.setData([]);
|
||||||
splitPositions.setData([]);
|
|
||||||
// Only keep showing the road, the cutpoints must be removed from the map
|
|
||||||
roadEventSource.setData([roadEventSource.data[0]])
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cancelButton.SetClass("block btn btn-secondary");
|
cancelButton.SetClass("block btn btn-secondary");
|
||||||
|
@ -111,4 +107,21 @@ export default class SplitRoadWizard extends Toggle {
|
||||||
super(mapView, splitToggle, splitClicked);
|
super(mapView, splitToggle, splitClicked);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static GetSplitLayout(): LayoutConfig {
|
||||||
|
return new LayoutConfig({
|
||||||
|
maintainer: "mapcomplete",
|
||||||
|
language: [],
|
||||||
|
startLon: 0,
|
||||||
|
startLat: 0,
|
||||||
|
description: undefined,
|
||||||
|
icon: "", startZoom: 0,
|
||||||
|
title: "Split locations",
|
||||||
|
version: "",
|
||||||
|
|
||||||
|
id: "splitpositions",
|
||||||
|
layers: [{id: "splitpositions", source: {osmTags: "_cutposition=yes"}, icon: "./assets/svg/plus.svg"}]
|
||||||
|
}, true, "split road wizard layout")
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
35
test.ts
35
test.ts
|
@ -1,6 +1,5 @@
|
||||||
import SplitRoadWizard from "./UI/Popup/SplitRoadWizard";
|
import SplitAction from "./Logic/Osm/SplitAction";
|
||||||
import State from "./State";
|
import {GeoOperations} from "./Logic/GeoOperations";
|
||||||
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
|
||||||
|
|
||||||
const way = {
|
const way = {
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
|
@ -47,7 +46,31 @@ const way = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
State.state = new State(AllKnownLayouts.allKnownLayouts.get("fietsstraten"));
|
let splitPoint = {
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {},
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [
|
||||||
|
4.490211009979248,
|
||||||
|
51.2041509326002
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let splitClose = {
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {},
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [
|
||||||
|
4.489563927054405,
|
||||||
|
51.2047546593862
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// State.state = new State(AllKnownLayouts.allKnownLayouts.get("fietsstraten"));
|
||||||
// add road to state
|
// add road to state
|
||||||
State.state.allElements.addOrGetElement(way);
|
// State.state.allElements.addOrGetElement(way);
|
||||||
new SplitRoadWizard("way/23583625").AttachTo("maindiv")
|
new SplitAction(way).DoSplit([splitPoint, splitClose].map(p => GeoOperations.nearestPoint(way,<[number, number]> p.geometry.coordinates)))
|
Loading…
Add table
Add a link
Reference in a new issue