Simplify handling of id rewrites by centralizing them to the changesethandler (which improves testability too), fixes a part of #564

This commit is contained in:
pietervdvn 2022-04-08 04:18:53 +02:00
parent c3f3f69c3b
commit 9238f0f381
7 changed files with 232 additions and 96 deletions

View file

@ -49,6 +49,29 @@ export class ElementStorage {
return this._elements.has(id); return this._elements.has(id);
} }
addAlias(oldId: string, newId: string){
if (newId === undefined) {
// We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
const element = this.getEventSourceById(oldId);
element.data._deleted = "yes"
element.ping();
return;
}
if (oldId == newId) {
return undefined;
}
const element = this.getEventSourceById( oldId);
if (element === undefined) {
// Element to rewrite not found, probably a node or relation that is not rendered
return undefined
}
element.data.id = newId;
this.addElementById(newId, element);
this.ContainingFeatures.set(newId, this.ContainingFeatures.get( oldId))
element.ping();
}
private addOrGetById(elementId: string, newProperties: any): UIEventSource<any> { private addOrGetById(elementId: string, newProperties: any): UIEventSource<any> {
if (!this._elements.has(elementId)) { if (!this._elements.has(elementId)) {
const eventSource = new UIEventSource<any>(newProperties, "tags of " + elementId); const eventSource = new UIEventSource<any>(newProperties, "tags of " + elementId);

View file

@ -71,6 +71,110 @@ export interface ChangeDescription {
export class ChangeDescriptionTools { export class ChangeDescriptionTools {
/**
* Rewrites all the ids in a changeDescription
*
* // should rewrite the id of the changed object
* const change = <ChangeDescription> {
* id: -1234,
* type: "node",
* meta:{
* theme:"test",
* changeType: "answer"
* },
* tags:[
* {
* k: "key",
* v: "value"
* }
* ]
* }
* }
* const mapping = new Map<string, string>([["node/-1234", "node/42"]])
* const rewritten = ChangeDescriptionTools.rewriteIds(change, mapping)
* rewritten.id // => 42
*
* // should rewrite ids in nodes of a way
* const change = <ChangeDescription> {
* type: "way",
* id: 789,
* changes: {
* nodes: [-1, -2, -3, 68453],
* coordinates: []
* },
* meta:{
* theme:"test",
* changeType: "create"
* }
* }
* const mapping = new Map<string, string>([["node/-1", "node/42"],["node/-2", "node/43"],["node/-3", "node/44"]])
* const rewritten = ChangeDescriptionTools.rewriteIds(change, mapping)
* rewritten.id // => 789
* rewritten.changes["nodes"] // => [42,43,44, 68453]
*
* // should rewrite ids in relationship members
* const change = <ChangeDescription> {
* type: "way",
* id: 789,
* changes: {
* members: [{type: "way", ref: -1, role: "outer"},{type: "way", ref: 48, role: "outer"}],
* },
* meta:{
* theme:"test",
* changeType: "create"
* }
* }
* const mapping = new Map<string, string>([["way/-1", "way/42"],["node/-2", "node/43"],["node/-3", "node/44"]])
* const rewritten = ChangeDescriptionTools.rewriteIds(change, mapping)
* rewritten.id // => 789
* rewritten.changes["members"] // => [{type: "way", ref: 42, role: "outer"},{type: "way", ref: 48, role: "outer"}]
*
*/
public static rewriteIds(change: ChangeDescription, mappings: Map<string, string>): ChangeDescription {
const key = change.type + "/" + change.id
const wayHasChangedNode = ((change.changes ?? {})["nodes"] ?? []).some(id => mappings.has("node/" + id));
const relationHasChangedMembers = ((change.changes ?? {})["members"] ?? [])
.some((obj:{type: string, ref: number}) => mappings.has(obj.type+"/" + obj.ref));
const hasSomeChange = mappings.has(key)
|| wayHasChangedNode || relationHasChangedMembers
if(hasSomeChange){
change = {...change}
}
if (mappings.has(key)) {
const [_, newId] = mappings.get(key).split("/")
change.id = Number.parseInt(newId)
}
if(wayHasChangedNode){
change.changes = {...change.changes}
change.changes["nodes"] = change.changes["nodes"].map(id => {
const key = "node/"+id
if(!mappings.has(key)){
return id
}
const [_, newId] = mappings.get(key).split("/")
return Number.parseInt(newId)
})
}
if(relationHasChangedMembers){
change.changes = {...change.changes}
change.changes["members"] = change.changes["members"].map(
(obj:{type: string, ref: number}) => {
const key = obj.type+"/"+obj.ref;
if(!mappings.has(key)){
return obj
}
const [_, newId] = mappings.get(key).split("/")
return {...obj, ref: Number.parseInt(newId)}
}
)
}
return change
}
public static getGeojsonGeometry(change: ChangeDescription): any { public static getGeojsonGeometry(change: ChangeDescription): any {
switch (change.type) { switch (change.type) {
case "node": case "node":

View file

@ -51,22 +51,6 @@ export default class CreateNewNodeAction extends OsmCreateAction {
} }
} }
public static registerIdRewrites(mappings: Map<string, string>) {
const toAdd: [string, number][] = []
this.previouslyCreatedPoints.forEach((oldId, key) => {
if (!mappings.has("node/" + oldId)) {
return;
}
const newId = Number(mappings.get("node/" + oldId).substr("node/".length))
toAdd.push([key, newId])
})
for (const [key, newId] of toAdd) {
CreateNewNodeAction.previouslyCreatedPoints.set(key, newId)
}
}
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
if (this._reusePreviouslyCreatedPoint) { if (this._reusePreviouslyCreatedPoint) {

View file

@ -2,7 +2,7 @@ import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject";
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants";
import OsmChangeAction from "./Actions/OsmChangeAction"; import OsmChangeAction from "./Actions/OsmChangeAction";
import {ChangeDescription} from "./Actions/ChangeDescription"; import {ChangeDescription, ChangeDescriptionTools} from "./Actions/ChangeDescription";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import {LocalStorageSource} from "../Web/LocalStorageSource"; import {LocalStorageSource} from "../Web/LocalStorageSource";
import SimpleMetaTagger from "../SimpleMetaTagger"; import SimpleMetaTagger from "../SimpleMetaTagger";
@ -143,10 +143,6 @@ export class Changes {
this.allChanges.ping() this.allChanges.ping()
} }
public registerIdRewrites(mappings: Map<string, string>): void {
CreateNewNodeAction.registerIdRewrites(mappings)
}
private calculateDistanceToChanges(change: OsmChangeAction, changeDescriptions: ChangeDescription[]) { private calculateDistanceToChanges(change: OsmChangeAction, changeDescriptions: ChangeDescription[]) {
const locations = this.historicalUserLocations?.features?.data const locations = this.historicalUserLocations?.features?.data
@ -226,17 +222,6 @@ export class Changes {
} }
console.log("Got the fresh objects!", osmObjects, "pending: ", pending) console.log("Got the fresh objects!", osmObjects, "pending: ", pending)
const changes: {
newObjects: OsmObject[],
modifiedObjects: OsmObject[]
deletedObjects: OsmObject[]
} = self.CreateChangesetObjects(pending, osmObjects)
if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) {
console.log("No changes to be made")
return true
}
const perType = Array.from( const perType = Array.from(
Utils.Hist(pending.filter(descr => descr.meta.changeType !== undefined && descr.meta.changeType !== null) Utils.Hist(pending.filter(descr => descr.meta.changeType !== undefined && descr.meta.changeType !== null)
.map(descr => descr.meta.changeType)), ([key, count]) => ( .map(descr => descr.meta.changeType)), ([key, count]) => (
@ -303,7 +288,19 @@ export class Changes {
] ]
await this._changesetHandler.UploadChangeset( await this._changesetHandler.UploadChangeset(
(csId) => Changes.createChangesetFor("" + csId, changes), (csId, remappings) =>{
if(remappings.size > 0){
console.log("Rewriting pending changes from", pending, "with", remappings)
pending = pending.map(ch => ChangeDescriptionTools.rewriteIds(ch, remappings))
console.log("Result is", pending)
}
const changes: {
newObjects: OsmObject[],
modifiedObjects: OsmObject[]
deletedObjects: OsmObject[]
} = self.CreateChangesetObjects(pending, osmObjects)
return Changes.createChangesetFor("" + csId, changes)
},
metatags, metatags,
openChangeset openChangeset
) )

View file

@ -23,6 +23,14 @@ export class ChangesetHandler {
private readonly auth: any; private readonly auth: any;
private readonly backend: string; private readonly backend: string;
/**
* Contains previously rewritten IDs
* @private
*/
private readonly _remappings = new Map<string, string>()
/** /**
* Use 'osmConnection.CreateChangesetHandler' instead * Use 'osmConnection.CreateChangesetHandler' instead
* @param dryRun * @param dryRun
@ -50,6 +58,7 @@ export class ChangesetHandler {
} }
/** /**
* Creates a new list which contains every key at most once * Creates a new list which contains every key at most once
* *
@ -104,7 +113,7 @@ export class ChangesetHandler {
* *
*/ */
public async UploadChangeset( public async UploadChangeset(
generateChangeXML: (csid: number) => string, generateChangeXML: (csid: number, remappings: Map<string, string>) => string,
extraMetaTags: ChangesetTag[], extraMetaTags: ChangesetTag[],
openChangeset: UIEventSource<number>): Promise<void> { openChangeset: UIEventSource<number>): Promise<void> {
@ -120,7 +129,7 @@ export class ChangesetHandler {
this.userDetails.ping(); this.userDetails.ping();
} }
if (this._dryRun.data) { if (this._dryRun.data) {
const changesetXML = generateChangeXML(123456); const changesetXML = generateChangeXML(123456, this._remappings);
console.log("Metatags are", extraMetaTags) console.log("Metatags are", extraMetaTags)
console.log(changesetXML); console.log(changesetXML);
return; return;
@ -131,7 +140,7 @@ export class ChangesetHandler {
try { try {
const csId = await this.OpenChangeset(extraMetaTags) const csId = await this.OpenChangeset(extraMetaTags)
openChangeset.setData(csId); openChangeset.setData(csId);
const changeset = generateChangeXML(csId); const changeset = generateChangeXML(csId, this._remappings);
console.trace("Opened a new changeset (openChangeset.data is undefined):", changeset); console.trace("Opened a new changeset (openChangeset.data is undefined):", changeset);
const changes = await this.UploadChange(csId, changeset) const changes = await this.UploadChange(csId, changeset)
const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(extraMetaTags, changes) const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(extraMetaTags, changes)
@ -162,7 +171,7 @@ export class ChangesetHandler {
const rewritings = await this.UploadChange( const rewritings = await this.UploadChange(
csId, csId,
generateChangeXML(csId)) generateChangeXML(csId, this._remappings))
const rewrittenTags = this.RewriteTagsOf(extraMetaTags, rewritings, oldChangesetMeta) const rewrittenTags = this.RewriteTagsOf(extraMetaTags, rewritings, oldChangesetMeta)
await this.UpdateTags(csId, rewrittenTags) await this.UpdateTags(csId, rewrittenTags)
@ -239,57 +248,67 @@ export class ChangesetHandler {
} }
private handleIdRewrite(node: any, type: string): [string, string] { /**
* 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] {
const oldId = parseInt(node.attributes.old_id.value); const oldId = parseInt(node.attributes.old_id.value);
if (node.attributes.new_id === undefined) { if (node.attributes.new_id === undefined) {
// We just removed this point! return [type+"/"+oldId, undefined];
const element = this.allElements.getEventSourceById("node/" + oldId);
element.data._deleted = "yes"
element.ping();
return;
} }
const newId = parseInt(node.attributes.new_id.value); const newId = parseInt(node.attributes.new_id.value);
// The actual mapping
const result: [string, string] = [type + "/" + oldId, type + "/" + newId] const result: [string, string] = [type + "/" + oldId, type + "/" + newId]
if (!(oldId !== undefined && newId !== undefined && if(oldId === newId){
!isNaN(oldId) && !isNaN(newId))) {
return undefined; return undefined;
} }
if (oldId == newId) {
return undefined;
}
const element = this.allElements.getEventSourceById("node/" + oldId);
if (element === undefined) {
// Element to rewrite not found, probably a node or relation that is not rendered
return undefined
}
element.data.id = type + "/" + newId;
this.allElements.addElementById(type + "/" + newId, element);
this.allElements.ContainingFeatures.set(type + "/" + newId, this.allElements.ContainingFeatures.get(type + "/" + oldId))
element.ping();
return result; return result;
} }
/**
* 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
*/
private parseUploadChangesetResponse(response: XMLDocument): Map<string, string> { private parseUploadChangesetResponse(response: XMLDocument): Map<string, string> {
const nodes = response.getElementsByTagName("node"); const nodes = response.getElementsByTagName("node");
const mappings = new Map<string, string>() const mappings : [string, string][]= []
for (const node of Array.from(nodes)) { for (const node of Array.from(nodes)) {
const mapping = this.handleIdRewrite(node, "node") const mapping = ChangesetHandler.parseIdRewrite(node, "node")
if (mapping !== undefined) { if (mapping !== undefined) {
mappings.set(mapping[0], mapping[1]) mappings.push(mapping)
} }
} }
const ways = response.getElementsByTagName("way"); const ways = response.getElementsByTagName("way");
for (const way of Array.from(ways)) { for (const way of Array.from(ways)) {
const mapping = this.handleIdRewrite(way, "way") const mapping = ChangesetHandler.parseIdRewrite(way, "way")
if (mapping !== undefined) { if (mapping !== undefined) {
mappings.set(mapping[0], mapping[1]) mappings.push(mapping)
} }
} }
this.changes.registerIdRewrites(mappings) for (const mapping of mappings) {
return mappings const [oldId, newId] = mapping
this.allElements.addAlias(oldId, newId);
if(newId !== undefined) {
this._remappings.set(mapping[0], mapping[1])
}
}
return new Map<string, string>(mappings)
} }
@ -335,7 +354,6 @@ export class ChangesetHandler {
tags: ChangesetTag[]) { tags: ChangesetTag[]) {
tags = ChangesetHandler.removeDuplicateMetaTags(tags) tags = ChangesetHandler.removeDuplicateMetaTags(tags)
console.trace("Updating tags of " + csId)
const self = this; const self = this;
return new Promise<string>(function (resolve, reject) { return new Promise<string>(function (resolve, reject) {
@ -351,7 +369,7 @@ export class ChangesetHandler {
`</changeset></osm>`].join("") `</changeset></osm>`].join("")
}, function (err, response) { }, function (err, response) {
if (response === undefined) { if (response === undefined) {
console.log("err", err); console.error("Updating the tags of changeset "+csId+" failed:", err);
reject(err) reject(err)
} else { } else {
resolve(response); resolve(response);
@ -397,7 +415,7 @@ export class ChangesetHandler {
`</changeset></osm>`].join("") `</changeset></osm>`].join("")
}, function (err, response) { }, function (err, response) {
if (response === undefined) { if (response === undefined) {
console.log("err", err); console.error("Opening a changeset failed:", err);
reject(err) reject(err)
} else { } else {
resolve(Number(response)); resolve(Number(response));
@ -421,7 +439,7 @@ export class ChangesetHandler {
content: changesetXML content: changesetXML
}, function (err, response) { }, function (err, response) {
if (response == null) { if (response == null) {
console.log("err", err); console.error("Uploading an actual change failed", err);
reject(err); reject(err);
} }
const changes = self.parseUploadChangesetResponse(response); const changes = self.parseUploadChangesetResponse(response);

View file

@ -210,6 +210,9 @@ export class TagUtils {
* TagUtils.Tag("xyz!~\\[\\]") // => new RegexTag("xyz", /^\[\]$/, true) * TagUtils.Tag("xyz!~\\[\\]") // => new RegexTag("xyz", /^\[\]$/, true)
* TagUtils.Tag("tags~(^|.*;)amenity=public_bookcase($|;.*)") // => new RegexTag("tags", /(^|.*;)amenity=public_bookcase($|;.*)/) * TagUtils.Tag("tags~(^|.*;)amenity=public_bookcase($|;.*)") // => new RegexTag("tags", /(^|.*;)amenity=public_bookcase($|;.*)/)
* TagUtils.Tag("service:bicycle:.*~~*") // => new RegexTag(/^service:bicycle:.*$/, /^..*$/) * TagUtils.Tag("service:bicycle:.*~~*") // => new RegexTag(/^service:bicycle:.*$/, /^..*$/)
*
* TagUtils.Tag("xyz<5").matchesProperties({xyz: 4}) // => true
* TagUtils.Tag("xyz<5").matchesProperties({xyz: 5}) // => false
*/ */
public static Tag(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter { public static Tag(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter {
try { try {
@ -272,12 +275,19 @@ export class TagUtils {
val = new Date(split[1].trim()).getTime() val = new Date(split[1].trim()).getTime()
} }
const f = (value: string | undefined) => { const f = (value: string | number | undefined) => {
if (value === undefined) { if (value === undefined) {
return false; return false;
} }
let b = Number(value?.trim()) let b: number
if (isNaN(b)) { if (typeof value === "number") {
b = value
} else if (typeof b === "string") {
b = Number(value?.trim())
} else {
b = Number(value)
}
if (isNaN(b) && typeof value === "string") {
b = Utils.ParseDate(value).getTime() b = Utils.ParseDate(value).getTime()
if (isNaN(b)) { if (isNaN(b)) {
return false return false

View file

@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants { export default class Constants {
public static vNumber = "0.18.0-alpha-2"; public static vNumber = "0.18.0-alpha-3";
public static ImgurApiKey = '7070e7167f0a25a' public static ImgurApiKey = '7070e7167f0a25a'
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"