forked from MapComplete/MapComplete
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:
parent
1f9aacfb29
commit
4172af6a72
118 changed files with 1422 additions and 1357 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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[]>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
||||
/**
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue