forked from MapComplete/MapComplete
Fix: upload flow deals better with point reuse: it actually opens the feature now
This commit is contained in:
parent
246b16317d
commit
c14cbc9fe9
7 changed files with 282 additions and 237 deletions
|
@ -1,5 +1,6 @@
|
|||
import { FeatureSource } from "../FeatureSource"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import { OsmTags } from "../../../Models/OsmFeature"
|
||||
|
||||
/**
|
||||
* Constructs a UIEventStore for the properties of every Feature, indexed by id
|
||||
|
@ -13,40 +14,6 @@ export default class FeaturePropertiesStore {
|
|||
}
|
||||
}
|
||||
|
||||
public getStore(id: string): UIEventSource<Record<string, string>> {
|
||||
return this._elements.get(id)
|
||||
}
|
||||
|
||||
public trackFeatureSource(source: FeatureSource) {
|
||||
const self = this
|
||||
source.features.addCallbackAndRunD((features) => {
|
||||
for (const feature of features) {
|
||||
const id = feature.properties.id
|
||||
if (id === undefined) {
|
||||
console.trace("Error: feature without ID:", feature)
|
||||
throw "Error: feature without ID"
|
||||
}
|
||||
|
||||
const source = self._elements.get(id)
|
||||
if (source === undefined) {
|
||||
self._elements.set(id, new UIEventSource<any>(feature.properties))
|
||||
continue
|
||||
}
|
||||
|
||||
if (source.data === feature.properties) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update the tags in the old store and link them
|
||||
const changeMade = FeaturePropertiesStore.mergeTags(source.data, feature.properties)
|
||||
feature.properties = source.data
|
||||
if (changeMade) {
|
||||
source.ping()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the tags of the old properties object, returns true if a change was made.
|
||||
* Metatags are overriden if they are in the new properties, but not removed
|
||||
|
@ -67,7 +34,7 @@ export default class FeaturePropertiesStore {
|
|||
}
|
||||
if (newProperties[oldPropertiesKey] === undefined) {
|
||||
changeMade = true
|
||||
delete oldProperties[oldPropertiesKey]
|
||||
// delete oldProperties[oldPropertiesKey]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,6 +50,48 @@ export default class FeaturePropertiesStore {
|
|||
return changeMade
|
||||
}
|
||||
|
||||
public getStore(id: string): UIEventSource<Record<string, string>> {
|
||||
const store = this._elements.get(id)
|
||||
if (store === undefined) {
|
||||
console.error("PANIC: no store for", id)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
public trackFeature(feature: { properties: OsmTags }) {
|
||||
const id = feature.properties.id
|
||||
if (id === undefined) {
|
||||
console.trace("Error: feature without ID:", feature)
|
||||
throw "Error: feature without ID"
|
||||
}
|
||||
|
||||
const source = this._elements.get(id)
|
||||
if (source === undefined) {
|
||||
this._elements.set(id, new UIEventSource<any>(feature.properties))
|
||||
return
|
||||
}
|
||||
|
||||
if (source.data === feature.properties) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the tags in the old store and link them
|
||||
const changeMade = FeaturePropertiesStore.mergeTags(source.data, feature.properties)
|
||||
feature.properties = <any>source.data
|
||||
if (changeMade) {
|
||||
source.ping()
|
||||
}
|
||||
}
|
||||
|
||||
public trackFeatureSource(source: FeatureSource) {
|
||||
const self = this
|
||||
source.features.addCallbackAndRunD((features) => {
|
||||
for (const feature of features) {
|
||||
self.trackFeature(<any>feature)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public addAlias(oldId: string, newId: string): void {
|
||||
if (newId === undefined) {
|
||||
|
|
|
@ -4,8 +4,9 @@ import { IndexedFeatureSource, WritableFeatureSource } from "../FeatureSource"
|
|||
import { UIEventSource } from "../../UIEventSource"
|
||||
import { ChangeDescription } from "../../Osm/Actions/ChangeDescription"
|
||||
import { OsmId, OsmTags } from "../../../Models/OsmFeature"
|
||||
import { Feature } from "geojson"
|
||||
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"
|
||||
import { Feature, Point } from "geojson"
|
||||
import { TagUtils } from "../../Tags/TagUtils"
|
||||
import FeaturePropertiesStore from "../Actors/FeaturePropertiesStore"
|
||||
|
||||
export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource {
|
||||
// This class name truly puts the 'Java' into 'Javascript'
|
||||
|
@ -15,115 +16,145 @@ export class NewGeometryFromChangesFeatureSource implements WritableFeatureSourc
|
|||
*
|
||||
* These elements are probably created by the 'SimpleAddUi' which generates a new point, but the import functionality might create a line or polygon too.
|
||||
* Other sources of new points are e.g. imports from nodes
|
||||
*
|
||||
* Alternatively, an already existing point might suddenly match the layer, especially if a point in a wall is reused
|
||||
*
|
||||
* Note that the FeaturePropertiesStore will track a featuresource, such as this one
|
||||
*/
|
||||
public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
|
||||
private readonly _seenChanges: Set<ChangeDescription>
|
||||
private readonly _features: Feature[]
|
||||
private readonly _backend: string
|
||||
private readonly _allElementStorage: IndexedFeatureSource
|
||||
private _featureProperties: FeaturePropertiesStore
|
||||
|
||||
constructor(changes: Changes, allElementStorage: IndexedFeatureSource, backendUrl: string) {
|
||||
const seenChanges = new Set<ChangeDescription>()
|
||||
const features = this.features.data
|
||||
constructor(
|
||||
changes: Changes,
|
||||
allElementStorage: IndexedFeatureSource,
|
||||
featureProperties: FeaturePropertiesStore
|
||||
) {
|
||||
this._allElementStorage = allElementStorage
|
||||
this._featureProperties = featureProperties
|
||||
this._seenChanges = new Set<ChangeDescription>()
|
||||
this._features = this.features.data
|
||||
this._backend = changes.backend
|
||||
const self = this
|
||||
const backend = changes.backend
|
||||
changes.pendingChanges.addCallbackAndRunD((changes) => {
|
||||
if (changes.length === 0) {
|
||||
return
|
||||
changes.pendingChanges.addCallbackAndRunD((changes) => self.handleChanges(changes))
|
||||
}
|
||||
|
||||
private addNewFeature(feature: Feature) {
|
||||
const features = this._features
|
||||
feature.id = feature.properties.id
|
||||
features.push(feature)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a single pending change
|
||||
* @returns true if something changed
|
||||
* @param change
|
||||
* @private
|
||||
*/
|
||||
private handleChange(change: ChangeDescription): boolean {
|
||||
const backend = this._backend
|
||||
const allElementStorage = this._allElementStorage
|
||||
|
||||
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
|
||||
// However, when a point is snapped to an already existing point, we have to create a representation for this point!
|
||||
// For this, we introspect the change
|
||||
if (allElementStorage.featuresById.data.has(change.type + "/" + change.id)) {
|
||||
// The current point already exists, we don't have to do anything here
|
||||
return false
|
||||
}
|
||||
console.debug("Detected a reused point, for", change)
|
||||
// The 'allElementsStore' does _not_ have this point yet, so we have to create it
|
||||
// However, we already create a store for it
|
||||
const { lon, lat } = <{ lon: number; lat: number }>change.changes
|
||||
const feature = <Feature<Point, OsmTags>>{
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: <OsmId>change.type + "/" + change.id,
|
||||
...TagUtils.changeAsProperties(change.tags),
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [lon, lat],
|
||||
},
|
||||
}
|
||||
this._featureProperties.trackFeature(feature)
|
||||
this.addNewFeature(feature)
|
||||
return true
|
||||
} 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
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const tags: OsmTags & { id: OsmId & string } = {
|
||||
id: <OsmId & string>(change.type + "/" + change.id),
|
||||
}
|
||||
for (const kv of change.tags) {
|
||||
tags[kv.k] = kv.v
|
||||
}
|
||||
|
||||
let somethingChanged = false
|
||||
tags["_backend"] = this._backend
|
||||
|
||||
function add(feature) {
|
||||
feature.id = feature.properties.id
|
||||
features.push(feature)
|
||||
somethingChanged = true
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
const n = new OsmNode(change.id)
|
||||
n.tags = tags
|
||||
n.lat = change.changes["lat"]
|
||||
n.lon = change.changes["lon"]
|
||||
const geojson = n.asGeoJson()
|
||||
this.addNewFeature(geojson)
|
||||
break
|
||||
case "way":
|
||||
const w = new OsmWay(change.id)
|
||||
w.tags = tags
|
||||
w.nodes = change.changes["nodes"]
|
||||
w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [lat, lon])
|
||||
this.addNewFeature(w.asGeoJson())
|
||||
break
|
||||
case "relation":
|
||||
const r = new OsmRelation(change.id)
|
||||
r.tags = tags
|
||||
r.members = change.changes["members"]
|
||||
this.addNewFeature(r.asGeoJson())
|
||||
break
|
||||
}
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error("Could not generate a new geometry to render on screen for:", e)
|
||||
}
|
||||
}
|
||||
|
||||
private handleChanges(changes: ChangeDescription[]) {
|
||||
const seenChanges = this._seenChanges
|
||||
if (changes.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let somethingChanged = false
|
||||
|
||||
for (const change of changes) {
|
||||
if (seenChanges.has(change)) {
|
||||
// Already handled
|
||||
continue
|
||||
}
|
||||
seenChanges.add(change)
|
||||
|
||||
if (change.tags === undefined) {
|
||||
// If tags is undefined, this is probably a new point that is part of a split road
|
||||
continue
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
if (seenChanges.has(change)) {
|
||||
// Already handled
|
||||
continue
|
||||
}
|
||||
seenChanges.add(change)
|
||||
|
||||
if (change.tags === undefined) {
|
||||
// If tags is undefined, this is probably a new point that is part of a split road
|
||||
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
|
||||
// However, when a point is snapped to an already existing point, we have to create a representation for this point!
|
||||
// For this, we introspect the change
|
||||
if (allElementStorage.featuresById.data.has(change.type + "/" + change.id)) {
|
||||
// The current point already exists, we don't have to do anything here
|
||||
continue
|
||||
}
|
||||
console.debug("Detected a reused point")
|
||||
// The 'allElementsStore' does _not_ have this point yet, so we have to create it
|
||||
new OsmObjectDownloader(backend)
|
||||
.DownloadObjectAsync(change.type + "/" + change.id)
|
||||
.then((feat) => {
|
||||
console.log("Got the reused point:", feat)
|
||||
if (feat === "deleted") {
|
||||
throw "Panic: snapping to a point, but this point has been deleted in the meantime"
|
||||
}
|
||||
for (const kv of change.tags) {
|
||||
feat.tags[kv.k] = kv.v
|
||||
}
|
||||
const geojson = feat.asGeoJson()
|
||||
self.features.data.push(geojson)
|
||||
self.features.ping()
|
||||
})
|
||||
continue
|
||||
} 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
|
||||
}
|
||||
|
||||
try {
|
||||
const tags: OsmTags & { id: OsmId & string } = {
|
||||
id: <OsmId & string>(change.type + "/" + change.id),
|
||||
}
|
||||
for (const kv of change.tags) {
|
||||
tags[kv.k] = kv.v
|
||||
}
|
||||
|
||||
tags["_backend"] = backendUrl
|
||||
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
const n = new OsmNode(change.id)
|
||||
n.tags = tags
|
||||
n.lat = change.changes["lat"]
|
||||
n.lon = change.changes["lon"]
|
||||
const geojson = n.asGeoJson()
|
||||
add(geojson)
|
||||
break
|
||||
case "way":
|
||||
const w = new OsmWay(change.id)
|
||||
w.tags = tags
|
||||
w.nodes = change.changes["nodes"]
|
||||
w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [
|
||||
lat,
|
||||
lon,
|
||||
])
|
||||
add(w.asGeoJson())
|
||||
break
|
||||
case "relation":
|
||||
const r = new OsmRelation(change.id)
|
||||
r.tags = tags
|
||||
r.members = change.changes["members"]
|
||||
add(r.asGeoJson())
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not generate a new geometry to render on screen for:", e)
|
||||
}
|
||||
}
|
||||
if (somethingChanged) {
|
||||
self.features.ping()
|
||||
}
|
||||
})
|
||||
somethingChanged ||= this.handleChange(change)
|
||||
}
|
||||
if (somethingChanged) {
|
||||
this.features.ping()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
|||
},
|
||||
meta: this.meta,
|
||||
}
|
||||
if (this._snapOnto === undefined) {
|
||||
if (this._snapOnto?.coordinates === undefined) {
|
||||
return [newPointChange]
|
||||
}
|
||||
|
||||
|
@ -113,6 +113,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
|||
console.log("Attempting to snap:", { geojson, projected, projectedCoor, index })
|
||||
// We check that it isn't close to an already existing point
|
||||
let reusedPointId = undefined
|
||||
let reusedPointCoordinates: [number, number] = undefined
|
||||
let outerring: [number, number][]
|
||||
|
||||
if (geojson.geometry.type === "LineString") {
|
||||
|
@ -125,11 +126,13 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
|||
if (GeoOperations.distanceBetween(prev, projectedCoor) < this._reusePointDistance) {
|
||||
// We reuse this point instead!
|
||||
reusedPointId = this._snapOnto.nodes[index]
|
||||
reusedPointCoordinates = this._snapOnto.coordinates[index]
|
||||
}
|
||||
const next = outerring[index + 1]
|
||||
if (GeoOperations.distanceBetween(next, projectedCoor) < this._reusePointDistance) {
|
||||
// We reuse this point instead!
|
||||
reusedPointId = this._snapOnto.nodes[index + 1]
|
||||
reusedPointCoordinates = this._snapOnto.coordinates[index + 1]
|
||||
}
|
||||
if (reusedPointId !== undefined) {
|
||||
this.setElementId(reusedPointId)
|
||||
|
@ -139,12 +142,13 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
|||
type: "node",
|
||||
id: reusedPointId,
|
||||
meta: this.meta,
|
||||
changes: { lat: reusedPointCoordinates[0], lon: reusedPointCoordinates[1] },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const locations = [
|
||||
...this._snapOnto.coordinates.map(([lat, lon]) => <[number, number]>[lon, lat]),
|
||||
...this._snapOnto.coordinates?.map(([lat, lon]) => <[number, number]>[lon, lat]),
|
||||
]
|
||||
const ids = [...this._snapOnto.nodes]
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue