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 {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 }[]>([]);
|
||||
|
|
|
@ -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 {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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default class CreateNewNodeAction {
|
||||
|
||||
}
|
|
@ -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[]) {
|
||||
|
|
|
@ -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, 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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -7,13 +7,15 @@ import State from "../../State";
|
|||
import ShowDataLayer from "../ShowDataLayer";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {LeafletMouseEvent} from "leaflet";
|
||||
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";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import SplitAction from "../../Logic/Osm/SplitAction";
|
||||
|
||||
export default class SplitRoadWizard extends Toggle {
|
||||
private static splitLayout = new UIEventSource(SplitRoadWizard.GetSplitLayout())
|
||||
|
||||
/**
|
||||
* A UI Element used for splitting roads
|
||||
*
|
||||
|
@ -23,25 +25,25 @@ export default class SplitRoadWizard extends Toggle {
|
|||
|
||||
const t = Translations.t.split;
|
||||
|
||||
// Contains the points on the road that are selected to split on
|
||||
const splitPositions = new UIEventSource([]);
|
||||
// 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 splitPoints = new UIEventSource<{feature: any, freshness: Date}[]>([]);
|
||||
|
||||
// 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
|
||||
const miniMap = new Minimap({background: State.state.backgroundLayer});
|
||||
miniMap.SetStyle("width: 100%; height: 50rem;");
|
||||
|
||||
// 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
|
||||
const roadElement = State.state.allElements.ContainingFeatures.get(id)
|
||||
const splitAction = new SplitAction(roadElement)
|
||||
const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]);
|
||||
// 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.
|
||||
|
@ -54,17 +56,14 @@ export default class SplitRoadWizard extends Toggle {
|
|||
|
||||
// Update point properties to let it match the layer
|
||||
pointOnRoad.properties._cutposition = "yes";
|
||||
pointOnRoad._matching_layer_id = "splitpositions";
|
||||
|
||||
// Add it to the list of all points and notify observers
|
||||
splitPositions.data.push(pointOnRoad);
|
||||
splitPositions.ping();
|
||||
pointOnRoad["_matching_layer_id"] = "splitpositions";
|
||||
|
||||
// let the state remember the point, to be able to retrieve it later by id
|
||||
State.state.allElements.addOrGetElement(pointOnRoad);
|
||||
|
||||
roadEventSource.data.push({feature: pointOnRoad, freshness: new Date()}); // show the point on the data layer
|
||||
roadEventSource.ping(); // not updated using .setData, so manually ping observers
|
||||
|
||||
// Add it to the list of all points and notify 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
|
||||
|
@ -88,19 +87,16 @@ export default class SplitRoadWizard extends Toggle {
|
|||
State.state.osmConnection.isLoggedIn)
|
||||
|
||||
// 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");
|
||||
const disabledSaveButton = new Button("Split here", undefined);
|
||||
disabledSaveButton.SetClass("block btn btn-disabled");
|
||||
// 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", () => {
|
||||
splitClicked.setData(false);
|
||||
|
||||
splitPositions.setData([]);
|
||||
// Only keep showing the road, the cutpoints must be removed from the map
|
||||
roadEventSource.setData([roadEventSource.data[0]])
|
||||
splitPoints.setData([]);
|
||||
});
|
||||
|
||||
cancelButton.SetClass("block btn btn-secondary");
|
||||
|
@ -111,4 +107,21 @@ export default class SplitRoadWizard extends Toggle {
|
|||
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 State from "./State";
|
||||
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
||||
import SplitAction from "./Logic/Osm/SplitAction";
|
||||
import {GeoOperations} from "./Logic/GeoOperations";
|
||||
|
||||
const way = {
|
||||
"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
|
||||
State.state.allElements.addOrGetElement(way);
|
||||
new SplitRoadWizard("way/23583625").AttachTo("maindiv")
|
||||
// State.state.allElements.addOrGetElement(way);
|
||||
new SplitAction(way).DoSplit([splitPoint, splitClose].map(p => GeoOperations.nearestPoint(way,<[number, number]> p.geometry.coordinates)))
|
Loading…
Reference in a new issue