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 { FeatureSource } from "../FeatureSource"
|
||||||
import { UIEventSource } from "../../UIEventSource"
|
import { UIEventSource } from "../../UIEventSource"
|
||||||
|
import { OsmTags } from "../../../Models/OsmFeature"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a UIEventStore for the properties of every Feature, indexed by id
|
* 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.
|
* 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
|
* 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) {
|
if (newProperties[oldPropertiesKey] === undefined) {
|
||||||
changeMade = true
|
changeMade = true
|
||||||
delete oldProperties[oldPropertiesKey]
|
// delete oldProperties[oldPropertiesKey]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +50,48 @@ export default class FeaturePropertiesStore {
|
||||||
return changeMade
|
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
|
// noinspection JSUnusedGlobalSymbols
|
||||||
public addAlias(oldId: string, newId: string): void {
|
public addAlias(oldId: string, newId: string): void {
|
||||||
if (newId === undefined) {
|
if (newId === undefined) {
|
||||||
|
|
|
@ -4,8 +4,9 @@ import { IndexedFeatureSource, WritableFeatureSource } from "../FeatureSource"
|
||||||
import { UIEventSource } from "../../UIEventSource"
|
import { UIEventSource } from "../../UIEventSource"
|
||||||
import { ChangeDescription } from "../../Osm/Actions/ChangeDescription"
|
import { ChangeDescription } from "../../Osm/Actions/ChangeDescription"
|
||||||
import { OsmId, OsmTags } from "../../../Models/OsmFeature"
|
import { OsmId, OsmTags } from "../../../Models/OsmFeature"
|
||||||
import { Feature } from "geojson"
|
import { Feature, Point } from "geojson"
|
||||||
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"
|
import { TagUtils } from "../../Tags/TagUtils"
|
||||||
|
import FeaturePropertiesStore from "../Actors/FeaturePropertiesStore"
|
||||||
|
|
||||||
export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource {
|
export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource {
|
||||||
// This class name truly puts the 'Java' into 'Javascript'
|
// 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.
|
* 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
|
* 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[]>([])
|
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) {
|
constructor(
|
||||||
const seenChanges = new Set<ChangeDescription>()
|
changes: Changes,
|
||||||
const features = this.features.data
|
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 self = this
|
||||||
const backend = changes.backend
|
changes.pendingChanges.addCallbackAndRunD((changes) => self.handleChanges(changes))
|
||||||
changes.pendingChanges.addCallbackAndRunD((changes) => {
|
}
|
||||||
if (changes.length === 0) {
|
|
||||||
return
|
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) {
|
switch (change.type) {
|
||||||
feature.id = feature.properties.id
|
case "node":
|
||||||
features.push(feature)
|
const n = new OsmNode(change.id)
|
||||||
somethingChanged = true
|
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) {
|
somethingChanged ||= this.handleChange(change)
|
||||||
if (seenChanges.has(change)) {
|
}
|
||||||
// Already handled
|
if (somethingChanged) {
|
||||||
continue
|
this.features.ping()
|
||||||
}
|
}
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
||||||
},
|
},
|
||||||
meta: this.meta,
|
meta: this.meta,
|
||||||
}
|
}
|
||||||
if (this._snapOnto === undefined) {
|
if (this._snapOnto?.coordinates === undefined) {
|
||||||
return [newPointChange]
|
return [newPointChange]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,6 +113,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
||||||
console.log("Attempting to snap:", { geojson, projected, projectedCoor, index })
|
console.log("Attempting to snap:", { geojson, projected, projectedCoor, index })
|
||||||
// We check that it isn't close to an already existing point
|
// We check that it isn't close to an already existing point
|
||||||
let reusedPointId = undefined
|
let reusedPointId = undefined
|
||||||
|
let reusedPointCoordinates: [number, number] = undefined
|
||||||
let outerring: [number, number][]
|
let outerring: [number, number][]
|
||||||
|
|
||||||
if (geojson.geometry.type === "LineString") {
|
if (geojson.geometry.type === "LineString") {
|
||||||
|
@ -125,11 +126,13 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
||||||
if (GeoOperations.distanceBetween(prev, projectedCoor) < this._reusePointDistance) {
|
if (GeoOperations.distanceBetween(prev, projectedCoor) < this._reusePointDistance) {
|
||||||
// We reuse this point instead!
|
// We reuse this point instead!
|
||||||
reusedPointId = this._snapOnto.nodes[index]
|
reusedPointId = this._snapOnto.nodes[index]
|
||||||
|
reusedPointCoordinates = this._snapOnto.coordinates[index]
|
||||||
}
|
}
|
||||||
const next = outerring[index + 1]
|
const next = outerring[index + 1]
|
||||||
if (GeoOperations.distanceBetween(next, projectedCoor) < this._reusePointDistance) {
|
if (GeoOperations.distanceBetween(next, projectedCoor) < this._reusePointDistance) {
|
||||||
// We reuse this point instead!
|
// We reuse this point instead!
|
||||||
reusedPointId = this._snapOnto.nodes[index + 1]
|
reusedPointId = this._snapOnto.nodes[index + 1]
|
||||||
|
reusedPointCoordinates = this._snapOnto.coordinates[index + 1]
|
||||||
}
|
}
|
||||||
if (reusedPointId !== undefined) {
|
if (reusedPointId !== undefined) {
|
||||||
this.setElementId(reusedPointId)
|
this.setElementId(reusedPointId)
|
||||||
|
@ -139,12 +142,13 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
||||||
type: "node",
|
type: "node",
|
||||||
id: reusedPointId,
|
id: reusedPointId,
|
||||||
meta: this.meta,
|
meta: this.meta,
|
||||||
|
changes: { lat: reusedPointCoordinates[0], lon: reusedPointCoordinates[1] },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const locations = [
|
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]
|
const ids = [...this._snapOnto.nodes]
|
||||||
|
|
||||||
|
|
|
@ -244,7 +244,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.newFeatures = new NewGeometryFromChangesFeatureSource(
|
this.newFeatures = new NewGeometryFromChangesFeatureSource(
|
||||||
this.changes,
|
this.changes,
|
||||||
indexedElements,
|
indexedElements,
|
||||||
this.osmConnection.Backend()
|
this.featureProperties
|
||||||
)
|
)
|
||||||
layoutSource.addSource(this.newFeatures)
|
layoutSource.addSource(this.newFeatures)
|
||||||
|
|
||||||
|
|
|
@ -376,12 +376,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
}
|
}
|
||||||
const background: RasterLayerProperties = this.rasterLayer?.data?.properties
|
const background: RasterLayerProperties = this.rasterLayer?.data?.properties
|
||||||
if (!background) {
|
if (!background) {
|
||||||
console.error(
|
|
||||||
"Attempting to 'setBackground', but the background is",
|
|
||||||
background,
|
|
||||||
"for",
|
|
||||||
map.getCanvas()
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this._currentRasterLayer === background.id) {
|
if (this._currentRasterLayer === background.id) {
|
||||||
|
@ -457,7 +451,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
if (!map) {
|
if (!map) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log("Rotation allowed:", allow)
|
|
||||||
if (allow === false) {
|
if (allow === false) {
|
||||||
map.rotateTo(0, { duration: 0 })
|
map.rotateTo(0, { duration: 0 })
|
||||||
map.setPitch(0)
|
map.setPitch(0)
|
||||||
|
|
|
@ -3,109 +3,109 @@
|
||||||
* This component ties together all the steps that are needed to create a new point.
|
* This component ties together all the steps that are needed to create a new point.
|
||||||
* There are many subcomponents which help with that
|
* There are many subcomponents which help with that
|
||||||
*/
|
*/
|
||||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||||
import PresetList from "./PresetList.svelte"
|
import PresetList from "./PresetList.svelte";
|
||||||
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig"
|
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig";
|
||||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||||
import Tr from "../../Base/Tr.svelte"
|
import Tr from "../../Base/Tr.svelte";
|
||||||
import SubtleButton from "../../Base/SubtleButton.svelte"
|
import SubtleButton from "../../Base/SubtleButton.svelte";
|
||||||
import FromHtml from "../../Base/FromHtml.svelte"
|
import FromHtml from "../../Base/FromHtml.svelte";
|
||||||
import Translations from "../../i18n/Translations.js"
|
import Translations from "../../i18n/Translations.js";
|
||||||
import TagHint from "../TagHint.svelte"
|
import TagHint from "../TagHint.svelte";
|
||||||
import { And } from "../../../Logic/Tags/And.js"
|
import { And } from "../../../Logic/Tags/And.js";
|
||||||
import LoginToggle from "../../Base/LoginToggle.svelte"
|
import LoginToggle from "../../Base/LoginToggle.svelte";
|
||||||
import Constants from "../../../Models/Constants.js"
|
import Constants from "../../../Models/Constants.js";
|
||||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
|
||||||
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
|
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||||
import LoginButton from "../../Base/LoginButton.svelte"
|
import LoginButton from "../../Base/LoginButton.svelte";
|
||||||
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"
|
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte";
|
||||||
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"
|
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||||
import { OsmWay } from "../../../Logic/Osm/OsmObject"
|
import { OsmWay } from "../../../Logic/Osm/OsmObject";
|
||||||
import { Tag } from "../../../Logic/Tags/Tag"
|
import { Tag } from "../../../Logic/Tags/Tag";
|
||||||
import type { WayId } from "../../../Models/OsmFeature"
|
import type { WayId } from "../../../Models/OsmFeature";
|
||||||
import Loading from "../../Base/Loading.svelte"
|
import Loading from "../../Base/Loading.svelte";
|
||||||
import type { GlobalFilter } from "../../../Models/GlobalFilter"
|
import type { GlobalFilter } from "../../../Models/GlobalFilter";
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte";
|
||||||
import NextButton from "../../Base/NextButton.svelte"
|
import NextButton from "../../Base/NextButton.svelte";
|
||||||
import BackButton from "../../Base/BackButton.svelte"
|
import BackButton from "../../Base/BackButton.svelte";
|
||||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
import ToSvelte from "../../Base/ToSvelte.svelte";
|
||||||
import Svg from "../../../Svg"
|
import Svg from "../../../Svg";
|
||||||
import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte"
|
import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte";
|
||||||
import { twJoin } from "tailwind-merge"
|
import { twJoin } from "tailwind-merge";
|
||||||
|
|
||||||
export let coordinate: { lon: number; lat: number }
|
export let coordinate: { lon: number; lat: number };
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState;
|
||||||
|
|
||||||
let selectedPreset: {
|
let selectedPreset: {
|
||||||
preset: PresetConfig
|
preset: PresetConfig
|
||||||
layer: LayerConfig
|
layer: LayerConfig
|
||||||
icon: string
|
icon: string
|
||||||
tags: Record<string, string>
|
tags: Record<string, string>
|
||||||
} = undefined
|
} = undefined;
|
||||||
let checkedOfGlobalFilters: number = 0
|
let checkedOfGlobalFilters: number = 0;
|
||||||
let confirmedCategory = false
|
let confirmedCategory = false;
|
||||||
$: if (selectedPreset === undefined) {
|
$: if (selectedPreset === undefined) {
|
||||||
confirmedCategory = false
|
confirmedCategory = false;
|
||||||
creating = false
|
creating = false;
|
||||||
checkedOfGlobalFilters = 0
|
checkedOfGlobalFilters = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let flayer: FilteredLayer = undefined
|
let flayer: FilteredLayer = undefined;
|
||||||
let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined
|
let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined;
|
||||||
let layerHasFilters: Store<boolean> | undefined = undefined
|
let layerHasFilters: Store<boolean> | undefined = undefined;
|
||||||
let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters
|
let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters;
|
||||||
let _globalFilter: GlobalFilter[] = []
|
let _globalFilter: GlobalFilter[] = [];
|
||||||
onDestroy(
|
onDestroy(
|
||||||
globalFilter.addCallbackAndRun((globalFilter) => {
|
globalFilter.addCallbackAndRun((globalFilter) => {
|
||||||
console.log("Global filters are", globalFilter)
|
console.log("Global filters are", globalFilter);
|
||||||
_globalFilter = globalFilter ?? []
|
_globalFilter = globalFilter ?? [];
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
$: {
|
$: {
|
||||||
flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id)
|
flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id);
|
||||||
layerIsDisplayed = flayer?.isDisplayed
|
layerIsDisplayed = flayer?.isDisplayed;
|
||||||
layerHasFilters = flayer?.hasFilter
|
layerHasFilters = flayer?.hasFilter;
|
||||||
}
|
}
|
||||||
const t = Translations.t.general.add
|
const t = Translations.t.general.add;
|
||||||
|
|
||||||
const zoom = state.mapProperties.zoom
|
const zoom = state.mapProperties.zoom;
|
||||||
|
|
||||||
const isLoading = state.dataIsLoading
|
const isLoading = state.dataIsLoading;
|
||||||
let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
|
let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined);
|
||||||
let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined)
|
let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined);
|
||||||
|
|
||||||
// Small helper variable: if the map is tapped, we should let the 'Next'-button grab some attention as users have to click _that_ to continue, not the map
|
// Small helper variable: if the map is tapped, we should let the 'Next'-button grab some attention as users have to click _that_ to continue, not the map
|
||||||
let preciseInputIsTapped = false
|
let preciseInputIsTapped = false;
|
||||||
|
|
||||||
let creating = false
|
let creating = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call when the user should restart the flow by clicking on the map, e.g. because they disabled filters.
|
* Call when the user should restart the flow by clicking on the map, e.g. because they disabled filters.
|
||||||
* Will delete the lastclick-location
|
* Will delete the lastclick-location
|
||||||
*/
|
*/
|
||||||
function abort() {
|
function abort() {
|
||||||
state.selectedElement.setData(undefined)
|
state.selectedElement.setData(undefined);
|
||||||
// When aborted, we force the contributors to place the pin _again_
|
// When aborted, we force the contributors to place the pin _again_
|
||||||
// This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map
|
// This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map
|
||||||
state.lastClickObject.features.setData([])
|
state.lastClickObject.features.setData([]);
|
||||||
preciseInputIsTapped = false
|
preciseInputIsTapped = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirm() {
|
async function confirm() {
|
||||||
creating = true
|
creating = true;
|
||||||
const location: { lon: number; lat: number } = preciseCoordinate.data
|
const location: { lon: number; lat: number } = preciseCoordinate.data;
|
||||||
const snapTo: WayId | undefined = <WayId>snappedToObject.data
|
const snapTo: WayId | undefined = <WayId>snappedToObject.data;
|
||||||
const tags: Tag[] = selectedPreset.preset.tags.concat(
|
const tags: Tag[] = selectedPreset.preset.tags.concat(
|
||||||
..._globalFilter.map((f) => f?.onNewPoint?.tags ?? [])
|
..._globalFilter.map((f) => f?.onNewPoint?.tags ?? [])
|
||||||
)
|
);
|
||||||
console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags)
|
console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags);
|
||||||
|
|
||||||
let snapToWay: undefined | OsmWay = undefined
|
let snapToWay: undefined | OsmWay = undefined;
|
||||||
if (snapTo !== undefined) {
|
if (snapTo !== undefined) {
|
||||||
const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0)
|
const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0);
|
||||||
if (downloaded !== "deleted") {
|
if (downloaded !== "deleted") {
|
||||||
snapToWay = downloaded
|
snapToWay = downloaded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,33 +113,42 @@
|
||||||
theme: state.layout?.id ?? "unkown",
|
theme: state.layout?.id ?? "unkown",
|
||||||
changeType: "create",
|
changeType: "create",
|
||||||
snapOnto: snapToWay,
|
snapOnto: snapToWay,
|
||||||
})
|
reusePointWithinMeters: 1
|
||||||
await state.changes.applyAction(newElementAction)
|
});
|
||||||
state.newFeatures.features.ping()
|
await state.changes.applyAction(newElementAction);
|
||||||
|
state.newFeatures.features.ping();
|
||||||
// The 'changes' should have created a new point, which added this into the 'featureProperties'
|
// The 'changes' should have created a new point, which added this into the 'featureProperties'
|
||||||
const newId = newElementAction.newElementId
|
const newId = newElementAction.newElementId;
|
||||||
console.log("Applied pending changes, fetching store for", newId)
|
console.log("Applied pending changes, fetching store for", newId);
|
||||||
const tagsStore = state.featureProperties.getStore(newId)
|
const tagsStore = state.featureProperties.getStore(newId);
|
||||||
|
if (!tagsStore) {
|
||||||
|
console.error("Bug: no tagsStore found for", newId);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
// Set some metainfo
|
// Set some metainfo
|
||||||
const properties = tagsStore.data
|
const properties = tagsStore.data;
|
||||||
if (snapTo) {
|
if (snapTo) {
|
||||||
// metatags (starting with underscore) are not uploaded, so we can safely mark this
|
// metatags (starting with underscore) are not uploaded, so we can safely mark this
|
||||||
delete properties["_referencing_ways"]
|
delete properties["_referencing_ways"];
|
||||||
properties["_referencing_ways"] = `["${snapTo}"]`
|
properties["_referencing_ways"] = `["${snapTo}"]`;
|
||||||
}
|
}
|
||||||
properties["_backend"] = state.osmConnection.Backend()
|
properties["_backend"] = state.osmConnection.Backend();
|
||||||
properties["_last_edit:timestamp"] = new Date().toISOString()
|
properties["_last_edit:timestamp"] = new Date().toISOString();
|
||||||
const userdetails = state.osmConnection.userDetails.data
|
const userdetails = state.osmConnection.userDetails.data;
|
||||||
properties["_last_edit:contributor"] = userdetails.name
|
properties["_last_edit:contributor"] = userdetails.name;
|
||||||
properties["_last_edit:uid"] = "" + userdetails.uid
|
properties["_last_edit:uid"] = "" + userdetails.uid;
|
||||||
tagsStore.ping()
|
tagsStore.ping();
|
||||||
}
|
}
|
||||||
const feature = state.indexedFeatures.featuresById.data.get(newId)
|
const feature = state.indexedFeatures.featuresById.data.get(newId);
|
||||||
abort()
|
console.log("Selecting feature", feature, "and opening their popup");
|
||||||
state.selectedLayer.setData(selectedPreset.layer)
|
abort();
|
||||||
state.selectedElement.setData(feature)
|
state.selectedLayer.setData(selectedPreset.layer);
|
||||||
tagsStore.ping()
|
state.selectedElement.setData(feature);
|
||||||
|
tagsStore.ping();
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmSync() {
|
||||||
|
confirm().then(_ => console.debug("New point successfully handled")).catch(e => console.error("Handling the new point went wrong due to", e));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -328,7 +337,7 @@
|
||||||
"absolute top-0 flex w-full justify-center p-12"
|
"absolute top-0 flex w-full justify-center p-12"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<NextButton on:click={confirm} clss="primary w-fit">
|
<NextButton on:click={confirmSync} clss="primary w-fit">
|
||||||
<div class="flex w-full justify-end gap-x-2">
|
<div class="flex w-full justify-end gap-x-2">
|
||||||
<Tr t={Translations.t.general.add.confirmLocation} />
|
<Tr t={Translations.t.general.add.confirmLocation} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -88,7 +88,6 @@
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
console.log("Inited 'checkMappings' to", checkedMappings);
|
|
||||||
if (confg.freeform?.key) {
|
if (confg.freeform?.key) {
|
||||||
if (!confg.multiAnswer) {
|
if (!confg.multiAnswer) {
|
||||||
// Somehow, setting multi-answer freeform values is broken if this is not set
|
// Somehow, setting multi-answer freeform values is broken if this is not set
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue