From 5d0fe31c4192553d5f005403eddb8017c2475134 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 28 Mar 2023 05:13:48 +0200 Subject: [PATCH] refactoring --- Logic/Actors/PendingChangesUploader.ts | 3 +- Logic/Actors/TitleHandler.ts | 20 +- Logic/ContributorCount.ts | 34 +- Logic/ExtraFunctions.ts | 1 - Logic/FeatureSource/Actors/GeoIndexedStore.ts | 41 ++ .../Actors/MetaTagRecalculator.ts | 3 +- .../Actors/SaveFeatureSourceToLocalStorage.ts | 36 ++ .../Actors/SaveTileToLocalStorageActor.ts | 149 ----- .../FeatureSource/Actors/TileLocalStorage.ts | 63 ++ Logic/FeatureSource/FeaturePipeline.ts | 581 ------------------ Logic/FeatureSource/FeatureSource.ts | 5 +- .../PerLayerFeatureSourceSplitter.ts | 64 +- .../Sources/ClippedFeatureSource.ts | 17 + .../Sources/FilteringFeatureSource.ts | 41 +- Logic/FeatureSource/Sources/GeoJsonSource.ts | 2 +- .../{ => Sources}/LayoutSource.ts | 51 +- .../OsmFeatureSource.ts | 3 +- .../Sources}/OverpassFeatureSource.ts | 27 +- .../Sources/RememberingSource.ts | 34 - .../Sources/TouchesBboxFeatureSource.ts | 29 + .../DynamicGeoJsonTileSource.ts | 4 +- .../TiledFeatureSource/DynamicTileSource.ts | 3 +- .../FullNodeDatabaseSource.ts | 11 +- .../LocalStorageFeatureSource.ts | 28 + .../TiledFeatureSource/README.md | 24 - .../TiledFeatureSource/TileHierarchy.ts | 24 - .../TiledFeatureSource/TileHierarchyMerger.ts | 58 -- .../TiledFeatureSource/TiledFeatureSource.ts | 249 -------- Logic/GeoOperations.ts | 118 ++-- .../CreateMultiPolygonWithPointReuseAction.ts | 14 +- .../Actions/CreateWayWithPointReuseAction.ts | 16 +- Logic/Osm/Actions/ReplaceGeometryAction.ts | 12 +- Logic/Osm/Geocoding.ts | 4 + Logic/SimpleMetaTagger.ts | 103 ++-- Logic/State/FeaturePipelineState.ts | 99 +-- Logic/State/LayerState.ts | 87 +-- Logic/State/UserRelatedState.ts | 6 +- Logic/UIEventSource.ts | 33 +- Logic/Web/MangroveReviews.ts | 29 +- Models/FilteredLayer.ts | 92 ++- Models/GlobalFilter.ts | 5 +- Models/MapProperties.ts | 4 +- Models/RasterLayers.ts | 9 + Models/ThemeConfig/FilterConfig.ts | 61 +- Models/ThemeConfig/Json/LayoutConfigJson.ts | 19 - Models/ThemeConfig/LayoutConfig.ts | 20 - Models/ThemeConfig/PointRenderingConfig.ts | 10 +- Models/ThemeViewState.ts | 278 +++++++++ State.ts | 16 - UI/Base/Checkbox.svelte | 13 + UI/Base/Dropdown.svelte | 15 + UI/Base/If.svelte | 11 +- UI/Base/IfNot.svelte | 18 + UI/Base/Loading.svelte | 13 + UI/Base/MapControlButton.svelte | 2 +- UI/Base/ToSvelte.svelte | 27 +- UI/BigComponents/ActionButtons.ts | 6 +- UI/BigComponents/CopyrightPanel.ts | 79 ++- UI/BigComponents/DownloadPanel.ts | 1 - UI/BigComponents/FilterView.ts | 113 +--- UI/BigComponents/Filterview.svelte | 79 +++ UI/BigComponents/GeolocationControl.ts | 10 +- UI/BigComponents/Geosearch.svelte | 94 +++ UI/BigComponents/RightControls.ts | 6 - UI/BigComponents/SelectedElementView.svelte | 75 +++ UI/BigComponents/StatisticsPanel.ts | 77 +-- UI/BigComponents/ThemeIntroductionPanel.ts | 2 + UI/BigComponents/UploadTraceToOsmUI.ts | 2 +- UI/DefaultGUI.ts | 36 -- UI/DefaultGuiState.ts | 40 +- UI/Image/DeleteImage.ts | 6 +- UI/Image/ImageCarousel.ts | 2 +- UI/Image/ImageUploadFlow.ts | 15 +- UI/Input/LengthInput.ts | 53 +- UI/Input/ValidatedTextField.ts | 15 +- UI/Map/MapLibreAdaptor.ts | 58 +- UI/Map/ShowDataLayer.ts | 51 +- UI/Map/ShowDataLayerOptions.ts | 11 +- UI/Map/ShowDataMultiLayer.ts | 39 -- UI/NewPoint/ConfirmLocationOfPoint.ts | 33 +- UI/OpeningHours/OpeningHours.ts | 2 +- UI/OpeningHours/OpeningHoursVisualization.ts | 4 +- UI/Popup/AddNoteCommentViz.ts | 27 +- UI/Popup/AutoApplyButton.ts | 64 +- UI/Popup/CloseNoteButton.ts | 12 +- UI/Popup/ExportAsGpxViz.ts | 13 +- UI/Popup/HistogramViz.ts | 17 +- UI/Popup/ImportButton.ts | 246 ++++---- UI/Popup/LanguageElement.ts | 16 +- UI/Popup/MapillaryLinkVis.ts | 14 +- UI/Popup/MinimapViz.ts | 79 ++- UI/Popup/MultiApply.ts | 18 +- UI/Popup/MultiApplyViz.ts | 10 +- UI/Popup/NearbyImageVis.ts | 19 +- UI/Popup/NearbyImages.ts | 20 +- UI/Popup/PlantNetDetectionViz.ts | 6 +- UI/Popup/ShareLinkViz.ts | 10 +- UI/Popup/StealViz.ts | 8 +- UI/Popup/TagApplyButton.ts | 20 +- UI/Popup/TagRenderingAnswer.svelte | 0 UI/Popup/TagRenderingAnswer.ts | 4 +- UI/Popup/UploadToOsmViz.ts | 14 +- UI/SpecialVisualization.ts | 68 +- UI/SpecialVisualizations.ts | 199 +++--- UI/SubstitutedTranslation.ts | 16 +- UI/ThemeViewGUI.svelte | 275 +++++---- Utils.ts | 30 + css/index-tailwind-output.css | 177 +++--- index.css | 19 - index.ts | 7 - package-lock.json | 159 +---- package.json | 2 + scripts/slice.ts | 145 +++-- test.ts | 8 +- 114 files changed, 2412 insertions(+), 2958 deletions(-) create mode 100644 Logic/FeatureSource/Actors/GeoIndexedStore.ts create mode 100644 Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts delete mode 100644 Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts create mode 100644 Logic/FeatureSource/Actors/TileLocalStorage.ts delete mode 100644 Logic/FeatureSource/FeaturePipeline.ts create mode 100644 Logic/FeatureSource/Sources/ClippedFeatureSource.ts rename Logic/FeatureSource/{ => Sources}/LayoutSource.ts (70%) rename Logic/FeatureSource/{TiledFeatureSource => Sources}/OsmFeatureSource.ts (98%) rename Logic/{Actors => FeatureSource/Sources}/OverpassFeatureSource.ts (88%) delete mode 100644 Logic/FeatureSource/Sources/RememberingSource.ts create mode 100644 Logic/FeatureSource/Sources/TouchesBboxFeatureSource.ts create mode 100644 Logic/FeatureSource/TiledFeatureSource/LocalStorageFeatureSource.ts delete mode 100644 Logic/FeatureSource/TiledFeatureSource/README.md delete mode 100644 Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts delete mode 100644 Logic/FeatureSource/TiledFeatureSource/TileHierarchyMerger.ts delete mode 100644 Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts create mode 100644 Models/ThemeViewState.ts delete mode 100644 State.ts create mode 100644 UI/Base/Checkbox.svelte create mode 100644 UI/Base/Dropdown.svelte create mode 100644 UI/Base/IfNot.svelte create mode 100644 UI/Base/Loading.svelte create mode 100644 UI/BigComponents/Filterview.svelte create mode 100644 UI/BigComponents/Geosearch.svelte create mode 100644 UI/BigComponents/SelectedElementView.svelte delete mode 100644 UI/Map/ShowDataMultiLayer.ts create mode 100644 UI/Popup/TagRenderingAnswer.svelte diff --git a/Logic/Actors/PendingChangesUploader.ts b/Logic/Actors/PendingChangesUploader.ts index cc7ebd436..e71d0bf82 100644 --- a/Logic/Actors/PendingChangesUploader.ts +++ b/Logic/Actors/PendingChangesUploader.ts @@ -2,11 +2,12 @@ import { Changes } from "../Osm/Changes" import Constants from "../../Models/Constants" import { UIEventSource } from "../UIEventSource" import { Utils } from "../../Utils" +import { Feature } from "geojson" export default class PendingChangesUploader { private lastChange: Date - constructor(changes: Changes, selectedFeature: UIEventSource) { + constructor(changes: Changes, selectedFeature: UIEventSource) { const self = this this.lastChange = new Date() changes.pendingChanges.addCallback(() => { diff --git a/Logic/Actors/TitleHandler.ts b/Logic/Actors/TitleHandler.ts index 442158374..f2228fa45 100644 --- a/Logic/Actors/TitleHandler.ts +++ b/Logic/Actors/TitleHandler.ts @@ -2,12 +2,19 @@ import { Store, UIEventSource } from "../UIEventSource" import Locale from "../../UI/i18n/Locale" import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer" import Combine from "../../UI/Base/Combine" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import { ElementStorage } from "../ElementStorage" import { Utils } from "../../Utils" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { Feature } from "geojson" +import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" export default class TitleHandler { - constructor(selectedElement: Store, layout: LayoutConfig, allElements: ElementStorage) { + constructor( + selectedElement: Store, + selectedLayer: Store, + allElements: FeaturePropertiesStore, + layout: LayoutConfig + ) { const currentTitle: Store = selectedElement.map( (selected) => { const defaultTitle = layout?.title?.txt ?? "MapComplete" @@ -17,13 +24,14 @@ export default class TitleHandler { } const tags = selected.properties - for (const layer of layout.layers) { + for (const layer of layout?.layers ?? []) { if (layer.title === undefined) { continue } if (layer.source.osmTags.matchesProperties(tags)) { const tagsSource = - allElements.getEventSourceById(tags.id) ?? new UIEventSource(tags) + allElements.getStore(tags.id) ?? + new UIEventSource>(tags) const title = new TagRenderingAnswer(tagsSource, layer.title, {}) return ( new Combine([defaultTitle, " | ", title]).ConstructElement() @@ -33,7 +41,7 @@ export default class TitleHandler { } return defaultTitle }, - [Locale.language] + [Locale.language, selectedLayer] ) currentTitle.addCallbackAndRunD((title) => { diff --git a/Logic/ContributorCount.ts b/Logic/ContributorCount.ts index ef827585d..d3ee51df4 100644 --- a/Logic/ContributorCount.ts +++ b/Logic/ContributorCount.ts @@ -1,39 +1,31 @@ /// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions import { Store, UIEventSource } from "./UIEventSource" -import FeaturePipeline from "./FeatureSource/FeaturePipeline" -import Loc from "../Models/Loc" import { BBox } from "./BBox" +import GeoIndexedStore from "./FeatureSource/Actors/GeoIndexedStore" export default class ContributorCount { public readonly Contributors: UIEventSource> = new UIEventSource< Map >(new Map()) - private readonly state: { - featurePipeline: FeaturePipeline - currentBounds: Store - locationControl: Store - } + private readonly perLayer: ReadonlyMap private lastUpdate: Date = undefined constructor(state: { - featurePipeline: FeaturePipeline - currentBounds: Store - locationControl: Store + bounds: Store + dataIsLoading: Store + perLayer: ReadonlyMap }) { - this.state = state + this.perLayer = state.perLayer const self = this - state.currentBounds.map((bbox) => { - self.update(bbox) - }) - state.featurePipeline.runningQuery.addCallbackAndRun((_) => - self.update(state.currentBounds.data) + state.bounds.mapD( + (bbox) => { + self.update(bbox) + }, + [state.dataIsLoading] ) } private update(bbox: BBox) { - if (bbox === undefined) { - return - } const now = new Date() if ( this.lastUpdate !== undefined && @@ -42,7 +34,9 @@ export default class ContributorCount { return } this.lastUpdate = now - const featuresList = this.state.featurePipeline.GetAllFeaturesWithin(bbox) + const featuresList = [].concat( + Array.from(this.perLayer.values()).map((fs) => fs.GetFeaturesWithin(bbox)) + ) const hist = new Map() for (const list of featuresList) { for (const feature of list) { diff --git a/Logic/ExtraFunctions.ts b/Logic/ExtraFunctions.ts index da0c9ca0d..f0a7d795b 100644 --- a/Logic/ExtraFunctions.ts +++ b/Logic/ExtraFunctions.ts @@ -1,6 +1,5 @@ import { GeoOperations } from "./GeoOperations" import Combine from "../UI/Base/Combine" -import RelationsTracker from "./Osm/RelationsTracker" import BaseUIElement from "../UI/BaseUIElement" import List from "../UI/Base/List" import Title from "../UI/Base/Title" diff --git a/Logic/FeatureSource/Actors/GeoIndexedStore.ts b/Logic/FeatureSource/Actors/GeoIndexedStore.ts new file mode 100644 index 000000000..ec11a9c70 --- /dev/null +++ b/Logic/FeatureSource/Actors/GeoIndexedStore.ts @@ -0,0 +1,41 @@ +import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource" +import { Feature } from "geojson" +import { BBox } from "../../BBox" +import { GeoOperations } from "../../GeoOperations" +import { Store } from "../../UIEventSource" +import FilteredLayer from "../../../Models/FilteredLayer" + +/** + * Allows the retrieval of all features in the requested BBox; useful for one-shot queries; + * + * Use a ClippedFeatureSource for a continuously updating featuresource + */ +export default class GeoIndexedStore implements FeatureSource { + public features: Store + + constructor(features: FeatureSource | Store) { + this.features = features["features"] ?? features + } + + /** + * Gets the current features within the given bbox. + * + * @param bbox + * @constructor + */ + public GetFeaturesWithin(bbox: BBox): Feature[] { + // TODO optimize + const bboxFeature = bbox.asGeoJson({}) + return this.features.data.filter( + (f) => GeoOperations.intersect(f, bboxFeature) !== undefined + ) + } +} + +export class GeoIndexedStoreForLayer extends GeoIndexedStore implements FeatureSourceForLayer { + readonly layer: FilteredLayer + constructor(features: FeatureSource | Store, layer: FilteredLayer) { + super(features) + this.layer = layer + } +} diff --git a/Logic/FeatureSource/Actors/MetaTagRecalculator.ts b/Logic/FeatureSource/Actors/MetaTagRecalculator.ts index aacf44b50..0cda95578 100644 --- a/Logic/FeatureSource/Actors/MetaTagRecalculator.ts +++ b/Logic/FeatureSource/Actors/MetaTagRecalculator.ts @@ -1,11 +1,10 @@ import { FeatureSourceForLayer, Tiled } from "../FeatureSource" import MetaTagging from "../../MetaTagging" import { ExtraFuncParams } from "../../ExtraFunctions" -import FeaturePipeline from "../FeaturePipeline" import { BBox } from "../../BBox" import { UIEventSource } from "../../UIEventSource" -/**** +/** * Concerned with the logic of updating the right layer at the right time */ class MetatagUpdater { diff --git a/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts b/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts new file mode 100644 index 000000000..29519b6bb --- /dev/null +++ b/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts @@ -0,0 +1,36 @@ +import FeatureSource, { Tiled } from "../FeatureSource" +import { Tiles } from "../../../Models/TileRange" +import { IdbLocalStorage } from "../../Web/IdbLocalStorage" +import { UIEventSource } from "../../UIEventSource" +import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" +import { BBox } from "../../BBox" +import SimpleFeatureSource from "../Sources/SimpleFeatureSource" +import FilteredLayer from "../../../Models/FilteredLayer" +import Loc from "../../../Models/Loc" +import { Feature } from "geojson" +import TileLocalStorage from "./TileLocalStorage" +import { GeoOperations } from "../../GeoOperations" +import { Utils } from "../../../Utils" + +/*** + * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run + * + * The data is saved in a tiled way on a fixed zoomlevel and is retrievable per layer. + * + * Also see the sibling class + */ +export default class SaveFeatureSourceToLocalStorage { + constructor(layername: string, zoomlevel: number, features: FeatureSource) { + const storage = TileLocalStorage.construct(layername) + features.features.addCallbackAndRunD((features) => { + const sliced = GeoOperations.slice(zoomlevel, features) + sliced.forEach((features, tileIndex) => { + const src = storage.getTileSource(tileIndex) + if (Utils.sameList(src.data, features)) { + return + } + src.setData(features) + }) + }) + } +} diff --git a/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts b/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts deleted file mode 100644 index 9cf1b1de5..000000000 --- a/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts +++ /dev/null @@ -1,149 +0,0 @@ -import FeatureSource, { Tiled } from "../FeatureSource" -import { Tiles } from "../../../Models/TileRange" -import { IdbLocalStorage } from "../../Web/IdbLocalStorage" -import { UIEventSource } from "../../UIEventSource" -import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" -import { BBox } from "../../BBox" -import SimpleFeatureSource from "../Sources/SimpleFeatureSource" -import FilteredLayer from "../../../Models/FilteredLayer" -import Loc from "../../../Models/Loc" -import { Feature } from "geojson" - -/*** - * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run - * - * Technically, more an Actor then a featuresource, but it fits more neatly this way - */ -export default class SaveTileToLocalStorageActor { - private readonly visitedTiles: UIEventSource> - private readonly _layer: LayerConfig - private readonly _flayer: FilteredLayer - private readonly initializeTime = new Date() - - constructor(layer: FilteredLayer) { - this._flayer = layer - this._layer = layer.layerDef - this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, { - defaultValue: new Map(), - }) - this.visitedTiles.stabilized(100).addCallbackAndRunD((tiles) => { - for (const key of Array.from(tiles.keys())) { - const tileFreshness = tiles.get(key) - - const toOld = - this.initializeTime.getTime() - tileFreshness.getTime() > - 1000 * this._layer.maxAgeOfCache - if (toOld) { - // Purge this tile - this.SetIdb(key, undefined) - console.debug("Purging tile", this._layer.id, key) - tiles.delete(key) - } - } - this.visitedTiles.ping() - return true - }) - } - - public LoadTilesFromDisk( - currentBounds: UIEventSource, - location: UIEventSource, - registerFreshness: (tileId: number, freshness: Date) => void, - registerTile: (src: FeatureSource & Tiled) => void - ) { - const self = this - const loadedTiles = new Set() - this.visitedTiles.addCallbackD((tiles) => { - if (tiles.size === 0) { - // We don't do anything yet as probably not yet loaded from disk - // We'll unregister later on - return - } - currentBounds.addCallbackAndRunD((bbox) => { - if (self._layer.minzoomVisible > location.data.zoom) { - // Not enough zoom - return - } - - // Iterate over all available keys in the local storage, check which are needed and fresh enough - for (const key of Array.from(tiles.keys())) { - const tileFreshness = tiles.get(key) - if (tileFreshness > self.initializeTime) { - // This tile is loaded by another source - continue - } - - registerFreshness(key, tileFreshness) - const tileBbox = BBox.fromTileIndex(key) - if (!bbox.overlapsWith(tileBbox)) { - continue - } - if (loadedTiles.has(key)) { - // Already loaded earlier - continue - } - loadedTiles.add(key) - this.GetIdb(key).then((features: Feature[]) => { - if (features === undefined) { - return - } - console.debug("Loaded tile " + self._layer.id + "_" + key + " from disk") - const src = new SimpleFeatureSource( - self._flayer, - key, - new UIEventSource(features) - ) - registerTile(src) - }) - } - }) - - return true // Remove the callback - }) - } - - public addTile(tile: FeatureSource & Tiled) { - const self = this - tile.features.addCallbackAndRunD((features) => { - const now = new Date() - - if (features.length > 0) { - self.SetIdb(tile.tileIndex, features) - } - // We _still_ write the time to know that this tile is empty! - this.MarkVisited(tile.tileIndex, now) - }) - } - - public poison(lon: number, lat: number) { - for (let z = 0; z < 25; z++) { - const { x, y } = Tiles.embedded_tile(lat, lon, z) - const tileId = Tiles.tile_index(z, x, y) - this.visitedTiles.data.delete(tileId) - } - } - - public MarkVisited(tileId: number, freshness: Date) { - this.visitedTiles.data.set(tileId, freshness) - this.visitedTiles.ping() - } - - private SetIdb(tileIndex, data) { - try { - IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data) - } catch (e) { - console.error( - "Could not save tile to indexed-db: ", - e, - "tileIndex is:", - tileIndex, - "for layer", - this._layer.id - ) - } - } - - private GetIdb(tileIndex) { - return IdbLocalStorage.GetDirectly(this._layer.id + "_" + tileIndex) - } -} diff --git a/Logic/FeatureSource/Actors/TileLocalStorage.ts b/Logic/FeatureSource/Actors/TileLocalStorage.ts new file mode 100644 index 000000000..642110d4e --- /dev/null +++ b/Logic/FeatureSource/Actors/TileLocalStorage.ts @@ -0,0 +1,63 @@ +import { IdbLocalStorage } from "../../Web/IdbLocalStorage" +import { UIEventSource } from "../../UIEventSource" + +/** + * A class which allows to read/write a tile to local storage. + * + * Does the heavy lifting for LocalStorageFeatureSource and SaveFeatureToLocalStorage + */ +export default class TileLocalStorage { + private static perLayer: Record> = {} + private readonly _layername: string + private readonly cachedSources: Record> = {} + + private constructor(layername: string) { + this._layername = layername + } + + public static construct(layername: string): TileLocalStorage { + const cached = TileLocalStorage.perLayer[layername] + if (cached) { + return cached + } + + const tls = new TileLocalStorage(layername) + TileLocalStorage.perLayer[layername] = tls + return tls + } + + /** + * Constructs a UIEventSource element which is synced with localStorage + * @param layername + * @param tileIndex + */ + public getTileSource(tileIndex: number): UIEventSource { + const cached = this.cachedSources[tileIndex] + if (cached) { + return cached + } + const src = UIEventSource.FromPromise(this.GetIdb(tileIndex)) + src.addCallbackD((data) => this.SetIdb(tileIndex, data)) + this.cachedSources[tileIndex] = src + return src + } + + private SetIdb(tileIndex: number, data): void { + try { + IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data) + } catch (e) { + console.error( + "Could not save tile to indexed-db: ", + e, + "tileIndex is:", + tileIndex, + "for layer", + this._layername + ) + } + } + + private GetIdb(tileIndex: number): Promise { + return IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex) + } +} diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts deleted file mode 100644 index fc7e668d7..000000000 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ /dev/null @@ -1,581 +0,0 @@ -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import FilteringFeatureSource from "./Sources/FilteringFeatureSource" -import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter" -import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "./FeatureSource" -import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource" -import { Store, UIEventSource } from "../UIEventSource" -import { TileHierarchyTools } from "./TiledFeatureSource/TileHierarchy" -import RememberingSource from "./Sources/RememberingSource" -import OverpassFeatureSource from "../Actors/OverpassFeatureSource" -import GeoJsonSource from "./Sources/GeoJsonSource" -import Loc from "../../Models/Loc" -import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor" -import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor" -import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource" -import { TileHierarchyMerger } from "./TiledFeatureSource/TileHierarchyMerger" -import { NewGeometryFromChangesFeatureSource } from "./Sources/NewGeometryFromChangesFeatureSource" -import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator" -/** - * Keeps track of the age of the loaded data. - * Has one freshness-Calculator for every layer - * @private - */ -import { BBox } from "../BBox" -import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource" -import { Tiles } from "../../Models/TileRange" -import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource" -import MapState from "../State/MapState" -import { OsmFeature } from "../../Models/OsmFeature" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import { FilterState } from "../../Models/FilteredLayer" -import { GeoOperations } from "../GeoOperations" -import { Utils } from "../../Utils" - -/** - * The features pipeline ties together a myriad of various datasources: - * - * - The Overpass-API - * - The OSM-API - * - Third-party geojson files, either sliced or directly. - * - * In order to truly understand this class, please have a look at the following diagram: https://cdn-images-1.medium.com/fit/c/800/618/1*qTK1iCtyJUr4zOyw4IFD7A.jpeg - * - * - */ -export default class FeaturePipeline { - public readonly sufficientlyZoomed: Store - public readonly runningQuery: Store - public readonly timeout: UIEventSource - public readonly somethingLoaded: UIEventSource = new UIEventSource(false) - public readonly newDataLoadedSignal: UIEventSource = - new UIEventSource(undefined) - /** - * Keeps track of all raw OSM-nodes. - * Only initialized if `ReplaceGeometryAction` is needed somewhere - */ - public readonly fullNodeDatabase?: FullNodeDatabaseSource - private readonly overpassUpdater: OverpassFeatureSource - private state: MapState - private readonly perLayerHierarchy: Map - private readonly oldestAllowedDate: Date - private readonly osmSourceZoomLevel - private readonly localStorageSavers = new Map() - - private readonly newGeometryHandler: NewGeometryFromChangesFeatureSource - - constructor( - handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, - state: MapState, - options?: { - /*Used for metatagging - will receive all the sources with changeGeometry applied but without filtering*/ - handleRawFeatureSource: (source: FeatureSourceForLayer) => void - } - ) { - this.state = state - - const self = this - const expiryInSeconds = Math.min( - ...(state.layoutToUse?.layers?.map((l) => l.maxAgeOfCache) ?? []) - ) - this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds) - this.osmSourceZoomLevel = state.osmApiTileSize.data - const useOsmApi = state.locationControl.map( - (l) => l.zoom > (state.overpassMaxZoom.data ?? 12) - ) - - state.changes.allChanges.addCallbackAndRun((allChanges) => { - allChanges - .filter((ch) => ch.id < 0 && ch.changes !== undefined) - .map((ch) => ch.changes) - .filter((coor) => coor["lat"] !== undefined && coor["lon"] !== undefined) - .forEach((coor) => { - state.layoutToUse.layers.forEach((l) => - self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"]) - ) - }) - }) - - this.sufficientlyZoomed = state.locationControl.map((location) => { - if (location?.zoom === undefined) { - return false - } - let minzoom = Math.min( - ...state.filteredLayers.data.map((layer) => layer.layerDef.minzoom ?? 18) - ) - return location.zoom >= minzoom - }) - - const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed) - - const perLayerHierarchy = new Map() - this.perLayerHierarchy = perLayerHierarchy - - // Given a tile, wraps it and passes it on to render (handled by 'handleFeatureSource' - function patchedHandleFeatureSource( - src: FeatureSourceForLayer & IndexedFeatureSource & Tiled - ) { - // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile - const withChanges = new ChangeGeometryApplicator(src, state.changes) - const srcFiltered = new FilteringFeatureSource(state, src.tileIndex, withChanges) - - handleFeatureSource(srcFiltered) - if (options?.handleRawFeatureSource) { - options.handleRawFeatureSource(withChanges) - } - self.somethingLoaded.setData(true) - // We do not mark as visited here, this is the responsability of the code near the actual loader (e.g. overpassLoader and OSMApiFeatureLoader) - } - - for (const filteredLayer of state.filteredLayers.data) { - const id = filteredLayer.layerDef.id - const source = filteredLayer.layerDef.source - - const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) => - patchedHandleFeatureSource(tile) - ) - perLayerHierarchy.set(id, hierarchy) - - if (id === "type_node") { - this.fullNodeDatabase = new FullNodeDatabaseSource(filteredLayer, (tile) => { - perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) - tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) - }) - continue - } - - const localTileSaver = new SaveTileToLocalStorageActor(filteredLayer) - this.localStorageSavers.set(filteredLayer.layerDef.id, localTileSaver) - - if (source.geojsonSource === undefined) { - // This is an OSM layer - // We load the cached values and register them - // Getting data from upstream happens a bit lower - localTileSaver.LoadTilesFromDisk( - state.currentBounds, - state.locationControl, - (tileIndex, freshness) => - self.freshnesses.get(id).addTileLoad(tileIndex, freshness), - (tile) => { - console.debug("Loaded tile ", id, tile.tileIndex, "from local cache") - new RegisteringAllFromFeatureSourceActor(tile, state.allElements) - hierarchy.registerTile(tile) - tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) - } - ) - - continue - } - - if (source.geojsonZoomLevel === undefined) { - // This is a 'load everything at once' geojson layer - const src = new GeoJsonSource(filteredLayer) - - if (source.isOsmCacheLayer) { - // We split them up into tiles anyway as it is an OSM source - TiledFeatureSource.createHierarchy(src, { - layer: src.layer, - minZoomLevel: this.osmSourceZoomLevel, - noDuplicates: true, - registerTile: (tile) => { - new RegisteringAllFromFeatureSourceActor(tile, state.allElements) - perLayerHierarchy.get(id).registerTile(tile) - tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) - }, - }) - } else { - new RegisteringAllFromFeatureSourceActor(src, state.allElements) - perLayerHierarchy.get(id).registerTile(src) - src.features.addCallbackAndRunD((_) => self.onNewDataLoaded(src)) - } - } else { - new DynamicGeoJsonTileSource( - filteredLayer, - (tile) => { - new RegisteringAllFromFeatureSourceActor(tile, state.allElements) - perLayerHierarchy.get(id).registerTile(tile) - tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) - }, - state - ) - } - } - - const osmFeatureSource = new OsmFeatureSource({ - isActive: useOsmApi, - neededTiles: neededTilesFromOsm, - handleTile: (tile) => { - new RegisteringAllFromFeatureSourceActor(tile, state.allElements) - if (tile.layer.layerDef.maxAgeOfCache > 0) { - const saver = self.localStorageSavers.get(tile.layer.layerDef.id) - if (saver === undefined) { - console.error( - "No localStorageSaver found for layer ", - tile.layer.layerDef.id - ) - } - saver?.addTile(tile) - } - perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) - tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) - }, - state: state, - markTileVisited: (tileId) => - state.filteredLayers.data.forEach((flayer) => { - const layer = flayer.layerDef - if (layer.maxAgeOfCache > 0) { - const saver = self.localStorageSavers.get(layer.id) - if (saver === undefined) { - console.error("No local storage saver found for ", layer.id) - } else { - saver.MarkVisited(tileId, new Date()) - } - } - self.freshnesses.get(layer.id).addTileLoad(tileId, new Date()) - }), - }) - - if (this.fullNodeDatabase !== undefined) { - osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => - this.fullNodeDatabase.handleOsmJson(osmJson, tileId) - ) - } - - const updater = this.initOverpassUpdater(state, useOsmApi) - this.overpassUpdater = updater - this.timeout = updater.timeout - - // Actually load data from the overpass source - new PerLayerFeatureSourceSplitter( - state.filteredLayers, - (source) => - TiledFeatureSource.createHierarchy(source, { - layer: source.layer, - minZoomLevel: source.layer.layerDef.minzoom, - noDuplicates: true, - maxFeatureCount: state.layoutToUse.clustering.minNeededElements, - maxZoomLevel: state.layoutToUse.clustering.maxZoom, - registerTile: (tile) => { - // We save the tile data for the given layer to local storage - data sourced from overpass - self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile) - perLayerHierarchy - .get(source.layer.layerDef.id) - .registerTile(new RememberingSource(tile)) - tile.features.addCallbackAndRunD((f) => { - if (f.length === 0) { - return - } - self.onNewDataLoaded(tile) - }) - }, - }), - updater, - { - handleLeftovers: (leftOvers) => { - console.warn("Overpass returned a few non-matched features:", leftOvers) - }, - } - ) - - // Also load points/lines that are newly added. - const newGeometry = new NewGeometryFromChangesFeatureSource( - state.changes, - state.allElements, - state.osmConnection._oauth_config.url - ) - this.newGeometryHandler = newGeometry - newGeometry.features.addCallbackAndRun((geometries) => { - console.debug("New geometries are:", geometries) - }) - - new RegisteringAllFromFeatureSourceActor(newGeometry, state.allElements) - // A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next - new PerLayerFeatureSourceSplitter( - state.filteredLayers, - (perLayer) => { - // We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this - perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer) - // AT last, we always apply the metatags whenever possible - perLayer.features.addCallbackAndRunD((_) => { - self.onNewDataLoaded(perLayer) - }) - }, - newGeometry, - { - handleLeftovers: (leftOvers) => { - console.warn("Got some leftovers from the filteredLayers: ", leftOvers) - }, - } - ) - - this.runningQuery = updater.runningQuery.map( - (overpass) => { - console.log( - "FeaturePipeline: runningQuery state changed: Overpass", - overpass ? "is querying," : "is idle,", - "osmFeatureSource is", - osmFeatureSource.isRunning - ? "is running and needs " + - neededTilesFromOsm.data?.length + - " tiles (already got " + - osmFeatureSource.downloadedTiles.size + - " tiles )" - : "is idle" - ) - return overpass || osmFeatureSource.isRunning.data - }, - [osmFeatureSource.isRunning] - ) - } - - public GetAllFeaturesWithin(bbox: BBox): OsmFeature[][] { - const self = this - const tiles: OsmFeature[][] = [] - Array.from(this.perLayerHierarchy.keys()).forEach((key) => { - const fetched: OsmFeature[][] = self.GetFeaturesWithin(key, bbox) - tiles.push(...fetched) - }) - return tiles - } - - public GetAllFeaturesAndMetaWithin( - bbox: BBox, - layerIdWhitelist?: Set - ): { features: OsmFeature[]; layer: string }[] { - const self = this - const tiles: { features: any[]; layer: string }[] = [] - Array.from(this.perLayerHierarchy.keys()).forEach((key) => { - if (layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)) { - return - } - return tiles.push({ - layer: key, - features: [].concat(...self.GetFeaturesWithin(key, bbox)), - }) - }) - return tiles - } - - /** - * Gets all the tiles which overlap with the given BBOX. - * This might imply that extra features might be shown - */ - public GetFeaturesWithin(layerId: string, bbox: BBox): OsmFeature[][] { - if (layerId === "*") { - return this.GetAllFeaturesWithin(bbox) - } - const requestedHierarchy = this.perLayerHierarchy.get(layerId) - if (requestedHierarchy === undefined) { - console.warn( - "Layer ", - layerId, - "is not defined. Try one of ", - Array.from(this.perLayerHierarchy.keys()) - ) - return undefined - } - return TileHierarchyTools.getTiles(requestedHierarchy, bbox) - .filter((featureSource) => featureSource.features?.data !== undefined) - .map((featureSource) => featureSource.features.data) - } - - public GetTilesPerLayerWithin( - bbox: BBox, - handleTile: (tile: FeatureSourceForLayer & Tiled) => void - ) { - Array.from(this.perLayerHierarchy.values()).forEach((hierarchy) => { - TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile) - }) - } - - private onNewDataLoaded(src: FeatureSource) { - this.newDataLoadedSignal.setData(src) - } - - private freshnessForVisibleLayers(z: number, x: number, y: number): Date { - let oldestDate = undefined - for (const flayer of this.state.filteredLayers.data) { - if (!flayer.isDisplayed.data && !flayer.layerDef.forceLoad) { - continue - } - if (this.state.locationControl.data.zoom < flayer.layerDef.minzoom) { - continue - } - if (flayer.layerDef.maxAgeOfCache === 0) { - return undefined - } - const freshnessCalc = this.freshnesses.get(flayer.layerDef.id) - if (freshnessCalc === undefined) { - console.warn("No freshness tracker found for ", flayer.layerDef.id) - return undefined - } - const freshness = freshnessCalc.freshnessFor(z, x, y) - if (freshness === undefined) { - // SOmething is undefined --> we return undefined as we have to download - return undefined - } - if (oldestDate === undefined || oldestDate > freshness) { - oldestDate = freshness - } - } - return oldestDate - } - - /* - * Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM - * */ - private getNeededTilesFromOsm(isSufficientlyZoomed: Store): Store { - const self = this - return this.state.currentBounds.map( - (bbox) => { - if (bbox === undefined) { - return [] - } - if (!isSufficientlyZoomed.data) { - return [] - } - const osmSourceZoomLevel = self.osmSourceZoomLevel - const range = bbox.containingTileRange(osmSourceZoomLevel) - const tileIndexes = [] - if (range.total >= 100) { - // Too much tiles! - return undefined - } - Tiles.MapRange(range, (x, y) => { - const i = Tiles.tile_index(osmSourceZoomLevel, x, y) - const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y) - if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) { - console.debug( - "Skipping tile", - osmSourceZoomLevel, - x, - y, - "as a decently fresh one is available" - ) - // The cached tiles contain decently fresh data - return undefined - } - tileIndexes.push(i) - }) - return tileIndexes - }, - [isSufficientlyZoomed] - ) - } - - private initOverpassUpdater( - state: { - layoutToUse: LayoutConfig - currentBounds: Store - locationControl: Store - readonly overpassUrl: Store - readonly overpassTimeout: Store - readonly overpassMaxZoom: Store - }, - useOsmApi: Store - ): OverpassFeatureSource { - const minzoom = Math.min(...state.layoutToUse.layers.map((layer) => layer.minzoom)) - const overpassIsActive = state.currentBounds.map( - (bbox) => { - if (bbox === undefined) { - console.debug("Disabling overpass source: no bbox") - return false - } - let zoom = state.locationControl.data.zoom - if (zoom < minzoom) { - // We are zoomed out over the zoomlevel of any layer - console.debug("Disabling overpass source: zoom < minzoom") - return false - } - - const range = bbox.containingTileRange(zoom) - if (range.total >= 5000) { - // Let's assume we don't have so much data cached - return true - } - const self = this - const allFreshnesses = Tiles.MapRange(range, (x, y) => - self.freshnessForVisibleLayers(zoom, x, y) - ) - return allFreshnesses.some( - (freshness) => freshness === undefined || freshness < this.oldestAllowedDate - ) - }, - [state.locationControl] - ) - - return new OverpassFeatureSource(state, { - padToTiles: state.locationControl.map((l) => Math.min(15, l.zoom + 1)), - isActive: useOsmApi.map((b) => !b && overpassIsActive.data, [overpassIsActive]), - }) - } - - /** - * Builds upon 'GetAllFeaturesAndMetaWithin', but does stricter BBOX-checking and applies the filters - */ - public getAllVisibleElementsWithmeta( - bbox: BBox - ): { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] { - if (bbox === undefined) { - console.warn("No bbox") - return [] - } - - const layers = Utils.toIdRecord(this.state.layoutToUse.layers) - const elementsWithMeta: { features: OsmFeature[]; layer: string }[] = - this.GetAllFeaturesAndMetaWithin(bbox) - - let elements: { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] = [] - let seenElements = new Set() - for (const elementsWithMetaElement of elementsWithMeta) { - const layer = layers[elementsWithMetaElement.layer] - if (layer.title === undefined) { - continue - } - const filtered = this.state.filteredLayers.data.find((fl) => fl.layerDef == layer) - for (let i = 0; i < elementsWithMetaElement.features.length; i++) { - const element = elementsWithMetaElement.features[i] - if (!filtered.isDisplayed.data) { - continue - } - if (seenElements.has(element.properties.id)) { - continue - } - seenElements.add(element.properties.id) - if (!bbox.overlapsWith(BBox.get(element))) { - continue - } - if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) { - continue - } - const activeFilters: FilterState[] = Array.from( - filtered.appliedFilters.data.values() - ) - if ( - !activeFilters.every( - (filter) => - filter?.currentFilter === undefined || - filter?.currentFilter?.matchesProperties(element.properties) - ) - ) { - continue - } - const center = GeoOperations.centerpointCoordinates(element) - elements.push({ - element, - center, - layer: layers[elementsWithMetaElement.layer], - }) - } - } - - return elements - } - - /** - * Inject a new point - */ - InjectNewPoint(geojson) { - this.newGeometryHandler.features.data.push(geojson) - this.newGeometryHandler.features.ping() - } -} diff --git a/Logic/FeatureSource/FeatureSource.ts b/Logic/FeatureSource/FeatureSource.ts index 038132f48..f55cd9592 100644 --- a/Logic/FeatureSource/FeatureSource.ts +++ b/Logic/FeatureSource/FeatureSource.ts @@ -1,4 +1,4 @@ -import { Store } from "../UIEventSource" +import { Store, UIEventSource } from "../UIEventSource" import FilteredLayer from "../../Models/FilteredLayer" import { BBox } from "../BBox" import { Feature } from "geojson" @@ -6,6 +6,9 @@ import { Feature } from "geojson" export default interface FeatureSource { features: Store } +export interface WritableFeatureSource extends FeatureSource { + features: UIEventSource +} export interface Tiled { tileIndex: number diff --git a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts index a2d17b538..05a5d8f32 100644 --- a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts +++ b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts @@ -1,48 +1,59 @@ -import FeatureSource from "./FeatureSource" -import { Store } from "../UIEventSource" +import FeatureSource, { FeatureSourceForLayer } from "./FeatureSource" import FilteredLayer from "../../Models/FilteredLayer" import SimpleFeatureSource from "./Sources/SimpleFeatureSource" import { Feature } from "geojson" +import { Utils } from "../../Utils" +import { UIEventSource } from "../UIEventSource" /** * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) * If this is the case, multiple objects with a different _matching_layer_id are generated. * In any case, this featureSource marks the objects with _matching_layer_id */ -export default class PerLayerFeatureSourceSplitter { +export default class PerLayerFeatureSourceSplitter< + T extends FeatureSourceForLayer = SimpleFeatureSource +> { + public readonly perLayer: ReadonlyMap constructor( - layers: Store, - handleLayerData: (source: FeatureSource, layer: FilteredLayer) => void, + layers: FilteredLayer[], upstream: FeatureSource, options?: { - tileIndex?: number + constructStore?: (features: UIEventSource, layer: FilteredLayer) => T handleLeftovers?: (featuresWithoutLayer: any[]) => void } ) { - const knownLayers = new Map() + const knownLayers = new Map() + this.perLayer = knownLayers + const layerSources = new Map>() - function update() { - const features = upstream.features?.data + const constructStore = + options?.constructStore ?? ((store, layer) => new SimpleFeatureSource(layer, store)) + for (const layer of layers) { + const src = new UIEventSource([]) + layerSources.set(layer.layerDef.id, src) + knownLayers.set(layer.layerDef.id, constructStore(src, layer)) + } + + upstream.features.addCallbackAndRunD((features) => { if (features === undefined) { return } - if (layers.data === undefined || layers.data.length === 0) { + if (layers === undefined) { return } // We try to figure out (for each feature) in which feature store it should be saved. - // Note that this splitter is only run when it is invoked by the overpass feature source, so we can't be sure in which layer it should go const featuresPerLayer = new Map() - const noLayerFound = [] + const noLayerFound: Feature[] = [] - for (const layer of layers.data) { + for (const layer of layers) { featuresPerLayer.set(layer.layerDef.id, []) } for (const f of features) { let foundALayer = false - for (const layer of layers.data) { + for (const layer of layers) { if (layer.layerDef.source.osmTags.matchesProperties(f.properties)) { // We have found our matching layer! featuresPerLayer.get(layer.layerDef.id).push(f) @@ -60,7 +71,7 @@ export default class PerLayerFeatureSourceSplitter { // At this point, we have our features per layer as a list // We assign them to the correct featureSources - for (const layer of layers.data) { + for (const layer of layers) { const id = layer.layerDef.id const features = featuresPerLayer.get(id) if (features === undefined) { @@ -68,25 +79,24 @@ export default class PerLayerFeatureSourceSplitter { continue } - let featureSource = knownLayers.get(id) - if (featureSource === undefined) { - // Not yet initialized - now is a good time - featureSource = new SimpleFeatureSource(layer) - featureSource.features.setData(features) - knownLayers.set(id, featureSource) - handleLayerData(featureSource, layer) - } else { - featureSource.features.setData(features) + const src = layerSources.get(id) + + if (Utils.sameList(src.data, features)) { + return } + src.setData(features) } // AT last, the leftovers are handled if (options?.handleLeftovers !== undefined && noLayerFound.length > 0) { options.handleLeftovers(noLayerFound) } - } + }) + } - layers.addCallback((_) => update()) - upstream.features.addCallbackAndRunD((_) => update()) + public forEach(f: (featureSource: FeatureSourceForLayer) => void) { + for (const fs of this.perLayer.values()) { + f(fs) + } } } diff --git a/Logic/FeatureSource/Sources/ClippedFeatureSource.ts b/Logic/FeatureSource/Sources/ClippedFeatureSource.ts new file mode 100644 index 000000000..f382a594c --- /dev/null +++ b/Logic/FeatureSource/Sources/ClippedFeatureSource.ts @@ -0,0 +1,17 @@ +import FeatureSource from "../FeatureSource" +import { Feature, Polygon } from "geojson" +import StaticFeatureSource from "./StaticFeatureSource" +import { GeoOperations } from "../../GeoOperations" + +/** + * Returns a clipped version of the original geojson. Ways which partially intersect the given feature will be split up + */ +export default class ClippedFeatureSource extends StaticFeatureSource { + constructor(features: FeatureSource, clipTo: Feature) { + super( + features.features.mapD((features) => { + return [].concat(features.map((feature) => GeoOperations.clipWith(feature, clipTo))) + }) + ) + } +} diff --git a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts index dcbbef71e..383f70c62 100644 --- a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts @@ -1,15 +1,15 @@ import { Store, UIEventSource } from "../../UIEventSource" -import FilteredLayer, { FilterState } from "../../../Models/FilteredLayer" +import FilteredLayer from "../../../Models/FilteredLayer" import FeatureSource from "../FeatureSource" import { TagsFilter } from "../../Tags/TagsFilter" import { Feature } from "geojson" -import { OsmTags } from "../../../Models/OsmFeature" +import { GlobalFilter } from "../../../Models/GlobalFilter" export default class FilteringFeatureSource implements FeatureSource { public features: UIEventSource = new UIEventSource([]) private readonly upstream: FeatureSource - private readonly _fetchStore?: (id: String) => Store - private readonly _globalFilters?: Store<{ filter: FilterState }[]> + private readonly _fetchStore?: (id: string) => Store> + private readonly _globalFilters?: Store private readonly _alreadyRegistered = new Set>() private readonly _is_dirty = new UIEventSource(false) private readonly _layer: FilteredLayer @@ -18,8 +18,8 @@ export default class FilteringFeatureSource implements FeatureSource { constructor( layer: FilteredLayer, upstream: FeatureSource, - fetchStore?: (id: String) => Store, - globalFilters?: Store<{ filter: FilterState }[]>, + fetchStore?: (id: string) => Store>, + globalFilters?: Store, metataggingUpdated?: Store ) { this.upstream = upstream @@ -32,9 +32,11 @@ export default class FilteringFeatureSource implements FeatureSource { self.update() }) - layer.appliedFilters.addCallback((_) => { - self.update() - }) + layer.appliedFilters.forEach((value) => + value.addCallback((_) => { + self.update() + }) + ) this._is_dirty.stabilized(1000).addCallbackAndRunD((dirty) => { if (dirty) { @@ -58,7 +60,7 @@ export default class FilteringFeatureSource implements FeatureSource { const layer = this._layer const features: Feature[] = this.upstream.features.data ?? [] const includedFeatureIds = new Set() - const globalFilters = self._globalFilters?.data?.map((f) => f.filter) + const globalFilters = self._globalFilters?.data?.map((f) => f) const newFeatures = (features ?? []).filter((f) => { self.registerCallback(f) @@ -71,19 +73,26 @@ export default class FilteringFeatureSource implements FeatureSource { return false } - const tagsFilter = Array.from(layer.appliedFilters?.data?.values() ?? []) - for (const filter of tagsFilter) { - const neededTags: TagsFilter = filter?.currentFilter + for (const filter of layer.layerDef.filters) { + const state = layer.appliedFilters.get(filter.id).data + if (state === undefined) { + continue + } + let neededTags: TagsFilter + if (typeof state === "string") { + // This filter uses fields + } else { + neededTags = filter.options[state].osmTags + } if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) { // Hidden by the filter on the layer itself - we want to hide it no matter what return false } } - for (const filter of globalFilters ?? []) { - const neededTags: TagsFilter = filter?.currentFilter + for (const globalFilter of globalFilters ?? []) { + const neededTags = globalFilter.osmTags if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) { - // Hidden by the filter on the layer itself - we want to hide it no matter what return false } } diff --git a/Logic/FeatureSource/Sources/GeoJsonSource.ts b/Logic/FeatureSource/Sources/GeoJsonSource.ts index 30e4fa604..82897cf22 100644 --- a/Logic/FeatureSource/Sources/GeoJsonSource.ts +++ b/Logic/FeatureSource/Sources/GeoJsonSource.ts @@ -58,7 +58,7 @@ export default class GeoJsonSource implements FeatureSource { .replace("{x_max}", "" + bounds.maxLon) } - const eventsource = new UIEventSource(undefined) + const eventsource = new UIEventSource([]) if (options?.isActive !== undefined) { options.isActive.addCallbackAndRunD(async (active) => { if (!active) { diff --git a/Logic/FeatureSource/LayoutSource.ts b/Logic/FeatureSource/Sources/LayoutSource.ts similarity index 70% rename from Logic/FeatureSource/LayoutSource.ts rename to Logic/FeatureSource/Sources/LayoutSource.ts index b4d62b49a..7e7f43fb6 100644 --- a/Logic/FeatureSource/LayoutSource.ts +++ b/Logic/FeatureSource/Sources/LayoutSource.ts @@ -1,14 +1,15 @@ -import FeatureSource from "./FeatureSource" -import { Store } from "../UIEventSource" -import FeatureSwitchState from "../State/FeatureSwitchState" -import OverpassFeatureSource from "../Actors/OverpassFeatureSource" -import { BBox } from "../BBox" -import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource" -import { Or } from "../Tags/Or" -import FeatureSourceMerger from "./Sources/FeatureSourceMerger" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import GeoJsonSource from "./Sources/GeoJsonSource" -import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource" +import GeoJsonSource from "./GeoJsonSource" +import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" +import FeatureSource from "../FeatureSource" +import { Or } from "../../Tags/Or" +import FeatureSwitchState from "../../State/FeatureSwitchState" +import OverpassFeatureSource from "./OverpassFeatureSource" +import { Store } from "../../UIEventSource" +import OsmFeatureSource from "./OsmFeatureSource" +import FeatureSourceMerger from "./FeatureSourceMerger" +import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource" +import { BBox } from "../../BBox" +import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource" /** * This source will fetch the needed data from various sources for the given layout. @@ -17,22 +18,24 @@ import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSou */ export default class LayoutSource extends FeatureSourceMerger { constructor( - filteredLayers: LayerConfig[], + layers: LayerConfig[], featureSwitches: FeatureSwitchState, newAndChangedElements: FeatureSource, mapProperties: { bounds: Store; zoom: Store }, backend: string, - isLayerActive: (id: string) => Store + isDisplayed: (id: string) => Store ) { const { bounds, zoom } = mapProperties // remove all 'special' layers - filteredLayers = filteredLayers.filter((flayer) => flayer.source !== null) + layers = layers.filter((flayer) => flayer.source !== null) - const geojsonlayers = filteredLayers.filter( - (flayer) => flayer.source.geojsonSource !== undefined - ) - const osmLayers = filteredLayers.filter( - (flayer) => flayer.source.geojsonSource === undefined + const geojsonlayers = layers.filter((layer) => layer.source.geojsonSource !== undefined) + const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined) + const fromCache = osmLayers.map( + (l) => + new LocalStorageFeatureSource(l.id, 15, mapProperties, { + isActive: isDisplayed(l.id), + }) ) const overpassSource = LayoutSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches) const osmApiSource = LayoutSource.setupOsmApiSource( @@ -43,11 +46,11 @@ export default class LayoutSource extends FeatureSourceMerger { featureSwitches ) const geojsonSources: FeatureSource[] = geojsonlayers.map((l) => - LayoutSource.setupGeojsonSource(l, mapProperties) + LayoutSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id)) ) - const expiryInSeconds = Math.min(...(filteredLayers?.map((l) => l.maxAgeOfCache) ?? [])) - super(overpassSource, osmApiSource, newAndChangedElements, ...geojsonSources) + const expiryInSeconds = Math.min(...(layers?.map((l) => l.maxAgeOfCache) ?? [])) + super(overpassSource, osmApiSource, newAndChangedElements, ...geojsonSources, ...fromCache) } private static setupGeojsonSource( @@ -56,6 +59,10 @@ export default class LayoutSource extends FeatureSourceMerger { isActive?: Store ): FeatureSource { const source = layer.source + isActive = mapProperties.zoom.map( + (z) => (isActive?.data ?? true) && z >= layer.maxzoom, + [isActive] + ) if (source.geojsonZoomLevel === undefined) { // This is a 'load everything at once' geojson layer return new GeoJsonSource(layer, { isActive }) diff --git a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts b/Logic/FeatureSource/Sources/OsmFeatureSource.ts similarity index 98% rename from Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts rename to Logic/FeatureSource/Sources/OsmFeatureSource.ts index 2df335811..e55db7e94 100644 --- a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts +++ b/Logic/FeatureSource/Sources/OsmFeatureSource.ts @@ -108,6 +108,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger { } private async LoadTile(z, x, y): Promise { + console.log("OsmFeatureSource: loading ", z, x, y) if (z >= 22) { throw "This is an absurd high zoom level" } @@ -126,7 +127,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger { let error = undefined try { - const osmJson = await Utils.downloadJson(url) + const osmJson = await Utils.downloadJsonCached(url, 2000) try { this.rawDataHandlers.forEach((handler) => handler(osmJson, Tiles.tile_index(z, x, y)) diff --git a/Logic/Actors/OverpassFeatureSource.ts b/Logic/FeatureSource/Sources/OverpassFeatureSource.ts similarity index 88% rename from Logic/Actors/OverpassFeatureSource.ts rename to Logic/FeatureSource/Sources/OverpassFeatureSource.ts index acccf872e..5e13f02cc 100644 --- a/Logic/Actors/OverpassFeatureSource.ts +++ b/Logic/FeatureSource/Sources/OverpassFeatureSource.ts @@ -1,13 +1,13 @@ -import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" -import { Or } from "../Tags/Or" -import { Overpass } from "../Osm/Overpass" -import FeatureSource from "../FeatureSource/FeatureSource" -import { Utils } from "../../Utils" -import { TagsFilter } from "../Tags/TagsFilter" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import { BBox } from "../BBox" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import { Feature } from "geojson" +import FeatureSource from "../FeatureSource" +import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" +import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" +import { Or } from "../../Tags/Or" +import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" +import { Overpass } from "../../Osm/Overpass" +import { Utils } from "../../../Utils" +import { TagsFilter } from "../../Tags/TagsFilter" +import { BBox } from "../../BBox" /** * A wrapper around the 'Overpass'-object. @@ -99,7 +99,11 @@ export default class OverpassFeatureSource implements FeatureSource { ) { return undefined } - const [bounds, date, updatedLayers] = await this.updateAsync() + const result = await this.updateAsync() + if (!result) { + return + } + const [bounds, date, updatedLayers] = result this._lastQueryBBox = bounds } @@ -188,6 +192,9 @@ export default class OverpassFeatureSource implements FeatureSource { if (data === undefined) { return undefined } + // Some metatags are delivered by overpass _without_ underscore-prefix; we fix them below + // TODO FIXME re-enable this data.features.forEach((f) => SimpleMetaTaggers.objectMetaInfo.applyMetaTagsOnFeature(f)) + self.features.setData(data.features) return [bounds, date, layersToDownload] } catch (e) { diff --git a/Logic/FeatureSource/Sources/RememberingSource.ts b/Logic/FeatureSource/Sources/RememberingSource.ts deleted file mode 100644 index fcc4b7f01..000000000 --- a/Logic/FeatureSource/Sources/RememberingSource.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Every previously added point is remembered, but new points are added. - * Data coming from upstream will always overwrite a previous value - */ -import FeatureSource, { Tiled } from "../FeatureSource" -import { Store, UIEventSource } from "../../UIEventSource" -import { BBox } from "../../BBox" -import { Feature } from "geojson" - -export default class RememberingSource implements FeatureSource, Tiled { - public readonly features: Store - public readonly tileIndex: number - public readonly bbox: BBox - - constructor(source: FeatureSource & Tiled) { - const self = this - this.tileIndex = source.tileIndex - this.bbox = source.bbox - - const empty = [] - const featureSource = new UIEventSource(empty) - this.features = featureSource - source.features.addCallbackAndRunD((features) => { - const oldFeatures = self.features?.data ?? empty - // Then new ids - const ids = new Set(features.map((f) => f.properties.id + f.geometry.type)) - // the old data - const oldData = oldFeatures.filter( - (old) => !ids.has(old.feature.properties.id + old.feature.geometry.type) - ) - featureSource.setData([...features, ...oldData]) - }) - } -} diff --git a/Logic/FeatureSource/Sources/TouchesBboxFeatureSource.ts b/Logic/FeatureSource/Sources/TouchesBboxFeatureSource.ts new file mode 100644 index 000000000..2fb3fd0a2 --- /dev/null +++ b/Logic/FeatureSource/Sources/TouchesBboxFeatureSource.ts @@ -0,0 +1,29 @@ +import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource" +import StaticFeatureSource from "./StaticFeatureSource" +import { GeoOperations } from "../../GeoOperations" +import { BBox } from "../../BBox" +import exp from "constants" +import FilteredLayer from "../../../Models/FilteredLayer" + +/** + * Results in a feature source which has all the elements that touch the given features + */ +export default class BBoxFeatureSource extends StaticFeatureSource { + constructor(features: FeatureSource, mustTouch: BBox) { + const bbox = mustTouch.asGeoJson({}) + super( + features.features.mapD((features) => + features.filter((feature) => GeoOperations.intersect(feature, bbox) !== undefined) + ) + ) + } +} + +export class BBoxFeatureSourceForLayer extends BBoxFeatureSource implements FeatureSourceForLayer { + constructor(features: FeatureSourceForLayer, mustTouch: BBox) { + super(features, mustTouch) + this.layer = features.layer + } + + readonly layer: FilteredLayer +} diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts index 30ed0cb37..cb787289c 100644 --- a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts @@ -84,7 +84,9 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { }) }, mapProperties, - { isActive: options.isActive } + { + isActive: options?.isActive, + } ) } } diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts index 3b3953396..65c249984 100644 --- a/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts @@ -5,7 +5,8 @@ import FeatureSource from "../FeatureSource" import FeatureSourceMerger from "../Sources/FeatureSourceMerger" /*** - * A tiled source which dynamically loads the required tiles at a fixed zoom level + * A tiled source which dynamically loads the required tiles at a fixed zoom level. + * A single featureSource will be initiliased for every tile in view; which will alter be merged into this featureSource */ export default class DynamicTileSource extends FeatureSourceMerger { constructor( diff --git a/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts b/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts index 13225ed15..a1f35ac5b 100644 --- a/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts @@ -1,11 +1,13 @@ -import TileHierarchy from "./TileHierarchy" import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource" import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject" import SimpleFeatureSource from "../Sources/SimpleFeatureSource" import FilteredLayer from "../../../Models/FilteredLayer" import { UIEventSource } from "../../UIEventSource" +import { OsmTags } from "../../../Models/OsmFeature"; +import { BBox } from "../../BBox"; +import { Feature, Point } from "geojson"; -export default class FullNodeDatabaseSource implements TileHierarchy { +export default class FullNodeDatabaseSource { public readonly loadedTiles = new Map() private readonly onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void private readonly layer: FilteredLayer @@ -81,4 +83,9 @@ export default class FullNodeDatabaseSource implements TileHierarchy { return this.parentWays.get(nodeId) } + + getNodesWithin(bBox: BBox) : Feature[]{ + // TODO + throw "TODO" + } } diff --git a/Logic/FeatureSource/TiledFeatureSource/LocalStorageFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/LocalStorageFeatureSource.ts new file mode 100644 index 000000000..96bbb5275 --- /dev/null +++ b/Logic/FeatureSource/TiledFeatureSource/LocalStorageFeatureSource.ts @@ -0,0 +1,28 @@ +import DynamicTileSource from "./DynamicTileSource" +import { Store } from "../../UIEventSource" +import { BBox } from "../../BBox" +import TileLocalStorage from "../Actors/TileLocalStorage" +import { Feature } from "geojson" +import StaticFeatureSource from "../Sources/StaticFeatureSource" + +export default class LocalStorageFeatureSource extends DynamicTileSource { + constructor( + layername: string, + zoomlevel: number, + mapProperties: { + bounds: Store + zoom: Store + }, + options?: { + isActive?: Store + } + ) { + const storage = TileLocalStorage.construct(layername) + super( + zoomlevel, + (tileIndex) => new StaticFeatureSource(storage.getTileSource(tileIndex)), + mapProperties, + options + ) + } +} diff --git a/Logic/FeatureSource/TiledFeatureSource/README.md b/Logic/FeatureSource/TiledFeatureSource/README.md deleted file mode 100644 index 914c1caf7..000000000 --- a/Logic/FeatureSource/TiledFeatureSource/README.md +++ /dev/null @@ -1,24 +0,0 @@ -Data in MapComplete can come from multiple sources. - -Currently, they are: - -- The Overpass-API -- The OSM-API -- One or more GeoJSON files. This can be a single file or a set of tiled geojson files -- LocalStorage, containing features from a previous visit -- Changes made by the user introducing new features - -When the data enters from Overpass or from the OSM-API, they are first distributed per layer: - -OVERPASS | ---PerLayerFeatureSource---> FeatureSourceForLayer[] -OSM | - -The GeoJSon files (not tiled) are then added to this list - -A single FeatureSourcePerLayer is then further handled by splitting it into a tile hierarchy. - -In order to keep thins snappy, they are distributed over a tiled database per layer. - -## Notes - -`cached-featuresbookcases` is the old key used `cahced-features{themeid}` and should be cleaned up \ No newline at end of file diff --git a/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts b/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts deleted file mode 100644 index c318fe5e1..000000000 --- a/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts +++ /dev/null @@ -1,24 +0,0 @@ -import FeatureSource, { Tiled } from "../FeatureSource" -import { BBox } from "../../BBox" - -export default interface TileHierarchy { - /** - * A mapping from 'tile_index' to the actual tile featrues - */ - loadedTiles: Map -} - -export class TileHierarchyTools { - public static getTiles( - hierarchy: TileHierarchy, - bbox: BBox - ): T[] { - const result: T[] = [] - hierarchy.loadedTiles.forEach((tile) => { - if (tile.bbox.overlapsWith(bbox)) { - result.push(tile) - } - }) - return result - } -} diff --git a/Logic/FeatureSource/TiledFeatureSource/TileHierarchyMerger.ts b/Logic/FeatureSource/TiledFeatureSource/TileHierarchyMerger.ts deleted file mode 100644 index b4de5333d..000000000 --- a/Logic/FeatureSource/TiledFeatureSource/TileHierarchyMerger.ts +++ /dev/null @@ -1,58 +0,0 @@ -import TileHierarchy from "./TileHierarchy" -import { UIEventSource } from "../../UIEventSource" -import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" -import FilteredLayer from "../../../Models/FilteredLayer" -import FeatureSourceMerger from "../Sources/FeatureSourceMerger" -import { Tiles } from "../../../Models/TileRange" -import { BBox } from "../../BBox" - -export class TileHierarchyMerger implements TileHierarchy { - public readonly loadedTiles: Map = new Map< - number, - FeatureSourceForLayer & Tiled - >() - public readonly layer: FilteredLayer - private readonly sources: Map> = new Map< - number, - UIEventSource - >() - private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void - - constructor( - layer: FilteredLayer, - handleTile: ( - src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, - index: number - ) => void - ) { - this.layer = layer - this._handleTile = handleTile - } - - /** - * Add another feature source for the given tile. - * Entries for this tile will be merged - * @param src - */ - public registerTile(src: FeatureSource & Tiled) { - const index = src.tileIndex - if (this.sources.has(index)) { - const sources = this.sources.get(index) - sources.data.push(src) - sources.ping() - return - } - - // We have to setup - const sources = new UIEventSource([src]) - this.sources.set(index, sources) - const merger = new FeatureSourceMerger( - this.layer, - index, - BBox.fromTile(...Tiles.tile_from_index(index)), - sources - ) - this.loadedTiles.set(index, merger) - this._handleTile(merger, index) - } -} diff --git a/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts deleted file mode 100644 index d49bf16d3..000000000 --- a/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts +++ /dev/null @@ -1,249 +0,0 @@ -import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" -import { Store, UIEventSource } from "../../UIEventSource" -import FilteredLayer from "../../../Models/FilteredLayer" -import TileHierarchy from "./TileHierarchy" -import { Tiles } from "../../../Models/TileRange" -import { BBox } from "../../BBox" -import { Feature } from "geojson"; - -/** - * Contains all features in a tiled fashion. - * The data will be automatically broken down into subtiles when there are too much features in a single tile or if the zoomlevel is too high - */ -export default class TiledFeatureSource - implements - Tiled, - IndexedFeatureSource, - FeatureSourceForLayer, - TileHierarchy -{ - public readonly z: number - public readonly x: number - public readonly y: number - public readonly parent: TiledFeatureSource - public readonly root: TiledFeatureSource - public readonly layer: FilteredLayer - /* An index of all known tiles. allTiles[z][x][y].get('layerid') will yield the corresponding tile. - * Only defined on the root element! - */ - public readonly loadedTiles: Map = undefined - - public readonly maxFeatureCount: number - public readonly name - public readonly features: UIEventSource - public readonly containedIds: Store> - - public readonly bbox: BBox - public readonly tileIndex: number - private upper_left: TiledFeatureSource - private upper_right: TiledFeatureSource - private lower_left: TiledFeatureSource - private lower_right: TiledFeatureSource - private readonly maxzoom: number - private readonly options: TiledFeatureSourceOptions - - private constructor( - z: number, - x: number, - y: number, - parent: TiledFeatureSource, - options?: TiledFeatureSourceOptions - ) { - this.z = z - this.x = x - this.y = y - this.bbox = BBox.fromTile(z, x, y) - this.tileIndex = Tiles.tile_index(z, x, y) - this.name = `TiledFeatureSource(${z},${x},${y})` - this.parent = parent - this.layer = options.layer - options = options ?? {} - this.maxFeatureCount = options?.maxFeatureCount ?? 250 - this.maxzoom = options.maxZoomLevel ?? 18 - this.options = options - if (parent === undefined) { - throw "Parent is not allowed to be undefined. Use null instead" - } - if (parent === null && z !== 0 && x !== 0 && y !== 0) { - throw "Invalid root tile: z, x and y should all be null" - } - if (parent === null) { - this.root = this - this.loadedTiles = new Map() - } else { - this.root = this.parent.root - this.loadedTiles = this.root.loadedTiles - const i = Tiles.tile_index(z, x, y) - this.root.loadedTiles.set(i, this) - } - this.features = new UIEventSource([]) - this.containedIds = this.features.map((features) => { - if (features === undefined) { - return undefined - } - return new Set(features.map((f) => f.properties.id)) - }) - - // We register this tile, but only when there is some data in it - if (this.options.registerTile !== undefined) { - this.features.addCallbackAndRunD((features) => { - if (features.length === 0) { - return - } - this.options.registerTile(this) - return true - }) - } - } - - public static createHierarchy( - features: FeatureSource, - options?: TiledFeatureSourceOptions - ): TiledFeatureSource { - options = { - ...options, - layer: features["layer"] ?? options.layer, - } - const root = new TiledFeatureSource(0, 0, 0, null, options) - features.features?.addCallbackAndRunD((feats) => root.addFeatures(feats)) - return root - } - - private isSplitNeeded(featureCount: number) { - if (this.upper_left !== undefined) { - // This tile has been split previously, so we keep on splitting - return true - } - if (this.z >= this.maxzoom) { - // We are not allowed to split any further - return false - } - if (this.options.minZoomLevel !== undefined && this.z < this.options.minZoomLevel) { - // We must have at least this zoom level before we are allowed to start splitting - return true - } - - // To much features - we split - return featureCount > this.maxFeatureCount - } - - /*** - * Adds the list of features to this hierarchy. - * If there are too much features, the list will be broken down and distributed over the subtiles (only retaining features that don't fit a subtile on this level) - * @param features - * @private - */ - private addFeatures(features: Feature[]) { - if (features === undefined || features.length === 0) { - return - } - - if (!this.isSplitNeeded(features.length)) { - this.features.setData(features) - return - } - - if (this.upper_left === undefined) { - this.upper_left = new TiledFeatureSource( - this.z + 1, - this.x * 2, - this.y * 2, - this, - this.options - ) - this.upper_right = new TiledFeatureSource( - this.z + 1, - this.x * 2 + 1, - this.y * 2, - this, - this.options - ) - this.lower_left = new TiledFeatureSource( - this.z + 1, - this.x * 2, - this.y * 2 + 1, - this, - this.options - ) - this.lower_right = new TiledFeatureSource( - this.z + 1, - this.x * 2 + 1, - this.y * 2 + 1, - this, - this.options - ) - } - - const ulf = [] - const urf = [] - const llf = [] - const lrf = [] - const overlapsboundary = [] - - for (const feature of features) { - const bbox = BBox.get(feature) - - // There are a few strategies to deal with features that cross tile boundaries - - if (this.options.noDuplicates) { - // Strategy 1: We put the feature into a somewhat matching tile - if (bbox.overlapsWith(this.upper_left.bbox)) { - ulf.push(feature) - } else if (bbox.overlapsWith(this.upper_right.bbox)) { - urf.push(feature) - } else if (bbox.overlapsWith(this.lower_left.bbox)) { - llf.push(feature) - } else if (bbox.overlapsWith(this.lower_right.bbox)) { - lrf.push(feature) - } else { - overlapsboundary.push(feature) - } - } else if (this.options.minZoomLevel === undefined) { - // Strategy 2: put it into a strictly matching tile (or in this tile, which is slightly too big) - if (bbox.isContainedIn(this.upper_left.bbox)) { - ulf.push(feature) - } else if (bbox.isContainedIn(this.upper_right.bbox)) { - urf.push(feature) - } else if (bbox.isContainedIn(this.lower_left.bbox)) { - llf.push(feature) - } else if (bbox.isContainedIn(this.lower_right.bbox)) { - lrf.push(feature) - } else { - overlapsboundary.push(feature) - } - } else { - // Strategy 3: We duplicate a feature on a boundary into every tile as we need to get to the minZoomLevel - if (bbox.overlapsWith(this.upper_left.bbox)) { - ulf.push(feature) - } - if (bbox.overlapsWith(this.upper_right.bbox)) { - urf.push(feature) - } - if (bbox.overlapsWith(this.lower_left.bbox)) { - llf.push(feature) - } - if (bbox.overlapsWith(this.lower_right.bbox)) { - lrf.push(feature) - } - } - } - this.upper_left.addFeatures(ulf) - this.upper_right.addFeatures(urf) - this.lower_left.addFeatures(llf) - this.lower_right.addFeatures(lrf) - this.features.setData(overlapsboundary) - } -} - -export interface TiledFeatureSourceOptions { - readonly maxFeatureCount?: number - readonly maxZoomLevel?: number - readonly minZoomLevel?: number - /** - * IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated. - * Setting 'dontEnforceMinZoomLevel' will assign to feature to some matching subtile. - */ - readonly noDuplicates?: boolean - readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void - readonly layer?: FilteredLayer -} diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 7957fb8cd..302e809a3 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -2,19 +2,34 @@ import { BBox } from "./BBox" import LayerConfig from "../Models/ThemeConfig/LayerConfig" import * as turf from "@turf/turf" import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf" -import { Feature, Geometry, MultiPolygon, Polygon } from "geojson" -import { GeoJSON, LineString, Point, Position } from "geojson" +import { + Feature, + GeoJSON, + Geometry, + LineString, + MultiPolygon, + Point, + Polygon, + Position, +} from "geojson" import togpx from "togpx" import Constants from "../Models/Constants" +import { Tiles } from "../Models/TileRange" export class GeoOperations { + private static readonly _earthRadius = 6378137 + private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2 + /** * Create a union between two features */ - static union = turf.union - static intersect = turf.intersect - private static readonly _earthRadius = 6378137 - private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2 + public static union(f0: Feature, f1: Feature): Feature | null { + return turf.union(f0, f1) + } + + public static intersect(f0: Feature, f1: Feature): Feature | null { + return turf.intersect(f0, f1) + } static surfaceAreaInSqMeters(feature: any) { return turf.area(feature) @@ -637,14 +652,14 @@ export class GeoOperations { */ static completelyWithin( feature: Feature, - possiblyEncloingFeature: Feature + possiblyEnclosingFeature: Feature ): boolean { - return booleanWithin(feature, possiblyEncloingFeature) + return booleanWithin(feature, possiblyEnclosingFeature) } /** * Create an intersection between two features. - * A new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary + * One or multiple new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary */ public static clipWith(toSplit: Feature, boundary: Feature): Feature[] { if (toSplit.geometry.type === "Point") { @@ -677,35 +692,6 @@ export class GeoOperations { throw "Invalid geometry type with GeoOperations.clipWith: " + toSplit.geometry.type } - /** - * Helper function which does the heavy lifting for 'inside' - */ - private static pointInPolygonCoordinates( - x: number, - y: number, - coordinates: [number, number][][] - ): boolean { - const inside = GeoOperations.pointWithinRing( - x, - y, - /*This is the outer ring of the polygon */ coordinates[0] - ) - if (!inside) { - return false - } - for (let i = 1; i < coordinates.length; i++) { - const inHole = GeoOperations.pointWithinRing( - x, - y, - coordinates[i] /* These are inner rings, aka holes*/ - ) - if (inHole) { - return false - } - } - return true - } - /** * * @@ -763,6 +749,62 @@ export class GeoOperations { throw "Unkown location type: " + location } } + + /** + * Constructs all tiles where features overlap with and puts those features in them. + * Long features (e.g. lines or polygons) which overlap with multiple tiles are referenced in each tile they overlap with + * @param zoomlevel + * @param features + */ + public static slice(zoomlevel: number, features: Feature[]): Map { + const tiles = new Map() + + for (const feature of features) { + const bbox = BBox.get(feature) + Tiles.MapRange(Tiles.tileRangeFrom(bbox, zoomlevel), (x, y) => { + const i = Tiles.tile_index(zoomlevel, x, y) + + let tiledata = tiles.get(i) + if (tiledata === undefined) { + tiledata = [] + tiles.set(i, tiledata) + } + tiledata.push(feature) + }) + } + + return tiles + } + + /** + * Helper function which does the heavy lifting for 'inside' + */ + private static pointInPolygonCoordinates( + x: number, + y: number, + coordinates: [number, number][][] + ): boolean { + const inside = GeoOperations.pointWithinRing( + x, + y, + /*This is the outer ring of the polygon */ coordinates[0] + ) + if (!inside) { + return false + } + for (let i = 1; i < coordinates.length; i++) { + const inHole = GeoOperations.pointWithinRing( + x, + y, + coordinates[i] /* These are inner rings, aka holes*/ + ) + if (inHole) { + return false + } + } + return true + } + private static pointWithinRing(x: number, y: number, ring: [number, number][]) { let inside = false for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { diff --git a/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts b/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts index 9e46a3791..353e94b1c 100644 --- a/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts +++ b/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts @@ -2,12 +2,12 @@ import { OsmCreateAction } from "./OsmChangeAction" import { Tag } from "../../Tags/Tag" import { Changes } from "../Changes" import { ChangeDescription } from "./ChangeDescription" -import FeaturePipelineState from "../../State/FeaturePipelineState" -import FeatureSource from "../../FeatureSource/FeatureSource" import CreateNewWayAction from "./CreateNewWayAction" import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWithPointReuseAction" import { And } from "../../Tags/And" import { TagUtils } from "../../Tags/TagUtils" +import { SpecialVisualizationState } from "../../../UI/SpecialVisualization" +import FeatureSource from "../../FeatureSource/FeatureSource" /** * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points @@ -26,14 +26,14 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct tags: Tag[], outerRingCoordinates: [number, number][], innerRingsCoordinates: [number, number][][], - state: FeaturePipelineState, + state: SpecialVisualizationState, config: MergePointConfig[], changeType: "import" | "create" | string ) { super(null, true) this._tags = [...tags, new Tag("type", "multipolygon")] this.changeType = changeType - this.theme = state?.layoutToUse?.id ?? "" + this.theme = state?.layout?.id ?? "" this.createOuterWay = new CreateWayWithPointReuseAction( [], outerRingCoordinates, @@ -45,7 +45,7 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct new CreateNewWayAction( [], ringCoordinates.map(([lon, lat]) => ({ lat, lon })), - { theme: state?.layoutToUse?.id } + { theme: state?.layout?.id } ) ) @@ -59,6 +59,10 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct } } + public async getPreview(): Promise { + return undefined + } + protected async CreateChangeDescriptions(changes: Changes): Promise { console.log("Running CMPWPRA") const descriptions: ChangeDescription[] = [] diff --git a/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts b/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts index ed549d4af..df99cc97c 100644 --- a/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts +++ b/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts @@ -2,7 +2,6 @@ import { OsmCreateAction } from "./OsmChangeAction" import { Tag } from "../../Tags/Tag" import { Changes } from "../Changes" import { ChangeDescription } from "./ChangeDescription" -import FeaturePipelineState from "../../State/FeaturePipelineState" import { BBox } from "../../BBox" import { TagsFilter } from "../../Tags/TagsFilter" import { GeoOperations } from "../../GeoOperations" @@ -10,6 +9,7 @@ import FeatureSource from "../../FeatureSource/FeatureSource" import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" import CreateNewNodeAction from "./CreateNewNodeAction" import CreateNewWayAction from "./CreateNewWayAction" +import { SpecialVisualizationState } from "../../../UI/SpecialVisualization" export interface MergePointConfig { withinRangeOfM: number @@ -62,14 +62,14 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { * lngLat-coordinates * @private */ - private _coordinateInfo: CoordinateInfo[] - private _state: FeaturePipelineState - private _config: MergePointConfig[] + private readonly _coordinateInfo: CoordinateInfo[] + private readonly _state: SpecialVisualizationState + private readonly _config: MergePointConfig[] constructor( tags: Tag[], coordinates: [number, number][], - state: FeaturePipelineState, + state: SpecialVisualizationState, config: MergePointConfig[] ) { super(null, true) @@ -188,7 +188,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { } public async CreateChangeDescriptions(changes: Changes): Promise { - const theme = this._state?.layoutToUse?.id + const theme = this._state?.layout?.id const allChanges: ChangeDescription[] = [] const nodeIdsToUse: { lat: number; lon: number; nodeId?: number }[] = [] for (let i = 0; i < this._coordinateInfo.length; i++) { @@ -252,9 +252,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] { const bbox = new BBox(coordinates) const state = this._state - const allNodes = [].concat( - ...(state?.featurePipeline?.GetFeaturesWithin("type_node", bbox.pad(1.2)) ?? []) - ) + const allNodes =state.fullNodeDatabase?.getNodesWithin(bbox.pad(1.2)) const maxDistance = Math.max(...this._config.map((c) => c.withinRangeOfM)) // Init coordianteinfo with undefined but the same length as coordinates diff --git a/Logic/Osm/Actions/ReplaceGeometryAction.ts b/Logic/Osm/Actions/ReplaceGeometryAction.ts index 94390f61f..3ec65299f 100644 --- a/Logic/Osm/Actions/ReplaceGeometryAction.ts +++ b/Logic/Osm/Actions/ReplaceGeometryAction.ts @@ -12,8 +12,8 @@ import { And } from "../../Tags/And" import { Utils } from "../../../Utils" import { OsmConnection } from "../OsmConnection" import { Feature } from "@turf/turf" -import FeaturePipeline from "../../FeatureSource/FeaturePipeline" -import { Geometry, LineString, Point, Polygon } from "geojson" +import { Geometry, LineString, Point } from "geojson" +import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" export default class ReplaceGeometryAction extends OsmChangeAction { /** @@ -22,7 +22,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { private readonly feature: any private readonly state: { osmConnection: OsmConnection - featurePipeline: FeaturePipeline + fullNodeDatabase?: FullNodeDatabaseSource } private readonly wayToReplaceId: string private readonly theme: string @@ -41,7 +41,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { constructor( state: { osmConnection: OsmConnection - featurePipeline: FeaturePipeline + fullNodeDatabase?: FullNodeDatabaseSource }, feature: any, wayToReplaceId: string, @@ -195,7 +195,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { }> { // TODO FIXME: if a new point has to be created, snap to already existing ways - const nodeDb = this.state.featurePipeline.fullNodeDatabase + const nodeDb = this.state.fullNodeDatabase if (nodeDb === undefined) { throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" } @@ -415,7 +415,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { } protected async CreateChangeDescriptions(changes: Changes): Promise { - const nodeDb = this.state.featurePipeline.fullNodeDatabase + const nodeDb = this.state.fullNodeDatabase if (nodeDb === undefined) { throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" } diff --git a/Logic/Osm/Geocoding.ts b/Logic/Osm/Geocoding.ts index d0241608c..09da7af6d 100644 --- a/Logic/Osm/Geocoding.ts +++ b/Logic/Osm/Geocoding.ts @@ -5,6 +5,10 @@ export interface GeoCodeResult { display_name: string lat: number lon: number + /** + * Format: + * [lat, lat, lon, lon] + */ boundingbox: number[] osm_type: "node" | "way" | "relation" osm_id: string diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index 4d23d1f6c..c68040a1a 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -15,6 +15,13 @@ import { OsmTags } from "../Models/OsmFeature" import { UIEventSource } from "./UIEventSource" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +/** + * All elements that are needed to perform metatagging + */ +export interface MetataggingState { + layout: LayoutConfig +} + export abstract class SimpleMetaTagger { public readonly keys: string[] public readonly doc: string @@ -60,7 +67,7 @@ export abstract class SimpleMetaTagger { feature: any, layer: LayerConfig, tagsStore: UIEventSource>, - state: { layout: LayoutConfig } + state: MetataggingState ): boolean } @@ -119,7 +126,7 @@ export class CountryTagger extends SimpleMetaTagger { }) } - applyMetaTagsOnFeature(feature, _, state) { + applyMetaTagsOnFeature(feature, _, tagsSource) { let centerPoint: any = GeoOperations.centerpoint(feature) const runningTasks = this.runningTasks const lat = centerPoint.geometry.coordinates[1] @@ -128,28 +135,29 @@ export class CountryTagger extends SimpleMetaTagger { CountryTagger.coder .GetCountryCodeAsync(lon, lat) .then((countries) => { - runningTasks.delete(feature) - try { - const oldCountry = feature.properties["_country"] - feature.properties["_country"] = countries[0].trim().toLowerCase() - if (oldCountry !== feature.properties["_country"]) { - const tagsSource = state?.allElements?.getEventSourceById( - feature.properties.id - ) - tagsSource?.ping() - } - } catch (e) { - console.warn(e) + const oldCountry = feature.properties["_country"] + const newCountry = countries[0].trim().toLowerCase() + if (oldCountry !== newCountry) { + tagsSource.data["_country"] = newCountry + tagsSource?.ping() } }) - .catch((_) => { - runningTasks.delete(feature) + .catch((e) => { + console.warn(e) }) + .finally(() => runningTasks.delete(feature)) return false } } class InlineMetaTagger extends SimpleMetaTagger { + public readonly applyMetaTagsOnFeature: ( + feature: any, + layer: LayerConfig, + tagsStore: UIEventSource, + state: MetataggingState + ) => boolean + constructor( docs: { keys: string[] @@ -166,23 +174,17 @@ class InlineMetaTagger extends SimpleMetaTagger { feature: any, layer: LayerConfig, tagsStore: UIEventSource, - state: { layout: LayoutConfig } + state: MetataggingState ) => boolean ) { super(docs) this.applyMetaTagsOnFeature = f } - - public readonly applyMetaTagsOnFeature: ( - feature: any, - layer: LayerConfig, - tagsStore: UIEventSource, - state: { layout: LayoutConfig } - ) => boolean } -export default class SimpleMetaTaggers { - public static readonly objectMetaInfo = new InlineMetaTagger( - { + +export class RewriteMetaInfoTags extends SimpleMetaTagger { + constructor() { + super({ keys: [ "_last_edit:contributor", "_last_edit:contributor:uid", @@ -192,30 +194,37 @@ export default class SimpleMetaTaggers { "_backend", ], doc: "Information about the last edit of this object.", - }, - (feature) => { - /*Note: also called by 'UpdateTagsFromOsmAPI'*/ + }) + } - const tgs = feature.properties - let movedSomething = false + applyMetaTagsOnFeature(feature: Feature): boolean { + /*Note: also called by 'UpdateTagsFromOsmAPI'*/ - function move(src: string, target: string) { - if (tgs[src] === undefined) { - return - } - tgs[target] = tgs[src] - delete tgs[src] - movedSomething = true + const tgs = feature.properties + let movedSomething = false + + function move(src: string, target: string) { + if (tgs[src] === undefined) { + return } - - move("user", "_last_edit:contributor") - move("uid", "_last_edit:contributor:uid") - move("changeset", "_last_edit:changeset") - move("timestamp", "_last_edit:timestamp") - move("version", "_version_number") - return movedSomething + tgs[target] = tgs[src] + delete tgs[src] + movedSomething = true } - ) + + move("user", "_last_edit:contributor") + move("uid", "_last_edit:contributor:uid") + move("changeset", "_last_edit:changeset") + move("timestamp", "_last_edit:timestamp") + move("version", "_version_number") + return movedSomething + } +} +export default class SimpleMetaTaggers { + /** + * A simple metatagger which rewrites various metatags as needed + */ + public static readonly objectMetaInfo = new RewriteMetaInfoTags() public static country = new CountryTagger() public static geometryType = new InlineMetaTagger( { diff --git a/Logic/State/FeaturePipelineState.ts b/Logic/State/FeaturePipelineState.ts index 9de67d4c1..a63ad9028 100644 --- a/Logic/State/FeaturePipelineState.ts +++ b/Logic/State/FeaturePipelineState.ts @@ -1,10 +1,5 @@ -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import FeaturePipeline from "../FeatureSource/FeaturePipeline" -import { Tiles } from "../../Models/TileRange" import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler" import Hash from "../Web/Hash" -import { BBox } from "../BBox" -import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator" export default class FeaturePipelineState { @@ -14,101 +9,9 @@ export default class FeaturePipelineState { public readonly featurePipeline: FeaturePipeline private readonly metatagRecalculator: MetaTagRecalculator - constructor(layoutToUse: LayoutConfig) { - const clustering = layoutToUse?.clustering - const clusterCounter = this.featureAggregator - const self = this - - /** - * We are a bit in a bind: - * There is the featurePipeline, which creates some sources during construction - * THere is the metatagger, which needs to have these sources registered AND which takes a FeaturePipeline as argument - * - * This is a bit of a catch-22 (except that it isn't) - * The sources that are registered in the constructor are saved into 'registeredSources' temporary - * - */ - const sourcesToRegister = [] - - function registerRaw(source: FeatureSourceForLayer & Tiled) { - if (self.metatagRecalculator === undefined) { - sourcesToRegister.push(source) - } else { - self.metatagRecalculator.registerSource(source) - } - } - - function registerSource(source: FeatureSourceForLayer & Tiled) { - clusterCounter.addTile(source) - const sourceBBox = source.features.map((allFeatures) => - BBox.bboxAroundAll(allFeatures.map(BBox.get)) - ) - - // Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering - source.features.map( - (f) => { - const z = self.locationControl.data.zoom - - if (!source.layer.isDisplayed.data) { - return false - } - - const bounds = self.currentBounds.data - if (bounds === undefined) { - // Map is not yet displayed - return false - } - - if (!sourceBBox.data.overlapsWith(bounds)) { - // Not within range -> features are hidden - return false - } - - if (z < source.layer.layerDef.minzoom) { - // Layer is always hidden for this zoom level - return false - } - - if (z > clustering.maxZoom) { - return true - } - - if (f.length > clustering.minNeededElements) { - // This tile alone already has too much features - return false - } - - let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex) - if (tileZ >= z) { - while (tileZ > z) { - tileZ-- - tileX = Math.floor(tileX / 2) - tileY = Math.floor(tileY / 2) - } - - if ( - clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY)) - ?.totalValue > clustering.minNeededElements - ) { - // To much elements - return false - } - } - - return true - }, - [self.currentBounds, source.layer.isDisplayed, sourceBBox] - ) - } - - this.featurePipeline = new FeaturePipeline(registerSource, this, { - handleRawFeatureSource: registerRaw, - }) + constructor() { this.metatagRecalculator = new MetaTagRecalculator(this, this.featurePipeline) this.metatagRecalculator.registerSource(this.currentView) - - sourcesToRegister.forEach((source) => self.metatagRecalculator.registerSource(source)) - new SelectedFeatureHandler(Hash.hash, this) } } diff --git a/Logic/State/LayerState.ts b/Logic/State/LayerState.ts index 46fd6989c..c2061e7f9 100644 --- a/Logic/State/LayerState.ts +++ b/Logic/State/LayerState.ts @@ -1,10 +1,8 @@ import { UIEventSource } from "../UIEventSource" import { GlobalFilter } from "../../Models/GlobalFilter" -import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" +import FilteredLayer from "../../Models/FilteredLayer" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import { OsmConnection } from "../Osm/OsmConnection" -import { LocalStorageSource } from "../Web/LocalStorageSource" -import { QueryParameters } from "../Web/QueryParameters" /** * The layer state keeps track of: @@ -36,83 +34,14 @@ export default class LayerState { this.osmConnection = osmConnection this.filteredLayers = new Map() for (const layer of layers) { - this.filteredLayers.set(layer.id, this.initFilteredLayer(layer, context)) + this.filteredLayers.set( + layer.id, + FilteredLayer.initLinkedState(layer, context, this.osmConnection) + ) } layers.forEach((l) => this.linkFilterStates(l)) } - private static getPref( - osmConnection: OsmConnection, - key: string, - layer: LayerConfig - ): UIEventSource { - return osmConnection.GetPreference(key, layer.shownByDefault + "").sync( - (v) => { - if (v === undefined) { - return undefined - } - return v === "true" - }, - [], - (b) => { - if (b === undefined) { - return undefined - } - return "" + b - } - ) - } - /** - * INitializes a filtered layer for the given layer. - * @param layer - * @param context: probably the theme-name. This is used to disambiguate the user settings; e.g. when using the same layer in different contexts - * @private - */ - private initFilteredLayer(layer: LayerConfig, context: string): FilteredLayer | undefined { - let isDisplayed: UIEventSource - const osmConnection = this.osmConnection - if (layer.syncSelection === "local") { - isDisplayed = LocalStorageSource.GetParsed( - context + "-layer-" + layer.id + "-enabled", - layer.shownByDefault - ) - } else if (layer.syncSelection === "theme-only") { - isDisplayed = LayerState.getPref( - osmConnection, - context + "-layer-" + layer.id + "-enabled", - layer - ) - } else if (layer.syncSelection === "global") { - isDisplayed = LayerState.getPref(osmConnection, "layer-" + layer.id + "-enabled", layer) - } else { - isDisplayed = QueryParameters.GetBooleanQueryParameter( - "layer-" + layer.id, - layer.shownByDefault, - "Wether or not layer " + layer.id + " is shown" - ) - } - - const flayer: FilteredLayer = { - isDisplayed, - layerDef: layer, - appliedFilters: new UIEventSource>( - new Map() - ), - } - layer.filters?.forEach((filterConfig) => { - const stateSrc = filterConfig.initState() - - stateSrc.addCallbackAndRun((state) => - flayer.appliedFilters.data.set(filterConfig.id, state) - ) - flayer.appliedFilters - .map((dict) => dict.get(filterConfig.id)) - .addCallback((state) => stateSrc.setData(state)) - }) - - return flayer - } - /** * Some layers copy the filter state of another layer - this is quite often the case for 'sibling'-layers, * (where two variations of the same layer are used, e.g. a specific type of shop on all zoom levels and all shops on high zoom). @@ -136,10 +65,6 @@ export default class LayerState { console.warn( "Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs ) - this.filteredLayers.set(layer.id, { - isDisplayed: toReuse.isDisplayed, - layerDef: layer, - appliedFilters: toReuse.appliedFilters, - }) + this.filteredLayers.set(layer.id, toReuse) } } diff --git a/Logic/State/UserRelatedState.ts b/Logic/State/UserRelatedState.ts index 9f6f115a6..8f80a5046 100644 --- a/Logic/State/UserRelatedState.ts +++ b/Logic/State/UserRelatedState.ts @@ -17,14 +17,10 @@ export default class UserRelatedState { The user credentials */ public osmConnection: OsmConnection - /** - THe change handler - */ - public changes: Changes /** * The key for mangrove */ - public mangroveIdentity: MangroveIdentity + public readonly mangroveIdentity: MangroveIdentity public readonly installedUserThemes: Store diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index b161f262b..6ca8ef63c 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -63,27 +63,10 @@ export class Stores { stable.setData(undefined) return } - const oldList = stable.data - if (oldList === list) { + if (Utils.sameList(stable.data, list)) { return } - if (oldList == list) { - return - } - if (oldList === undefined || oldList.length !== list.length) { - stable.setData(list) - return - } - - for (let i = 0; i < list.length; i++) { - if (oldList[i] !== list[i]) { - stable.setData(list) - return - } - } - - // No actual changes, so we don't do anything - return + stable.setData(list) }) return stable } @@ -93,7 +76,7 @@ export abstract class Store implements Readable { abstract readonly data: T /** - * OPtional value giving a title to the UIEventSource, mainly used for debugging + * Optional value giving a title to the UIEventSource, mainly used for debugging */ public readonly tag: string | undefined @@ -794,4 +777,14 @@ export class UIEventSource extends Store implements Writable { update(f: Updater & ((value: T) => T)): void { this.setData(f(this.data)) } + + /** + * Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well. + * However, this value can be overriden without affecting source + */ + static feedFrom(store: Store): UIEventSource { + const src = new UIEventSource(store.data) + store.addCallback((t) => src.setData(t)) + return src + } } diff --git a/Logic/Web/MangroveReviews.ts b/Logic/Web/MangroveReviews.ts index a0076e027..e2ed56e12 100644 --- a/Logic/Web/MangroveReviews.ts +++ b/Logic/Web/MangroveReviews.ts @@ -1,10 +1,8 @@ import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" import { MangroveReviews, Review } from "mangrove-reviews-typescript" import { Utils } from "../../Utils" -import { Feature, Geometry, Position } from "geojson" +import { Feature, Position } from "geojson" import { GeoOperations } from "../GeoOperations" -import { OsmTags } from "../../Models/OsmFeature" -import { ElementStorage } from "../ElementStorage" export class MangroveIdentity { public readonly keypair: Store @@ -67,11 +65,9 @@ export default class FeatureReviews { private readonly _identity: MangroveIdentity private constructor( - feature: Feature, - state: { - allElements: ElementStorage - mangroveIdentity?: MangroveIdentity - }, + feature: Feature, + tagsSource: UIEventSource>, + mangroveIdentity?: MangroveIdentity, options?: { nameKey?: "name" | string fallbackName?: string @@ -80,8 +76,7 @@ export default class FeatureReviews { ) { const centerLonLat = GeoOperations.centerpointCoordinates(feature) ;[this._lon, this._lat] = centerLonLat - this._identity = - state?.mangroveIdentity ?? new MangroveIdentity(new UIEventSource(undefined)) + this._identity = mangroveIdentity ?? new MangroveIdentity(new UIEventSource(undefined)) const nameKey = options?.nameKey ?? "name" if (feature.geometry.type === "Point") { @@ -108,9 +103,7 @@ export default class FeatureReviews { this._uncertainty = options?.uncertaintyRadius ?? maxDistance } - this._name = state.allElements - .getEventSourceById(feature.properties.id) - .map((tags) => tags[nameKey] ?? options?.fallbackName) + this._name = tagsSource .map((tags) => tags[nameKey] ?? options?.fallbackName) this.subjectUri = this.ConstructSubjectUri() @@ -136,11 +129,9 @@ export default class FeatureReviews { * Construct a featureReviewsFor or fetches it from the cache */ public static construct( - feature: Feature, - state: { - allElements: ElementStorage - mangroveIdentity?: MangroveIdentity - }, + feature: Feature, + tagsSource: UIEventSource>, + mangroveIdentity?: MangroveIdentity, options?: { nameKey?: "name" | string fallbackName?: string @@ -152,7 +143,7 @@ export default class FeatureReviews { if (cached !== undefined) { return cached } - const featureReviews = new FeatureReviews(feature, state, options) + const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options) FeatureReviews._featureReviewsCache[key] = featureReviews return featureReviews } diff --git a/Models/FilteredLayer.ts b/Models/FilteredLayer.ts index 3263c35ae..1cbe82a58 100644 --- a/Models/FilteredLayer.ts +++ b/Models/FilteredLayer.ts @@ -1,14 +1,90 @@ import { UIEventSource } from "../Logic/UIEventSource" import LayerConfig from "./ThemeConfig/LayerConfig" -import { TagsFilter } from "../Logic/Tags/TagsFilter" +import { OsmConnection } from "../Logic/Osm/OsmConnection" +import { LocalStorageSource } from "../Logic/Web/LocalStorageSource" +import { QueryParameters } from "../Logic/Web/QueryParameters" -export interface FilterState { - currentFilter: TagsFilter - state: string | number -} - -export default interface FilteredLayer { +export default class FilteredLayer { + /** + * Wether or not the specified layer is shown + */ readonly isDisplayed: UIEventSource - readonly appliedFilters: UIEventSource> + /** + * Maps the filter.option.id onto the actual used state + */ + readonly appliedFilters: Map> readonly layerDef: LayerConfig + + constructor( + layer: LayerConfig, + appliedFilters?: Map>, + isDisplayed?: UIEventSource + ) { + this.layerDef = layer + this.isDisplayed = isDisplayed ?? new UIEventSource(true) + this.appliedFilters = + appliedFilters ?? new Map>() + } + + /** + * Creates a FilteredLayer which is tied into the QueryParameters and/or user preferences + */ + public static initLinkedState( + layer: LayerConfig, + context: string, + osmConnection: OsmConnection + ) { + let isDisplayed: UIEventSource + if (layer.syncSelection === "local") { + isDisplayed = LocalStorageSource.GetParsed( + context + "-layer-" + layer.id + "-enabled", + layer.shownByDefault + ) + } else if (layer.syncSelection === "theme-only") { + isDisplayed = FilteredLayer.getPref( + osmConnection, + context + "-layer-" + layer.id + "-enabled", + layer + ) + } else if (layer.syncSelection === "global") { + isDisplayed = FilteredLayer.getPref( + osmConnection, + "layer-" + layer.id + "-enabled", + layer + ) + } else { + isDisplayed = QueryParameters.GetBooleanQueryParameter( + "layer-" + layer.id, + layer.shownByDefault, + "Whether or not layer " + layer.id + " is shown" + ) + } + + const appliedFilters = new Map>() + for (const subfilter of layer.filters) { + appliedFilters.set(subfilter.id, subfilter.initState()) + } + return new FilteredLayer(layer, appliedFilters, isDisplayed) + } + private static getPref( + osmConnection: OsmConnection, + key: string, + layer: LayerConfig + ): UIEventSource { + return osmConnection.GetPreference(key, layer.shownByDefault + "").sync( + (v) => { + if (v === undefined) { + return undefined + } + return v === "true" + }, + [], + (b) => { + if (b === undefined) { + return undefined + } + return "" + b + } + ) + } } diff --git a/Models/GlobalFilter.ts b/Models/GlobalFilter.ts index c57a818a7..91750c7bd 100644 --- a/Models/GlobalFilter.ts +++ b/Models/GlobalFilter.ts @@ -1,9 +1,10 @@ import { Translation, TypedTranslation } from "../UI/i18n/Translation" -import { FilterState } from "./FilteredLayer" import { Tag } from "../Logic/Tags/Tag" +import { TagsFilter } from "../Logic/Tags/TagsFilter" export interface GlobalFilter { - filter: FilterState + osmTags: TagsFilter + state: number | string | undefined id: string onNewPoint: { safetyCheck: Translation diff --git a/Models/MapProperties.ts b/Models/MapProperties.ts index 39ac125c2..cbb34e7f3 100644 --- a/Models/MapProperties.ts +++ b/Models/MapProperties.ts @@ -5,8 +5,10 @@ import { RasterLayerPolygon } from "./RasterLayers" export interface MapProperties { readonly location: UIEventSource<{ lon: number; lat: number }> readonly zoom: UIEventSource - readonly bounds: Store + readonly bounds: UIEventSource readonly rasterLayer: UIEventSource readonly maxbounds: UIEventSource readonly allowMoving: UIEventSource + + readonly allowZooming: UIEventSource } diff --git a/Models/RasterLayers.ts b/Models/RasterLayers.ts index 84d9fc53e..ec75de3c8 100644 --- a/Models/RasterLayers.ts +++ b/Models/RasterLayers.ts @@ -36,6 +36,14 @@ export class AvailableRasterLayers { geometry: BBox.global.asGeometry(), } + public static readonly maplibre: RasterLayerPolygon = { + type: "Feature", + properties: { + name: "MapLibre", + url: null, + }, + geometry: BBox.global.asGeometry(), + } public static layersAvailableAt( location: Store<{ lon: number; lat: number }> ): Store { @@ -58,6 +66,7 @@ export class AvailableRasterLayers { return GeoOperations.inside(lonlat, eliPolygon) }) matching.unshift(AvailableRasterLayers.osmCarto) + matching.unshift(AvailableRasterLayers.maplibre) matching.push(...AvailableRasterLayers.globalLayers) return matching }) diff --git a/Models/ThemeConfig/FilterConfig.ts b/Models/ThemeConfig/FilterConfig.ts index 4be92468d..a7774fb2a 100644 --- a/Models/ThemeConfig/FilterConfig.ts +++ b/Models/ThemeConfig/FilterConfig.ts @@ -5,7 +5,6 @@ import Translations from "../../UI/i18n/Translations" import { TagUtils } from "../../Logic/Tags/TagUtils" import { TagConfigJson } from "./Json/TagConfigJson" import { UIEventSource } from "../../Logic/UIEventSource" -import { FilterState } from "../FilteredLayer" import { QueryParameters } from "../../Logic/Web/QueryParameters" import { Utils } from "../../Utils" import { RegexTag } from "../../Logic/Tags/RegexTag" @@ -144,14 +143,7 @@ export default class FilterConfig { }) } - public initState(): UIEventSource { - function reset(state: FilterState): string { - if (state === undefined) { - return "" - } - return "" + state.state - } - + public initState(): UIEventSource { let defaultValue = "" if (this.options.length > 1) { defaultValue = "" + (this.defaultSelection ?? 0) @@ -159,6 +151,8 @@ export default class FilterConfig { // Only a single option if (this.defaultSelection === 0) { defaultValue = "true" + } else { + defaultValue = "false" } } const qp = QueryParameters.GetQueryParameter( @@ -168,12 +162,6 @@ export default class FilterConfig { ) if (this.options.length > 1) { - // This is a multi-option filter; state should be a number which selects the correct entry - const possibleStates: FilterState[] = this.options.map((opt, i) => ({ - currentFilter: opt.osmTags, - state: i, - })) - // We map the query parameter for this case return qp.sync( (str) => { @@ -182,62 +170,29 @@ export default class FilterConfig { // Nope, not a correct number! return undefined } - return possibleStates[parsed] + return parsed }, [], - reset + (n) => "" + n ) } const option = this.options[0] if (option.fields.length > 0) { - return qp.sync( - (str) => { - // There are variables in play! - // str should encode a json-hash - try { - const props = JSON.parse(str) - - const origTags = option.originalTagsSpec - const rewrittenTags = Utils.WalkJson(origTags, (v) => { - if (typeof v !== "string") { - return v - } - for (const key in props) { - v = (v).replace("{" + key + "}", props[key]) - } - return v - }) - const parsed = TagUtils.Tag(rewrittenTags) - return { - currentFilter: parsed, - state: str, - } - } catch (e) { - return undefined - } - }, - [], - reset - ) + return qp } - // The last case is pretty boring: it is checked or it isn't - const filterState: FilterState = { - currentFilter: option.osmTags, - state: "true", - } return qp.sync( (str) => { // Only a single option exists here if (str === "true") { - return filterState + return 0 } return undefined }, [], - reset + (n) => (n === undefined ? "false" : "true") ) } diff --git a/Models/ThemeConfig/Json/LayoutConfigJson.ts b/Models/ThemeConfig/Json/LayoutConfigJson.ts index 4a2a761b7..33099a062 100644 --- a/Models/ThemeConfig/Json/LayoutConfigJson.ts +++ b/Models/ThemeConfig/Json/LayoutConfigJson.ts @@ -205,25 +205,6 @@ export interface LayoutConfigJson { } )[] - /** - * If defined, data will be clustered. - * Defaults to {maxZoom: 16, minNeeded: 500} - */ - clustering?: - | { - /** - * All zoom levels above 'maxzoom' are not clustered anymore. - * Defaults to 18 - */ - maxZoom?: number - /** - * The number of elements per tile needed to start clustering - * If clustering is defined, defaults to 250 - */ - minNeededElements?: number - } - | false - /** * The URL of a custom CSS stylesheet to modify the layout */ diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index 4068b4fe8..a85240120 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -40,10 +40,6 @@ export default class LayoutConfig implements LayoutInformation { public defaultBackgroundId?: string public layers: LayerConfig[] public tileLayerSources: TilesourceConfig[] - public readonly clustering?: { - maxZoom: number - minNeededElements: number - } public readonly hideFromOverview: boolean public lockLocation: boolean | [[number, number], [number, number]] public readonly enableUserBadge: boolean @@ -188,22 +184,6 @@ export default class LayoutConfig implements LayoutInformation { context + ".extraLink" ) - this.clustering = { - maxZoom: 16, - minNeededElements: 250, - } - if (json.clustering === false) { - this.clustering = { - maxZoom: 0, - minNeededElements: 100000, - } - } else if (json.clustering) { - this.clustering = { - maxZoom: json.clustering.maxZoom ?? 18, - minNeededElements: json.clustering.minNeededElements ?? 250, - } - } - this.hideFromOverview = json.hideFromOverview ?? false this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined this.enableUserBadge = json.enableUserBadge ?? true diff --git a/Models/ThemeConfig/PointRenderingConfig.ts b/Models/ThemeConfig/PointRenderingConfig.ts index e8a467bf8..876ff425c 100644 --- a/Models/ThemeConfig/PointRenderingConfig.ts +++ b/Models/ThemeConfig/PointRenderingConfig.ts @@ -11,8 +11,6 @@ import { FixedUiElement } from "../../UI/Base/FixedUiElement" import Img from "../../UI/Base/Img" import Combine from "../../UI/Base/Combine" import { VariableUiElement } from "../../UI/Base/VariableUIElement" -import { OsmTags } from "../OsmFeature" -import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" export default class PointRenderingConfig extends WithContextLoader { private static readonly allowed_location_codes = new Set([ @@ -176,7 +174,7 @@ export default class PointRenderingConfig extends WithContextLoader { return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin) } - public GetSimpleIcon(tags: Store): BaseUIElement { + public GetSimpleIcon(tags: Store>): BaseUIElement { const self = this if (this.icon === undefined) { return undefined @@ -187,7 +185,7 @@ export default class PointRenderingConfig extends WithContextLoader { } public RenderIcon( - tags: Store, + tags: Store>, clickable: boolean, options?: { noSize?: false | boolean @@ -277,7 +275,7 @@ export default class PointRenderingConfig extends WithContextLoader { } } - private GetBadges(tags: Store): BaseUIElement { + private GetBadges(tags: Store>): BaseUIElement { if (this.iconBadges.length === 0) { return undefined } @@ -309,7 +307,7 @@ export default class PointRenderingConfig extends WithContextLoader { ).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0") } - private GetLabel(tags: Store): BaseUIElement { + private GetLabel(tags: Store>): BaseUIElement { if (this.label === undefined) { return undefined } diff --git a/Models/ThemeViewState.ts b/Models/ThemeViewState.ts new file mode 100644 index 000000000..f2f36e34f --- /dev/null +++ b/Models/ThemeViewState.ts @@ -0,0 +1,278 @@ +import LayoutConfig from "./ThemeConfig/LayoutConfig" +import { SpecialVisualizationState } from "../UI/SpecialVisualization" +import { Changes } from "../Logic/Osm/Changes" +import { Store, UIEventSource } from "../Logic/UIEventSource" +import FeatureSource, { + IndexedFeatureSource, + WritableFeatureSource, +} from "../Logic/FeatureSource/FeatureSource" +import { OsmConnection } from "../Logic/Osm/OsmConnection" +import { DefaultGuiState } from "../UI/DefaultGuiState" +import { MapProperties } from "./MapProperties" +import LayerState from "../Logic/State/LayerState" +import { Feature } from "geojson" +import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" +import { Map as MlMap } from "maplibre-gl" +import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning" +import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor" +import { GeoLocationState } from "../Logic/State/GeoLocationState" +import FeatureSwitchState from "../Logic/State/FeatureSwitchState" +import { QueryParameters } from "../Logic/Web/QueryParameters" +import UserRelatedState from "../Logic/State/UserRelatedState" +import LayerConfig from "./ThemeConfig/LayerConfig" +import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" +import { AvailableRasterLayers, RasterLayerPolygon } from "./RasterLayers" +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" +import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor" +import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader" +import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater" +import { BBox } from "../Logic/BBox" +import Constants from "./Constants" +import Hotkeys from "../UI/Base/Hotkeys" +import Translations from "../UI/i18n/Translations" +import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" + +/** + * + * The themeviewState contains all the state needed for the themeViewGUI. + * + * This is pretty much the 'brain' or the HQ of MapComplete + * + * It ties up all the needed elements and starts some actors. + */ +export default class ThemeViewState implements SpecialVisualizationState { + readonly layout: LayoutConfig + readonly map: UIEventSource + readonly changes: Changes + readonly featureSwitches: FeatureSwitchState + readonly featureSwitchIsTesting: Store + readonly featureSwitchUserbadge: Store + + readonly featureProperties: FeaturePropertiesStore + + readonly osmConnection: OsmConnection + readonly selectedElement: UIEventSource + readonly mapProperties: MapProperties + + readonly dataIsLoading: Store // TODO + readonly guistate: DefaultGuiState + readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO + + readonly historicalUserLocations: WritableFeatureSource + readonly indexedFeatures: IndexedFeatureSource + readonly layerState: LayerState + readonly perLayer: ReadonlyMap + readonly availableLayers: Store + readonly selectedLayer: UIEventSource + readonly userRelatedState: UserRelatedState + readonly geolocation: GeoLocationHandler + + constructor(layout: LayoutConfig) { + this.layout = layout + this.guistate = new DefaultGuiState() + this.map = new UIEventSource(undefined) + const initial = new InitialMapPositioning(layout) + this.mapProperties = new MapLibreAdaptor(this.map, initial) + const geolocationState = new GeoLocationState() + + this.featureSwitches = new FeatureSwitchState(layout) + this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting + this.featureSwitchUserbadge = this.featureSwitches.featureSwitchUserbadge + + this.osmConnection = new OsmConnection({ + dryRun: this.featureSwitches.featureSwitchIsTesting, + fakeUser: this.featureSwitches.featureSwitchFakeUser.data, + oauth_token: QueryParameters.GetQueryParameter( + "oauth_token", + undefined, + "Used to complete the login" + ), + osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data, + }) + this.userRelatedState = new UserRelatedState(this.osmConnection, layout?.language) + this.selectedElement = new UIEventSource(undefined, "Selected element") + this.selectedLayer = new UIEventSource(undefined, "Selected layer") + this.geolocation = new GeoLocationHandler( + geolocationState, + this.selectedElement, + this.mapProperties, + this.userRelatedState.gpsLocationHistoryRetentionTime + ) + this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location) + + this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id) + const indexedElements = new LayoutSource( + layout.layers, + this.featureSwitches, + new StaticFeatureSource([]), + this.mapProperties, + this.osmConnection.Backend(), + (id) => this.layerState.filteredLayers.get(id).isDisplayed + ) + this.featureProperties = new FeaturePropertiesStore(indexedElements) + const perLayer = new PerLayerFeatureSourceSplitter( + Array.from(this.layerState.filteredLayers.values()), + indexedElements, + { + constructStore: (features, layer) => new GeoIndexedStoreForLayer(features, layer), + } + ) + this.perLayer = perLayer.perLayer + + this.perLayer.forEach((fs) => { + new SaveFeatureSourceToLocalStorage(fs.layer.layerDef.id, 15, fs) + + const filtered = new FilteringFeatureSource( + fs.layer, + fs, + (id) => this.featureProperties.getStore(id), + this.layerState.globalFilters + ) + const doShowLayer = this.mapProperties.zoom.map( + (z) => + (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), + [fs.layer.isDisplayed] + ) + doShowLayer.addCallbackAndRunD((doShow) => + console.log( + "Layer", + fs.layer.layerDef.id, + "is", + doShow, + this.mapProperties.zoom.data, + fs.layer.layerDef.minzoom + ) + ) + new ShowDataLayer(this.map, { + layer: fs.layer.layerDef, + features: filtered, + doShowLayer, + selectedElement: this.selectedElement, + selectedLayer: this.selectedLayer, + fetchStore: (id) => this.featureProperties.getStore(id), + }) + }) + + this.changes = new Changes( + { + dryRun: this.featureSwitches.featureSwitchIsTesting, + allElements: indexedElements, + featurePropertiesStore: this.featureProperties, + osmConnection: this.osmConnection, + historicalUserLocations: this.geolocation.historicalUserLocations, + }, + layout?.isLeftRightSensitive() ?? false + ) + + this.initActors() + this.drawSpecialLayers() + this.initHotkeys() + this.miscSetup() + } + + /** + * Various small methods that need to be called + */ + private miscSetup() { + this.userRelatedState.markLayoutAsVisited(this.layout) + } + + private initHotkeys() { + Hotkeys.RegisterHotkey( + { nomod: "Escape", onUp: true }, + Translations.t.hotkeyDocumentation.closeSidebar, + () => { + this.selectedElement.setData(undefined) + this.guistate.closeAll() + } + ) + } + + /** + * Add the special layers to the map + * @private + */ + private drawSpecialLayers() { + type AddedByDefaultTypes = typeof Constants.added_by_default[number] + /** + * A listing which maps the layerId onto the featureSource + */ + const empty = [] + const specialLayers: Record = { + home_location: this.userRelatedState.homeLocation, + gps_location: this.geolocation.currentUserLocation, + gps_location_history: this.geolocation.historicalUserLocations, + gps_track: this.geolocation.historicalUserLocationsTrack, + selected_element: new StaticFeatureSource( + this.selectedElement.map((f) => (f === undefined ? empty : [f])) + ), + range: new StaticFeatureSource( + this.mapProperties.maxbounds.map((bbox) => + bbox === undefined ? empty : [bbox.asGeoJson({ id: "range" })] + ) + ), + current_view: new StaticFeatureSource( + this.mapProperties.bounds.map((bbox) => + bbox === undefined ? empty : [bbox.asGeoJson({ id: "current_view" })] + ) + ), + } + if (this.layout?.lockLocation) { + const bbox = new BBox(this.layout.lockLocation) + this.mapProperties.maxbounds.setData(bbox) + ShowDataLayer.showRange( + this.map, + new StaticFeatureSource([bbox.asGeoJson({})]), + this.featureSwitches.featureSwitchIsTesting + ) + } + + this.layerState.filteredLayers + .get("range") + ?.isDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true) + + this.layerState.filteredLayers.forEach((flayer) => { + const features = specialLayers[flayer.layerDef.id] + if (features === undefined) { + return + } + new ShowDataLayer(this.map, { + features, + doShowLayer: flayer.isDisplayed, + layer: flayer.layerDef, + selectedElement: this.selectedElement, + selectedLayer: this.selectedLayer, + }) + }) + } + + /** + * Setup various services for which no reference are needed + * @private + */ + private initActors() { + // Various actors that we don't need to reference + new TitleHandler( + this.selectedElement, + this.selectedLayer, + this.featureProperties, + this.layout + ) + new ChangeToElementsActor(this.changes, this.featureProperties) + new PendingChangesUploader(this.changes, this.selectedElement) + new SelectedElementTagsUpdater({ + allElements: this.featureProperties, + changes: this.changes, + selectedElement: this.selectedElement, + layoutToUse: this.layout, + osmConnection: this.osmConnection, + }) + } +} diff --git a/State.ts b/State.ts deleted file mode 100644 index 2572dc84a..000000000 --- a/State.ts +++ /dev/null @@ -1,16 +0,0 @@ -import LayoutConfig from "./Models/ThemeConfig/LayoutConfig" -import FeaturePipelineState from "./Logic/State/FeaturePipelineState" - -/** - * Contains the global state: a bunch of UI-event sources - */ - -export default class State extends FeaturePipelineState { - /* The singleton of the global state - */ - public static state: FeaturePipelineState - - constructor(layoutToUse: LayoutConfig) { - super(layoutToUse) - } -} diff --git a/UI/Base/Checkbox.svelte b/UI/Base/Checkbox.svelte new file mode 100644 index 000000000..93fec9483 --- /dev/null +++ b/UI/Base/Checkbox.svelte @@ -0,0 +1,13 @@ + + + diff --git a/UI/Base/Dropdown.svelte b/UI/Base/Dropdown.svelte new file mode 100644 index 000000000..2e711d954 --- /dev/null +++ b/UI/Base/Dropdown.svelte @@ -0,0 +1,15 @@ + + + diff --git a/UI/Base/If.svelte b/UI/Base/If.svelte index 124ff330b..5255b8046 100644 --- a/UI/Base/If.svelte +++ b/UI/Base/If.svelte @@ -1,14 +1,23 @@ {#if _c} + {:else} + {/if} diff --git a/UI/Base/IfNot.svelte b/UI/Base/IfNot.svelte new file mode 100644 index 000000000..baab7c190 --- /dev/null +++ b/UI/Base/IfNot.svelte @@ -0,0 +1,18 @@ + + +{#if _c} + +{/if} diff --git a/UI/Base/Loading.svelte b/UI/Base/Loading.svelte new file mode 100644 index 000000000..2db2e4481 --- /dev/null +++ b/UI/Base/Loading.svelte @@ -0,0 +1,13 @@ + + +
+
+ +
+
+ +
+
diff --git a/UI/Base/MapControlButton.svelte b/UI/Base/MapControlButton.svelte index fd87d3d24..9d875b984 100644 --- a/UI/Base/MapControlButton.svelte +++ b/UI/Base/MapControlButton.svelte @@ -8,6 +8,6 @@ -
dispatch("click", e)} class="subtle-background block rounded-full min-w-10 h-10 pointer-events-auto m-0.5 md:m-1 p-1"> +
dispatch("click", e)} class="subtle-background rounded-full min-w-10 w-fit h-10 m-0.5 md:m-1 p-1">
diff --git a/UI/Base/ToSvelte.svelte b/UI/Base/ToSvelte.svelte index 69dfe51f8..7edaddf1b 100644 --- a/UI/Base/ToSvelte.svelte +++ b/UI/Base/ToSvelte.svelte @@ -1,18 +1,23 @@ diff --git a/UI/BigComponents/ActionButtons.ts b/UI/BigComponents/ActionButtons.ts index 1946ea893..fad2a0224 100644 --- a/UI/BigComponents/ActionButtons.ts +++ b/UI/BigComponents/ActionButtons.ts @@ -13,6 +13,7 @@ import { OpenIdEditor, OpenJosm } from "./CopyrightPanel" import Toggle from "../Input/Toggle" import ScrollableFullScreen from "../Base/ScrollableFullScreen" import { DefaultGuiState } from "../DefaultGuiState" +import DefaultGUI from "../DefaultGUI" export class BackToThemeOverview extends Toggle { constructor( @@ -42,6 +43,7 @@ export class ActionButtons extends Combine { readonly locationControl: Store readonly osmConnection: OsmConnection readonly featureSwitchMoreQuests: Store + readonly defaultGuiState: DefaultGuiState }) { const imgSize = "h-6 w-6" const iconStyle = "height: 1.5rem; width: 1.5rem" @@ -82,8 +84,8 @@ export class ActionButtons extends Combine { Translations.t.translations.activateButton ).onClick(() => { ScrollableFullScreen.collapse() - DefaultGuiState.state.userInfoIsOpened.setData(true) - DefaultGuiState.state.userInfoFocusedQuestion.setData("translation-mode") + state.defaultGuiState.userInfoIsOpened.setData(true) + state.defaultGuiState.userInfoFocusedQuestion.setData("translation-mode") }), ]) this.SetClass("block w-full link-no-underline") diff --git a/UI/BigComponents/CopyrightPanel.ts b/UI/BigComponents/CopyrightPanel.ts index b9f0c5c75..0a7d2bb84 100644 --- a/UI/BigComponents/CopyrightPanel.ts +++ b/UI/BigComponents/CopyrightPanel.ts @@ -14,54 +14,53 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import Title from "../Base/Title" import { SubtleButton } from "../Base/SubtleButton" import Svg from "../../Svg" -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" import { BBox } from "../../Logic/BBox" -import Loc from "../../Models/Loc" import Toggle from "../Input/Toggle" import { OsmConnection } from "../../Logic/Osm/OsmConnection" import Constants from "../../Models/Constants" import ContributorCount from "../../Logic/ContributorCount" import Img from "../Base/Img" import { TypedTranslation } from "../i18n/Translation" +import GeoIndexedStore from "../../Logic/FeatureSource/Actors/GeoIndexedStore" export class OpenIdEditor extends VariableUiElement { constructor( - state: { readonly locationControl: Store }, + mapProperties: { location: Store<{ lon: number; lat: number }>; zoom: Store }, iconStyle?: string, objectId?: string ) { const t = Translations.t.general.attribution super( - state.locationControl.map((location) => { - let elementSelect = "" - if (objectId !== undefined) { - const parts = objectId.split("/") - const tp = parts[0] - if ( - parts.length === 2 && - !isNaN(Number(parts[1])) && - (tp === "node" || tp === "way" || tp === "relation") - ) { - elementSelect = "&" + tp + "=" + parts[1] + mapProperties.location.map( + (location) => { + let elementSelect = "" + if (objectId !== undefined) { + const parts = objectId.split("/") + const tp = parts[0] + if ( + parts.length === 2 && + !isNaN(Number(parts[1])) && + (tp === "node" || tp === "way" || tp === "relation") + ) { + elementSelect = "&" + tp + "=" + parts[1] + } } - } - const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${ - location?.zoom ?? 0 - }/${location?.lat ?? 0}/${location?.lon ?? 0}` - return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, { - url: idLink, - newTab: true, - }) - }) + const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${ + mapProperties.zoom?.data ?? 0 + }/${location?.lat ?? 0}/${location?.lon ?? 0}` + return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, { + url: idLink, + newTab: true, + }) + }, + [mapProperties.zoom] + ) ) } } export class OpenJosm extends Combine { - constructor( - state: { osmConnection: OsmConnection; currentBounds: Store }, - iconStyle?: string - ) { + constructor(osmConnection: OsmConnection, bounds: Store, iconStyle?: string) { const t = Translations.t.general.attribution const josmState = new UIEventSource(undefined) @@ -83,21 +82,21 @@ export class OpenJosm extends Combine { const toggle = new Toggle( new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => { - const bounds: any = state.currentBounds.data - if (bounds === undefined) { - return undefined + const bbox = bounds.data + if (bbox === undefined) { + return } - const top = bounds.getNorth() - const bottom = bounds.getSouth() - const right = bounds.getEast() - const left = bounds.getWest() + const top = bbox.getNorth() + const bottom = bbox.getSouth() + const right = bbox.getEast() + const left = bbox.getWest() const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}` Utils.download(josmLink) .then((answer) => josmState.setData(answer.replace(/\n/g, "").trim())) .catch((_) => josmState.setData("ERROR")) }), undefined, - state.osmConnection.userDetails.map( + osmConnection.userDetails.map( (ud) => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible ) ) @@ -113,14 +112,14 @@ export default class CopyrightPanel extends Combine { private static LicenseObject = CopyrightPanel.GenerateLicenses() constructor(state: { - layoutToUse: LayoutConfig - featurePipeline: FeaturePipeline - currentBounds: Store - locationControl: UIEventSource + layout: LayoutConfig + bounds: Store osmConnection: OsmConnection + dataIsLoading: Store + perLayer: ReadonlyMap }) { const t = Translations.t.general.attribution - const layoutToUse = state.layoutToUse + const layoutToUse = state.layout const iconAttributions: BaseUIElement[] = layoutToUse.usedImages.map( CopyrightPanel.IconAttribution diff --git a/UI/BigComponents/DownloadPanel.ts b/UI/BigComponents/DownloadPanel.ts index d07f95d25..98ffad07e 100644 --- a/UI/BigComponents/DownloadPanel.ts +++ b/UI/BigComponents/DownloadPanel.ts @@ -1,7 +1,6 @@ import { SubtleButton } from "../Base/SubtleButton" import Svg from "../../Svg" import Translations from "../i18n/Translations" -import State from "../../State" import { Utils } from "../../Utils" import Combine from "../Base/Combine" import CheckBoxes from "../Input/Checkboxes" diff --git a/UI/BigComponents/FilterView.ts b/UI/BigComponents/FilterView.ts index e7bdbd59a..c81c02748 100644 --- a/UI/BigComponents/FilterView.ts +++ b/UI/BigComponents/FilterView.ts @@ -1,16 +1,13 @@ import { Utils } from "../../Utils" -import { FixedInputElement } from "../Input/FixedInputElement" -import { RadioButton } from "../Input/RadioButton" import { VariableUiElement } from "../Base/VariableUIElement" -import Toggle, { ClickableToggle } from "../Input/Toggle" +import Toggle from "../Input/Toggle" import Combine from "../Base/Combine" import Translations from "../i18n/Translations" import { Translation } from "../i18n/Translation" import Svg from "../../Svg" import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" import BaseUIElement from "../BaseUIElement" -import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" -import BackgroundSelector from "./BackgroundSelector" +import FilteredLayer from "../../Models/FilteredLayer" import FilterConfig from "../../Models/ThemeConfig/FilterConfig" import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" import { SubstitutedTranslation } from "../SubstitutedTranslation" @@ -18,9 +15,7 @@ import ValidatedTextField from "../Input/ValidatedTextField" import { QueryParameters } from "../../Logic/Web/QueryParameters" import { TagUtils } from "../../Logic/Tags/TagUtils" import { InputElement } from "../Input/InputElement" -import { DropDown } from "../Input/DropDown" import { FixedUiElement } from "../Base/FixedUiElement" -import BaseLayer from "../../Models/BaseLayer" import Loc from "../../Models/Loc" import { BackToThemeOverview } from "./ActionButtons" @@ -272,102 +267,6 @@ export class LayerFilterPanel extends Combine { return [tr, settableFilter] } - private static createCheckboxFilter( - filterConfig: FilterConfig - ): [BaseUIElement, UIEventSource] { - let option = filterConfig.options[0] - - const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6") - const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2 w-6") - const qp = QueryParameters.GetBooleanQueryParameter( - "filter-" + filterConfig.id, - false, - "Is filter '" + filterConfig.options[0].question.textFor("en") + " enabled?" - ) - const toggle = new ClickableToggle( - new Combine([icon, option.question.Clone().SetClass("block")]).SetClass("flex"), - new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass( - "flex" - ), - qp - ) - .ToggleOnClick() - .SetClass("block m-1") - - return [ - toggle, - toggle.isEnabled.sync( - (enabled) => - enabled - ? { - currentFilter: option.osmTags, - state: "true", - } - : undefined, - [], - (f) => f !== undefined - ), - ] - } - - private static createMultiFilter( - filterConfig: FilterConfig - ): [BaseUIElement, UIEventSource] { - let options = filterConfig.options - - const values: FilterState[] = options.map((f, i) => ({ - currentFilter: f.osmTags, - state: i, - })) - let filterPicker: InputElement - const value = QueryParameters.GetQueryParameter( - "filter-" + filterConfig.id, - "0", - "Value for filter " + filterConfig.id - ).sync( - (str) => Number(str), - [], - (n) => "" + n - ) - - if (options.length <= 6) { - filterPicker = new RadioButton( - options.map( - (option, i) => - new FixedInputElement(option.question.Clone().SetClass("block"), i) - ), - { - value, - dontStyle: true, - } - ) - } else { - filterPicker = new DropDown( - "", - options.map((option, i) => ({ - value: i, - shown: option.question.Clone(), - })), - value - ) - } - - return [ - filterPicker, - filterPicker.GetValue().sync( - (i) => values[i], - [], - (selected) => { - const v = selected?.state - if (v === undefined || typeof v === "string") { - return undefined - } - return v - } - ), - ] - } - private static createFilter( state: {}, filterConfig: FilterConfig @@ -376,12 +275,6 @@ export class LayerFilterPanel extends Combine { return LayerFilterPanel.createFilterWithFields(state, filterConfig) } - if (filterConfig.options.length === 1) { - return LayerFilterPanel.createCheckboxFilter(filterConfig) - } - - const filter = LayerFilterPanel.createMultiFilter(filterConfig) - filter[0].SetClass("pl-2") - return filter + return undefined } } diff --git a/UI/BigComponents/Filterview.svelte b/UI/BigComponents/Filterview.svelte new file mode 100644 index 000000000..f1e0c3508 --- /dev/null +++ b/UI/BigComponents/Filterview.svelte @@ -0,0 +1,79 @@ + +{#if filteredLayer.layerDef.name} +
+ + +
+ {#each filteredLayer.layerDef.filters as filter} +
+ + + {#if filter.options.length === 1 && filter.options[0].fields.length === 0} + + {/if} + + {#if filter.options.length > 1} + + {#each filter.options as option, i} + + {/each} + + {/if} + + +
+ {/each} +
+
+ +
+{/if} diff --git a/UI/BigComponents/GeolocationControl.ts b/UI/BigComponents/GeolocationControl.ts index f1aea540c..3ebfd42d2 100644 --- a/UI/BigComponents/GeolocationControl.ts +++ b/UI/BigComponents/GeolocationControl.ts @@ -1,8 +1,7 @@ import { VariableUiElement } from "../Base/VariableUIElement" import Svg from "../../Svg" -import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { UIEventSource } from "../../Logic/UIEventSource" import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler" -import { BBox } from "../../Logic/BBox" import Hotkeys from "../Base/Hotkeys" import Translations from "../i18n/Translations" import Constants from "../../Models/Constants" @@ -94,14 +93,13 @@ export class GeolocationControl extends VariableUiElement { return } - if (geolocationState.currentGPSLocation.data === undefined) { + // A location _is_ known! Let's move to this location + const currentLocation = geolocationState.currentGPSLocation.data + if (currentLocation === undefined) { // No location is known yet, not much we can do lastClick.setData(new Date()) return } - - // A location _is_ known! Let's move to this location - const currentLocation = geolocationState.currentGPSLocation.data const inBounds = state.bounds.data.contains([ currentLocation.longitude, currentLocation.latitude, diff --git a/UI/BigComponents/Geosearch.svelte b/UI/BigComponents/Geosearch.svelte new file mode 100644 index 000000000..fe40cb7c7 --- /dev/null +++ b/UI/BigComponents/Geosearch.svelte @@ -0,0 +1,94 @@ + + +
+
+ + {#if isRunning} + {Translations.t.general.search.searching} + {:else if feedback !== undefined} +
feedback = undefined}> + {feedback} +
+ {:else } + keypr.key === "Enter" ? performSearch() : undefined} + + bind:value={searchContents} + placeholder={Translations.t.general.search.search}> + {/if} + +
+
+ +
+
diff --git a/UI/BigComponents/RightControls.ts b/UI/BigComponents/RightControls.ts index c34daa479..7c5280aba 100644 --- a/UI/BigComponents/RightControls.ts +++ b/UI/BigComponents/RightControls.ts @@ -1,12 +1,6 @@ import Combine from "../Base/Combine" -import Toggle from "../Input/Toggle" -import MapControlButton from "../MapControlButton" -import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler" -import Svg from "../../Svg" import MapState from "../../Logic/State/MapState" -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" import LevelSelector from "./LevelSelector" -import { GeolocationControl } from "./GeolocationControl" export default class RightControls extends Combine { constructor(state: MapState & { featurePipeline: FeaturePipeline }) { diff --git a/UI/BigComponents/SelectedElementView.svelte b/UI/BigComponents/SelectedElementView.svelte new file mode 100644 index 000000000..5c6715326 --- /dev/null +++ b/UI/BigComponents/SelectedElementView.svelte @@ -0,0 +1,75 @@ + + +
+
selectedElement.setData(undefined)}>close
+
+ + new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, layer.data.title, specialVisState), [layer]))}> + +
+ + {#each $layer.titleIcons as titleIconConfig (titleIconConfig.id)} +
+ new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, titleIconConfig, specialVisState)))}> +
+ + {/each} +
+ + +
+ +
    + + {#each Object.keys($_tags) as key} +
  • {key}={$_tags[key]}
  • + {/each} +
+
diff --git a/UI/BigComponents/StatisticsPanel.ts b/UI/BigComponents/StatisticsPanel.ts index ed8dbab49..2bb479b7b 100644 --- a/UI/BigComponents/StatisticsPanel.ts +++ b/UI/BigComponents/StatisticsPanel.ts @@ -4,20 +4,14 @@ import Title from "../Base/Title" import TagRenderingChart from "./TagRenderingChart" import Combine from "../Base/Combine" import Locale from "../i18n/Locale" -import { UIEventSource } from "../../Logic/UIEventSource" -import { OsmFeature } from "../../Models/OsmFeature" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource" +import BaseUIElement from "../BaseUIElement" -export default class StatisticsPanel extends VariableUiElement { - constructor( - elementsInview: UIEventSource<{ element: OsmFeature; layer: LayerConfig }[]>, - state: { - layoutToUse: LayoutConfig - } - ) { +export default class StatisticsForLayerPanel extends VariableUiElement { + constructor(elementsInview: FeatureSourceForLayer) { + const layer = elementsInview.layer.layerDef super( - elementsInview.stabilized(1000).map( + elementsInview.features.stabilized(1000).map( (features) => { if (features === undefined) { return new Loading("Loading data") @@ -25,40 +19,33 @@ export default class StatisticsPanel extends VariableUiElement { if (features.length === 0) { return "No elements in view" } - const els = [] - for (const layer of state.layoutToUse.layers) { - if (layer.name === undefined) { - continue - } - const featuresForLayer = features - .filter((f) => f.layer === layer) - .map((f) => f.element) - if (featuresForLayer.length === 0) { - continue - } - els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8")) - - const layerStats = [] - for (const tagRendering of layer?.tagRenderings ?? []) { - const chart = new TagRenderingChart(featuresForLayer, tagRendering, { - chartclasses: "w-full", - chartstyle: "height: 60rem", - includeTitle: false, - }) - const title = new Title( - tagRendering.question?.Clone() ?? tagRendering.id, - 4 - ).SetClass("mt-8") - if (!chart.HasClass("hidden")) { - layerStats.push( - new Combine([title, chart]).SetClass( - "flex flex-col w-full lg:w-1/3" - ) - ) - } - } - els.push(new Combine(layerStats).SetClass("flex flex-wrap")) + const els: BaseUIElement[] = [] + const featuresForLayer = features + if (featuresForLayer.length === 0) { + return } + els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8")) + + const layerStats = [] + for (const tagRendering of layer?.tagRenderings ?? []) { + const chart = new TagRenderingChart(featuresForLayer, tagRendering, { + chartclasses: "w-full", + chartstyle: "height: 60rem", + includeTitle: false, + }) + const title = new Title( + tagRendering.question?.Clone() ?? tagRendering.id, + 4 + ).SetClass("mt-8") + if (!chart.HasClass("hidden")) { + layerStats.push( + new Combine([title, chart]).SetClass( + "flex flex-col w-full lg:w-1/3" + ) + ) + } + } + els.push(new Combine(layerStats).SetClass("flex flex-wrap")) return new Combine(els) }, [Locale.language] diff --git a/UI/BigComponents/ThemeIntroductionPanel.ts b/UI/BigComponents/ThemeIntroductionPanel.ts index 02dc660c6..587cc0be4 100644 --- a/UI/BigComponents/ThemeIntroductionPanel.ts +++ b/UI/BigComponents/ThemeIntroductionPanel.ts @@ -11,6 +11,7 @@ import LoggedInUserIndicator from "../LoggedInUserIndicator" import { ActionButtons } from "./ActionButtons" import { BBox } from "../../Logic/BBox" import Loc from "../../Models/Loc" +import { DefaultGuiState } from "../DefaultGuiState" export default class ThemeIntroductionPanel extends Combine { constructor( @@ -24,6 +25,7 @@ export default class ThemeIntroductionPanel extends Combine { osmConnection: OsmConnection currentBounds: Store locationControl: UIEventSource + defaultGuiState: DefaultGuiState }, guistate?: { userInfoIsOpened: UIEventSource } ) { diff --git a/UI/BigComponents/UploadTraceToOsmUI.ts b/UI/BigComponents/UploadTraceToOsmUI.ts index 290a6b3db..3d7c43910 100644 --- a/UI/BigComponents/UploadTraceToOsmUI.ts +++ b/UI/BigComponents/UploadTraceToOsmUI.ts @@ -17,7 +17,7 @@ export default class UploadTraceToOsmUI extends LoginToggle { constructor( trace: (title: string) => string, state: { - layoutToUse: LayoutConfig + layout: LayoutConfig osmConnection: OsmConnection readonly featureSwitchUserbadge: Store }, diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts index c793c7a42..af3cc05dc 100644 --- a/UI/DefaultGUI.ts +++ b/UI/DefaultGUI.ts @@ -1,5 +1,4 @@ import FeaturePipelineState from "../Logic/State/FeaturePipelineState" -import State from "../State" import { Utils } from "../Utils" import { UIEventSource } from "../Logic/UIEventSource" import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs" @@ -11,14 +10,11 @@ import BaseUIElement from "./BaseUIElement" import LeftControls from "./BigComponents/LeftControls" import RightControls from "./BigComponents/RightControls" import CenterMessageBox from "./CenterMessageBox" -import ShowDataLayer from "./ShowDataLayer/ShowDataLayer" import ScrollableFullScreen from "./Base/ScrollableFullScreen" import Translations from "./i18n/Translations" import SimpleAddUI from "./BigComponents/SimpleAddUI" import StrayClickHandler from "../Logic/Actors/StrayClickHandler" import { DefaultGuiState } from "./DefaultGuiState" -import LayerConfig from "../Models/ThemeConfig/LayerConfig" -import home_location_json from "../assets/layers/home_location/home_location.json" import NewNoteUi from "./Popup/NewNoteUi" import Combine from "./Base/Combine" import AddNewMarker from "./BigComponents/AddNewMarker" @@ -32,7 +28,6 @@ import { FixedUiElement } from "./Base/FixedUiElement" import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" import { GeoLocationState } from "../Logic/State/GeoLocationState" import Hotkeys from "./Base/Hotkeys" -import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers" import CopyrightPanel from "./BigComponents/CopyrightPanel" import SvelteUIElement from "./Base/SvelteUIElement" import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte" @@ -50,9 +45,6 @@ export default class DefaultGUI { constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { this.state = state this.guiState = guiState - if (this.state.featureSwitchGeolocation.data) { - this.geolocationHandler = new GeoLocationHandler(new GeoLocationState(), state) - } } public setup() { @@ -74,10 +66,6 @@ export default class DefaultGUI { this.state.backgroundLayer.setData(AvailableBaseLayers.osmCarto) } ) - - Utils.downloadJson("./service-worker-version") - .then((data) => console.log("Service worker", data)) - .catch((_) => console.log("Service worker not active")) } public setupClickDialogOnMap( @@ -173,13 +161,6 @@ export default class DefaultGUI { this.setupClickDialogOnMap(guiState.filterViewIsOpened, state) - new ShowDataLayer({ - leafletMap: state.leafletMap, - layerToShow: new LayerConfig(home_location_json, "home_location", true), - features: state.homeLocation, - state, - }) - const selectedElement: FilteredLayer = state.filteredLayers.data.filter( (l) => l.layerDef.id === "selected_element" )[0] @@ -285,23 +266,6 @@ export default class DefaultGUI { .SetClass("flex items-center justify-center normal-background h-full") .AttachTo("on-small-screen") - new Combine([ - Toggle.If(state.featureSwitchSearch, () => { - const search = new SearchAndGo(state).SetClass( - "shadow rounded-full h-min w-full overflow-hidden sm:max-w-sm pointer-events-auto" - ) - Hotkeys.RegisterHotkey( - { ctrl: "F" }, - Translations.t.hotkeyDocumentation.selectSearch, - () => { - search.focus() - } - ) - - return search - }), - ]).AttachTo("top-right") - new LeftControls(state, guiState).AttachTo("bottom-left") new RightControls(state, this.geolocationHandler).AttachTo("bottom-right") diff --git a/UI/DefaultGuiState.ts b/UI/DefaultGuiState.ts index e56870e1a..48b9ad71f 100644 --- a/UI/DefaultGuiState.ts +++ b/UI/DefaultGuiState.ts @@ -1,13 +1,13 @@ import { UIEventSource } from "../Logic/UIEventSource" -import { QueryParameters } from "../Logic/Web/QueryParameters" import Hash from "../Logic/Web/Hash" export class DefaultGuiState { - static state: DefaultGuiState - public readonly welcomeMessageIsOpened: UIEventSource = new UIEventSource( false ) + + public readonly menuIsOpened: UIEventSource = new UIEventSource(false) + public readonly downloadControlIsOpened: UIEventSource = new UIEventSource( false ) @@ -22,25 +22,17 @@ export class DefaultGuiState { public readonly userInfoFocusedQuestion: UIEventSource = new UIEventSource( undefined ) - public readonly welcomeMessageOpenedTab: UIEventSource + + private readonly sources: Record> = { + welcome: this.welcomeMessageIsOpened, + download: this.downloadControlIsOpened, + filters: this.filterViewIsOpened, + copyright: this.copyrightViewIsOpened, + currentview: this.currentViewControlIsOpened, + userinfo: this.userInfoIsOpened, + } constructor() { - this.welcomeMessageOpenedTab = UIEventSource.asFloat( - QueryParameters.GetQueryParameter( - "tab", - "0", - `The tab that is shown in the welcome-message.` - ) - ) - const sources = { - welcome: this.welcomeMessageIsOpened, - download: this.downloadControlIsOpened, - filters: this.filterViewIsOpened, - copyright: this.copyrightViewIsOpened, - currentview: this.currentViewControlIsOpened, - userinfo: this.userInfoIsOpened, - } - const self = this this.userInfoIsOpened.addCallback((isOpen) => { if (!isOpen) { @@ -49,10 +41,16 @@ export class DefaultGuiState { } }) - sources[Hash.hash.data?.toLowerCase()]?.setData(true) + this.sources[Hash.hash.data?.toLowerCase()]?.setData(true) if (Hash.hash.data === "" || Hash.hash.data === undefined) { this.welcomeMessageIsOpened.setData(true) } } + + public closeAll() { + for (const sourceKey in this.sources) { + this.sources[sourceKey].setData(false) + } + } } diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts index 636750607..93f182e22 100644 --- a/UI/Image/DeleteImage.ts +++ b/UI/Image/DeleteImage.ts @@ -13,7 +13,7 @@ export default class DeleteImage extends Toggle { constructor( key: string, tags: Store, - state: { layoutToUse: LayoutConfig; changes?: Changes; osmConnection?: OsmConnection } + state: { layout: LayoutConfig; changes?: Changes; osmConnection?: OsmConnection } ) { const oldValue = tags.data[key] const isDeletedBadge = Translations.t.image.isDeleted @@ -24,7 +24,7 @@ export default class DeleteImage extends Toggle { await state?.changes?.applyAction( new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, { changeType: "delete-image", - theme: state.layoutToUse.id, + theme: state.layout.id, }) ) }) @@ -39,7 +39,7 @@ export default class DeleteImage extends Toggle { await state?.changes?.applyAction( new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data, { changeType: "answer", - theme: state.layoutToUse.id, + theme: state.layout.id, }) ) }) diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index c9ca57f3c..8818252d7 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -14,7 +14,7 @@ export class ImageCarousel extends Toggle { constructor( images: Store<{ key: string; url: string; provider: ImageProvider }[]>, tags: Store, - state: { osmConnection?: OsmConnection; changes?: Changes; layoutToUse: LayoutConfig } + state: { osmConnection?: OsmConnection; changes?: Changes; layout: LayoutConfig } ) { const uiElements = images.map( (imageURLS: { key: string; url: string; provider: ImageProvider }[]) => { diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index 84011ce96..30842a14a 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -11,26 +11,19 @@ import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import { FixedUiElement } from "../Base/FixedUiElement" import { VariableUiElement } from "../Base/VariableUIElement" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import { OsmConnection } from "../../Logic/Osm/OsmConnection" -import { Changes } from "../../Logic/Osm/Changes" import Loading from "../Base/Loading" import { LoginToggle } from "../Popup/LoginButton" import Constants from "../../Models/Constants" import { DefaultGuiState } from "../DefaultGuiState" import ScrollableFullScreen from "../Base/ScrollableFullScreen" +import { SpecialVisualizationState } from "../SpecialVisualization" export class ImageUploadFlow extends Toggle { private static readonly uploadCountsPerId = new Map>() constructor( tagsSource: Store, - state: { - osmConnection: OsmConnection - layoutToUse: LayoutConfig - changes: Changes - featureSwitchUserbadge: Store - }, + state: SpecialVisualizationState, imagePrefix: string = "image", text: string = undefined ) { @@ -56,7 +49,7 @@ export class ImageUploadFlow extends Toggle { await state.changes.applyAction( new ChangeTagAction(tags.id, new Tag(key, url), tagsSource.data, { changeType: "add-image", - theme: state.layoutToUse.id, + theme: state.layout.id, }) ) console.log("Adding image:" + key, url) @@ -111,7 +104,7 @@ export class ImageUploadFlow extends Toggle { const tags = tagsSource.data - const layout = state?.layoutToUse + const layout = state?.layout let matchingLayer: LayerConfig = undefined for (const layer of layout?.layers ?? []) { if (layer.source.osmTags.matchesProperties(tags)) { diff --git a/UI/Input/LengthInput.ts b/UI/Input/LengthInput.ts index 21d813681..1e538896e 100644 --- a/UI/Input/LengthInput.ts +++ b/UI/Input/LengthInput.ts @@ -1,31 +1,30 @@ import { InputElement } from "./InputElement" -import { UIEventSource } from "../../Logic/UIEventSource" +import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" import Combine from "../Base/Combine" import Svg from "../../Svg" -import { Utils } from "../../Utils" import Loc from "../../Models/Loc" import { GeoOperations } from "../../Logic/GeoOperations" -import Minimap, { MinimapObj } from "../Base/Minimap" import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" import BaseUIElement from "../BaseUIElement" +import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers" /** * Selects a length after clicking on the minimap, in meters */ export default class LengthInput extends InputElement { - private readonly _location: UIEventSource + private readonly _location: Store private readonly value: UIEventSource - private readonly background: UIEventSource + private readonly background: Store constructor( - mapBackground: UIEventSource, location: UIEventSource, + mapBackground?: UIEventSource, value?: UIEventSource ) { super() this._location = location this.value = value ?? new UIEventSource(undefined) - this.background = mapBackground + this.background = mapBackground ?? new ImmutableStore(AvailableRasterLayers.osmCarto) this.SetClass("block") } @@ -41,28 +40,26 @@ export default class LengthInput extends InputElement { protected InnerConstructElement(): HTMLElement { let map: BaseUIElement & MinimapObj = undefined let layerControl: BaseUIElement = undefined - if (!Utils.runningFromConsole) { - map = Minimap.createMiniMap({ - background: this.background, - allowMoving: false, - location: this._location, - attribution: true, - leafletOptions: { - tap: true, - }, - }) + map = Minimap.createMiniMap({ + background: this.background, + allowMoving: false, + location: this._location, + attribution: true, + leafletOptions: { + tap: true, + }, + }) - layerControl = new BackgroundMapSwitch( - { - locationControl: this._location, - backgroundLayer: this.background, - }, - this.background, - { - allowedCategories: ["map", "photo"], - } - ) - } + layerControl = new BackgroundMapSwitch( + { + locationControl: this._location, + backgroundLayer: this.background, + }, + this.background, + { + allowedCategories: ["map", "photo"], + } + ) const crosshair = new Combine([ Svg.length_crosshair_svg().SetStyle( `position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);` diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 45c57fb3b..4cc8864f6 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -3,7 +3,7 @@ import * as EmailValidator from "email-validator" import { parsePhoneNumberFromString } from "libphonenumber-js" import { InputElement } from "./InputElement" import { TextField } from "./TextField" -import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" import CombinedInputElement from "./CombinedInputElement" import SimpleDatePicker from "./SimpleDatePicker" import OpeningHoursInput from "../OpeningHours/OpeningHoursInput" @@ -25,6 +25,7 @@ import InputElementMap from "./InputElementMap" import Translations from "../i18n/Translations" import { Translation } from "../i18n/Translation" import Locale from "../i18n/Locale" +import { RasterLayerPolygon } from "../../Models/RasterLayers" export class TextFieldDef { public readonly name: string @@ -638,7 +639,7 @@ class LengthTextField extends TextFieldDef { location?: [number, number] args?: string[] feature?: any - mapBackgroundLayer?: Store + mapBackgroundLayer?: Store } ) => { options = options ?? {} @@ -674,14 +675,18 @@ class LengthTextField extends TextFieldDef { zoom: zoom, }) if (args[1]) { - // We have a prefered map! + // The arguments indicate the preferred background type options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo( location, - new UIEventSource(args[1].split(",")) + new ImmutableStore(args[1].split(",")) ) } const background = options?.mapBackgroundLayer - const li = new LengthInput(new UIEventSource(background.data), location, value) + const li = new LengthInput( + new UIEventSource(background.data), + location, + value + ) li.SetStyle("height: 20rem;") return li } diff --git a/UI/Map/MapLibreAdaptor.ts b/UI/Map/MapLibreAdaptor.ts index a4e6c1597..f3232168b 100644 --- a/UI/Map/MapLibreAdaptor.ts +++ b/UI/Map/MapLibreAdaptor.ts @@ -21,12 +21,20 @@ export class MapLibreAdaptor implements MapProperties { "keyboard", "touchZoomRotate", ] + private static maplibre_zoom_handlers = [ + "scrollZoom", + "boxZoom", + "doubleClickZoom", + "touchZoomRotate", + ] readonly location: UIEventSource<{ lon: number; lat: number }> readonly zoom: UIEventSource - readonly bounds: Store + readonly bounds: UIEventSource readonly rasterLayer: UIEventSource readonly maxbounds: UIEventSource readonly allowMoving: UIEventSource + readonly allowZooming: UIEventSource + readonly lastClickLocation: Store private readonly _maplibreMap: Store private readonly _bounds: UIEventSource /** @@ -50,11 +58,14 @@ export class MapLibreAdaptor implements MapProperties { }) this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined) this.allowMoving = state?.allowMoving ?? new UIEventSource(true) + this.allowZooming = state?.allowZooming ?? new UIEventSource(true) this._bounds = new UIEventSource(undefined) this.bounds = this._bounds this.rasterLayer = state?.rasterLayer ?? new UIEventSource(undefined) + const lastClickLocation = new UIEventSource<{ lon: number; lat: number }>(undefined) + this.lastClickLocation = lastClickLocation const self = this maplibreMap.addCallbackAndRunD((map) => { map.on("load", () => { @@ -63,11 +74,13 @@ export class MapLibreAdaptor implements MapProperties { self.SetZoom(self.zoom.data) self.setMaxBounds(self.maxbounds.data) self.setAllowMoving(self.allowMoving.data) + self.setAllowZooming(self.allowZooming.data) }) self.MoveMapToCurrentLoc(self.location.data) self.SetZoom(self.zoom.data) self.setMaxBounds(self.maxbounds.data) self.setAllowMoving(self.allowMoving.data) + self.setAllowZooming(self.allowZooming.data) map.on("moveend", () => { const dt = this.location.data dt.lon = map.getCenter().lng @@ -81,6 +94,11 @@ export class MapLibreAdaptor implements MapProperties { ]) self._bounds.setData(bbox) }) + map.on("click", (e) => { + const lon = e.lngLat.lng + const lat = e.lngLat.lat + lastClickLocation.setData({ lon, lat }) + }) }) this.rasterLayer.addCallback((_) => @@ -95,6 +113,8 @@ export class MapLibreAdaptor implements MapProperties { this.zoom.addCallbackAndRunD((z) => self.SetZoom(z)) this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox)) this.allowMoving.addCallbackAndRun((allowMoving) => self.setAllowMoving(allowMoving)) + this.allowZooming.addCallbackAndRun((allowZooming) => self.setAllowZooming(allowZooming)) + this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds)) } /** @@ -205,7 +225,7 @@ export class MapLibreAdaptor implements MapProperties { // already the correct background layer, nothing to do return } - if (background === undefined) { + if (!background?.url) { // no background to set this.removeCurrentLayer(map) this._currentRasterLayer = undefined @@ -266,4 +286,38 @@ export class MapLibreAdaptor implements MapProperties { } } } + + private setAllowZooming(allow: true | boolean | undefined) { + const map = this._maplibreMap.data + if (map === undefined) { + return + } + if (allow === false) { + for (const id of MapLibreAdaptor.maplibre_zoom_handlers) { + map[id].disable() + } + } else { + for (const id of MapLibreAdaptor.maplibre_zoom_handlers) { + map[id].enable() + } + } + } + + private setBounds(bounds: BBox) { + const map = this._maplibreMap.data + if (map === undefined) { + return + } + const oldBounds = map.getBounds() + const e = 0.0000001 + const hasDiff = + Math.abs(oldBounds.getWest() - bounds.getWest()) > e && + Math.abs(oldBounds.getEast() - bounds.getEast()) > e && + Math.abs(oldBounds.getNorth() - bounds.getNorth()) > e && + Math.abs(oldBounds.getSouth() - bounds.getSouth()) > e + if (!hasDiff) { + return + } + map.fitBounds(bounds.toLngLat()) + } } diff --git a/UI/Map/ShowDataLayer.ts b/UI/Map/ShowDataLayer.ts index 5694ae2d6..3032a4eff 100644 --- a/UI/Map/ShowDataLayer.ts +++ b/UI/Map/ShowDataLayer.ts @@ -1,4 +1,4 @@ -import { ImmutableStore, Store } from "../../Logic/UIEventSource" +import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" import type { Map as MlMap } from "maplibre-gl" import { GeoJSONSource, Marker } from "maplibre-gl" import { ShowDataLayerOptions } from "./ShowDataLayerOptions" @@ -14,10 +14,13 @@ import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig" import { Utils } from "../../Utils" import * as range_layer from "../../assets/layers/range/range.json" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" +import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" +import FilteredLayer from "../../Models/FilteredLayer" +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" class PointRenderingLayer { private readonly _config: PointRenderingConfig - private readonly _fetchStore?: (id: string) => Store + private readonly _fetchStore?: (id: string) => Store> private readonly _map: MlMap private readonly _onClick: (feature: Feature) => void private readonly _allMarkers: Map = new Map() @@ -27,7 +30,7 @@ class PointRenderingLayer { features: FeatureSource, config: PointRenderingConfig, visibility?: Store, - fetchStore?: (id: string) => Store, + fetchStore?: (id: string) => Store>, onClick?: (feature: Feature) => void ) { this._config = config @@ -96,7 +99,7 @@ class PointRenderingLayer { } private addPoint(feature: Feature, loc: [number, number]): Marker { - let store: Store + let store: Store> if (this._fetchStore) { store = this._fetchStore(feature.properties.id) } else { @@ -143,7 +146,7 @@ class LineRenderingLayer { private readonly _map: MlMap private readonly _config: LineRenderingConfig private readonly _visibility?: Store - private readonly _fetchStore?: (id: string) => Store + private readonly _fetchStore?: (id: string) => Store> private readonly _onClick?: (feature: Feature) => void private readonly _layername: string private readonly _listenerInstalledOn: Set = new Set() @@ -154,7 +157,7 @@ class LineRenderingLayer { layername: string, config: LineRenderingConfig, visibility?: Store, - fetchStore?: (id: string) => Store, + fetchStore?: (id: string) => Store>, onClick?: (feature: Feature) => void ) { this._layername = layername @@ -212,9 +215,10 @@ class LineRenderingLayer { promoteId: "id", }) // @ts-ignore + const linelayer = this._layername + "_line" map.addLayer({ source: this._layername, - id: this._layername + "_line", + id: linelayer, type: "line", paint: { "line-color": ["feature-state", "color"], @@ -227,9 +231,10 @@ class LineRenderingLayer { }, }) + const polylayer = this._layername + "_polygon" map.addLayer({ source: this._layername, - id: this._layername + "_polygon", + id: polylayer, type: "fill", filter: ["in", ["geometry-type"], ["literal", ["Polygon", "MultiPolygon"]]], layout: {}, @@ -238,6 +243,11 @@ class LineRenderingLayer { "fill-opacity": 0.1, }, }) + + this._visibility.addCallbackAndRunD((visible) => { + map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none") + map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none") + }) } else { src.setData({ type: "FeatureCollection", @@ -295,6 +305,24 @@ export default class ShowDataLayer { map.addCallbackAndRunD((map) => self.initDrawFeatures(map)) } + public static showMultipleLayers( + mlmap: UIEventSource, + features: FeatureSource, + layers: LayerConfig[], + options?: Partial + ) { + const perLayer = new PerLayerFeatureSourceSplitter( + layers.map((l) => new FilteredLayer(l)), + new StaticFeatureSource(features) + ) + perLayer.forEach((fs) => { + new ShowDataLayer(mlmap, { + layer: fs.layer.layerDef, + features: fs, + ...(options ?? {}), + }) + }) + } public static showRange( map: Store, features: FeatureSource, @@ -318,8 +346,11 @@ export default class ShowDataLayer { } private initDrawFeatures(map: MlMap) { - const { features, doShowLayer, fetchStore, selectedElement } = this._options - const onClick = (feature: Feature) => selectedElement?.setData(feature) + let { features, doShowLayer, fetchStore, selectedElement, selectedLayer } = this._options + const onClick = (feature: Feature) => { + 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( diff --git a/UI/Map/ShowDataLayerOptions.ts b/UI/Map/ShowDataLayerOptions.ts index 524e9847c..c20960a72 100644 --- a/UI/Map/ShowDataLayerOptions.ts +++ b/UI/Map/ShowDataLayerOptions.ts @@ -1,6 +1,8 @@ import FeatureSource from "../../Logic/FeatureSource/FeatureSource" import { Store, UIEventSource } from "../../Logic/UIEventSource" import { OsmTags } from "../../Models/OsmFeature" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { Feature } from "geojson" export interface ShowDataLayerOptions { /** @@ -11,7 +13,12 @@ export interface ShowDataLayerOptions { * Indication of the current selected element; overrides some filters. * When a feature is tapped, the feature will be put in there */ - selectedElement?: UIEventSource + selectedElement?: UIEventSource + + /** + * When a feature of this layer is tapped, the layer will be marked + */ + selectedLayer?: UIEventSource /** * If set, zoom to the features when initially loaded and when they are changed @@ -26,5 +33,5 @@ export interface ShowDataLayerOptions { * Function which fetches the relevant store. * If given, the map will update when a property is changed */ - fetchStore?: (id: string) => UIEventSource + fetchStore?: (id: string) => Store> } diff --git a/UI/Map/ShowDataMultiLayer.ts b/UI/Map/ShowDataMultiLayer.ts deleted file mode 100644 index 677e71d76..000000000 --- a/UI/Map/ShowDataMultiLayer.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * SHows geojson on the given leaflet map, but attempts to figure out the correct layer first - */ -import { ImmutableStore, Store } from "../../Logic/UIEventSource" -import ShowDataLayer from "./ShowDataLayer" -import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" -import FilteredLayer from "../../Models/FilteredLayer" -import { ShowDataLayerOptions } from "./ShowDataLayerOptions" -import { Map as MlMap } from "maplibre-gl" -import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource" -import { GlobalFilter } from "../../Models/GlobalFilter" - -export default class ShowDataMultiLayer { - constructor( - map: Store, - options: ShowDataLayerOptions & { - layers: FilteredLayer[] - globalFilters?: Store - } - ) { - new PerLayerFeatureSourceSplitter( - new ImmutableStore(options.layers), - (features, layer) => { - const newOptions = { - ...options, - layer: layer.layerDef, - features: new FilteringFeatureSource( - layer, - features, - options.fetchStore, - options.globalFilters - ), - } - new ShowDataLayer(map, newOptions) - }, - options.features - ) - } -} diff --git a/UI/NewPoint/ConfirmLocationOfPoint.ts b/UI/NewPoint/ConfirmLocationOfPoint.ts index 47a203ecb..dd2c6f4bb 100644 --- a/UI/NewPoint/ConfirmLocationOfPoint.ts +++ b/UI/NewPoint/ConfirmLocationOfPoint.ts @@ -1,6 +1,4 @@ import { UIEventSource } from "../../Logic/UIEventSource" -import { OsmConnection } from "../../Logic/Osm/OsmConnection" -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" import BaseUIElement from "../BaseUIElement" import LocationInput from "../Input/LocationInput" import { BBox } from "../../Logic/BBox" @@ -18,18 +16,13 @@ import { Tag } from "../../Logic/Tags/Tag" import { WayId } from "../../Models/OsmFeature" import { Translation } from "../i18n/Translation" import { Feature } from "geojson" -import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers" -import { GlobalFilter } from "../../Logic/State/GlobalFilter" +import { AvailableRasterLayers } from "../../Models/RasterLayers" +import { SpecialVisualizationState } from "../SpecialVisualization" +import ClippedFeatureSource from "../../Logic/FeatureSource/Sources/ClippedFeatureSource" export default class ConfirmLocationOfPoint extends Combine { constructor( - state: { - globalFilters: UIEventSource - featureSwitchIsTesting: UIEventSource - osmConnection: OsmConnection - featurePipeline: FeaturePipeline - backgroundLayer?: UIEventSource - }, + state: SpecialVisualizationState, filterViewIsOpened: UIEventSource, preset: PresetInfo, confirmText: BaseUIElement, @@ -55,7 +48,7 @@ export default class ConfirmLocationOfPoint extends Combine { const locationSrc = new UIEventSource(zloc) let backgroundLayer = new UIEventSource( - state?.backgroundLayer?.data ?? AvailableRasterLayers.osmCarto + state?.mapProperties.rasterLayer?.data ?? AvailableRasterLayers.osmCarto ) if (preset.preciseInput.preferredBackground) { const defaultBackground = AvailableRasterLayers.SelectBestLayerAccordingTo( @@ -105,15 +98,13 @@ export default class ConfirmLocationOfPoint extends Combine { Math.max(preset.boundsFactor ?? 0.25, 2) ) loadedBbox = bbox - const allFeatures: Feature[] = [] - preset.preciseInput.snapToLayers.forEach((layerId) => { - console.log("Snapping to", layerId) - state.featurePipeline - .GetFeaturesWithin(layerId, bbox) - ?.forEach((feats) => allFeatures.push(...(feats))) - }) - console.log("Snapping to", allFeatures) - snapToFeatures.setData(allFeatures) + const sources = preset.preciseInput.snapToLayers.map( + (layerId) => + new ClippedFeatureSource( + state.perLayer.get(layerId), + bbox.asGeoJson({}) + ) + ) }) } } diff --git a/UI/OpeningHours/OpeningHours.ts b/UI/OpeningHours/OpeningHours.ts index 8587609b2..a0ce4ec00 100644 --- a/UI/OpeningHours/OpeningHours.ts +++ b/UI/OpeningHours/OpeningHours.ts @@ -488,7 +488,7 @@ export class OH { } public static CreateOhObject( - tags: object & { _lat: number; _lon: number; _country?: string }, + tags: Record & { _lat: number; _lon: number; _country?: string }, textToParse: string ) { // noinspection JSPotentiallyInvalidConstructorUsage diff --git a/UI/OpeningHours/OpeningHoursVisualization.ts b/UI/OpeningHours/OpeningHoursVisualization.ts index 011d59bb2..be9acd840 100644 --- a/UI/OpeningHours/OpeningHoursVisualization.ts +++ b/UI/OpeningHours/OpeningHoursVisualization.ts @@ -23,7 +23,7 @@ export default class OpeningHoursVisualization extends Toggle { ] constructor( - tags: UIEventSource, + tags: UIEventSource>, state: { osmConnection?: OsmConnection }, key: string, prefix = "", @@ -49,7 +49,7 @@ export default class OpeningHoursVisualization extends Toggle { } try { return OpeningHoursVisualization.CreateFullVisualisation( - OH.CreateOhObject(tags.data, ohtext) + OH.CreateOhObject(tags.data, ohtext) ) } catch (e) { console.warn(e, e.stack) diff --git a/UI/Popup/AddNoteCommentViz.ts b/UI/Popup/AddNoteCommentViz.ts index 8f0eb7ac9..0749d5cca 100644 --- a/UI/Popup/AddNoteCommentViz.ts +++ b/UI/Popup/AddNoteCommentViz.ts @@ -8,7 +8,8 @@ import Toggle from "../Input/Toggle" import { LoginToggle } from "./LoginButton" import Combine from "../Base/Combine" import Title from "../Base/Title" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" +import { UIEventSource } from "../../Logic/UIEventSource" export class AddNoteCommentViz implements SpecialVisualization { funcName = "add_note_comment" @@ -21,7 +22,11 @@ export class AddNoteCommentViz implements SpecialVisualization { }, ] - public constr(state, tags, args) { + public constr( + state: SpecialVisualizationState, + tags: UIEventSource>, + args: string[] + ) { const t = Translations.t.notes const textField = new TextField({ placeholder: t.addCommentPlaceholder, @@ -62,12 +67,11 @@ export class AddNoteCommentViz implements SpecialVisualization { return t.addCommentAndClose }) ) - ).onClick(() => { + ).onClick(async () => { const id = tags.data[args[1] ?? "id"] - state.osmConnection.closeNote(id, txt.data).then((_) => { - tags.data["closed_at"] = new Date().toISOString() - tags.ping() - }) + await state.osmConnection.closeNote(id, txt.data) + tags.data["closed_at"] = new Date().toISOString() + tags.ping() }) const reopen = new SubtleButton( @@ -80,12 +84,11 @@ export class AddNoteCommentViz implements SpecialVisualization { return t.reopenNoteAndComment }) ) - ).onClick(() => { + ).onClick(async () => { const id = tags.data[args[1] ?? "id"] - state.osmConnection.reopenNote(id, txt.data).then((_) => { - tags.data["closed_at"] = undefined - tags.ping() - }) + await state.osmConnection.reopenNote(id, txt.data) + tags.data["closed_at"] = undefined + tags.ping() }) const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "") diff --git a/UI/Popup/AutoApplyButton.ts b/UI/Popup/AutoApplyButton.ts index 51d004e51..47d1b633f 100644 --- a/UI/Popup/AutoApplyButton.ts +++ b/UI/Popup/AutoApplyButton.ts @@ -1,7 +1,5 @@ -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" import BaseUIElement from "../BaseUIElement" import { Stores, UIEventSource } from "../../Logic/UIEventSource" -import { DefaultGuiState } from "../DefaultGuiState" import { SubtleButton } from "../Base/SubtleButton" import Img from "../Base/Img" import { FixedUiElement } from "../Base/FixedUiElement" @@ -9,8 +7,6 @@ import Combine from "../Base/Combine" import Link from "../Base/Link" import { SubstitutedTranslation } from "../SubstitutedTranslation" import { Utils } from "../../Utils" -import Minimap from "../Base/Minimap" -import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import { VariableUiElement } from "../Base/VariableUIElement" import Loading from "../Base/Loading" @@ -23,15 +19,21 @@ import FilteredLayer from "../../Models/FilteredLayer" import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" import Lazy from "../Base/Lazy" import List from "../Base/List" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" +import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource" +import { MapLibreAdaptor } from "../Map/MapLibreAdaptor" +import ShowDataLayer from "../Map/ShowDataLayer" +import SvelteUIElement from "../Base/SvelteUIElement" +import MaplibreMap from "../Map/MaplibreMap.svelte" export interface AutoAction extends SpecialVisualization { supportsAutoAction: boolean applyActionOn( state: { - layoutToUse: LayoutConfig + layout: LayoutConfig changes: Changes + indexedFeatures: IndexedFeatureSource }, tagSource: UIEventSource, argument: string[] @@ -43,7 +45,7 @@ class ApplyButton extends UIElement { private readonly text: string private readonly targetTagRendering: string private readonly target_layer_id: string - private readonly state: FeaturePipelineState + private readonly state: SpecialVisualizationState private readonly target_feature_ids: string[] private readonly buttonState = new UIEventSource< "idle" | "running" | "done" | { error: string } @@ -52,7 +54,7 @@ class ApplyButton extends UIElement { private readonly tagRenderingConfig: TagRenderingConfig constructor( - state: FeaturePipelineState, + state: SpecialVisualizationState, target_feature_ids: string[], options: { target_layer_id: string @@ -68,9 +70,7 @@ class ApplyButton extends UIElement { this.targetTagRendering = options.targetTagRendering this.text = options.text this.icon = options.icon - this.layer = this.state.filteredLayers.data.find( - (l) => l.layerDef.id === this.target_layer_id - ) + this.layer = this.state.layerState.filteredLayers.get(this.target_layer_id) this.tagRenderingConfig = this.layer.layerDef.tagRenderings.find( (tr) => tr.id === this.targetTagRendering ) @@ -101,22 +101,23 @@ class ApplyButton extends UIElement { ), ]).SetClass("subtle") - const previewMap = Minimap.createMiniMap({ - allowMoving: false, - background: this.state.backgroundLayer, - addLayerControl: true, - }).SetClass("h-48") + const mlmap = new UIEventSource(undefined) + const mla = new MapLibreAdaptor(mlmap, { + rasterLayer: this.state.mapProperties.rasterLayer, + }) + mla.allowZooming.setData(false) + mla.allowMoving.setData(false) + + const previewMap = new SvelteUIElement(MaplibreMap, { map: mlmap }).SetClass("h-48") const features = this.target_feature_ids.map((id) => - this.state.allElements.ContainingFeatures.get(id) + this.state.indexedFeatures.featuresById.data.get(id) ) - new ShowDataLayer({ - leafletMap: previewMap.leafletMap, - zoomToFeatures: true, + new ShowDataLayer(mlmap, { features: StaticFeatureSource.fromGeojson(features), - state: this.state, - layerToShow: this.layer.layerDef, + zoomToFeatures: true, + layer: this.layer.layerDef, }) return new VariableUiElement( @@ -144,7 +145,7 @@ class ApplyButton extends UIElement { console.log("Applying auto-action on " + this.target_feature_ids.length + " features") for (const targetFeatureId of this.target_feature_ids) { - const featureTags = this.state.allElements.getEventSourceById(targetFeatureId) + const featureTags = this.state.featureProperties.getStore(targetFeatureId) const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt const specialRenderings = Utils.NoNull( SubstitutedTranslation.ExtractSpecialComponents(rendering).map((x) => x.special) @@ -153,8 +154,8 @@ class ApplyButton extends UIElement { if (specialRenderings.length == 0) { console.warn( "AutoApply: feature " + - targetFeatureId + - " got a rendering without supported auto actions:", + targetFeatureId + + " got a rendering without supported auto actions:", rendering ) } @@ -224,7 +225,7 @@ export default class AutoApplyButton implements SpecialVisualization { "To effectively use this button, you'll need some ingredients:", new List([ "A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " + - supportedActions.join(", "), + supportedActions.join(", "), "A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ", new Link("current_view", "./BuiltinLayers.md#current_view"), "Then, use a calculated tag on the host feature to determine the overlapping object ids", @@ -234,18 +235,17 @@ export default class AutoApplyButton implements SpecialVisualization { } constr( - state: FeaturePipelineState, - tagSource: UIEventSource, - argument: string[], - guistate: DefaultGuiState + state: SpecialVisualizationState, + tagSource: UIEventSource>, + argument: string[] ): BaseUIElement { try { if ( - !state.layoutToUse.official && + !state.layout.official && !( state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === - OsmConnection.oauth_configs["osm-test"].url + OsmConnection.oauth_configs["osm-test"].url ) ) { const t = Translations.t.general.add.import diff --git a/UI/Popup/CloseNoteButton.ts b/UI/Popup/CloseNoteButton.ts index e5556c831..eb83d4c17 100644 --- a/UI/Popup/CloseNoteButton.ts +++ b/UI/Popup/CloseNoteButton.ts @@ -1,4 +1,3 @@ -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" import BaseUIElement from "../BaseUIElement" import Translations from "../i18n/Translations" import { Utils } from "../../Utils" @@ -7,7 +6,8 @@ import Img from "../Base/Img" import { SubtleButton } from "../Base/SubtleButton" import Toggle from "../Input/Toggle" import { LoginToggle } from "./LoginButton" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" +import { UIEventSource } from "../../Logic/UIEventSource" export class CloseNoteButton implements SpecialVisualization { public readonly funcName = "close_note" @@ -43,7 +43,11 @@ export class CloseNoteButton implements SpecialVisualization { }, ] - public constr(state: FeaturePipelineState, tags, args): BaseUIElement { + public constr( + state: SpecialVisualizationState, + tags: UIEventSource>, + args: string[] + ): BaseUIElement { const t = Translations.t.notes const params: { @@ -78,7 +82,7 @@ export class CloseNoteButton implements SpecialVisualization { closeButton = new Toggle( closeButton, params.zoomButton ?? "", - state.locationControl.map((l) => l.zoom >= Number(params.minZoom)) + state.mapProperties.zoom.map((zoom) => zoom >= Number(params.minZoom)) ) } diff --git a/UI/Popup/ExportAsGpxViz.ts b/UI/Popup/ExportAsGpxViz.ts index e8340824c..eb5241c51 100644 --- a/UI/Popup/ExportAsGpxViz.ts +++ b/UI/Popup/ExportAsGpxViz.ts @@ -4,14 +4,15 @@ import Svg from "../../Svg" import Combine from "../Base/Combine" import { GeoOperations } from "../../Logic/GeoOperations" import { Utils } from "../../Utils" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" +import { UIEventSource } from "../../Logic/UIEventSource" export class ExportAsGpxViz implements SpecialVisualization { funcName = "export_as_gpx" docs = "Exports the selected feature as GPX-file" args = [] - constr(state, tagSource) { + constr(state: SpecialVisualizationState, tagSource: UIEventSource>) { const t = Translations.t.general.download return new SubtleButton( @@ -23,10 +24,10 @@ export class ExportAsGpxViz implements SpecialVisualization { ).onClick(() => { console.log("Exporting as GPX!") const tags = tagSource.data - const feature = state.allElements.ContainingFeatures.get(tags.id) - const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags) - const gpx = GeoOperations.AsGpx(feature, matchingLayer) - const title = matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" + const feature = state.indexedFeatures.featuresById.data.get(tags.id) + const layer = state?.layout?.getMatchingLayer(tags) + const gpx = GeoOperations.AsGpx(feature, { layer }) + const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", { mimetype: "{gpx=application/gpx+xml}", }) diff --git a/UI/Popup/HistogramViz.ts b/UI/Popup/HistogramViz.ts index 27602743f..d83d1dcf6 100644 --- a/UI/Popup/HistogramViz.ts +++ b/UI/Popup/HistogramViz.ts @@ -1,9 +1,8 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" -import { FixedUiElement } from "../Base/FixedUiElement" -// import Histogram from "../BigComponents/Histogram"; -// import {SpecialVisualization} from "../SpecialVisualization"; +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" +import Histogram from "../BigComponents/Histogram" -export class HistogramViz { +export class HistogramViz implements SpecialVisualization { funcName = "histogram" docs = "Create a histogram for a list of given values, read from the properties." example = @@ -30,7 +29,11 @@ export class HistogramViz { }, ] - constr(state, tagSource: UIEventSource, args: string[]) { + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + args: string[] + ) { let assignColors = undefined if (args.length >= 3) { const colors = [...args] @@ -63,10 +66,8 @@ export class HistogramViz { return undefined } }) - return new FixedUiElement("HISTORGRAM") - /* return new Histogram(listSource, args[1], args[2], { assignColor: assignColors, - })*/ + }) } } diff --git a/UI/Popup/ImportButton.ts b/UI/Popup/ImportButton.ts index 31c463f0d..b2ff72073 100644 --- a/UI/Popup/ImportButton.ts +++ b/UI/Popup/ImportButton.ts @@ -1,51 +1,47 @@ -import BaseUIElement from "../BaseUIElement" -import { SubtleButton } from "../Base/SubtleButton" -import { UIEventSource } from "../../Logic/UIEventSource" -import Combine from "../Base/Combine" -import { VariableUiElement } from "../Base/VariableUIElement" -import Translations from "../i18n/Translations" -import Toggle from "../Input/Toggle" -import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction" -import Loading from "../Base/Loading" -import { OsmConnection } from "../../Logic/Osm/OsmConnection" -import Lazy from "../Base/Lazy" -import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint" -import Img from "../Base/Img" -import FilteredLayer from "../../Models/FilteredLayer" -import { FixedUiElement } from "../Base/FixedUiElement" -import Svg from "../../Svg" -import { Utils } from "../../Utils" -import Minimap from "../Base/Minimap" -import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" -import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" -import CreateWayWithPointReuseAction, { - MergePointConfig, -} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction" -import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction" -import FeatureSource from "../../Logic/FeatureSource/FeatureSource" -import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject" -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" -import { DefaultGuiState } from "../DefaultGuiState" -import { PresetInfo } from "../BigComponents/SimpleAddUI" -import { TagUtils } from "../../Logic/Tags/TagUtils" -import { And } from "../../Logic/Tags/And" -import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction" -import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction" -import { Tag } from "../../Logic/Tags/Tag" -import TagApplyButton from "./TagApplyButton" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import conflation_json from "../../assets/layers/conflation/conflation.json" -import { GeoOperations } from "../../Logic/GeoOperations" -import { LoginToggle } from "./LoginButton" -import { AutoAction } from "./AutoApplyButton" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import { Changes } from "../../Logic/Osm/Changes" -import { ElementStorage } from "../../Logic/ElementStorage" -import Hash from "../../Logic/Web/Hash" -import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig" -import { SpecialVisualization } from "../SpecialVisualization" +import BaseUIElement from "../BaseUIElement"; +import { SubtleButton } from "../Base/SubtleButton"; +import { UIEventSource } from "../../Logic/UIEventSource"; +import Combine from "../Base/Combine"; +import { VariableUiElement } from "../Base/VariableUIElement"; +import Translations from "../i18n/Translations"; +import Toggle from "../Input/Toggle"; +import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; +import Loading from "../Base/Loading"; +import { OsmConnection } from "../../Logic/Osm/OsmConnection"; +import Lazy from "../Base/Lazy"; +import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"; +import Img from "../Base/Img"; +import FilteredLayer from "../../Models/FilteredLayer"; +import { FixedUiElement } from "../Base/FixedUiElement"; +import Svg from "../../Svg"; +import { Utils } from "../../Utils"; +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; +import CreateWayWithPointReuseAction, { MergePointConfig } from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction"; +import OsmChangeAction, { OsmCreateAction } from "../../Logic/Osm/Actions/OsmChangeAction"; +import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; +import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"; +import { PresetInfo } from "../BigComponents/SimpleAddUI"; +import { TagUtils } from "../../Logic/Tags/TagUtils"; +import { And } from "../../Logic/Tags/And"; +import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction"; +import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"; +import { Tag } from "../../Logic/Tags/Tag"; +import TagApplyButton from "./TagApplyButton"; +import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; +import conflation_json from "../../assets/layers/conflation/conflation.json"; +import { GeoOperations } from "../../Logic/GeoOperations"; +import { LoginToggle } from "./LoginButton"; +import { AutoAction } from "./AutoApplyButton"; +import Hash from "../../Logic/Web/Hash"; +import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig"; +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"; import Maproulette from "../../Logic/Maproulette"; +import { Feature, Point } from "geojson"; +import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"; +import ShowDataLayer from "../Map/ShowDataLayer"; +import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"; +import SvelteUIElement from "../Base/SvelteUIElement"; +import MaplibreMap from "../Map/MaplibreMap.svelte"; /** * A helper class for the various import-flows. @@ -106,7 +102,7 @@ ${Utils.special_visualizations_importRequirementDocs} } abstract constructElement( - state: FeaturePipelineState, + state: SpecialVisualizationState, args: { max_snap_distance: string snap_onto_layers: string @@ -116,13 +112,16 @@ ${Utils.special_visualizations_importRequirementDocs} newTags: UIEventSource targetLayer: string }, - tagSource: UIEventSource, - guiState: DefaultGuiState, - feature: any, + tagSource: UIEventSource>, + feature: Feature, onCancelClicked: () => void ): BaseUIElement - constr(state, tagSource: UIEventSource, argsRaw, guiState) { + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + argsRaw: string[] + ) { /** * Some generic import button pre-validation is implemented here: * - Are we logged in? @@ -139,7 +138,7 @@ ${Utils.special_visualizations_importRequirementDocs} { // Some initial validation if ( - !state.layoutToUse.official && + !state.layout.official && !( state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === @@ -148,11 +147,9 @@ ${Utils.special_visualizations_importRequirementDocs} ) { return new Combine([t.officialThemesOnly.SetClass("alert"), t.howToTest]) } - const targetLayer: FilteredLayer = state.filteredLayers.data.filter( - (fl) => fl.layerDef.id === args.targetLayer - )[0] + const targetLayer: FilteredLayer = state.layerState.filteredLayers.get(args.targetLayer) if (targetLayer === undefined) { - const e = `Target layer not defined: error in import button for theme: ${state.layoutToUse.id}: layer ${args.targetLayer} not found` + const e = `Target layer not defined: error in import button for theme: ${state.layout.id}: layer ${args.targetLayer} not found` console.error(e) return new FixedUiElement(e).SetClass("alert") } @@ -167,7 +164,7 @@ ${Utils.special_visualizations_importRequirementDocs} const inviteToImportButton = new SubtleButton(img, args.text) const id = tagSource.data.id - const feature = state.allElements.ContainingFeatures.get(id) + const feature = state.indexedFeatures.featuresById.data.get(id) // Explanation of the tags that will be applied onto the imported/conflated object @@ -205,22 +202,13 @@ ${Utils.special_visualizations_importRequirementDocs} return tags._imported === "yes" }) - /**** THe actual panel showing the import guiding map ****/ - const importGuidingPanel = this.constructElement( - state, - args, - tagSource, - guiState, - feature, - () => importClicked.setData(false) + /**** The actual panel showing the import guiding map ****/ + const importGuidingPanel = this.constructElement(state, args, tagSource, feature, () => + importClicked.setData(false) ) const importFlow = new Toggle( - new Toggle( - new Loading(t0.stillLoading), - importGuidingPanel, - state.featurePipeline.runningQuery - ), + new Toggle(new Loading(t0.stillLoading), importGuidingPanel, state.dataIsLoading), inviteToImportButton, importClicked ) @@ -230,7 +218,7 @@ ${Utils.special_visualizations_importRequirementDocs} new Toggle( new Toggle(t.hasBeenImported, importFlow, isImported), t.zoomInMore.SetClass("alert block"), - state.locationControl.map((l) => l.zoom >= 18) + state.mapProperties.zoom.map((zoom) => zoom >= 18) ), pleaseLoginButton, state @@ -258,8 +246,13 @@ ${Utils.special_visualizations_importRequirementDocs} protected abstract canBeImported(feature: any) + private static readonly conflationLayer = new LayerConfig( + conflation_json, + "all_known_layers", + true + ) protected createConfirmPanelForWay( - state: FeaturePipelineState, + state: SpecialVisualizationState, args: { max_snap_distance: string snap_onto_layers: string @@ -270,32 +263,32 @@ ${Utils.special_visualizations_importRequirementDocs} }, feature: any, originalFeatureTags: UIEventSource, - action: OsmChangeAction & { getPreview(): Promise; newElementId?: string }, + action: OsmChangeAction & { getPreview?(): Promise; newElementId?: string }, onCancel: () => void ): BaseUIElement { const self = this - const confirmationMap = Minimap.createMiniMap({ - allowMoving: state.featureSwitchIsDebugging.data ?? false, - background: state.backgroundLayer, + const map = new UIEventSource(undefined) + new MapLibreAdaptor(map, { + allowMoving: UIEventSource.feedFrom(state.featureSwitchIsTesting), + allowZooming: UIEventSource.feedFrom(state.featureSwitchIsTesting), + rasterLayer: state.mapProperties.rasterLayer, }) + const confirmationMap = new SvelteUIElement(MaplibreMap, { map }) confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl") - // SHow all relevant data - including (eventually) the way of which the geometry will be replaced - new ShowDataMultiLayer({ - leafletMap: confirmationMap.leafletMap, - zoomToFeatures: true, - features: StaticFeatureSource.fromGeojson([feature]), - state: state, - layers: state.filteredLayers, - }) + ShowDataLayer.showMultipleLayers( + map, + new StaticFeatureSource([feature]), + state.layout.layers, + { zoomToFeatures: true } + ) + // Show all relevant data - including (eventually) the way of which the geometry will be replaced action.getPreview().then((changePreview) => { - new ShowDataLayer({ - leafletMap: confirmationMap.leafletMap, + new ShowDataLayer(map, { zoomToFeatures: false, features: changePreview, - state, - layerToShow: new LayerConfig(conflation_json, "all_known_layers", true), + layer: AbstractImportButton.conflationLayer, }) }) @@ -317,9 +310,9 @@ ${Utils.special_visualizations_importRequirementDocs} { originalFeatureTags.data["_imported"] = "yes" originalFeatureTags.ping() // will set isImported as per its definition - state.changes.applyAction(action) + await state.changes.applyAction(action) const newId = action.newElementId ?? action.mainObjectId - state.selectedElement.setData(state.allElements.ContainingFeatures.get(newId)) + state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(newId)) } }) @@ -392,7 +385,7 @@ export class ConflateButton extends AbstractImportButton { } constructElement( - state: FeaturePipelineState, + state: SpecialVisualizationState, args: { max_snap_distance: string snap_onto_layers: string @@ -403,8 +396,7 @@ export class ConflateButton extends AbstractImportButton { targetLayer: string }, tagSource: UIEventSource, - guiState: DefaultGuiState, - feature: any, + feature: Feature, onCancelClicked: () => void ): BaseUIElement { const nodesMustMatch = args.snap_onto_layers @@ -424,10 +416,15 @@ export class ConflateButton extends AbstractImportButton { const key = args["way_to_conflate"] const wayToConflate = tagSource.data[key] feature = GeoOperations.removeOvernoding(feature) - const action = new ReplaceGeometryAction(state, feature, wayToConflate, { - theme: state.layoutToUse.id, - newTags: args.newTags.data, - }) + const action: OsmChangeAction & { getPreview(): Promise } = new ReplaceGeometryAction( + state, + feature, + wayToConflate, + { + theme: state.layout.id, + newTags: args.newTags.data, + } + ) return this.createConfirmPanelForWay( state, @@ -498,9 +495,9 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction newTags: UIEventSource targetLayer: string }, - state: FeaturePipelineState, + state: SpecialVisualizationState, mergeConfigs: any[] - ) { + ): OsmCreateAction & { getPreview(): Promise; newElementId?: string } { const coors = feature.geometry.coordinates if (feature.geometry.type === "Polygon" && coors.length > 1) { const outer = coors[0] @@ -525,8 +522,8 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction } async applyActionOn( - state: { layoutToUse: LayoutConfig; changes: Changes; allElements: ElementStorage }, - originalFeatureTags: UIEventSource, + state: SpecialVisualizationState, + originalFeatureTags: UIEventSource>, argument: string[] ): Promise { const id = originalFeatureTags.data.id @@ -535,14 +532,9 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction } AbstractImportButton.importedIds.add(originalFeatureTags.data.id) const args = this.parseArgs(argument, originalFeatureTags) - const feature = state.allElements.ContainingFeatures.get(id) + const feature = state.indexedFeatures.featuresById.data.get(id) const mergeConfigs = this.GetMergeConfig(args) - const action = ImportWayButton.CreateAction( - feature, - args, - state, - mergeConfigs - ) + const action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs) await state.changes.applyAction(action) } @@ -557,7 +549,13 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction return deps } - constructElement(state, args, originalFeatureTags, guiState, feature, onCancel): BaseUIElement { + constructElement( + state: SpecialVisualizationState, + args, + originalFeatureTags: UIEventSource>, + feature, + onCancel + ): BaseUIElement { const geometry = feature.geometry if (!(geometry.type == "LineString" || geometry.type === "Polygon")) { @@ -567,7 +565,12 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction // Upload the way to OSM const mergeConfigs = this.GetMergeConfig(args) - let action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs) + let action: OsmCreateAction & {getPreview?: any} = ImportWayButton.CreateAction( + feature, + args, + state, + mergeConfigs + ) return this.createConfirmPanelForWay( state, args, @@ -663,10 +666,9 @@ export class ImportPointButton extends AbstractImportButton { note_id: string maproulette_id: string }, - state: FeaturePipelineState, - guiState: DefaultGuiState, + state: SpecialVisualizationState, originalFeatureTags: UIEventSource, - feature: any, + feature: Feature, onCancel: () => void, close: () => void ): BaseUIElement { @@ -690,7 +692,7 @@ export class ImportPointButton extends AbstractImportButton { } const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { - theme: state.layoutToUse.id, + theme: state.layout.id, changeType: "import", snapOnto: snapOnto, specialMotivation: specialMotivation, @@ -698,7 +700,7 @@ export class ImportPointButton extends AbstractImportButton { await state.changes.applyAction(newElementAction) state.selectedElement.setData( - state.allElements.ContainingFeatures.get(newElementAction.newElementId) + state.indexedFeatures.featuresById.data.get(newElementAction.newElementId) ) Hash.hash.setData(newElementAction.newElementId) @@ -742,19 +744,17 @@ export class ImportPointButton extends AbstractImportButton { const presetInfo = { tags: args.newTags.data, icon: () => new Img(args.icon), - layerToAddTo: state.filteredLayers.data.filter( - (l) => l.layerDef.id === args.targetLayer - )[0], + layerToAddTo: state.layerState.filteredLayers.get(args.targetLayer), name: args.text, title: Translations.T(args.text), preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise boundsFactor: 3, } - const [lon, lat] = feature.geometry.coordinates + const [lon, lat] = <[number,number]> feature.geometry.coordinates return new ConfirmLocationOfPoint( state, - guiState.filterViewIsOpened, + state.guistate.filterViewIsOpened, presetInfo, Translations.W(args.text), { @@ -783,10 +783,9 @@ export class ImportPointButton extends AbstractImportButton { } constructElement( - state, + state: SpecialVisualizationState, args, originalFeatureTags, - guiState, feature, onCancel: () => void ): BaseUIElement { @@ -797,7 +796,6 @@ export class ImportPointButton extends AbstractImportButton { ImportPointButton.createConfirmPanelForPoint( args, state, - guiState, originalFeatureTags, feature, onCancel, diff --git a/UI/Popup/LanguageElement.ts b/UI/Popup/LanguageElement.ts index 12bd75bd6..b07e90232 100644 --- a/UI/Popup/LanguageElement.ts +++ b/UI/Popup/LanguageElement.ts @@ -1,9 +1,7 @@ -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" import BaseUIElement from "../BaseUIElement" import { UIEventSource } from "../../Logic/UIEventSource" -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" import { VariableUiElement } from "../Base/VariableUIElement" -import { OsmTags } from "../../Models/OsmFeature" import all_languages from "../../assets/language_translations.json" import { Translation } from "../i18n/Translation" import Combine from "../Base/Combine" @@ -16,10 +14,9 @@ import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" import { And } from "../../Logic/Tags/And" import { Tag } from "../../Logic/Tags/Tag" import { EditButton, SaveButton } from "./SaveButton" -import { FixedUiElement } from "../Base/FixedUiElement" import Translations from "../i18n/Translations" import Toggle from "../Input/Toggle" -import { On } from "../../Models/ThemeConfig/Conversion/Conversion" +import { Feature } from "geojson" export class LanguageElement implements SpecialVisualization { funcName: string = "language_chooser" @@ -79,9 +76,10 @@ export class LanguageElement implements SpecialVisualization { ` constr( - state: FeaturePipelineState, - tagSource: UIEventSource, - argument: string[] + state: SpecialVisualizationState, + tagSource: UIEventSource>, + argument: string[], + feature: Feature ): BaseUIElement { let [key, question, item_render, single_render, all_render, on_no_known_languages, mode] = argument @@ -172,7 +170,7 @@ export class LanguageElement implements SpecialVisualization { new And(selection), tagSource.data, { - theme: state?.layoutToUse?.id ?? "unkown", + theme: state?.layout?.id ?? "unkown", changeType: "answer", } ) diff --git a/UI/Popup/MapillaryLinkVis.ts b/UI/Popup/MapillaryLinkVis.ts index e3c401a52..721e6be73 100644 --- a/UI/Popup/MapillaryLinkVis.ts +++ b/UI/Popup/MapillaryLinkVis.ts @@ -2,7 +2,9 @@ import { GeoOperations } from "../../Logic/GeoOperations" import { MapillaryLink } from "../BigComponents/MapillaryLink" import { UIEventSource } from "../../Logic/UIEventSource" import Loc from "../../Models/Loc" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" +import { Feature } from "geojson" +import BaseUIElement from "../BaseUIElement" export class MapillaryLinkVis implements SpecialVisualization { funcName = "mapillary_link" @@ -15,9 +17,13 @@ export class MapillaryLinkVis implements SpecialVisualization { }, ] - public constr(state, tagsSource, args) { - const feat = state.allElements.ContainingFeatures.get(tagsSource.data.id) - const [lon, lat] = GeoOperations.centerpointCoordinates(feat) + public constr( + state: SpecialVisualizationState, + tagsSource: UIEventSource>, + args: string[], + feature: Feature + ): BaseUIElement { + const [lon, lat] = GeoOperations.centerpointCoordinates(feature) let zoom = Number(args[0]) if (isNaN(zoom)) { zoom = 18 diff --git a/UI/Popup/MinimapViz.ts b/UI/Popup/MinimapViz.ts index 1a53373a6..ccf061f68 100644 --- a/UI/Popup/MinimapViz.ts +++ b/UI/Popup/MinimapViz.ts @@ -1,9 +1,14 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" -import Loc from "../../Models/Loc" -import Minimap from "../Base/Minimap" -import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" +import { Feature } from "geojson" +import { MapLibreAdaptor } from "../Map/MapLibreAdaptor" +import SvelteUIElement from "../Base/SvelteUIElement" +import MaplibreMap from "../Map/MaplibreMap.svelte" +import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" +import FilteredLayer from "../../Models/FilteredLayer" +import ShowDataLayer from "../Map/ShowDataLayer" +import { stat } from "fs" export class MinimapViz implements SpecialVisualization { funcName = "minimap" @@ -22,16 +27,20 @@ export class MinimapViz implements SpecialVisualization { ] example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`" - constr(state, tagSource, args, _) { + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + args: string[] + ) { if (state === undefined) { return undefined } const keys = [...args] keys.splice(0, 1) - const featureStore = state.allElements.ContainingFeatures - const featuresToShow: Store<{ freshness: Date; feature: any }[]> = tagSource.map( - (properties) => { - const features: { freshness: Date; feature: any }[] = [] + const featuresToShow: Store = state.indexedFeatures.featuresById.map( + (featuresById) => { + const properties = tagSource.data + const features: Feature[] = [] for (const key of keys) { const value = properties[key] if (value === undefined || value === null) { @@ -45,21 +54,22 @@ export class MinimapViz implements SpecialVisualization { } for (const id of idList) { - const feature = featureStore.get(id) + const feature = featuresById.get(id) if (feature === undefined) { console.warn("No feature found for id ", id) continue } - features.push({ - freshness: new Date(), - feature, - }) + features.push(feature) } } return features - } + }, + [tagSource] ) - const properties = tagSource.data + + const mlmap = new UIEventSource(undefined) + const mla = new MapLibreAdaptor(mlmap) + let zoom = 18 if (args[0]) { const parsed = Number(args[0]) @@ -67,33 +77,18 @@ export class MinimapViz implements SpecialVisualization { zoom = parsed } } - const locationSource = new UIEventSource({ - lat: Number(properties._lat), - lon: Number(properties._lon), - zoom: zoom, - }) - const minimap = Minimap.createMiniMap({ - background: state.backgroundLayer, - location: locationSource, - allowMoving: false, - }) + mla.zoom.setData(zoom) + mla.allowMoving.setData(false) + mla.allowZooming.setData(false) - locationSource.addCallback((loc) => { - if (loc.zoom > zoom) { - // We zoom back - locationSource.data.zoom = zoom - locationSource.ping() - } - }) + ShowDataLayer.showMultipleLayers( + mlmap, + new StaticFeatureSource(featuresToShow), + state.layout.layers + ) - new ShowDataMultiLayer({ - leafletMap: minimap["leafletMap"], - zoomToFeatures: true, - layers: state.filteredLayers, - features: new StaticFeatureSource(featuresToShow), - }) - - minimap.SetStyle("overflow: hidden; pointer-events: none;") - return minimap + return new SvelteUIElement(MaplibreMap, { map: mlmap }).SetStyle( + "overflow: hidden; pointer-events: none;" + ) } } diff --git a/UI/Popup/MultiApply.ts b/UI/Popup/MultiApply.ts index df6829d8c..ca0a5382b 100644 --- a/UI/Popup/MultiApply.ts +++ b/UI/Popup/MultiApply.ts @@ -2,17 +2,14 @@ import { Store } from "../../Logic/UIEventSource" import BaseUIElement from "../BaseUIElement" import Combine from "../Base/Combine" import { SubtleButton } from "../Base/SubtleButton" -import { Changes } from "../../Logic/Osm/Changes" import { FixedUiElement } from "../Base/FixedUiElement" import Translations from "../i18n/Translations" import { VariableUiElement } from "../Base/VariableUIElement" import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" import { Tag } from "../../Logic/Tags/Tag" -import { ElementStorage } from "../../Logic/ElementStorage" import { And } from "../../Logic/Tags/And" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import Toggle from "../Input/Toggle" -import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import { SpecialVisualizationState } from "../SpecialVisualization" export interface MultiApplyParams { featureIds: Store @@ -21,12 +18,7 @@ export interface MultiApplyParams { autoapply: boolean overwrite: boolean tagsSource: Store - state: { - changes: Changes - allElements: ElementStorage - layoutToUse: LayoutConfig - osmConnection: OsmConnection - } + state: SpecialVisualizationState } class MultiApplyExecutor { @@ -68,14 +60,14 @@ class MultiApplyExecutor { console.log("Multi-applying changes...") const featuresToChange = this.params.featureIds.data const changes = this.params.state.changes - const allElements = this.params.state.allElements + const allElements = this.params.state.featureProperties const keysToChange = this.params.keysToApply const overwrite = this.params.overwrite const selfTags = this.params.tagsSource.data - const theme = this.params.state.layoutToUse.id + const theme = this.params.state.layout.id for (const id of featuresToChange) { const tagsToApply: Tag[] = [] - const otherFeatureTags = allElements.getEventSourceById(id).data + const otherFeatureTags = allElements.getStore(id).data for (const key of keysToChange) { const newValue = selfTags[key] if (newValue === undefined) { diff --git a/UI/Popup/MultiApplyViz.ts b/UI/Popup/MultiApplyViz.ts index c481afdcf..ad9357f22 100644 --- a/UI/Popup/MultiApplyViz.ts +++ b/UI/Popup/MultiApplyViz.ts @@ -1,6 +1,6 @@ -import { Store } from "../../Logic/UIEventSource" +import { Store, UIEventSource } from "../../Logic/UIEventSource" import MultiApply from "./MultiApply" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" export class MultiApplyViz implements SpecialVisualization { funcName = "multi_apply" @@ -31,7 +31,11 @@ export class MultiApplyViz implements SpecialVisualization { example = "{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}" - constr(state, tagsSource, args) { + constr( + state: SpecialVisualizationState, + tagsSource: UIEventSource>, + args: string[] + ) { const featureIdsKey = args[0] const keysToApply = args[1].split(";") const text = args[2] diff --git a/UI/Popup/NearbyImageVis.ts b/UI/Popup/NearbyImageVis.ts index 7a66ecde3..52e359c3b 100644 --- a/UI/Popup/NearbyImageVis.ts +++ b/UI/Popup/NearbyImageVis.ts @@ -1,6 +1,4 @@ -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" import { UIEventSource } from "../../Logic/UIEventSource" -import { DefaultGuiState } from "../DefaultGuiState" import BaseUIElement from "../BaseUIElement" import Translations from "../i18n/Translations" import { GeoOperations } from "../../Logic/GeoOperations" @@ -19,7 +17,7 @@ import { VariableUiElement } from "../Base/VariableUIElement" import Toggle from "../Input/Toggle" import Title from "../Base/Title" import { MapillaryLinkVis } from "./MapillaryLinkVis" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" export class NearbyImageVis implements SpecialVisualization { args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [ @@ -39,14 +37,13 @@ export class NearbyImageVis implements SpecialVisualization { funcName = "nearby_images" constr( - state: FeaturePipelineState, - tagSource: UIEventSource, - args: string[], - guistate: DefaultGuiState + state: SpecialVisualizationState, + tagSource: UIEventSource>, + args: string[] ): BaseUIElement { const t = Translations.t.image.nearbyPictures const mode: "open" | "expandable" | "collapsable" = args[0] - const feature = state.allElements.ContainingFeatures.get(tagSource.data.id) + const feature = state.indexedFeatures.featuresById.data.get(tagSource.data.id) const [lon, lat] = GeoOperations.centerpointCoordinates(feature) const id: string = tagSource.data["id"] const canBeEdited: boolean = !!id?.match("(node|way|relation)/-?[0-9]+") @@ -69,7 +66,7 @@ export class NearbyImageVis implements SpecialVisualization { } await state?.changes?.applyAction( new ChangeTagAction(id, new And(tags), tagSource.data, { - theme: state?.layoutToUse.id, + theme: state?.layout.id, changeType: "link-image", }) ) @@ -116,8 +113,8 @@ export class NearbyImageVis implements SpecialVisualization { maxDaysOld: 365 * 3, } const slideshow = canBeEdited - ? new SelectOneNearbyImage(options, state) - : new NearbyImages(options, state) + ? new SelectOneNearbyImage(options, state.indexedFeatures) + : new NearbyImages(options, state.indexedFeatures) const controls = new Combine([ towardsCenter, new Combine([ diff --git a/UI/Popup/NearbyImages.ts b/UI/Popup/NearbyImages.ts index e21657483..6ddc1ab6f 100644 --- a/UI/Popup/NearbyImages.ts +++ b/UI/Popup/NearbyImages.ts @@ -13,9 +13,10 @@ import Translations from "../i18n/Translations" import { Mapillary } from "../../Logic/ImageProviders/Mapillary" import { SubtleButton } from "../Base/SubtleButton" import { GeoOperations } from "../../Logic/GeoOperations" -import { ElementStorage } from "../../Logic/ElementStorage" import Lazy from "../Base/Lazy" import P4C from "pic4carto" +import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource" + export interface P4CPicture { pictureUrl: string date?: number @@ -47,15 +48,15 @@ export interface NearbyImageOptions { } class ImagesInLoadedDataFetcher { - private allElements: ElementStorage + private indexedFeatures: IndexedFeatureSource - constructor(state: { allElements: ElementStorage }) { - this.allElements = state.allElements + constructor(indexedFeatures: IndexedFeatureSource) { + this.indexedFeatures = indexedFeatures } public fetchAround(loc: { lon: number; lat: number; searchRadius?: number }): P4CPicture[] { const foundImages: P4CPicture[] = [] - this.allElements.ContainingFeatures.forEach((feature) => { + this.indexedFeatures.features.data.forEach((feature) => { const props = feature.properties const images = [] if (props.image) { @@ -100,7 +101,7 @@ class ImagesInLoadedDataFetcher { } export default class NearbyImages extends Lazy { - constructor(options: NearbyImageOptions, state?: { allElements: ElementStorage }) { + constructor(options: NearbyImageOptions, state?: IndexedFeatureSource) { super(() => { const t = Translations.t.image.nearbyPictures const shownImages = options.shownImagesCount ?? new UIEventSource(25) @@ -171,10 +172,7 @@ export default class NearbyImages extends Lazy { ) } - private static buildPictureFetcher( - options: NearbyImageOptions, - state?: { allElements: ElementStorage } - ) { + private static buildPictureFetcher(options: NearbyImageOptions, state?: IndexedFeatureSource) { const picManager = new P4C.PicturesManager({}) const searchRadius = options.searchRadius ?? 500 @@ -283,7 +281,7 @@ export class SelectOneNearbyImage extends NearbyImages implements InputElement

}, - state?: { allElements: ElementStorage } + state?: IndexedFeatureSource ) { super(options, state) this.value = options.value ?? new UIEventSource(undefined) diff --git a/UI/Popup/PlantNetDetectionViz.ts b/UI/Popup/PlantNetDetectionViz.ts index fcbd52109..206131673 100644 --- a/UI/Popup/PlantNetDetectionViz.ts +++ b/UI/Popup/PlantNetDetectionViz.ts @@ -12,7 +12,7 @@ import Combine from "../Base/Combine" import Svg from "../../Svg" import Translations from "../i18n/Translations" import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"; export class PlantNetDetectionViz implements SpecialVisualization { funcName = "plantnet_detection" @@ -27,7 +27,7 @@ export class PlantNetDetectionViz implements SpecialVisualization { }, ] - public constr(state, tags, args) { + public constr(state: SpecialVisualizationState, tags: UIEventSource>, args: string[]) { let imagePrefixes: string[] = undefined if (args.length > 0) { imagePrefixes = [].concat(...args.map((a) => a.split(","))) @@ -53,7 +53,7 @@ export class PlantNetDetectionViz implements SpecialVisualization { ]), tags.data, { - theme: state.layoutToUse.id, + theme: state.layout.id, changeType: "plantnet-ai-detection", } ) diff --git a/UI/Popup/ShareLinkViz.ts b/UI/Popup/ShareLinkViz.ts index 438aa2e28..c63cffcf1 100644 --- a/UI/Popup/ShareLinkViz.ts +++ b/UI/Popup/ShareLinkViz.ts @@ -3,7 +3,7 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import ShareButton from "../BigComponents/ShareButton" import Svg from "../../Svg" import { FixedUiElement } from "../Base/FixedUiElement" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"; export class ShareLinkViz implements SpecialVisualization { funcName = "share_link" @@ -17,12 +17,12 @@ export class ShareLinkViz implements SpecialVisualization { }, ] - public constr(state, tagSource: UIEventSource, args) { + public constr(state: SpecialVisualizationState, tagSource: UIEventSource>, args: string[]) { if (window.navigator.share) { const generateShareData = () => { - const title = state?.layoutToUse?.title?.txt ?? "MapComplete" + const title = state?.layout?.title?.txt ?? "MapComplete" - let matchingLayer: LayerConfig = state?.layoutToUse?.getMatchingLayer( + let matchingLayer: LayerConfig = state?.layout?.getMatchingLayer( tagSource?.data ) let name = @@ -41,7 +41,7 @@ export class ShareLinkViz implements SpecialVisualization { return { title: name, url: url, - text: state?.layoutToUse?.shortDescription?.txt ?? "MapComplete", + text: state?.layout?.shortDescription?.txt ?? "MapComplete", } } diff --git a/UI/Popup/StealViz.ts b/UI/Popup/StealViz.ts index 28bdd3ed8..20e74ebca 100644 --- a/UI/Popup/StealViz.ts +++ b/UI/Popup/StealViz.ts @@ -4,7 +4,7 @@ import { VariableUiElement } from "../Base/VariableUIElement" import BaseUIElement from "../BaseUIElement" import EditableTagRendering from "./EditableTagRendering" import Combine from "../Base/Combine" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" export class StealViz implements SpecialVisualization { funcName = "steal" @@ -21,12 +21,12 @@ export class StealViz implements SpecialVisualization { required: true, }, ] - constr(state, featureTags, args) { + constr(state: SpecialVisualizationState, featureTags, args) { const [featureIdKey, layerAndtagRenderingIds] = args const tagRenderings: [LayerConfig, TagRenderingConfig][] = [] for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) { const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".") - const layer = state.layoutToUse.layers.find((l) => l.id === layerId) + const layer = state.layout.layers.find((l) => l.id === layerId) const tagRendering = layer.tagRenderings.find((tr) => tr.id === tagRenderingId) tagRenderings.push([layer, tagRendering]) } @@ -39,7 +39,7 @@ export class StealViz implements SpecialVisualization { if (featureId === undefined) { return undefined } - const otherTags = state.allElements.getEventSourceById(featureId) + const otherTags = state.featureProperties.getStore(featureId) const elements: BaseUIElement[] = [] for (const [layer, tagRendering] of tagRenderings) { const el = new EditableTagRendering( diff --git a/UI/Popup/TagApplyButton.ts b/UI/Popup/TagApplyButton.ts index 12fe157e3..1027b224d 100644 --- a/UI/Popup/TagApplyButton.ts +++ b/UI/Popup/TagApplyButton.ts @@ -11,10 +11,9 @@ import { And } from "../../Logic/Tags/And" import Toggle from "../Input/Toggle" import { Utils } from "../../Utils" import { Tag } from "../../Logic/Tags/Tag" -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import { Changes } from "../../Logic/Osm/Changes" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" export default class TagApplyButton implements AutoAction, SpecialVisualization { public readonly funcName = "tag_apply" @@ -76,7 +75,10 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization return tgsSpec } - public static generateTagsToApply(spec: string, tagSource: Store): Store { + public static generateTagsToApply( + spec: string, + tagSource: Store> + ): Store { // Check whether we need to look up a single value if (!spec.includes(";") && !spec.includes("=") && spec.includes("$")) { @@ -110,7 +112,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization async applyActionOn( state: { - layoutToUse: LayoutConfig + layout: LayoutConfig changes: Changes }, tags: UIEventSource, @@ -125,7 +127,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization new And(tagsToApply.data), tags.data, // We pass in the tags of the selected element, not the tags of the target element! { - theme: state.layoutToUse.id, + theme: state.layout.id, changeType: "answer", } ) @@ -133,8 +135,8 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization } public constr( - state: FeaturePipelineState, - tags: UIEventSource, + state: SpecialVisualizationState, + tags: UIEventSource>, args: string[] ): BaseUIElement { const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) @@ -162,9 +164,9 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization const applyButton = new SubtleButton( image, new Combine([msg, tagsExplanation]).SetClass("flex flex-col") - ).onClick(() => { - self.applyActionOn(state, tags, args) + ).onClick(async () => { applied.setData(true) + await self.applyActionOn(state, tags, args) }) return new Toggle( diff --git a/UI/Popup/TagRenderingAnswer.svelte b/UI/Popup/TagRenderingAnswer.svelte new file mode 100644 index 000000000..e69de29bb diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 3263c8e56..338be0c07 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -6,6 +6,7 @@ import { SubstitutedTranslation } from "../SubstitutedTranslation" import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" import Combine from "../Base/Combine" import Img from "../Base/Img" +import { SpecialVisualisationState } from "../SpecialVisualization" /*** * Displays the correct value for a known tagrendering @@ -14,7 +15,7 @@ export default class TagRenderingAnswer extends VariableUiElement { constructor( tagsSource: UIEventSource, configuration: TagRenderingConfig, - state: any, + state: SpecialVisualisationState, contentClasses: string = "", contentStyle: string = "", options?: { @@ -24,6 +25,7 @@ export default class TagRenderingAnswer extends VariableUiElement { if (configuration === undefined) { throw "Trying to generate a tagRenderingAnswer without configuration..." } + UIEventSource if (tagsSource === undefined) { throw "Trying to generate a tagRenderingAnswer without tagSource..." } diff --git a/UI/Popup/UploadToOsmViz.ts b/UI/Popup/UploadToOsmViz.ts index 9fbd54760..f25f72386 100644 --- a/UI/Popup/UploadToOsmViz.ts +++ b/UI/Popup/UploadToOsmViz.ts @@ -3,7 +3,8 @@ import { Feature } from "geojson" import { Point } from "@turf/turf" import { GeoLocationPointProperties } from "../../Logic/State/GeoLocationState" import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI" -import { SpecialVisualization } from "../SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" +import { UIEventSource } from "../../Logic/UIEventSource" /** * Wrapper around 'UploadTraceToOsmUI' @@ -14,15 +15,20 @@ export class UploadToOsmViz implements SpecialVisualization { "Uploads the GPS-history as GPX to OpenStreetMap.org; clears the history afterwards. The actual feature is ignored." args = [] - constr(state, featureTags, args) { + constr( + state: SpecialVisualizationState, + featureTags: UIEventSource>, + args: string[] + ) { function getTrace(title: string) { title = title?.trim() if (title === undefined || title === "") { title = "Uploaded with MapComplete" } title = Utils.EncodeXmlValue(title) - const userLocations: Feature[] = - state.historicalUserLocations.features.data.map((f) => f.feature) + const userLocations = []>( + state.historicalUserLocations.features.data + ) const trackPoints: string[] = [] for (const l of userLocations) { let trkpt = ` ` diff --git a/UI/SpecialVisualization.ts b/UI/SpecialVisualization.ts index f41284b9b..4e46c0008 100644 --- a/UI/SpecialVisualization.ts +++ b/UI/SpecialVisualization.ts @@ -1,18 +1,70 @@ -import { UIEventSource } from "../Logic/UIEventSource" +import { Store, UIEventSource } from "../Logic/UIEventSource" import BaseUIElement from "./BaseUIElement" -import FeaturePipelineState from "../Logic/State/FeaturePipelineState" import { DefaultGuiState } from "./DefaultGuiState" +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import FeatureSource, { + IndexedFeatureSource, + WritableFeatureSource, +} from "../Logic/FeatureSource/FeatureSource" +import { OsmConnection } from "../Logic/Osm/OsmConnection" +import { Changes } from "../Logic/Osm/Changes" +import { MapProperties } from "../Models/MapProperties" +import LayerState from "../Logic/State/LayerState" +import { Feature } from "geojson" +import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" +import UserRelatedState from "../Logic/State/UserRelatedState" +import { MangroveIdentity } from "../Logic/Web/MangroveReviews" +import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" + +/** + * The state needed to render a special Visualisation. + */ +export interface SpecialVisualizationState { + readonly guistate: DefaultGuiState + readonly layout: LayoutConfig + + readonly layerState: LayerState + readonly featureProperties: { getStore(id: string): UIEventSource> } + + readonly indexedFeatures: IndexedFeatureSource + + readonly historicalUserLocations: WritableFeatureSource + + readonly osmConnection: OsmConnection + readonly featureSwitchUserbadge: Store + readonly featureSwitchIsTesting: Store + readonly changes: Changes + /** + * State of the main map + */ + readonly mapProperties: MapProperties + + readonly selectedElement: UIEventSource + + /** + * If data is currently being fetched from external sources + */ + readonly dataIsLoading: Store + /** + * Only needed for 'ReplaceGeometryAction' + */ + readonly fullNodeDatabase?: FullNodeDatabaseSource + + readonly perLayer: ReadonlyMap + readonly userRelatedState: { readonly mangroveIdentity: MangroveIdentity } +} export interface SpecialVisualization { funcName: string - constr: ( - state: FeaturePipelineState, - tagSource: UIEventSource, - argument: string[], - guistate: DefaultGuiState - ) => BaseUIElement docs: string | BaseUIElement example?: string args: { name: string; defaultValue?: string; doc: string; required?: false | boolean }[] getLayerDependencies?: (argument: string[]) => string[] + + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + argument: string[], + feature: Feature + ): BaseUIElement } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 18842bda1..db4d1bd35 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -44,15 +44,13 @@ import { LoginToggle } from "./Popup/LoginButton" import Toggle from "./Input/Toggle" import { SubstitutedTranslation } from "./SubstitutedTranslation" import List from "./Base/List" -import { OsmFeature } from "../Models/OsmFeature" -import LayerConfig from "../Models/ThemeConfig/LayerConfig" -import { GeoOperations } from "../Logic/GeoOperations" import StatisticsPanel from "./BigComponents/StatisticsPanel" import AutoApplyButton from "./Popup/AutoApplyButton" import { LanguageElement } from "./Popup/LanguageElement" import FeatureReviews from "../Logic/Web/MangroveReviews" import Maproulette from "../Logic/Maproulette" import SvelteUIElement from "./Base/SvelteUIElement" +import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" export default class SpecialVisualizations { public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList() @@ -146,63 +144,17 @@ export default class SpecialVisualizations { new MultiApplyViz(), new ExportAsGpxViz(), new AddNoteCommentViz(), + new CloseNoteButton(), new PlantNetDetectionViz(), + + new TagApplyButton(), + new ImportPointButton(), new ImportWayButton(), new ConflateButton(), - new TagApplyButton(), - new CloseNoteButton(), + new NearbyImageVis(), - new MapillaryLinkVis(), - new LanguageElement(), - { - funcName: "all_tags", - docs: "Prints all key-value pairs of the object - used for debugging", - args: [], - constr: (state, tags: UIEventSource) => - new SvelteUIElement(AllTagsPanel, { tags, state }), - }, - { - funcName: "image_carousel", - docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)", - args: [ - { - name: "image_key", - defaultValue: AllImageProviders.defaultKeys.join(","), - doc: "The keys given to the images, e.g. if image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, etc... Multiple values are allowed if ';'-separated ", - }, - ], - constr: (state, tags, args) => { - let imagePrefixes: string[] = undefined - if (args.length > 0) { - imagePrefixes = [].concat(...args.map((a) => a.split(","))) - } - return new ImageCarousel( - AllImageProviders.LoadImagesFor(tags, imagePrefixes), - tags, - state - ) - }, - }, - { - funcName: "image_upload", - docs: "Creates a button where a user can upload an image to IMGUR", - args: [ - { - name: "image-key", - doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", - defaultValue: "image", - }, - { - name: "label", - doc: "The text to show on the button", - defaultValue: "Add image", - }, - ], - constr: (state, tags, args) => { - return new ImageUploadFlow(tags, state, args[0], args[1]) - }, - }, + { funcName: "wikipedia", docs: "A box showing the corresponding wikipedia article - based on the wikidata tag", @@ -267,6 +219,56 @@ export default class SpecialVisualizations { }) ), }, + new MapillaryLinkVis(), + new LanguageElement(), + { + funcName: "all_tags", + docs: "Prints all key-value pairs of the object - used for debugging", + args: [], + constr: (state, tags: UIEventSource) => + new SvelteUIElement(AllTagsPanel, { tags, state }), + }, + { + funcName: "image_carousel", + docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)", + args: [ + { + name: "image_key", + defaultValue: AllImageProviders.defaultKeys.join(","), + doc: "The keys given to the images, e.g. if image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, etc... Multiple values are allowed if ';'-separated ", + }, + ], + constr: (state, tags, args) => { + let imagePrefixes: string[] = undefined + if (args.length > 0) { + imagePrefixes = [].concat(...args.map((a) => a.split(","))) + } + return new ImageCarousel( + AllImageProviders.LoadImagesFor(tags, imagePrefixes), + tags, + state + ) + }, + }, + { + funcName: "image_upload", + docs: "Creates a button where a user can upload an image to IMGUR", + args: [ + { + name: "image-key", + doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", + defaultValue: "image", + }, + { + name: "label", + doc: "The text to show on the button", + defaultValue: "Add image", + }, + ], + constr: (state, tags, args) => { + return new ImageUploadFlow(tags, state, args[0], args[1]) + }, + }, { funcName: "reviews", docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten", @@ -283,14 +285,18 @@ export default class SpecialVisualizations { doc: "The identifier to use, if tags[subjectKey] as specified above is not available. This is effectively a fallback value", }, ], - constr: (state, tags, args) => { + constr: (state, tags, args, feature) => { const nameKey = args[0] ?? "name" let fallbackName = args[1] - const feature = state.allElements.ContainingFeatures.get(tags.data.id) - const mangrove = FeatureReviews.construct(feature, state, { - nameKey: nameKey, - fallbackName, - }) + const mangrove = FeatureReviews.construct( + feature, + tags, + state.userRelatedState.mangroveIdentity, + { + nameKey: nameKey, + fallbackName, + } + ) const form = new ReviewForm((r) => mangrove.createReview(r), state) return new ReviewElement(mangrove, form) @@ -348,7 +354,7 @@ export default class SpecialVisualizations { doc: "The path (or shorthand) that should be returned", }, ], - constr: (state, tagSource: UIEventSource, args) => { + constr: (_, tagSource: UIEventSource, args) => { const url = args[0] const shorthands = args[1] const neededValue = args[2] @@ -380,7 +386,7 @@ export default class SpecialVisualizations { return undefined } const allUnits = [].concat( - ...(state?.layoutToUse?.layers?.map((lyr) => lyr.units) ?? []) + ...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []) ) const unit = allUnits.filter((unit) => unit.isApplicableToKey(key) @@ -409,8 +415,8 @@ export default class SpecialVisualizations { ).onClick(() => { console.log("Exporting as Geojson") const tags = tagSource.data - const feature = state.allElements.ContainingFeatures.get(tags.id) - const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags) + const feature = state.indexedFeatures.featuresById.data.get(tags.id) + const matchingLayer = state?.layout?.getMatchingLayer(tags) const title = matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson" const data = JSON.stringify(feature, null, " ") @@ -429,15 +435,15 @@ export default class SpecialVisualizations { docs: "Opens the current view in the iD-editor", args: [], constr: (state, feature) => { - return new OpenIdEditor(state, undefined, feature.data.id) + return new OpenIdEditor(state.mapProperties, undefined, feature.data.id) }, }, { funcName: "open_in_josm", docs: "Opens the current view in the JOSM-editor", args: [], - constr: (state, feature) => { - return new OpenJosm(state) + constr: (state) => { + return new OpenJosm(state.osmConnection, state.mapProperties.bounds) }, }, { @@ -560,10 +566,10 @@ export default class SpecialVisualizations { docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'", example: "`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.", - constr: (state, tagsSource) => + constr: (state, tagsSource, args, feature) => new VariableUiElement( tagsSource.map((tags) => { - const layer = state.layoutToUse.getMatchingLayer(tags) + const layer = state.layout.getMatchingLayer(tags) const title = layer?.title?.GetRenderValue(tags) if (title === undefined) { return undefined @@ -575,7 +581,7 @@ export default class SpecialVisualizations { { funcName: "maproulette_task", args: [], - constr(state, tagSource, argument, guistate) { + constr(state, tagSource) { let parentId = tagSource.data.mr_challengeId if (parentId === undefined) { console.warn("Element ", tagSource.data.id, " has no mr_challengeId") @@ -666,7 +672,7 @@ export default class SpecialVisualizations { Number(maproulette_id), Number(status), { - tags: `MapComplete MapComplete:${state.layoutToUse.id}`, + tags: `MapComplete MapComplete:${state.layout.id}`, } ) tagsSource.data["mr_taskStatus"] = @@ -715,40 +721,19 @@ export default class SpecialVisualizations { docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer", args: [], constr: (state, tagsSource, args, guiState) => { - const elementsInview = new UIEventSource< - { - distance: number - center: [number, number] - element: OsmFeature - layer: LayerConfig - }[] - >([]) - - function update() { - const mapCenter = <[number, number]>[ - state.locationControl.data.lon, - state.locationControl.data.lon, - ] - const bbox = state.currentBounds.data - const elements = state.featurePipeline - .getAllVisibleElementsWithmeta(bbox) - .map((el) => { - const distance = GeoOperations.distanceBetween(el.center, mapCenter) - return { ...el, distance } - }) - elements.sort((e0, e1) => e0.distance - e1.distance) - elementsInview.setData(elements) - } - - state.currentBounds.addCallbackAndRun(update) - state.featurePipeline.newDataLoadedSignal.addCallback(update) - state.filteredLayers.addCallbackAndRun((fls) => { - for (const fl of fls) { - fl.isDisplayed.addCallback(update) - fl.appliedFilters.addCallback(update) - } - }) - return new StatisticsPanel(elementsInview, state) + return new Combine( + state.layout.layers + .filter((l) => l.name !== null) + .map( + (l) => { + const fs = state.perLayer.get(l.id) + const bbox = state.mapProperties.bounds.data + const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox) + return new StatisticsPanel(fsBboxed) + }, + [state.mapProperties.bounds] + ) + ) }, }, { @@ -777,7 +762,7 @@ export default class SpecialVisualizations { required: true, }, ], - constr(state, tags, args) { + constr(__, tags, args) { return new VariableUiElement( tags.map((tags) => { const [to, subject, body, button_text] = args.map((str) => diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index d8480d381..65e4cd077 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -2,7 +2,7 @@ import { UIEventSource } from "../Logic/UIEventSource" import { Translation } from "./i18n/Translation" import Locale from "./i18n/Locale" import { FixedUiElement } from "./Base/FixedUiElement" -import SpecialVisualizations from "./SpecialVisualizations" +// import SpecialVisualizations from "./SpecialVisualizations" import { Utils } from "../Utils" import { VariableUiElement } from "./Base/VariableUIElement" import Combine from "./Base/Combine" @@ -10,13 +10,13 @@ import BaseUIElement from "./BaseUIElement" import { DefaultGuiState } from "./DefaultGuiState" import FeaturePipelineState from "../Logic/State/FeaturePipelineState" import LinkToWeblate from "./Base/LinkToWeblate" -import { SpecialVisualization } from "./SpecialVisualization" +import { SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization" export class SubstitutedTranslation extends VariableUiElement { public constructor( translation: Translation, tagsSource: UIEventSource>, - state: FeaturePipelineState, + state: SpecialVisualizationState, mapping: Map< string, | BaseUIElement @@ -78,7 +78,7 @@ export class SubstitutedTranslation extends VariableUiElement { } try { return viz.func - .constr(state, tagsSource, proto.special.args, DefaultGuiState.state) + .constr(state, tagsSource, proto.special.args) ?.SetStyle(proto.special.style) } catch (e) { console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e) @@ -125,7 +125,7 @@ export class SubstitutedTranslation extends VariableUiElement { } for (const knownSpecial of extraMappings.concat( - SpecialVisualizations.specialVisualizations + [] // TODO enable SpecialVisualizations.specialVisualizations )) { // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way' const matched = template.match( @@ -181,10 +181,10 @@ export class SubstitutedTranslation extends VariableUiElement { console.warn( "Found a suspicious special rendering value in: ", template, - " did you mean one of: ", - SpecialVisualizations.specialVisualizations + " did you mean one of: " + /*SpecialVisualizations.specialVisualizations .map((sp) => sp.funcName + "()") - .join(", ") + .join(", ")*/ ) } diff --git a/UI/ThemeViewGUI.svelte b/UI/ThemeViewGUI.svelte index a68e718b9..90daaae91 100644 --- a/UI/ThemeViewGUI.svelte +++ b/UI/ThemeViewGUI.svelte @@ -1,154 +1,66 @@

-
Hello world
-
- +
+ state.guistate.welcomeMessageIsOpened.setData(true)}> +
+ + + {layout.title} + +
+
+ state.guistate.menuIsOpened.setData(true)}> + +
-
+
+ state.guistate.filterViewIsOpened.setData(true)}> + +
@@ -160,11 +72,116 @@ - new GeolocationControl(geolocation, mapproperties).SetClass("block w-8 h-8")}> +
-
+
+ + +
+ +
+
state.guistate.filterViewIsOpened.setData(false)}>Close
+ + {#each layout.layers as layer} + + {/each} + + +
+
+ + + +
+
+
state.guistate.welcomeMessageIsOpened.setData(false)}>Close
+ + + selected ? "tab-selected" : "tab-unselected"}>About + selected ? "tab-selected" : "tab-unselected"}>Tab 2 + selected ? "tab-selected" : "tab-unselected"}>Tab 3 + + + + layout.description}> + {Translations.t.general.welcomeExplanation.general} + {#if layout.layers.some((l) => l.presets?.length > 0)} + + {Translations.t.general.welcomeExplanation.addNew} + + {/if} + + + layout.descriptionTail}> +
+ +
+ + +
+ Content 2 + Content 3 +
+
+
+
+
+ + + + +
+
+
state.guistate.menuIsOpened.setData(false)}>Close
+ + + selected ? "tab-selected" : "tab-unselected"}>About MapComplete + selected ? "tab-selected" : "tab-unselected"}>Settings + selected ? "tab-selected" : "tab-unselected"}>Privacy + + + + About MC + + + + User settings + Privacy + + +
+
+
+ + +
+ + + +
+
+ + diff --git a/Utils.ts b/Utils.ts index 8bc0e011c..531516bc7 100644 --- a/Utils.ts +++ b/Utils.ts @@ -1317,4 +1317,34 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be // If the element has a parent, repeat the process for the parent element return Utils.findParentWithScrolling(element.parentElement) } + + /** + * Returns true if the contents of `a` are the same (and in the same order) as `b`. + * Might have false negatives in some cases + * @param a + * @param b + */ + public static sameList(a: ReadonlyArray, b: ReadonlyArray) { + if (a == b) { + return true + } + if (a === undefined || a === null || b === undefined || b === null) { + return false + } + if (a.length !== b.length) { + return false + } + for (let i = 0; i < a.length; i++) { + let ai = a[i] + let bi = b[i] + if (ai == bi) { + continue + } + if (ai === bi) { + continue + } + return false + } + return true + } } diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index d563745b4..2229edffe 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -572,34 +572,58 @@ video { width: 100%; } +.\!container { + width: 100% !important; +} + @media (min-width: 640px) { .container { max-width: 640px; } + + .\!container { + max-width: 640px !important; + } } @media (min-width: 768px) { .container { max-width: 768px; } + + .\!container { + max-width: 768px !important; + } } @media (min-width: 1024px) { .container { max-width: 1024px; } + + .\!container { + max-width: 1024px !important; + } } @media (min-width: 1280px) { .container { max-width: 1280px; } + + .\!container { + max-width: 1280px !important; + } } @media (min-width: 1536px) { .container { max-width: 1536px; } + + .\!container { + max-width: 1536px !important; + } } .sr-only { @@ -626,6 +650,10 @@ video { visibility: visible; } +.\!visible { + visibility: visible !important; +} + .invisible { visibility: hidden; } @@ -823,10 +851,6 @@ video { margin-bottom: 0.75rem; } -.mb-4 { - margin-bottom: 1rem; -} - .mt-1 { margin-top: 0.25rem; } @@ -847,10 +871,26 @@ video { margin-top: 1rem; } +.mt-2 { + margin-top: 0.5rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + .ml-4 { margin-left: 1rem; } +.mt-6 { + margin-top: 1.5rem; +} + .mb-24 { margin-bottom: 6rem; } @@ -859,10 +899,6 @@ video { margin-left: 0.25rem; } -.mt-2 { - margin-top: 0.5rem; -} - .mb-2 { margin-bottom: 0.5rem; } @@ -871,10 +907,6 @@ video { margin-top: 3rem; } -.ml-2 { - margin-left: 0.5rem; -} - .ml-3 { margin-left: 0.75rem; } @@ -903,10 +935,6 @@ video { margin-top: 2rem; } -.mt-6 { - margin-top: 1.5rem; -} - .mb-8 { margin-bottom: 2rem; } @@ -987,11 +1015,6 @@ video { height: 100%; } -.h-min { - height: -webkit-min-content; - height: min-content; -} - .h-8 { height: 2rem; } @@ -1032,14 +1055,14 @@ video { height: 0.75rem; } -.h-11 { - height: 2.75rem; -} - .h-6 { height: 1.5rem; } +.h-11 { + height: 2.75rem; +} + .h-96 { height: 24rem; } @@ -1060,6 +1083,10 @@ video { max-height: 20vh; } +.max-h-8 { + max-height: 2rem; +} + .max-h-32 { max-height: 8rem; } @@ -1068,10 +1095,6 @@ video { max-height: 1.75rem; } -.max-h-8 { - max-height: 2rem; -} - .min-h-\[8rem\] { min-height: 8rem; } @@ -1124,16 +1147,16 @@ video { width: 0.75rem; } -.w-11 { - width: 2.75rem; -} - .w-fit { width: -webkit-fit-content; width: -moz-fit-content; width: fit-content; } +.w-11 { + width: 2.75rem; +} + .w-1\/2 { width: 50%; } @@ -1303,10 +1326,18 @@ video { justify-content: space-between; } +.gap-1 { + gap: 0.25rem; +} + .gap-4 { gap: 1rem; } +.gap-y-1 { + row-gap: 0.25rem; +} + .space-x-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.5rem * var(--tw-space-x-reverse)); @@ -1443,6 +1474,11 @@ video { border-color: rgb(219 234 254 / var(--tw-border-opacity)); } +.border-gray-400 { + --tw-border-opacity: 1; + border-color: rgb(156 163 175 / var(--tw-border-opacity)); +} + .border-gray-300 { --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity)); @@ -1458,11 +1494,6 @@ video { border-color: rgb(132 204 22 / var(--tw-border-opacity)); } -.border-gray-400 { - --tw-border-opacity: 1; - border-color: rgb(156 163 175 / var(--tw-border-opacity)); -} - .border-blue-500 { --tw-border-opacity: 1; border-color: rgb(59 130 246 / var(--tw-border-opacity)); @@ -1549,6 +1580,10 @@ video { padding: 0.5rem; } +.p-8 { + padding: 2rem; +} + .p-3 { padding: 0.75rem; } @@ -1593,6 +1628,10 @@ video { padding-left: 1rem; } +.pt-2 { + padding-top: 0.5rem; +} + .pl-2 { padding-left: 0.5rem; } @@ -1617,10 +1656,6 @@ video { padding-right: 0.25rem; } -.pt-2 { - padding-top: 0.5rem; -} - .pb-2 { padding-bottom: 0.5rem; } @@ -1633,6 +1668,10 @@ video { padding-right: 0.5rem; } +.pt-0\.5 { + padding-top: 0.125rem; +} + .pb-8 { padding-bottom: 2rem; } @@ -1653,10 +1692,6 @@ video { padding-right: 0px; } -.pt-0\.5 { - padding-top: 0.125rem; -} - .pb-4 { padding-bottom: 1rem; } @@ -1868,12 +1903,6 @@ video { transition-duration: 150ms; } -.transition-all { - transition-property: all; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - .transition-\[color\2c background-color\2c box-shadow\] { transition-property: color,background-color,box-shadow; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); @@ -2478,26 +2507,6 @@ li::marker { } } -/**************************************/ - -#topleft-tools { - display: block; - position: absolute; - z-index: 5000; - transition: all 500ms linear; - left: 0; - right: 0; -} - -.welcomeMessage { - display: block; - max-width: calc(100vw - 5em); - width: 40em; - max-height: calc(100vh - 15em); - background-color: var(--background-color); - color: var(--foreground-color); -} - /***************** Info box (box containing features and questions ******************/ input { @@ -2670,6 +2679,10 @@ input { top: 0.75rem; } + .sm\:m-6 { + margin: 1.5rem; + } + .sm\:mx-auto { margin-left: auto; margin-right: auto; @@ -2701,10 +2714,6 @@ input { width: 6rem; } - .sm\:max-w-sm { - max-width: 24rem; - } - .sm\:max-w-xl { max-width: 36rem; } @@ -2725,6 +2734,10 @@ input { border-width: 4px; } + .sm\:p-6 { + padding: 1.5rem; + } + .sm\:p-0\.5 { padding: 0.125rem; } @@ -2782,6 +2795,10 @@ input { margin: 0.25rem; } + .md\:m-8 { + margin: 2rem; + } + .md\:m-2 { margin: 0.5rem; } @@ -2834,10 +2851,6 @@ input { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .md\:flex-row { - flex-direction: row; - } - .md\:rounded-xl { border-radius: 0.75rem; } @@ -2888,10 +2901,6 @@ input { font-size: 1.25rem; line-height: 1.75rem; } - - .md\:w-160 { - width: 40rem; - } } @media (min-width: 1024px) { diff --git a/index.css b/index.css index 4f707d254..fdaf2101d 100644 --- a/index.css +++ b/index.css @@ -542,25 +542,6 @@ li::marker { } } -/**************************************/ - -#topleft-tools { - display: block; - position: absolute; - z-index: 5000; - transition: all 500ms linear; - left: 0; - right: 0; -} - -.welcomeMessage { - display: block; - max-width: calc(100vw - 5em); - width: 40em; - max-height: calc(100vh - 15em); - background-color: var(--background-color); - color: var(--foreground-color); -} /***************** Info box (box containing features and questions ******************/ diff --git a/index.ts b/index.ts index 3a8fec515..a012c8cd5 100644 --- a/index.ts +++ b/index.ts @@ -5,7 +5,6 @@ import AllThemesGui from "./UI/AllThemesGui" import DetermineLayout from "./Logic/DetermineLayout" import LayoutConfig from "./Models/ThemeConfig/LayoutConfig" import DefaultGUI from "./UI/DefaultGUI" -import State from "./State" import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation" import { DefaultGuiState } from "./UI/DefaultGuiState" @@ -27,13 +26,7 @@ class Init { } const guiState = new DefaultGuiState() - State.state = new State(layoutToUse) DefaultGuiState.state = guiState - // This 'leaks' the global state via the window object, useful for debugging - // @ts-ignore - window.mapcomplete_state = State.state - - new DefaultGUI(State.state, guiState).setup() } } diff --git a/package-lock.json b/package-lock.json index 2f1747ea6..ba8d50ec5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "license": "GPL-3.0-or-later", "dependencies": { "@onsvisual/svelte-maps": "^1.1.6", + "@rgossiaux/svelte-headlessui": "^1.0.2", + "@rgossiaux/svelte-heroicons": "^0.1.2", "@rollup/plugin-typescript": "^11.0.0", "@turf/boolean-intersects": "^6.5.0", "@turf/buffer": "^6.5.0", @@ -31,10 +33,6 @@ "jest-mock": "^29.4.1", "jspdf": "^2.5.1", "latlon2country": "^1.2.6", - "leaflet": "^1.9.2", - "leaflet-polylineoffset": "^1.1.1", - "leaflet-providers": "^1.13.0", - "leaflet-simple-map-screenshoter": "^0.4.5", "libphonenumber-js": "^1.10.8", "lz-string": "^1.4.4", "mangrove-reviews-typescript": "^1.1.0", @@ -64,8 +62,6 @@ "@tsconfig/svelte": "^3.0.0", "@types/chai": "^4.3.0", "@types/geojson": "^7946.0.10", - "@types/leaflet-markercluster": "^1.0.3", - "@types/leaflet-providers": "^1.2.0", "@types/lz-string": "^1.3.34", "@types/node": "^18.11.18", "@types/papaparse": "^5.3.1", @@ -1912,6 +1908,22 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@rgossiaux/svelte-headlessui": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rgossiaux/svelte-headlessui/-/svelte-headlessui-1.0.2.tgz", + "integrity": "sha512-sauopYTSivhzXe1kAvgawkhyYJcQlK8Li3p0d2OtcCIVprOzdbard5lbqWB4xHDv83zAobt2mR08oizO2poHLQ==", + "peerDependencies": { + "svelte": "^3.44.0" + } + }, + "node_modules/@rgossiaux/svelte-heroicons": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@rgossiaux/svelte-heroicons/-/svelte-heroicons-0.1.2.tgz", + "integrity": "sha512-c5Ep1QDvBo9HD/P0AxbXItDbn6x77fldCjjL0aBjNseUntV4fkdHkBde1IaLr8i0kmrhTSovjkIen8W83jUPzg==", + "peerDependencies": { + "svelte": "^3.44.0" + } + }, "node_modules/@rollup/plugin-json": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.0.0.tgz", @@ -3663,34 +3675,6 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, - "node_modules/@types/leaflet": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.0.tgz", - "integrity": "sha512-7LeOSj7EloC5UcyOMo+1kc3S1UT3MjJxwqsMT1d2PTyvQz53w0Y0oSSk9nwZnOZubCmBvpSNGceucxiq+ZPEUw==", - "dev": true, - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/leaflet-markercluster": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/leaflet-markercluster/-/leaflet-markercluster-1.0.3.tgz", - "integrity": "sha512-GMVrmiFoNdYa0smv+OlnSulhso2BJVnRxNcYAG4l7rC2jCbhp72dvNoLSPnpdJ7MyMScImt9YHFEjLefHO11Uw==", - "deprecated": "'@types/leaflet-markercluster' is now '@types/leaflet.markercluster'.", - "dev": true, - "dependencies": { - "@types/leaflet": "*" - } - }, - "node_modules/@types/leaflet-providers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/leaflet-providers/-/leaflet-providers-1.2.1.tgz", - "integrity": "sha512-uNyuXiNV2q3fmgNjQji2P6RjQISmL40bbOL91/3OAwiE3XhkLKPmSAtAcfe11MAIz45iEjdFZJWppq9QyfnPIw==", - "dev": true, - "dependencies": { - "@types/leaflet": "*" - } - }, "node_modules/@types/lz-string": { "version": "1.3.34", "resolved": "https://registry.npmjs.org/@types/lz-string/-/lz-string-1.3.34.tgz", @@ -5357,11 +5341,6 @@ "doctest-ts-improved": "dist/main.js" } }, - "node_modules/dom-to-image-more": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-2.13.1.tgz", - "integrity": "sha512-ApVHqdGkwSMNcHFoJD/3BNfSTEq0a+GaVU8JNO29n+RZnwOUVtK8zznn4onXHJJlJT63dxQ2n5bwPgENlDZbnA==" - }, "node_modules/dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", @@ -5854,11 +5833,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/file-saver": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", - "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" - }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -7284,30 +7258,6 @@ "turf": "^3.0.14" } }, - "node_modules/leaflet": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz", - "integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==" - }, - "node_modules/leaflet-polylineoffset": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/leaflet-polylineoffset/-/leaflet-polylineoffset-1.1.1.tgz", - "integrity": "sha512-WcEjAROx9IhIVwSMoFy9p2QBCG9YeuGtJl4ZdunIgj4xbCdTrUkBj8JdonUeCyLPnD2/Vrem/raOPHm5LvebSw==" - }, - "node_modules/leaflet-providers": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/leaflet-providers/-/leaflet-providers-1.13.0.tgz", - "integrity": "sha512-f/sN5wdgBbVA2jcCYzScIfYNxKdn2wBJP9bu+5cRX9Xj6g8Bt1G9Sr8WgJAt/ckIFIc3LVVxCBNFpSCfTuUElg==" - }, - "node_modules/leaflet-simple-map-screenshoter": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/leaflet-simple-map-screenshoter/-/leaflet-simple-map-screenshoter-0.4.5.tgz", - "integrity": "sha512-bvd++mQstpgb7F7c2v5jCLlUitQ6CcYLZ56do7PVyyVpGvnlhIEk5S6dLXqpe3itSr2D/re5yUAOf8zBt95ViQ==", - "dependencies": { - "dom-to-image-more": "^2.8.0", - "file-saver": "^2.0.2" - } - }, "node_modules/levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -13319,6 +13269,18 @@ "integrity": "sha512-bjdcC2t77qBWA499nOZjwCK8tpRX8TgoMMYKWRIIIpL8fsDgG/Myd6FcGasV5RdD3v0im33RAjiZOA2Ybp+S3g==", "dev": true }, + "@rgossiaux/svelte-headlessui": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rgossiaux/svelte-headlessui/-/svelte-headlessui-1.0.2.tgz", + "integrity": "sha512-sauopYTSivhzXe1kAvgawkhyYJcQlK8Li3p0d2OtcCIVprOzdbard5lbqWB4xHDv83zAobt2mR08oizO2poHLQ==", + "requires": {} + }, + "@rgossiaux/svelte-heroicons": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@rgossiaux/svelte-heroicons/-/svelte-heroicons-0.1.2.tgz", + "integrity": "sha512-c5Ep1QDvBo9HD/P0AxbXItDbn6x77fldCjjL0aBjNseUntV4fkdHkBde1IaLr8i0kmrhTSovjkIen8W83jUPzg==", + "requires": {} + }, "@rollup/plugin-json": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.0.0.tgz", @@ -14701,33 +14663,6 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, - "@types/leaflet": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.0.tgz", - "integrity": "sha512-7LeOSj7EloC5UcyOMo+1kc3S1UT3MjJxwqsMT1d2PTyvQz53w0Y0oSSk9nwZnOZubCmBvpSNGceucxiq+ZPEUw==", - "dev": true, - "requires": { - "@types/geojson": "*" - } - }, - "@types/leaflet-markercluster": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/leaflet-markercluster/-/leaflet-markercluster-1.0.3.tgz", - "integrity": "sha512-GMVrmiFoNdYa0smv+OlnSulhso2BJVnRxNcYAG4l7rC2jCbhp72dvNoLSPnpdJ7MyMScImt9YHFEjLefHO11Uw==", - "dev": true, - "requires": { - "@types/leaflet": "*" - } - }, - "@types/leaflet-providers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/leaflet-providers/-/leaflet-providers-1.2.1.tgz", - "integrity": "sha512-uNyuXiNV2q3fmgNjQji2P6RjQISmL40bbOL91/3OAwiE3XhkLKPmSAtAcfe11MAIz45iEjdFZJWppq9QyfnPIw==", - "dev": true, - "requires": { - "@types/leaflet": "*" - } - }, "@types/lz-string": { "version": "1.3.34", "resolved": "https://registry.npmjs.org/@types/lz-string/-/lz-string-1.3.34.tgz", @@ -15997,11 +15932,6 @@ "typescript": "^4.6.2" } }, - "dom-to-image-more": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-2.13.1.tgz", - "integrity": "sha512-ApVHqdGkwSMNcHFoJD/3BNfSTEq0a+GaVU8JNO29n+RZnwOUVtK8zznn4onXHJJlJT63dxQ2n5bwPgENlDZbnA==" - }, "dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", @@ -16404,11 +16334,6 @@ "escape-string-regexp": "^1.0.5" } }, - "file-saver": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", - "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -17472,30 +17397,6 @@ "turf": "^3.0.14" } }, - "leaflet": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz", - "integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==" - }, - "leaflet-polylineoffset": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/leaflet-polylineoffset/-/leaflet-polylineoffset-1.1.1.tgz", - "integrity": "sha512-WcEjAROx9IhIVwSMoFy9p2QBCG9YeuGtJl4ZdunIgj4xbCdTrUkBj8JdonUeCyLPnD2/Vrem/raOPHm5LvebSw==" - }, - "leaflet-providers": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/leaflet-providers/-/leaflet-providers-1.13.0.tgz", - "integrity": "sha512-f/sN5wdgBbVA2jcCYzScIfYNxKdn2wBJP9bu+5cRX9Xj6g8Bt1G9Sr8WgJAt/ckIFIc3LVVxCBNFpSCfTuUElg==" - }, - "leaflet-simple-map-screenshoter": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/leaflet-simple-map-screenshoter/-/leaflet-simple-map-screenshoter-0.4.5.tgz", - "integrity": "sha512-bvd++mQstpgb7F7c2v5jCLlUitQ6CcYLZ56do7PVyyVpGvnlhIEk5S6dLXqpe3itSr2D/re5yUAOf8zBt95ViQ==", - "requires": { - "dom-to-image-more": "^2.8.0", - "file-saver": "^2.0.2" - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", diff --git a/package.json b/package.json index b1b3564c4..4f8a4e18f 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,8 @@ ], "dependencies": { "@onsvisual/svelte-maps": "^1.1.6", + "@rgossiaux/svelte-headlessui": "^1.0.2", + "@rgossiaux/svelte-heroicons": "^0.1.2", "@rollup/plugin-typescript": "^11.0.0", "@turf/boolean-intersects": "^6.5.0", "@turf/buffer": "^6.5.0", diff --git a/scripts/slice.ts b/scripts/slice.ts index a78641854..77b7b65c9 100644 --- a/scripts/slice.ts +++ b/scripts/slice.ts @@ -1,5 +1,4 @@ import * as fs from "fs" -import TiledFeatureSource from "../Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource" import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" import * as readline from "readline" import ScriptUtils from "./ScriptUtils" @@ -7,6 +6,8 @@ import { Utils } from "../Utils" import Script from "./Script" import { BBox } from "../Logic/BBox" import { GeoOperations } from "../Logic/GeoOperations" +import { Tiles } from "../Models/TileRange" +import { Feature } from "geojson" /** * This script slices a big newline-delimeted geojson file into tiled geojson @@ -91,6 +92,71 @@ class Slice extends Script { } } + private handleTileData( + features: Feature[], + tileIndex: number, + outputDirectory: string, + doSlice: boolean, + handled: number, + maxNumberOfTiles: number + ) { + const [z, x, y] = Tiles.tile_from_index(tileIndex) + const path = `${outputDirectory}/tile_${z}_${x}_${y}.geojson` + const box = BBox.fromTileIndex(tileIndex) + if (doSlice) { + features = Utils.NoNull( + features.map((f) => { + const bbox = box.asGeoJson({}) + const properties = { + ...f.properties, + id: (f.properties?.id ?? "") + "_" + z + "_" + x + "_" + y, + } + + if (GeoOperations.completelyWithin(bbox, f)) { + bbox.properties = properties + return bbox + } + const intersection = GeoOperations.intersect(f, box.asGeoJson({})) + if (intersection) { + intersection.properties = properties + } + return intersection + }) + ) + } + features.forEach((f) => { + delete f.bbox + }) + if (features.length === 0) { + ScriptUtils.erasableLog( + handled + "/" + maxNumberOfTiles, + "Not writing ", + path, + ": no features" + ) + return + } + fs.writeFileSync( + path, + JSON.stringify( + { + type: "FeatureCollection", + features: features, + }, + null, + " " + ) + ) + ScriptUtils.erasableLog( + handled + "/" + maxNumberOfTiles, + "Written ", + path, + "which has ", + features.length, + "features" + ) + } + async main(args: string[]) { console.log("GeoJSON slicer") if (args.length < 3) { @@ -136,75 +202,18 @@ class Slice extends Script { } const maxNumberOfTiles = Math.pow(2, zoomlevel) * Math.pow(2, zoomlevel) let handled = 0 - TiledFeatureSource.createHierarchy(StaticFeatureSource.fromGeojson(allFeatures), { - minZoomLevel: zoomlevel, - maxZoomLevel: zoomlevel, - maxFeatureCount: Number.MAX_VALUE, - registerTile: (tile) => { + StaticFeatureSource.fromGeojson(allFeatures).features.addCallbackAndRun((feats) => { + GeoOperations.slice(zoomlevel, feats).forEach((tileData, tileIndex) => { handled = handled + 1 - const path = `${outputDirectory}/tile_${tile.z}_${tile.x}_${tile.y}.geojson` - const box = BBox.fromTile(tile.z, tile.x, tile.y) - let features = tile.features.data.map((ff) => ff.feature) - if (doSlice) { - features = Utils.NoNull( - features.map((f) => { - const bbox = box.asGeoJson({}) - const properties = { - ...f.properties, - id: - (f.properties?.id ?? "") + - "_" + - tile.z + - "_" + - tile.x + - "_" + - tile.y, - } - - if (GeoOperations.completelyWithin(bbox, f)) { - bbox.properties = properties - return bbox - } - const intersection = GeoOperations.intersect(f, box.asGeoJson({})) - if (intersection) { - intersection.properties = properties - } - return intersection - }) - ) - } - features.forEach((f) => { - delete f.bbox - }) - if (features.length === 0) { - ScriptUtils.erasableLog( - handled + "/" + maxNumberOfTiles, - "Not writing ", - path, - ": no features" - ) - return - } - fs.writeFileSync( - path, - JSON.stringify( - { - type: "FeatureCollection", - features: features, - }, - null, - " " - ) + this.handleTileData( + tileData, + tileIndex, + outputDirectory, + doSlice, + handled, + maxNumberOfTiles ) - ScriptUtils.erasableLog( - handled + "/" + maxNumberOfTiles, - "Written ", - path, - "which has ", - tile.features.data.length, - "features" - ) - }, + }) }) } } diff --git a/test.ts b/test.ts index 73c93eb39..56d594dab 100644 --- a/test.ts +++ b/test.ts @@ -1,17 +1,17 @@ import SvelteUIElement from "./UI/Base/SvelteUIElement" import ThemeViewGUI from "./UI/ThemeViewGUI.svelte" import { FixedUiElement } from "./UI/Base/FixedUiElement" -import { QueryParameters } from "./Logic/Web/QueryParameters" import LayoutConfig from "./Models/ThemeConfig/LayoutConfig" import * as benches from "./assets/generated/themes/benches.json" async function main() { - new FixedUiElement("Determining layout...").AttachTo("maindiv") - const qp = QueryParameters.GetQueryParameter("layout", "") new FixedUiElement("").AttachTo("extradiv") const layout = new LayoutConfig(benches, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data) - console.log("Using layout", layout.id) new SvelteUIElement(ThemeViewGUI, { layout }).AttachTo("maindiv") } +async function test() { + // new LengthInput().AttachTo("maindiv") +} +//test().then((_) => {}) main().then((_) => {})