Refactoring: fix rendering of new roads, generated by a split

This commit is contained in:
Pieter Vander Vennet 2023-04-20 01:52:23 +02:00
parent 840990c08b
commit 8eb2c68f79
34 changed files with 443 additions and 333 deletions

View file

@ -1,6 +1,9 @@
import { Changes } from "../Osm/Changes" import { Changes } from "../Osm/Changes"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"; import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
/**
* Applies tag changes onto the featureStore
*/
export default class ChangeToElementsActor { export default class ChangeToElementsActor {
constructor(changes: Changes, allElements: FeaturePropertiesStore) { constructor(changes: Changes, allElements: FeaturePropertiesStore) {
changes.pendingChanges.addCallbackAndRun((changes) => { changes.pendingChanges.addCallbackAndRun((changes) => {

View file

@ -55,12 +55,13 @@ class SingleTileSaver {
*/ */
export default class SaveFeatureSourceToLocalStorage { export default class SaveFeatureSourceToLocalStorage {
constructor( constructor(
backend: string,
layername: string, layername: string,
zoomlevel: number, zoomlevel: number,
features: FeatureSource, features: FeatureSource,
featureProperties: FeaturePropertiesStore featureProperties: FeaturePropertiesStore
) { ) {
const storage = TileLocalStorage.construct<Feature[]>(layername) const storage = TileLocalStorage.construct<Feature[]>(backend, layername)
const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>() const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>()
features.features.addCallbackAndRunD((features) => { features.features.addCallbackAndRunD((features) => {
const sliced = GeoOperations.slice(zoomlevel, features) const sliced = GeoOperations.slice(zoomlevel, features)

View file

@ -17,14 +17,15 @@ export default class TileLocalStorage<T> {
this._layername = layername this._layername = layername
} }
public static construct<T>(layername: string): TileLocalStorage<T> { public static construct<T>(backend: string, layername: string): TileLocalStorage<T> {
const cached = TileLocalStorage.perLayer[layername] const key = backend + "_" + layername
const cached = TileLocalStorage.perLayer[key]
if (cached) { if (cached) {
return cached return cached
} }
const tls = new TileLocalStorage<T>(layername) const tls = new TileLocalStorage<T>(key)
TileLocalStorage.perLayer[layername] = tls TileLocalStorage.perLayer[key] = tls
return tls return tls
} }
@ -46,7 +47,7 @@ export default class TileLocalStorage<T> {
return src return src
} }
private async SetIdb(tileIndex: number, data): Promise<void> { private async SetIdb(tileIndex: number, data: any): Promise<void> {
try { try {
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data) await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data)
} catch (e) { } catch (e) {

View file

@ -3,22 +3,18 @@
*/ */
import { Changes } from "../../Osm/Changes" import { Changes } from "../../Osm/Changes"
import { UIEventSource } from "../../UIEventSource" import { UIEventSource } from "../../UIEventSource"
import { FeatureSourceForLayer, IndexedFeatureSource } from "../FeatureSource" import { FeatureSource, IndexedFeatureSource } from "../FeatureSource"
import FilteredLayer from "../../../Models/FilteredLayer"
import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription" import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription"
import { Feature } from "geojson" import { Feature } from "geojson"
export default class ChangeGeometryApplicator implements FeatureSourceForLayer { export default class ChangeGeometryApplicator implements FeatureSource {
public readonly features: UIEventSource<Feature[]> = public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
new UIEventSource<Feature[]>([])
public readonly layer: FilteredLayer
private readonly source: IndexedFeatureSource private readonly source: IndexedFeatureSource
private readonly changes: Changes private readonly changes: Changes
constructor(source: IndexedFeatureSource & FeatureSourceForLayer, changes: Changes) { constructor(source: IndexedFeatureSource, changes: Changes) {
this.source = source this.source = source
this.changes = changes this.changes = changes
this.layer = source.layer
this.features = new UIEventSource<Feature[]>(undefined) this.features = new UIEventSource<Feature[]>(undefined)
@ -30,10 +26,10 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
private update() { private update() {
const upstreamFeatures = this.source.features.data const upstreamFeatures = this.source.features.data
const upstreamIds = this.source.containedIds.data const upstreamIds = this.source.featuresById.data
const changesToApply = this.changes.allChanges.data?.filter( const changesToApply = this.changes.allChanges.data?.filter(
(ch) => (ch) =>
// Does upsteram have this element? If not, we skip // Does upstream have this element? If not, we skip
upstreamIds.has(ch.type + "/" + ch.id) && upstreamIds.has(ch.type + "/" + ch.id) &&
// Are any (geometry) changes defined? // Are any (geometry) changes defined?
ch.changes !== undefined && ch.changes !== undefined &&
@ -61,7 +57,7 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
for (const feature of upstreamFeatures) { for (const feature of upstreamFeatures) {
const changesForFeature = changesPerId.get(feature.properties.id) const changesForFeature = changesPerId.get(feature.properties.id)
if (changesForFeature === undefined) { if (changesForFeature === undefined) {
// No changes for this element // No changes for this element - simply pass it along to downstream
newFeatures.push(feature) newFeatures.push(feature)
continue continue
} }

View file

@ -1,5 +1,5 @@
import { Store, UIEventSource } from "../../UIEventSource" import { Store, UIEventSource } from "../../UIEventSource"
import { FeatureSource , IndexedFeatureSource } from "../FeatureSource" import { FeatureSource, IndexedFeatureSource } from "../FeatureSource"
import { Feature } from "geojson" import { Feature } from "geojson"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
@ -19,6 +19,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource {
this._featuresById = new UIEventSource<Map<string, Feature>>(undefined) this._featuresById = new UIEventSource<Map<string, Feature>>(undefined)
this.featuresById = this._featuresById this.featuresById = this._featuresById
const self = this const self = this
sources = Utils.NoNull(sources)
for (let source of sources) { for (let source of sources) {
source.features.addCallback(() => { source.features.addCallback(() => {
self.addData(sources.map((s) => s.features.data)) self.addData(sources.map((s) => s.features.data))
@ -28,7 +29,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource {
this._sources = sources this._sources = sources
} }
protected addSource(source: FeatureSource) { public addSource(source: FeatureSource) {
this._sources.push(source) this._sources.push(source)
source.features.addCallbackAndRun(() => { source.features.addCallbackAndRun(() => {
this.addData(this._sources.map((s) => s.features.data)) this.addData(this._sources.map((s) => s.features.data))

View file

@ -4,13 +4,14 @@ import { FeatureSource } from "../FeatureSource"
import { Or } from "../../Tags/Or" import { Or } from "../../Tags/Or"
import FeatureSwitchState from "../../State/FeatureSwitchState" import FeatureSwitchState from "../../State/FeatureSwitchState"
import OverpassFeatureSource from "./OverpassFeatureSource" import OverpassFeatureSource from "./OverpassFeatureSource"
import { ImmutableStore, Store } from "../../UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
import OsmFeatureSource from "./OsmFeatureSource" import OsmFeatureSource from "./OsmFeatureSource"
import FeatureSourceMerger from "./FeatureSourceMerger" import FeatureSourceMerger from "./FeatureSourceMerger"
import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource" import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource"
import { BBox } from "../../BBox" import { BBox } from "../../BBox"
import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource" import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource"
import StaticFeatureSource from "./StaticFeatureSource" import StaticFeatureSource from "./StaticFeatureSource"
import { OsmPreferences } from "../../Osm/OsmPreferences"
/** /**
* This source will fetch the needed data from various sources for the given layout. * This source will fetch the needed data from various sources for the given layout.
@ -18,15 +19,14 @@ import StaticFeatureSource from "./StaticFeatureSource"
* Note that special layers (with `source=null` will be ignored) * Note that special layers (with `source=null` will be ignored)
*/ */
export default class LayoutSource extends FeatureSourceMerger { export default class LayoutSource extends FeatureSourceMerger {
private readonly _isLoading: UIEventSource<boolean> = new UIEventSource<boolean>(false)
/** /**
* Indicates if a data source is loading something * Indicates if a data source is loading something
* TODO fixme
*/ */
public readonly isLoading: Store<boolean> = new ImmutableStore(false) public readonly isLoading: Store<boolean> = this._isLoading
constructor( constructor(
layers: LayerConfig[], layers: LayerConfig[],
featureSwitches: FeatureSwitchState, featureSwitches: FeatureSwitchState,
newAndChangedElements: FeatureSource,
mapProperties: { bounds: Store<BBox>; zoom: Store<number> }, mapProperties: { bounds: Store<BBox>; zoom: Store<number> },
backend: string, backend: string,
isDisplayed: (id: string) => Store<boolean> isDisplayed: (id: string) => Store<boolean>
@ -39,7 +39,7 @@ export default class LayoutSource extends FeatureSourceMerger {
const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined) const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined)
const fromCache = osmLayers.map( const fromCache = osmLayers.map(
(l) => (l) =>
new LocalStorageFeatureSource(l.id, 15, mapProperties, { new LocalStorageFeatureSource(backend, l.id, 15, mapProperties, {
isActive: isDisplayed(l.id), isActive: isDisplayed(l.id),
}) })
) )
@ -56,7 +56,17 @@ export default class LayoutSource extends FeatureSourceMerger {
) )
const expiryInSeconds = Math.min(...(layers?.map((l) => l.maxAgeOfCache) ?? [])) const expiryInSeconds = Math.min(...(layers?.map((l) => l.maxAgeOfCache) ?? []))
super(overpassSource, osmApiSource, newAndChangedElements, ...geojsonSources, ...fromCache)
super(overpassSource, osmApiSource, ...geojsonSources, ...fromCache)
const self = this
function setIsLoading() {
const loading = overpassSource?.runningQuery?.data && osmApiSource?.isRunning?.data
self._isLoading.setData(loading)
}
overpassSource?.runningQuery?.addCallbackAndRun((_) => setIsLoading())
osmApiSource?.isRunning?.addCallbackAndRun((_) => setIsLoading())
} }
private static setupGeojsonSource( private static setupGeojsonSource(
@ -83,9 +93,9 @@ export default class LayoutSource extends FeatureSourceMerger {
zoom: Store<number>, zoom: Store<number>,
backend: string, backend: string,
featureSwitches: FeatureSwitchState featureSwitches: FeatureSwitchState
): FeatureSource { ): OsmFeatureSource | undefined {
if (osmLayers.length == 0) { if (osmLayers.length == 0) {
return new StaticFeatureSource(new ImmutableStore([])) return undefined
} }
const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom)) const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom))
const isActive = zoom.mapD((z) => { const isActive = zoom.mapD((z) => {
@ -115,9 +125,9 @@ export default class LayoutSource extends FeatureSourceMerger {
bounds: Store<BBox>, bounds: Store<BBox>,
zoom: Store<number>, zoom: Store<number>,
featureSwitches: FeatureSwitchState featureSwitches: FeatureSwitchState
): FeatureSource { ): OverpassFeatureSource | undefined {
if (osmLayers.length == 0) { if (osmLayers.length == 0) {
return new StaticFeatureSource(new ImmutableStore([])) return undefined
} }
const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom)) const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom))
const isActive = zoom.mapD((z) => { const isActive = zoom.mapD((z) => {

View file

@ -1,13 +1,12 @@
import { Changes } from "../../Osm/Changes" import { Changes } from "../../Osm/Changes"
import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject" import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject"
import { FeatureSource } from "../FeatureSource" 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 { ElementStorage } from "../../ElementStorage"
import { OsmId, OsmTags } from "../../../Models/OsmFeature" import { OsmId, OsmTags } from "../../../Models/OsmFeature"
import { Feature } from "geojson" import { Feature } from "geojson"
export class NewGeometryFromChangesFeatureSource implements FeatureSource { export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource {
// This class name truly puts the 'Java' into 'Javascript' // This class name truly puts the 'Java' into 'Javascript'
/** /**
@ -18,7 +17,7 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
*/ */
public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([]) public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
constructor(changes: Changes, allElementStorage: ElementStorage, backendUrl: string) { constructor(changes: Changes, allElementStorage: IndexedFeatureSource, backendUrl: string) {
const seenChanges = new Set<ChangeDescription>() const seenChanges = new Set<ChangeDescription>()
const features = this.features.data const features = this.features.data
const self = this const self = this
@ -53,7 +52,7 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
// In _most_ of the cases, this means that this _isn't_ a new 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! // 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 // For this, we introspect the change
if (allElementStorage.has(change.type + "/" + change.id)) { if (allElementStorage.featuresById.data.has(change.type + "/" + change.id)) {
// The current point already exists, we don't have to do anything here // The current point already exists, we don't have to do anything here
continue continue
} }
@ -65,7 +64,6 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
feat.tags[kv.k] = kv.v feat.tags[kv.k] = kv.v
} }
const geojson = feat.asGeoJson() const geojson = feat.asGeoJson()
allElementStorage.addOrGetElement(geojson)
self.features.data.push(geojson) self.features.data.push(geojson)
self.features.ping() self.features.ping()
}) })

View file

@ -41,7 +41,6 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
this.isActive = options.isActive ?? new ImmutableStore(true) this.isActive = options.isActive ?? new ImmutableStore(true)
this._backend = options.backend ?? "https://www.openstreetmap.org" this._backend = options.backend ?? "https://www.openstreetmap.org"
this._bounds.addCallbackAndRunD((bbox) => this.loadData(bbox)) this._bounds.addCallbackAndRunD((bbox) => this.loadData(bbox))
console.log("Allowed tags are:", this.allowedTags)
} }
private async loadData(bbox: BBox) { private async loadData(bbox: BBox) {
@ -108,7 +107,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
} }
private async LoadTile(z, x, y): Promise<void> { private async LoadTile(z, x, y): Promise<void> {
console.log("OsmFeatureSource: loading ", z, x, y) console.log("OsmFeatureSource: loading ", z, x, y, "from", this._backend)
if (z >= 22) { if (z >= 22) {
throw "This is an absurd high zoom level" throw "This is an absurd high zoom level"
} }

View file

@ -22,13 +22,18 @@ export interface SnappingOptions {
* The resulting snap coordinates will be written into this UIEventSource * The resulting snap coordinates will be written into this UIEventSource
*/ */
snapLocation?: UIEventSource<{ lon: number; lat: number }> snapLocation?: UIEventSource<{ lon: number; lat: number }>
/**
* If the projected point is within `reusePointWithin`-meter of an already existing point
*/
reusePointWithin?: number
} }
export default class SnappingFeatureSource implements FeatureSource { export default class SnappingFeatureSource implements FeatureSource {
public readonly features: Store<Feature<Point>[]> public readonly features: Store<Feature<Point>[]>
/*Contains the id of the way it snapped to*/
private readonly _snappedTo: UIEventSource<string>
public readonly snappedTo: Store<string> public readonly snappedTo: Store<string>
private readonly _snappedTo: UIEventSource<string>
constructor( constructor(
snapTo: FeatureSource, snapTo: FeatureSource,

View file

@ -7,6 +7,7 @@ import StaticFeatureSource from "../Sources/StaticFeatureSource"
export default class LocalStorageFeatureSource extends DynamicTileSource { export default class LocalStorageFeatureSource extends DynamicTileSource {
constructor( constructor(
backend: string,
layername: string, layername: string,
zoomlevel: number, zoomlevel: number,
mapProperties: { mapProperties: {
@ -17,7 +18,7 @@ export default class LocalStorageFeatureSource extends DynamicTileSource {
isActive?: Store<boolean> isActive?: Store<boolean>
} }
) { ) {
const storage = TileLocalStorage.construct<Feature[]>(layername) const storage = TileLocalStorage.construct<Feature[]>(backend, layername)
super( super(
zoomlevel, zoomlevel,
(tileIndex) => (tileIndex) =>

View file

@ -294,6 +294,10 @@ export class GeoOperations {
* Mostly used as helper for 'nearestPoint' * Mostly used as helper for 'nearestPoint'
* @param way * @param way
*/ */
public static forceLineString(way: Feature<LineString | Polygon>): Feature<LineString>
public static forceLineString(
way: Feature<MultiLineString | MultiPolygon>
): Feature<MultiLineString>
public static forceLineString( public static forceLineString(
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon> way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
): Feature<LineString | MultiLineString> { ): Feature<LineString | MultiLineString> {
@ -972,4 +976,9 @@ export class GeoOperations {
}, },
} }
} }
static centerpointCoordinatesObj(geojson: Feature) {
const [lon, lat] = GeoOperations.centerpointCoordinates(geojson)
return { lon, lat }
}
} }

View file

@ -4,6 +4,7 @@ import { GeoOperations } from "../../GeoOperations"
import OsmChangeAction from "./OsmChangeAction" import OsmChangeAction from "./OsmChangeAction"
import { ChangeDescription } from "./ChangeDescription" import { ChangeDescription } from "./ChangeDescription"
import RelationSplitHandler from "./RelationSplitHandler" import RelationSplitHandler from "./RelationSplitHandler"
import { Feature, LineString } from "geojson"
interface SplitInfo { interface SplitInfo {
originalIndex?: number // or negative for new elements originalIndex?: number // or negative for new elements
@ -14,9 +15,9 @@ interface SplitInfo {
export default class SplitAction extends OsmChangeAction { export default class SplitAction extends OsmChangeAction {
private readonly wayId: string private readonly wayId: string
private readonly _splitPointsCoordinates: [number, number][] // lon, lat private readonly _splitPointsCoordinates: [number, number][] // lon, lat
private _meta: { theme: string; changeType: "split" } private readonly _meta: { theme: string; changeType: "split" }
private _toleranceInMeters: number private readonly _toleranceInMeters: number
private _withNewCoordinates: (coordinates: [number, number][]) => void private readonly _withNewCoordinates: (coordinates: [number, number][]) => void
/** /**
* Create a changedescription for splitting a point. * Create a changedescription for splitting a point.
@ -197,7 +198,7 @@ export default class SplitAction extends OsmChangeAction {
* If another point is closer then ~5m, we reuse that point * If another point is closer then ~5m, we reuse that point
*/ */
private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] { private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] {
const wayGeoJson = osmWay.asGeoJson() const wayGeoJson = <Feature<LineString>>osmWay.asGeoJson()
// Should be [lon, lat][] // Should be [lon, lat][]
const originalPoints: [number, number][] = osmWay.coordinates.map((c) => [c[1], c[0]]) const originalPoints: [number, number][] = osmWay.coordinates.map((c) => [c[1], c[0]])
const allPoints: { const allPoints: {

View file

@ -18,10 +18,6 @@ import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesSto
* Needs an authenticator via OsmConnection * Needs an authenticator via OsmConnection
*/ */
export class Changes { export class Changes {
/**
* All the newly created features as featureSource + all the modified features
*/
public readonly features = new UIEventSource<{ feature: any; freshness: Date }[]>([])
public readonly pendingChanges: UIEventSource<ChangeDescription[]> = public readonly pendingChanges: UIEventSource<ChangeDescription[]> =
LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", []) LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined) public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)

View file

@ -213,7 +213,7 @@ class RewriteMetaInfoTags extends SimpleMetaTagger {
move("changeset", "_last_edit:changeset") move("changeset", "_last_edit:changeset")
move("timestamp", "_last_edit:timestamp") move("timestamp", "_last_edit:timestamp")
move("version", "_version_number") move("version", "_version_number")
feature.properties._backend = "https://openstreetmap.org" feature.properties._backend = feature.properties._backend ?? "https://openstreetmap.org"
return movedSomething return movedSomething
} }
} }

View file

@ -38,8 +38,9 @@ export class IdbLocalStorage {
return src return src
} }
public static SetDirectly(key: string, value): Promise<void> { public static SetDirectly(key: string, value: any): Promise<void> {
return idb.set(key, value) const copy = Utils.Clone(value)
return idb.set(key, copy)
} }
static GetDirectly(key: string): Promise<void> { static GetDirectly(key: string): Promise<void> {

View file

@ -43,6 +43,7 @@ export default class Constants {
public static readonly no_include = [ public static readonly no_include = [
"conflation", "conflation",
"split_point", "split_point",
"split_road",
"current_view", "current_view",
"matchpoint", "matchpoint",
"import_candidate", "import_candidate",

View file

@ -596,6 +596,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
id: "split-button", id: "split-button",
render: { "*": "{split_button()}" }, render: { "*": "{split_button()}" },
}) })
delete json.allowSplit
} }
if (json.allowMove && !ValidationUtils.hasSpecialVisualisation(json, "move_button")) { if (json.allowMove && !ValidationUtils.hasSpecialVisualisation(json, "move_button")) {
@ -611,7 +612,16 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
}) })
} }
if (json.deletion && !ValidationUtils.hasSpecialVisualisation(json, "all_tags")) { if (
json.source !== "special" &&
json.source !== "special:library" &&
json.tagRenderings &&
!json.tagRenderings.some((tr) => tr["id"] === "last_edit")
) {
json.tagRenderings.push(this._desugaring.tagRenderings.get("last_edit"))
}
if (!ValidationUtils.hasSpecialVisualisation(json, "all_tags")) {
const trc: TagRenderingConfigJson = { const trc: TagRenderingConfigJson = {
id: "all-tags", id: "all-tags",
render: { "*": "{all_tags()}" }, render: { "*": "{all_tags()}" },
@ -623,16 +633,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
], ],
}, },
} }
json.tagRenderings.push(trc) json.tagRenderings?.push(trc)
}
if (
json.source !== "special" &&
json.source !== "special:library" &&
json.tagRenderings &&
!json.tagRenderings.some((tr) => tr["id"] === "last_edit")
) {
json.tagRenderings.push(this._desugaring.tagRenderings.get("last_edit"))
} }
return { result: json } return { result: json }

View file

@ -26,7 +26,6 @@ import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore" import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter" import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource" import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
import ShowDataLayer from "../UI/Map/ShowDataLayer" import ShowDataLayer from "../UI/Map/ShowDataLayer"
import TitleHandler from "../Logic/Actors/TitleHandler" import TitleHandler from "../Logic/Actors/TitleHandler"
@ -39,9 +38,10 @@ import Hotkeys from "../UI/Base/Hotkeys"
import Translations from "../UI/i18n/Translations" import Translations from "../UI/i18n/Translations"
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource" import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
import SimpleFeatureSource from "../Logic/FeatureSource/Sources/SimpleFeatureSource"
import { MenuState } from "./MenuState" import { MenuState } from "./MenuState"
import MetaTagging from "../Logic/MetaTagging" import MetaTagging from "../Logic/MetaTagging"
import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"
import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
/** /**
* *
@ -65,7 +65,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
readonly selectedElement: UIEventSource<Feature> readonly selectedElement: UIEventSource<Feature>
readonly mapProperties: MapProperties & ExportableMap readonly mapProperties: MapProperties & ExportableMap
readonly dataIsLoading: Store<boolean> // TODO readonly dataIsLoading: Store<boolean>
readonly guistate: MenuState readonly guistate: MenuState
readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
@ -80,6 +80,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
readonly geolocation: GeoLocationHandler readonly geolocation: GeoLocationHandler
readonly lastClickObject: WritableFeatureSource readonly lastClickObject: WritableFeatureSource
constructor(layout: LayoutConfig) { constructor(layout: LayoutConfig) {
this.layout = layout this.layout = layout
this.guistate = new MenuState(layout.id) this.guistate = new MenuState(layout.id)
@ -121,49 +122,69 @@ export default class ThemeViewState implements SpecialVisualizationState {
const self = this const self = this
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id) this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
this.newFeatures = new SimpleFeatureSource(undefined)
const layoutSource = new LayoutSource(
layout.layers,
this.featureSwitches,
this.newFeatures,
this.mapProperties,
this.osmConnection.Backend(),
(id) => self.layerState.filteredLayers.get(id).isDisplayed
)
this.indexedFeatures = layoutSource
this.dataIsLoading = layoutSource.isLoading
const lastClick = (this.lastClickObject = new LastClickFeatureSource(
this.mapProperties.lastClickLocation,
this.layout
))
const indexedElements = this.indexedFeatures
this.featureProperties = new FeaturePropertiesStore(indexedElements)
const perLayer = new PerLayerFeatureSourceSplitter(
Array.from(this.layerState.filteredLayers.values()).filter(
(l) => l.layerDef?.source !== null
),
indexedElements,
{
constructStore: (features, layer) => new GeoIndexedStoreForLayer(features, layer),
handleLeftovers: (features) => {
console.warn(
"Got ",
features.length,
"leftover features, such as",
features[0].properties
)
},
}
)
this.perLayer = perLayer.perLayer
{
/* Setup the layout source
* A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too
*/
const layoutSource = new LayoutSource(
layout.layers,
this.featureSwitches,
this.mapProperties,
this.osmConnection.Backend(),
(id) => self.layerState.filteredLayers.get(id).isDisplayed
)
this.indexedFeatures = layoutSource
this.dataIsLoading = layoutSource.isLoading
const indexedElements = this.indexedFeatures
this.featureProperties = new FeaturePropertiesStore(indexedElements)
this.changes = new Changes(
{
dryRun: this.featureSwitches.featureSwitchIsTesting,
allElements: indexedElements,
featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations,
},
layout?.isLeftRightSensitive() ?? false
)
this.newFeatures = new NewGeometryFromChangesFeatureSource(
this.changes,
indexedElements,
this.osmConnection.Backend()
)
layoutSource.addSource(this.newFeatures)
const perLayer = new PerLayerFeatureSourceSplitter(
Array.from(this.layerState.filteredLayers.values()).filter(
(l) => l.layerDef?.source !== null
),
new ChangeGeometryApplicator(this.indexedFeatures, this.changes),
{
constructStore: (features, layer) =>
new GeoIndexedStoreForLayer(features, layer),
handleLeftovers: (features) => {
console.warn(
"Got ",
features.length,
"leftover features, such as",
features[0].properties
)
},
}
)
this.perLayer = perLayer.perLayer
}
this.perLayer.forEach((fs) => { this.perLayer.forEach((fs) => {
new SaveFeatureSourceToLocalStorage( /* TODO enable new SaveFeatureSourceToLocalStorage(
this.osmConnection.Backend(),
fs.layer.layerDef.id, fs.layer.layerDef.id,
15, 15,
fs, fs,
this.featureProperties this.featureProperties
) )//*/
const filtered = new FilteringFeatureSource( const filtered = new FilteringFeatureSource(
fs.layer, fs.layer,
@ -187,16 +208,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
}) })
}) })
this.changes = new Changes( const lastClick = (this.lastClickObject = new LastClickFeatureSource(
{ this.mapProperties.lastClickLocation,
dryRun: this.featureSwitches.featureSwitchIsTesting, this.layout
allElements: indexedElements, ))
featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations,
},
layout?.isLeftRightSensitive() ?? false
)
this.initActors() this.initActors()
this.drawSpecialLayers(lastClick) this.drawSpecialLayers(lastClick)
@ -211,9 +226,13 @@ export default class ThemeViewState implements SpecialVisualizationState {
private miscSetup() { private miscSetup() {
this.userRelatedState.markLayoutAsVisited(this.layout) this.userRelatedState.markLayoutAsVisited(this.layout)
this.selectedElement.addCallbackAndRunD(() => { this.selectedElement.addCallbackAndRunD((feature) => {
// As soon as we have a selected element, we clear it // As soon as we have a selected element, we clear the selected element
// This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature // This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature
// The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear
if (feature.properties.id === "last_click") {
return
}
this.lastClickObject.features.setData([]) this.lastClickObject.features.setData([])
}) })
} }

View file

@ -11,7 +11,6 @@ import { Utils } from "../../Utils"
import { MapillaryLink } from "./MapillaryLink" import { MapillaryLink } from "./MapillaryLink"
import { OpenIdEditor, OpenJosm } from "./CopyrightPanel" import { OpenIdEditor, OpenJosm } from "./CopyrightPanel"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import { DefaultGuiState } from "../DefaultGuiState" import { DefaultGuiState } from "../DefaultGuiState"
export class BackToThemeOverview extends Toggle { export class BackToThemeOverview extends Toggle {
@ -78,14 +77,6 @@ export class ActionButtons extends Combine {
new OpenIdEditor(state, iconStyle), new OpenIdEditor(state, iconStyle),
new MapillaryLink(state, iconStyle), new MapillaryLink(state, iconStyle),
new OpenJosm(state, iconStyle).SetClass("hidden-on-mobile"), new OpenJosm(state, iconStyle).SetClass("hidden-on-mobile"),
new SubtleButton(
Svg.translate_ui().SetStyle(iconStyle),
Translations.t.translations.activateButton
).onClick(() => {
ScrollableFullScreen.collapse()
state.defaultGuiState.userInfoIsOpened.setData(true)
state.defaultGuiState.userInfoFocusedQuestion.setData("translation-mode")
}),
]) ])
this.SetClass("block w-full link-no-underline") this.SetClass("block w-full link-no-underline")
} }

View file

@ -0,0 +1,104 @@
<script lang="ts">
/**
* This component shows a map which focuses on a single OSM-Way (linestring) feature.
* Clicking the map will add a new 'scissor' point, projected on the linestring (and possible snapped to an already existing node within the linestring;
* clicking this point again will remove it.
* The bound 'value' will contain the location of these projected points.
* Points are not coalesced with already existing nodes within the way; it is up to the code actually splitting the way to decide to reuse an existing point or not
*
* This component is _not_ responsible for the rest of the flow, e.g. the confirm button
*/
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson";
import split_point from "../../assets/layers/split_point/split_point.json";
import split_road from "../../assets/layers/split_road/split_road.json";
import { UIEventSource } from "../../Logic/UIEventSource";
import { Map as MlMap } from "maplibre-gl";
import type { MapProperties } from "../../Models/MapProperties";
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor";
import MaplibreMap from "../Map/MaplibreMap.svelte";
import { OsmWay } from "../../Logic/Osm/OsmObject";
import ShowDataLayer from "../Map/ShowDataLayer";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import { GeoOperations } from "../../Logic/GeoOperations";
import { BBox } from "../../Logic/BBox";
import type { Feature, LineString, Point } from "geojson";
const splitpoint_style = new LayerConfig(
<LayerConfigJson>split_point,
"(BUILTIN) SplitRoadWizard.ts",
true
) as const;
const splitroad_style = new LayerConfig(
<LayerConfigJson>split_road,
"(BUILTIN) SplitRoadWizard.ts",
true
) as const;
/**
* The way to focus on
*/
export let osmWay: OsmWay
/**
* How to render this layer.
* A default is given
*/
export let layer: LayerConfig = splitroad_style
/**
* Optional: use these properties to set e.g. background layer
*/
export let mapProperties: undefined | Partial<MapProperties> = undefined;
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
let adaptor = new MapLibreAdaptor(map, mapProperties);
const wayGeojson: Feature<LineString> = GeoOperations.forceLineString( osmWay.asGeoJson())
adaptor.location.setData(GeoOperations.centerpointCoordinatesObj(wayGeojson))
adaptor.bounds.setData(BBox.get(wayGeojson).pad(2))
adaptor.maxbounds.setData(BBox.get(wayGeojson).pad(2))
new ShowDataLayer(map, {
features: new StaticFeatureSource([wayGeojson]),
drawMarkers: false,
layer: layer
})
export let splitPoints: UIEventSource< Feature<
Point,
{
id: number
index: number
dist: number
location: number
}
>[]> = new UIEventSource([])
const splitPointsFS = new StaticFeatureSource(splitPoints)
new ShowDataLayer(map, {
layer: splitpoint_style,
features: splitPointsFS,
onClick: (clickedFeature: Feature) => {
console.log("Clicked feature is", clickedFeature, splitPoints.data)
const i = splitPoints.data.findIndex(f => f === clickedFeature)
if(i < 0){
return
}
splitPoints.data.splice(i, 1)
splitPoints.ping()
}
})
let id = 0
adaptor.lastClickLocation.addCallbackD(({lon, lat}) => {
const projected = GeoOperations.nearestPoint(wayGeojson, [lon, lat])
projected.properties["id"] = id
id++
splitPoints.data.push(<any> projected)
splitPoints.ping()
})
</script>
<div class="w-full h-full">
<MaplibreMap {map}></MaplibreMap>
</div>

View file

@ -90,6 +90,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
self.setAllowMoving(self.allowMoving.data) self.setAllowMoving(self.allowMoving.data)
self.setAllowZooming(self.allowZooming.data) self.setAllowZooming(self.allowZooming.data)
self.setMinzoom(self.minzoom.data) self.setMinzoom(self.minzoom.data)
self.setBounds(self.bounds.data)
}) })
self.MoveMapToCurrentLoc(self.location.data) self.MoveMapToCurrentLoc(self.location.data)
self.SetZoom(self.zoom.data) self.SetZoom(self.zoom.data)
@ -97,6 +98,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
self.setAllowMoving(self.allowMoving.data) self.setAllowMoving(self.allowMoving.data)
self.setAllowZooming(self.allowZooming.data) self.setAllowZooming(self.allowZooming.data)
self.setMinzoom(self.minzoom.data) self.setMinzoom(self.minzoom.data)
self.setBounds(self.bounds.data)
this.updateStores() this.updateStores()
map.on("moveend", () => this.updateStores()) map.on("moveend", () => this.updateStores())
map.on("click", (e) => { map.on("click", (e) => {
@ -238,18 +240,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
container.style.height = document.documentElement.clientHeight + "px" container.style.height = document.documentElement.clientHeight + "px"
} }
const markerCanvas: HTMLCanvasElement = await html2canvas( await html2canvas(
map.getCanvasContainer(), map.getCanvasContainer(),
{ {
backgroundColor: "#00000000", backgroundColor: "#00000000",
canvas: drawOn, canvas: drawOn,
} }
) )
const markers = await new Promise<Blob>((resolve) =>
markerCanvas.toBlob((data) => resolve(data))
)
console.log("Markers:", markers, markerCanvas)
// destinationCtx.drawImage(markerCanvas, 0, 0)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally { } finally {
@ -429,7 +426,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
private setBounds(bounds: BBox) { private setBounds(bounds: BBox) {
const map = this._maplibreMap.data const map = this._maplibreMap.data
if (map === undefined) { if (map === undefined || bounds === undefined) {
return return
} }
const oldBounds = map.getBounds() const oldBounds = map.getBounds()

View file

@ -24,7 +24,7 @@
$map.resize(); $map.resize();
}); });
}); });
const styleUrl = "https://api.maptiler.com/maps/streets/style.json?key=GvoVAJgu46I5rZapJuAy"; const styleUrl = "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy";
</script> </script>
<main> <main>
<Map bind:center={center} <Map bind:center={center}

View file

@ -197,7 +197,7 @@ class LineRenderingLayer {
this._fetchStore = fetchStore this._fetchStore = fetchStore
this._onClick = onClick this._onClick = onClick
const self = this const self = this
features.features.addCallbackAndRunD((features) => self.update(features)) features.features.addCallbackAndRunD(() => self.update(features.features))
} }
private calculatePropsFor( private calculatePropsFor(
@ -229,13 +229,23 @@ class LineRenderingLayer {
return calculatedProps return calculatedProps
} }
private async update(features: Feature[]) { private currentSourceData
private async update(featureSource: Store<Feature[]>) {
const map = this._map const map = this._map
while (!map.isStyleLoaded()) { while (!map.isStyleLoaded()) {
await Utils.waitFor(100) await Utils.waitFor(100)
} }
// After waiting 'till the map has loaded, the data might have changed already
// As such, we only now read the features from the featureSource and compare with the previously set data
const features = featureSource.data
const src = <GeoJSONSource>map.getSource(this._layername) const src = <GeoJSONSource>map.getSource(this._layername)
if (this.currentSourceData === features) {
// Already up to date
return
}
if (src === undefined) { if (src === undefined) {
this.currentSourceData = features
map.addSource(this._layername, { map.addSource(this._layername, {
type: "geojson", type: "geojson",
data: { data: {
@ -262,7 +272,6 @@ class LineRenderingLayer {
}) })
map.on("click", linelayer, (e) => { map.on("click", linelayer, (e) => {
console.log("Click", e)
e.originalEvent["consumed"] = true e.originalEvent["consumed"] = true
this._onClick(e.features[0]) this._onClick(e.features[0])
}) })
@ -297,9 +306,10 @@ class LineRenderingLayer {
} }
}) })
} else { } else {
this.currentSourceData = features
src.setData({ src.setData({
type: "FeatureCollection", type: "FeatureCollection",
features, features: this.currentSourceData,
}) })
} }
@ -345,10 +355,21 @@ export default class ShowDataLayer {
"ShowDataLayer.ts:range.json" "ShowDataLayer.ts:range.json"
) )
private readonly _map: Store<MlMap> private readonly _map: Store<MlMap>
private readonly _options: ShowDataLayerOptions & { layer: LayerConfig } private readonly _options: ShowDataLayerOptions & {
layer: LayerConfig
drawMarkers?: true | boolean
drawLines?: true | boolean
}
private readonly _popupCache: Map<string, ScrollableFullScreen> private readonly _popupCache: Map<string, ScrollableFullScreen>
constructor(map: Store<MlMap>, options: ShowDataLayerOptions & { layer: LayerConfig }) { constructor(
map: Store<MlMap>,
options: ShowDataLayerOptions & {
layer: LayerConfig
drawMarkers?: true | boolean
drawLines?: true | boolean
}
) {
this._map = map this._map = map
this._options = options this._options = options
this._popupCache = new Map() this._popupCache = new Map()
@ -405,28 +426,31 @@ export default class ShowDataLayer {
selectedElement?.setData(feature) selectedElement?.setData(feature)
selectedLayer?.setData(this._options.layer) selectedLayer?.setData(this._options.layer)
}) })
for (let i = 0; i < this._options.layer.lineRendering.length; i++) { if (this._options.drawLines !== false) {
const lineRenderingConfig = this._options.layer.lineRendering[i] for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
new LineRenderingLayer( const lineRenderingConfig = this._options.layer.lineRendering[i]
map, new LineRenderingLayer(
features, map,
this._options.layer.id + "_linerendering_" + i, features,
lineRenderingConfig, this._options.layer.id + "_linerendering_" + i,
doShowLayer, lineRenderingConfig,
fetchStore, doShowLayer,
onClick fetchStore,
) onClick
)
}
} }
if (this._options.drawMarkers !== false) {
for (const pointRenderingConfig of this._options.layer.mapRendering) { for (const pointRenderingConfig of this._options.layer.mapRendering) {
new PointRenderingLayer( new PointRenderingLayer(
map, map,
features, features,
pointRenderingConfig, pointRenderingConfig,
doShowLayer, doShowLayer,
fetchStore, fetchStore,
onClick onClick
) )
}
} }
features.features.addCallbackAndRunD((_) => this.zoomToCurrentFeatures(map)) features.features.addCallbackAndRunD((_) => this.zoomToCurrentFeatures(map))
} }

View file

@ -83,19 +83,9 @@
snapOnto: snapToWay snapOnto: snapToWay
}); });
await state.changes.applyAction(newElementAction); await state.changes.applyAction(newElementAction);
// The 'changes' should have created a new point, which added this into the 'featureProperties'
const newId = newElementAction.newElementId; const newId = newElementAction.newElementId;
state.newFeatures.features.data.push({
type: "Feature",
properties: {
id: newId,
...TagUtils.KVtoProperties(tags)
},
geometry: {
type: "Point",
coordinates: [location.lon, location.lat]
}
});
state.newFeatures.features.ping();
const tagsStore = state.featureProperties.getStore(newId); const tagsStore = state.featureProperties.getStore(newId);
{ {
// Set some metainfo // Set some metainfo

View file

@ -58,6 +58,7 @@
]) ])
} }
}; };
// Normally, the 'Changes' will generate the new element. The 'notes' are an exception to this
state.newFeatures.features.data.push(feature); state.newFeatures.features.data.push(feature);
state.newFeatures.features.ping(); state.newFeatures.features.ping();
state.selectedElement?.setData(feature); state.selectedElement?.setData(feature);

View file

@ -5,7 +5,7 @@ import { OsmConnection, OsmServiceState } from "../../Logic/Osm/OsmConnection"
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import Loading from "../Base/Loading" import Loading from "../Base/Loading"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import { Store } from "../../Logic/UIEventSource" import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import { Translation } from "../i18n/Translation" import { Translation } from "../i18n/Translation"
@ -13,13 +13,13 @@ class LoginButton extends SubtleButton {
constructor( constructor(
text: BaseUIElement | string, text: BaseUIElement | string,
state: { state: {
osmConnection: OsmConnection osmConnection?: OsmConnection
}, },
icon?: BaseUIElement | string icon?: BaseUIElement | string
) { ) {
super(icon ?? Svg.login_ui(), text) super(icon ?? Svg.login_ui(), text)
this.onClick(() => { this.onClick(() => {
state.osmConnection.AttemptLogin() state.osmConnection?.AttemptLogin()
}) })
} }
} }
@ -32,13 +32,16 @@ export class LoginToggle extends VariableUiElement {
* If logging in is not possible for some reason, an appropriate error message is shown * If logging in is not possible for some reason, an appropriate error message is shown
* *
* State contains the 'osmConnection' to work with * State contains the 'osmConnection' to work with
* @param el: Element to show when logged in
* @param text: To show on the login button. Default: nothing
* @param state: if no osmConnection is given, assumes test situation and will show 'el' as if logged in
*/ */
constructor( constructor(
el: BaseUIElement, el: BaseUIElement,
text: BaseUIElement | string, text: BaseUIElement | string,
state: { state: {
readonly osmConnection: OsmConnection readonly osmConnection?: OsmConnection
readonly featureSwitchUserbadge: Store<boolean> readonly featureSwitchUserbadge?: Store<boolean>
} }
) { ) {
const loading = new Loading("Trying to log in...") const loading = new Loading("Trying to log in...")
@ -51,14 +54,14 @@ export class LoginToggle extends VariableUiElement {
} }
super( super(
state.osmConnection.loadingStatus.map( state.osmConnection?.loadingStatus?.map(
(osmConnectionState) => { (osmConnectionState) => {
if (state.featureSwitchUserbadge.data == false) { if (state.featureSwitchUserbadge?.data == false) {
// All features to login with are disabled // All features to login with are disabled
return undefined return undefined
} }
const apiState = state.osmConnection.apiIsOnline.data const apiState = state.osmConnection?.apiIsOnline?.data ?? "online"
const apiTranslation = offlineModes[apiState] const apiTranslation = offlineModes[apiState]
if (apiTranslation !== undefined) { if (apiTranslation !== undefined) {
return new Combine([ return new Combine([
@ -77,15 +80,15 @@ export class LoginToggle extends VariableUiElement {
return el return el
} }
// Error! // Fallback
return new LoginButton( return new LoginButton(
Translations.t.general.loginFailed, Translations.t.general.loginFailed,
state, state,
Svg.invalid_svg() Svg.invalid_svg()
) )
}, },
[state.featureSwitchUserbadge, state.osmConnection.apiIsOnline] [state.featureSwitchUserbadge, state.osmConnection?.apiIsOnline]
) ) ?? new ImmutableStore(el) //
) )
} }
} }

View file

@ -2,33 +2,25 @@ import Toggle from "../Input/Toggle"
import Svg from "../../Svg" import Svg from "../../Svg"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import { SubtleButton } from "../Base/SubtleButton" import { SubtleButton } from "../Base/SubtleButton"
import { GeoOperations } from "../../Logic/GeoOperations"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import { Button } from "../Base/Button" import { Button } from "../Base/Button"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import SplitAction from "../../Logic/Osm/Actions/SplitAction" import SplitAction from "../../Logic/Osm/Actions/SplitAction"
import Title from "../Base/Title" import Title from "../Base/Title"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { BBox } from "../../Logic/BBox"
import split_point from "../../assets/layers/split_point/split_point.json"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Changes } from "../../Logic/Osm/Changes"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import FilteredLayer from "../../Models/FilteredLayer"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import { LoginToggle } from "./LoginButton" import { LoginToggle } from "./LoginButton"
import { SpecialVisualizationState } from "../SpecialVisualization" import SvelteUIElement from "../Base/SvelteUIElement"
import WaySplitMap from "../BigComponents/WaySplitMap.svelte"
import { OsmObject } from "../../Logic/Osm/OsmObject"
import { Feature, Point } from "geojson"
import { WayId } from "../../Models/OsmFeature"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Changes } from "../../Logic/Osm/Changes"
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
export default class SplitRoadWizard extends Combine { export default class SplitRoadWizard extends Combine {
private static splitLayerStyling = new LayerConfig(
split_point,
"(BUILTIN) SplitRoadWizard.ts",
true
)
public dialogIsOpened: UIEventSource<boolean> public dialogIsOpened: UIEventSource<boolean>
/** /**
@ -37,20 +29,34 @@ export default class SplitRoadWizard extends Combine {
* @param id: The id of the road to remove * @param id: The id of the road to remove
* @param state: the state of the application * @param state: the state of the application
*/ */
constructor(id: string, state: SpecialVisualizationState) { constructor(
id: WayId,
state: {
layout?: LayoutConfig
osmConnection?: OsmConnection
changes?: Changes
indexedFeatures?: IndexedFeatureSource
selectedElement?: UIEventSource<Feature>
}
) {
const t = Translations.t.split const t = Translations.t.split
// Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring // Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring
const splitPoints = new UIEventSource<{ feature: any; freshness: Date }[]>([]) const splitPoints = new UIEventSource<Feature<Point>[]>([])
const hasBeenSplit = new UIEventSource(false) const hasBeenSplit = new UIEventSource(false)
// Toggle variable between show split button and map // Toggle variable between show split button and map
const splitClicked = new UIEventSource<boolean>(false) const splitClicked = new UIEventSource<boolean>(false)
const leafletMap = new UIEventSource<BaseUIElement>( const leafletMap = new UIEventSource<BaseUIElement>(undefined)
SplitRoadWizard.setupMapComponent(id, splitPoints, state)
) function initMap() {
SplitRoadWizard.setupMapComponent(id, splitPoints).then((mapComponent) =>
leafletMap.setData(mapComponent.SetClass("w-full h-80"))
)
}
initMap()
// Toggle between splitmap // Toggle between splitmap
const splitButton = new SubtleButton( const splitButton = new SubtleButton(
@ -70,23 +76,19 @@ export default class SplitRoadWizard extends Combine {
splitClicked.setData(false) splitClicked.setData(false)
const splitAction = new SplitAction( const splitAction = new SplitAction(
id, id,
splitPoints.data.map((ff) => ff.feature.geometry.coordinates), splitPoints.data.map((ff) => <[number, number]>(<Point>ff.geometry).coordinates),
{ {
theme: state?.layoutToUse?.id, theme: state?.layout?.id,
}, },
5, 5
(coordinates) => {
state.allElements.ContainingFeatures.get(id).geometry["coordinates"] =
coordinates
}
) )
await state.changes.applyAction(splitAction) await state.changes?.applyAction(splitAction)
// We throw away the old map and splitpoints, and create a new map from scratch // We throw away the old map and splitpoints, and create a new map from scratch
splitPoints.setData([]) splitPoints.setData([])
leafletMap.setData(SplitRoadWizard.setupMapComponent(id, splitPoints, state)) initMap()
// Close the popup. The contributor has to select a segment again to make sure they continue editing the correct segment; see #1219 // Close the popup. The contributor has to select a segment again to make sure they continue editing the correct segment; see #1219
ScrollableFullScreen.collapse() state.selectedElement?.setData(undefined)
}) })
saveButton.SetClass("btn btn-primary mr-3") saveButton.SetClass("btn btn-primary mr-3")
@ -131,95 +133,14 @@ export default class SplitRoadWizard extends Combine {
}) })
} }
private static setupMapComponent( private static async setupMapComponent(
id: string, id: WayId,
splitPoints: UIEventSource<{ feature: any; freshness: Date }[]>, splitPoints: UIEventSource<Feature[]>
state: { ): Promise<BaseUIElement> {
filteredLayers: UIEventSource<FilteredLayer[]> const osmWay = await OsmObject.DownloadObjectAsync(id)
backgroundLayer: UIEventSource<BaseLayer> return new SvelteUIElement(WaySplitMap, {
featureSwitchIsTesting: UIEventSource<boolean> osmWay,
featureSwitchIsDebugging: UIEventSource<boolean> splitPoints,
featureSwitchShowAllQuestions: UIEventSource<boolean>
osmConnection: OsmConnection
featureSwitchUserbadge: UIEventSource<boolean>
changes: Changes
layoutToUse: LayoutConfig
allElements: ElementStorage
}
): BaseUIElement {
// Load the road with given id on the minimap
const roadElement = state.allElements.ContainingFeatures.get(id)
// Minimap on which you can select the points to be splitted
const miniMap = Minimap.createMiniMap({
background: state.backgroundLayer,
allowMoving: true,
leafletOptions: {
minZoom: 14,
},
}) })
miniMap.SetStyle("width: 100%; height: 24rem").SetClass("rounded-xl overflow-hidden")
miniMap.installBounds(BBox.get(roadElement).pad(0.25), false)
// Define how a cut is displayed on the map
// Datalayer displaying the road and the cut points (if any)
new ShowDataMultiLayer({
features: StaticFeatureSource.fromGeojson([roadElement]),
layers: state.filteredLayers,
leafletMap: miniMap.leafletMap,
zoomToFeatures: true,
state,
})
new ShowDataLayer({
features: new StaticFeatureSource(splitPoints),
leafletMap: miniMap.leafletMap,
zoomToFeatures: false,
layerToShow: SplitRoadWizard.splitLayerStyling,
state,
})
/**
* Handles a click on the overleaf map.
* Finds the closest intersection with the road and adds a point there, ready to confirm the cut.
* @param coordinates Clicked location, [lon, lat]
*/
function onMapClick(coordinates) {
// First, we check if there is another, already existing point nearby
const points = splitPoints.data
.map((f, i) => [f.feature, i])
.filter(
(p) => GeoOperations.distanceBetween(p[0].geometry.coordinates, coordinates) < 5
)
.map((p) => p[1])
.sort((a, b) => a - b)
.reverse(/*Copy/derived list, inplace reverse is fine*/)
if (points.length > 0) {
for (const point of points) {
splitPoints.data.splice(point, 1)
}
splitPoints.ping()
return
}
// Get nearest point on the road
const pointOnRoad = GeoOperations.nearestPoint(<any>roadElement, coordinates) // pointOnRoad is a geojson
// Update point properties to let it match the layer
pointOnRoad.properties["_split_point"] = "yes"
// Add it to the list of all points and notify observers
splitPoints.data.push({ feature: pointOnRoad, freshness: new Date() }) // show the point on the data layer
splitPoints.ping() // not updated using .setData, so manually ping observers
}
// When clicked, pass clicked location coordinates to onMapClick function
miniMap.leafletMap.addCallbackAndRunD((leafletMap) =>
leafletMap.on("click", (mouseEvent: LeafletMouseEvent) => {
onMapClick([mouseEvent.latlng.lng, mouseEvent.latlng.lat])
})
)
return miniMap
} }
} }

View file

@ -1,18 +1,18 @@
import { Store, UIEventSource } from "../Logic/UIEventSource" import { Store, UIEventSource } from "../Logic/UIEventSource";
import BaseUIElement from "./BaseUIElement" import BaseUIElement from "./BaseUIElement";
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource";
import { OsmConnection } from "../Logic/Osm/OsmConnection" import { OsmConnection } from "../Logic/Osm/OsmConnection";
import { Changes } from "../Logic/Osm/Changes" import { Changes } from "../Logic/Osm/Changes";
import { ExportableMap, MapProperties } from "../Models/MapProperties" import { ExportableMap, MapProperties } from "../Models/MapProperties";
import LayerState from "../Logic/State/LayerState" import LayerState from "../Logic/State/LayerState";
import { Feature, Geometry } from "geojson" import { Feature, Geometry } from "geojson";
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
import { MangroveIdentity } from "../Logic/Web/MangroveReviews" import { MangroveIdentity } from "../Logic/Web/MangroveReviews";
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore";
import LayerConfig from "../Models/ThemeConfig/LayerConfig" import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import FeatureSwitchState from "../Logic/State/FeatureSwitchState" import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import { MenuState } from "../Models/MenuState" import { MenuState } from "../Models/MenuState";
/** /**
* The state needed to render a special Visualisation. * The state needed to render a special Visualisation.

View file

@ -77,8 +77,9 @@ import Lazy from "./Base/Lazy"
import { CheckBox } from "./Input/Checkboxes" import { CheckBox } from "./Input/Checkboxes"
import Slider from "./Input/Slider" import Slider from "./Input/Slider"
import DeleteWizard from "./Popup/DeleteWizard" import DeleteWizard from "./Popup/DeleteWizard"
import { OsmId, OsmTags } from "../Models/OsmFeature" import { OsmId, OsmTags, WayId } from "../Models/OsmFeature"
import MoveWizard from "./Popup/MoveWizard" import MoveWizard from "./Popup/MoveWizard"
import SplitRoadWizard from "./Popup/SplitRoadWizard"
class NearbyImageVis implements SpecialVisualization { class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -532,16 +533,17 @@ export default class SpecialVisualizations {
args: [], args: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
return new VariableUiElement( return new VariableUiElement(
// TODO
tagSource tagSource
.map((tags) => tags.id) .map((tags) => tags.id)
.map((id) => new FixedUiElement("TODO: enable splitting")) // new SplitRoadWizard(id, state)) .map((id) => {
if (id.startsWith("way/")) {
return new SplitRoadWizard(<WayId>id, state)
}
return undefined
})
) )
}, },
}, },

View file

@ -0,0 +1,21 @@
{
"id": "split_road",
"description": "Layer rendering the way to split in the 'splitRoadWizard'. This one is used instead of the variable rendering by the themes themselves, as they might not always be very visible",
"minzoom": 1,
"source": "special",
"name": null,
"title": null,
"mapRendering": [
{
"location": [
"point"
],
"icon": "bug",
"iconSize": "30,30,center"
},
{
"width": "8",
"color": "black"
}
]
}

View file

@ -379,7 +379,7 @@
], ],
"overrideAll": { "overrideAll": {
"allowSplit": true, "allowSplit": true,
"tagRenderings+": [ "+tagRenderings": [
{ {
"id": "is_cyclestreet", "id": "is_cyclestreet",
"question": { "question": {

View file

@ -1061,6 +1061,10 @@ video {
height: 10rem; height: 10rem;
} }
.h-80 {
height: 20rem;
}
.max-h-20vh { .max-h-20vh {
max-height: 20vh; max-height: 20vh;
} }
@ -1081,6 +1085,10 @@ video {
min-height: 8rem; min-height: 8rem;
} }
.w-full {
width: 100%;
}
.w-8 { .w-8 {
width: 2rem; width: 2rem;
} }
@ -1105,10 +1113,6 @@ video {
width: 1.5rem; width: 1.5rem;
} }
.w-full {
width: 100%;
}
.w-screen { .w-screen {
width: 100vw; width: 100vw;
} }

View file

@ -18,7 +18,7 @@
</head> </head>
<body> <body>
<div id="maindiv">'maindiv' not attached</div> <div id="maindiv" class="w-full h-full">'maindiv' not attached</div>
<div id="extradiv">'extradiv' not attached</div> <div id="extradiv">'extradiv' not attached</div>
<script type="module" src="./test.ts"></script> <script type="module" src="./test.ts"></script>

11
test.ts
View file

@ -9,6 +9,10 @@ import { UIEventSource } from "./Logic/UIEventSource"
import { VariableUiElement } from "./UI/Base/VariableUIElement" import { VariableUiElement } from "./UI/Base/VariableUIElement"
import { FixedUiElement } from "./UI/Base/FixedUiElement" import { FixedUiElement } from "./UI/Base/FixedUiElement"
import Title from "./UI/Base/Title" import Title from "./UI/Base/Title"
import WaySplitMap from "./UI/BigComponents/WaySplitMap.svelte"
import SvelteUIElement from "./UI/Base/SvelteUIElement"
import { OsmObject } from "./Logic/Osm/OsmObject"
import SplitRoadWizard from "./UI/Popup/SplitRoadWizard"
function testspecial() { function testspecial() {
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data) const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
@ -41,5 +45,10 @@ function testinput() {
} }
new Combine(els).SetClass("flex flex-col").AttachTo("maindiv") new Combine(els).SetClass("flex flex-col").AttachTo("maindiv")
} }
testinput()
async function testWaySplit() {
new SplitRoadWizard("way/28717919", {}).SetClass("w-full h-full").AttachTo("maindiv")
}
testWaySplit().then((_) => console.log("inited"))
//testinput()
// testspecial() // testspecial()