Refactoring: fix delete indication, fix splitroad, fix addition of multiple new points snapped onto the same way (all will properly attach now)

This commit is contained in:
Pieter Vander Vennet 2023-04-20 17:42:07 +02:00
parent 1f9aacfb29
commit 4172af6a72
118 changed files with 1422 additions and 1357 deletions

View file

@ -74,12 +74,10 @@ export default class SelectedElementTagsUpdater {
try {
const osmObject = await state.osmObjectDownloader.DownloadObjectAsync(id)
if (osmObject === "deleted") {
console.warn("The current selected element has been deleted upstream!")
console.debug("The current selected element has been deleted upstream!", id)
const currentTagsSource = state.featureProperties.getStore(id)
if (currentTagsSource.data["_deleted"] === "yes") {
return
}
currentTagsSource.data["_deleted"] = "yes"
currentTagsSource.addCallbackAndRun((tags) => console.trace("Tags are", tags))
currentTagsSource.ping()
return
}

View file

@ -12,6 +12,7 @@ export default class FeaturePropertiesStore {
this._source = source
const self = this
source.features.addCallbackAndRunD((features) => {
console.log("Re-indexing features")
for (const feature of features) {
const id = feature.properties.id
if (id === undefined) {
@ -21,6 +22,7 @@ export default class FeaturePropertiesStore {
const source = self._elements.get(id)
if (source === undefined) {
console.log("Adding feature store for", id)
self._elements.set(id, new UIEventSource<any>(feature.properties))
continue
}

View file

@ -1,7 +1,6 @@
import { Store, UIEventSource } from "../../UIEventSource"
import FilteredLayer from "../../../Models/FilteredLayer"
import { FeatureSource } from "../FeatureSource"
import { TagsFilter } from "../../Tags/TagsFilter"
import { Feature } from "geojson"
import { GlobalFilter } from "../../../Models/GlobalFilter"
@ -54,6 +53,7 @@ export default class FilteringFeatureSource implements FeatureSource {
this.update()
}
private update() {
const self = this
const layer = this._layer
@ -87,7 +87,7 @@ export default class FilteringFeatureSource implements FeatureSource {
}
}
// Something new has been found!
// Something new has been found (or something was deleted)!
this.features.setData(newFeatures)
}

View file

@ -43,7 +43,15 @@ export default class LayoutSource extends FeatureSourceMerger {
isActive: isDisplayed(l.id),
})
)
const overpassSource = LayoutSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches)
const overpassSource = LayoutSource.setupOverpass(
backend,
osmLayers,
bounds,
zoom,
featureSwitches
)
const osmApiSource = LayoutSource.setupOsmApiSource(
osmLayers,
bounds,
@ -121,6 +129,7 @@ export default class LayoutSource extends FeatureSourceMerger {
}
private static setupOverpass(
backend: string,
osmLayers: LayerConfig[],
bounds: Store<BBox>,
zoom: Store<number>,

View file

@ -23,7 +23,7 @@ export class NewGeometryFromChangesFeatureSource implements WritableFeatureSourc
const features = this.features.data
const self = this
const backend = changes.backend
changes.pendingChanges.stabilized(100).addCallbackAndRunD((changes) => {
changes.pendingChanges.addCallbackAndRunD((changes) => {
if (changes.length === 0) {
return
}
@ -48,6 +48,7 @@ export class NewGeometryFromChangesFeatureSource implements WritableFeatureSourc
continue
}
console.log("Handling pending change")
if (change.id > 0) {
// This is an already existing object
// In _most_ of the cases, this means that this _isn't_ a new object
@ -74,11 +75,9 @@ export class NewGeometryFromChangesFeatureSource implements WritableFeatureSourc
self.features.ping()
})
continue
} else if (change.id < 0 && change.changes === undefined) {
// The geometry is not described - not a new point
if (change.id < 0) {
console.error("WARNING: got a new point without geometry!")
}
} else if (change.changes === undefined) {
// The geometry is not described - not a new point or geometry change, but probably a tagchange to a newly created point
// Not something that should be handled here
continue
}

View file

@ -20,12 +20,12 @@ export default abstract class OsmChangeAction {
this.mainObjectId = mainObjectId
}
public Perform(changes: Changes) {
public async Perform(changes: Changes) {
if (this.isUsed) {
throw "This ChangeAction is already used"
}
this.isUsed = true
return this.CreateChangeDescriptions(changes)
return await this.CreateChangeDescriptions(changes)
}
protected abstract CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]>

View file

@ -34,50 +34,40 @@ export default class OsmObjectDownloader {
}
async DownloadObjectAsync(id: NodeId, maxCacheAgeInSecs?: number): Promise<OsmNode | "deleted">
async DownloadObjectAsync(id: WayId, maxCacheAgeInSecs?: number): Promise<OsmWay | "deleted">
async DownloadObjectAsync(
id: RelationId,
maxCacheAgeInSecs?: number
): Promise<OsmRelation | undefined>
async DownloadObjectAsync(id: OsmId, maxCacheAgeInSecs?: number): Promise<OsmObject | "deleted">
async DownloadObjectAsync(
id: string,
maxCacheAgeInSecs?: number
): Promise<OsmObject | "deleted">
async DownloadObjectAsync(
id: string,
maxCacheAgeInSecs?: number
): Promise<OsmObject | "deleted"> {
async DownloadObjectAsync(id: string, maxCacheAgeInSecs?: number) {
// Wait until uploading is done
if (this._changes) {
await this._changes.isUploading.AsPromise((o) => o === false)
}
const splitted = id.split("/")
const type = splitted[0]
const idN = Number(splitted[1])
let obj: OsmObject | "deleted"
if (idN < 0) {
throw "Invalid request: cannot download OsmObject " + id + ", it has a negative id"
obj = this.constructObject(<"node" | "way" | "relation">type, idN)
} else {
obj = await this.RawDownloadObjectAsync(type, idN, maxCacheAgeInSecs)
}
const full = !id.startsWith("node") ? "/full" : ""
const url = `${this.backend}api/0.6/${id}${full}`
const rawData = await Utils.downloadJsonCachedAdvanced(
url,
(maxCacheAgeInSecs ?? 10) * 1000
)
if (rawData["error"] !== undefined && rawData["statuscode"] === 410) {
return "deleted"
if (obj === "deleted") {
return obj
}
// A full query might contain more then just the requested object (e.g. nodes that are part of a way, where we only want the way)
const parsed = OsmObject.ParseObjects(rawData["content"].elements)
// Lets fetch the object we need
for (const osmObject of parsed) {
if (osmObject.type !== type) {
continue
}
if (osmObject.id !== idN) {
continue
}
// Found the one!
return osmObject
}
throw "PANIC: requested object is not part of the response"
return await this.applyPendingChanges(obj)
}
public DownloadHistory(id: NodeId): UIEventSource<OsmNode[]>
@ -149,4 +139,105 @@ export default class OsmObjectDownloader {
return rel
})
}
private applyNodeChange(object: OsmNode, change: { lat: number; lon: number }) {
object.lat = change.lat
object.lon = change.lon
}
private applyWayChange(object: OsmWay, change: { nodes: number[]; coordinates }) {
object.nodes = change.nodes
object.coordinates = change.coordinates.map(([lat, lon]) => [lon, lat])
}
private applyRelationChange(
object: OsmRelation,
change: { members: { type: "node" | "way" | "relation"; ref: number; role: string }[] }
) {
object.members = change.members
}
private async applyPendingChanges(object: OsmObject): Promise<OsmObject | "deleted"> {
if (!this._changes) {
return object
}
const pendingChanges = this._changes.pendingChanges.data
for (const pendingChange of pendingChanges) {
if (object.id !== pendingChange.id || object.type !== pendingChange.type) {
continue
}
if (pendingChange.doDelete) {
return "deleted"
}
if (pendingChange.tags) {
for (const { k, v } of pendingChange.tags) {
if (v === undefined) {
delete object.tags[k]
} else {
object.tags[k] = v
}
}
}
if (pendingChange.changes) {
switch (pendingChange.type) {
case "node":
this.applyNodeChange(<OsmNode>object, <any>pendingChange.changes)
break
case "way":
this.applyWayChange(<OsmWay>object, <any>pendingChange.changes)
break
case "relation":
this.applyRelationChange(<OsmRelation>object, <any>pendingChange.changes)
break
}
}
}
return object
}
/**
* Creates an empty object of the specified type with the specified id.
* We assume that the pending changes will be applied on them, filling in details such as coordinates, tags, ...
*/
private constructObject(type: "node" | "way" | "relation", id: number): OsmObject {
switch (type) {
case "node":
return new OsmNode(id)
case "way":
return new OsmWay(id)
case "relation":
return new OsmRelation(id)
}
}
private async RawDownloadObjectAsync(
type: string,
idN: number,
maxCacheAgeInSecs?: number
): Promise<OsmObject | "deleted"> {
const full = type !== "node" ? "/full" : ""
const url = `${this.backend}api/0.6/${type}/${idN}${full}`
const rawData = await Utils.downloadJsonCachedAdvanced(
url,
(maxCacheAgeInSecs ?? 10) * 1000
)
if (rawData["error"] !== undefined && rawData["statuscode"] === 410) {
return "deleted"
}
// A full query might contain more then just the requested object (e.g. nodes that are part of a way, where we only want the way)
const parsed = OsmObject.ParseObjects(rawData["content"].elements)
// Lets fetch the object we need
for (const osmObject of parsed) {
if (osmObject.type !== type) {
continue
}
if (osmObject.id !== idN) {
continue
}
// Found the one!
return osmObject
}
throw "PANIC: requested object is not part of the response"
}
}

View file

@ -556,6 +556,27 @@ export default class SimpleMetaTaggers {
return true
}
)
private static timeSinceLastEdit = new InlineMetaTagger(
{
keys: ["_last_edit:passed_time"],
doc: "Gives the number of seconds since the last edit. Note that this will _not_ update, but rather be the number of seconds elapsed at the moment this tag is read first",
isLazy: true,
includesDates: true,
},
(feature, layer, tagsStore) => {
Utils.AddLazyProperty(feature.properties, "_last_edit:passed_time", () => {
const lastEditTimestamp = new Date(
feature.properties["_last_edit:timestamp"]
).getTime()
const now: number = Date.now()
const millisElapsed = now - lastEditTimestamp
return "" + millisElapsed / 1000
})
return true
}
)
public static metatags: SimpleMetaTagger[] = [
SimpleMetaTaggers.latlon,
SimpleMetaTaggers.layerInfo,
@ -572,6 +593,7 @@ export default class SimpleMetaTaggers {
SimpleMetaTaggers.geometryType,
SimpleMetaTaggers.levels,
SimpleMetaTaggers.referencingWays,
SimpleMetaTaggers.timeSinceLastEdit,
]
/**

View file

@ -30,7 +30,6 @@ export default class FeatureSwitchState {
public readonly featureSwitchFilter: UIEventSource<boolean>
public readonly featureSwitchEnableExport: UIEventSource<boolean>
public readonly featureSwitchFakeUser: UIEventSource<boolean>
public readonly featureSwitchExportAsPdf: UIEventSource<boolean>
public readonly overpassUrl: UIEventSource<string[]>
public readonly overpassTimeout: UIEventSource<number>
public readonly overpassMaxZoom: UIEventSource<number>
@ -125,14 +124,9 @@ export default class FeatureSwitchState {
this.featureSwitchEnableExport = featSw(
"fs-export",
(layoutToUse) => layoutToUse?.enableExportButton ?? false,
(layoutToUse) => layoutToUse?.enableExportButton ?? true,
"Enable the export as GeoJSON and CSV button"
)
this.featureSwitchExportAsPdf = featSw(
"fs-pdf",
(layoutToUse) => layoutToUse?.enablePdfDownload ?? false,
"Enable the PDF download button"
)
this.featureSwitchApiURL = QueryParameters.GetQueryParameter(
"backend",

View file

@ -250,6 +250,7 @@ export default class UserRelatedState {
const amendedPrefs = new UIEventSource<Record<string, string>>({
_theme: layout?.id,
_backend: this.osmConnection.Backend(),
_applicationOpened: new Date().toISOString(),
})
const osmConnection = this.osmConnection
@ -299,7 +300,6 @@ export default class UserRelatedState {
"" + (total - untranslated_count)
amendedPrefs.data["_translation_percentage"] =
"" + Math.floor((100 * (total - untranslated_count)) / total)
console.log("Setting zenLinks", zenLinks)
amendedPrefs.data["_translation_links"] = JSON.stringify(zenLinks)
}
amendedPrefs.ping()
@ -355,7 +355,8 @@ export default class UserRelatedState {
amendedPrefs.addCallbackD((tags) => {
for (const key in tags) {
if (key.startsWith("_")) {
if (key.startsWith("_") || key === "mapcomplete-language") {
// Language is managed seperately
continue
}
this.osmConnection.GetPreference(key, undefined, { prefix: "" }).setData(tags[key])

View file

@ -244,8 +244,9 @@ export abstract class Store<T> implements Readable<T> {
const self = this
condition = condition ?? ((t) => t !== undefined)
return new Promise((resolve) => {
if (condition(self.data)) {
resolve(self.data)
const data = self.data
if (condition(data)) {
resolve(data)
} else {
self.addCallbackD((data) => {
resolve(data)