2021-07-15 20:47:28 +02:00
|
|
|
import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject";
|
2020-10-02 19:00:24 +02:00
|
|
|
import State from "../../State";
|
2020-10-27 01:01:34 +01:00
|
|
|
import {UIEventSource} from "../UIEventSource";
|
2021-01-02 19:09:49 +01:00
|
|
|
import Constants from "../../Models/Constants";
|
2021-07-15 20:47:28 +02:00
|
|
|
import OsmChangeAction from "./Actions/OsmChangeAction";
|
|
|
|
import {ChangeDescription} from "./Actions/ChangeDescription";
|
|
|
|
import {Utils} from "../../Utils";
|
2021-07-22 14:25:23 +02:00
|
|
|
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
2020-06-24 00:35:19 +02:00
|
|
|
|
2021-01-03 03:09:52 +01:00
|
|
|
/**
|
|
|
|
* Handles all changes made to OSM.
|
|
|
|
* Needs an authenticator via OsmConnection
|
|
|
|
*/
|
2021-07-15 20:47:28 +02:00
|
|
|
export class Changes {
|
2020-08-27 18:44:16 +02:00
|
|
|
|
2021-07-10 13:47:53 +02:00
|
|
|
|
2021-09-26 23:35:26 +02:00
|
|
|
private _nextId: number = -1; // Newly assigned ID's are negative
|
2021-04-23 12:56:56 +02:00
|
|
|
public readonly name = "Newly added features"
|
2021-02-20 22:18:42 +01:00
|
|
|
/**
|
2021-07-15 20:47:28 +02:00
|
|
|
* All the newly created features as featureSource + all the modified features
|
2021-02-20 22:18:42 +01:00
|
|
|
*/
|
2021-07-10 13:47:53 +02:00
|
|
|
public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
|
2020-08-27 18:44:16 +02:00
|
|
|
|
2021-09-22 05:02:09 +02:00
|
|
|
public readonly pendingChanges: UIEventSource<ChangeDescription[]> = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
|
|
|
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
|
2021-07-13 00:40:27 +02:00
|
|
|
private readonly isUploading = new UIEventSource(false);
|
2021-09-09 00:05:51 +02:00
|
|
|
|
|
|
|
private readonly previouslyCreated: OsmObject[] = []
|
2020-08-27 18:44:16 +02:00
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
constructor() {
|
2021-09-22 05:02:09 +02:00
|
|
|
// We keep track of all changes just as well
|
|
|
|
this.allChanges.setData([...this.pendingChanges.data])
|
2021-09-22 16:07:56 +02:00
|
|
|
// If a pending change contains a negative ID, we save that
|
2021-09-26 23:35:26 +02:00
|
|
|
this._nextId = Math.min(-1, ...this.pendingChanges.data?.map(pch => pch.id) ?? [])
|
|
|
|
|
|
|
|
// Note: a changeset might be reused which was opened just before and might have already used some ids
|
|
|
|
// This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
|
2021-07-15 20:47:28 +02:00
|
|
|
}
|
|
|
|
|
2021-07-18 14:52:09 +02:00
|
|
|
private static createChangesetFor(csId: string,
|
2021-09-09 00:05:51 +02:00
|
|
|
allChanges: {
|
|
|
|
modifiedObjects: OsmObject[],
|
|
|
|
newObjects: OsmObject[],
|
|
|
|
deletedObjects: OsmObject[]
|
|
|
|
}): string {
|
2021-07-15 20:47:28 +02:00
|
|
|
|
|
|
|
const changedElements = allChanges.modifiedObjects ?? []
|
2021-07-18 14:52:09 +02:00
|
|
|
const newElements = allChanges.newObjects ?? []
|
|
|
|
const deletedElements = allChanges.deletedObjects ?? []
|
2021-07-15 20:47:28 +02:00
|
|
|
|
|
|
|
let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`;
|
|
|
|
if (newElements.length > 0) {
|
|
|
|
changes +=
|
|
|
|
"\n<create>\n" +
|
|
|
|
newElements.map(e => e.ChangesetXML(csId)).join("\n") +
|
|
|
|
"</create>";
|
2020-06-24 00:35:19 +02:00
|
|
|
}
|
2021-07-15 20:47:28 +02:00
|
|
|
if (changedElements.length > 0) {
|
|
|
|
changes +=
|
|
|
|
"\n<modify>\n" +
|
|
|
|
changedElements.map(e => e.ChangesetXML(csId)).join("\n") +
|
|
|
|
"\n</modify>";
|
2020-06-24 00:35:19 +02:00
|
|
|
}
|
2020-08-27 18:44:16 +02:00
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
if (deletedElements.length > 0) {
|
|
|
|
changes +=
|
2021-10-04 03:12:42 +02:00
|
|
|
"\n<delete>\n" +
|
2021-07-15 20:47:28 +02:00
|
|
|
deletedElements.map(e => e.ChangesetXML(csId)).join("\n") +
|
2021-10-04 03:12:42 +02:00
|
|
|
"\n</delete>"
|
2020-08-06 19:42:10 +02:00
|
|
|
}
|
2020-08-27 11:11:20 +02:00
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
changes += "</osmChange>";
|
|
|
|
return changes;
|
2020-06-24 00:35:19 +02:00
|
|
|
}
|
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
private static GetNeededIds(changes: ChangeDescription[]) {
|
|
|
|
return Utils.Dedup(changes.filter(c => c.id >= 0)
|
|
|
|
.map(c => c.type + "/" + c.id))
|
|
|
|
}
|
2021-07-10 13:47:53 +02:00
|
|
|
|
2021-09-09 00:05:51 +02:00
|
|
|
/**
|
|
|
|
* Returns a new ID and updates the value for the next ID
|
|
|
|
*/
|
|
|
|
public getNewID() {
|
2021-09-22 16:07:56 +02:00
|
|
|
return this._nextId--;
|
2021-09-09 00:05:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Uploads all the pending changes in one go.
|
|
|
|
* Triggered by the 'PendingChangeUploader'-actor in Actors
|
|
|
|
*/
|
|
|
|
public flushChanges(flushreason: string = undefined) {
|
|
|
|
if (this.pendingChanges.data.length === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.isUploading.data) {
|
|
|
|
console.log("Is already uploading... Abort")
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.isUploading.setData(true)
|
|
|
|
|
2021-10-04 03:12:42 +02:00
|
|
|
this.flushChangesAsync()
|
2021-09-26 23:35:26 +02:00
|
|
|
.then(_ => {
|
|
|
|
this.isUploading.setData(false)
|
|
|
|
console.log("Changes flushed!");
|
|
|
|
})
|
|
|
|
.catch(e => {
|
|
|
|
this.isUploading.setData(false)
|
|
|
|
console.error("Flushing changes failed due to", e);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-04 03:12:42 +02:00
|
|
|
/**
|
|
|
|
* UPload the selected changes to OSM.
|
|
|
|
* Returns 'true' if successfull and if they can be removed
|
|
|
|
* @param pending
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
private async flushSelectChanges(pending: ChangeDescription[]): Promise<boolean>{
|
|
|
|
const self = this;
|
|
|
|
const neededIds = Changes.GetNeededIds(pending)
|
|
|
|
const osmObjects = await Promise.all(neededIds.map(id => OsmObject.DownloadObjectAsync(id)));
|
|
|
|
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 meta = pending[0].meta
|
|
|
|
|
|
|
|
const perType = Array.from(Utils.Hist(pending.map(descr => descr.meta.changeType)), ([key, count]) => ({
|
|
|
|
key: key,
|
|
|
|
value: count,
|
|
|
|
aggregate: true
|
|
|
|
}))
|
|
|
|
const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined)
|
|
|
|
.map(descr => ({
|
|
|
|
key: descr.meta.changeType+":"+descr.type+"/"+descr.id,
|
|
|
|
value: descr.meta.specialMotivation
|
|
|
|
}))
|
|
|
|
const metatags = [{
|
|
|
|
key: "comment",
|
|
|
|
value: "Adding data with #MapComplete for theme #"+meta.theme
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key:"theme",
|
|
|
|
value:meta.theme
|
|
|
|
},
|
|
|
|
...perType,
|
|
|
|
...motivations
|
|
|
|
]
|
|
|
|
|
|
|
|
await State.state.osmConnection.changesetHandler.UploadChangeset(
|
|
|
|
(csId) => Changes.createChangesetFor(""+csId, changes),
|
|
|
|
metatags
|
|
|
|
)
|
|
|
|
|
|
|
|
console.log("Upload successfull!")
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private async flushChangesAsync(): Promise<void> {
|
2021-09-09 00:05:51 +02:00
|
|
|
const self = this;
|
2021-09-26 23:35:26 +02:00
|
|
|
try {
|
2021-10-03 21:53:31 +02:00
|
|
|
// At last, we build the changeset and upload
|
|
|
|
const pending = self.pendingChanges.data;
|
2021-10-04 03:12:42 +02:00
|
|
|
|
|
|
|
const pendingPerTheme = new Map<string, ChangeDescription[]>()
|
|
|
|
for (const changeDescription of pending) {
|
|
|
|
const theme = changeDescription.meta.theme
|
|
|
|
if(!pendingPerTheme.has(theme)){
|
|
|
|
pendingPerTheme.set(theme, [])
|
|
|
|
}
|
|
|
|
pendingPerTheme.get(theme).push(changeDescription)
|
|
|
|
}
|
|
|
|
|
|
|
|
const successes = await Promise.all(Array.from(pendingPerTheme, ([key , value]) => value)
|
|
|
|
.map(async pendingChanges => {
|
|
|
|
try{
|
|
|
|
return await self.flushSelectChanges(pendingChanges);
|
|
|
|
}catch(e){
|
|
|
|
console.error("Could not upload some changes:",e)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
|
|
|
|
if(!successes.some(s => s == false)){
|
|
|
|
// All changes successfull, we clear the data!
|
|
|
|
this.pendingChanges.setData([]);
|
2021-09-09 00:05:51 +02:00
|
|
|
}
|
2021-09-26 23:35:26 +02:00
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e)
|
|
|
|
self.pendingChanges.setData([])
|
2021-10-04 03:12:42 +02:00
|
|
|
}finally {
|
2021-09-26 23:35:26 +02:00
|
|
|
self.isUploading.setData(false)
|
|
|
|
}
|
2021-09-09 00:05:51 +02:00
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-09-29 01:12:38 +02:00
|
|
|
public async applyAction(action: OsmChangeAction): Promise<void> {
|
|
|
|
const changes = await action.Perform(this)
|
|
|
|
console.log("Received changes:", changes)
|
|
|
|
this.pendingChanges.data.push(...changes);
|
|
|
|
this.pendingChanges.ping();
|
|
|
|
this.allChanges.data.push(...changes)
|
|
|
|
this.allChanges.ping()
|
2021-09-09 00:05:51 +02:00
|
|
|
}
|
|
|
|
|
2021-07-18 14:52:09 +02:00
|
|
|
private CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): {
|
2021-07-15 20:47:28 +02:00
|
|
|
newObjects: OsmObject[],
|
|
|
|
modifiedObjects: OsmObject[]
|
|
|
|
deletedObjects: OsmObject[]
|
|
|
|
|
|
|
|
} {
|
|
|
|
const objects: Map<string, OsmObject> = new Map<string, OsmObject>()
|
|
|
|
const states: Map<string, "unchanged" | "created" | "modified" | "deleted"> = new Map();
|
|
|
|
|
|
|
|
for (const o of downloadedOsmObjects) {
|
|
|
|
objects.set(o.type + "/" + o.id, o)
|
|
|
|
states.set(o.type + "/" + o.id, "unchanged")
|
2021-01-03 03:09:52 +01:00
|
|
|
}
|
2021-07-10 13:47:53 +02:00
|
|
|
|
2021-07-18 14:52:09 +02:00
|
|
|
for (const o of this.previouslyCreated) {
|
2021-09-09 00:05:51 +02:00
|
|
|
objects.set(o.type + "/" + o.id, o)
|
2021-07-18 14:52:09 +02:00
|
|
|
states.set(o.type + "/" + o.id, "unchanged")
|
|
|
|
}
|
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
let changed = false;
|
2021-01-03 03:09:52 +01:00
|
|
|
for (const change of changes) {
|
2021-07-15 20:47:28 +02:00
|
|
|
const id = change.type + "/" + change.id
|
|
|
|
if (!objects.has(id)) {
|
2021-09-09 00:05:51 +02:00
|
|
|
if (change.id >= 0) {
|
|
|
|
throw "Did not get an object that should be known: " + id
|
2021-07-18 14:52:09 +02:00
|
|
|
}
|
2021-07-15 20:47:28 +02:00
|
|
|
// This is a new object that should be created
|
|
|
|
states.set(id, "created")
|
2021-07-18 14:52:09 +02:00
|
|
|
console.log("Creating object for changeDescription", change)
|
2021-07-15 20:47:28 +02:00
|
|
|
let osmObj: OsmObject = undefined;
|
|
|
|
switch (change.type) {
|
|
|
|
case "node":
|
|
|
|
const n = new OsmNode(change.id)
|
|
|
|
n.lat = change.changes["lat"]
|
|
|
|
n.lon = change.changes["lon"]
|
|
|
|
osmObj = n
|
|
|
|
break;
|
|
|
|
case "way":
|
|
|
|
const w = new OsmWay(change.id)
|
|
|
|
w.nodes = change.changes["nodes"]
|
|
|
|
osmObj = w
|
|
|
|
break;
|
|
|
|
case "relation":
|
|
|
|
const r = new OsmRelation(change.id)
|
|
|
|
r.members = change.changes["members"]
|
|
|
|
osmObj = r
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (osmObj === undefined) {
|
|
|
|
throw "Hmm? This is a bug"
|
|
|
|
}
|
|
|
|
objects.set(id, osmObj)
|
2021-07-18 14:52:09 +02:00
|
|
|
this.previouslyCreated.push(osmObj)
|
2021-01-03 03:09:52 +01:00
|
|
|
}
|
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
const state = states.get(id)
|
|
|
|
if (change.doDelete) {
|
|
|
|
if (state === "created") {
|
|
|
|
states.set(id, "unchanged")
|
|
|
|
} else {
|
|
|
|
states.set(id, "deleted")
|
|
|
|
}
|
|
|
|
}
|
2021-07-14 15:28:02 +02:00
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
const obj = objects.get(id)
|
|
|
|
// Apply tag changes
|
|
|
|
for (const kv of change.tags ?? []) {
|
|
|
|
const k = kv.k
|
|
|
|
let v = kv.v
|
|
|
|
|
|
|
|
if (v === "") {
|
|
|
|
v = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const oldV = obj.type[k]
|
|
|
|
if (oldV === v) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
obj.tags[k] = v;
|
|
|
|
changed = true;
|
2021-07-14 15:28:02 +02:00
|
|
|
|
2020-06-24 00:35:19 +02:00
|
|
|
|
2021-07-14 15:28:02 +02:00
|
|
|
}
|
2021-07-10 13:47:53 +02:00
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
if (change.changes !== undefined) {
|
|
|
|
switch (change.type) {
|
|
|
|
case "node":
|
|
|
|
// @ts-ignore
|
|
|
|
const nlat = change.changes.lat;
|
|
|
|
// @ts-ignore
|
|
|
|
const nlon = change.changes.lon;
|
|
|
|
const n = <OsmNode>obj
|
|
|
|
if (n.lat !== nlat || n.lon !== nlon) {
|
|
|
|
n.lat = nlat;
|
|
|
|
n.lon = nlon;
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case "way":
|
|
|
|
const nnodes = change.changes["nodes"]
|
|
|
|
const w = <OsmWay>obj
|
|
|
|
if (!Utils.Identical(nnodes, w.nodes)) {
|
|
|
|
w.nodes = nnodes
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case "relation":
|
|
|
|
const nmembers: { type: "node" | "way" | "relation", ref: number, role: string }[] = change.changes["members"]
|
|
|
|
const r = <OsmRelation>obj
|
|
|
|
if (!Utils.Identical(nmembers, r.members, (a, b) => {
|
|
|
|
return a.role === b.role && a.type === b.type && a.ref === b.ref
|
|
|
|
})) {
|
|
|
|
r.members = nmembers;
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
break;
|
2021-07-10 13:47:53 +02:00
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
}
|
2021-09-09 00:05:51 +02:00
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
}
|
2021-01-04 22:14:56 +01:00
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
if (changed && state === "unchanged") {
|
|
|
|
states.set(id, "modified")
|
|
|
|
}
|
2021-07-10 13:47:53 +02:00
|
|
|
}
|
|
|
|
|
2021-07-10 15:52:52 +02:00
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
const result = {
|
|
|
|
newObjects: [],
|
|
|
|
modifiedObjects: [],
|
|
|
|
deletedObjects: []
|
|
|
|
}
|
2021-09-09 00:05:51 +02:00
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
objects.forEach((v, id) => {
|
2021-07-15 00:39:11 +02:00
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
const state = states.get(id)
|
|
|
|
if (state === "created") {
|
|
|
|
result.newObjects.push(v)
|
|
|
|
}
|
|
|
|
if (state === "modified") {
|
|
|
|
result.modifiedObjects.push(v)
|
|
|
|
}
|
|
|
|
if (state === "deleted") {
|
|
|
|
result.deletedObjects.push(v)
|
|
|
|
}
|
2021-07-10 13:47:53 +02:00
|
|
|
|
2021-07-10 15:52:52 +02:00
|
|
|
})
|
|
|
|
|
2021-07-15 20:47:28 +02:00
|
|
|
return result
|
|
|
|
}
|
2021-09-26 23:35:26 +02:00
|
|
|
|
|
|
|
public registerIdRewrites(mappings: Map<string, string>): void {
|
2021-10-03 21:53:31 +02:00
|
|
|
|
2021-09-26 23:35:26 +02:00
|
|
|
}
|
2020-06-24 00:35:19 +02:00
|
|
|
}
|