forked from MapComplete/MapComplete
More work on conflation logic
This commit is contained in:
parent
d9ad4daaf6
commit
8485773a1d
3 changed files with 315 additions and 213 deletions
|
@ -81,7 +81,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
public async getPreview(): Promise<FeatureSource> {
|
public async getPreview(): Promise<FeatureSource> {
|
||||||
const {closestIds, allNodesById, detachedNodes} = await this.GetClosestIds();
|
const {closestIds, allNodesById, detachedNodes, reprojectedNodes} = await this.GetClosestIds();
|
||||||
const preview: GeoJSONObject[] = closestIds.map((newId, i) => {
|
const preview: GeoJSONObject[] = closestIds.map((newId, i) => {
|
||||||
if (this.identicalTo[i] !== undefined) {
|
if (this.identicalTo[i] !== undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -92,7 +92,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties: {
|
properties: {
|
||||||
"newpoint": "yes",
|
"newpoint": "yes",
|
||||||
"id": "replace-geometry-move-" + i
|
"id": "replace-geometry-move-" + i,
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
|
@ -100,33 +100,60 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const origPoint = allNodesById.get(newId).centerpoint()
|
|
||||||
|
const origNode = allNodesById.get(newId);
|
||||||
return {
|
return {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties: {
|
properties: {
|
||||||
"move": "yes",
|
"move": "yes",
|
||||||
"osm-id": newId,
|
"osm-id": newId,
|
||||||
"id": "replace-geometry-move-" + i
|
"id": "replace-geometry-move-" + i,
|
||||||
|
"original-node-tags": JSON.stringify(origNode.tags)
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "LineString",
|
type: "LineString",
|
||||||
coordinates: [[origPoint[1], origPoint[0]], this.targetCoordinates[i]]
|
coordinates: [[origNode.lon, origNode.lat], this.targetCoordinates[i]]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
reprojectedNodes.forEach(({newLat, newLon, nodeId}) => {
|
||||||
|
|
||||||
|
const origNode = allNodesById.get(nodeId);
|
||||||
|
const feature = {
|
||||||
|
type: "Feature",
|
||||||
|
properties: {
|
||||||
|
"move": "yes",
|
||||||
|
"reprojection":"yes",
|
||||||
|
"osm-id": nodeId,
|
||||||
|
"id": "replace-geometry-reproject-" + nodeId ,
|
||||||
|
"original-node-tags": JSON.stringify(origNode.tags)
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: "LineString",
|
||||||
|
coordinates: [[origNode.lon, origNode.lat], [newLon, newLat]]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
preview.push(feature)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
detachedNodes.forEach(({reason}, id) => {
|
detachedNodes.forEach(({reason}, id) => {
|
||||||
const origPoint = allNodesById.get(id).centerpoint()
|
const origNode = allNodesById.get(id);
|
||||||
const feature = {
|
const feature = {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties: {
|
properties: {
|
||||||
"detach": "yes",
|
"detach": "yes",
|
||||||
"id": "replace-geometry-detach-" + id,
|
"id": "replace-geometry-detach-" + id,
|
||||||
"detach-reason":reason
|
"detach-reason": reason,
|
||||||
|
"original-node-tags": JSON.stringify(origNode.tags)
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
coordinates: [origPoint[1], origPoint[0]]
|
coordinates: [origNode.lon, origNode.lat]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
preview.push(feature)
|
preview.push(feature)
|
||||||
|
@ -137,18 +164,254 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
/**
|
||||||
|
* For 'this.feature`, gets a corresponding closest node that alreay exsists.
|
||||||
|
*
|
||||||
|
* This method contains the main logic for this module, as it decides which node gets moved where.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public async GetClosestIds(): Promise<{
|
||||||
|
|
||||||
const allChanges: ChangeDescription[] = []
|
// A list of the same length as targetCoordinates, containing which OSM-point to move. If undefined, a new point will be created
|
||||||
const actualIdsToUse: number[] = []
|
closestIds: number[],
|
||||||
|
allNodesById: Map<number, OsmNode>,
|
||||||
|
osmWay: OsmWay,
|
||||||
|
detachedNodes: Map<number, {
|
||||||
|
reason: string,
|
||||||
|
hasTags: boolean
|
||||||
|
}>,
|
||||||
|
reprojectedNodes: Map<number, {
|
||||||
|
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
|
||||||
|
projectAfterIndex: number,
|
||||||
|
newLat: number,
|
||||||
|
newLon: number,
|
||||||
|
nodeId: number
|
||||||
|
}>
|
||||||
|
}> {
|
||||||
|
// TODO FIXME: if a new point has to be created, snap to already existing ways
|
||||||
|
|
||||||
const nodeDb = this.state.featurePipeline.fullNodeDatabase;
|
const nodeDb = this.state.featurePipeline.fullNodeDatabase;
|
||||||
if (nodeDb === undefined) {
|
if (nodeDb === undefined) {
|
||||||
throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
|
throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
|
||||||
}
|
}
|
||||||
|
const self = this;
|
||||||
|
let parsed: OsmObject[];
|
||||||
|
{
|
||||||
|
// Gather the needed OsmObjects
|
||||||
|
const splitted = this.wayToReplaceId.split("/");
|
||||||
|
const type = splitted[0];
|
||||||
|
const idN = Number(splitted[1]);
|
||||||
|
if (idN < 0 || type !== "way") {
|
||||||
|
throw "Invalid ID to conflate: " + this.wayToReplaceId
|
||||||
|
}
|
||||||
|
const url = `${this.state.osmConnection._oauth_config.url}/api/0.6/${this.wayToReplaceId}/full`;
|
||||||
|
const rawData = await Utils.downloadJsonCached(url, 1000)
|
||||||
|
parsed = OsmObject.ParseObjects(rawData.elements);
|
||||||
|
}
|
||||||
|
const allNodes = parsed.filter(o => o.type === "node")
|
||||||
|
const osmWay = <OsmWay>parsed[parsed.length - 1]
|
||||||
|
if (osmWay.type !== "way") {
|
||||||
|
throw "WEIRD: expected an OSM-way as last element here!"
|
||||||
|
}
|
||||||
|
const allNodesById = new Map<number, OsmNode>()
|
||||||
|
for (const node of allNodes) {
|
||||||
|
allNodesById.set(node.id, <OsmNode>node)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* For every already existing OSM-point, we calculate:
|
||||||
|
*
|
||||||
|
* - the distance to every target point.
|
||||||
|
* - Wether this node has (other) parent ways, which might restrict movement
|
||||||
|
* - Wether this node has tags set
|
||||||
|
*
|
||||||
|
* Having tags and/or being connected to another way indicate that there is some _relation_ with objects in the neighbourhood.
|
||||||
|
*
|
||||||
|
* The Replace-geometry action should try its best to honour these. Some 'wiggling' is allowed (e.g. moving an entrance a bit), but these relations should not be broken.l
|
||||||
|
*/
|
||||||
|
const distances = new Map<number /* osmId*/,
|
||||||
|
/** target coordinate index --> distance (or undefined if a duplicate)*/
|
||||||
|
number[]>();
|
||||||
|
|
||||||
const {closestIds, osmWay, detachedNodes} = await this.GetClosestIds()
|
const nodeInfo = new Map<number /* osmId*/, {
|
||||||
const detachedNodeIds = Array.from(detachedNodes.keys());
|
distances: number[],
|
||||||
|
// Part of some other way then the one that should be replaced
|
||||||
|
partOfWay: boolean,
|
||||||
|
hasTags: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
for (const node of allNodes) {
|
||||||
|
|
||||||
|
const parentWays = nodeDb.GetParentWays(node.id)
|
||||||
|
if (parentWays === undefined) {
|
||||||
|
throw "PANIC: the way to replace has a node which has no parents at all. Is it deleted in the meantime?"
|
||||||
|
}
|
||||||
|
const parentWayIds = parentWays.data.map(w => w.type + "/" + w.id)
|
||||||
|
const idIndex = parentWayIds.indexOf(this.wayToReplaceId)
|
||||||
|
if (idIndex < 0) {
|
||||||
|
throw "PANIC: the way to replace has a node, which is _not_ part of this was according to the node..."
|
||||||
|
}
|
||||||
|
parentWayIds.splice(idIndex, 1)
|
||||||
|
const partOfSomeWay = parentWayIds.length > 0
|
||||||
|
const hasTags = Object.keys(node.tags).length > 1;
|
||||||
|
|
||||||
|
const nodeDistances = this.targetCoordinates.map(_ => undefined)
|
||||||
|
for (let i = 0; i < this.targetCoordinates.length; i++) {
|
||||||
|
if (this.identicalTo[i] !== undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const targetCoordinate = this.targetCoordinates[i];
|
||||||
|
const cp = node.centerpoint()
|
||||||
|
const d = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]])
|
||||||
|
if (d > 25) {
|
||||||
|
// This is too much to move
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (d < 3 || !(hasTags || partOfSomeWay)) {
|
||||||
|
// If there is some relation: cap the move distance to 3m
|
||||||
|
nodeDistances[i] = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
distances.set(node.id, nodeDistances)
|
||||||
|
nodeInfo.set(node.id, {
|
||||||
|
distances: nodeDistances,
|
||||||
|
partOfWay: partOfSomeWay,
|
||||||
|
hasTags
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const closestIds = this.targetCoordinates.map(_ => undefined)
|
||||||
|
const unusedIds = new Map<number, {
|
||||||
|
reason: string,
|
||||||
|
hasTags: boolean
|
||||||
|
}>();
|
||||||
|
{
|
||||||
|
// Search best merge candidate
|
||||||
|
/**
|
||||||
|
* Then, we search the node that has to move the least distance and add this as mapping.
|
||||||
|
* We do this until no points are left
|
||||||
|
*/
|
||||||
|
let candidate: number;
|
||||||
|
let moveDistance: number;
|
||||||
|
/**
|
||||||
|
* The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates
|
||||||
|
*/
|
||||||
|
do {
|
||||||
|
candidate = undefined;
|
||||||
|
moveDistance = Infinity;
|
||||||
|
distances.forEach((distances, nodeId) => {
|
||||||
|
const minDist = Math.min(...Utils.NoNull(distances))
|
||||||
|
if (moveDistance > minDist) {
|
||||||
|
// We have found a candidate to move
|
||||||
|
candidate = nodeId
|
||||||
|
moveDistance = minDist
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (candidate !== undefined) {
|
||||||
|
// We found a candidate... Search the corresponding target id:
|
||||||
|
let targetId: number = undefined;
|
||||||
|
let lowestDistance = Number.MAX_VALUE
|
||||||
|
let nodeDistances = distances.get(candidate)
|
||||||
|
for (let i = 0; i < nodeDistances.length; i++) {
|
||||||
|
const d = nodeDistances[i]
|
||||||
|
if (d !== undefined && d < lowestDistance) {
|
||||||
|
lowestDistance = d;
|
||||||
|
targetId = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This candidates role is done, it can be removed from the distance matrix
|
||||||
|
distances.delete(candidate)
|
||||||
|
|
||||||
|
if (targetId !== undefined) {
|
||||||
|
// At this point, we have our target coordinate index: targetId!
|
||||||
|
// Lets map it...
|
||||||
|
closestIds[targetId] = candidate
|
||||||
|
|
||||||
|
// To indicate that this targetCoordinate is taken, we remove them from the distances matrix
|
||||||
|
distances.forEach(dists => {
|
||||||
|
dists[targetId] = undefined
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Seems like all the targetCoordinates have found a source point
|
||||||
|
unusedIds.set(candidate, {
|
||||||
|
reason: "Unused by new way",
|
||||||
|
hasTags: nodeInfo.get(candidate).hasTags
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (candidate !== undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are still unused values in 'distances', they are definitively unused
|
||||||
|
distances.forEach((_, nodeId) => {
|
||||||
|
unusedIds.set(nodeId, {
|
||||||
|
reason: "Unused by new way",
|
||||||
|
hasTags: nodeInfo.get(nodeId).hasTags
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const reprojectedNodes = new Map<number, {
|
||||||
|
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
|
||||||
|
projectAfterIndex: number,
|
||||||
|
newLat: number,
|
||||||
|
newLon: number,
|
||||||
|
nodeId: number
|
||||||
|
}>();
|
||||||
|
{
|
||||||
|
// Lets check the unused ids: can they be detached or do they signify some relation with the object?
|
||||||
|
unusedIds.forEach(({}, id) => {
|
||||||
|
const info = nodeInfo.get(id)
|
||||||
|
if (!(info.hasTags || info.partOfWay)) {
|
||||||
|
// Nothing special here, we detach
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current node has tags and/or has an attached other building.
|
||||||
|
// We should project them and move them onto the building on an appropriate place
|
||||||
|
const node = allNodesById.get(id)
|
||||||
|
|
||||||
|
// Project the node onto the target way to calculate the new coordinates
|
||||||
|
const way = {
|
||||||
|
type: "Feature",
|
||||||
|
properties: {},
|
||||||
|
geometry: {
|
||||||
|
type: "LineString",
|
||||||
|
coordinates: self.targetCoordinates
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const projected = GeoOperations.nearestPoint(
|
||||||
|
way, [node.lon, node.lat]
|
||||||
|
)
|
||||||
|
console.trace("Node "+id+" should be kept and projected to ", projected)
|
||||||
|
|
||||||
|
reprojectedNodes.set(id, {
|
||||||
|
newLon: projected.geometry.coordinates[0],
|
||||||
|
newLat: projected.geometry.coordinates[1],
|
||||||
|
projectAfterIndex: projected.properties.index,
|
||||||
|
nodeId: id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
reprojectedNodes.forEach((_, nodeId) => unusedIds.delete(nodeId))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {closestIds, allNodesById, osmWay, detachedNodes: unusedIds, reprojectedNodes};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||||
|
throw "Use reprojectedNodes!" // TODO FIXME
|
||||||
|
const nodeDb = this.state.featurePipeline.fullNodeDatabase;
|
||||||
|
if (nodeDb === undefined) {
|
||||||
|
throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
|
||||||
|
}
|
||||||
|
|
||||||
|
const {closestIds, osmWay, detachedNodes, reprojectedNodes} = await this.GetClosestIds()
|
||||||
|
const allChanges: ChangeDescription[] = []
|
||||||
|
const actualIdsToUse: number[] = []
|
||||||
for (let i = 0; i < closestIds.length; i++) {
|
for (let i = 0; i < closestIds.length; i++) {
|
||||||
if (this.identicalTo[i] !== undefined) {
|
if (this.identicalTo[i] !== undefined) {
|
||||||
const j = this.identicalTo[i]
|
const j = this.identicalTo[i]
|
||||||
|
@ -214,35 +477,40 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
|
|
||||||
|
|
||||||
// Some nodes might need to be deleted
|
// Some nodes might need to be deleted
|
||||||
if (detachedNodeIds.length > 0) {
|
const detachedNodeIds = Array.from(detachedNodes.keys());
|
||||||
|
|
||||||
|
if (detachedNodes.size > 0) {
|
||||||
for (const nodeId of detachedNodeIds) {
|
detachedNodes.forEach(({hasTags, reason}, nodeId) => {
|
||||||
const parentWays = nodeDb.GetParentWays(nodeId)
|
const parentWays = nodeDb.GetParentWays(nodeId)
|
||||||
const index = parentWays.data.map(w => w.id).indexOf(osmWay.id)
|
const index = parentWays.data.map(w => w.id).indexOf(osmWay.id)
|
||||||
if(index < 0){
|
if (index < 0) {
|
||||||
console.error("ReplaceGeometryAction is trying to detach node "+nodeId+", but it isn't listed as being part of way "+osmWay.id)
|
console.error("ReplaceGeometryAction is trying to detach node " + nodeId + ", but it isn't listed as being part of way " + osmWay.id)
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
// We detachted this node - so we unregister
|
// We detachted this node - so we unregister
|
||||||
parentWays.data.splice(index, 1)
|
parentWays.data.splice(index, 1)
|
||||||
parentWays.ping();
|
parentWays.ping();
|
||||||
if(parentWays.data.length == 0){
|
|
||||||
// This point has no other ways anymore - lets clean it!
|
if (hasTags) {
|
||||||
console.log("Removing node "+nodeId, "as it isn't needed anymore by any way")
|
// Has tags: we leave this node alone
|
||||||
|
return;
|
||||||
allChanges.push({
|
|
||||||
meta: {
|
|
||||||
theme: this.theme,
|
|
||||||
changeType: "delete"
|
|
||||||
},
|
|
||||||
doDelete: true,
|
|
||||||
type: "node",
|
|
||||||
id: nodeId,
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
if (parentWays.data.length != 0) {
|
||||||
|
// Still part of other ways: we leave this node alone!
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Removing node " + nodeId, "as it isn't needed anymore by any way")
|
||||||
|
allChanges.push({
|
||||||
|
meta: {
|
||||||
|
theme: this.theme,
|
||||||
|
changeType: "delete"
|
||||||
|
},
|
||||||
|
doDelete: true,
|
||||||
|
type: "node",
|
||||||
|
id: nodeId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -250,180 +518,5 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
return allChanges
|
return allChanges
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* For 'this.feature`, gets a corresponding closest node that alreay exsists.
|
|
||||||
*
|
|
||||||
* This method contains the main logic for this module, as it decides which node gets moved where.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public async GetClosestIds(): Promise<{
|
|
||||||
|
|
||||||
// A list of the same length as targetCoordinates, containing which OSM-point to move. If undefined, a new point will be created
|
|
||||||
closestIds: number[],
|
|
||||||
allNodesById: Map<number, OsmNode>,
|
|
||||||
osmWay: OsmWay,
|
|
||||||
detachedNodes: Map<number, {
|
|
||||||
reason: string
|
|
||||||
}>
|
|
||||||
}> {
|
|
||||||
// TODO FIXME: cap move length on points which are embedded into other ways (ev. disconnect them)
|
|
||||||
// TODO FIXME: if a new point has to be created, snap to already existing ways
|
|
||||||
|
|
||||||
const nodeDb = this.state.featurePipeline.fullNodeDatabase;
|
|
||||||
if (nodeDb === undefined) {
|
|
||||||
throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed: OsmObject[];
|
|
||||||
{
|
|
||||||
// Gather the needed OsmObjects
|
|
||||||
const splitted = this.wayToReplaceId.split("/");
|
|
||||||
const type = splitted[0];
|
|
||||||
const idN = Number(splitted[1]);
|
|
||||||
if (idN < 0 || type !== "way") {
|
|
||||||
throw "Invalid ID to conflate: " + this.wayToReplaceId
|
|
||||||
}
|
|
||||||
const url = `${this.state.osmConnection._oauth_config.url}/api/0.6/${this.wayToReplaceId}/full`;
|
|
||||||
const rawData = await Utils.downloadJsonCached(url, 1000)
|
|
||||||
parsed = OsmObject.ParseObjects(rawData.elements);
|
|
||||||
}
|
|
||||||
const allNodes = parsed.filter(o => o.type === "node")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For every already existing OSM-point, we calculate:
|
|
||||||
*
|
|
||||||
* - the distance to every target point.
|
|
||||||
* - Wether this node has (other) parent ways, which might restrict movement
|
|
||||||
* - Wether this node has tags set
|
|
||||||
*
|
|
||||||
* Having tags and/or being connected to another way indicate that there is some _relation_ with objects in the neighbourhood.
|
|
||||||
*
|
|
||||||
* The Replace-geometry action should try its best to honour these. Some 'wiggling' is allowed (e.g. moving an entrance a bit), but these relations should not be broken.l
|
|
||||||
*/
|
|
||||||
const distances = new Map<number /* osmId*/,
|
|
||||||
/** target coordinate index --> distance (or undefined if a duplicate)*/
|
|
||||||
number[]>();
|
|
||||||
|
|
||||||
const nodeInfo = new Map<number /* osmId*/, {
|
|
||||||
distances :number[],
|
|
||||||
// Part of some other way then the one that should be replaced
|
|
||||||
partOfWay: boolean,
|
|
||||||
hasTags: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
for (const node of allNodes) {
|
|
||||||
|
|
||||||
const parentWays = nodeDb.GetParentWays(node.id)
|
|
||||||
if(parentWays === undefined){
|
|
||||||
throw "PANIC: the way to replace has a node which has no parents at all. Is it deleted in the meantime?"
|
|
||||||
}
|
|
||||||
const parentWayIds = parentWays.data.map(w => w.type+"/"+w.id)
|
|
||||||
const idIndex = parentWayIds.indexOf(this.wayToReplaceId)
|
|
||||||
if(idIndex < 0){
|
|
||||||
throw "PANIC: the way to replace has a node, which is _not_ part of this was according to the node..."
|
|
||||||
}
|
|
||||||
parentWayIds.splice(idIndex, 1)
|
|
||||||
const partOfSomeWay = parentWayIds.length > 0
|
|
||||||
|
|
||||||
|
|
||||||
const nodeDistances = this.targetCoordinates.map(_ => undefined)
|
|
||||||
for (let i = 0; i < this.targetCoordinates.length; i++) {
|
|
||||||
if (this.identicalTo[i] !== undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const targetCoordinate = this.targetCoordinates[i];
|
|
||||||
const cp = node.centerpoint()
|
|
||||||
nodeDistances[i] = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]])
|
|
||||||
}
|
|
||||||
distances.set(node.id, nodeDistances)
|
|
||||||
nodeInfo.set(node.id, {
|
|
||||||
distances: nodeDistances,
|
|
||||||
partOfWay: partOfSomeWay,
|
|
||||||
hasTags: Object.keys(node.tags).length > 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const closestIds = this.targetCoordinates.map(_ => undefined)
|
|
||||||
const unusedIds = new Map<number, {
|
|
||||||
reason: string
|
|
||||||
}>();
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Then, we search the node that has to move the least distance and add this as mapping.
|
|
||||||
* We do this until no points are left
|
|
||||||
*/
|
|
||||||
let candidate: number;
|
|
||||||
let moveDistance: number;
|
|
||||||
/**
|
|
||||||
* The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates
|
|
||||||
*/
|
|
||||||
do {
|
|
||||||
candidate = undefined;
|
|
||||||
moveDistance = Infinity;
|
|
||||||
distances.forEach((distances, nodeId) => {
|
|
||||||
const minDist = Math.min(...Utils.NoNull(distances))
|
|
||||||
if (moveDistance > minDist) {
|
|
||||||
// We have found a candidate to move
|
|
||||||
candidate = nodeId
|
|
||||||
moveDistance = minDist
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (candidate !== undefined) {
|
|
||||||
// We found a candidate... Search the corresponding target id:
|
|
||||||
let targetId: number = undefined;
|
|
||||||
let lowestDistance = Number.MAX_VALUE
|
|
||||||
let nodeDistances = distances.get(candidate)
|
|
||||||
for (let i = 0; i < nodeDistances.length; i++) {
|
|
||||||
const d = nodeDistances[i]
|
|
||||||
if (d !== undefined && d < lowestDistance) {
|
|
||||||
lowestDistance = d;
|
|
||||||
targetId = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This candidates role is done, it can be removed from the distance matrix
|
|
||||||
distances.delete(candidate)
|
|
||||||
|
|
||||||
if (targetId !== undefined) {
|
|
||||||
// At this point, we have our target coordinate index: targetId!
|
|
||||||
// Lets map it...
|
|
||||||
closestIds[targetId] = candidate
|
|
||||||
|
|
||||||
// To indicate that this targetCoordinate is taken, we remove them from the distances matrix
|
|
||||||
distances.forEach(dists => {
|
|
||||||
dists[targetId] = undefined
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Seems like all the targetCoordinates have found a source point
|
|
||||||
unusedIds.set(candidate,{
|
|
||||||
reason: "Unused by new way"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (candidate !== undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are still unused values in 'distances', they are definitively unused
|
|
||||||
distances.forEach((_, nodeId) => {
|
|
||||||
unusedIds.set(nodeId,{
|
|
||||||
reason: "Unused by new way"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
{
|
|
||||||
// Some extra data is included for rendering
|
|
||||||
const osmWay = <OsmWay>parsed[parsed.length - 1]
|
|
||||||
if (osmWay.type !== "way") {
|
|
||||||
throw "WEIRD: expected an OSM-way as last element here!"
|
|
||||||
}
|
|
||||||
const allNodesById = new Map<number, OsmNode>()
|
|
||||||
for (const node of allNodes) {
|
|
||||||
allNodesById.set(node.id, <OsmNode>node)
|
|
||||||
}
|
|
||||||
return {closestIds, allNodesById, osmWay, detachedNodes: unusedIds};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -391,10 +391,9 @@ export class OsmWay extends OsmObject {
|
||||||
// This is probably part of a relation which hasn't been fully downloaded
|
// This is probably part of a relation which hasn't been fully downloaded
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const cp = node.centerpoint();
|
this.coordinates.push(node.centerpoint());
|
||||||
this.coordinates.push(cp);
|
latSum += node.lat
|
||||||
latSum += cp[0]
|
lonSum += node.lon
|
||||||
lonSum += cp[1]
|
|
||||||
}
|
}
|
||||||
let count = this.coordinates.length;
|
let count = this.coordinates.length;
|
||||||
this.lat = latSum / count;
|
this.lat = latSum / count;
|
||||||
|
|
|
@ -31,6 +31,10 @@
|
||||||
"icon": {
|
"icon": {
|
||||||
"render": "circle:#0f0",
|
"render": "circle:#0f0",
|
||||||
"mappings": [
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "reprojection=yes",
|
||||||
|
"then": "ring:#f00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"if": "move=no",
|
"if": "move=no",
|
||||||
"then": "ring:#0f0"
|
"then": "ring:#0f0"
|
||||||
|
@ -42,6 +46,12 @@
|
||||||
{
|
{
|
||||||
"location": "start",
|
"location": "start",
|
||||||
"icon": "square:#f00",
|
"icon": "square:#f00",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "reprojection=yes",
|
||||||
|
"then": "ring:#f00"
|
||||||
|
}
|
||||||
|
],
|
||||||
"iconSize": {
|
"iconSize": {
|
||||||
"render": "10,10,center",
|
"render": "10,10,center",
|
||||||
"mappings": [
|
"mappings": [
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue