import GeoJsonSource from "./GeoJsonSource" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import { FeatureSource, IndexedFeatureSource, UpdatableFeatureSource } from "../FeatureSource" import { Or } from "../../Tags/Or" import FeatureSwitchState from "../../State/FeatureSwitchState" import OverpassFeatureSource from "./OverpassFeatureSource" import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" import OsmFeatureSource from "./OsmFeatureSource" import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource" import { BBox } from "../../BBox" import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource" import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource" import DynamicMvtileSource from "../TiledFeatureSource/DynamicMvtTileSource" import FeatureSourceMerger from "./FeatureSourceMerger" import { Feature } from "geojson" import { OsmFeature } from "../../../Models/OsmFeature" /** * 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 ThemeSource implements IndexedFeatureSource { /** * Indicates if a data source is loading something */ public readonly isLoading: Store public static readonly fromCacheZoomLevel = 15 public features: UIEventSource = new UIEventSource([]) public readonly featuresById: Store> private readonly core: Store private readonly addedSources: FeatureSource[] = [] private readonly addedItems: OsmFeature[] = [] constructor( layers: LayerConfig[], featureSwitches: FeatureSwitchState, mapProperties: { bounds: Store; zoom: Store }, backend: string, isDisplayed: (id: string) => Store, mvtAvailableLayers: Store>, fullNodeDatabaseSource?: FullNodeDatabaseSource ) { const isLoading = new UIEventSource(true) this.isLoading = isLoading const features = (this.features = new UIEventSource([])) const featuresById = (this.featuresById = new UIEventSource(new Map())) this.core = mvtAvailableLayers.mapD((mvtAvailableLayers) => { const core = new ThemeSourceCore( layers, featureSwitches, mapProperties, backend, isDisplayed, mvtAvailableLayers, isLoading, fullNodeDatabaseSource ) this.addedSources.forEach((src) => core.addSource(src)) this.addedItems.forEach((item) => core.addItem(item)) core.features.addCallbackAndRun((data) => features.set(data)) core.featuresById.addCallbackAndRun((data) => featuresById.set(data)) return core }) } public async downloadAll() { return this.core.data.downloadAll() } public addSource(source: FeatureSource) { this.core.data?.addSource(source) this.addedSources.push(source) } public addItem(obj: OsmFeature) { this.core.data?.addItem(obj) this.addedItems.push(obj) } } /** * This source will fetch the needed data from various sources for the given layout. * * Note that special layers (with `source=null` will be ignored) */ class ThemeSourceCore extends FeatureSourceMerger { /** * This source is _only_ triggered when the data is downloaded for CSV export * @private */ private readonly _downloadAll: OverpassFeatureSource private readonly _mapBounds: Store constructor( layers: LayerConfig[], featureSwitches: FeatureSwitchState, mapProperties: { bounds: Store; zoom: Store }, backend: string, isDisplayed: (id: string) => Store, mvtAvailableLayers: Set, isLoading: UIEventSource, fullNodeDatabaseSource?: FullNodeDatabaseSource ) { const { bounds, zoom } = mapProperties // remove all 'special' layers layers = layers.filter((layer) => layer.source !== null && layer.source !== undefined) const geojsonlayers = layers.filter((layer) => layer.source.geojsonSource !== undefined) const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined) const fromCache = new Map() if (featureSwitches.featureSwitchCache.data) { for (const layer of osmLayers) { const src = new LocalStorageFeatureSource( backend, layer, ThemeSource.fromCacheZoomLevel, mapProperties, { isActive: isDisplayed(layer.id), maxAge: layer.maxAgeOfCache, } ) fromCache.set(layer.id, src) } } const mvtSources: UpdatableFeatureSource[] = osmLayers .filter((f) => mvtAvailableLayers.has(f.id)) .map((l) => ThemeSourceCore.setupMvtSource(l, mapProperties, isDisplayed(l.id))) const nonMvtSources: FeatureSource[] = [] const nonMvtLayers: LayerConfig[] = osmLayers.filter((l) => !mvtAvailableLayers.has(l.id)) const osmApiSource = ThemeSourceCore.setupOsmApiSource( osmLayers, bounds, zoom, backend, featureSwitches, fullNodeDatabaseSource ) nonMvtSources.push(osmApiSource) let overpassSource: OverpassFeatureSource = undefined if (nonMvtLayers.length > 0) { console.log( "Layers ", nonMvtLayers.map((l) => l.id), " cannot be fetched from the cache server, defaulting to overpass/OSM-api" ) overpassSource = ThemeSourceCore.setupOverpass(osmLayers, bounds, zoom, featureSwitches) nonMvtSources.push(overpassSource) } function setIsLoading() { const loading = overpassSource?.runningQuery?.data || osmApiSource?.isRunning?.data isLoading.setData(loading) } overpassSource?.runningQuery?.addCallbackAndRun(() => setIsLoading()) osmApiSource?.isRunning?.addCallbackAndRun(() => setIsLoading()) const geojsonSources: UpdatableFeatureSource[] = geojsonlayers.map((l) => ThemeSourceCore.setupGeojsonSource(l, mapProperties, isDisplayed(l.id)) ) const downloadAll = new OverpassFeatureSource( { layers: layers.filter((l) => l.isNormal()), bounds: mapProperties.bounds, zoom: mapProperties.zoom, overpassUrl: featureSwitches.overpassUrl, overpassTimeout: featureSwitches.overpassTimeout, overpassMaxZoom: new ImmutableStore(99), widenFactor: 0, }, { ignoreZoom: true, isActive: new ImmutableStore(false), } ) super( ...geojsonSources, ...Array.from(fromCache.values()), ...mvtSources, ...nonMvtSources, downloadAll ) this._downloadAll = downloadAll this._mapBounds = mapProperties.bounds } private static setupMvtSource( layer: LayerConfig, mapProperties: { zoom: Store; bounds: Store }, isActive?: Store ): UpdatableFeatureSource { return new DynamicMvtileSource(layer, mapProperties, { isActive }) } private static setupGeojsonSource( layer: LayerConfig, mapProperties: { zoom: Store; bounds: Store }, isActiveByFilter?: Store ): UpdatableFeatureSource { const source = layer.source const isActive = mapProperties.zoom.map( (z) => (isActiveByFilter?.data ?? true) && z >= layer.minzoom, [isActiveByFilter] ) 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, fullNodeDatabase: FullNodeDatabaseSource ): OsmFeatureSource | undefined { if (osmLayers.length == 0) { return undefined } 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, patchRelations: true, fullNodeDatabase, }) } private static setupOverpass( osmLayers: LayerConfig[], bounds: Store, zoom: Store, featureSwitches: FeatureSwitchState ): OverpassFeatureSource | undefined { if (osmLayers.length == 0) { return undefined } 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, layers: osmLayers, widenFactor: 1.5, overpassUrl: featureSwitches.overpassUrl, overpassTimeout: featureSwitches.overpassTimeout, overpassMaxZoom: featureSwitches.overpassMaxZoom, }, { padToTiles: zoom.map((zoom) => Math.min(15, zoom + 1)), isActive, } ) } public async downloadAll() { console.log("Downloading all data:") await this._downloadAll.updateAsync(this._mapBounds.data) // await Promise.all(this.supportsForceDownload.map((i) => i.updateAsync())) console.log("Done") } }