From b94a8f5745ba79083754ce2062aa035050a86087 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 26 Mar 2023 05:58:28 +0200 Subject: [PATCH] refactoring: more state splitting, basic layoutFeatureSource --- Logic/Actors/ChangeToElementsActor.ts | 6 +- Logic/Actors/OverpassFeatureSource.ts | 167 ++++------ Logic/Actors/SelectedElementTagsUpdater.ts | 55 ++-- Logic/BBox.ts | 3 + Logic/ElementStorage.ts | 120 ------- Logic/ExtraFunctions.ts | 15 - .../Actors/FeaturePropertiesStore.ts | 107 +++++++ .../Actors/MetaTagRecalculator.ts | 2 - .../RegisteringAllFromFeatureSourceActor.ts | 20 -- Logic/FeatureSource/FeaturePipeline.ts | 38 +-- Logic/FeatureSource/FeatureSource.ts | 2 +- Logic/FeatureSource/LayoutSource.ts | 129 ++++++++ .../PerLayerFeatureSourceSplitter.ts | 8 +- .../Sources/FeatureSourceMerger.ts | 84 ++--- .../Sources/FilteringFeatureSource.ts | 58 ++-- Logic/FeatureSource/Sources/GeoJsonSource.ts | 192 +++++------ .../Sources/SimpleFeatureSource.ts | 8 +- .../FeatureSource/TileFreshnessCalculator.ts | 64 ---- .../DynamicGeoJsonTileSource.ts | 44 +-- .../TiledFeatureSource/DynamicTileSource.ts | 116 +++---- .../TiledFeatureSource/OsmFeatureSource.ts | 148 ++++----- .../TiledFeatureSource/TileHierarchy.ts | 2 +- Logic/MetaTagging.ts | 34 +- Logic/Osm/Changes.ts | 19 +- Logic/Osm/ChangesetHandler.ts | 158 +++------- Logic/Osm/OsmConnection.ts | 158 ++++------ Logic/Osm/Overpass.ts | 9 +- Logic/Osm/RelationsTracker.ts | 76 ----- Logic/SimpleMetaTagger.ts | 297 ++++++++++-------- Logic/State/FeaturePipelineState.ts | 32 +- Models/MapProperties.ts | 2 - Models/ThemeConfig/DependencyCalculator.ts | 1 - Models/ThemeConfig/Json/LayerConfigJson.ts | 39 +-- Models/ThemeConfig/LayerConfig.ts | 4 +- Models/ThemeConfig/SourceConfig.ts | 2 - Models/TileRange.ts | 12 + UI/AllThemesGui.ts | 16 +- UI/BigComponents/FeaturedMessage.ts | 103 ------ .../CompareToAlreadyExistingNotes.ts | 2 - UI/Map/MapLibreAdaptor.ts | 9 +- UI/Map/ShowDataLayer.ts | 120 +++---- UI/Map/ShowDataLayerOptions.ts | 15 +- UI/Map/ShowDataMultiLayer.ts | 23 +- UI/ShowDataLayer/ShowOverlayLayer.ts | 21 -- .../ShowOverlayLayerImplementation.ts | 1 + UI/ShowDataLayer/TileHierarchyAggregator.ts | 257 --------------- UI/ThemeViewGUI.svelte | 38 ++- .../layers/grass_in_parks/grass_in_parks.json | 70 ----- assets/themes/speelplekken/speelplekken.json | 24 +- assets/welcome_message.json | 61 ---- css/index-tailwind-output.css | 11 +- scripts/generateCache.ts | 30 +- test.ts | 3 +- test/Logic/ExtraFunctions.spec.ts | 1 - 54 files changed, 1067 insertions(+), 1969 deletions(-) delete mode 100644 Logic/ElementStorage.ts create mode 100644 Logic/FeatureSource/Actors/FeaturePropertiesStore.ts delete mode 100644 Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts create mode 100644 Logic/FeatureSource/LayoutSource.ts delete mode 100644 Logic/FeatureSource/TileFreshnessCalculator.ts delete mode 100644 Logic/Osm/RelationsTracker.ts delete mode 100644 UI/BigComponents/FeaturedMessage.ts delete mode 100644 UI/ShowDataLayer/ShowOverlayLayer.ts delete mode 100644 UI/ShowDataLayer/TileHierarchyAggregator.ts delete mode 100644 assets/layers/grass_in_parks/grass_in_parks.json delete mode 100644 assets/welcome_message.json diff --git a/Logic/Actors/ChangeToElementsActor.ts b/Logic/Actors/ChangeToElementsActor.ts index 1eac44bfd..3354dbfa6 100644 --- a/Logic/Actors/ChangeToElementsActor.ts +++ b/Logic/Actors/ChangeToElementsActor.ts @@ -1,15 +1,15 @@ -import { ElementStorage } from "../ElementStorage" import { Changes } from "../Osm/Changes" +import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"; export default class ChangeToElementsActor { - constructor(changes: Changes, allElements: ElementStorage) { + constructor(changes: Changes, allElements: FeaturePropertiesStore) { changes.pendingChanges.addCallbackAndRun((changes) => { for (const change of changes) { const id = change.type + "/" + change.id if (!allElements.has(id)) { continue // Ignored as the geometryFixer will introduce this } - const src = allElements.getEventSourceById(id) + const src = allElements.getStore(id) let changed = false for (const kv of change.tags ?? []) { diff --git a/Logic/Actors/OverpassFeatureSource.ts b/Logic/Actors/OverpassFeatureSource.ts index bf8c44464..acccf872e 100644 --- a/Logic/Actors/OverpassFeatureSource.ts +++ b/Logic/Actors/OverpassFeatureSource.ts @@ -1,23 +1,19 @@ -import { Store, UIEventSource } from "../UIEventSource" +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 SimpleMetaTagger from "../SimpleMetaTagger" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import RelationsTracker from "../Osm/RelationsTracker" import { BBox } from "../BBox" -import Loc from "../../Models/Loc" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import Constants from "../../Models/Constants" -import TileFreshnessCalculator from "../FeatureSource/TileFreshnessCalculator" -import { Tiles } from "../../Models/TileRange" import { Feature } from "geojson" +/** + * A wrapper around the 'Overpass'-object. + * It has more logic and will automatically fetch the data for the right bbox and the active layers + */ export default class OverpassFeatureSource implements FeatureSource { - public readonly name = "OverpassFeatureSource" - /** * The last loaded features, as geojson */ @@ -26,106 +22,67 @@ export default class OverpassFeatureSource implements FeatureSource { public readonly runningQuery: UIEventSource = new UIEventSource(false) public readonly timeout: UIEventSource = new UIEventSource(0) - public readonly relationsTracker: RelationsTracker - private readonly retries: UIEventSource = new UIEventSource(0) private readonly state: { - readonly locationControl: Store + readonly zoom: Store readonly layoutToUse: LayoutConfig readonly overpassUrl: Store readonly overpassTimeout: Store - readonly currentBounds: Store + readonly bounds: Store } private readonly _isActive: Store - /** - * Callback to handle all the data - */ - private readonly onBboxLoaded: ( - bbox: BBox, - date: Date, - layers: LayerConfig[], - zoomlevel: number - ) => void - - /** - * Keeps track of how fresh the data is - * @private - */ - private readonly freshnesses: Map + private readonly padToZoomLevel?: Store + private _lastQueryBBox: BBox constructor( state: { - readonly locationControl: Store readonly layoutToUse: LayoutConfig + readonly zoom: Store readonly overpassUrl: Store readonly overpassTimeout: Store readonly overpassMaxZoom: Store - readonly currentBounds: Store + readonly bounds: Store }, - options: { - padToTiles: Store + options?: { + padToTiles?: Store isActive?: Store - relationTracker: RelationsTracker - onBboxLoaded?: ( - bbox: BBox, - date: Date, - layers: LayerConfig[], - zoomlevel: number - ) => void - freshnesses?: Map } ) { this.state = state - this._isActive = options.isActive - this.onBboxLoaded = options.onBboxLoaded - this.relationsTracker = options.relationTracker - this.freshnesses = options.freshnesses + this._isActive = options?.isActive ?? new ImmutableStore(true) + this.padToZoomLevel = options?.padToTiles const self = this - state.currentBounds.addCallback((_) => { - self.update(options.padToTiles.data) + state.bounds.addCallbackD((_) => { + self.updateAsyncIfNeeded() }) } + /** + * Creates the 'Overpass'-object for the given layers + * @param interpreterUrl + * @param layersToDownload + * @constructor + * @private + */ private GetFilter(interpreterUrl: string, layersToDownload: LayerConfig[]): Overpass { - let filters: TagsFilter[] = [] - let extraScripts: string[] = [] - for (const layer of layersToDownload) { - if (layer.source.overpassScript !== undefined) { - extraScripts.push(layer.source.overpassScript) - } else { - filters.push(layer.source.osmTags) - } - } + let filters: TagsFilter[] = layersToDownload.map((layer) => layer.source.osmTags) filters = Utils.NoNull(filters) - extraScripts = Utils.NoNull(extraScripts) - if (filters.length + extraScripts.length === 0) { + if (filters.length === 0) { return undefined } - return new Overpass( - new Or(filters), - extraScripts, - interpreterUrl, - this.state.overpassTimeout, - this.relationsTracker - ) + return new Overpass(new Or(filters), [], interpreterUrl, this.state.overpassTimeout) } - private update(paddedZoomLevel: number) { - if (!this._isActive.data) { + /** + * + * @private + */ + private async updateAsyncIfNeeded(): Promise { + if (!this._isActive?.data) { + console.log("OverpassFeatureSource: not triggering as not active") return } - const self = this - this.updateAsync(paddedZoomLevel).then((bboxDate) => { - if (bboxDate === undefined || self.onBboxLoaded === undefined) { - return - } - const [bbox, date, layers] = bboxDate - self.onBboxLoaded(bbox, date, layers, paddedZoomLevel) - }) - } - - private async updateAsync(padToZoomLevel: number): Promise<[BBox, Date, LayerConfig[]]> { if (this.runningQuery.data) { console.log("Still running a query, not updating") return undefined @@ -135,15 +92,27 @@ export default class OverpassFeatureSource implements FeatureSource { console.log("Still in timeout - not updating") return undefined } + const requestedBounds = this.state.bounds.data + if ( + this._lastQueryBBox !== undefined && + requestedBounds.isContainedIn(this._lastQueryBBox) + ) { + return undefined + } + const [bounds, date, updatedLayers] = await this.updateAsync() + this._lastQueryBBox = bounds + } + /** + * Download the relevant data from overpass. Attempt to use a different server; only downloads the relevant layers + * @private + */ + private async updateAsync(): Promise<[BBox, Date, LayerConfig[]]> { let data: any = undefined let date: Date = undefined let lastUsed = 0 const layersToDownload = [] - const neededTiles = this.state.currentBounds.data - .expandToTileBounds(padToZoomLevel) - .containingTileRange(padToZoomLevel) for (const layer of this.state.layoutToUse.layers) { if (typeof layer === "string") { throw "A layer was not expanded!" @@ -151,7 +120,7 @@ export default class OverpassFeatureSource implements FeatureSource { if (layer.source === undefined) { continue } - if (this.state.locationControl.data.zoom < layer.minzoom) { + if (this.state.zoom.data < layer.minzoom) { continue } if (layer.doNotDownload) { @@ -161,31 +130,10 @@ export default class OverpassFeatureSource implements FeatureSource { // Not our responsibility to download this layer! continue } - const freshness = this.freshnesses?.get(layer.id) - if (freshness !== undefined) { - const oldestDataDate = - Math.min( - ...Tiles.MapRange(neededTiles, (x, y) => { - const date = freshness.freshnessFor(padToZoomLevel, x, y) - if (date === undefined) { - return 0 - } - return date.getTime() - }) - ) / 1000 - const now = new Date().getTime() - const minRequiredAge = now / 1000 - layer.maxAgeOfCache - if (oldestDataDate >= minRequiredAge) { - // still fresh enough - not updating - continue - } - } - layersToDownload.push(layer) } if (layersToDownload.length == 0) { - console.debug("Not updating - no layers needed") return } @@ -194,12 +142,13 @@ export default class OverpassFeatureSource implements FeatureSource { if (overpassUrls === undefined || overpassUrls.length === 0) { throw "Panic: overpassFeatureSource didn't receive any overpassUrls" } + // Note: the bounds are updated between attempts, in case that the user zoomed around let bounds: BBox do { try { - bounds = this.state.currentBounds.data + bounds = this.state.bounds.data ?.pad(this.state.layoutToUse.widenFactor) - ?.expandToTileBounds(padToZoomLevel) + ?.expandToTileBounds(this.padToZoomLevel?.data) if (bounds === undefined) { return undefined @@ -228,7 +177,6 @@ export default class OverpassFeatureSource implements FeatureSource { while (self.timeout.data > 0) { await Utils.waitFor(1000) - console.log(self.timeout.data) self.timeout.data-- self.timeout.ping() } @@ -240,14 +188,7 @@ export default class OverpassFeatureSource implements FeatureSource { if (data === undefined) { return undefined } - data.features.forEach((feature) => - SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature( - feature, - undefined, - this.state - ) - ) - self.features.setData(data.features.map((f) => ({ feature: f, freshness: date }))) + self.features.setData(data.features) return [bounds, date, layersToDownload] } catch (e) { console.error("Got the overpass response, but could not process it: ", e, e.stack) diff --git a/Logic/Actors/SelectedElementTagsUpdater.ts b/Logic/Actors/SelectedElementTagsUpdater.ts index 3df0a4804..bb90fe28b 100644 --- a/Logic/Actors/SelectedElementTagsUpdater.ts +++ b/Logic/Actors/SelectedElementTagsUpdater.ts @@ -2,12 +2,13 @@ * This actor will download the latest version of the selected element from OSM and update the tags if necessary. */ import { UIEventSource } from "../UIEventSource" -import { ElementStorage } from "../ElementStorage" import { Changes } from "../Osm/Changes" import { OsmObject } from "../Osm/OsmObject" import { OsmConnection } from "../Osm/OsmConnection" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import SimpleMetaTagger from "../SimpleMetaTagger" +import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" +import { Feature } from "geojson" export default class SelectedElementTagsUpdater { private static readonly metatags = new Set([ @@ -19,28 +20,34 @@ export default class SelectedElementTagsUpdater { "id", ]) + private readonly state: { + selectedElement: UIEventSource + allElements: FeaturePropertiesStore + changes: Changes + osmConnection: OsmConnection + layoutToUse: LayoutConfig + } + constructor(state: { - selectedElement: UIEventSource - allElements: ElementStorage + selectedElement: UIEventSource + allElements: FeaturePropertiesStore changes: Changes osmConnection: OsmConnection layoutToUse: LayoutConfig }) { + this.state = state state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { - if (isLoggedIn) { - SelectedElementTagsUpdater.installCallback(state) - return true + if (!isLoggedIn) { + return } + this.installCallback() + // We only have to do this once... + return true }) } - public static installCallback(state: { - selectedElement: UIEventSource - allElements: ElementStorage - changes: Changes - osmConnection: OsmConnection - layoutToUse: LayoutConfig - }) { + private installCallback() { + const state = this.state state.selectedElement.addCallbackAndRunD(async (s) => { let id = s.properties?.id @@ -62,7 +69,7 @@ export default class SelectedElementTagsUpdater { const latestTags = await OsmObject.DownloadPropertiesOf(id) if (latestTags === "deleted") { console.warn("The current selected element has been deleted upstream!") - const currentTagsSource = state.allElements.getEventSourceById(id) + const currentTagsSource = state.allElements.getStore(id) if (currentTagsSource.data["_deleted"] === "yes") { return } @@ -70,25 +77,15 @@ export default class SelectedElementTagsUpdater { currentTagsSource.ping() return } - SelectedElementTagsUpdater.applyUpdate(state, latestTags, id) + this.applyUpdate(latestTags, id) console.log("Updated", id) } catch (e) { console.warn("Could not update", id, " due to", e) } }) } - - public static applyUpdate( - state: { - selectedElement: UIEventSource - allElements: ElementStorage - changes: Changes - osmConnection: OsmConnection - layoutToUse: LayoutConfig - }, - latestTags: any, - id: string - ) { + private applyUpdate(latestTags: any, id: string) { + const state = this.state try { const leftRightSensitive = state.layoutToUse.isLeftRightSensitive() @@ -115,7 +112,7 @@ export default class SelectedElementTagsUpdater { // With the changes applied, we merge them onto the upstream object let somethingChanged = false - const currentTagsSource = state.allElements.getEventSourceById(id) + const currentTagsSource = state.allElements.getStore(id) const currentTags = currentTagsSource.data for (const key in latestTags) { let osmValue = latestTags[key] @@ -135,7 +132,7 @@ export default class SelectedElementTagsUpdater { if (currentKey.startsWith("_")) { continue } - if (this.metatags.has(currentKey)) { + if (SelectedElementTagsUpdater.metatags.has(currentKey)) { continue } if (currentKey in latestTags) { diff --git a/Logic/BBox.ts b/Logic/BBox.ts index 3a875196e..7f4214790 100644 --- a/Logic/BBox.ts +++ b/Logic/BBox.ts @@ -214,6 +214,9 @@ export class BBox { * @param zoomlevel */ expandToTileBounds(zoomlevel: number): BBox { + if(zoomlevel === undefined){ + return this + } const ul = Tiles.embedded_tile(this.minLat, this.minLon, zoomlevel) const lr = Tiles.embedded_tile(this.maxLat, this.maxLon, zoomlevel) const boundsul = Tiles.tile_bounds_lon_lat(ul.z, ul.x, ul.y) diff --git a/Logic/ElementStorage.ts b/Logic/ElementStorage.ts deleted file mode 100644 index 49213b99f..000000000 --- a/Logic/ElementStorage.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Keeps track of a dictionary 'elementID' -> UIEventSource - */ -import { UIEventSource } from "./UIEventSource" -import { GeoJSONObject } from "@turf/turf" -import { Feature, Geometry, Point } from "geojson" -import { OsmTags } from "../Models/OsmFeature" - -export class ElementStorage { - public ContainingFeatures = new Map>() - private _elements = new Map>() - - constructor() {} - - addElementById(id: string, eventSource: UIEventSource) { - this._elements.set(id, eventSource) - } - - /** - * Creates a UIEventSource for the tags of the given feature. - * If an UIEventsource has been created previously, the same UIEventSource will be returned - * - * Note: it will cleverly merge the tags, if needed - */ - addOrGetElement(feature: Feature): UIEventSource { - const elementId = feature.properties.id - const newProperties = feature.properties - - const es = this.addOrGetById(elementId, newProperties) - - // At last, we overwrite the tag of the new feature to use the tags in the already existing event source - feature.properties = es.data - - if (!this.ContainingFeatures.has(elementId)) { - this.ContainingFeatures.set(elementId, feature) - } - - return es - } - - getEventSourceById(elementId): UIEventSource { - if (elementId === undefined) { - return undefined - } - return this._elements.get(elementId) - } - - has(id) { - return this._elements.has(id) - } - - addAlias(oldId: string, newId: string) { - if (newId === undefined) { - // We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap! - const element = this.getEventSourceById(oldId) - element.data._deleted = "yes" - element.ping() - return - } - - if (oldId == newId) { - return undefined - } - const element = this.getEventSourceById(oldId) - if (element === undefined) { - // Element to rewrite not found, probably a node or relation that is not rendered - return undefined - } - element.data.id = newId - this.addElementById(newId, element) - this.ContainingFeatures.set(newId, this.ContainingFeatures.get(oldId)) - element.ping() - } - - private addOrGetById(elementId: string, newProperties: any): UIEventSource { - if (!this._elements.has(elementId)) { - const eventSource = new UIEventSource(newProperties, "tags of " + elementId) - this._elements.set(elementId, eventSource) - return eventSource - } - - const es = this._elements.get(elementId) - if (es.data == newProperties) { - // Reference comparison gives the same object! we can just return the event source - return es - } - const keptKeys = es.data - // The element already exists - // We use the new feature to overwrite all the properties in the already existing eventsource - const debug_msg = [] - let somethingChanged = false - for (const k in newProperties) { - if (!newProperties.hasOwnProperty(k)) { - continue - } - const v = newProperties[k] - - if (keptKeys[k] !== v) { - if (v === undefined) { - // The new value is undefined; the tag might have been removed - // It might be a metatag as well - // In the latter case, we do keep the tag! - if (!k.startsWith("_")) { - delete keptKeys[k] - debug_msg.push("Erased " + k) - } - } else { - keptKeys[k] = v - debug_msg.push(k + " --> " + v) - } - - somethingChanged = true - } - } - if (somethingChanged) { - es.ping() - } - return es - } -} diff --git a/Logic/ExtraFunctions.ts b/Logic/ExtraFunctions.ts index 3da6f34b4..da0c9ca0d 100644 --- a/Logic/ExtraFunctions.ts +++ b/Logic/ExtraFunctions.ts @@ -14,7 +14,6 @@ export interface ExtraFuncParams { * Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...] */ getFeaturesWithin: (layerId: string, bbox: BBox) => Feature[][] - memberships: RelationsTracker getFeatureById: (id: string) => Feature } @@ -401,19 +400,6 @@ class ClosestNObjectFunc implements ExtraFunction { } } -class Memberships implements ExtraFunction { - _name = "memberships" - _doc = - "Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of. " + - "\n\n" + - "For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`" - _args = [] - - _f(params, feat) { - return () => params.memberships.knownRelations.data.get(feat.properties.id) ?? [] - } -} - class GetParsed implements ExtraFunction { _name = "get" _doc = @@ -481,7 +467,6 @@ export class ExtraFunctions { new IntersectionFunc(), new ClosestObjectFunc(), new ClosestNObjectFunc(), - new Memberships(), new GetParsed(), ] diff --git a/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts b/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts new file mode 100644 index 000000000..b4700e72b --- /dev/null +++ b/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts @@ -0,0 +1,107 @@ +import FeatureSource, { IndexedFeatureSource } from "../FeatureSource" +import { UIEventSource } from "../../UIEventSource" + +/** + * Constructs a UIEventStore for the properties of every Feature, indexed by id + */ +export default class FeaturePropertiesStore { + private readonly _source: FeatureSource & IndexedFeatureSource + private readonly _elements = new Map>() + + constructor(source: FeatureSource & IndexedFeatureSource) { + this._source = source + const self = this + source.features.addCallbackAndRunD((features) => { + for (const feature of features) { + const id = feature.properties.id + if (id === undefined) { + console.trace("Error: feature without ID:", feature) + throw "Error: feature without ID" + } + + const source = self._elements.get(id) + if (source === undefined) { + self._elements.set(id, new UIEventSource(feature.properties)) + continue + } + + if (source.data === feature.properties) { + continue + } + + // Update the tags in the old store and link them + const changeMade = FeaturePropertiesStore.mergeTags(source.data, feature.properties) + feature.properties = source.data + if (changeMade) { + source.ping() + } + } + }) + } + + public getStore(id: string): UIEventSource> { + return this._elements.get(id) + } + + /** + * Overwrites the tags of the old properties object, returns true if a change was made. + * Metatags are overriden if they are in the new properties, but not removed + * @param oldProperties + * @param newProperties + * @private + */ + private static mergeTags( + oldProperties: Record, + newProperties: Record + ): boolean { + let changeMade = false + + for (const oldPropertiesKey in oldProperties) { + // Delete properties from the old record if it is not in the new store anymore + if (oldPropertiesKey.startsWith("_")) { + continue + } + if (newProperties[oldPropertiesKey] === undefined) { + changeMade = true + delete oldProperties[oldPropertiesKey] + } + } + + // Copy all properties from the new record into the old + for (const newPropertiesKey in newProperties) { + const v = newProperties[newPropertiesKey] + if (oldProperties[newPropertiesKey] !== v) { + oldProperties[newPropertiesKey] = v + changeMade = true + } + } + + return changeMade + } + + addAlias(oldId: string, newId: string): void { + if (newId === undefined) { + // We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap! + const element = this._elements.get(oldId) + element.data._deleted = "yes" + element.ping() + return + } + + if (oldId == newId) { + return + } + const element = this._elements.get(oldId) + if (element === undefined) { + // Element to rewrite not found, probably a node or relation that is not rendered + return + } + element.data.id = newId + this._elements.set(newId, element) + element.ping() + } + + has(id: string) { + return this._elements.has(id) + } +} diff --git a/Logic/FeatureSource/Actors/MetaTagRecalculator.ts b/Logic/FeatureSource/Actors/MetaTagRecalculator.ts index 165279b92..aacf44b50 100644 --- a/Logic/FeatureSource/Actors/MetaTagRecalculator.ts +++ b/Logic/FeatureSource/Actors/MetaTagRecalculator.ts @@ -1,6 +1,5 @@ import { FeatureSourceForLayer, Tiled } from "../FeatureSource" import MetaTagging from "../../MetaTagging" -import { ElementStorage } from "../../ElementStorage" import { ExtraFuncParams } from "../../ExtraFunctions" import FeaturePipeline from "../FeaturePipeline" import { BBox } from "../../BBox" @@ -39,7 +38,6 @@ class MetatagUpdater { } return featurePipeline.GetFeaturesWithin(layerId, bbox) }, - memberships: featurePipeline.relationTracker, } this.isDirty.stabilized(100).addCallback((dirty) => { if (dirty) { diff --git a/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts b/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts deleted file mode 100644 index 756c11da3..000000000 --- a/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts +++ /dev/null @@ -1,20 +0,0 @@ -import FeatureSource from "../FeatureSource"; -import { Store } from "../../UIEventSource"; -import { ElementStorage } from "../../ElementStorage"; -import { Feature } from "geojson"; - -/** - * Makes sure that every feature is added to the ElementsStorage, so that the tags-eventsource can be retrieved - */ -export default class RegisteringAllFromFeatureSourceActor { - public readonly features: Store - - constructor(source: FeatureSource, allElements: ElementStorage) { - this.features = source.features - this.features.addCallbackAndRunD((features) => { - for (const feature of features) { - allElements.addOrGetElement( feature) - } - }) - } -} diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index 2492c0c10..fc7e668d7 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -13,16 +13,18 @@ import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFea import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor" import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource" import { TileHierarchyMerger } from "./TiledFeatureSource/TileHierarchyMerger" -import RelationsTracker from "../Osm/RelationsTracker" 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 TileFreshnessCalculator from "./TileFreshnessCalculator" import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource" import MapState from "../State/MapState" -import { ElementStorage } from "../ElementStorage" import { OsmFeature } from "../../Models/OsmFeature" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import { FilterState } from "../../Models/FilteredLayer" @@ -47,7 +49,6 @@ export default class FeaturePipeline { public readonly somethingLoaded: UIEventSource = new UIEventSource(false) public readonly newDataLoadedSignal: UIEventSource = new UIEventSource(undefined) - public readonly relationTracker: RelationsTracker /** * Keeps track of all raw OSM-nodes. * Only initialized if `ReplaceGeometryAction` is needed somewhere @@ -56,12 +57,6 @@ export default class FeaturePipeline { private readonly overpassUpdater: OverpassFeatureSource private state: MapState private readonly perLayerHierarchy: Map - /** - * Keeps track of the age of the loaded data. - * Has one freshness-Calculator for every layer - * @private - */ - private readonly freshnesses = new Map() private readonly oldestAllowedDate: Date private readonly osmSourceZoomLevel private readonly localStorageSavers = new Map() @@ -87,7 +82,6 @@ export default class FeaturePipeline { const useOsmApi = state.locationControl.map( (l) => l.zoom > (state.overpassMaxZoom.data ?? 12) ) - this.relationTracker = new RelationsTracker() state.changes.allChanges.addCallbackAndRun((allChanges) => { allChanges @@ -141,11 +135,8 @@ export default class FeaturePipeline { ) perLayerHierarchy.set(id, hierarchy) - this.freshnesses.set(id, new TileFreshnessCalculator()) - if (id === "type_node") { this.fullNodeDatabase = new FullNodeDatabaseSource(filteredLayer, (tile) => { - new RegisteringAllFromFeatureSourceActor(tile, state.allElements) perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) }) @@ -473,7 +464,6 @@ export default class FeaturePipeline { private initOverpassUpdater( state: { - allElements: ElementStorage layoutToUse: LayoutConfig currentBounds: Store locationControl: Store @@ -513,26 +503,10 @@ export default class FeaturePipeline { [state.locationControl] ) - const self = this - const updater = new OverpassFeatureSource(state, { + return new OverpassFeatureSource(state, { padToTiles: state.locationControl.map((l) => Math.min(15, l.zoom + 1)), - relationTracker: this.relationTracker, isActive: useOsmApi.map((b) => !b && overpassIsActive.data, [overpassIsActive]), - freshnesses: this.freshnesses, - onBboxLoaded: (bbox, date, downloadedLayers, paddedToZoomLevel) => { - Tiles.MapRange(bbox.containingTileRange(paddedToZoomLevel), (x, y) => { - const tileIndex = Tiles.tile_index(paddedToZoomLevel, x, y) - downloadedLayers.forEach((layer) => { - self.freshnesses.get(layer.id).addTileLoad(tileIndex, date) - self.localStorageSavers.get(layer.id)?.MarkVisited(tileIndex, date) - }) - }) - }, }) - - // Register everything in the state' 'AllElements' - new RegisteringAllFromFeatureSourceActor(updater, state.allElements) - return updater } /** diff --git a/Logic/FeatureSource/FeatureSource.ts b/Logic/FeatureSource/FeatureSource.ts index 243b8efd9..038132f48 100644 --- a/Logic/FeatureSource/FeatureSource.ts +++ b/Logic/FeatureSource/FeatureSource.ts @@ -23,5 +23,5 @@ export interface FeatureSourceForLayer extends FeatureSource { * A feature source which is aware of the indexes it contains */ export interface IndexedFeatureSource extends FeatureSource { - readonly containedIds: Store> + readonly featuresById: Store> } diff --git a/Logic/FeatureSource/LayoutSource.ts b/Logic/FeatureSource/LayoutSource.ts new file mode 100644 index 000000000..b4d62b49a --- /dev/null +++ b/Logic/FeatureSource/LayoutSource.ts @@ -0,0 +1,129 @@ +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" + +/** + * This source will fetch the needed data from various sources for the given layout. + * + * Note that special layers (with `source=null` will be ignored) + */ +export default class LayoutSource extends FeatureSourceMerger { + constructor( + filteredLayers: LayerConfig[], + featureSwitches: FeatureSwitchState, + newAndChangedElements: FeatureSource, + mapProperties: { bounds: Store; zoom: Store }, + backend: string, + isLayerActive: (id: string) => Store + ) { + const { bounds, zoom } = mapProperties + // remove all 'special' layers + filteredLayers = filteredLayers.filter((flayer) => flayer.source !== null) + + const geojsonlayers = filteredLayers.filter( + (flayer) => flayer.source.geojsonSource !== undefined + ) + const osmLayers = filteredLayers.filter( + (flayer) => flayer.source.geojsonSource === undefined + ) + const overpassSource = LayoutSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches) + const osmApiSource = LayoutSource.setupOsmApiSource( + osmLayers, + bounds, + zoom, + backend, + featureSwitches + ) + const geojsonSources: FeatureSource[] = geojsonlayers.map((l) => + LayoutSource.setupGeojsonSource(l, mapProperties) + ) + + const expiryInSeconds = Math.min(...(filteredLayers?.map((l) => l.maxAgeOfCache) ?? [])) + super(overpassSource, osmApiSource, newAndChangedElements, ...geojsonSources) + } + + private static setupGeojsonSource( + layer: LayerConfig, + mapProperties: { zoom: Store; bounds: Store }, + isActive?: Store + ): FeatureSource { + const source = layer.source + if (source.geojsonZoomLevel === undefined) { + // This is a 'load everything at once' geojson layer + return new GeoJsonSource(layer, { isActive }) + } else { + return new DynamicGeoJsonTileSource(layer, mapProperties, { isActive }) + } + } + + private static setupOsmApiSource( + osmLayers: LayerConfig[], + bounds: Store, + zoom: Store, + backend: string, + featureSwitches: FeatureSwitchState + ): FeatureSource { + const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom)) + const isActive = zoom.mapD((z) => { + if (z < minzoom) { + // We are zoomed out over the zoomlevel of any layer + console.debug("Disabling overpass source: zoom < minzoom") + return false + } + + // Overpass should handle this if zoomed out a bit + return z > featureSwitches.overpassMaxZoom.data + }) + const allowedFeatures = new Or(osmLayers.map((l) => l.source.osmTags)).optimize() + if (typeof allowedFeatures === "boolean") { + throw "Invalid filter to init OsmFeatureSource: it optimizes away to " + allowedFeatures + } + return new OsmFeatureSource({ + allowedFeatures, + bounds, + backend, + isActive, + }) + } + + private static setupOverpass( + osmLayers: LayerConfig[], + bounds: Store, + zoom: Store, + featureSwitches: FeatureSwitchState + ): FeatureSource { + const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom)) + const isActive = zoom.mapD((z) => { + if (z < minzoom) { + // We are zoomed out over the zoomlevel of any layer + console.debug("Disabling overpass source: zoom < minzoom") + return false + } + + return z <= featureSwitches.overpassMaxZoom.data + }) + + return new OverpassFeatureSource( + { + zoom, + bounds, + layoutToUse: featureSwitches.layoutToUse, + overpassUrl: featureSwitches.overpassUrl, + overpassTimeout: featureSwitches.overpassTimeout, + overpassMaxZoom: featureSwitches.overpassMaxZoom, + }, + { + padToTiles: zoom.map((zoom) => Math.min(15, zoom + 1)), + isActive, + } + ) + } +} diff --git a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts index ef92543a7..a2d17b538 100644 --- a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts +++ b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts @@ -1,4 +1,4 @@ -import FeatureSource, { FeatureSourceForLayer, Tiled } from "./FeatureSource" +import FeatureSource from "./FeatureSource" import { Store } from "../UIEventSource" import FilteredLayer from "../../Models/FilteredLayer" import SimpleFeatureSource from "./Sources/SimpleFeatureSource" @@ -12,7 +12,7 @@ import { Feature } from "geojson" export default class PerLayerFeatureSourceSplitter { constructor( layers: Store, - handleLayerData: (source: FeatureSourceForLayer & Tiled) => void, + handleLayerData: (source: FeatureSource, layer: FilteredLayer) => void, upstream: FeatureSource, options?: { tileIndex?: number @@ -71,10 +71,10 @@ export default class PerLayerFeatureSourceSplitter { let featureSource = knownLayers.get(id) if (featureSource === undefined) { // Not yet initialized - now is a good time - featureSource = new SimpleFeatureSource(layer, options?.tileIndex) + featureSource = new SimpleFeatureSource(layer) featureSource.features.setData(features) knownLayers.set(id, featureSource) - handleLayerData(featureSource) + handleLayerData(featureSource, layer) } else { featureSource.features.setData(features) } diff --git a/Logic/FeatureSource/Sources/FeatureSourceMerger.ts b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts index 8034ab697..7221b2a03 100644 --- a/Logic/FeatureSource/Sources/FeatureSourceMerger.ts +++ b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts @@ -1,58 +1,40 @@ -import { UIEventSource } from "../../UIEventSource" -import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" -import FilteredLayer from "../../../Models/FilteredLayer" -import { BBox } from "../../BBox" +import { Store, UIEventSource } from "../../UIEventSource" +import FeatureSource, { IndexedFeatureSource } from "../FeatureSource" import { Feature } from "geojson" -export default class FeatureSourceMerger - implements FeatureSourceForLayer, Tiled, IndexedFeatureSource -{ +/** + * + */ +export default class FeatureSourceMerger implements IndexedFeatureSource { public features: UIEventSource = new UIEventSource([]) - public readonly layer: FilteredLayer - public readonly tileIndex: number - public readonly bbox: BBox - public readonly containedIds: UIEventSource> = new UIEventSource>( - new Set() - ) - private readonly _sources: UIEventSource + public readonly featuresById: Store> + private readonly _featuresById: UIEventSource> + private readonly _sources: FeatureSource[] = [] /** - * Merges features from different featureSources for a single layer - * Uses the freshest feature available in the case multiple sources offer data with the same identifier + * Merges features from different featureSources. + * In case that multiple features have the same id, the latest `_version_number` will be used. Otherwise, we will take the last one */ - constructor( - layer: FilteredLayer, - tileIndex: number, - bbox: BBox, - sources: UIEventSource - ) { - this.tileIndex = tileIndex - this.bbox = bbox - this._sources = sources - this.layer = layer + constructor(...sources: FeatureSource[]) { + this._featuresById = new UIEventSource>(undefined) + this.featuresById = this._featuresById const self = this + for (let source of sources) { + source.features.addCallback(() => { + self.addData(sources.map((s) => s.features.data)) + }) + } + this.addData(sources.map((s) => s.features.data)) + this._sources = sources + } - const handledSources = new Set() - - sources.addCallbackAndRunD((sources) => { - let newSourceRegistered = false - for (let i = 0; i < sources.length; i++) { - let source = sources[i] - if (handledSources.has(source)) { - continue - } - handledSources.add(source) - newSourceRegistered = true - source.features.addCallback(() => { - self.Update() - }) - if (newSourceRegistered) { - self.Update() - } - } + protected addSource(source: FeatureSource) { + this._sources.push(source) + source.features.addCallbackAndRun(() => { + this.addData(this._sources.map((s) => s.features.data)) }) } - private Update() { + protected addData(featuress: Feature[][]) { let somethingChanged = false const all: Map = new Map() // We seed the dictionary with the previously loaded features @@ -61,11 +43,11 @@ export default class FeatureSourceMerger all.set(oldValue.properties.id, oldValue) } - for (const source of this._sources.data) { - if (source?.features?.data === undefined) { + for (const features of featuress) { + if (features === undefined) { continue } - for (const f of source.features.data) { + for (const f of features) { const id = f.properties.id if (!all.has(id)) { // This is a new feature @@ -77,7 +59,7 @@ export default class FeatureSourceMerger // This value has been seen already, either in a previous run or by a previous datasource // Let's figure out if something changed const oldV = all.get(id) - if (oldV === f) { + if (oldV == f) { continue } all.set(id, f) @@ -91,10 +73,10 @@ export default class FeatureSourceMerger } const newList = [] - all.forEach((value, _) => { + all.forEach((value, key) => { newList.push(value) }) - this.containedIds.setData(new Set(all.keys())) this.features.setData(newList) + this._featuresById.setData(all) } } diff --git a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts index 2a3655373..dcbbef71e 100644 --- a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts @@ -1,45 +1,32 @@ import { Store, UIEventSource } from "../../UIEventSource" import FilteredLayer, { FilterState } from "../../../Models/FilteredLayer" -import { FeatureSourceForLayer, Tiled } from "../FeatureSource" -import { BBox } from "../../BBox" -import { ElementStorage } from "../../ElementStorage" +import FeatureSource from "../FeatureSource" import { TagsFilter } from "../../Tags/TagsFilter" import { Feature } from "geojson" +import { OsmTags } from "../../../Models/OsmFeature" -export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled { +export default class FilteringFeatureSource implements FeatureSource { public features: UIEventSource = new UIEventSource([]) - public readonly layer: FilteredLayer - public readonly tileIndex: number - public readonly bbox: BBox - private readonly upstream: FeatureSourceForLayer - private readonly state: { - locationControl: Store<{ zoom: number }> - selectedElement: Store - globalFilters?: Store<{ filter: FilterState }[]> - allElements: ElementStorage - } - private readonly _alreadyRegistered = new Set>() + private readonly upstream: FeatureSource + private readonly _fetchStore?: (id: String) => Store + private readonly _globalFilters?: Store<{ filter: FilterState }[]> + private readonly _alreadyRegistered = new Set>() private readonly _is_dirty = new UIEventSource(false) + private readonly _layer: FilteredLayer private previousFeatureSet: Set = undefined constructor( - state: { - locationControl: Store<{ zoom: number }> - selectedElement: Store - allElements: ElementStorage - globalFilters?: Store<{ filter: FilterState }[]> - }, - tileIndex, - upstream: FeatureSourceForLayer, - metataggingUpdated?: UIEventSource + layer: FilteredLayer, + upstream: FeatureSource, + fetchStore?: (id: String) => Store, + globalFilters?: Store<{ filter: FilterState }[]>, + metataggingUpdated?: Store ) { - this.tileIndex = tileIndex - this.bbox = tileIndex === undefined ? undefined : BBox.fromTileIndex(tileIndex) this.upstream = upstream - this.state = state + this._fetchStore = fetchStore + this._layer = layer + this._globalFilters = globalFilters - this.layer = upstream.layer - const layer = upstream.layer const self = this upstream.features.addCallback(() => { self.update() @@ -59,7 +46,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti self._is_dirty.setData(true) }) - state.globalFilters?.addCallback((_) => { + globalFilters?.addCallback((_) => { self.update() }) @@ -68,10 +55,10 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti private update() { const self = this - const layer = this.upstream.layer + const layer = this._layer const features: Feature[] = this.upstream.features.data ?? [] const includedFeatureIds = new Set() - const globalFilters = self.state.globalFilters?.data?.map((f) => f.filter) + const globalFilters = self._globalFilters?.data?.map((f) => f.filter) const newFeatures = (features ?? []).filter((f) => { self.registerCallback(f) @@ -126,7 +113,10 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti } private registerCallback(feature: any) { - const src = this.state?.allElements?.addOrGetElement(feature) + if (this._fetchStore === undefined) { + return + } + const src = this._fetchStore(feature) if (src == undefined) { return } @@ -136,7 +126,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti this._alreadyRegistered.add(src) const self = this - // Add a callback as a changed tag migh change the filter + // Add a callback as a changed tag might change the filter src.addCallbackAndRunD((_) => { self._is_dirty.setData(true) }) diff --git a/Logic/FeatureSource/Sources/GeoJsonSource.ts b/Logic/FeatureSource/Sources/GeoJsonSource.ts index 99525508b..30e4fa604 100644 --- a/Logic/FeatureSource/Sources/GeoJsonSource.ts +++ b/Logic/FeatureSource/Sources/GeoJsonSource.ts @@ -1,59 +1,53 @@ /** * Fetches a geojson file somewhere and passes it along */ -import { UIEventSource } from "../../UIEventSource" -import FilteredLayer from "../../../Models/FilteredLayer" +import { Store, UIEventSource } from "../../UIEventSource" import { Utils } from "../../../Utils" -import { FeatureSourceForLayer, Tiled } from "../FeatureSource" -import { Tiles } from "../../../Models/TileRange" +import FeatureSource from "../FeatureSource" import { BBox } from "../../BBox" import { GeoOperations } from "../../GeoOperations" import { Feature } from "geojson" +import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" +import { Tiles } from "../../../Models/TileRange" -export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { - public readonly features: UIEventSource - public readonly state = new UIEventSource(undefined) - public readonly name - public readonly isOsmCache: boolean - public readonly layer: FilteredLayer - public readonly tileIndex - public readonly bbox +export default class GeoJsonSource implements FeatureSource { + public readonly features: Store private readonly seenids: Set private readonly idKey?: string public constructor( - flayer: FilteredLayer, - zxy?: [number, number, number] | BBox, + layer: LayerConfig, options?: { + zxy?: number | [number, number, number] | BBox featureIdBlacklist?: Set + isActive?: Store } ) { - if (flayer.layerDef.source.geojsonZoomLevel !== undefined && zxy === undefined) { + if (layer.source.geojsonZoomLevel !== undefined && options?.zxy === undefined) { throw "Dynamic layers are not supported. Use 'DynamicGeoJsonTileSource instead" } - this.layer = flayer - this.idKey = flayer.layerDef.source.idKey + this.idKey = layer.source.idKey this.seenids = options?.featureIdBlacklist ?? new Set() - let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id) + let url = layer.source.geojsonSource.replace("{layer}", layer.id) + let zxy = options?.zxy if (zxy !== undefined) { let tile_bbox: BBox + if (typeof zxy === "number") { + zxy = Tiles.tile_from_index(zxy) + } if (zxy instanceof BBox) { tile_bbox = zxy } else { const [z, x, y] = zxy tile_bbox = BBox.fromTile(z, x, y) - - this.tileIndex = Tiles.tile_index(z, x, y) - this.bbox = BBox.fromTile(z, x, y) url = url .replace("{z}", "" + z) .replace("{x}", "" + x) .replace("{y}", "" + y) } - let bounds: { minLat: number; maxLat: number; minLon: number; maxLon: number } = - tile_bbox - if (this.layer.layerDef.source.mercatorCrs) { + let bounds: Record<"minLat" | "maxLat" | "minLon" | "maxLon", number> = tile_bbox + if (layer.source.mercatorCrs) { bounds = tile_bbox.toMercator() } @@ -62,103 +56,83 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { .replace("{y_max}", "" + bounds.maxLat) .replace("{x_min}", "" + bounds.minLon) .replace("{x_max}", "" + bounds.maxLon) - } else { - this.tileIndex = Tiles.tile_index(0, 0, 0) - this.bbox = BBox.global } - this.name = "GeoJsonSource of " + url - - this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer - this.features = new UIEventSource([]) - this.LoadJSONFrom(url) + const eventsource = new UIEventSource(undefined) + if (options?.isActive !== undefined) { + options.isActive.addCallbackAndRunD(async (active) => { + if (!active) { + return + } + this.LoadJSONFrom(url, eventsource, layer) + .then((_) => console.log("Loaded geojson " + url)) + .catch((err) => console.error("Could not load ", url, "due to", err)) + return true + }) + } else { + this.LoadJSONFrom(url, eventsource, layer) + .then((_) => console.log("Loaded geojson " + url)) + .catch((err) => console.error("Could not load ", url, "due to", err)) + } + this.features = eventsource } - private LoadJSONFrom(url: string) { - const eventSource = this.features + private async LoadJSONFrom( + url: string, + eventSource: UIEventSource, + layer: LayerConfig + ): Promise { const self = this - Utils.downloadJsonCached(url, 60 * 60) - .then((json) => { - self.state.setData("loaded") - // TODO: move somewhere else, just for testing - // Check for maproulette data - if (url.startsWith("https://maproulette.org/api/v2/tasks/box/")) { - console.log("MapRoulette data detected") - const data = json - let maprouletteFeatures: any[] = [] - data.forEach((element) => { - maprouletteFeatures.push({ - type: "Feature", - geometry: { - type: "Point", - coordinates: [element.point.lng, element.point.lat], - }, - properties: { - // Map all properties to the feature - ...element, - }, - }) - }) - json.features = maprouletteFeatures + let json = await Utils.downloadJsonCached(url, 60 * 60) + + if (json.features === undefined || json.features === null) { + json.features = [] + } + + if (layer.source.mercatorCrs) { + json = GeoOperations.GeoJsonToWGS84(json) + } + + const time = new Date() + const newFeatures: Feature[] = [] + let i = 0 + let skipped = 0 + for (const feature of json.features) { + const props = feature.properties + for (const key in props) { + if (props[key] === null) { + delete props[key] } - if (json.features === undefined || json.features === null) { - return + if (typeof props[key] !== "string") { + // Make sure all the values are string, it crashes stuff otherwise + props[key] = JSON.stringify(props[key]) } + } - if (self.layer.layerDef.source.mercatorCrs) { - json = GeoOperations.GeoJsonToWGS84(json) - } + if (self.idKey !== undefined) { + props.id = props[self.idKey] + } - const time = new Date() - const newFeatures: Feature[] = [] - let i = 0 - let skipped = 0 - for (const feature of json.features) { - const props = feature.properties - for (const key in props) { - if (props[key] === null) { - delete props[key] - } + if (props.id === undefined) { + props.id = url + "/" + i + feature.id = url + "/" + i + i++ + } + if (self.seenids.has(props.id)) { + skipped++ + continue + } + self.seenids.add(props.id) - if (typeof props[key] !== "string") { - // Make sure all the values are string, it crashes stuff otherwise - props[key] = JSON.stringify(props[key]) - } - } + let freshness: Date = time + if (feature.properties["_last_edit:timestamp"] !== undefined) { + freshness = new Date(props["_last_edit:timestamp"]) + } - if (self.idKey !== undefined) { - props.id = props[self.idKey] - } + newFeatures.push(feature) + } - if (props.id === undefined) { - props.id = url + "/" + i - feature.id = url + "/" + i - i++ - } - if (self.seenids.has(props.id)) { - skipped++ - continue - } - self.seenids.add(props.id) - - let freshness: Date = time - if (feature.properties["_last_edit:timestamp"] !== undefined) { - freshness = new Date(props["_last_edit:timestamp"]) - } - - newFeatures.push(feature) - } - - if (newFeatures.length == 0) { - return - } - - eventSource.setData(eventSource.data.concat(newFeatures)) - }) - .catch((msg) => { - console.debug("Could not load geojson layer", url, "due to", msg) - self.state.setData({ error: msg }) - }) + eventSource.setData(newFeatures) } } diff --git a/Logic/FeatureSource/Sources/SimpleFeatureSource.ts b/Logic/FeatureSource/Sources/SimpleFeatureSource.ts index ca0854337..543a26ad5 100644 --- a/Logic/FeatureSource/Sources/SimpleFeatureSource.ts +++ b/Logic/FeatureSource/Sources/SimpleFeatureSource.ts @@ -4,16 +4,12 @@ import { FeatureSourceForLayer, Tiled } from "../FeatureSource" import { BBox } from "../../BBox" import { Feature } from "geojson" -export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled { +export default class SimpleFeatureSource implements FeatureSourceForLayer { public readonly features: UIEventSource public readonly layer: FilteredLayer - public readonly bbox: BBox = BBox.global - public readonly tileIndex: number - constructor(layer: FilteredLayer, tileIndex: number, featureSource?: UIEventSource) { + constructor(layer: FilteredLayer, featureSource?: UIEventSource) { this.layer = layer - this.tileIndex = tileIndex ?? 0 - this.bbox = BBox.fromTileIndex(this.tileIndex) this.features = featureSource ?? new UIEventSource([]) } } diff --git a/Logic/FeatureSource/TileFreshnessCalculator.ts b/Logic/FeatureSource/TileFreshnessCalculator.ts deleted file mode 100644 index 3d1adde97..000000000 --- a/Logic/FeatureSource/TileFreshnessCalculator.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Tiles } from "../../Models/TileRange" - -export default class TileFreshnessCalculator { - /** - * All the freshnesses per tile index - * @private - */ - private readonly freshnesses = new Map() - - /** - * Marks that some data got loaded for this layer - * @param tileId - * @param freshness - */ - public addTileLoad(tileId: number, freshness: Date) { - const existingFreshness = this.freshnessFor(...Tiles.tile_from_index(tileId)) - if (existingFreshness >= freshness) { - return - } - this.freshnesses.set(tileId, freshness) - - // Do we have freshness for the neighbouring tiles? If so, we can mark the tile above as loaded too! - let [z, x, y] = Tiles.tile_from_index(tileId) - if (z === 0) { - return - } - x = x - (x % 2) // Make the tiles always even - y = y - (y % 2) - - const ul = this.freshnessFor(z, x, y)?.getTime() - if (ul === undefined) { - return - } - const ur = this.freshnessFor(z, x + 1, y)?.getTime() - if (ur === undefined) { - return - } - const ll = this.freshnessFor(z, x, y + 1)?.getTime() - if (ll === undefined) { - return - } - const lr = this.freshnessFor(z, x + 1, y + 1)?.getTime() - if (lr === undefined) { - return - } - - const leastFresh = Math.min(ul, ur, ll, lr) - const date = new Date() - date.setTime(leastFresh) - this.addTileLoad(Tiles.tile_index(z - 1, Math.floor(x / 2), Math.floor(y / 2)), date) - } - - public freshnessFor(z: number, x: number, y: number): Date { - if (z < 0) { - return undefined - } - const tileId = Tiles.tile_index(z, x, y) - if (this.freshnesses.has(tileId)) { - return this.freshnesses.get(tileId) - } - // recurse up - return this.freshnessFor(z - 1, Math.floor(x / 2), Math.floor(y / 2)) - } -} diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts index dd2b69ffe..30ed0cb37 100644 --- a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts @@ -1,23 +1,24 @@ -import FilteredLayer from "../../../Models/FilteredLayer" -import { FeatureSourceForLayer, Tiled } from "../FeatureSource" -import { UIEventSource } from "../../UIEventSource" +import { Store } from "../../UIEventSource" import DynamicTileSource from "./DynamicTileSource" import { Utils } from "../../../Utils" import GeoJsonSource from "../Sources/GeoJsonSource" import { BBox } from "../../BBox" +import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" export default class DynamicGeoJsonTileSource extends DynamicTileSource { private static whitelistCache = new Map() constructor( - layer: FilteredLayer, - registerLayer: (layer: FeatureSourceForLayer & Tiled) => void, - state: { - locationControl?: UIEventSource<{ zoom?: number }> - currentBounds: UIEventSource + layer: LayerConfig, + mapProperties: { + zoom: Store + bounds: Store + }, + options?: { + isActive?: Store } ) { - const source = layer.layerDef.source + const source = layer.source if (source.geojsonZoomLevel === undefined) { throw "Invalid layer: geojsonZoomLevel expected" } @@ -30,7 +31,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { const whitelistUrl = source.geojsonSource .replace("{z}", "" + source.geojsonZoomLevel) .replace("{x}_{y}.geojson", "overview.json") - .replace("{layer}", layer.layerDef.id) + .replace("{layer}", layer.id) if (DynamicGeoJsonTileSource.whitelistCache.has(whitelistUrl)) { whitelist = DynamicGeoJsonTileSource.whitelistCache.get(whitelistUrl) @@ -56,14 +57,13 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { DynamicGeoJsonTileSource.whitelistCache.set(whitelistUrl, whitelist) }) .catch((err) => { - console.warn("No whitelist found for ", layer.layerDef.id, err) + console.warn("No whitelist found for ", layer.id, err) }) } } const blackList = new Set() super( - layer, source.geojsonZoomLevel, (zxy) => { if (whitelist !== undefined) { @@ -78,25 +78,13 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { } } - const src = new GeoJsonSource(layer, zxy, { + return new GeoJsonSource(layer, { + zxy, featureIdBlacklist: blackList, }) - - registerLayer(src) - return src }, - state + mapProperties, + { isActive: options.isActive } ) } - - public static RegisterWhitelist(url: string, json: any) { - const data = new Map>() - for (const x in json) { - if (x === "zoom") { - continue - } - data.set(Number(x), new Set(json[x])) - } - DynamicGeoJsonTileSource.whitelistCache.set(url, data) - } } diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts index 78f759818..3b3953396 100644 --- a/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts @@ -1,87 +1,65 @@ -import FilteredLayer from "../../../Models/FilteredLayer" -import { FeatureSourceForLayer, Tiled } from "../FeatureSource" -import { UIEventSource } from "../../UIEventSource" -import TileHierarchy from "./TileHierarchy" +import { Store, Stores } from "../../UIEventSource" import { Tiles } from "../../../Models/TileRange" import { BBox } from "../../BBox" +import FeatureSource from "../FeatureSource" +import FeatureSourceMerger from "../Sources/FeatureSourceMerger" /*** * A tiled source which dynamically loads the required tiles at a fixed zoom level */ -export default class DynamicTileSource implements TileHierarchy { - public readonly loadedTiles: Map - private readonly _loadedTiles = new Set() - +export default class DynamicTileSource extends FeatureSourceMerger { constructor( - layer: FilteredLayer, zoomlevel: number, - constructTile: (zxy: [number, number, number]) => FeatureSourceForLayer & Tiled, - state: { - currentBounds: UIEventSource - locationControl?: UIEventSource<{ zoom?: number }> + constructSource: (tileIndex) => FeatureSource, + mapProperties: { + bounds: Store + zoom: Store + }, + options?: { + isActive?: Store } ) { - const self = this - - this.loadedTiles = new Map() - const neededTiles = state.currentBounds - .map( - (bounds) => { - if (bounds === undefined) { - // We'll retry later - return undefined - } - - if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) { - // No need to download! - the layer is disabled - return undefined - } - - if ( - state.locationControl?.data?.zoom !== undefined && - state.locationControl.data.zoom < layer.layerDef.minzoom - ) { - // No need to download! - the layer is disabled - return undefined - } - - const tileRange = Tiles.TileRangeBetween( - zoomlevel, - bounds.getNorth(), - bounds.getEast(), - bounds.getSouth(), - bounds.getWest() - ) - if (tileRange.total > 10000) { - console.error( - "Got a really big tilerange, bounds and location might be out of sync" + super() + const loadedTiles = new Set() + const neededTiles: Store = Stores.ListStabilized( + mapProperties.bounds + .mapD( + (bounds) => { + if (options?.isActive?.data === false) { + // No need to download! - the layer is disabled + return undefined + } + const tileRange = Tiles.TileRangeBetween( + zoomlevel, + bounds.getNorth(), + bounds.getEast(), + bounds.getSouth(), + bounds.getWest() ) - return undefined - } + if (tileRange.total > 10000) { + console.error( + "Got a really big tilerange, bounds and location might be out of sync" + ) + return undefined + } - const needed = Tiles.MapRange(tileRange, (x, y) => - Tiles.tile_index(zoomlevel, x, y) - ).filter((i) => !self._loadedTiles.has(i)) - if (needed.length === 0) { - return undefined - } - return needed - }, - [layer.isDisplayed, state.locationControl] - ) - .stabilized(250) + const needed = Tiles.MapRange(tileRange, (x, y) => + Tiles.tile_index(zoomlevel, x, y) + ).filter((i) => !loadedTiles.has(i)) + if (needed.length === 0) { + return undefined + } + return needed + }, + [options?.isActive, mapProperties.zoom] + ) + .stabilized(250) + ) neededTiles.addCallbackAndRunD((neededIndexes) => { - console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes) - if (neededIndexes === undefined) { - return - } for (const neededIndex of neededIndexes) { - self._loadedTiles.add(neededIndex) - const src = constructTile(Tiles.tile_from_index(neededIndex)) - if (src !== undefined) { - self.loadedTiles.set(neededIndex, src) - } + loadedTiles.add(neededIndex) + super.addSource(constructSource(neededIndex)) } }) } diff --git a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts index d2ada6426..2df335811 100644 --- a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts @@ -1,93 +1,68 @@ import { Utils } from "../../../Utils" import OsmToGeoJson from "osmtogeojson" -import StaticFeatureSource from "../Sources/StaticFeatureSource" -import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter" -import { Store, UIEventSource } from "../../UIEventSource" -import FilteredLayer from "../../../Models/FilteredLayer" -import { FeatureSourceForLayer, Tiled } from "../FeatureSource" +import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" import { Tiles } from "../../../Models/TileRange" import { BBox } from "../../BBox" -import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" -import { Or } from "../../Tags/Or" import { TagsFilter } from "../../Tags/TagsFilter" import { OsmObject } from "../../Osm/OsmObject" -import { FeatureCollection } from "@turf/turf" +import { Feature } from "geojson" +import FeatureSourceMerger from "../Sources/FeatureSourceMerger" /** * If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile' */ -export default class OsmFeatureSource { - public readonly isRunning: UIEventSource = new UIEventSource(false) - public readonly downloadedTiles = new Set() - public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = [] +export default class OsmFeatureSource extends FeatureSourceMerger { + private readonly _bounds: Store + private readonly isActive: Store private readonly _backend: string - private readonly filteredLayers: Store - private readonly handleTile: (fs: FeatureSourceForLayer & Tiled) => void - private isActive: Store - private options: { - handleTile: (tile: FeatureSourceForLayer & Tiled) => void - isActive: Store - neededTiles: Store - markTileVisited?: (tileId: number) => void - } private readonly allowedTags: TagsFilter + public readonly isRunning: UIEventSource = new UIEventSource(false) + public rawDataHandlers: ((osmJson: any, tileIndex: number) => void)[] = [] + + private readonly _downloadedTiles: Set = new Set() + private readonly _downloadedData: Feature[][] = [] /** - * - * @param options: allowedFeatures is normally calculated from the layoutToUse + * Downloads data directly from the OSM-api within the given bounds. + * All features which match the TagsFilter 'allowedFeatures' are kept and converted into geojson */ constructor(options: { - handleTile: (tile: FeatureSourceForLayer & Tiled) => void - isActive: Store - neededTiles: Store - state: { - readonly filteredLayers: UIEventSource - readonly osmConnection: { - Backend(): string - } - readonly layoutToUse?: LayoutConfig - } - readonly allowedFeatures?: TagsFilter - markTileVisited?: (tileId: number) => void + bounds: Store + readonly allowedFeatures: TagsFilter + backend?: "https://openstreetmap.org/" | string + /** + * If given: this featureSwitch will not update if the store contains 'false' + */ + isActive?: Store }) { - this.options = options - this._backend = options.state.osmConnection.Backend() - this.filteredLayers = options.state.filteredLayers.map((layers) => - layers.filter((layer) => layer.layerDef.source.geojsonSource === undefined) - ) - this.handleTile = options.handleTile - this.isActive = options.isActive - const self = this - options.neededTiles.addCallbackAndRunD((neededTiles) => { - self.Update(neededTiles) - }) - - const neededLayers = (options.state.layoutToUse?.layers ?? []) - .filter((layer) => !layer.doNotDownload) - .filter( - (layer) => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer - ) - this.allowedTags = - options.allowedFeatures ?? new Or(neededLayers.map((l) => l.source.osmTags)) + super() + this._bounds = options.bounds + this.allowedTags = options.allowedFeatures + this.isActive = options.isActive ?? new ImmutableStore(true) + this._backend = options.backend ?? "https://www.openstreetmap.org" + this._bounds.addCallbackAndRunD((bbox) => this.loadData(bbox)) + console.log("Allowed tags are:", this.allowedTags) } - private async Update(neededTiles: number[]) { - if (this.options.isActive?.data === false) { + private async loadData(bbox: BBox) { + if (this.isActive?.data === false) { + console.log("OsmFeatureSource: not triggering: inactive") return } - neededTiles = neededTiles.filter((tile) => !this.downloadedTiles.has(tile)) + const z = 15 + const neededTiles = Tiles.tileRangeFrom(bbox, z) - if (neededTiles.length == 0) { + if (neededTiles.total == 0) { return } this.isRunning.setData(true) try { - for (const neededTile of neededTiles) { - this.downloadedTiles.add(neededTile) - await this.LoadTile(...Tiles.tile_from_index(neededTile)) - } + const tileNumbers = Tiles.MapRange(neededTiles, (x, y) => { + return Tiles.tile_index(z, x, y) + }) + await Promise.all(tileNumbers.map((i) => this.LoadTile(...Tiles.tile_from_index(i)))) } catch (e) { console.error(e) } finally { @@ -95,6 +70,11 @@ export default class OsmFeatureSource { } } + private registerFeatures(features: Feature[]): void { + this._downloadedData.push(features) + super.addData(this._downloadedData) + } + /** * The requested tile might only contain part of the relation. * @@ -135,6 +115,11 @@ export default class OsmFeatureSource { if (z < 14) { throw `Zoom ${z} is too much for OSM to handle! Use a higher zoom level!` } + const index = Tiles.tile_index(z, x, y) + if (this._downloadedTiles.has(index)) { + return + } + this._downloadedTiles.add(index) const bbox = BBox.fromTile(z, x, y) const url = `${this._backend}/api/0.6/map?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` @@ -146,43 +131,28 @@ export default class OsmFeatureSource { this.rawDataHandlers.forEach((handler) => handler(osmJson, Tiles.tile_index(z, x, y)) ) - const geojson = >OsmToGeoJson( + let features = []>OsmToGeoJson( osmJson, // @ts-ignore { flatProperties: true, } - ) + ).features // The geojson contains _all_ features at the given location // We only keep what is needed - geojson.features = geojson.features.filter((feature) => + features = features.filter((feature) => this.allowedTags.matchesProperties(feature.properties) ) - for (let i = 0; i < geojson.features.length; i++) { - geojson.features[i] = await this.patchIncompleteRelations( - geojson.features[i], - osmJson - ) + for (let i = 0; i < features.length; i++) { + features[i] = await this.patchIncompleteRelations(features[i], osmJson) } - geojson.features.forEach((f) => { + features.forEach((f) => { f.properties["_backend"] = this._backend }) - - const index = Tiles.tile_index(z, x, y) - new PerLayerFeatureSourceSplitter( - this.filteredLayers, - this.handleTile, - new StaticFeatureSource(geojson.features), - { - tileIndex: index, - } - ) - if (this.options.markTileVisited) { - this.options.markTileVisited(index) - } + this.registerFeatures(features) } catch (e) { console.error( "PANIC: got the tile from the OSM-api, but something crashed handling this tile" @@ -202,10 +172,12 @@ export default class OsmFeatureSource { if (e === "rate limited") { return } - await this.LoadTile(z + 1, x * 2, y * 2) - await this.LoadTile(z + 1, 1 + x * 2, y * 2) - await this.LoadTile(z + 1, x * 2, 1 + y * 2) - await this.LoadTile(z + 1, 1 + x * 2, 1 + y * 2) + await Promise.all([ + this.LoadTile(z + 1, x * 2, y * 2), + this.LoadTile(z + 1, 1 + x * 2, y * 2), + this.LoadTile(z + 1, x * 2, 1 + y * 2), + this.LoadTile(z + 1, 1 + x * 2, 1 + y * 2), + ]) } if (error !== undefined) { diff --git a/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts b/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts index 93d883c7e..c318fe5e1 100644 --- a/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts +++ b/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts @@ -1,7 +1,7 @@ import FeatureSource, { Tiled } from "../FeatureSource" import { BBox } from "../../BBox" -export default interface TileHierarchy { +export default interface TileHierarchy { /** * A mapping from 'tile_index' to the actual tile featrues */ diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index 68fb03a3d..ae59c95c0 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -1,8 +1,9 @@ import SimpleMetaTaggers, { SimpleMetaTagger } from "./SimpleMetaTagger" import { ExtraFuncParams, ExtraFunctions } from "./ExtraFunctions" import LayerConfig from "../Models/ThemeConfig/LayerConfig" -import { ElementStorage } from "./ElementStorage" import { Feature } from "geojson" +import FeaturePropertiesStore from "./FeatureSource/Actors/FeaturePropertiesStore" +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" /** * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... @@ -12,7 +13,7 @@ import { Feature } from "geojson" export default class MetaTagging { private static errorPrintCount = 0 private static readonly stopErrorOutputAt = 10 - private static retaggingFuncCache = new Map void)[]>() + private static retaggingFuncCache = new Map void)[]>() /** * This method (re)calculates all metatags and calculated tags on every given object. @@ -24,7 +25,8 @@ export default class MetaTagging { features: Feature[], params: ExtraFuncParams, layer: LayerConfig, - state?: { allElements?: ElementStorage }, + layout: LayoutConfig, + featurePropertiesStores?: FeaturePropertiesStore, options?: { includeDates?: true | boolean includeNonDates?: true | boolean @@ -50,13 +52,14 @@ export default class MetaTagging { } // The calculated functions - per layer - which add the new keys - const layerFuncs = this.createRetaggingFunc(layer, state) + const layerFuncs = this.createRetaggingFunc(layer) + const state = { layout } let atLeastOneFeatureChanged = false for (let i = 0; i < features.length; i++) { - const ff = features[i] - const feature = ff + const feature = features[i] + const tags = featurePropertiesStores?.getStore(feature.properties.id) let somethingChanged = false let definedTags = new Set(Object.getOwnPropertyNames(feature.properties)) for (const metatag of metatagsToApply) { @@ -72,14 +75,19 @@ export default class MetaTagging { continue } somethingChanged = true - metatag.applyMetaTagsOnFeature(feature, layer, state) + metatag.applyMetaTagsOnFeature(feature, layer, tags, state) if (options?.evaluateStrict) { for (const key of metatag.keys) { feature.properties[key] } } } else { - const newValueAdded = metatag.applyMetaTagsOnFeature(feature, layer, state) + const newValueAdded = metatag.applyMetaTagsOnFeature( + feature, + layer, + tags, + state + ) /* Note that the expression: * `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)` * Is WRONG @@ -111,7 +119,7 @@ export default class MetaTagging { } if (somethingChanged) { - state?.allElements?.getEventSourceById(feature.properties.id)?.ping() + featurePropertiesStores?.getStore(feature.properties.id)?.ping() atLeastOneFeatureChanged = true } } @@ -199,20 +207,16 @@ export default class MetaTagging { /** * Creates the function which adds all the calculated tags to a feature. Called once per layer - * @param layer - * @param state - * @private */ private static createRetaggingFunc( - layer: LayerConfig, - state + layer: LayerConfig ): (params: ExtraFuncParams, feature: any) => boolean { const calculatedTags: [string, string, boolean][] = layer.calculatedTags if (calculatedTags === undefined || calculatedTags.length === 0) { return undefined } - let functions: ((feature: any) => void)[] = MetaTagging.retaggingFuncCache.get(layer.id) + let functions: ((feature: Feature) => void)[] = MetaTagging.retaggingFuncCache.get(layer.id) if (functions === undefined) { functions = MetaTagging.createFunctionsForFeature(layer.id, calculatedTags) MetaTagging.retaggingFuncCache.set(layer.id, functions) diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index 67e44a4a8..300c115cb 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -6,19 +6,18 @@ import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescr import { Utils } from "../../Utils" import { LocalStorageSource } from "../Web/LocalStorageSource" import SimpleMetaTagger from "../SimpleMetaTagger" -import FeatureSource from "../FeatureSource/FeatureSource" -import { ElementStorage } from "../ElementStorage" +import FeatureSource, { IndexedFeatureSource } from "../FeatureSource/FeatureSource" import { GeoLocationPointProperties } from "../State/GeoLocationState" import { GeoOperations } from "../GeoOperations" import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler" import { OsmConnection } from "./OsmConnection" +import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" /** * Handles all changes made to OSM. * Needs an authenticator via OsmConnection */ export class Changes { - public readonly name = "Newly added features" /** * All the newly created features as featureSource + all the modified features */ @@ -26,7 +25,7 @@ export class Changes { public readonly pendingChanges: UIEventSource = LocalStorageSource.GetParsed("pending-changes", []) public readonly allChanges = new UIEventSource(undefined) - public readonly state: { allElements: ElementStorage; osmConnection: OsmConnection } + public readonly state: { allElements: IndexedFeatureSource; osmConnection: OsmConnection } public readonly extraComment: UIEventSource = new UIEventSource(undefined) private readonly historicalUserLocations: FeatureSource @@ -38,7 +37,9 @@ export class Changes { constructor( state?: { - allElements: ElementStorage + dryRun: UIEventSource + allElements: IndexedFeatureSource + featurePropertiesStore: FeaturePropertiesStore osmConnection: OsmConnection historicalUserLocations: FeatureSource }, @@ -50,8 +51,10 @@ export class Changes { // If a pending change contains a negative ID, we save that this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id) ?? [])) this.state = state - this._changesetHandler = state?.osmConnection?.CreateChangesetHandler( - state.allElements, + this._changesetHandler = new ChangesetHandler( + state.dryRun, + state.osmConnection, + state.featurePropertiesStore, this ) this.historicalUserLocations = state.historicalUserLocations @@ -187,7 +190,7 @@ export class Changes { const changedObjectCoordinates: [number, number][] = [] - const feature = this.state.allElements.ContainingFeatures.get(change.mainObjectId) + const feature = this.state.allElements.featuresById.data.get(change.mainObjectId) if (feature !== undefined) { changedObjectCoordinates.push(GeoOperations.centerpointCoordinates(feature)) } diff --git a/Logic/Osm/ChangesetHandler.ts b/Logic/Osm/ChangesetHandler.ts index 2661f9402..2276e0e71 100644 --- a/Logic/Osm/ChangesetHandler.ts +++ b/Logic/Osm/ChangesetHandler.ts @@ -1,7 +1,6 @@ import escapeHtml from "escape-html" import UserDetails, { OsmConnection } from "./OsmConnection" import { UIEventSource } from "../UIEventSource" -import { ElementStorage } from "../ElementStorage" import Locale from "../../UI/i18n/Locale" import Constants from "../../Models/Constants" import { Changes } from "./Changes" @@ -14,12 +13,11 @@ export interface ChangesetTag { } export class ChangesetHandler { - private readonly allElements: ElementStorage + private readonly allElements: { addAlias: (id0: String, id1: string) => void } private osmConnection: OsmConnection private readonly changes: Changes private readonly _dryRun: UIEventSource private readonly userDetails: UIEventSource - private readonly auth: any private readonly backend: string /** @@ -28,20 +26,11 @@ export class ChangesetHandler { */ private readonly _remappings = new Map() - /** - * Use 'osmConnection.CreateChangesetHandler' instead - * @param dryRun - * @param osmConnection - * @param allElements - * @param changes - * @param auth - */ constructor( dryRun: UIEventSource, osmConnection: OsmConnection, - allElements: ElementStorage, - changes: Changes, - auth + allElements: { addAlias: (id0: String, id1: string) => void }, + changes: Changes ) { this.osmConnection = osmConnection this.allElements = allElements @@ -49,7 +38,6 @@ export class ChangesetHandler { this._dryRun = dryRun this.userDetails = osmConnection.userDetails this.backend = osmConnection._oauth_config.url - this.auth = auth if (dryRun) { console.log("DRYRUN ENABLED") @@ -61,7 +49,7 @@ export class ChangesetHandler { * * ChangesetHandler.removeDuplicateMetaTags([{key: "k", value: "v"}, {key: "k0", value: "v0"}, {key: "k", value:"v"}] // => [{key: "k", value: "v"}, {key: "k0", value: "v0"}] */ - public static removeDuplicateMetaTags(extraMetaTags: ChangesetTag[]): ChangesetTag[] { + private static removeDuplicateMetaTags(extraMetaTags: ChangesetTag[]): ChangesetTag[] { const r: ChangesetTag[] = [] const seen = new Set() for (const extraMetaTag of extraMetaTags) { @@ -82,7 +70,7 @@ export class ChangesetHandler { * @param rewriteIds * @private */ - static rewriteMetaTags(extraMetaTags: ChangesetTag[], rewriteIds: Map) { + private static rewriteMetaTags(extraMetaTags: ChangesetTag[], rewriteIds: Map) { let hasChange = false for (const tag of extraMetaTags) { const match = tag.key.match(/^([a-zA-Z0-9_]+):(node\/-[0-9])$/) @@ -198,7 +186,7 @@ export class ChangesetHandler { * @param rewriteIds: the mapping of ids * @param oldChangesetMeta: the metadata-object of the already existing changeset */ - public RewriteTagsOf( + private RewriteTagsOf( extraMetaTags: ChangesetTag[], rewriteIds: Map, oldChangesetMeta: { @@ -318,28 +306,14 @@ export class ChangesetHandler { } private async CloseChangeset(changesetId: number = undefined): Promise { - const self = this - return new Promise(function (resolve, reject) { - if (changesetId === undefined) { - return - } - self.auth.xhr( - { - method: "PUT", - path: "/api/0.6/changeset/" + changesetId + "/close", - }, - function (err, response) { - if (response == null) { - console.log("err", err) - } - console.log("Closed changeset ", changesetId) - resolve() - } - ) - }) + if (changesetId === undefined) { + return + } + await this.osmConnection.put("changeset/" + changesetId + "/close") + console.log("Closed changeset ", changesetId) } - async GetChangesetMeta(csId: number): Promise<{ + private async GetChangesetMeta(csId: number): Promise<{ id: number open: boolean uid: number @@ -358,34 +332,16 @@ export class ChangesetHandler { private async UpdateTags(csId: number, tags: ChangesetTag[]) { tags = ChangesetHandler.removeDuplicateMetaTags(tags) - const self = this - return new Promise(function (resolve, reject) { - tags = Utils.NoNull(tags).filter( - (tag) => - tag.key !== undefined && - tag.value !== undefined && - tag.key !== "" && - tag.value !== "" - ) - const metadata = tags.map((kv) => ``) - - self.auth.xhr( - { - method: "PUT", - path: "/api/0.6/changeset/" + csId, - options: { header: { "Content-Type": "text/xml" } }, - content: [``, metadata, ``].join(""), - }, - function (err, response) { - if (response === undefined) { - console.error("Updating the tags of changeset " + csId + " failed:", err) - reject(err) - } else { - resolve(response) - } - } - ) - }) + tags = Utils.NoNull(tags).filter( + (tag) => + tag.key !== undefined && + tag.value !== undefined && + tag.key !== "" && + tag.value !== "" + ) + const metadata = tags.map((kv) => ``) + const content = [``, metadata, ``].join("") + return this.osmConnection.put("changeset/" + csId, content, { "Content-Type": "text/xml" }) } private defaultChangesetTags(): ChangesetTag[] { @@ -413,57 +369,35 @@ export class ChangesetHandler { * @constructor * @private */ - private OpenChangeset(changesetTags: ChangesetTag[]): Promise { - const self = this - return new Promise(function (resolve, reject) { - const metadata = changesetTags - .map((cstag) => [cstag.key, cstag.value]) - .filter((kv) => (kv[1] ?? "") !== "") - .map((kv) => ``) - .join("\n") + private async OpenChangeset(changesetTags: ChangesetTag[]): Promise { + const metadata = changesetTags + .map((cstag) => [cstag.key, cstag.value]) + .filter((kv) => (kv[1] ?? "") !== "") + .map((kv) => ``) + .join("\n") - self.auth.xhr( - { - method: "PUT", - path: "/api/0.6/changeset/create", - options: { header: { "Content-Type": "text/xml" } }, - content: [``, metadata, ``].join(""), - }, - function (err, response) { - if (response === undefined) { - console.error("Opening a changeset failed:", err) - reject(err) - } else { - resolve(Number(response)) - } - } - ) - }) + const csId = await this.osmConnection.put( + "changeset/create", + [``, metadata, ``].join(""), + { "Content-Type": "text/xml" } + ) + return Number(csId) } /** * Upload a changesetXML */ - private UploadChange(changesetId: number, changesetXML: string): Promise> { - const self = this - return new Promise(function (resolve, reject) { - self.auth.xhr( - { - method: "POST", - options: { header: { "Content-Type": "text/xml" } }, - path: "/api/0.6/changeset/" + changesetId + "/upload", - content: changesetXML, - }, - function (err, response) { - if (response == null) { - console.error("Uploading an actual change failed", err) - reject(err) - } - const changes = self.parseUploadChangesetResponse(response) - console.log("Uploaded changeset ", changesetId) - resolve(changes) - } - ) - }) + private async UploadChange( + changesetId: number, + changesetXML: string + ): Promise> { + const response = await this.osmConnection.post( + "changeset/" + changesetId + "/upload", + changesetXML, + { "Content-Type": "text/xml" } + ) + const changes = this.parseUploadChangesetResponse(response) + console.log("Uploaded changeset ", changesetId) + return changes } } diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index f4b5c323c..70cc9f89d 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -1,13 +1,8 @@ import osmAuth from "osm-auth" import { Store, Stores, UIEventSource } from "../UIEventSource" import { OsmPreferences } from "./OsmPreferences" -import { ChangesetHandler } from "./ChangesetHandler" -import { ElementStorage } from "../ElementStorage" -import Svg from "../../Svg" -import Img from "../../UI/Base/Img" import { Utils } from "../../Utils" import { OsmObject } from "./OsmObject" -import { Changes } from "./Changes" export default class UserDetails { public loggedIn = false @@ -148,16 +143,6 @@ export class OsmConnection { } } - public CreateChangesetHandler(allElements: ElementStorage, changes: Changes) { - return new ChangesetHandler( - this._dryRun, - /*casting is needed to make the tests work*/ this, - allElements, - changes, - this.auth - ) - } - public GetPreference( key: string, defaultValue: string = undefined, @@ -288,6 +273,57 @@ export class OsmConnection { ) } + /** + * Interact with the API. + * + * @param path: the path to query, without host and without '/api/0.6'. Example 'notes/1234/close' + */ + public async interact( + path: string, + method: "GET" | "POST" | "PUT" | "DELETE", + header?: Record, + content?: string + ): Promise { + return new Promise((ok, error) => { + this.auth.xhr( + { + method, + options: { + header, + }, + content, + path: `/api/0.6/${path}`, + }, + function (err, response) { + if (err !== null) { + error(err) + } else { + ok(response) + } + } + ) + }) + } + + public async post( + path: string, + content?: string, + header?: Record + ): Promise { + return await this.interact(path, "POST", header, content) + } + public async put( + path: string, + content?: string, + header?: Record + ): Promise { + return await this.interact(path, "PUT", header, content) + } + + public async get(path: string, header?: Record): Promise { + return await this.interact(path, "GET", header) + } + public closeNote(id: number | string, text?: string): Promise { let textSuffix = "" if ((text ?? "") !== "") { @@ -299,21 +335,7 @@ export class OsmConnection { ok() }) } - return new Promise((ok, error) => { - this.auth.xhr( - { - method: "POST", - path: `/api/0.6/notes/${id}/close${textSuffix}`, - }, - function (err, _) { - if (err !== null) { - error(err) - } else { - ok() - } - } - ) - }) + return this.post(`notes/${id}/close${textSuffix}`) } public reopenNote(id: number | string, text?: string): Promise { @@ -327,24 +349,10 @@ export class OsmConnection { if ((text ?? "") !== "") { textSuffix = "?text=" + encodeURIComponent(text) } - return new Promise((ok, error) => { - this.auth.xhr( - { - method: "POST", - path: `/api/0.6/notes/${id}/reopen${textSuffix}`, - }, - function (err, _) { - if (err !== null) { - error(err) - } else { - ok() - } - } - ) - }) + return this.post(`notes/${id}/reopen${textSuffix}`) } - public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { + public async openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { if (this._dryRun.data) { console.warn("Dryrun enabled - not actually opening note with text ", text) return new Promise<{ id: number }>((ok) => { @@ -356,29 +364,13 @@ export class OsmConnection { } const auth = this.auth const content = { lat, lon, text } - return new Promise((ok, error) => { - auth.xhr( - { - method: "POST", - path: `/api/0.6/notes.json`, - options: { - header: { "Content-Type": "application/json" }, - }, - content: JSON.stringify(content), - }, - function (err, response: string) { - console.log("RESPONSE IS", response) - if (err !== null) { - error(err) - } else { - const parsed = JSON.parse(response) - const id = parsed.properties.id - console.log("OPENED NOTE", id) - ok({ id }) - } - } - ) + const response = await this.post("notes.json", JSON.stringify(content), { + "Content-Type": "application/json", }) + const parsed = JSON.parse(response) + const id = parsed.properties.id + console.log("OPENED NOTE", id) + return id } public async uploadGpxTrack( @@ -434,31 +426,13 @@ export class OsmConnection { } body += "--" + boundary + "--\r\n" - return new Promise((ok, error) => { - auth.xhr( - { - method: "POST", - path: `/api/0.6/gpx/create`, - options: { - header: { - "Content-Type": "multipart/form-data; boundary=" + boundary, - "Content-Length": body.length, - }, - }, - content: body, - }, - function (err, response: string) { - console.log("RESPONSE IS", response) - if (err !== null) { - error(err) - } else { - const parsed = JSON.parse(response) - console.log("Uploaded GPX track", parsed) - ok({ id: parsed }) - } - } - ) + const response = await this.post("gpx/create", body, { + "Content-Type": "multipart/form-data; boundary=" + boundary, + "Content-Length": body.length, }) + const parsed = JSON.parse(response) + console.log("Uploaded GPX track", parsed) + return { id: parsed } } public addCommentToNote(id: number | string, text: string): Promise { diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts index 1293857fc..9a4b53edb 100644 --- a/Logic/Osm/Overpass.ts +++ b/Logic/Osm/Overpass.ts @@ -1,5 +1,4 @@ import { TagsFilter } from "../Tags/TagsFilter" -import RelationsTracker from "./RelationsTracker" import { Utils } from "../../Utils" import { ImmutableStore, Store } from "../UIEventSource" import { BBox } from "../BBox" @@ -15,14 +14,12 @@ export class Overpass { private readonly _timeout: Store private readonly _extraScripts: string[] private readonly _includeMeta: boolean - private _relationTracker: RelationsTracker constructor( filter: TagsFilter, extraScripts: string[], interpreterUrl: string, timeout?: Store, - relationTracker?: RelationsTracker, includeMeta = true ) { this._timeout = timeout ?? new ImmutableStore(90) @@ -34,7 +31,6 @@ export class Overpass { this._filter = optimized this._extraScripts = extraScripts this._includeMeta = includeMeta - this._relationTracker = relationTracker } public async queryGeoJson(bounds: BBox): Promise<[FeatureCollection, Date]> { @@ -57,7 +53,6 @@ export class Overpass { } public async ExecuteQuery(query: string): Promise<[FeatureCollection, Date]> { - const self = this const json = await Utils.downloadJson(this.buildUrl(query)) if (json.elements.length === 0 && json.remark !== undefined) { @@ -68,7 +63,6 @@ export class Overpass { console.warn("No features for", json) } - self._relationTracker?.RegisterRelations(json) const geojson = osmtogeojson(json) const osmTime = new Date(json.osm3s.timestamp_osm_base) return [geojson, osmTime] @@ -104,7 +98,6 @@ export class Overpass { /** * Constructs the actual script to execute on Overpass with geocoding * 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink' - * */ public buildScriptInArea( area: { osm_type: "way" | "relation"; osm_id: number }, @@ -142,7 +135,7 @@ export class Overpass { * Little helper method to quickly open overpass-turbo in the browser */ public static AsOverpassTurboLink(tags: TagsFilter) { - const overpass = new Overpass(tags, [], "", undefined, undefined, false) + const overpass = new Overpass(tags, [], "", undefined, false) const script = overpass.buildScript("", "({{bbox}})", true) const url = "http://overpass-turbo.eu/?Q=" return url + encodeURIComponent(script) diff --git a/Logic/Osm/RelationsTracker.ts b/Logic/Osm/RelationsTracker.ts deleted file mode 100644 index e4c65ba87..000000000 --- a/Logic/Osm/RelationsTracker.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { UIEventSource } from "../UIEventSource" - -export interface Relation { - id: number - type: "relation" - members: { - type: "way" | "node" | "relation" - ref: number - role: string - }[] - tags: any - // Alias for tags; tags == properties - properties: any -} - -export default class RelationsTracker { - public knownRelations = new UIEventSource>( - new Map(), - "Relation memberships" - ) - - constructor() {} - - /** - * Gets an overview of the relations - except for multipolygons. We don't care about those - * @param overpassJson - * @constructor - */ - private static GetRelationElements(overpassJson: any): Relation[] { - const relations = overpassJson.elements.filter( - (element) => element.type === "relation" && element.tags.type !== "multipolygon" - ) - for (const relation of relations) { - relation.properties = relation.tags - } - return relations - } - - public RegisterRelations(overpassJson: any): void { - this.UpdateMembershipTable(RelationsTracker.GetRelationElements(overpassJson)) - } - - /** - * Build a mapping of {memberId --> {role in relation, id of relation} } - * @param relations - * @constructor - */ - private UpdateMembershipTable(relations: Relation[]): void { - const memberships = this.knownRelations.data - let changed = false - for (const relation of relations) { - for (const member of relation.members) { - const role = { - role: member.role, - relation: relation, - } - const key = member.type + "/" + member.ref - if (!memberships.has(key)) { - memberships.set(key, []) - } - const knownRelations = memberships.get(key) - - const alreadyExists = knownRelations.some((knownRole) => { - return knownRole.role === role.role && knownRole.relation === role.relation - }) - if (!alreadyExists) { - knownRelations.push(role) - changed = true - } - } - } - if (changed) { - this.knownRelations.ping() - } - } -} diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index 1a0fbb8fd..4d23d1f6c 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -11,19 +11,145 @@ import Constants from "../Models/Constants" import { TagUtils } from "./Tags/TagUtils" import { Feature, LineString } from "geojson" import { OsmObject } from "./Osm/OsmObject" +import { OsmTags } from "../Models/OsmFeature" +import { UIEventSource } from "./UIEventSource" +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" -export class SimpleMetaTagger { +export abstract class SimpleMetaTagger { public readonly keys: string[] public readonly doc: string public readonly isLazy: boolean public readonly includesDates: boolean - public readonly applyMetaTagsOnFeature: (feature: any, layer: LayerConfig, state) => boolean /*** * A function that adds some extra data to a feature * @param docs: what does this extra data do? - * @param f: apply the changes. Returns true if something changed */ + protected constructor(docs: { + keys: string[] + doc: string + /** + * Set this flag if the data is volatile or date-based. + * It'll _won't_ be cached in this case + */ + includesDates?: boolean + isLazy?: boolean + cleanupRetagger?: boolean + }) { + this.keys = docs.keys + this.doc = docs.doc + this.isLazy = docs.isLazy + this.includesDates = docs.includesDates ?? false + if (!docs.cleanupRetagger) { + for (const key of docs.keys) { + if (!key.startsWith("_") && key.toLowerCase().indexOf("theme") < 0) { + throw `Incorrect key for a calculated meta value '${key}': it should start with underscore (_)` + } + } + } + } + + /** + * Applies the metatag-calculation, returns 'true' if the upstream source needs to be pinged + * @param feature + * @param layer + * @param tagsStore + * @param state + */ + public abstract applyMetaTagsOnFeature( + feature: any, + layer: LayerConfig, + tagsStore: UIEventSource>, + state: { layout: LayoutConfig } + ): boolean +} + +export class ReferencingWaysMetaTagger extends SimpleMetaTagger { + /** + * Disable this metatagger, e.g. for caching or tests + * This is a bit a work-around + */ + public static enabled = true + + constructor() { + super({ + keys: ["_referencing_ways"], + isLazy: true, + doc: "_referencing_ways contains - for a node - which ways use this this node as point in their geometry. ", + }) + } + + public applyMetaTagsOnFeature(feature, layer, tags, state) { + if (!ReferencingWaysMetaTagger.enabled) { + return false + } + //this function has some extra code to make it work in SimpleAddUI.ts to also work for newly added points + const id = feature.properties.id + if (!id.startsWith("node/")) { + return false + } + console.trace("Downloading referencing ways for", feature.properties.id) + OsmObject.DownloadReferencingWays(id).then((referencingWays) => { + const currentTagsSource = state.allElements?.getEventSourceById(id) ?? [] + const wayIds = referencingWays.map((w) => "way/" + w.id) + wayIds.sort() + const wayIdsStr = wayIds.join(";") + if (wayIdsStr !== "" && currentTagsSource.data["_referencing_ways"] !== wayIdsStr) { + currentTagsSource.data["_referencing_ways"] = wayIdsStr + currentTagsSource.ping() + } + }) + + return true + } +} + +export class CountryTagger extends SimpleMetaTagger { + private static readonly coder = new CountryCoder( + Constants.countryCoderEndpoint, + Utils.downloadJson + ) + public runningTasks: Set = new Set() + + constructor() { + super({ + keys: ["_country"], + doc: "The country code of the property (with latlon2country)", + includesDates: false, + }) + } + + applyMetaTagsOnFeature(feature, _, state) { + let centerPoint: any = GeoOperations.centerpoint(feature) + const runningTasks = this.runningTasks + const lat = centerPoint.geometry.coordinates[1] + const lon = centerPoint.geometry.coordinates[0] + runningTasks.add(feature) + 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) + } + }) + .catch((_) => { + runningTasks.delete(feature) + }) + return false + } +} + +class InlineMetaTagger extends SimpleMetaTagger { constructor( docs: { keys: string[] @@ -36,115 +162,26 @@ export class SimpleMetaTagger { isLazy?: boolean cleanupRetagger?: boolean }, - f: (feature: any, layer: LayerConfig, state) => boolean + f: ( + feature: any, + layer: LayerConfig, + tagsStore: UIEventSource, + state: { layout: LayoutConfig } + ) => boolean ) { - this.keys = docs.keys - this.doc = docs.doc - this.isLazy = docs.isLazy + super(docs) this.applyMetaTagsOnFeature = f - this.includesDates = docs.includesDates ?? false - if (!docs.cleanupRetagger) { - for (const key of docs.keys) { - if (!key.startsWith("_") && key.toLowerCase().indexOf("theme") < 0) { - throw `Incorrect key for a calculated meta value '${key}': it should start with underscore (_)` - } - } - } } + + public readonly applyMetaTagsOnFeature: ( + feature: any, + layer: LayerConfig, + tagsStore: UIEventSource, + state: { layout: LayoutConfig } + ) => boolean } - -export class ReferencingWaysMetaTagger extends SimpleMetaTagger { - /** - * Disable this metatagger, e.g. for caching or tests - * This is a bit a work-around - */ - public static enabled = true - constructor() { - super( - { - keys: ["_referencing_ways"], - isLazy: true, - doc: "_referencing_ways contains - for a node - which ways use this this node as point in their geometry. ", - }, - (feature, _, state) => { - if (!ReferencingWaysMetaTagger.enabled) { - return false - } - //this function has some extra code to make it work in SimpleAddUI.ts to also work for newly added points - const id = feature.properties.id - if (!id.startsWith("node/")) { - return false - } - console.trace("Downloading referencing ways for", feature.properties.id) - OsmObject.DownloadReferencingWays(id).then((referencingWays) => { - const currentTagsSource = state.allElements?.getEventSourceById(id) ?? [] - const wayIds = referencingWays.map((w) => "way/" + w.id) - wayIds.sort() - const wayIdsStr = wayIds.join(";") - if ( - wayIdsStr !== "" && - currentTagsSource.data["_referencing_ways"] !== wayIdsStr - ) { - currentTagsSource.data["_referencing_ways"] = wayIdsStr - currentTagsSource.ping() - } - }) - - return true - } - ) - } -} - -export class CountryTagger extends SimpleMetaTagger { - private static readonly coder = new CountryCoder( - Constants.countryCoderEndpoint, - Utils.downloadJson - ) - public runningTasks: Set - - constructor() { - const runningTasks = new Set() - super( - { - keys: ["_country"], - doc: "The country code of the property (with latlon2country)", - includesDates: false, - }, - (feature, _, state) => { - let centerPoint: any = GeoOperations.centerpoint(feature) - const lat = centerPoint.geometry.coordinates[1] - const lon = centerPoint.geometry.coordinates[0] - runningTasks.add(feature) - 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) - } - }) - .catch((_) => { - runningTasks.delete(feature) - }) - return false - } - ) - this.runningTasks = runningTasks - } -} - export default class SimpleMetaTaggers { - public static readonly objectMetaInfo = new SimpleMetaTagger( + public static readonly objectMetaInfo = new InlineMetaTagger( { keys: [ "_last_edit:contributor", @@ -180,7 +217,7 @@ export default class SimpleMetaTaggers { } ) public static country = new CountryTagger() - public static geometryType = new SimpleMetaTagger( + public static geometryType = new InlineMetaTagger( { keys: ["_geometry:type"], doc: "Adds the geometry type as property. This is identical to the GoeJson geometry type and is one of `Point`,`LineString`, `Polygon` and exceptionally `MultiPolygon` or `MultiLineString`", @@ -191,6 +228,7 @@ export default class SimpleMetaTaggers { return changed } ) + public static referencingWays = new ReferencingWaysMetaTagger() private static readonly cardinalDirections = { N: 0, NNE: 22.5, @@ -209,7 +247,7 @@ export default class SimpleMetaTaggers { NW: 315, NNW: 337.5, } - private static latlon = new SimpleMetaTagger( + private static latlon = new InlineMetaTagger( { keys: ["_lat", "_lon"], doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)", @@ -225,13 +263,13 @@ export default class SimpleMetaTaggers { return true } ) - private static layerInfo = new SimpleMetaTagger( + private static layerInfo = new InlineMetaTagger( { doc: "The layer-id to which this feature belongs. Note that this might be return any applicable if `passAllFeatures` is defined.", keys: ["_layer"], includesDates: false, }, - (feature, _, layer) => { + (feature, layer) => { if (feature.properties._layer === layer.id) { return false } @@ -239,7 +277,7 @@ export default class SimpleMetaTaggers { return true } ) - private static noBothButLeftRight = new SimpleMetaTagger( + private static noBothButLeftRight = new InlineMetaTagger( { keys: [ "sidewalk:left", @@ -251,7 +289,7 @@ export default class SimpleMetaTaggers { includesDates: false, cleanupRetagger: true, }, - (feature, state, layer) => { + (feature, layer) => { if (!layer.lineRendering.some((lr) => lr.leftRightSensitive)) { return } @@ -259,7 +297,7 @@ export default class SimpleMetaTaggers { return SimpleMetaTaggers.removeBothTagging(feature.properties) } ) - private static surfaceArea = new SimpleMetaTagger( + private static surfaceArea = new InlineMetaTagger( { keys: ["_surface", "_surface:ha"], doc: "The surface area of the feature, in square meters and in hectare. Not set on points and ways", @@ -292,7 +330,7 @@ export default class SimpleMetaTaggers { return true } ) - private static levels = new SimpleMetaTagger( + private static levels = new InlineMetaTagger( { doc: "Extract the 'level'-tag into a normalized, ';'-separated value", keys: ["_level"], @@ -311,15 +349,14 @@ export default class SimpleMetaTaggers { return true } ) - - private static canonicalize = new SimpleMetaTagger( + private static canonicalize = new InlineMetaTagger( { doc: "If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`; `1` will be rewritten to `1m` as well)", keys: ["Theme-defined keys"], }, - (feature, _, state) => { + (feature, _, __, state) => { const units = Utils.NoNull( - [].concat(...(state?.layoutToUse?.layers?.map((layer) => layer.units) ?? [])) + [].concat(...(state?.layout?.layers?.map((layer) => layer.units) ?? [])) ) if (units.length == 0) { return @@ -369,7 +406,7 @@ export default class SimpleMetaTaggers { return rewritten } ) - private static lngth = new SimpleMetaTagger( + private static lngth = new InlineMetaTagger( { keys: ["_length", "_length:km"], doc: "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter", @@ -383,14 +420,14 @@ export default class SimpleMetaTaggers { return true } ) - private static isOpen = new SimpleMetaTagger( + private static isOpen = new InlineMetaTagger( { keys: ["_isOpen"], doc: "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')", includesDates: true, isLazy: true, }, - (feature, _, state) => { + (feature) => { if (Utils.runningFromConsole) { // We are running from console, thus probably creating a cache // isOpen is irrelevant @@ -438,11 +475,9 @@ export default class SimpleMetaTaggers { } }, }) - - const tagsSource = state.allElements.getEventSourceById(feature.properties.id) } ) - private static directionSimplified = new SimpleMetaTagger( + private static directionSimplified = new InlineMetaTagger( { keys: ["_direction:numerical", "_direction:leftright"], doc: "_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map", @@ -466,8 +501,7 @@ export default class SimpleMetaTaggers { return true } ) - - private static directionCenterpoint = new SimpleMetaTagger( + private static directionCenterpoint = new InlineMetaTagger( { keys: ["_direction:centerpoint"], isLazy: true, @@ -500,8 +534,7 @@ export default class SimpleMetaTaggers { return true } ) - - private static currentTime = new SimpleMetaTagger( + private static currentTime = new InlineMetaTagger( { keys: ["_now:date", "_now:datetime"], doc: "Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely", @@ -523,9 +556,6 @@ export default class SimpleMetaTaggers { return true } ) - - public static referencingWays = new ReferencingWaysMetaTagger() - public static metatags: SimpleMetaTagger[] = [ SimpleMetaTaggers.latlon, SimpleMetaTaggers.layerInfo, @@ -543,9 +573,6 @@ export default class SimpleMetaTaggers { SimpleMetaTaggers.levels, SimpleMetaTaggers.referencingWays, ] - public static readonly lazyTags: string[] = [].concat( - ...SimpleMetaTaggers.metatags.filter((tagger) => tagger.isLazy).map((tagger) => tagger.keys) - ) /** * Edits the given object to rewrite 'both'-tagging into a 'left-right' tagging scheme. diff --git a/Logic/State/FeaturePipelineState.ts b/Logic/State/FeaturePipelineState.ts index 429952f37..9de67d4c1 100644 --- a/Logic/State/FeaturePipelineState.ts +++ b/Logic/State/FeaturePipelineState.ts @@ -1,34 +1,21 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import FeaturePipeline from "../FeatureSource/FeaturePipeline" import { Tiles } from "../../Models/TileRange" -import { TileHierarchyAggregator } from "../../UI/ShowDataLayer/TileHierarchyAggregator" -import { UIEventSource } from "../UIEventSource" -import MapState from "./MapState" import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler" import Hash from "../Web/Hash" import { BBox } from "../BBox" -import FeatureInfoBox from "../../UI/Popup/FeatureInfoBox" import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator" -import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import ShowDataLayer from "../../UI/Map/ShowDataLayer" export default class FeaturePipelineState { /** * The piece of code which fetches data from various sources and shows it on the background map */ public readonly featurePipeline: FeaturePipeline - private readonly featureAggregator: TileHierarchyAggregator private readonly metatagRecalculator: MetaTagRecalculator - private readonly popups: Map = new Map< - string, - ScrollableFullScreen - >() constructor(layoutToUse: LayoutConfig) { const clustering = layoutToUse?.clustering - this.featureAggregator = TileHierarchyAggregator.createHierarchy(this) const clusterCounter = this.featureAggregator const self = this @@ -58,7 +45,7 @@ export default class FeaturePipelineState { ) // Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering - const doShowFeatures = source.features.map( + source.features.map( (f) => { const z = self.locationControl.data.zoom @@ -112,14 +99,6 @@ export default class FeaturePipelineState { }, [self.currentBounds, source.layer.isDisplayed, sourceBBox] ) - - new ShowDataLayer(self.maplibreMap, { - features: source, - layer: source.layer.layerDef, - doShowLayer: doShowFeatures, - selectedElement: self.selectedElement, - buildPopup: (tags, layer) => self.CreatePopup(tags, layer), - }) } this.featurePipeline = new FeaturePipeline(registerSource, this, { @@ -132,13 +111,4 @@ export default class FeaturePipelineState { new SelectedFeatureHandler(Hash.hash, this) } - - public CreatePopup(tags: UIEventSource, layer: LayerConfig): ScrollableFullScreen { - if (this.popups.has(tags.data.id)) { - return this.popups.get(tags.data.id) - } - const popup = new FeatureInfoBox(tags, layer, this) - this.popups.set(tags.data.id, popup) - return popup - } } diff --git a/Models/MapProperties.ts b/Models/MapProperties.ts index 9ac228f57..39ac125c2 100644 --- a/Models/MapProperties.ts +++ b/Models/MapProperties.ts @@ -7,8 +7,6 @@ export interface MapProperties { readonly zoom: UIEventSource readonly bounds: Store readonly rasterLayer: UIEventSource - readonly maxbounds: UIEventSource - readonly allowMoving: UIEventSource } diff --git a/Models/ThemeConfig/DependencyCalculator.ts b/Models/ThemeConfig/DependencyCalculator.ts index 1e305a036..87dd96187 100644 --- a/Models/ThemeConfig/DependencyCalculator.ts +++ b/Models/ThemeConfig/DependencyCalculator.ts @@ -94,7 +94,6 @@ export default class DependencyCalculator { return [] }, - memberships: undefined, } // Init the extra patched functions... ExtraFunctions.FullPatchFeature(params, obj) diff --git a/Models/ThemeConfig/Json/LayerConfigJson.ts b/Models/ThemeConfig/Json/LayerConfigJson.ts index 67a6f5571..c6b7a0199 100644 --- a/Models/ThemeConfig/Json/LayerConfigJson.ts +++ b/Models/ThemeConfig/Json/LayerConfigJson.ts @@ -42,35 +42,19 @@ export interface LayerConfigJson { * * Note: a source must always be defined. 'special' is only allowed if this is a builtin-layer */ - source: "special" | "special:library" | ({ - /** - * Every source must set which tags have to be present in order to load the given layer. - */ - osmTags: TagConfigJson - /** - * The maximum amount of seconds that a tile is allowed to linger in the cache - */ - maxCacheAge?: number - } & ( - | { + source: + | "special" + | "special:library" + | ({ /** - * If set, this custom overpass-script will be used instead of building one by using the OSM-tags. - * Specifying OSM-tags is still obligatory and will still hide non-matching items and they will be used for the rest of the pipeline. - * _This should be really rare_. - * - * For example, when you want to fetch all grass-areas in parks and which are marked as publicly accessible: - * ``` - * "source": { - * "overpassScript": - * "way[\"leisure\"=\"park\"];node(w);is_in;area._[\"leisure\"=\"park\"];(way(area)[\"landuse\"=\"grass\"]; node(w); );", - * "osmTags": "access=yes" - * } - * ``` - * + * Every source must set which tags have to be present in order to load the given layer. */ - overpassScript?: string - } - | { + osmTags: TagConfigJson + /** + * The maximum amount of seconds that a tile is allowed to linger in the cache + */ + maxCacheAge?: number + } & { /** * The actual source of the data to load, if loaded via geojson. * @@ -104,7 +88,6 @@ export interface LayerConfigJson { */ idKey?: string }) - ) /** * diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index 599aa3fb3..db15a5df4 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -68,6 +68,8 @@ export default class LayerConfig extends WithContextLoader { public readonly forceLoad: boolean public readonly syncSelection: typeof LayerConfig.syncSelectionAllowed[number] // this is a trick to conver a constant array of strings into a type union of these values + public readonly _needsFullNodeDatabase = false + constructor(json: LayerConfigJson, context?: string, official: boolean = true) { context = context + "." + json.id const translationContext = "layers:" + json.id @@ -250,7 +252,7 @@ export default class LayerConfig extends WithContextLoader { | "osmbasedmap" | "historicphoto" | string - )[] + )[] if (typeof pr.preciseInput.preferredBackground === "string") { preferredBackground = [pr.preciseInput.preferredBackground] } else { diff --git a/Models/ThemeConfig/SourceConfig.ts b/Models/ThemeConfig/SourceConfig.ts index 32fb0333b..646eaa1cc 100644 --- a/Models/ThemeConfig/SourceConfig.ts +++ b/Models/ThemeConfig/SourceConfig.ts @@ -3,7 +3,6 @@ import { RegexTag } from "../../Logic/Tags/RegexTag" export default class SourceConfig { public osmTags?: TagsFilter - public readonly overpassScript?: string public geojsonSource?: string public geojsonZoomLevel?: number public isOsmCacheLayer: boolean @@ -68,7 +67,6 @@ export default class SourceConfig { } } this.osmTags = params.osmTags ?? new RegexTag("id", /.*/) - this.overpassScript = params.overpassScript this.geojsonSource = params.geojsonSource this.geojsonZoomLevel = params.geojsonSourceLevel this.isOsmCacheLayer = params.isOsmCache ?? false diff --git a/Models/TileRange.ts b/Models/TileRange.ts index 2454ed471..a23b45966 100644 --- a/Models/TileRange.ts +++ b/Models/TileRange.ts @@ -1,3 +1,5 @@ +import { BBox } from "../Logic/BBox" + export interface TileRange { xstart: number ystart: number @@ -85,6 +87,16 @@ export class Tiles { return { x: Tiles.lon2tile(lon, z), y: Tiles.lat2tile(lat, z), z: z } } + static tileRangeFrom(bbox: BBox, zoomlevel: number) { + return Tiles.TileRangeBetween( + zoomlevel, + bbox.getNorth(), + bbox.getWest(), + bbox.getSouth(), + bbox.getEast() + ) + } + static TileRangeBetween( zoomlevel: number, lat0: number, diff --git a/UI/AllThemesGui.ts b/UI/AllThemesGui.ts index b71df1389..c55e188c0 100644 --- a/UI/AllThemesGui.ts +++ b/UI/AllThemesGui.ts @@ -5,28 +5,32 @@ import MoreScreen from "./BigComponents/MoreScreen" import Translations from "./i18n/Translations" import Constants from "../Models/Constants" import { Utils } from "../Utils" -import LanguagePicker1 from "./LanguagePicker" +import LanguagePicker from "./LanguagePicker" import IndexText from "./BigComponents/IndexText" -import FeaturedMessage from "./BigComponents/FeaturedMessage" import { ImportViewerLinks } from "./BigComponents/UserInformation" import { LoginToggle } from "./Popup/LoginButton" +import { ImmutableStore } from "../Logic/UIEventSource" +import { OsmConnection } from "../Logic/Osm/OsmConnection" export default class AllThemesGui { setup() { try { new FixedUiElement("").AttachTo("centermessage") - const state = new UserRelatedState(undefined) + const osmConnection = new OsmConnection() + const state = new UserRelatedState(osmConnection) const intro = new Combine([ - new LanguagePicker1(Translations.t.index.title.SupportedLanguages(), "").SetClass( + new LanguagePicker(Translations.t.index.title.SupportedLanguages(), "").SetClass( "flex absolute top-2 right-3" ), new IndexText(), ]) new Combine([ intro, - new FeaturedMessage().SetClass("mb-4 block"), new MoreScreen(state, true), - new LoginToggle(undefined, Translations.t.index.logIn, state), + new LoginToggle(undefined, Translations.t.index.logIn, { + osmConnection, + featureSwitchUserbadge: new ImmutableStore(true), + }), new ImportViewerLinks(state.osmConnection), Translations.t.general.aboutMapcomplete .Subs({ osmcha_link: Utils.OsmChaLinkFor(7) }) diff --git a/UI/BigComponents/FeaturedMessage.ts b/UI/BigComponents/FeaturedMessage.ts deleted file mode 100644 index e1be38f7c..000000000 --- a/UI/BigComponents/FeaturedMessage.ts +++ /dev/null @@ -1,103 +0,0 @@ -import Combine from "../Base/Combine" -import welcome_messages from "../../assets/welcome_message.json" -import BaseUIElement from "../BaseUIElement" -import { FixedUiElement } from "../Base/FixedUiElement" -import MoreScreen from "./MoreScreen" -import themeOverview from "../../assets/generated/theme_overview.json" -import Translations from "../i18n/Translations" -import Title from "../Base/Title" - -export default class FeaturedMessage extends Combine { - constructor() { - const now = new Date() - let welcome_message = undefined - for (const wm of FeaturedMessage.WelcomeMessages()) { - if (wm.start_date >= now) { - continue - } - if (wm.end_date <= now) { - continue - } - - if (welcome_message !== undefined) { - console.warn("Multiple applicable messages today:", welcome_message.featured_theme) - } - welcome_message = wm - } - welcome_message = welcome_message ?? undefined - - super([FeaturedMessage.CreateFeaturedBox(welcome_message)]) - } - - public static WelcomeMessages(): { - start_date: Date - end_date: Date - message: string - featured_theme?: string - }[] { - const all_messages: { - start_date: Date - end_date: Date - message: string - featured_theme?: string - }[] = [] - - const themesById = new Map() - for (const theme of themeOverview) { - themesById.set(theme.id, theme) - } - - for (const i in welcome_messages) { - if (isNaN(Number(i))) { - continue - } - const wm = welcome_messages[i] - if (wm === null) { - continue - } - if (themesById.get(wm.featured_theme) === undefined) { - console.log("THEMES BY ID:", themesById) - console.error("Unkown featured theme for ", wm) - continue - } - - if (!wm.message) { - console.error("Featured message is missing for", wm) - continue - } - - all_messages.push({ - start_date: new Date(wm.start_date), - end_date: new Date(wm.end_date), - message: wm.message, - featured_theme: wm.featured_theme, - }) - } - return all_messages - } - - public static CreateFeaturedBox(welcome_message: { - message: string - featured_theme?: string - }): BaseUIElement { - const els: BaseUIElement[] = [] - if (welcome_message === undefined) { - return undefined - } - const title = new Title(Translations.t.index.featuredThemeTitle.Clone()) - const msg = new FixedUiElement(welcome_message.message).SetClass("link-underline font-lg") - els.push(new Combine([title, msg]).SetClass("m-4")) - if (welcome_message.featured_theme !== undefined) { - const theme = themeOverview.filter((th) => th.id === welcome_message.featured_theme)[0] - - els.push( - MoreScreen.createLinkButton({}, theme) - .SetClass("m-4 self-center md:w-160") - .SetStyle("height: min-content;") - ) - } - return new Combine(els).SetClass( - "border-2 border-grey-400 rounded-xl flex flex-col md:flex-row" - ) - } -} diff --git a/UI/ImportFlow/CompareToAlreadyExistingNotes.ts b/UI/ImportFlow/CompareToAlreadyExistingNotes.ts index 56cc4932d..fc69ef53d 100644 --- a/UI/ImportFlow/CompareToAlreadyExistingNotes.ts +++ b/UI/ImportFlow/CompareToAlreadyExistingNotes.ts @@ -7,7 +7,6 @@ import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNot import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" import GeoJsonSource from "../../Logic/FeatureSource/Sources/GeoJsonSource" import MetaTagging from "../../Logic/MetaTagging" -import RelationsTracker from "../../Logic/Osm/RelationsTracker" import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource" import Minimap from "../Base/Minimap" import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" @@ -58,7 +57,6 @@ export class CompareToAlreadyExistingNotes MetaTagging.addMetatags( f, { - memberships: new RelationsTracker(), getFeaturesWithin: () => [], getFeatureById: () => undefined, }, diff --git a/UI/Map/MapLibreAdaptor.ts b/UI/Map/MapLibreAdaptor.ts index 2b2f4053f..a4e6c1597 100644 --- a/UI/Map/MapLibreAdaptor.ts +++ b/UI/Map/MapLibreAdaptor.ts @@ -7,7 +7,6 @@ import { BBox } from "../../Logic/BBox" import { MapProperties } from "../../Models/MapProperties" import SvelteUIElement from "../Base/SvelteUIElement" import MaplibreMap from "./MaplibreMap.svelte" -import Constants from "../../Models/Constants" /** * The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties` @@ -51,7 +50,7 @@ export class MapLibreAdaptor implements MapProperties { }) this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined) this.allowMoving = state?.allowMoving ?? new UIEventSource(true) - this._bounds = new UIEventSource(BBox.global) + this._bounds = new UIEventSource(undefined) this.bounds = this._bounds this.rasterLayer = state?.rasterLayer ?? new UIEventSource(undefined) @@ -75,6 +74,12 @@ export class MapLibreAdaptor implements MapProperties { dt.lat = map.getCenter().lat this.location.ping() this.zoom.setData(Math.round(map.getZoom() * 10) / 10) + const bounds = map.getBounds() + const bbox = new BBox([ + [bounds.getEast(), bounds.getNorth()], + [bounds.getWest(), bounds.getSouth()], + ]) + self._bounds.setData(bbox) }) }) diff --git a/UI/Map/ShowDataLayer.ts b/UI/Map/ShowDataLayer.ts index 2f2d820a7..5694ae2d6 100644 --- a/UI/Map/ShowDataLayer.ts +++ b/UI/Map/ShowDataLayer.ts @@ -1,6 +1,6 @@ import { ImmutableStore, Store } from "../../Logic/UIEventSource" import type { Map as MlMap } from "maplibre-gl" -import { Marker } from "maplibre-gl" +import { GeoJSONSource, Marker } from "maplibre-gl" import { ShowDataLayerOptions } from "./ShowDataLayerOptions" import { GeoOperations } from "../../Logic/GeoOperations" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" @@ -19,7 +19,7 @@ class PointRenderingLayer { private readonly _config: PointRenderingConfig private readonly _fetchStore?: (id: string) => Store private readonly _map: MlMap - private readonly _onClick: (id: string) => void + private readonly _onClick: (feature: Feature) => void private readonly _allMarkers: Map = new Map() constructor( @@ -28,7 +28,7 @@ class PointRenderingLayer { config: PointRenderingConfig, visibility?: Store, fetchStore?: (id: string) => Store, - onClick?: (id: string) => void + onClick?: (feature: Feature) => void ) { this._config = config this._map = map @@ -109,7 +109,7 @@ class PointRenderingLayer { if (this._onClick) { const self = this el.addEventListener("click", function () { - self._onClick(feature.properties.id) + self._onClick(feature) }) } @@ -144,7 +144,7 @@ class LineRenderingLayer { private readonly _config: LineRenderingConfig private readonly _visibility?: Store private readonly _fetchStore?: (id: string) => Store - private readonly _onClick?: (id: string) => void + private readonly _onClick?: (feature: Feature) => void private readonly _layername: string private readonly _listenerInstalledOn: Set = new Set() @@ -155,7 +155,7 @@ class LineRenderingLayer { config: LineRenderingConfig, visibility?: Store, fetchStore?: (id: string) => Store, - onClick?: (id: string) => void + onClick?: (feature: Feature) => void ) { this._layername = layername this._map = map @@ -174,20 +174,17 @@ class LineRenderingLayer { const config = this._config for (const key of LineRenderingLayer.lineConfigKeys) { - const v = config[key]?.GetRenderValue(properties)?.Subs(properties).txt - calculatedProps[key] = v + calculatedProps[key] = config[key]?.GetRenderValue(properties)?.Subs(properties).txt } for (const key of LineRenderingLayer.lineConfigKeysColor) { let v = config[key]?.GetRenderValue(properties)?.Subs(properties).txt if (v === undefined) { continue } - console.log("Color", v) if (v.length == 9 && v.startsWith("#")) { // This includes opacity calculatedProps[key + "-opacity"] = parseInt(v.substring(7), 16) / 256 v = v.substring(0, 7) - console.log("Color >", v, calculatedProps[key + "-opacity"]) } calculatedProps[key] = v } @@ -196,7 +193,6 @@ class LineRenderingLayer { calculatedProps[key] = Number(v) } - console.log("Calculated props:", calculatedProps, "for", properties.id) return calculatedProps } @@ -205,52 +201,53 @@ class LineRenderingLayer { while (!map.isStyleLoaded()) { await Utils.waitFor(100) } - map.addSource(this._layername, { - type: "geojson", - data: { + const src = map.getSource(this._layername) + if (src === undefined) { + map.addSource(this._layername, { + type: "geojson", + data: { + type: "FeatureCollection", + features, + }, + promoteId: "id", + }) + // @ts-ignore + map.addLayer({ + source: this._layername, + id: this._layername + "_line", + type: "line", + paint: { + "line-color": ["feature-state", "color"], + "line-opacity": ["feature-state", "color-opacity"], + "line-width": ["feature-state", "width"], + "line-offset": ["feature-state", "offset"], + }, + layout: { + "line-cap": "round", + }, + }) + + map.addLayer({ + source: this._layername, + id: this._layername + "_polygon", + type: "fill", + filter: ["in", ["geometry-type"], ["literal", ["Polygon", "MultiPolygon"]]], + layout: {}, + paint: { + "fill-color": ["feature-state", "fillColor"], + "fill-opacity": 0.1, + }, + }) + } else { + src.setData({ type: "FeatureCollection", features, - }, - promoteId: "id", - }) - - map.addLayer({ - source: this._layername, - id: this._layername + "_line", - type: "line", - paint: { - "line-color": ["feature-state", "color"], - "line-opacity": ["feature-state", "color-opacity"], - "line-width": ["feature-state", "width"], - "line-offset": ["feature-state", "offset"], - }, - }) - - /*[ - "color", - "width", - "dashArray", - "lineCap", - "offset", - "fill", - "fillColor", - ]*/ - map.addLayer({ - source: this._layername, - id: this._layername + "_polygon", - type: "fill", - filter: ["in", ["geometry-type"], ["literal", ["Polygon", "MultiPolygon"]]], - layout: {}, - paint: { - "fill-color": ["feature-state", "fillColor"], - "fill-opacity": 0.1, - }, - }) + }) + } for (let i = 0; i < features.length; i++) { const feature = features[i] const id = feature.properties.id ?? feature.id - console.log("ID is", id) if (id === undefined) { console.trace( "Got a feature without ID; this causes rendering bugs:", @@ -310,23 +307,6 @@ export default class ShowDataLayer { }) } - private openOrReusePopup(id: string): void { - if (!this._popupCache || !this._options.fetchStore) { - return - } - if (this._popupCache.has(id)) { - this._popupCache.get(id).Activate() - return - } - const tags = this._options.fetchStore(id) - if (!tags) { - return - } - const popup = this._options.buildPopup(tags, this._options.layer) - this._popupCache.set(id, popup) - popup.Activate() - } - private zoomToCurrentFeatures(map: MlMap) { if (this._options.zoomToFeatures) { const features = this._options.features.features.data @@ -338,8 +318,8 @@ export default class ShowDataLayer { } private initDrawFeatures(map: MlMap) { - const { features, doShowLayer, fetchStore, buildPopup } = this._options - const onClick = buildPopup === undefined ? undefined : (id) => this.openOrReusePopup(id) + const { features, doShowLayer, fetchStore, selectedElement } = this._options + const onClick = (feature: Feature) => selectedElement?.setData(feature) 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 dde88b663..524e9847c 100644 --- a/UI/Map/ShowDataLayerOptions.ts +++ b/UI/Map/ShowDataLayerOptions.ts @@ -1,8 +1,5 @@ import FeatureSource from "../../Logic/FeatureSource/FeatureSource" import { Store, UIEventSource } from "../../Logic/UIEventSource" -import { ElementStorage } from "../../Logic/ElementStorage" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import ScrollableFullScreen from "../Base/ScrollableFullScreen" import { OsmTags } from "../../Models/OsmFeature" export interface ShowDataLayerOptions { @@ -11,15 +8,10 @@ export interface ShowDataLayerOptions { */ features: FeatureSource /** - * Indication of the current selected element; overrides some filters + * Indication of the current selected element; overrides some filters. + * When a feature is tapped, the feature will be put in there */ selectedElement?: UIEventSource - /** - * What popup to build when a feature is selected - */ - buildPopup?: - | undefined - | ((tags: UIEventSource, layer: LayerConfig) => ScrollableFullScreen) /** * If set, zoom to the features when initially loaded and when they are changed @@ -31,7 +23,8 @@ export interface ShowDataLayerOptions { doShowLayer?: Store /** - * Function which fetches the relevant store + * Function which fetches the relevant store. + * If given, the map will update when a property is changed */ fetchStore?: (id: string) => UIEventSource } diff --git a/UI/Map/ShowDataMultiLayer.ts b/UI/Map/ShowDataMultiLayer.ts index 84710b6fc..677e71d76 100644 --- a/UI/Map/ShowDataMultiLayer.ts +++ b/UI/Map/ShowDataMultiLayer.ts @@ -1,24 +1,35 @@ /** * SHows geojson on the given leaflet map, but attempts to figure out the correct layer first */ -import { Store } from "../../Logic/UIEventSource" +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: Store } + options: ShowDataLayerOptions & { + layers: FilteredLayer[] + globalFilters?: Store + } ) { new PerLayerFeatureSourceSplitter( - options.layers, - (perLayer) => { + new ImmutableStore(options.layers), + (features, layer) => { const newOptions = { ...options, - layer: perLayer.layer.layerDef, - features: perLayer, + layer: layer.layerDef, + features: new FilteringFeatureSource( + layer, + features, + options.fetchStore, + options.globalFilters + ), } new ShowDataLayer(map, newOptions) }, diff --git a/UI/ShowDataLayer/ShowOverlayLayer.ts b/UI/ShowDataLayer/ShowOverlayLayer.ts deleted file mode 100644 index 697d5e0d1..000000000 --- a/UI/ShowDataLayer/ShowOverlayLayer.ts +++ /dev/null @@ -1,21 +0,0 @@ -import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" -import { UIEventSource } from "../../Logic/UIEventSource" - -export default class ShowOverlayLayer { - public static implementation: ( - config: TilesourceConfig, - leafletMap: UIEventSource, - isShown?: UIEventSource - ) => void - - constructor( - config: TilesourceConfig, - leafletMap: UIEventSource, - isShown: UIEventSource = undefined - ) { - if (ShowOverlayLayer.implementation === undefined) { - throw "Call ShowOverlayLayerImplemenation.initialize() first before using this" - } - ShowOverlayLayer.implementation(config, leafletMap, isShown) - } -} diff --git a/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts b/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts index 82e461bbe..64a6d3ab1 100644 --- a/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts +++ b/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts @@ -3,6 +3,7 @@ import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" import { UIEventSource } from "../../Logic/UIEventSource" import ShowOverlayLayer from "./ShowOverlayLayer" +// TODO port this to maplibre! export default class ShowOverlayLayerImplementation { public static Implement() { ShowOverlayLayer.implementation = ShowOverlayLayerImplementation.AddToMap diff --git a/UI/ShowDataLayer/TileHierarchyAggregator.ts b/UI/ShowDataLayer/TileHierarchyAggregator.ts deleted file mode 100644 index 71ba7addc..000000000 --- a/UI/ShowDataLayer/TileHierarchyAggregator.ts +++ /dev/null @@ -1,257 +0,0 @@ -import FeatureSource, { - FeatureSourceForLayer, - Tiled, -} from "../../Logic/FeatureSource/FeatureSource" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import { UIEventSource } from "../../Logic/UIEventSource" -import { Tiles } from "../../Models/TileRange" -import { BBox } from "../../Logic/BBox" -import FilteredLayer from "../../Models/FilteredLayer" -import { Feature } from "geojson" - -/** - * A feature source containing but a single feature, which keeps stats about a tile - */ -export class TileHierarchyAggregator implements FeatureSource { - private static readonly empty = [] - public totalValue: number = 0 - public showCount: number = 0 - public hiddenCount: number = 0 - public readonly features = new UIEventSource(TileHierarchyAggregator.empty) - public readonly name - private _parent: TileHierarchyAggregator - private _root: TileHierarchyAggregator - private readonly _z: number - private readonly _x: number - private readonly _y: number - private readonly _tileIndex: number - private _counter: SingleTileCounter - private _subtiles: [ - TileHierarchyAggregator, - TileHierarchyAggregator, - TileHierarchyAggregator, - TileHierarchyAggregator - ] = [undefined, undefined, undefined, undefined] - private readonly featuresStatic = [] - private readonly featureProperties: { - count: string - kilocount: string - tileId: string - id: string - showCount: string - totalCount: string - } - private readonly _state: { filteredLayers: UIEventSource } - private readonly updateSignal = new UIEventSource(undefined) - - private constructor( - parent: TileHierarchyAggregator, - state: { - filteredLayers: UIEventSource - }, - z: number, - x: number, - y: number - ) { - this._parent = parent - this._state = state - this._root = parent?._root ?? this - this._z = z - this._x = x - this._y = y - this._tileIndex = Tiles.tile_index(z, x, y) - this.name = "Count(" + this._tileIndex + ")" - - const totals = { - id: "" + this._tileIndex, - tileId: "" + this._tileIndex, - count: `0`, - kilocount: "0", - showCount: "0", - totalCount: "0", - } - this.featureProperties = totals - - const now = new Date() - const feature = { - type: "Feature", - properties: totals, - geometry: { - type: "Point", - coordinates: Tiles.centerPointOf(z, x, y), - }, - } - this.featuresStatic.push({ feature: feature, freshness: now }) - - const bbox = BBox.fromTile(z, x, y) - const box = { - type: "Feature", - properties: totals, - geometry: { - type: "Polygon", - coordinates: [ - [ - [bbox.minLon, bbox.minLat], - [bbox.minLon, bbox.maxLat], - [bbox.maxLon, bbox.maxLat], - [bbox.maxLon, bbox.minLat], - [bbox.minLon, bbox.minLat], - ], - ], - }, - } - this.featuresStatic.push({ feature: box, freshness: now }) - } - - public static createHierarchy(state: { filteredLayers: UIEventSource }) { - return new TileHierarchyAggregator(undefined, state, 0, 0, 0) - } - - public getTile(tileIndex): TileHierarchyAggregator { - if (tileIndex === this._tileIndex) { - return this - } - let [tileZ, tileX, tileY] = Tiles.tile_from_index(tileIndex) - while (tileZ - 1 > this._z) { - tileX = Math.floor(tileX / 2) - tileY = Math.floor(tileY / 2) - tileZ-- - } - const xDiff = tileX - 2 * this._x - const yDiff = tileY - 2 * this._y - const subtileIndex = yDiff * 2 + xDiff - return this._subtiles[subtileIndex]?.getTile(tileIndex) - } - - public addTile(source: FeatureSourceForLayer & Tiled) { - const self = this - if (source.tileIndex === this._tileIndex) { - if (this._counter === undefined) { - this._counter = new SingleTileCounter(this._tileIndex) - this._counter.countsPerLayer.addCallbackAndRun((_) => self.update()) - } - this._counter.addTileCount(source) - } else { - // We have to give it to one of the subtiles - let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex) - while (tileZ - 1 > this._z) { - tileX = Math.floor(tileX / 2) - tileY = Math.floor(tileY / 2) - tileZ-- - } - const xDiff = tileX - 2 * this._x - const yDiff = tileY - 2 * this._y - - const subtileIndex = yDiff * 2 + xDiff - if (this._subtiles[subtileIndex] === undefined) { - this._subtiles[subtileIndex] = new TileHierarchyAggregator( - this, - this._state, - tileZ, - tileX, - tileY - ) - } - this._subtiles[subtileIndex].addTile(source) - } - this.updateSignal.setData(source) - } - private update() { - const newMap = new Map() - let total = 0 - let hiddenCount = 0 - let showCount = 0 - let isShown: Map = new Map() - for (const filteredLayer of this._state.filteredLayers.data) { - isShown.set(filteredLayer.layerDef.id, filteredLayer) - } - this?._counter?.countsPerLayer?.data?.forEach((count, layerId) => { - newMap.set("layer:" + layerId, count) - total += count - this.featureProperties["direct_layer:" + layerId] = count - const flayer = isShown.get(layerId) - if (flayer.isDisplayed.data && this._z >= flayer.layerDef.minzoom) { - showCount += count - } else { - hiddenCount += count - } - }) - - for (const tile of this._subtiles) { - if (tile === undefined) { - continue - } - total += tile.totalValue - - showCount += tile.showCount - hiddenCount += tile.hiddenCount - - for (const key in tile.featureProperties) { - if (key.startsWith("layer:")) { - newMap.set( - key, - (newMap.get(key) ?? 0) + Number(tile.featureProperties[key] ?? 0) - ) - } - } - } - - this.totalValue = total - this.showCount = showCount - this.hiddenCount = hiddenCount - this._parent?.update() - - if (total === 0) { - this.features.setData(TileHierarchyAggregator.empty) - } else { - this.featureProperties.count = "" + total - this.featureProperties.kilocount = "" + Math.floor(total / 1000) - this.featureProperties.showCount = "" + showCount - this.featureProperties.totalCount = "" + total - newMap.forEach((value, key) => { - this.featureProperties[key] = "" + value - }) - - this.features.data = this.featuresStatic - this.features.ping() - } - } -} - -/** - * Keeps track of a single tile - */ -class SingleTileCounter implements Tiled { - public readonly bbox: BBox - public readonly tileIndex: number - public readonly countsPerLayer: UIEventSource> = new UIEventSource< - Map - >(new Map()) - public readonly z: number - public readonly x: number - public readonly y: number - private readonly registeredLayers: Map = new Map() - - constructor(tileIndex: number) { - this.tileIndex = tileIndex - this.bbox = BBox.fromTileIndex(tileIndex) - const [z, x, y] = Tiles.tile_from_index(tileIndex) - this.z = z - this.x = x - this.y = y - } - - public addTileCount(source: FeatureSourceForLayer) { - const layer = source.layer.layerDef - this.registeredLayers.set(layer.id, layer) - const self = this - source.features.map( - (f) => { - const isDisplayed = source.layer.isDisplayed.data - self.countsPerLayer.data.set(layer.id, isDisplayed ? f.length : 0) - self.countsPerLayer.ping() - }, - [source.layer.isDisplayed] - ) - } -} diff --git a/UI/ThemeViewGUI.svelte b/UI/ThemeViewGUI.svelte index d2dcd750c..a68e718b9 100644 --- a/UI/ThemeViewGUI.svelte +++ b/UI/ThemeViewGUI.svelte @@ -11,7 +11,6 @@ import { QueryParameters } from "../Logic/Web/QueryParameters"; import UserRelatedState from "../Logic/State/UserRelatedState"; import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"; - import { ElementStorage } from "../Logic/ElementStorage"; import { Changes } from "../Logic/Osm/Changes"; import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor"; import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader"; @@ -28,6 +27,12 @@ import LayerState from "../Logic/State/LayerState"; import Constants from "../Models/Constants"; import type { Feature } from "geojson"; + import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"; + import ShowDataMultiLayer from "./Map/ShowDataMultiLayer"; + import { Or } from "../Logic/Tags/Or"; + import LayoutSource from "../Logic/FeatureSource/LayoutSource"; + import { type OsmTags } from "../Models/OsmFeature"; + export let layout: LayoutConfig; const maplibremap: UIEventSource = new UIEventSource(undefined); @@ -49,16 +54,34 @@ }); const userRelatedState = new UserRelatedState(osmConnection, layout?.language); const selectedElement = new UIEventSource(undefined, "Selected element"); + selectedElement.addCallbackAndRunD(s => console.log("Selected element:", s)) const geolocation = new GeoLocationHandler(geolocationState, selectedElement, mapproperties, userRelatedState.gpsLocationHistoryRetentionTime); - const allElements = new ElementStorage(); + const tags = new Or(layout.layers.filter(l => l.source !== null&& Constants.priviliged_layers.indexOf(l.id) < 0 && l.source.geojsonSource === undefined).map(l => l.source.osmTags )) + const layerState = new LayerState(osmConnection, layout.layers, layout.id) + + const indexedElements = new LayoutSource(layout.layers, featureSwitches, new StaticFeatureSource([]), mapproperties, osmConnection.Backend(), + (id) => layerState.filteredLayers.get(id).isDisplayed + ) + + const allElements = new FeaturePropertiesStore(indexedElements) const changes = new Changes({ - allElements, + dryRun: featureSwitches.featureSwitchIsTesting, + allElements: indexedElements, + featurePropertiesStore: allElements, osmConnection, historicalUserLocations: geolocation.historicalUserLocations }, layout?.isLeftRightSensitive() ?? false); - console.log("Setting up layerstate...") - const layerState = new LayerState(osmConnection, layout.layers, layout.id) + + new ShowDataMultiLayer(maplibremap, { + layers: Array.from(layerState.filteredLayers.values()), + features: indexedElements, + fetchStore: id => > allElements.getStore(id), + selectedElement, + globalFilters: layerState.globalFilters + }) + + { // Various actors that we don't need to reference // TODO enable new TitleHandler(selectedElement,layout,allElements) @@ -98,7 +121,7 @@ current_view: new StaticFeatureSource(mapproperties.bounds.map(bbox => bbox === undefined ? empty : [bbox.asGeoJson({id:"current_view"})])), } layerState.filteredLayers.get("range")?.isDisplayed?.syncWith(featureSwitches.featureSwitchIsTesting, true) -console.log("RAnge fs", specialLayers.range) + specialLayers.range.features.addCallbackAndRun(fs => console.log("Range.features:", JSON.stringify(fs))) layerState.filteredLayers.forEach((flayer) => { const features = specialLayers[flayer.layerDef.id] @@ -116,7 +139,8 @@ console.log("RAnge fs", specialLayers.range) -
+
+
Hello world
diff --git a/assets/layers/grass_in_parks/grass_in_parks.json b/assets/layers/grass_in_parks/grass_in_parks.json deleted file mode 100644 index e583384a6..000000000 --- a/assets/layers/grass_in_parks/grass_in_parks.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "id": "grass_in_parks", - "name": { - "nl": "Toegankelijke grasvelden in parken" - }, - "source": { - "osmTags": { - "or": [ - "name=Park Oude God", - { - "and": [ - "landuse=grass", - { - "or": [ - "access=public", - "access=yes" - ] - } - ] - } - ] - }, - "overpassScript": "way[\"leisure\"=\"park\"];node(w);is_in;area._[\"leisure\"=\"park\"];(way(area)[\"landuse\"=\"grass\"]; node(w); );" - }, - "minzoom": 0, - "title": { - "render": { - "nl": "Speelweide in een park" - }, - "mappings": [ - { - "if": "name~*", - "then": { - "nl": "{name}" - } - } - ] - }, - "tagRenderings": [ - "images", - { - "id": "explanation", - "render": "Op dit grasveld in het park mag je spelen, picnicken, zitten, ..." - }, - { - "id": "grass-in-parks-reviews", - "render": "{reviews(name, landuse=grass )}" - } - ], - "mapRendering": [ - { - "icon": "./assets/themes/playgrounds/playground.svg", - "iconSize": "40,40,center", - "location": [ - "point", - "centroid" - ] - }, - { - "color": "#0f0", - "width": "1" - } - ], - "description": { - "en": "Searches for all accessible grass patches within public parks - these are 'groenzones'", - "nl": "Dit zoekt naar alle toegankelijke grasvelden binnen publieke parken - dit zijn 'groenzones'", - "de": "Sucht nach allen zugänglichen Grasflächen in öffentlichen Parks - dies sind 'Grünzonen'", - "ca": "Cerques per a tots els camins d'herba accessibles dins dels parcs públics - aquests són «groenzones»" - } -} \ No newline at end of file diff --git a/assets/themes/speelplekken/speelplekken.json b/assets/themes/speelplekken/speelplekken.json index c4049473f..8f7371945 100644 --- a/assets/themes/speelplekken/speelplekken.json +++ b/assets/themes/speelplekken/speelplekken.json @@ -93,22 +93,6 @@ ] } }, - { - "builtin": "grass_in_parks", - "override": { - "minzoom": 14, - "source": { - "geoJsonLocal": "http://127.0.0.1:8080/speelplekken_{layer}_{z}_{x}_{y}.geojson", - "geoJson": "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson", - "geoJsonZoomLevel": 14, - "isOsmCache": true - }, - "calculatedTags": [ - "_is_shadowed=feat.overlapWith('shadow').length > 0 ? 'yes': ''", - "_video:id=feat.properties.video === undefined ? undefined : new URL(feat.properties.video).searchParams.get('v')" - ] - } - }, { "builtin": "sport_pitch", "override": { @@ -129,7 +113,6 @@ "builtin": "slow_roads", "override": { "calculatedTags": [ - "_part_of_walking_routes=Array.from(new Set(feat.memberships().map(r => \"\" + r.relation.tags.name + \"\"))).join(', ')", "_is_shadowed=feat.overlapWith('shadow').length > 0 ? 'yes': ''" ], "source": { @@ -268,11 +251,6 @@ }, "overrideAll": { "+tagRenderings": [ - { - "id": "part-of-walk", - "render": "Maakt deel uit van {_part_of_walking_routes}", - "condition": "_part_of_walking_routes~*" - }, { "id": "has-video", "freeform": { @@ -289,4 +267,4 @@ ], "isShown": "_is_shadowed!=yes" } -} \ No newline at end of file +} diff --git a/assets/welcome_message.json b/assets/welcome_message.json deleted file mode 100644 index 31f736510..000000000 --- a/assets/welcome_message.json +++ /dev/null @@ -1,61 +0,0 @@ -[ - { - "start_date": "2022-05-30", - "end_date":"2022-06-05", - "message": "The 3rd of June is World Bicycle DayX. Go find a bike shop or bike pump nearby", - "featured_theme": "cyclofix" - }, - { - "start_date": "2022-04-24", - "end_date": "2022-05-30", - "message": "Help translating MapComplete! If you have some free time, please translate MapComplete to your favourite language. Read the instructions here" - }, - { - "start_date": "2022-04-18", - "end_date": "2022-04-24", - "message": "The 23rd of april is World Book Day. Go grab a book in a public bookcase (which is a piece of street furniture containing books where books can be taken and exchanged). Or alternative, search and map all of them in your neighbourhood!", - "featured_theme": "bookcases" - }, - { - "start_date": "2022-04-11", - "end_date": "2022-04-18", - "message": "The 15th of april is World Art Day - the ideal moment to go out, enjoy some artwork and add missing artwork to the map. And of course, you can snap some pictures", - "featured_theme": "artwork" - }, - { - "start_date": "2022-03-24", - "end_date": "2022-03-31", - "message": "The 22nd of March is World Water Day. Time to go out and find all the public drinking water spots!", - "featured_theme": "drinking_water" - }, - { - "start_date": "2022-01-24", - "end_date": "2022-01-30", - "message": "The 28th of January is International Privacy Day. Do you want to know where all the surveillance cameras are? Go find out!", - "featured_theme": "surveillance" - }, - { - "start_date": "2021-12-27", - "end_date": "2021-12-30", - "message": "In more normal circumstances, there would be a very cool gathering in Leipzig around this time with thousands of tech-minded people. However, due to some well-known circumstances, it is a virtual-only event this year as well. However, there might be a local hackerspace nearby to fill in this void", - "featured_theme": "hackerspaces" - }, - { - "start_date": "2021-11-01", - "end_date": "2021-11-07", - "message": "The first days of november is, in many European traditions, a moment that we remember our deceased. That is why this week the ghost bikes are featured. A ghost bike is a memorial in the form of a bicycle painted white which is placed to remember a cyclist whom was killed in a traffic accident. The ghostbike-theme shows such memorials. Even though there are already too much such memorials on the map, please add missing ones if you encounter them.", - "featured_theme": "ghostbikes" - }, - { - "start_date": "2021-10-25", - "end_date": "2021-11-01", - "message": "Did you know you could link OpenStreetMap with Wikidata? With name:etymology:wikidata, it is even possible to link to whom or what a feature is named after. Quite some volunteers have done this - because it is interesting or for the Equal Street Names-project. For this, a new theme has been created which shows the Wikipedia page and Wikimedia-images of this tag and which makes it easy to link them both with the search box. Give it a try!", - "featured_theme": "etymology" - }, - { - "start_date": "2021-10-17", - "end_date": "2021-10-25", - "message": "

Hi all!

Thanks for using MapComplete. It has been quite a ride since it's inception, a bit over a year ago. MapComplete has grown significantly recently, which you can read more about on in my diary entry.

Furthermore, NicoleLaine made a really cool new theme about postboxes, so make sure to check it out!

", - "featured_theme": "postboxes" - } -] \ No newline at end of file diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index a518e8e70..d563745b4 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -1443,11 +1443,6 @@ video { border-color: rgb(219 234 254 / var(--tw-border-opacity)); } -.border-red-500 { - --tw-border-opacity: 1; - border-color: rgb(239 68 68 / var(--tw-border-opacity)); -} - .border-gray-300 { --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity)); @@ -1873,6 +1868,12 @@ 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); diff --git a/scripts/generateCache.ts b/scripts/generateCache.ts index 24ff393b9..052008af3 100644 --- a/scripts/generateCache.ts +++ b/scripts/generateCache.ts @@ -7,7 +7,6 @@ import { existsSync, readFileSync, writeFileSync } from "fs" import { TagsFilter } from "../Logic/Tags/TagsFilter" import { Or } from "../Logic/Tags/Or" import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" -import RelationsTracker from "../Logic/Osm/RelationsTracker" import * as OsmToGeoJson from "osmtogeojson" import MetaTagging from "../Logic/MetaTagging" import { ImmutableStore, UIEventSource } from "../Logic/UIEventSource" @@ -26,13 +25,11 @@ import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeat import Loc from "../Models/Loc" import { Feature } from "geojson" import { BBox } from "../Logic/BBox" -import { bboxClip } from "@turf/turf" ScriptUtils.fixUtils() function createOverpassObject( theme: LayoutConfig, - relationTracker: RelationsTracker, backend: string ) { let filters: TagsFilter[] = [] @@ -52,12 +49,7 @@ function createOverpassObject( } } - // Check if data for this layer has already been loaded - if (layer.source.overpassScript !== undefined) { - extraScripts.push(layer.source.overpassScript) - } else { - filters.push(layer.source.osmTags) - } + filters.push(layer.source.osmTags) } filters = Utils.NoNull(filters) extraScripts = Utils.NoNull(extraScripts) @@ -69,7 +61,6 @@ function createOverpassObject( extraScripts, backend, new UIEventSource(60), - relationTracker ) } @@ -86,7 +77,6 @@ async function downloadRaw( targetdir: string, r: TileRange, theme: LayoutConfig, - relationTracker: RelationsTracker ): Promise<{ failed: number; skipped: number }> { let downloaded = 0 let failed = 0 @@ -130,7 +120,6 @@ async function downloadRaw( } const overpass = createOverpassObject( theme, - relationTracker, Constants.defaultOverpassUrls[failed % Constants.defaultOverpassUrls.length] ) const url = overpass.buildQuery( @@ -233,7 +222,6 @@ function loadAllTiles( function sliceToTiles( allFeatures: FeatureSource, theme: LayoutConfig, - relationsTracker: RelationsTracker, targetdir: string, pointsOnlyLayers: string[], clip: boolean @@ -244,8 +232,7 @@ function sliceToTiles( let indexisBuilt = false function buildIndex() { - for (const ff of allFeatures.features.data) { - const f = ff.feature + for (const f of allFeatures.features.data) { indexedFeatures.set(f.properties.id, f) } indexisBuilt = true @@ -281,9 +268,8 @@ function sliceToTiles( MetaTagging.addMetatags( source.features.data, { - memberships: relationsTracker, getFeaturesWithin: (_) => { - return [allFeatures.features.data.map((f) => f.feature)] + return [allFeatures.features.data] }, getFeatureById: getFeatureById, }, @@ -348,7 +334,7 @@ function sliceToTiles( } let strictlyCalculated = 0 let featureCount = 0 - let features: Feature[] = filteredTile.features.data.map((f) => f.feature) + let features: Feature[] = filteredTile.features.data for (const feature of features) { // Some cleanup @@ -444,7 +430,7 @@ function sliceToTiles( source, new UIEventSource(undefined) ) - const features = filtered.features.data.map((f) => f.feature) + const features = filtered.features.data const points = features.map((feature) => GeoOperations.centerpoint(feature)) console.log("Writing points overview for ", layerId) @@ -571,11 +557,9 @@ export async function main(args: string[]) { } } - const relationTracker = new RelationsTracker() - let failed = 0 do { - const cachingResult = await downloadRaw(targetdir, tileRange, theme, relationTracker) + const cachingResult = await downloadRaw(targetdir, tileRange, theme) failed = cachingResult.failed if (failed > 0) { await ScriptUtils.sleep(30000) @@ -584,7 +568,7 @@ export async function main(args: string[]) { const extraFeatures = await downloadExtraData(theme) const allFeaturesSource = loadAllTiles(targetdir, tileRange, theme, extraFeatures) - sliceToTiles(allFeaturesSource, theme, relationTracker, targetdir, generatePointLayersFor, clip) + sliceToTiles(allFeaturesSource, theme, targetdir, generatePointLayersFor, clip) } let args = [...process.argv] diff --git a/test.ts b/test.ts index b1375d4ab..73c93eb39 100644 --- a/test.ts +++ b/test.ts @@ -2,12 +2,13 @@ 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 { AllKnownLayoutsLazy } from "./Customizations/AllKnownLayouts" 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") diff --git a/test/Logic/ExtraFunctions.spec.ts b/test/Logic/ExtraFunctions.spec.ts index cb9598b08..869ffb359 100644 --- a/test/Logic/ExtraFunctions.spec.ts +++ b/test/Logic/ExtraFunctions.spec.ts @@ -115,7 +115,6 @@ describe("OverlapFunc", () => { const params: ExtraFuncParams = { getFeatureById: (id) => undefined, getFeaturesWithin: () => [[door]], - memberships: undefined, } ExtraFunctions.FullPatchFeature(params, hermanTeirlinck)