diff --git a/Logic/Actors/LayerResetter.ts b/Logic/Actors/BackgroundLayerResetter.ts similarity index 100% rename from Logic/Actors/LayerResetter.ts rename to Logic/Actors/BackgroundLayerResetter.ts diff --git a/Logic/FeatureSource/Actors/LocalStorageSaverActor.ts b/Logic/FeatureSource/Actors/LocalStorageSaverActor.ts new file mode 100644 index 000000000..9d0cb6e3b --- /dev/null +++ b/Logic/FeatureSource/Actors/LocalStorageSaverActor.ts @@ -0,0 +1,35 @@ +import {FeatureSourceForLayer} from "./FeatureSource"; +import {Utils} from "../../Utils"; + +/*** + * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run + * + * Technically, more an Actor then a featuresource, but it fits more neatly this ay + */ +export default class LocalStorageSaverActor { + public static readonly storageKey: string = "cached-features"; + + constructor(source: FeatureSourceForLayer, x: number, y: number, z: number) { + source.features.addCallbackAndRunD(features => { + const index = Utils.tile_index(z, x, y) + const key = `${LocalStorageSaverActor.storageKey}-${source.layer.layerDef.id}-${index}` + const now = new Date().getTime() + + if (features.length == 0) { + return; + } + + try { + localStorage.setItem(key, JSON.stringify(features)); + console.log("Saved ", features.length, "elements to", key) + localStorage.setItem(key + "-time", JSON.stringify(now)) + } catch (e) { + console.warn("Could not save the features to local storage:", e) + } + }) + + + } + + +} \ No newline at end of file diff --git a/Logic/FeatureSource/RegisteringFeatureSource.ts b/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts similarity index 88% rename from Logic/FeatureSource/RegisteringFeatureSource.ts rename to Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts index e464a60b8..b3b921195 100644 --- a/Logic/FeatureSource/RegisteringFeatureSource.ts +++ b/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts @@ -2,7 +2,7 @@ import FeatureSource from "./FeatureSource"; import {UIEventSource} from "../UIEventSource"; import State from "../../State"; -export default class RegisteringFeatureSource implements FeatureSource { +export default class RegisteringAllFromFeatureSourceActor { public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; public readonly name; diff --git a/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts b/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts deleted file mode 100644 index 87d91bb0c..000000000 --- a/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts +++ /dev/null @@ -1,64 +0,0 @@ -import FeatureSource from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import FilteredLayer from "../../Models/FilteredLayer"; - - -/** - * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) - * If this is the case, multiple objects with a different _matching_layer_id are generated. - * In any case, this featureSource marks the objects with _matching_layer_id - */ -export default class FeatureDuplicatorPerLayer implements FeatureSource { - public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; - - public readonly name; - - constructor(layers: UIEventSource, upstream: FeatureSource) { - this.name = "FeatureDuplicator of " + upstream.name; - this.features = upstream.features.map(features => { - const newFeatures: { feature: any, freshness: Date }[] = []; - if (features === undefined) { - return newFeatures; - } - - for (const f of features) { - if (f.feature._matching_layer_id) { - // Already matched previously - // We simply add it - newFeatures.push(f); - continue; - } - - - let foundALayer = false; - for (const layer of layers.data) { - if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) { - foundALayer = true; - if (layer.layerDef.passAllFeatures) { - - // We copy the feature; the "properties" field is kept identical though! - // Keeping "properties" identical is needed, as it might break the 'allElementStorage' otherwise - const newFeature = { - geometry: f.feature.geometry, - id: f.feature.id, - type: f.feature.type, - properties: f.feature.properties, - _matching_layer_id: layer.layerDef.id - } - newFeatures.push({feature: newFeature, freshness: f.freshness}); - } else { - // If not 'passAllFeatures', we are done - f.feature._matching_layer_id = layer.layerDef.id; - newFeatures.push(f); - break; - } - } - } - } - return newFeatures; - - }) - - } - -} \ No newline at end of file diff --git a/Logic/FeatureSource/FilteringFeatureSource.ts b/Logic/FeatureSource/FilteringFeatureSource.ts deleted file mode 100644 index 718f711b1..000000000 --- a/Logic/FeatureSource/FilteringFeatureSource.ts +++ /dev/null @@ -1,162 +0,0 @@ -import FeatureSource from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import Loc from "../../Models/Loc"; -import Hash from "../Web/Hash"; -import {TagsFilter} from "../Tags/TagsFilter"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; - -export default class FilteringFeatureSource implements FeatureSource { - public features: UIEventSource<{ feature: any; freshness: Date }[]> = - new UIEventSource<{ feature: any; freshness: Date }[]>([]); - public readonly name = "FilteringFeatureSource"; - - constructor( - layers: UIEventSource<{ - isDisplayed: UIEventSource; - layerDef: LayerConfig; - appliedFilters: UIEventSource; - }[]>, - location: UIEventSource, - selectedElement: UIEventSource, - upstream: FeatureSource - ) { - const self = this; - - function update() { - const layerDict = {}; - if (layers.data.length == 0) { - console.warn("No layers defined!"); - return; - } - for (const layer of layers.data) { - const prev = layerDict[layer.layerDef.id] - if (prev !== undefined) { - // We have seen this layer before! - // We prefer the one which has a name - if (layer.layerDef.name === undefined) { - // This one is hidden, so we skip it - console.log("Ignoring layer selection from ", layer) - continue; - } - } - layerDict[layer.layerDef.id] = layer; - } - - const features: { feature: any; freshness: Date }[] = - upstream.features.data; - - const missingLayers = new Set(); - - const newFeatures = features.filter((f) => { - const layerId = f.feature._matching_layer_id; - - if ( - selectedElement.data?.id === f.feature.id || - f.feature.id === Hash.hash.data) { - // This is the selected object - it gets a free pass even if zoom is not sufficient or it is filtered away - return true; - } - - if (layerId === undefined) { - return false; - } - const layer: { - isDisplayed: UIEventSource; - layerDef: LayerConfig; - appliedFilters: UIEventSource; - } = layerDict[layerId]; - if (layer === undefined) { - missingLayers.add(layerId); - return false; - } - - const isShown = layer.layerDef.isShown; - const tags = f.feature.properties; - if (isShown.IsKnown(tags)) { - const result = layer.layerDef.isShown.GetRenderValue( - f.feature.properties - ).txt; - if (result !== "yes") { - return false; - } - } - - const tagsFilter = layer.appliedFilters.data; - if (tagsFilter) { - if (!tagsFilter.matchesProperties(f.feature.properties)) { - // Hidden by the filter on the layer itself - we want to hide it no matter wat - return false; - } - } - if (!FilteringFeatureSource.showLayer(layer, location)) { - // The layer itself is either disabled or hidden due to zoom constraints - // We should return true, but it might still match some other layer - return false; - } - - return true; - }); - - self.features.setData(newFeatures); - if (missingLayers.size > 0) { - console.error( - "Some layers were not found: ", - Array.from(missingLayers) - ); - } - } - - upstream.features.addCallback(() => { - update(); - }); - location - .map((l) => { - // We want something that is stable for the shown layers - const displayedLayerIndexes = []; - for (let i = 0; i < layers.data.length; i++) { - const layer = layers.data[i]; - if (l.zoom < layer.layerDef.minzoom) { - continue; - } - - if (!layer.isDisplayed.data) { - continue; - } - displayedLayerIndexes.push(i); - } - return displayedLayerIndexes.join(","); - }) - .addCallback(() => { - update(); - }); - - layers.addCallback(update); - - const registered = new Set>(); - layers.addCallbackAndRun((layers) => { - for (const layer of layers) { - if (registered.has(layer.isDisplayed)) { - continue; - } - registered.add(layer.isDisplayed); - layer.isDisplayed.addCallback(() => update()); - layer.appliedFilters.addCallback(() => update()); - } - }); - - update(); - } - - private static showLayer( - layer: { - isDisplayed: UIEventSource; - layerDef: LayerConfig; - }, - location: UIEventSource - ) { - return ( - layer.isDisplayed.data && - layer.layerDef.minzoomVisible <= location.data.zoom - ); - } -} diff --git a/Logic/FeatureSource/GeoJsonSource.ts b/Logic/FeatureSource/GeoJsonSource.ts deleted file mode 100644 index 0c28e1488..000000000 --- a/Logic/FeatureSource/GeoJsonSource.ts +++ /dev/null @@ -1,207 +0,0 @@ -import FeatureSource from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import Loc from "../../Models/Loc"; -import State from "../../State"; -import {Utils} from "../../Utils"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; - - -/** - * Fetches a geojson file somewhere and passes it along - */ -export default class GeoJsonSource implements FeatureSource { - - public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; - public readonly name; - public readonly isOsmCache: boolean - private onFail: ((errorMsg: any, url: string) => void) = undefined; - private readonly layerId: string; - private readonly seenids: Set = new Set() - - private constructor(locationControl: UIEventSource, - flayer: { isDisplayed: UIEventSource, layerDef: LayerConfig }, - onFail?: ((errorMsg: any) => void)) { - this.layerId = flayer.layerDef.id; - let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); - this.name = "GeoJsonSource of " + url; - const zoomLevel = flayer.layerDef.source.geojsonZoomLevel; - - this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer; - - this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) - - if (zoomLevel === undefined) { - // This is a classic, static geojson layer - if (onFail === undefined) { - onFail = _ => { - } - } - this.onFail = onFail; - - this.LoadJSONFrom(url) - } else { - this.ConfigureDynamicLayer(url, zoomLevel, locationControl, flayer) - } - } - - /** - * Merges together the layers which have the same source - * @param flayers - * @param locationControl - * @constructor - */ - public static ConstructMultiSource(flayers: { isDisplayed: UIEventSource, layerDef: LayerConfig }[], locationControl: UIEventSource): GeoJsonSource[] { - - const flayersPerSource = new Map, layerDef: LayerConfig }[]>(); - for (const flayer of flayers) { - const url = flayer.layerDef.source.geojsonSource?.replace(/{layer}/g, flayer.layerDef.id) - if (url === undefined) { - continue; - } - - if (!flayersPerSource.has(url)) { - flayersPerSource.set(url, []) - } - flayersPerSource.get(url).push(flayer) - } - - const sources: GeoJsonSource[] = [] - - flayersPerSource.forEach((flayers, key) => { - if (flayers.length == 1) { - sources.push(new GeoJsonSource(locationControl, flayers[0])); - return; - } - - const zoomlevels = Utils.Dedup(flayers.map(flayer => "" + (flayer.layerDef.source.geojsonZoomLevel ?? ""))) - if (zoomlevels.length > 1) { - throw "Multiple zoomlevels defined for same geojson source " + key - } - - let isShown = new UIEventSource(true, "IsShown for multiple layers: or of multiple values"); - for (const flayer of flayers) { - flayer.isDisplayed.addCallbackAndRun(() => { - let value = false; - for (const flayer of flayers) { - value = flayer.isDisplayed.data || value; - } - isShown.setData(value); - }); - - } - - const source = new GeoJsonSource(locationControl, { - isDisplayed: isShown, - layerDef: flayers[0].layerDef // We only care about the source info here - }) - sources.push(source) - - }) - return sources; - - } - - private ConfigureDynamicLayer(url: string, zoomLevel: number, locationControl: UIEventSource, flayer: { isDisplayed: UIEventSource, layerDef: LayerConfig }) { - // This is a dynamic template with a fixed zoom level - url = url.replace("{z}", "" + zoomLevel) - const loadedTiles = new Set(); - const self = this; - this.onFail = (msg, url) => { - console.warn(`Could not load geojson layer from`, url, "due to", msg) - loadedTiles.add(url); // We add the url to the 'loadedTiles' in order to not reload it in the future - } - - const neededTiles = locationControl.map( - location => { - if (!flayer.isDisplayed.data) { - // No need to download! - the layer is disabled - return undefined; - } - - if (location.zoom < flayer.layerDef.minzoom) { - // No need to download! - the layer is disabled - return undefined; - } - - // Yup, this is cheating to just get the bounds here - const bounds = State.state.leafletMap.data?.getBounds() - if(bounds === undefined){ - // We'll retry later - return undefined - } - const tileRange = Utils.TileRangeBetween(zoomLevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) - const needed = Utils.MapRange(tileRange, (x, y) => { - return url.replace("{x}", "" + x).replace("{y}", "" + y); - }) - return new Set(needed); - } - , [flayer.isDisplayed, State.state.leafletMap]); - neededTiles.stabilized(250).addCallback((needed: Set) => { - if (needed === undefined) { - return; - } - - needed.forEach(neededTile => { - if (loadedTiles.has(neededTile)) { - return; - } - - loadedTiles.add(neededTile) - self.LoadJSONFrom(neededTile) - - }) - }) - - } - - private LoadJSONFrom(url: string) { - const eventSource = this.features; - const self = this; - Utils.downloadJson(url) - .then(json => { - if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) { - self.onFail("Runtime error (timeout)", url) - return; - } - const time = new Date(); - const newFeatures: { feature: any, freshness: Date } [] = [] - let i = 0; - let skipped = 0; - for (const feature of json.features) { - const props = feature.presets - for (const key in props) { - if(typeof props[key] !== "string"){ - props[key] = ""+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) - - let freshness: Date = time; - if (feature.properties["_last_edit:timestamp"] !== undefined) { - freshness = new Date(props["_last_edit:timestamp"]) - } - - newFeatures.push({feature: feature, freshness: freshness}) - } - console.debug("Downloaded " + newFeatures.length + " new features and " + skipped + " already seen features from " + url); - - if (newFeatures.length == 0) { - return; - } - - eventSource.setData(eventSource.data.concat(newFeatures)) - - }).catch(msg => self.onFail(msg, url)) - } - -} diff --git a/Logic/FeatureSource/LocalStorageSaver.ts b/Logic/FeatureSource/LocalStorageSaver.ts deleted file mode 100644 index 354adb75a..000000000 --- a/Logic/FeatureSource/LocalStorageSaver.ts +++ /dev/null @@ -1,41 +0,0 @@ -/*** - * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run - * - * Technically, more an Actor then a featuresource, but it fits more neatly this ay - */ -import FeatureSource from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; - -export default class LocalStorageSaver implements FeatureSource { - public static readonly storageKey: string = "cached-features"; - public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; - - public readonly name = "LocalStorageSaver"; - - constructor(source: FeatureSource, layout: UIEventSource) { - this.features = source.features; - - this.features.addCallbackAndRunD(features => { - const now = new Date().getTime() - features = features.filter(f => layout.data.cacheTimeout > Math.abs(now - f.freshness.getTime()) / 1000) - - - if (features.length == 0) { - return; - } - - try { - const key = LocalStorageSaver.storageKey + layout.data.id - localStorage.setItem(key, JSON.stringify(features)); - console.log("Saved ", features.length, "elements to", key) - } catch (e) { - console.warn("Could not save the features to local storage:", e) - } - }) - - - } - - -} \ No newline at end of file diff --git a/Logic/FeatureSource/MetaTaggingFeatureSource.ts b/Logic/FeatureSource/MetaTaggingFeatureSource.ts deleted file mode 100644 index 3ae5c9015..000000000 --- a/Logic/FeatureSource/MetaTaggingFeatureSource.ts +++ /dev/null @@ -1,52 +0,0 @@ -import FeatureSource from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import State from "../../State"; -import Hash from "../Web/Hash"; -import MetaTagging from "../MetaTagging"; - -export default class MetaTaggingFeatureSource implements FeatureSource { - public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined); - - public readonly name; - - /*** - * Constructs a new metatagger which'll calculate various tags - * @param allFeaturesSource: A source where all the currently known features can be found - used to calculate overlaps etc - * @param source: the source of features that should get their metatag and which should be exported again - * @param updateTrigger - */ - constructor(allFeaturesSource: UIEventSource<{ feature: any; freshness: Date }[]>, source: FeatureSource, updateTrigger?: UIEventSource) { - const self = this; - this.name = "MetaTagging of " + source.name - - if (allFeaturesSource === undefined) { - throw ("UIEVentSource is undefined") - } - - function update() { - const featuresFreshness = source.features.data - if (featuresFreshness === undefined) { - return; - } - featuresFreshness.forEach(featureFresh => { - const feature = featureFresh.feature; - - if (Hash.hash.data === feature.properties.id) { - State.state.selectedElement.setData(feature); - } - }) - - MetaTagging.addMetatags(featuresFreshness, - allFeaturesSource, - State.state.knownRelations.data, State.state.layoutToUse.data.layers); - self.features.setData(featuresFreshness); - } - - source.features.addCallbackAndRun(_ => update()); - updateTrigger?.addCallback(_ => { - console.debug("Updating because of external call") - update(); - }) - } - -} \ No newline at end of file diff --git a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts new file mode 100644 index 000000000..296f318b7 --- /dev/null +++ b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts @@ -0,0 +1,87 @@ +import FeatureSource from "./FeatureSource"; +import {UIEventSource} from "../UIEventSource"; +import FilteredLayer from "../../Models/FilteredLayer"; +import OverpassFeatureSource from "../Actors/OverpassFeatureSource"; +import SimpleFeatureSource from "./SimpleFeatureSource"; + + +/** + * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) + * If this is the case, multiple objects with a different _matching_layer_id are generated. + * In any case, this featureSource marks the objects with _matching_layer_id + */ +export default class PerLayerFeatureSourceSplitter { + + constructor(layers: UIEventSource, + handleLayerData: (source: FeatureSource) => void, + upstream: OverpassFeatureSource) { + + const knownLayers = new Map() + + function update() { + const features = upstream.features.data; + if (features === undefined) { + return; + } + if(layers.data === undefined){ + return; + } + + // We try to figure out (for each feature) in which feature store it should be saved. + // Note that this splitter is only run when it is invoked by the overpass feature source, so we can't be sure in which layer it should go + + const featuresPerLayer = new Map(); + + function addTo(layer: FilteredLayer, feature: { feature, freshness }) { + const id = layer.layerDef.id + const list = featuresPerLayer.get(id) + if (list !== undefined) { + list.push(feature) + } else { + featuresPerLayer.set(id, [feature]) + } + } + + for (const f of features) { + for (const layer of layers.data) { + if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) { + // We have found our matching layer! + addTo(layer, f) + if (!layer.layerDef.passAllFeatures) { + // If not 'passAllFeatures', we are done for this feature + break; + } + } + } + } + + // At this point, we have our features per layer as a list + // We assign them to the correct featureSources + for (const layer of layers.data) { + const id = layer.layerDef.id; + const features = featuresPerLayer.get(id) + if (features === undefined) { + // No such features for this layer + continue; + } + + let featureSource = knownLayers.get(id) + if (featureSource === undefined) { + // Not yet initialized - now is a good time + featureSource = new SimpleFeatureSource(layer) + knownLayers.set(id, featureSource) + handleLayerData(featureSource) + } + featureSource.features.setData(features) + } + + + upstream.features.addCallbackAndRunD(_ => update()) + layers.addCallbackAndRunD(_ => update()) + + } + + layers.addCallbackAndRunD(_ => update()) + upstream.features.addCallbackAndRunD(_ => update()) + } +} \ No newline at end of file diff --git a/Logic/FeatureSource/FeatureSourceMerger.ts b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts similarity index 62% rename from Logic/FeatureSource/FeatureSourceMerger.ts rename to Logic/FeatureSource/Sources/FeatureSourceMerger.ts index 3d1b794c6..17d6db690 100644 --- a/Logic/FeatureSource/FeatureSourceMerger.ts +++ b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts @@ -1,27 +1,44 @@ -import FeatureSource from "./FeatureSource"; +import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource"; import {UIEventSource} from "../UIEventSource"; +import FilteredLayer from "../../Models/FilteredLayer"; /** - * Merges features from different featureSources + * 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 */ -export default class FeatureSourceMerger implements FeatureSource { +export default class FeatureSourceMerger implements FeatureSourceForLayer { public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); public readonly name; - private readonly _sources: FeatureSource[]; + public readonly layer: FilteredLayer + private readonly _sources: UIEventSource; - constructor(sources: FeatureSource[]) { + constructor(layer: FilteredLayer ,sources: UIEventSource) { this._sources = sources; - this.name = "SourceMerger of (" + sources.map(s => s.name).join(", ") + ")" + this.layer = layer; + this.name = "SourceMerger" const self = this; - for (let i = 0; i < sources.length; i++) { - let source = sources[i]; - source.features.addCallback(() => { - self.Update(); - }); - } - this.Update(); + + 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(); + } + } + }) + } private Update() { @@ -34,7 +51,7 @@ export default class FeatureSourceMerger implements FeatureSource { all.set(oldValue.feature.id + oldValue.feature._matching_layer_id, oldValue) } - for (const source of this._sources) { + for (const source of this._sources.data) { if (source?.features?.data === undefined) { continue; } @@ -64,7 +81,7 @@ export default class FeatureSourceMerger implements FeatureSource { } const newList = []; - all.forEach((value, key) => { + all.forEach((value, _) => { newList.push(value) }) this.features.setData(newList); diff --git a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts new file mode 100644 index 000000000..67a12af35 --- /dev/null +++ b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts @@ -0,0 +1,101 @@ +import {FeatureSourceForLayer} from "./FeatureSource"; +import {UIEventSource} from "../UIEventSource"; +import Hash from "../Web/Hash"; +import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; +import FilteredLayer from "../../Models/FilteredLayer"; + +export default class FilteringFeatureSource implements FeatureSourceForLayer { + public features: UIEventSource<{ feature: any; freshness: Date }[]> = + new UIEventSource<{ feature: any; freshness: Date }[]>([]); + public readonly name = "FilteringFeatureSource"; + public readonly layer: FilteredLayer; + + constructor( + state: { + locationControl: UIEventSource<{ zoom: number }>, + selectedElement: UIEventSource, + }, + upstream: FeatureSourceForLayer + ) { + const self = this; + + this.layer = upstream.layer; + const layer = upstream.layer; + + function update() { + const features: { feature: any; freshness: Date }[] = upstream.features.data; + const newFeatures = features.filter((f) => { + if ( + state.selectedElement.data?.id === f.feature.id || + f.feature.id === Hash.hash.data) { + // This is the selected object - it gets a free pass even if zoom is not sufficient or it is filtered away + return true; + } + + const isShown = layer.layerDef.isShown; + const tags = f.feature.properties; + if (isShown.IsKnown(tags)) { + const result = layer.layerDef.isShown.GetRenderValue( + f.feature.properties + ).txt; + if (result !== "yes") { + return false; + } + } + + const tagsFilter = layer.appliedFilters.data; + if (tagsFilter) { + if (!tagsFilter.matchesProperties(f.feature.properties)) { + // Hidden by the filter on the layer itself - we want to hide it no matter wat + return false; + } + } + if (!FilteringFeatureSource.showLayer(layer, state.locationControl.data)) { + // The layer itself is either disabled or hidden due to zoom constraints + // We should return true, but it might still match some other layer + return false; + } + return true; + }); + + self.features.setData(newFeatures); + } + + upstream.features.addCallback(() => { + update(); + }); + + let isShown = state.locationControl.map((l) => FilteringFeatureSource.showLayer(layer, l), + [layer.isDisplayed]) + + isShown.addCallback(isShown => { + if (isShown) { + update(); + } else { + self.features.setData([]) + } + }); + + layer.appliedFilters.addCallback(_ => { + if(!isShown.data){ + // Currently not shown. + // Note that a change in 'isSHown' will trigger an update as well, so we don't have to watch it another time + return; + } + update() + }) + + update(); + } + + private static showLayer( + layer: { + isDisplayed: UIEventSource; + layerDef: LayerConfig; + }, + location: { zoom: number }) { + return layer.isDisplayed.data && + layer.layerDef.minzoomVisible <= location.zoom; + + } +} diff --git a/Logic/FeatureSource/Sources/GeoJsonSource.ts b/Logic/FeatureSource/Sources/GeoJsonSource.ts new file mode 100644 index 000000000..165f672cf --- /dev/null +++ b/Logic/FeatureSource/Sources/GeoJsonSource.ts @@ -0,0 +1,95 @@ +import {FeatureSourceForLayer} from "./FeatureSource"; +import {UIEventSource} from "../UIEventSource"; +import {Utils} from "../../Utils"; +import FilteredLayer from "../../Models/FilteredLayer"; +import {control} from "leaflet"; + + +/** + * Fetches a geojson file somewhere and passes it along + */ +export default class GeoJsonSource implements FeatureSourceForLayer { + + public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; + public readonly name; + public readonly isOsmCache: boolean + private onFail: ((errorMsg: any, url: string) => void) = undefined; + private readonly seenids: Set = new Set() + public readonly layer: FilteredLayer; + + + public constructor(flayer: FilteredLayer, + zxy?: [number, number, number]) { + + if (flayer.layerDef.source.geojsonZoomLevel !== undefined && zxy === undefined) { + throw "Dynamic layers are not supported. Use 'DynamicGeoJsonTileSource instead" + } + + this.layer = flayer; + let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); + if (zxy !== undefined) { + url = url + .replace('{z}', "" + zxy[0]) + .replace('{x}', "" + zxy[1]) + .replace('{y}', "" + zxy[2]) + } + + this.name = "GeoJsonSource of " + url; + + this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer; + this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) + this.LoadJSONFrom(url) + } + + + private LoadJSONFrom(url: string) { + const eventSource = this.features; + const self = this; + Utils.downloadJson(url) + .then(json => { + if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) { + self.onFail("Runtime error (timeout)", url) + return; + } + const time = new Date(); + const newFeatures: { feature: any, freshness: Date } [] = [] + let i = 0; + let skipped = 0; + for (const feature of json.features) { + const props = feature.properties + for (const key in props) { + if (typeof props[key] !== "string") { + props[key] = "" + 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) + + let freshness: Date = time; + if (feature.properties["_last_edit:timestamp"] !== undefined) { + freshness = new Date(props["_last_edit:timestamp"]) + } + + newFeatures.push({feature: feature, freshness: freshness}) + } + console.debug("Downloaded " + newFeatures.length + " new features and " + skipped + " already seen features from " + url); + + if (newFeatures.length == 0) { + return; + } + + eventSource.setData(eventSource.data.concat(newFeatures)) + + }).catch(msg => console.error("Could not load geojon layer", url, "due to", msg)) + } + +} diff --git a/Logic/FeatureSource/LocalStorageSource.ts b/Logic/FeatureSource/Sources/LocalStorageSource.ts similarity index 91% rename from Logic/FeatureSource/LocalStorageSource.ts rename to Logic/FeatureSource/Sources/LocalStorageSource.ts index e072b948e..c8c28b5c0 100644 --- a/Logic/FeatureSource/LocalStorageSource.ts +++ b/Logic/FeatureSource/Sources/LocalStorageSource.ts @@ -1,6 +1,6 @@ import FeatureSource from "./FeatureSource"; import {UIEventSource} from "../UIEventSource"; -import LocalStorageSaver from "./LocalStorageSaver"; +import LocalStorageSaverActor from "./LocalStorageSaverActor"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; export default class LocalStorageSource implements FeatureSource { @@ -9,7 +9,7 @@ export default class LocalStorageSource implements FeatureSource { constructor(layout: UIEventSource) { this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) - const key = LocalStorageSaver.storageKey + layout.data.id + const key = LocalStorageSaverActor.storageKey + layout.data.id layout.addCallbackAndRun(_ => { try { const fromStorage = localStorage.getItem(key); diff --git a/Logic/FeatureSource/OsmApiFeatureSource.ts b/Logic/FeatureSource/Sources/OsmApiFeatureSource.ts similarity index 85% rename from Logic/FeatureSource/OsmApiFeatureSource.ts rename to Logic/FeatureSource/Sources/OsmApiFeatureSource.ts index d48cc5f4b..de3157da2 100644 --- a/Logic/FeatureSource/OsmApiFeatureSource.ts +++ b/Logic/FeatureSource/Sources/OsmApiFeatureSource.ts @@ -4,7 +4,6 @@ import {OsmObject} from "../Osm/OsmObject"; import {Utils} from "../../Utils"; import Loc from "../../Models/Loc"; import FilteredLayer from "../../Models/FilteredLayer"; -import Constants from "../../Models/Constants"; export default class OsmApiFeatureSource implements FeatureSource { @@ -15,19 +14,23 @@ export default class OsmApiFeatureSource implements FeatureSource { leafletMap: UIEventSource; locationControl: UIEventSource, filteredLayers: UIEventSource}; - constructor(minZoom = undefined, state: {locationControl: UIEventSource, filteredLayers: UIEventSource, leafletMap: UIEventSource}) { + constructor(state: {locationControl: UIEventSource, filteredLayers: UIEventSource, leafletMap: UIEventSource, + overpassMaxZoom: UIEventSource}) { this._state = state; - if(minZoom !== undefined){ + const self = this; + function update(){ + const minZoom = state.overpassMaxZoom.data; + const location = state.locationControl.data + if(minZoom === undefined || location === undefined){ + return; + } if(minZoom < 14){ throw "MinZoom should be at least 14 or higher, OSM-api won't work otherwise" } - const self = this; - state.locationControl.addCallbackAndRunD(location => { - if(location.zoom > minZoom){ - return; - } - self.loadArea() - }) + if(location.zoom > minZoom){ + return; + } + self.loadArea() } } @@ -59,10 +62,6 @@ export default class OsmApiFeatureSource implements FeatureSource { if (disabledLayers.length > 0) { return false; } - const loc = this._state.locationControl.data; - if (loc.zoom < Constants.useOsmApiAt) { - return false; - } if (this._state.leafletMap.data === undefined) { return false; // Not yet inited } diff --git a/Logic/FeatureSource/RememberingSource.ts b/Logic/FeatureSource/Sources/RememberingSource.ts similarity index 74% rename from Logic/FeatureSource/RememberingSource.ts rename to Logic/FeatureSource/Sources/RememberingSource.ts index 458b278d0..42b0b0ba3 100644 --- a/Logic/FeatureSource/RememberingSource.ts +++ b/Logic/FeatureSource/Sources/RememberingSource.ts @@ -1,12 +1,14 @@ -/** - * Every previously added point is remembered, but new points are added - */ -import FeatureSource from "./FeatureSource"; + +import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource"; import {UIEventSource} from "../UIEventSource"; - +import FilteredLayer from "../../Models/FilteredLayer"; +/** + * Every previously added point is remembered, but new points are added. + * Data coming from upstream will always overwrite a previous value + */ export default class RememberingSource implements FeatureSource { - public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>; + public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>; public readonly name; constructor(source: FeatureSource) { @@ -20,9 +22,9 @@ export default class RememberingSource implements FeatureSource { } // Then new ids - const ids = new Set(features.map(f => f.feature.properties.id + f.feature.geometry.type + f.feature._matching_layer_id)); + const ids = new Set(features.map(f => f.feature.properties.id + f.feature.geometry.type)); // the old data - const oldData = oldFeatures.filter(old => !ids.has(old.feature.properties.id + old.feature.geometry.type + old.feature._matching_layer_id)) + const oldData = oldFeatures.filter(old => !ids.has(old.feature.properties.id + old.feature.geometry.type)) return [...features, ...oldData]; }) } diff --git a/Logic/FeatureSource/Sources/SimpleFeatureSource.ts b/Logic/FeatureSource/Sources/SimpleFeatureSource.ts new file mode 100644 index 000000000..6237a2ddb --- /dev/null +++ b/Logic/FeatureSource/Sources/SimpleFeatureSource.ts @@ -0,0 +1,16 @@ +import {FeatureSourceForLayer} from "./FeatureSource"; +import {UIEventSource} from "../UIEventSource"; +import FilteredLayer from "../../Models/FilteredLayer"; + +export default class SimpleFeatureSource implements FeatureSourceForLayer { + public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); + public readonly name: string = "SimpleFeatureSource"; + public readonly layer: FilteredLayer; + + constructor(layer: FilteredLayer) { + this.name = "SimpleFeatureSource("+layer.layerDef.id+")" + this.layer = layer + } + + +} \ No newline at end of file diff --git a/Logic/FeatureSource/WayHandlingApplyingFeatureSource.ts b/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts similarity index 61% rename from Logic/FeatureSource/WayHandlingApplyingFeatureSource.ts rename to Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts index b2e5fba14..54e1137a0 100644 --- a/Logic/FeatureSource/WayHandlingApplyingFeatureSource.ts +++ b/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts @@ -1,4 +1,4 @@ -import FeatureSource from "./FeatureSource"; +import {FeatureSourceForLayer} from "./FeatureSource"; import {UIEventSource} from "../UIEventSource"; import {GeoOperations} from "../GeoOperations"; import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; @@ -6,39 +6,31 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; /** * This is the part of the pipeline which introduces extra points at the center of an area (but only if this is demanded by the wayhandling) */ -export default class WayHandlingApplyingFeatureSource implements FeatureSource { +export default class WayHandlingApplyingFeatureSource implements FeatureSourceForLayer { public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; public readonly name; + public readonly layer; - constructor(layers: UIEventSource<{ - layerDef: LayerConfig - }[]>, - upstream: FeatureSource) { + constructor(upstream: FeatureSourceForLayer) { this.name = "Wayhandling of " + upstream.name; + this.layer = upstream.layer + const layer = upstream.layer.layerDef; + + if (layer.wayHandling === LayerConfig.WAYHANDLING_DEFAULT) { + // We don't have to do anything fancy + // lets just wire up the upstream + this.features = upstream.features; + return; + } + this.features = upstream.features.map( features => { if (features === undefined) { return; } - - const layerDict = {}; - let allDefaultWayHandling = true; - for (const layer of layers.data) { - layerDict[layer.layerDef.id] = layer; - if (layer.layerDef.wayHandling !== LayerConfig.WAYHANDLING_DEFAULT) { - allDefaultWayHandling = false; - } - } - const newFeatures: { feature: any, freshness: Date }[] = []; for (const f of features) { const feat = f.feature; - const layerId = feat._matching_layer_id; - const layer: LayerConfig = layerDict[layerId].layerDef; - if (layer === undefined) { - console.error("No layer found with id " + layerId); - continue; - } if (layer.wayHandling === LayerConfig.WAYHANDLING_DEFAULT) { newFeatures.push(f); @@ -47,19 +39,17 @@ export default class WayHandlingApplyingFeatureSource implements FeatureSource { if (feat.geometry.type === "Point") { newFeatures.push(f); - // it is a point, nothing to do here + // feature is a point, nothing to do here continue; } // Create the copy const centerPoint = GeoOperations.centerpoint(feat); - centerPoint["_matching_layer_id"] = feat._matching_layer_id; newFeatures.push({feature: centerPoint, freshness: f.freshness}); if (layer.wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY) { newFeatures.push(f); } - } return newFeatures; } diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts new file mode 100644 index 000000000..e69de29bb diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts new file mode 100644 index 000000000..931b85d3c --- /dev/null +++ b/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts @@ -0,0 +1,72 @@ +/*** + * A tiled source which dynamically loads the required tiles + */ +import State from "../../../State"; +import FilteredLayer from "../../../Models/FilteredLayer"; +import {FeatureSourceForLayer} from "../FeatureSource"; +import {Utils} from "../../../Utils"; +import {UIEventSource} from "../../UIEventSource"; +import Loc from "../../../Models/Loc"; + +export default class DynamicTileSource { + private readonly _loadedTiles = new Set(); + + public readonly existingTiles: Map> = new Map>() + + constructor( + layer: FilteredLayer, + zoomlevel: number, + constructTile: (xy: [number, number]) => FeatureSourceForLayer, + state: { + locationControl: UIEventSource + leafletMap: any + } + ) { + state = State.state + const self = this; + const neededTiles = state.locationControl.map( + location => { + if (!layer.isDisplayed.data) { + // No need to download! - the layer is disabled + return undefined; + } + + if (location.zoom < layer.layerDef.minzoom) { + // No need to download! - the layer is disabled + return undefined; + } + + // Yup, this is cheating to just get the bounds here + const bounds = state.leafletMap.data?.getBounds() + if (bounds === undefined) { + // We'll retry later + return undefined + } + const tileRange = Utils.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) + + const needed = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i)) + if(needed.length === 0){ + return undefined + } + return needed + } + , [layer.isDisplayed, state.leafletMap]).stabilized(250); + + neededTiles.addCallbackAndRunD(neededIndexes => { + for (const neededIndex of neededIndexes) { + self._loadedTiles.add(neededIndex) + const xy = Utils.tile_from_index(zoomlevel, neededIndex) + const src = constructTile(xy) + let xmap = self.existingTiles.get(xy[0]) + if(xmap === undefined){ + xmap = new Map() + self.existingTiles.set(xy[0], xmap) + } + xmap.set(xy[1], src) + } + }) + + + } + +} \ No newline at end of file diff --git a/Logic/FeatureSource/TiledFeatureSource/README.md b/Logic/FeatureSource/TiledFeatureSource/README.md new file mode 100644 index 000000000..36b0c1b01 --- /dev/null +++ b/Logic/FeatureSource/TiledFeatureSource/README.md @@ -0,0 +1,3 @@ +Data in MapComplete can come from multiple sources. + +In order to keep thins snappy, they are distributed over a tiled database \ No newline at end of file diff --git a/Logic/FeatureSource/TiledFeatureSource/SingleLayerSource.ts b/Logic/FeatureSource/TiledFeatureSource/SingleLayerSource.ts new file mode 100644 index 000000000..e69de29bb diff --git a/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts b/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts new file mode 100644 index 000000000..e69de29bb diff --git a/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts new file mode 100644 index 000000000..e69de29bb diff --git a/Logic/FeatureSource/TiledFeatureSource/TiledFromLocalStorageSource.ts b/Logic/FeatureSource/TiledFeatureSource/TiledFromLocalStorageSource.ts new file mode 100644 index 000000000..58c1fcaeb --- /dev/null +++ b/Logic/FeatureSource/TiledFeatureSource/TiledFromLocalStorageSource.ts @@ -0,0 +1,40 @@ +import FilteredLayer from "../../../Models/FilteredLayer"; +import {FeatureSourceForLayer} from "../FeatureSource"; +import {UIEventSource} from "../../UIEventSource"; +import Loc from "../../../Models/Loc"; +import GeoJsonSource from "../GeoJsonSource"; +import DynamicTileSource from "./DynamicTileSource"; + +export default class DynamicGeoJsonTileSource extends DynamicTileSource { + constructor(layer: FilteredLayer, + registerLayer: (layer: FeatureSourceForLayer) => void, + state: { + locationControl: UIEventSource + leafletMap: any + }) { + const source = layer.layerDef.source + if (source.geojsonZoomLevel === undefined) { + throw "Invalid layer: geojsonZoomLevel expected" + } + if (source.geojsonSource === undefined) { + throw "Invalid layer: geojsonSource expected" + } + + super( + layer, + source.geojsonZoomLevel, + (xy) => { + const xyz: [number, number, number] = [xy[0], xy[1], source.geojsonZoomLevel] + const src = new GeoJsonSource( + layer, + xyz + ) + registerLayer(src) + return src + }, + state + ); + + } + +} \ No newline at end of file diff --git a/Logic/Osm/ExtractRelations.ts b/Logic/Osm/RelationsTracker.ts similarity index 100% rename from Logic/Osm/ExtractRelations.ts rename to Logic/Osm/RelationsTracker.ts