2020-11-18 12:50:28 +01:00
import escapeHtml from "escape-html"
2021-12-13 02:05:34 +01:00
import UserDetails , { OsmConnection } from "./OsmConnection"
2023-04-13 20:58:49 +02:00
import { Store , UIEventSource } from "../UIEventSource"
2020-09-21 23:57:50 +02:00
import Locale from "../../UI/i18n/Locale"
2021-01-02 19:09:49 +01:00
import Constants from "../../Models/Constants"
2021-09-26 23:35:26 +02:00
import { Changes } from "./Changes"
2021-10-04 03:12:42 +02:00
import { Utils } from "../../Utils"
2023-09-28 23:50:27 +02:00
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
2021-10-04 03:12:42 +02:00
export interface ChangesetTag {
key : string
value : string | number
aggregate? : boolean
}
2020-08-26 15:36:04 +02:00
export class ChangesetHandler {
2023-09-25 02:11:42 +02:00
private readonly allElements : FeaturePropertiesStore
2021-10-04 03:12:42 +02:00
private osmConnection : OsmConnection
2021-09-26 23:35:26 +02:00
private readonly changes : Changes
2023-04-13 20:58:49 +02:00
private readonly _dryRun : Store < boolean >
2020-09-15 02:29:31 +02:00
private readonly userDetails : UIEventSource < UserDetails >
2021-10-04 03:12:42 +02:00
private readonly backend : string
2022-04-08 04:18:53 +02:00
/ * *
* Contains previously rewritten IDs
* @private
* /
2024-07-15 01:51:15 +02:00
public readonly _remappings = new Map < string , string > ( )
2024-06-24 13:11:35 +02:00
private readonly _reportError : ( e : string | Error ) = > void
2022-04-08 04:18:53 +02:00
2022-02-14 02:50:21 +01:00
constructor (
2023-04-13 20:58:49 +02:00
dryRun : Store < boolean > ,
2021-10-04 03:12:42 +02:00
osmConnection : OsmConnection ,
2023-09-28 23:50:27 +02:00
allElements :
| FeaturePropertiesStore
| { addAlias : ( id0 : string , id1 : string ) = > void }
| undefined ,
2024-06-20 15:12:51 +02:00
changes : Changes ,
reportError : ( e : string | Error ) = > void
2021-09-26 23:35:26 +02:00
) {
2021-10-04 03:12:42 +02:00
this . osmConnection = osmConnection
2024-06-20 15:12:51 +02:00
this . _reportError = reportError
2023-09-28 23:50:27 +02:00
this . allElements = < FeaturePropertiesStore > allElements
2021-09-26 23:35:26 +02:00
this . changes = changes
2020-08-26 15:36:04 +02:00
this . _dryRun = dryRun
2020-08-27 11:11:20 +02:00
this . userDetails = osmConnection . userDetails
2021-10-04 03:12:42 +02:00
this . backend = osmConnection . _oauth_config . url
2020-08-26 15:36:04 +02:00
2023-10-19 16:34:42 +02:00
if ( dryRun . data ) {
2020-08-26 15:36:04 +02:00
console . log ( "DRYRUN ENABLED" )
}
}
2022-03-15 13:44:34 +01:00
/ * *
* Creates a new list which contains every key at most once
2022-03-16 14:06:10 +01:00
*
* ChangesetHandler . removeDuplicateMetaTags ( [ { key : "k" , value : "v" } , { key : "k0" , value : "v0" } , { key : "k" , value : "v" } ] // => [{key: "k", value: "v"}, {key: "k0", value: "v0"}]
2022-03-15 13:44:34 +01:00
* /
2023-03-26 05:58:28 +02:00
private static removeDuplicateMetaTags ( extraMetaTags : ChangesetTag [ ] ) : ChangesetTag [ ] {
2022-03-15 13:44:34 +01:00
const r : ChangesetTag [ ] = [ ]
const seen = new Set < string > ( )
for ( const extraMetaTag of extraMetaTags ) {
if ( seen . has ( extraMetaTag . key ) ) {
continue
}
r . push ( extraMetaTag )
seen . add ( extraMetaTag . key )
}
return r
}
2022-02-01 00:09:28 +01:00
/ * *
2022-03-14 03:33:03 +01:00
* Inplace rewrite of extraMetaTags
2022-02-01 00:09:28 +01:00
* If the metatags contain a special motivation of the format "<change-type>:node/-<number>" , this method will rewrite this negative number to the actual ID
* The key is changed _in place_ ; true will be returned if a change has been applied
* @param extraMetaTags
* @param rewriteIds
2023-04-07 03:54:11 +02:00
* @public for testing purposes
2022-02-01 00:09:28 +01:00
* /
2023-04-07 03:54:11 +02:00
public static rewriteMetaTags ( extraMetaTags : ChangesetTag [ ] , rewriteIds : Map < string , string > ) {
2022-02-01 00:09:28 +01:00
let hasChange = false
for ( const tag of extraMetaTags ) {
const match = tag . key . match ( /^([a-zA-Z0-9_]+):(node\/-[0-9])$/ )
if ( match == null ) {
continue
}
// This is a special motivation which has a negative ID -> we check for rewrites
const [ _ , reason , id ] = match
if ( rewriteIds . has ( id ) ) {
tag . key = reason + ":" + rewriteIds . get ( id )
hasChange = true
}
}
return hasChange
}
2021-06-30 18:48:23 +02:00
/ * *
* The full logic to upload a change to one or more elements .
*
* This method will attempt to reuse an existing , open changeset for this theme ( or open one if none available ) .
* Then , it will upload a changes - xml within this changeset ( and leave the changeset open )
* When upload is successfull , eventual id - rewriting will be handled ( aka : don ' t worry about that )
*
* If 'dryrun' is specified , the changeset XML will be printed to console instead of being uploaded
*
* /
2021-09-26 23:35:26 +02:00
public async UploadChangeset (
2022-04-08 04:18:53 +02:00
generateChangeXML : ( csid : number , remappings : Map < string , string > ) = > string ,
2021-12-17 19:28:05 +01:00
extraMetaTags : ChangesetTag [ ] ,
openChangeset : UIEventSource < number >
) : Promise < void > {
2021-10-04 03:12:42 +02:00
if (
! extraMetaTags . some ( ( tag ) = > tag . key === "comment" ) ||
! extraMetaTags . some ( ( tag ) = > tag . key === "theme" )
) {
throw "The meta tags should at least contain a `comment` and a `theme`"
}
2022-09-08 21:40:48 +02:00
2022-03-14 03:33:03 +01:00
extraMetaTags = [ . . . extraMetaTags , . . . this . defaultChangesetTags ( ) ]
2022-03-15 13:44:34 +01:00
extraMetaTags = ChangesetHandler . removeDuplicateMetaTags ( extraMetaTags )
2021-06-19 18:28:30 +02:00
if ( this . userDetails . data . csCount == 0 ) {
2020-09-15 02:29:31 +02:00
// The user became a contributor!
this . userDetails . data . csCount = 1
this . userDetails . ping ( )
}
2022-01-21 01:57:16 +01:00
if ( this . _dryRun . data ) {
2022-04-08 04:18:53 +02:00
const changesetXML = generateChangeXML ( 123456 , this . _remappings )
2021-11-09 01:49:07 +01:00
console . log ( "Metatags are" , extraMetaTags )
2020-08-26 15:36:04 +02:00
console . log ( changesetXML )
return
}
2021-12-17 19:28:05 +01:00
if ( openChangeset . data === undefined ) {
2020-08-27 11:11:20 +02:00
// We have to open a new changeset
2021-09-26 23:35:26 +02:00
try {
2021-10-04 03:12:42 +02:00
const csId = await this . OpenChangeset ( extraMetaTags )
2021-12-17 19:28:05 +01:00
openChangeset . setData ( csId )
2022-04-08 04:18:53 +02:00
const changeset = generateChangeXML ( csId , this . _remappings )
2023-06-01 02:52:21 +02:00
console . log (
2021-12-17 19:28:05 +01:00
"Opened a new changeset (openChangeset.data is undefined):" ,
2023-06-14 20:39:36 +02:00
changeset ,
extraMetaTags
2022-09-08 21:40:48 +02:00
)
2022-03-14 03:33:03 +01:00
const changes = await this . UploadChange ( csId , changeset )
2022-02-01 00:09:28 +01:00
const hasSpecialMotivationChanges = ChangesetHandler . rewriteMetaTags (
extraMetaTags ,
changes
)
if ( hasSpecialMotivationChanges ) {
// At this point, 'extraMetaTags' will have changed - we need to set the tags again
2022-12-16 01:02:46 +01:00
await this . UpdateTags ( csId , extraMetaTags )
2022-02-01 00:09:28 +01:00
}
2021-09-26 23:35:26 +02:00
} catch ( e ) {
2024-06-24 13:11:35 +02:00
if ( this . _reportError ) {
2024-06-20 15:12:51 +02:00
this . _reportError ( e )
}
2024-08-14 13:53:56 +02:00
if ( ( < XMLHttpRequest > e ) . status === 400 ) {
2024-08-09 20:38:13 +02:00
// This request is invalid. We simply drop the changes and hope that someone will analyze what went wrong with it in the upload; we pretend everything went fine
return
}
2024-07-21 10:52:51 +02:00
console . warn (
"Could not open/upload changeset due to " ,
e ,
"trying again with a another fresh changeset "
)
2021-12-17 19:28:05 +01:00
openChangeset . setData ( undefined )
2024-06-20 15:12:51 +02:00
throw e
2021-09-26 23:35:26 +02:00
}
2020-08-27 11:11:20 +02:00
} else {
// There still exists an open changeset (or at least we hope so)
2021-10-04 03:12:42 +02:00
// Let's check!
2021-12-17 19:28:05 +01:00
const csId = openChangeset . data
2021-09-26 23:35:26 +02:00
try {
2021-10-04 03:12:42 +02:00
const oldChangesetMeta = await this . GetChangesetMeta ( csId )
if ( ! oldChangesetMeta . open ) {
// Mark the CS as closed...
2021-12-17 19:28:05 +01:00
console . log ( "Could not fetch the metadata from the already open changeset" )
openChangeset . setData ( undefined )
2021-10-04 03:12:42 +02:00
// ... and try again. As the cs is closed, no recursive loop can exist
2021-12-17 19:28:05 +01:00
await this . UploadChangeset ( generateChangeXML , extraMetaTags , openChangeset )
2021-10-04 03:12:42 +02:00
return
}
2022-03-14 03:33:03 +01:00
const rewritings = await this . UploadChange (
2021-09-26 23:35:26 +02:00
csId ,
2022-04-08 04:18:53 +02:00
generateChangeXML ( csId , this . _remappings )
2022-03-14 03:33:03 +01:00
)
2022-09-08 21:40:48 +02:00
2022-03-14 03:33:03 +01:00
const rewrittenTags = this . RewriteTagsOf (
extraMetaTags ,
rewritings ,
oldChangesetMeta
2022-09-08 21:40:48 +02:00
)
2022-03-14 03:33:03 +01:00
await this . UpdateTags ( csId , rewrittenTags )
2021-09-26 23:35:26 +02:00
} catch ( e ) {
2024-06-24 13:11:35 +02:00
if ( this . _reportError ) {
2024-07-21 10:52:51 +02:00
this . _reportError (
"Could not reuse changeset " +
csId +
", might be closed: " +
2024-08-09 20:38:13 +02:00
( e . stacktrace ? ? e . status ? ? "" + e )
2024-07-21 10:52:51 +02:00
)
2024-06-20 15:12:51 +02:00
}
2021-09-26 23:35:26 +02:00
console . warn ( "Could not upload, changeset is probably closed: " , e )
2021-12-17 19:28:05 +01:00
openChangeset . setData ( undefined )
2024-06-20 15:12:51 +02:00
throw e
2021-09-26 23:35:26 +02:00
}
2020-08-27 11:11:20 +02:00
}
2020-08-26 15:36:04 +02:00
}
2022-02-01 00:09:28 +01:00
/ * *
2022-03-14 03:33:03 +01:00
* Given an existing changeset with metadata and extraMetaTags to add , will fuse them to a new set of metatags
* Does not yet send data
2022-02-01 00:09:28 +01:00
* @param extraMetaTags : new changeset tags to add / fuse with this changeset
2022-03-14 03:33:03 +01:00
* @param rewriteIds : the mapping of ids
2022-02-01 00:09:28 +01:00
* @param oldChangesetMeta : the metadata - object of the already existing changeset
2023-04-07 03:54:11 +02:00
*
* @public for testing purposes
2022-02-01 00:09:28 +01:00
* /
2023-04-07 03:54:11 +02:00
public RewriteTagsOf (
2022-03-14 03:33:03 +01:00
extraMetaTags : ChangesetTag [ ] ,
2022-02-01 00:09:28 +01:00
rewriteIds : Map < string , string > ,
oldChangesetMeta : {
open : boolean
id : number
uid : number // User ID
changes_count : number
tags : any
2022-03-14 03:33:03 +01:00
}
) : ChangesetTag [ ] {
2022-02-01 00:09:28 +01:00
// Note: extraMetaTags is where all the tags are collected into
// same as 'extraMetaTag', but indexed
// Note that updates to 'extraTagsById.get(<key>).value = XYZ' is shared with extraMetatags
const extraTagsById = new Map < string , ChangesetTag > ( )
for ( const extraMetaTag of extraMetaTags ) {
extraTagsById . set ( extraMetaTag . key , extraMetaTag )
}
const oldCsTags = oldChangesetMeta . tags
for ( const key in oldCsTags ) {
const newMetaTag = extraTagsById . get ( key )
const existingValue = oldCsTags [ key ]
if ( newMetaTag !== undefined && newMetaTag . value === existingValue ) {
continue
}
if ( newMetaTag === undefined ) {
extraMetaTags . push ( {
key : key ,
2024-07-21 10:52:51 +02:00
value : oldCsTags [ key ] ,
2022-02-01 00:09:28 +01:00
} )
continue
}
if ( newMetaTag . aggregate ) {
let n = Number ( newMetaTag . value )
if ( isNaN ( n ) ) {
n = 0
}
let o = Number ( oldCsTags [ key ] )
if ( isNaN ( o ) ) {
o = 0
}
// We _update_ the tag itself, as it'll be updated in 'extraMetaTags' straight away
newMetaTag . value = "" + ( n + o )
} else {
// The old value is overwritten, thus we drop this old key
}
}
ChangesetHandler . rewriteMetaTags ( extraMetaTags , rewriteIds )
2022-03-14 03:33:03 +01:00
return extraMetaTags
2022-02-01 00:09:28 +01:00
}
2022-04-08 04:18:53 +02:00
/ * *
* Updates the id in the AllElements store , returns the new ID
* @param node : the XML - element , e . g . < node old_id = "-1" new_id = "9650458521" new_version = "1" / >
* @param type
* @private
* /
private static parseIdRewrite ( node : any , type : string ) : [ string , string ] {
2021-11-07 16:34:51 +01:00
const oldId = parseInt ( node . attributes . old_id . value )
if ( node . attributes . new_id === undefined ) {
2022-04-08 04:18:53 +02:00
return [ type + "/" + oldId , undefined ]
2021-11-07 16:34:51 +01:00
}
const newId = parseInt ( node . attributes . new_id . value )
2022-04-08 04:18:53 +02:00
// The actual mapping
2021-11-07 16:34:51 +01:00
const result : [ string , string ] = [ type + "/" + oldId , type + "/" + newId ]
2022-04-08 04:18:53 +02:00
if ( oldId === newId ) {
2021-11-07 16:34:51 +01:00
return undefined
}
return result
}
2022-04-08 04:18:53 +02:00
/ * *
* Given a diff - result XML of the form
* < diffResult version = "0.6" >
* < node old_id = "-1" new_id = "9650458521" new_version = "1" / >
* < way old_id = "-2" new_id = "1050127772" new_version = "1" / >
* < / diffResult > ,
* will :
*
* - create a mapping ` {'node/-1' --> "node/9650458521", 'way/-2' --> "way/9650458521"}
* - Call this . changes . registerIdRewrites
* - Call handleIdRewrites as needed
* @param response
* @private
* /
2022-02-01 00:09:28 +01:00
private parseUploadChangesetResponse ( response : XMLDocument ) : Map < string , string > {
2021-11-07 16:34:51 +01:00
const nodes = response . getElementsByTagName ( "node" )
2022-04-08 04:18:53 +02:00
const mappings : [ string , string ] [ ] = [ ]
2022-09-08 21:40:48 +02:00
2022-03-17 21:51:53 +01:00
for ( const node of Array . from ( nodes ) ) {
2022-04-08 04:18:53 +02:00
const mapping = ChangesetHandler . parseIdRewrite ( node , "node" )
2021-11-07 16:34:51 +01:00
if ( mapping !== undefined ) {
2022-04-08 04:18:53 +02:00
mappings . push ( mapping )
2021-11-07 16:34:51 +01:00
}
}
const ways = response . getElementsByTagName ( "way" )
2022-03-17 21:51:53 +01:00
for ( const way of Array . from ( ways ) ) {
2022-04-08 04:18:53 +02:00
const mapping = ChangesetHandler . parseIdRewrite ( way , "way" )
2021-11-07 16:34:51 +01:00
if ( mapping !== undefined ) {
2022-04-08 04:18:53 +02:00
mappings . push ( mapping )
2021-11-07 16:34:51 +01:00
}
}
2022-04-08 04:18:53 +02:00
for ( const mapping of mappings ) {
const [ oldId , newId ] = mapping
2023-04-13 20:58:49 +02:00
this . allElements ? . addAlias ( oldId , newId )
2022-04-08 04:18:53 +02:00
if ( newId !== undefined ) {
this . _remappings . set ( mapping [ 0 ] , mapping [ 1 ] )
}
}
return new Map < string , string > ( mappings )
2021-11-07 16:34:51 +01:00
}
2021-06-30 18:48:23 +02:00
2023-04-07 03:54:11 +02:00
// noinspection JSUnusedLocalSymbols
2021-10-04 03:12:42 +02:00
private async CloseChangeset ( changesetId : number = undefined ) : Promise < void > {
2023-03-26 05:58:28 +02:00
if ( changesetId === undefined ) {
return
}
await this . osmConnection . put ( "changeset/" + changesetId + "/close" )
console . log ( "Closed changeset " , changesetId )
2021-06-19 18:28:30 +02:00
}
2020-08-26 15:36:04 +02:00
2023-03-26 05:58:28 +02:00
private async GetChangesetMeta ( csId : number ) : Promise < {
2021-10-04 03:12:42 +02:00
id : number
open : boolean
uid : number
changes_count : number
tags : any
} > {
const url = ` ${ this . backend } /api/0.6/changeset/ ${ csId } `
const csData = await Utils . downloadJson ( url )
return csData . elements [ 0 ]
}
2022-02-01 00:09:28 +01:00
/ * *
* Puts the specified tags onto the changesets as they are .
* This method will erase previously set tags
* /
2021-10-04 03:12:42 +02:00
private async UpdateTags ( csId : number , tags : ChangesetTag [ ] ) {
2022-03-15 13:44:34 +01:00
tags = ChangesetHandler . removeDuplicateMetaTags ( tags )
2021-10-04 03:12:42 +02:00
2023-03-26 05:58:28 +02:00
tags = Utils . NoNull ( tags ) . filter (
( tag ) = >
tag . key !== undefined &&
tag . value !== undefined &&
tag . key !== "" &&
tag . value !== ""
)
const metadata = tags . map ( ( kv ) = > ` <tag k=" ${ kv . key } " v=" ${ escapeHtml ( kv . value ) } "/> ` )
const content = [ ` <osm><changeset> ` , metadata , ` </changeset></osm> ` ] . join ( "" )
return this . osmConnection . put ( "changeset/" + csId , content , { "Content-Type" : "text/xml" } )
2021-10-04 03:12:42 +02:00
}
2022-09-08 21:40:48 +02:00
2022-03-14 03:33:03 +01:00
private defaultChangesetTags ( ) : ChangesetTag [ ] {
2024-05-06 14:23:54 +02:00
const usedGps = this . changes . state [ "currentUserLocation" ] ? . features ? . data ? . length > 0
const hasMorePrivacy = ! ! this . changes . state ? . featureSwitches ? . featureSwitchMorePrivacy ? . data
const setSourceAsSurvey = ! hasMorePrivacy && usedGps
2022-03-14 03:33:03 +01:00
return [
[ "created_by" , ` MapComplete ${ Constants . vNumber } ` ] ,
[ "locale" , Locale . language . data ] ,
[ "host" , ` ${ window . location . origin } ${ window . location . pathname } ` ] ,
2024-06-16 16:06:26 +02:00
[ "source" , setSourceAsSurvey ? "survey" : undefined ] ,
2024-07-21 10:52:51 +02:00
[ "imagery" , this . changes . state [ "backgroundLayer" ] ? . data ? . id ] ,
2022-03-14 03:33:03 +01:00
] . map ( ( [ key , value ] ) = > ( {
key ,
value ,
2024-07-21 10:52:51 +02:00
aggregate : false ,
2022-03-14 03:33:03 +01:00
} ) )
}
2021-10-04 03:12:42 +02:00
2022-03-14 03:33:03 +01:00
/ * *
* Opens a changeset with the specified tags
* @param changesetTags
* @constructor
* @private
* /
2023-03-26 05:58:28 +02:00
private async OpenChangeset ( changesetTags : ChangesetTag [ ] ) : Promise < number > {
const metadata = changesetTags
. map ( ( cstag ) = > [ cstag . key , cstag . value ] )
. filter ( ( kv ) = > ( kv [ 1 ] ? ? "" ) !== "" )
. map ( ( kv ) = > ` <tag k=" ${ kv [ 0 ] } " v=" ${ escapeHtml ( kv [ 1 ] ) } "/> ` )
. join ( "\n" )
const csId = await this . osmConnection . put (
"changeset/create" ,
[ ` <osm><changeset> ` , metadata , ` </changeset></osm> ` ] . join ( "" ) ,
{ "Content-Type" : "text/xml" }
)
return Number ( csId )
2020-08-26 15:36:04 +02:00
}
2021-06-30 18:48:23 +02:00
/ * *
* Upload a changesetXML
* /
2023-03-26 05:58:28 +02:00
private async UploadChange (
changesetId : number ,
changesetXML : string
) : Promise < Map < string , string > > {
2024-07-17 18:42:39 +02:00
const response = await this . osmConnection . post < XMLDocument > (
2023-03-26 05:58:28 +02:00
"changeset/" + changesetId + "/upload" ,
changesetXML ,
{ "Content-Type" : "text/xml" }
)
const changes = this . parseUploadChangesetResponse ( response )
console . log ( "Uploaded changeset " , changesetId )
return changes
2020-08-26 15:36:04 +02:00
}
2022-03-06 22:01:01 +01:00
}