diff --git a/Logic/Actors/ChangeToElementsActor.ts b/Logic/Actors/ChangeToElementsActor.ts index 3354dbfa6..3bb0f50ad 100644 --- a/Logic/Actors/ChangeToElementsActor.ts +++ b/Logic/Actors/ChangeToElementsActor.ts @@ -1,6 +1,9 @@ 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 { constructor(changes: Changes, allElements: FeaturePropertiesStore) { changes.pendingChanges.addCallbackAndRun((changes) => { diff --git a/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts b/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts index dbb4ae855..47e777d21 100644 --- a/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts +++ b/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts @@ -55,12 +55,13 @@ class SingleTileSaver { */ export default class SaveFeatureSourceToLocalStorage { constructor( + backend: string, layername: string, zoomlevel: number, features: FeatureSource, featureProperties: FeaturePropertiesStore ) { - const storage = TileLocalStorage.construct(layername) + const storage = TileLocalStorage.construct(backend, layername) const singleTileSavers: Map = new Map() features.features.addCallbackAndRunD((features) => { const sliced = GeoOperations.slice(zoomlevel, features) diff --git a/Logic/FeatureSource/Actors/TileLocalStorage.ts b/Logic/FeatureSource/Actors/TileLocalStorage.ts index 9ee40c603..dc92acf85 100644 --- a/Logic/FeatureSource/Actors/TileLocalStorage.ts +++ b/Logic/FeatureSource/Actors/TileLocalStorage.ts @@ -17,14 +17,15 @@ export default class TileLocalStorage { this._layername = layername } - public static construct(layername: string): TileLocalStorage { - const cached = TileLocalStorage.perLayer[layername] + public static construct(backend: string, layername: string): TileLocalStorage { + const key = backend + "_" + layername + const cached = TileLocalStorage.perLayer[key] if (cached) { return cached } - const tls = new TileLocalStorage(layername) - TileLocalStorage.perLayer[layername] = tls + const tls = new TileLocalStorage(key) + TileLocalStorage.perLayer[key] = tls return tls } @@ -46,7 +47,7 @@ export default class TileLocalStorage { return src } - private async SetIdb(tileIndex: number, data): Promise { + private async SetIdb(tileIndex: number, data: any): Promise { try { await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data) } catch (e) { diff --git a/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts b/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts index fb92d69c6..b0675c1bc 100644 --- a/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts +++ b/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts @@ -3,22 +3,18 @@ */ import { Changes } from "../../Osm/Changes" import { UIEventSource } from "../../UIEventSource" -import { FeatureSourceForLayer, IndexedFeatureSource } from "../FeatureSource" -import FilteredLayer from "../../../Models/FilteredLayer" +import { FeatureSource, IndexedFeatureSource } from "../FeatureSource" import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription" import { Feature } from "geojson" -export default class ChangeGeometryApplicator implements FeatureSourceForLayer { - public readonly features: UIEventSource = - new UIEventSource([]) - public readonly layer: FilteredLayer +export default class ChangeGeometryApplicator implements FeatureSource { + public readonly features: UIEventSource = new UIEventSource([]) private readonly source: IndexedFeatureSource private readonly changes: Changes - constructor(source: IndexedFeatureSource & FeatureSourceForLayer, changes: Changes) { + constructor(source: IndexedFeatureSource, changes: Changes) { this.source = source this.changes = changes - this.layer = source.layer this.features = new UIEventSource(undefined) @@ -30,10 +26,10 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer { private update() { 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( (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) && // Are any (geometry) changes defined? ch.changes !== undefined && @@ -61,7 +57,7 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer { for (const feature of upstreamFeatures) { const changesForFeature = changesPerId.get(feature.properties.id) if (changesForFeature === undefined) { - // No changes for this element + // No changes for this element - simply pass it along to downstream newFeatures.push(feature) continue } diff --git a/Logic/FeatureSource/Sources/FeatureSourceMerger.ts b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts index cd5bda68a..52121ada2 100644 --- a/Logic/FeatureSource/Sources/FeatureSourceMerger.ts +++ b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts @@ -1,5 +1,5 @@ import { Store, UIEventSource } from "../../UIEventSource" -import { FeatureSource , IndexedFeatureSource } from "../FeatureSource" +import { FeatureSource, IndexedFeatureSource } from "../FeatureSource" import { Feature } from "geojson" import { Utils } from "../../../Utils" @@ -19,6 +19,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource { this._featuresById = new UIEventSource>(undefined) this.featuresById = this._featuresById const self = this + sources = Utils.NoNull(sources) for (let source of sources) { source.features.addCallback(() => { self.addData(sources.map((s) => s.features.data)) @@ -28,7 +29,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource { this._sources = sources } - protected addSource(source: FeatureSource) { + public addSource(source: FeatureSource) { this._sources.push(source) source.features.addCallbackAndRun(() => { this.addData(this._sources.map((s) => s.features.data)) diff --git a/Logic/FeatureSource/Sources/LayoutSource.ts b/Logic/FeatureSource/Sources/LayoutSource.ts index 53626bc14..e142a489a 100644 --- a/Logic/FeatureSource/Sources/LayoutSource.ts +++ b/Logic/FeatureSource/Sources/LayoutSource.ts @@ -4,13 +4,14 @@ import { FeatureSource } from "../FeatureSource" import { Or } from "../../Tags/Or" import FeatureSwitchState from "../../State/FeatureSwitchState" import OverpassFeatureSource from "./OverpassFeatureSource" -import { ImmutableStore, Store } from "../../UIEventSource" +import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" import OsmFeatureSource from "./OsmFeatureSource" import FeatureSourceMerger from "./FeatureSourceMerger" import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource" import { BBox } from "../../BBox" import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource" import StaticFeatureSource from "./StaticFeatureSource" +import { OsmPreferences } from "../../Osm/OsmPreferences" /** * 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) */ export default class LayoutSource extends FeatureSourceMerger { + private readonly _isLoading: UIEventSource = new UIEventSource(false) /** * Indicates if a data source is loading something - * TODO fixme */ - public readonly isLoading: Store = new ImmutableStore(false) + public readonly isLoading: Store = this._isLoading constructor( layers: LayerConfig[], featureSwitches: FeatureSwitchState, - newAndChangedElements: FeatureSource, mapProperties: { bounds: Store; zoom: Store }, backend: string, isDisplayed: (id: string) => Store @@ -39,7 +39,7 @@ export default class LayoutSource extends FeatureSourceMerger { const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined) const fromCache = osmLayers.map( (l) => - new LocalStorageFeatureSource(l.id, 15, mapProperties, { + new LocalStorageFeatureSource(backend, l.id, 15, mapProperties, { isActive: isDisplayed(l.id), }) ) @@ -56,7 +56,17 @@ export default class LayoutSource extends FeatureSourceMerger { ) 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( @@ -83,9 +93,9 @@ export default class LayoutSource extends FeatureSourceMerger { zoom: Store, backend: string, featureSwitches: FeatureSwitchState - ): FeatureSource { + ): OsmFeatureSource | undefined { if (osmLayers.length == 0) { - return new StaticFeatureSource(new ImmutableStore([])) + return undefined } const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom)) const isActive = zoom.mapD((z) => { @@ -115,9 +125,9 @@ export default class LayoutSource extends FeatureSourceMerger { bounds: Store, zoom: Store, featureSwitches: FeatureSwitchState - ): FeatureSource { + ): OverpassFeatureSource | undefined { if (osmLayers.length == 0) { - return new StaticFeatureSource(new ImmutableStore([])) + return undefined } const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom)) const isActive = zoom.mapD((z) => { diff --git a/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts b/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts index 639c375ed..44b6e13f5 100644 --- a/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts +++ b/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts @@ -1,13 +1,12 @@ import { Changes } from "../../Osm/Changes" import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject" -import { FeatureSource } from "../FeatureSource" +import { IndexedFeatureSource, WritableFeatureSource } from "../FeatureSource" import { UIEventSource } from "../../UIEventSource" import { ChangeDescription } from "../../Osm/Actions/ChangeDescription" -import { ElementStorage } from "../../ElementStorage" import { OsmId, OsmTags } from "../../../Models/OsmFeature" import { Feature } from "geojson" -export class NewGeometryFromChangesFeatureSource implements FeatureSource { +export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource { // This class name truly puts the 'Java' into 'Javascript' /** @@ -18,7 +17,7 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { */ public readonly features: UIEventSource = new UIEventSource([]) - constructor(changes: Changes, allElementStorage: ElementStorage, backendUrl: string) { + constructor(changes: Changes, allElementStorage: IndexedFeatureSource, backendUrl: string) { const seenChanges = new Set() const features = this.features.data 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 // 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.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 continue } @@ -65,7 +64,6 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { feat.tags[kv.k] = kv.v } const geojson = feat.asGeoJson() - allElementStorage.addOrGetElement(geojson) self.features.data.push(geojson) self.features.ping() }) diff --git a/Logic/FeatureSource/Sources/OsmFeatureSource.ts b/Logic/FeatureSource/Sources/OsmFeatureSource.ts index e55db7e94..91a819e2b 100644 --- a/Logic/FeatureSource/Sources/OsmFeatureSource.ts +++ b/Logic/FeatureSource/Sources/OsmFeatureSource.ts @@ -41,7 +41,6 @@ export default class OsmFeatureSource extends FeatureSourceMerger { this.isActive = options.isActive ?? new ImmutableStore(true) this._backend = options.backend ?? "https://www.openstreetmap.org" this._bounds.addCallbackAndRunD((bbox) => this.loadData(bbox)) - console.log("Allowed tags are:", this.allowedTags) } private async loadData(bbox: BBox) { @@ -108,7 +107,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger { } private async LoadTile(z, x, y): Promise { - console.log("OsmFeatureSource: loading ", z, x, y) + console.log("OsmFeatureSource: loading ", z, x, y, "from", this._backend) if (z >= 22) { throw "This is an absurd high zoom level" } diff --git a/Logic/FeatureSource/Sources/SnappingFeatureSource.ts b/Logic/FeatureSource/Sources/SnappingFeatureSource.ts index 43dab4d22..6cc85858f 100644 --- a/Logic/FeatureSource/Sources/SnappingFeatureSource.ts +++ b/Logic/FeatureSource/Sources/SnappingFeatureSource.ts @@ -22,13 +22,18 @@ export interface SnappingOptions { * The resulting snap coordinates will be written into this UIEventSource */ 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 { public readonly features: Store[]> - - private readonly _snappedTo: UIEventSource + /*Contains the id of the way it snapped to*/ public readonly snappedTo: Store + private readonly _snappedTo: UIEventSource constructor( snapTo: FeatureSource, diff --git a/Logic/FeatureSource/TiledFeatureSource/LocalStorageFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/LocalStorageFeatureSource.ts index 972c48a8e..d83db825e 100644 --- a/Logic/FeatureSource/TiledFeatureSource/LocalStorageFeatureSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/LocalStorageFeatureSource.ts @@ -7,6 +7,7 @@ import StaticFeatureSource from "../Sources/StaticFeatureSource" export default class LocalStorageFeatureSource extends DynamicTileSource { constructor( + backend: string, layername: string, zoomlevel: number, mapProperties: { @@ -17,7 +18,7 @@ export default class LocalStorageFeatureSource extends DynamicTileSource { isActive?: Store } ) { - const storage = TileLocalStorage.construct(layername) + const storage = TileLocalStorage.construct(backend, layername) super( zoomlevel, (tileIndex) => diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index c9af7c6e2..741affcb9 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -294,6 +294,10 @@ export class GeoOperations { * Mostly used as helper for 'nearestPoint' * @param way */ + public static forceLineString(way: Feature): Feature + public static forceLineString( + way: Feature + ): Feature public static forceLineString( way: Feature ): Feature { @@ -972,4 +976,9 @@ export class GeoOperations { }, } } + + static centerpointCoordinatesObj(geojson: Feature) { + const [lon, lat] = GeoOperations.centerpointCoordinates(geojson) + return { lon, lat } + } } diff --git a/Logic/Osm/Actions/SplitAction.ts b/Logic/Osm/Actions/SplitAction.ts index ba95dbe36..6c8166220 100644 --- a/Logic/Osm/Actions/SplitAction.ts +++ b/Logic/Osm/Actions/SplitAction.ts @@ -4,6 +4,7 @@ import { GeoOperations } from "../../GeoOperations" import OsmChangeAction from "./OsmChangeAction" import { ChangeDescription } from "./ChangeDescription" import RelationSplitHandler from "./RelationSplitHandler" +import { Feature, LineString } from "geojson" interface SplitInfo { originalIndex?: number // or negative for new elements @@ -14,9 +15,9 @@ interface SplitInfo { export default class SplitAction extends OsmChangeAction { private readonly wayId: string private readonly _splitPointsCoordinates: [number, number][] // lon, lat - private _meta: { theme: string; changeType: "split" } - private _toleranceInMeters: number - private _withNewCoordinates: (coordinates: [number, number][]) => void + private readonly _meta: { theme: string; changeType: "split" } + private readonly _toleranceInMeters: number + private readonly _withNewCoordinates: (coordinates: [number, number][]) => void /** * 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 */ private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] { - const wayGeoJson = osmWay.asGeoJson() + const wayGeoJson = >osmWay.asGeoJson() // Should be [lon, lat][] const originalPoints: [number, number][] = osmWay.coordinates.map((c) => [c[1], c[0]]) const allPoints: { diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index f4cba09ac..b7580ad77 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -18,10 +18,6 @@ import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesSto * Needs an authenticator via OsmConnection */ 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 = LocalStorageSource.GetParsed("pending-changes", []) public readonly allChanges = new UIEventSource(undefined) diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index 246ba2f4e..34e36d7cb 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -213,7 +213,7 @@ class RewriteMetaInfoTags extends SimpleMetaTagger { move("changeset", "_last_edit:changeset") move("timestamp", "_last_edit:timestamp") move("version", "_version_number") - feature.properties._backend = "https://openstreetmap.org" + feature.properties._backend = feature.properties._backend ?? "https://openstreetmap.org" return movedSomething } } diff --git a/Logic/Web/IdbLocalStorage.ts b/Logic/Web/IdbLocalStorage.ts index 8abc2a9e5..81ce2b6eb 100644 --- a/Logic/Web/IdbLocalStorage.ts +++ b/Logic/Web/IdbLocalStorage.ts @@ -38,8 +38,9 @@ export class IdbLocalStorage { return src } - public static SetDirectly(key: string, value): Promise { - return idb.set(key, value) + public static SetDirectly(key: string, value: any): Promise { + const copy = Utils.Clone(value) + return idb.set(key, copy) } static GetDirectly(key: string): Promise { diff --git a/Models/Constants.ts b/Models/Constants.ts index 21bbe9223..18b6cf429 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -43,6 +43,7 @@ export default class Constants { public static readonly no_include = [ "conflation", "split_point", + "split_road", "current_view", "matchpoint", "import_candidate", diff --git a/Models/ThemeConfig/Conversion/PrepareLayer.ts b/Models/ThemeConfig/Conversion/PrepareLayer.ts index ad1ddae4d..368b9f42e 100644 --- a/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -596,6 +596,7 @@ export class AddEditingElements extends DesugaringStep { id: "split-button", render: { "*": "{split_button()}" }, }) + delete json.allowSplit } if (json.allowMove && !ValidationUtils.hasSpecialVisualisation(json, "move_button")) { @@ -611,7 +612,16 @@ export class AddEditingElements extends DesugaringStep { }) } - 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 = { id: "all-tags", render: { "*": "{all_tags()}" }, @@ -623,16 +633,7 @@ export class AddEditingElements extends DesugaringStep { ], }, } - 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")) + json.tagRenderings?.push(trc) } return { result: json } diff --git a/Models/ThemeViewState.ts b/Models/ThemeViewState.ts index 75b59559d..5850f13ec 100644 --- a/Models/ThemeViewState.ts +++ b/Models/ThemeViewState.ts @@ -26,7 +26,6 @@ import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore" import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter" -import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource" import ShowDataLayer from "../UI/Map/ShowDataLayer" import TitleHandler from "../Logic/Actors/TitleHandler" @@ -39,9 +38,10 @@ import Hotkeys from "../UI/Base/Hotkeys" import Translations from "../UI/i18n/Translations" import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource" -import SimpleFeatureSource from "../Logic/FeatureSource/Sources/SimpleFeatureSource" import { MenuState } from "./MenuState" 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 readonly mapProperties: MapProperties & ExportableMap - readonly dataIsLoading: Store // TODO + readonly dataIsLoading: Store readonly guistate: MenuState readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO @@ -80,6 +80,7 @@ export default class ThemeViewState implements SpecialVisualizationState { readonly geolocation: GeoLocationHandler readonly lastClickObject: WritableFeatureSource + constructor(layout: LayoutConfig) { this.layout = layout this.guistate = new MenuState(layout.id) @@ -121,49 +122,69 @@ export default class ThemeViewState implements SpecialVisualizationState { const self = this 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) => { - new SaveFeatureSourceToLocalStorage( + /* TODO enable new SaveFeatureSourceToLocalStorage( + this.osmConnection.Backend(), fs.layer.layerDef.id, 15, fs, this.featureProperties - ) + )//*/ const filtered = new FilteringFeatureSource( fs.layer, @@ -187,16 +208,10 @@ export default class ThemeViewState implements SpecialVisualizationState { }) }) - this.changes = new Changes( - { - dryRun: this.featureSwitches.featureSwitchIsTesting, - allElements: indexedElements, - featurePropertiesStore: this.featureProperties, - osmConnection: this.osmConnection, - historicalUserLocations: this.geolocation.historicalUserLocations, - }, - layout?.isLeftRightSensitive() ?? false - ) + const lastClick = (this.lastClickObject = new LastClickFeatureSource( + this.mapProperties.lastClickLocation, + this.layout + )) this.initActors() this.drawSpecialLayers(lastClick) @@ -211,9 +226,13 @@ export default class ThemeViewState implements SpecialVisualizationState { private miscSetup() { this.userRelatedState.markLayoutAsVisited(this.layout) - this.selectedElement.addCallbackAndRunD(() => { - // As soon as we have a selected element, we clear it + this.selectedElement.addCallbackAndRunD((feature) => { + // 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 + // 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([]) }) } diff --git a/UI/BigComponents/ActionButtons.ts b/UI/BigComponents/ActionButtons.ts index beba67986..8cd9fbccd 100644 --- a/UI/BigComponents/ActionButtons.ts +++ b/UI/BigComponents/ActionButtons.ts @@ -11,7 +11,6 @@ import { Utils } from "../../Utils" import { MapillaryLink } from "./MapillaryLink" import { OpenIdEditor, OpenJosm } from "./CopyrightPanel" import Toggle from "../Input/Toggle" -import ScrollableFullScreen from "../Base/ScrollableFullScreen" import { DefaultGuiState } from "../DefaultGuiState" export class BackToThemeOverview extends Toggle { @@ -78,14 +77,6 @@ export class ActionButtons extends Combine { new OpenIdEditor(state, iconStyle), new MapillaryLink(state, iconStyle), 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") } diff --git a/UI/BigComponents/WaySplitMap.svelte b/UI/BigComponents/WaySplitMap.svelte new file mode 100644 index 000000000..3f3a4c4f8 --- /dev/null +++ b/UI/BigComponents/WaySplitMap.svelte @@ -0,0 +1,104 @@ + +
+ +
diff --git a/UI/Map/MapLibreAdaptor.ts b/UI/Map/MapLibreAdaptor.ts index 4b28bdea7..b3de1bb38 100644 --- a/UI/Map/MapLibreAdaptor.ts +++ b/UI/Map/MapLibreAdaptor.ts @@ -90,6 +90,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { self.setAllowMoving(self.allowMoving.data) self.setAllowZooming(self.allowZooming.data) self.setMinzoom(self.minzoom.data) + self.setBounds(self.bounds.data) }) self.MoveMapToCurrentLoc(self.location.data) self.SetZoom(self.zoom.data) @@ -97,6 +98,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { self.setAllowMoving(self.allowMoving.data) self.setAllowZooming(self.allowZooming.data) self.setMinzoom(self.minzoom.data) + self.setBounds(self.bounds.data) this.updateStores() map.on("moveend", () => this.updateStores()) map.on("click", (e) => { @@ -238,18 +240,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { container.style.height = document.documentElement.clientHeight + "px" } - const markerCanvas: HTMLCanvasElement = await html2canvas( + await html2canvas( map.getCanvasContainer(), { backgroundColor: "#00000000", canvas: drawOn, } ) - const markers = await new Promise((resolve) => - markerCanvas.toBlob((data) => resolve(data)) - ) - console.log("Markers:", markers, markerCanvas) - // destinationCtx.drawImage(markerCanvas, 0, 0) } catch (e) { console.error(e) } finally { @@ -429,7 +426,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { private setBounds(bounds: BBox) { const map = this._maplibreMap.data - if (map === undefined) { + if (map === undefined || bounds === undefined) { return } const oldBounds = map.getBounds() diff --git a/UI/Map/MaplibreMap.svelte b/UI/Map/MaplibreMap.svelte index 7ebaa56b2..a3dd0c198 100644 --- a/UI/Map/MaplibreMap.svelte +++ b/UI/Map/MaplibreMap.svelte @@ -24,7 +24,7 @@ $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";
self.update(features)) + features.features.addCallbackAndRunD(() => self.update(features.features)) } private calculatePropsFor( @@ -229,13 +229,23 @@ class LineRenderingLayer { return calculatedProps } - private async update(features: Feature[]) { + private currentSourceData + private async update(featureSource: Store) { const map = this._map while (!map.isStyleLoaded()) { 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 = map.getSource(this._layername) + if (this.currentSourceData === features) { + // Already up to date + return + } if (src === undefined) { + this.currentSourceData = features map.addSource(this._layername, { type: "geojson", data: { @@ -262,7 +272,6 @@ class LineRenderingLayer { }) map.on("click", linelayer, (e) => { - console.log("Click", e) e.originalEvent["consumed"] = true this._onClick(e.features[0]) }) @@ -297,9 +306,10 @@ class LineRenderingLayer { } }) } else { + this.currentSourceData = features src.setData({ type: "FeatureCollection", - features, + features: this.currentSourceData, }) } @@ -345,10 +355,21 @@ export default class ShowDataLayer { "ShowDataLayer.ts:range.json" ) private readonly _map: Store - private readonly _options: ShowDataLayerOptions & { layer: LayerConfig } + private readonly _options: ShowDataLayerOptions & { + layer: LayerConfig + drawMarkers?: true | boolean + drawLines?: true | boolean + } private readonly _popupCache: Map - constructor(map: Store, options: ShowDataLayerOptions & { layer: LayerConfig }) { + constructor( + map: Store, + options: ShowDataLayerOptions & { + layer: LayerConfig + drawMarkers?: true | boolean + drawLines?: true | boolean + } + ) { this._map = map this._options = options this._popupCache = new Map() @@ -405,28 +426,31 @@ export default class ShowDataLayer { selectedElement?.setData(feature) selectedLayer?.setData(this._options.layer) }) - for (let i = 0; i < this._options.layer.lineRendering.length; i++) { - const lineRenderingConfig = this._options.layer.lineRendering[i] - new LineRenderingLayer( - map, - features, - this._options.layer.id + "_linerendering_" + i, - lineRenderingConfig, - doShowLayer, - fetchStore, - onClick - ) + if (this._options.drawLines !== false) { + for (let i = 0; i < this._options.layer.lineRendering.length; i++) { + const lineRenderingConfig = this._options.layer.lineRendering[i] + new LineRenderingLayer( + map, + features, + this._options.layer.id + "_linerendering_" + i, + lineRenderingConfig, + doShowLayer, + fetchStore, + onClick + ) + } } - - for (const pointRenderingConfig of this._options.layer.mapRendering) { - new PointRenderingLayer( - map, - features, - pointRenderingConfig, - doShowLayer, - fetchStore, - onClick - ) + if (this._options.drawMarkers !== false) { + for (const pointRenderingConfig of this._options.layer.mapRendering) { + new PointRenderingLayer( + map, + features, + pointRenderingConfig, + doShowLayer, + fetchStore, + onClick + ) + } } features.features.addCallbackAndRunD((_) => this.zoomToCurrentFeatures(map)) } diff --git a/UI/Popup/AddNewPoint/AddNewPoint.svelte b/UI/Popup/AddNewPoint/AddNewPoint.svelte index 26b397591..fef8dcf20 100644 --- a/UI/Popup/AddNewPoint/AddNewPoint.svelte +++ b/UI/Popup/AddNewPoint/AddNewPoint.svelte @@ -83,19 +83,9 @@ snapOnto: snapToWay }); await state.changes.applyAction(newElementAction); + // The 'changes' should have created a new point, which added this into the 'featureProperties' 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); { // Set some metainfo diff --git a/UI/Popup/CreateNewNote.svelte b/UI/Popup/CreateNewNote.svelte index 882c21dde..c57b763a1 100644 --- a/UI/Popup/CreateNewNote.svelte +++ b/UI/Popup/CreateNewNote.svelte @@ -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.ping(); state.selectedElement?.setData(feature); diff --git a/UI/Popup/LoginButton.ts b/UI/Popup/LoginButton.ts index 93016aa3f..866359469 100644 --- a/UI/Popup/LoginButton.ts +++ b/UI/Popup/LoginButton.ts @@ -5,7 +5,7 @@ import { OsmConnection, OsmServiceState } from "../../Logic/Osm/OsmConnection" import { VariableUiElement } from "../Base/VariableUIElement" import Loading from "../Base/Loading" import Translations from "../i18n/Translations" -import { Store } from "../../Logic/UIEventSource" +import { ImmutableStore, Store } from "../../Logic/UIEventSource" import Combine from "../Base/Combine" import { Translation } from "../i18n/Translation" @@ -13,13 +13,13 @@ class LoginButton extends SubtleButton { constructor( text: BaseUIElement | string, state: { - osmConnection: OsmConnection + osmConnection?: OsmConnection }, icon?: BaseUIElement | string ) { super(icon ?? Svg.login_ui(), text) 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 * * 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( el: BaseUIElement, text: BaseUIElement | string, state: { - readonly osmConnection: OsmConnection - readonly featureSwitchUserbadge: Store + readonly osmConnection?: OsmConnection + readonly featureSwitchUserbadge?: Store } ) { const loading = new Loading("Trying to log in...") @@ -51,14 +54,14 @@ export class LoginToggle extends VariableUiElement { } super( - state.osmConnection.loadingStatus.map( + state.osmConnection?.loadingStatus?.map( (osmConnectionState) => { - if (state.featureSwitchUserbadge.data == false) { + if (state.featureSwitchUserbadge?.data == false) { // All features to login with are disabled return undefined } - const apiState = state.osmConnection.apiIsOnline.data + const apiState = state.osmConnection?.apiIsOnline?.data ?? "online" const apiTranslation = offlineModes[apiState] if (apiTranslation !== undefined) { return new Combine([ @@ -77,15 +80,15 @@ export class LoginToggle extends VariableUiElement { return el } - // Error! + // Fallback return new LoginButton( Translations.t.general.loginFailed, state, Svg.invalid_svg() ) }, - [state.featureSwitchUserbadge, state.osmConnection.apiIsOnline] - ) + [state.featureSwitchUserbadge, state.osmConnection?.apiIsOnline] + ) ?? new ImmutableStore(el) // ) } } diff --git a/UI/Popup/SplitRoadWizard.ts b/UI/Popup/SplitRoadWizard.ts index 8971b0e20..bf3ab9197 100644 --- a/UI/Popup/SplitRoadWizard.ts +++ b/UI/Popup/SplitRoadWizard.ts @@ -2,33 +2,25 @@ import Toggle from "../Input/Toggle" import Svg from "../../Svg" import { UIEventSource } from "../../Logic/UIEventSource" import { SubtleButton } from "../Base/SubtleButton" -import { GeoOperations } from "../../Logic/GeoOperations" import Combine from "../Base/Combine" import { Button } from "../Base/Button" import Translations from "../i18n/Translations" import SplitAction from "../../Logic/Osm/Actions/SplitAction" 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 { VariableUiElement } from "../Base/VariableUIElement" -import ScrollableFullScreen from "../Base/ScrollableFullScreen" 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 { - private static splitLayerStyling = new LayerConfig( - split_point, - "(BUILTIN) SplitRoadWizard.ts", - true - ) - public dialogIsOpened: UIEventSource /** @@ -37,20 +29,34 @@ export default class SplitRoadWizard extends Combine { * @param id: The id of the road to remove * @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 + } + ) { 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 - const splitPoints = new UIEventSource<{ feature: any; freshness: Date }[]>([]) + const splitPoints = new UIEventSource[]>([]) const hasBeenSplit = new UIEventSource(false) // Toggle variable between show split button and map const splitClicked = new UIEventSource(false) - const leafletMap = new UIEventSource( - SplitRoadWizard.setupMapComponent(id, splitPoints, state) - ) + const leafletMap = new UIEventSource(undefined) + + function initMap() { + SplitRoadWizard.setupMapComponent(id, splitPoints).then((mapComponent) => + leafletMap.setData(mapComponent.SetClass("w-full h-80")) + ) + } + initMap() // Toggle between splitmap const splitButton = new SubtleButton( @@ -70,23 +76,19 @@ export default class SplitRoadWizard extends Combine { splitClicked.setData(false) const splitAction = new SplitAction( id, - splitPoints.data.map((ff) => ff.feature.geometry.coordinates), + splitPoints.data.map((ff) => <[number, number]>(ff.geometry).coordinates), { - theme: state?.layoutToUse?.id, + theme: state?.layout?.id, }, - 5, - (coordinates) => { - state.allElements.ContainingFeatures.get(id).geometry["coordinates"] = - coordinates - } + 5 ) - 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 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 - ScrollableFullScreen.collapse() + state.selectedElement?.setData(undefined) }) saveButton.SetClass("btn btn-primary mr-3") @@ -131,95 +133,14 @@ export default class SplitRoadWizard extends Combine { }) } - private static setupMapComponent( - id: string, - splitPoints: UIEventSource<{ feature: any; freshness: Date }[]>, - state: { - filteredLayers: UIEventSource - backgroundLayer: UIEventSource - featureSwitchIsTesting: UIEventSource - featureSwitchIsDebugging: UIEventSource - featureSwitchShowAllQuestions: UIEventSource - osmConnection: OsmConnection - featureSwitchUserbadge: UIEventSource - 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, - }, + private static async setupMapComponent( + id: WayId, + splitPoints: UIEventSource + ): Promise { + const osmWay = await OsmObject.DownloadObjectAsync(id) + return new SvelteUIElement(WaySplitMap, { + osmWay, + splitPoints, }) - 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(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 } } diff --git a/UI/SpecialVisualization.ts b/UI/SpecialVisualization.ts index f249dfc10..c30da739c 100644 --- a/UI/SpecialVisualization.ts +++ b/UI/SpecialVisualization.ts @@ -1,18 +1,18 @@ -import { Store, UIEventSource } from "../Logic/UIEventSource" -import BaseUIElement from "./BaseUIElement" -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" -import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" -import { OsmConnection } from "../Logic/Osm/OsmConnection" -import { Changes } from "../Logic/Osm/Changes" -import { ExportableMap, MapProperties } from "../Models/MapProperties" -import LayerState from "../Logic/State/LayerState" -import { Feature, Geometry } from "geojson" -import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" -import { MangroveIdentity } from "../Logic/Web/MangroveReviews" -import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" -import LayerConfig from "../Models/ThemeConfig/LayerConfig" -import FeatureSwitchState from "../Logic/State/FeatureSwitchState" -import { MenuState } from "../Models/MenuState" +import { Store, UIEventSource } from "../Logic/UIEventSource"; +import BaseUIElement from "./BaseUIElement"; +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; +import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"; +import { OsmConnection } from "../Logic/Osm/OsmConnection"; +import { Changes } from "../Logic/Osm/Changes"; +import { ExportableMap, MapProperties } from "../Models/MapProperties"; +import LayerState from "../Logic/State/LayerState"; +import { Feature, Geometry } from "geojson"; +import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; +import { MangroveIdentity } from "../Logic/Web/MangroveReviews"; +import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"; +import LayerConfig from "../Models/ThemeConfig/LayerConfig"; +import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; +import { MenuState } from "../Models/MenuState"; /** * The state needed to render a special Visualisation. diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 9e1acdf16..3f5d119d6 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -77,8 +77,9 @@ import Lazy from "./Base/Lazy" import { CheckBox } from "./Input/Checkboxes" import Slider from "./Input/Slider" import DeleteWizard from "./Popup/DeleteWizard" -import { OsmId, OsmTags } from "../Models/OsmFeature" +import { OsmId, OsmTags, WayId } from "../Models/OsmFeature" import MoveWizard from "./Popup/MoveWizard" +import SplitRoadWizard from "./Popup/SplitRoadWizard" class NearbyImageVis implements SpecialVisualization { // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests @@ -532,16 +533,17 @@ export default class SpecialVisualizations { args: [], constr( state: SpecialVisualizationState, - tagSource: UIEventSource>, - argument: string[], - feature: Feature, - layer: LayerConfig + tagSource: UIEventSource> ): BaseUIElement { return new VariableUiElement( - // TODO tagSource .map((tags) => tags.id) - .map((id) => new FixedUiElement("TODO: enable splitting")) // new SplitRoadWizard(id, state)) + .map((id) => { + if (id.startsWith("way/")) { + return new SplitRoadWizard(id, state) + } + return undefined + }) ) }, }, diff --git a/assets/layers/split_road/split_road.json b/assets/layers/split_road/split_road.json new file mode 100644 index 000000000..2b15e115f --- /dev/null +++ b/assets/layers/split_road/split_road.json @@ -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" + } + ] +} diff --git a/assets/themes/cyclestreets/cyclestreets.json b/assets/themes/cyclestreets/cyclestreets.json index 7cc4efcb2..04b61a22c 100644 --- a/assets/themes/cyclestreets/cyclestreets.json +++ b/assets/themes/cyclestreets/cyclestreets.json @@ -379,7 +379,7 @@ ], "overrideAll": { "allowSplit": true, - "tagRenderings+": [ + "+tagRenderings": [ { "id": "is_cyclestreet", "question": { @@ -723,4 +723,4 @@ } ] } -} \ No newline at end of file +} diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index dbc66652c..c0df2b84b 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -1061,6 +1061,10 @@ video { height: 10rem; } +.h-80 { + height: 20rem; +} + .max-h-20vh { max-height: 20vh; } @@ -1081,6 +1085,10 @@ video { min-height: 8rem; } +.w-full { + width: 100%; +} + .w-8 { width: 2rem; } @@ -1105,10 +1113,6 @@ video { width: 1.5rem; } -.w-full { - width: 100%; -} - .w-screen { width: 100vw; } diff --git a/test.html b/test.html index bc09c2964..a84eca7e2 100644 --- a/test.html +++ b/test.html @@ -18,7 +18,7 @@ -
'maindiv' not attached
+
'maindiv' not attached
'extradiv' not attached
diff --git a/test.ts b/test.ts index 0b18421a9..b54dbd3aa 100644 --- a/test.ts +++ b/test.ts @@ -9,6 +9,10 @@ import { UIEventSource } from "./Logic/UIEventSource" import { VariableUiElement } from "./UI/Base/VariableUIElement" import { FixedUiElement } from "./UI/Base/FixedUiElement" 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() { const layout = new LayoutConfig(theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data) @@ -41,5 +45,10 @@ function testinput() { } 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()