2021-11-03 00:44:53 +01:00
import OsmChangeAction from "./OsmChangeAction" ;
import { Changes } from "../Changes" ;
import { ChangeDescription } from "./ChangeDescription" ;
import { Tag } from "../../Tags/Tag" ;
import FeatureSource from "../../FeatureSource/FeatureSource" ;
import { OsmNode , OsmObject , OsmWay } from "../OsmObject" ;
import { GeoOperations } from "../../GeoOperations" ;
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" ;
import CreateNewNodeAction from "./CreateNewNodeAction" ;
import ChangeTagAction from "./ChangeTagAction" ;
import { And } from "../../Tags/And" ;
import { Utils } from "../../../Utils" ;
import { OsmConnection } from "../OsmConnection" ;
2021-12-23 03:36:03 +01:00
import { GeoJSONObject } from "@turf/turf" ;
import FeaturePipeline from "../../FeatureSource/FeaturePipeline" ;
2021-11-03 00:44:53 +01:00
export default class ReplaceGeometryAction extends OsmChangeAction {
2021-12-23 03:36:03 +01:00
/ * *
* The target feature - mostly used for the metadata
* /
2021-11-03 00:44:53 +01:00
private readonly feature : any ;
private readonly state : {
2021-12-23 03:36:03 +01:00
osmConnection : OsmConnection ,
featurePipeline : FeaturePipeline
2021-11-03 00:44:53 +01:00
} ;
private readonly wayToReplaceId : string ;
private readonly theme : string ;
2021-11-04 02:16:07 +01:00
/ * *
2021-12-23 03:36:03 +01:00
* The target coordinates that should end up in OpenStreetMap .
* This is identical to either this . feature . geometry . coordinates or - in case of a polygon - feature . geometry . coordinates [ 0 ]
2021-11-04 02:16:07 +01:00
* /
2021-11-03 00:44:53 +01:00
private readonly targetCoordinates : [ number , number ] [ ] ;
2021-11-04 02:16:07 +01:00
/ * *
* If a target coordinate is close to another target coordinate , 'identicalTo' will point to the first index .
* /
private readonly identicalTo : number [ ]
2021-11-03 00:44:53 +01:00
private readonly newTags : Tag [ ] | undefined ;
constructor (
state : {
2021-12-23 03:36:03 +01:00
osmConnection : OsmConnection ,
featurePipeline : FeaturePipeline
2021-11-03 00:44:53 +01:00
} ,
feature : any ,
wayToReplaceId : string ,
options : {
theme : string ,
newTags? : Tag [ ]
}
) {
2021-11-09 01:49:07 +01:00
super ( wayToReplaceId , false ) ;
2021-11-03 00:44:53 +01:00
this . state = state ;
this . feature = feature ;
this . wayToReplaceId = wayToReplaceId ;
this . theme = options . theme ;
const geom = this . feature . geometry
let coordinates : [ number , number ] [ ]
if ( geom . type === "LineString" ) {
coordinates = geom . coordinates
} else if ( geom . type === "Polygon" ) {
coordinates = geom . coordinates [ 0 ]
}
2021-12-23 03:36:03 +01:00
this . targetCoordinates = coordinates
2021-11-04 02:16:07 +01:00
this . identicalTo = coordinates . map ( _ = > undefined )
for ( let i = 0 ; i < coordinates . length ; i ++ ) {
if ( this . identicalTo [ i ] !== undefined ) {
continue
}
for ( let j = i + 1 ; j < coordinates . length ; j ++ ) {
2021-11-12 18:39:38 +01:00
const d = GeoOperations . distanceBetween ( coordinates [ i ] , coordinates [ j ] )
2021-11-04 02:16:07 +01:00
if ( d < 0.1 ) {
this . identicalTo [ j ] = i
}
}
}
2021-11-03 00:44:53 +01:00
this . newTags = options . newTags
}
2021-12-23 03:36:03 +01:00
// noinspection JSUnusedGlobalSymbols
2021-11-04 02:16:07 +01:00
public async getPreview ( ) : Promise < FeatureSource > {
2022-01-05 16:36:08 +01:00
const { closestIds , allNodesById , detachedNodes , reprojectedNodes } = await this . GetClosestIds ( ) ;
2021-12-23 03:36:03 +01:00
const preview : GeoJSONObject [ ] = closestIds . map ( ( newId , i ) = > {
2021-11-07 16:34:51 +01:00
if ( this . identicalTo [ i ] !== undefined ) {
2021-11-04 02:16:07 +01:00
return undefined
}
2021-11-07 16:34:51 +01:00
2021-11-03 00:44:53 +01:00
if ( newId === undefined ) {
return {
type : "Feature" ,
properties : {
"newpoint" : "yes" ,
2022-01-05 16:36:08 +01:00
"id" : "replace-geometry-move-" + i ,
2021-11-03 00:44:53 +01:00
} ,
geometry : {
type : "Point" ,
coordinates : this.targetCoordinates [ i ]
}
} ;
}
2022-01-05 16:36:08 +01:00
const origNode = allNodesById . get ( newId ) ;
2021-11-03 00:44:53 +01:00
return {
type : "Feature" ,
properties : {
"move" : "yes" ,
"osm-id" : newId ,
2022-01-05 16:36:08 +01:00
"id" : "replace-geometry-move-" + i ,
"original-node-tags" : JSON . stringify ( origNode . tags )
} ,
geometry : {
type : "LineString" ,
coordinates : [ [ origNode . lon , origNode . lat ] , this . targetCoordinates [ i ] ]
}
} ;
} )
2022-01-06 14:39:42 +01:00
reprojectedNodes . forEach ( ( { newLat , newLon , nodeId } ) = > {
2022-01-05 16:36:08 +01:00
const origNode = allNodesById . get ( nodeId ) ;
2022-01-06 14:39:42 +01:00
const feature = {
2022-01-05 16:36:08 +01:00
type : "Feature" ,
properties : {
"move" : "yes" ,
2022-01-06 14:39:42 +01:00
"reprojection" : "yes" ,
2022-01-05 16:36:08 +01:00
"osm-id" : nodeId ,
2022-01-06 14:39:42 +01:00
"id" : "replace-geometry-reproject-" + nodeId ,
2022-01-05 16:36:08 +01:00
"original-node-tags" : JSON . stringify ( origNode . tags )
2021-11-03 00:44:53 +01:00
} ,
geometry : {
type : "LineString" ,
2022-01-05 16:36:08 +01:00
coordinates : [ [ origNode . lon , origNode . lat ] , [ newLon , newLat ] ]
2021-11-03 00:44:53 +01:00
}
} ;
2022-01-05 16:36:08 +01:00
preview . push ( feature )
2021-11-03 00:44:53 +01:00
} )
2021-12-23 03:36:03 +01:00
2022-01-05 16:36:08 +01:00
2022-01-01 01:59:50 +01:00
detachedNodes . forEach ( ( { reason } , id ) = > {
2022-01-05 16:36:08 +01:00
const origNode = allNodesById . get ( id ) ;
2021-12-23 03:36:03 +01:00
const feature = {
type : "Feature" ,
properties : {
"detach" : "yes" ,
2022-01-01 01:59:50 +01:00
"id" : "replace-geometry-detach-" + id ,
2022-01-05 16:36:08 +01:00
"detach-reason" : reason ,
"original-node-tags" : JSON . stringify ( origNode . tags )
2021-12-23 03:36:03 +01:00
} ,
geometry : {
type : "Point" ,
2022-01-05 16:36:08 +01:00
coordinates : [ origNode . lon , origNode . lat ]
2021-12-23 03:36:03 +01:00
}
} ;
preview . push ( feature )
2022-01-01 01:59:50 +01:00
} )
2021-12-23 03:36:03 +01:00
2021-11-04 02:16:07 +01:00
return new StaticFeatureSource ( Utils . NoNull ( preview ) , false )
2021-11-03 00:44:53 +01:00
}
/ * *
2021-12-24 02:51:01 +01:00
* For ' this . feature ` , gets a corresponding closest node that alreay exsists.
2022-01-05 16:36:08 +01:00
*
2021-12-24 02:51:01 +01:00
* This method contains the main logic for this module , as it decides which node gets moved where .
2022-01-05 16:36:08 +01:00
*
2021-11-03 00:44:53 +01:00
* /
2021-12-30 20:41:45 +01:00
public async GetClosestIds ( ) : Promise < {
2021-12-23 03:36:03 +01:00
// 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 ,
2022-01-01 01:59:50 +01:00
detachedNodes : Map < number , {
2022-01-05 16:36:08 +01:00
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 ,
2022-01-06 14:39:42 +01:00
distance : number ,
2022-01-05 16:36:08 +01:00
newLat : number ,
newLon : number ,
nodeId : number
2022-01-01 01:59:50 +01:00
} >
2021-12-23 03:36:03 +01:00
} > {
2021-11-03 00:44:53 +01:00
// TODO FIXME: if a new point has to be created, snap to already existing ways
2021-12-23 03:36:03 +01:00
2022-01-01 01:59:50 +01:00
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)"
}
2022-01-05 16:36:08 +01:00
const self = this ;
2021-12-23 03:36:03 +01:00
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
}
2022-01-06 14:39:42 +01:00
const url = ` ${ this . state . osmConnection ? . _oauth_config ? . url ? ? "https://openstreetmap.org" } /api/0.6/ ${ this . wayToReplaceId } /full ` ;
2021-12-23 03:36:03 +01:00
const rawData = await Utils . downloadJsonCached ( url , 1000 )
parsed = OsmObject . ParseObjects ( rawData . elements ) ;
2021-11-03 00:44:53 +01:00
}
const allNodes = parsed . filter ( o = > o . type === "node" )
2022-01-05 16:36:08 +01:00
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 )
}
2021-11-03 00:44:53 +01:00
/ * *
2021-12-30 20:41:45 +01:00
* For every already existing OSM - point , we calculate :
2022-01-05 16:36:08 +01:00
*
2021-12-30 20:41:45 +01:00
* - the distance to every target point .
* - Wether this node has ( other ) parent ways , which might restrict movement
* - Wether this node has tags set
2022-01-05 16:36:08 +01:00
*
2021-12-30 20:41:45 +01:00
* Having tags and / or being connected to another way indicate that there is some _relation_ with objects in the neighbourhood .
2022-01-05 16:36:08 +01:00
*
2021-12-30 20:41:45 +01:00
* 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
2021-11-03 00:44:53 +01:00
* /
2021-12-30 20:41:45 +01:00
const distances = new Map < number /* osmId*/ ,
2022-01-05 16:36:08 +01:00
/** target coordinate index --> distance (or undefined if a duplicate)*/
number [ ] > ( ) ;
2022-01-01 01:59:50 +01:00
const nodeInfo = new Map < number /* osmId*/ , {
2022-01-05 16:36:08 +01:00
distances : number [ ] ,
2022-01-01 01:59:50 +01:00
// Part of some other way then the one that should be replaced
partOfWay : boolean ,
hasTags : boolean
} > ( )
2022-01-05 16:36:08 +01:00
2021-12-23 03:36:03 +01:00
for ( const node of allNodes ) {
2022-01-05 16:36:08 +01:00
2022-01-01 01:59:50 +01:00
const parentWays = nodeDb . GetParentWays ( node . id )
2022-01-05 16:36:08 +01:00
if ( parentWays === undefined ) {
2022-01-01 01:59:50 +01:00
throw "PANIC: the way to replace has a node which has no parents at all. Is it deleted in the meantime?"
}
2022-01-05 16:36:08 +01:00
const parentWayIds = parentWays . data . map ( w = > w . type + "/" + w . id )
2022-01-01 01:59:50 +01:00
const idIndex = parentWayIds . indexOf ( this . wayToReplaceId )
2022-01-05 16:36:08 +01:00
if ( idIndex < 0 ) {
2022-01-01 01:59:50 +01:00
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
2022-01-05 16:36:08 +01:00
const hasTags = Object . keys ( node . tags ) . length > 1 ;
2021-12-23 03:36:03 +01:00
const nodeDistances = this . targetCoordinates . map ( _ = > undefined )
for ( let i = 0 ; i < this . targetCoordinates . length ; i ++ ) {
if ( this . identicalTo [ i ] !== undefined ) {
continue ;
2021-11-03 00:44:53 +01:00
}
2021-12-23 03:36:03 +01:00
const targetCoordinate = this . targetCoordinates [ i ] ;
const cp = node . centerpoint ( )
2022-01-05 16:36:08 +01:00
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 ;
}
2021-11-03 00:44:53 +01:00
}
2021-12-23 03:36:03 +01:00
distances . set ( node . id , nodeDistances )
2022-01-01 01:59:50 +01:00
nodeInfo . set ( node . id , {
distances : nodeDistances ,
partOfWay : partOfSomeWay ,
2022-01-05 16:36:08 +01:00
hasTags
2022-01-01 01:59:50 +01:00
} )
2021-11-03 00:44:53 +01:00
}
2021-12-30 20:41:45 +01:00
const closestIds = this . targetCoordinates . map ( _ = > undefined )
2022-01-01 01:59:50 +01:00
const unusedIds = new Map < number , {
2022-01-05 16:36:08 +01:00
reason : string ,
hasTags : boolean
2022-01-01 01:59:50 +01:00
} > ( ) ;
2021-12-30 20:41:45 +01:00
{
2022-01-05 16:36:08 +01:00
// 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
} )
}
2021-11-04 02:16:07 +01:00
}
2022-01-05 16:36:08 +01:00
} 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
2021-12-23 03:36:03 +01:00
} )
2022-01-05 16:36:08 +01:00
} )
2021-12-23 03:36:03 +01:00
2022-01-05 16:36:08 +01:00
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 ,
2022-01-06 14:39:42 +01:00
distance : number ,
2022-01-05 16:36:08 +01:00
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
2021-11-03 00:44:53 +01:00
}
2021-12-23 03:36:03 +01:00
2022-01-05 16:36:08 +01:00
// 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 ]
)
reprojectedNodes . set ( id , {
newLon : projected.geometry.coordinates [ 0 ] ,
newLat : projected.geometry.coordinates [ 1 ] ,
projectAfterIndex : projected.properties.index ,
2022-01-06 14:39:42 +01:00
distance : projected.properties.dist ,
2022-01-05 16:36:08 +01:00
nodeId : id
} )
} )
2022-01-06 14:39:42 +01:00
2022-01-05 16:36:08 +01:00
reprojectedNodes . forEach ( ( _ , nodeId ) = > unusedIds . delete ( nodeId ) )
2022-01-06 14:39:42 +01:00
2022-01-05 16:36:08 +01:00
}
2021-12-23 03:36:03 +01:00
2022-01-05 16:36:08 +01:00
return { closestIds , allNodesById , osmWay , detachedNodes : unusedIds , reprojectedNodes } ;
}
2021-12-23 03:36:03 +01:00
2022-01-05 16:36:08 +01:00
protected async CreateChangeDescriptions ( changes : Changes ) : Promise < ChangeDescription [ ] > {
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 ++ ) {
if ( this . identicalTo [ i ] !== undefined ) {
const j = this . identicalTo [ i ]
actualIdsToUse . push ( actualIdsToUse [ j ] )
continue
}
const closestId = closestIds [ i ] ;
const [ lon , lat ] = this . targetCoordinates [ i ]
if ( closestId === undefined ) {
const newNodeAction = new CreateNewNodeAction (
[ ] ,
lat , lon ,
{
allowReuseOfPreviouslyCreatedPoints : true ,
theme : this.theme , changeType : null
2022-01-01 01:59:50 +01:00
} )
2022-01-05 16:36:08 +01:00
const changeDescr = await newNodeAction . CreateChangeDescriptions ( changes )
allChanges . push ( . . . changeDescr )
actualIdsToUse . push ( newNodeAction . newElementIdNumber )
} else {
const change = < ChangeDescription > {
id : closestId ,
type : "node" ,
meta : {
theme : this.theme ,
changeType : "move"
} ,
changes : { lon , lat }
2021-11-03 00:44:53 +01:00
}
2022-01-05 16:36:08 +01:00
actualIdsToUse . push ( closestId )
allChanges . push ( change )
2021-11-03 00:44:53 +01:00
}
2022-01-05 16:36:08 +01:00
}
2021-11-03 00:44:53 +01:00
2022-01-05 16:36:08 +01:00
if ( this . newTags !== undefined && this . newTags . length > 0 ) {
const addExtraTags = new ChangeTagAction (
this . wayToReplaceId ,
new And ( this . newTags ) ,
osmWay . tags , {
theme : this.theme ,
changeType : "conflation"
}
)
allChanges . push ( . . . await addExtraTags . CreateChangeDescriptions ( changes ) )
}
2022-01-06 14:39:42 +01:00
const newCoordinates = [ . . . this . targetCoordinates ]
{
// Add reprojected nodes to the way
const proj = Array . from ( reprojectedNodes . values ( ) )
proj . sort ( ( a , b ) = > {
// Sort descending
const diff = b . projectAfterIndex - a . projectAfterIndex ;
if ( diff !== 0 ) {
return diff
}
return b . distance - a . distance ;
} )
for ( const reprojectedNode of proj ) {
const change = < ChangeDescription > {
id : reprojectedNode.nodeId ,
type : "node" ,
meta : {
theme : this.theme ,
changeType : "move"
} ,
changes : { lon : reprojectedNode.newLon , lat : reprojectedNode.newLat }
}
allChanges . push ( change )
actualIdsToUse . splice ( reprojectedNode . projectAfterIndex + 1 , 0 , reprojectedNode . nodeId )
newCoordinates . splice ( reprojectedNode . projectAfterIndex + 1 , 0 , [ reprojectedNode . newLon , reprojectedNode . newLat ] )
}
}
2022-01-05 16:36:08 +01:00
// Actually change the nodes of the way!
allChanges . push ( {
type : "way" ,
id : osmWay.id ,
changes : {
nodes : actualIdsToUse ,
2022-01-06 14:39:42 +01:00
coordinates : newCoordinates
2022-01-05 16:36:08 +01:00
} ,
meta : {
theme : this.theme ,
changeType : "conflation"
2021-12-23 03:36:03 +01:00
}
2022-01-05 16:36:08 +01:00
} )
// Some nodes might need to be deleted
if ( detachedNodes . size > 0 ) {
detachedNodes . forEach ( ( { hasTags , reason } , nodeId ) = > {
const parentWays = nodeDb . GetParentWays ( nodeId )
const index = parentWays . data . map ( w = > w . id ) . indexOf ( osmWay . id )
if ( index < 0 ) {
console . error ( "ReplaceGeometryAction is trying to detach node " + nodeId + ", but it isn't listed as being part of way " + osmWay . id )
return ;
}
// We detachted this node - so we unregister
parentWays . data . splice ( index , 1 )
parentWays . ping ( ) ;
if ( hasTags ) {
// Has tags: we leave this node alone
return ;
}
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 ,
} )
} )
2021-11-03 00:44:53 +01:00
}
2022-01-05 16:36:08 +01:00
return allChanges
2021-11-03 00:44:53 +01:00
}
}