forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			643 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			643 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
 | |
| import FilteringFeatureSource from "./Sources/FilteringFeatureSource"
 | |
| import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter"
 | |
| import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "./FeatureSource"
 | |
| import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource"
 | |
| import { Store, UIEventSource } from "../UIEventSource"
 | |
| import { TileHierarchyTools } from "./TiledFeatureSource/TileHierarchy"
 | |
| import RememberingSource from "./Sources/RememberingSource"
 | |
| import OverpassFeatureSource from "../Actors/OverpassFeatureSource"
 | |
| import GeoJsonSource from "./Sources/GeoJsonSource"
 | |
| import Loc from "../../Models/Loc"
 | |
| import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor"
 | |
| import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor"
 | |
| import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource"
 | |
| import { TileHierarchyMerger } from "./TiledFeatureSource/TileHierarchyMerger"
 | |
| import RelationsTracker from "../Osm/RelationsTracker"
 | |
| import { NewGeometryFromChangesFeatureSource } from "./Sources/NewGeometryFromChangesFeatureSource"
 | |
| import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator"
 | |
| 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"
 | |
| import { GeoOperations } from "../GeoOperations"
 | |
| import { Utils } from "../../Utils"
 | |
| 
 | |
| /**
 | |
|  * The features pipeline ties together a myriad of various datasources:
 | |
|  *
 | |
|  * - The Overpass-API
 | |
|  * - The OSM-API
 | |
|  * - Third-party geojson files, either sliced or directly.
 | |
|  *
 | |
|  * In order to truly understand this class, please have a look at the following diagram: https://cdn-images-1.medium.com/fit/c/800/618/1*qTK1iCtyJUr4zOyw4IFD7A.jpeg
 | |
|  *
 | |
|  *
 | |
|  */
 | |
| export default class FeaturePipeline {
 | |
|     public readonly sufficientlyZoomed: Store<boolean>
 | |
|     public readonly runningQuery: Store<boolean>
 | |
|     public readonly timeout: UIEventSource<number>
 | |
|     public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false)
 | |
|     public readonly newDataLoadedSignal: UIEventSource<FeatureSource> =
 | |
|         new UIEventSource<FeatureSource>(undefined)
 | |
|     public readonly relationTracker: RelationsTracker
 | |
|     /**
 | |
|      * Keeps track of all raw OSM-nodes.
 | |
|      * Only initialized if 'type_node' is defined as layer
 | |
|      */
 | |
|     public readonly fullNodeDatabase?: FullNodeDatabaseSource
 | |
|     private readonly overpassUpdater: OverpassFeatureSource
 | |
|     private state: MapState
 | |
|     private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>
 | |
|     /**
 | |
|      * Keeps track of the age of the loaded data.
 | |
|      * Has one freshness-Calculator for every layer
 | |
|      * @private
 | |
|      */
 | |
|     private readonly freshnesses = new Map<string, TileFreshnessCalculator>()
 | |
|     private readonly oldestAllowedDate: Date
 | |
|     private readonly osmSourceZoomLevel
 | |
|     private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>()
 | |
| 
 | |
|     private readonly newGeometryHandler: NewGeometryFromChangesFeatureSource
 | |
| 
 | |
|     constructor(
 | |
|         handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void,
 | |
|         state: MapState,
 | |
|         options?: {
 | |
|             /*Used for metatagging - will receive all the sources with changeGeometry applied but without filtering*/
 | |
|             handleRawFeatureSource: (source: FeatureSourceForLayer) => void
 | |
|         }
 | |
|     ) {
 | |
|         this.state = state
 | |
| 
 | |
|         const self = this
 | |
|         const expiryInSeconds = Math.min(
 | |
|             ...(state.layoutToUse?.layers?.map((l) => l.maxAgeOfCache) ?? [])
 | |
|         )
 | |
|         this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds)
 | |
|         this.osmSourceZoomLevel = state.osmApiTileSize.data
 | |
|         const useOsmApi = state.locationControl.map(
 | |
|             (l) => l.zoom > (state.overpassMaxZoom.data ?? 12)
 | |
|         )
 | |
|         this.relationTracker = new RelationsTracker()
 | |
| 
 | |
|         state.changes.allChanges.addCallbackAndRun((allChanges) => {
 | |
|             allChanges
 | |
|                 .filter((ch) => ch.id < 0 && ch.changes !== undefined)
 | |
|                 .map((ch) => ch.changes)
 | |
|                 .filter((coor) => coor["lat"] !== undefined && coor["lon"] !== undefined)
 | |
|                 .forEach((coor) => {
 | |
|                     state.layoutToUse.layers.forEach((l) =>
 | |
|                         self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"])
 | |
|                     )
 | |
|                 })
 | |
|         })
 | |
| 
 | |
|         this.sufficientlyZoomed = state.locationControl.map((location) => {
 | |
|             if (location?.zoom === undefined) {
 | |
|                 return false
 | |
|             }
 | |
|             let minzoom = Math.min(
 | |
|                 ...state.filteredLayers.data.map((layer) => layer.layerDef.minzoom ?? 18)
 | |
|             )
 | |
|             return location.zoom >= minzoom
 | |
|         })
 | |
| 
 | |
|         const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed)
 | |
| 
 | |
|         const perLayerHierarchy = new Map<string, TileHierarchyMerger>()
 | |
|         this.perLayerHierarchy = perLayerHierarchy
 | |
| 
 | |
|         // Given a tile, wraps it and passes it on to render (handled by 'handleFeatureSource'
 | |
|         function patchedHandleFeatureSource(
 | |
|             src: FeatureSourceForLayer & IndexedFeatureSource & Tiled
 | |
|         ) {
 | |
|             // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
 | |
|             const withChanges = new ChangeGeometryApplicator(src, state.changes)
 | |
|             const srcFiltered = new FilteringFeatureSource(state, src.tileIndex, withChanges)
 | |
| 
 | |
|             handleFeatureSource(srcFiltered)
 | |
|             if (options?.handleRawFeatureSource) {
 | |
|                 options.handleRawFeatureSource(withChanges)
 | |
|             }
 | |
|             self.somethingLoaded.setData(true)
 | |
|             // We do not mark as visited here, this is the responsability of the code near the actual loader (e.g. overpassLoader and OSMApiFeatureLoader)
 | |
|         }
 | |
| 
 | |
|         function handlePriviligedFeatureSource(src: FeatureSourceForLayer & Tiled) {
 | |
|             // Passthrough to passed function, except that it registers as well
 | |
|             handleFeatureSource(src)
 | |
|             src.features.addCallbackAndRunD((fs) => {
 | |
|                 fs.forEach((ff) => state.allElements.addOrGetElement(ff.feature))
 | |
|             })
 | |
|         }
 | |
| 
 | |
|         for (const filteredLayer of state.filteredLayers.data) {
 | |
|             const id = filteredLayer.layerDef.id
 | |
|             const source = filteredLayer.layerDef.source
 | |
| 
 | |
|             const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) =>
 | |
|                 patchedHandleFeatureSource(tile)
 | |
|             )
 | |
|             perLayerHierarchy.set(id, hierarchy)
 | |
| 
 | |
|             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))
 | |
|                 })
 | |
|                 continue
 | |
|             }
 | |
| 
 | |
|             if (id === "gps_location") {
 | |
|                 handlePriviligedFeatureSource(state.currentUserLocation)
 | |
|                 continue
 | |
|             }
 | |
| 
 | |
|             if (id === "gps_location_history") {
 | |
|                 handlePriviligedFeatureSource(state.historicalUserLocations)
 | |
|                 continue
 | |
|             }
 | |
| 
 | |
|             if (id === "gps_track") {
 | |
|                 handlePriviligedFeatureSource(state.historicalUserLocationsTrack)
 | |
|                 continue
 | |
|             }
 | |
| 
 | |
|             if (id === "home_location") {
 | |
|                 handlePriviligedFeatureSource(state.homeLocation)
 | |
|                 continue
 | |
|             }
 | |
| 
 | |
|             if (id === "current_view") {
 | |
|                 handlePriviligedFeatureSource(state.currentView)
 | |
|                 continue
 | |
|             }
 | |
| 
 | |
|             const localTileSaver = new SaveTileToLocalStorageActor(filteredLayer)
 | |
|             this.localStorageSavers.set(filteredLayer.layerDef.id, localTileSaver)
 | |
| 
 | |
|             if (source.geojsonSource === undefined) {
 | |
|                 // This is an OSM layer
 | |
|                 // We load the cached values and register them
 | |
|                 // Getting data from upstream happens a bit lower
 | |
|                 localTileSaver.LoadTilesFromDisk(
 | |
|                     state.currentBounds,
 | |
|                     state.locationControl,
 | |
|                     (tileIndex, freshness) =>
 | |
|                         self.freshnesses.get(id).addTileLoad(tileIndex, freshness),
 | |
|                     (tile) => {
 | |
|                         console.debug("Loaded tile ", id, tile.tileIndex, "from local cache")
 | |
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
 | |
|                         hierarchy.registerTile(tile)
 | |
|                         tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
 | |
|                     }
 | |
|                 )
 | |
| 
 | |
|                 continue
 | |
|             }
 | |
| 
 | |
|             if (source.geojsonZoomLevel === undefined) {
 | |
|                 // This is a 'load everything at once' geojson layer
 | |
|                 const src = new GeoJsonSource(filteredLayer)
 | |
| 
 | |
|                 if (source.isOsmCacheLayer) {
 | |
|                     // We split them up into tiles anyway as it is an OSM source
 | |
|                     TiledFeatureSource.createHierarchy(src, {
 | |
|                         layer: src.layer,
 | |
|                         minZoomLevel: this.osmSourceZoomLevel,
 | |
|                         noDuplicates: true,
 | |
|                         registerTile: (tile) => {
 | |
|                             new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
 | |
|                             perLayerHierarchy.get(id).registerTile(tile)
 | |
|                             tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
 | |
|                         },
 | |
|                     })
 | |
|                 } else {
 | |
|                     new RegisteringAllFromFeatureSourceActor(src, state.allElements)
 | |
|                     perLayerHierarchy.get(id).registerTile(src)
 | |
|                     src.features.addCallbackAndRunD((_) => self.onNewDataLoaded(src))
 | |
|                 }
 | |
|             } else {
 | |
|                 new DynamicGeoJsonTileSource(
 | |
|                     filteredLayer,
 | |
|                     (tile) => {
 | |
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
 | |
|                         perLayerHierarchy.get(id).registerTile(tile)
 | |
|                         tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
 | |
|                     },
 | |
|                     state
 | |
|                 )
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         const osmFeatureSource = new OsmFeatureSource({
 | |
|             isActive: useOsmApi,
 | |
|             neededTiles: neededTilesFromOsm,
 | |
|             handleTile: (tile) => {
 | |
|                 new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
 | |
|                 if (tile.layer.layerDef.maxAgeOfCache > 0) {
 | |
|                     const saver = self.localStorageSavers.get(tile.layer.layerDef.id)
 | |
|                     if (saver === undefined) {
 | |
|                         console.error(
 | |
|                             "No localStorageSaver found for layer ",
 | |
|                             tile.layer.layerDef.id
 | |
|                         )
 | |
|                     }
 | |
|                     saver?.addTile(tile)
 | |
|                 }
 | |
|                 perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
 | |
|                 tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
 | |
|             },
 | |
|             state: state,
 | |
|             markTileVisited: (tileId) =>
 | |
|                 state.filteredLayers.data.forEach((flayer) => {
 | |
|                     const layer = flayer.layerDef
 | |
|                     if (layer.maxAgeOfCache > 0) {
 | |
|                         const saver = self.localStorageSavers.get(layer.id)
 | |
|                         if (saver === undefined) {
 | |
|                             console.error("No local storage saver found for ", layer.id)
 | |
|                         } else {
 | |
|                             saver.MarkVisited(tileId, new Date())
 | |
|                         }
 | |
|                     }
 | |
|                     self.freshnesses.get(layer.id).addTileLoad(tileId, new Date())
 | |
|                 }),
 | |
|         })
 | |
| 
 | |
|         if (this.fullNodeDatabase !== undefined) {
 | |
|             osmFeatureSource.rawDataHandlers.push((osmJson, tileId) =>
 | |
|                 this.fullNodeDatabase.handleOsmJson(osmJson, tileId)
 | |
|             )
 | |
|         }
 | |
| 
 | |
|         const updater = this.initOverpassUpdater(state, useOsmApi)
 | |
|         this.overpassUpdater = updater
 | |
|         this.timeout = updater.timeout
 | |
| 
 | |
|         // Actually load data from the overpass source
 | |
|         new PerLayerFeatureSourceSplitter(
 | |
|             state.filteredLayers,
 | |
|             (source) =>
 | |
|                 TiledFeatureSource.createHierarchy(source, {
 | |
|                     layer: source.layer,
 | |
|                     minZoomLevel: source.layer.layerDef.minzoom,
 | |
|                     noDuplicates: true,
 | |
|                     maxFeatureCount: state.layoutToUse.clustering.minNeededElements,
 | |
|                     maxZoomLevel: state.layoutToUse.clustering.maxZoom,
 | |
|                     registerTile: (tile) => {
 | |
|                         // We save the tile data for the given layer to local storage - data sourced from overpass
 | |
|                         self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile)
 | |
|                         perLayerHierarchy
 | |
|                             .get(source.layer.layerDef.id)
 | |
|                             .registerTile(new RememberingSource(tile))
 | |
|                         tile.features.addCallbackAndRunD((f) => {
 | |
|                             if (f.length === 0) {
 | |
|                                 return
 | |
|                             }
 | |
|                             self.onNewDataLoaded(tile)
 | |
|                         })
 | |
|                     },
 | |
|                 }),
 | |
|             updater,
 | |
|             {
 | |
|                 handleLeftovers: (leftOvers) => {
 | |
|                     console.warn("Overpass returned a few non-matched features:", leftOvers)
 | |
|                 },
 | |
|             }
 | |
|         )
 | |
| 
 | |
|         // Also load points/lines that are newly added.
 | |
|         const newGeometry = new NewGeometryFromChangesFeatureSource(
 | |
|             state.changes,
 | |
|             state.allElements,
 | |
|             state.osmConnection._oauth_config.url
 | |
|         )
 | |
|         this.newGeometryHandler = newGeometry
 | |
|         newGeometry.features.addCallbackAndRun((geometries) => {
 | |
|             console.debug("New geometries are:", geometries)
 | |
|         })
 | |
| 
 | |
|         new RegisteringAllFromFeatureSourceActor(newGeometry, state.allElements)
 | |
|         // A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next
 | |
|         new PerLayerFeatureSourceSplitter(
 | |
|             state.filteredLayers,
 | |
|             (perLayer) => {
 | |
|                 // We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
 | |
|                 perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer)
 | |
|                 // AT last, we always apply the metatags whenever possible
 | |
|                 perLayer.features.addCallbackAndRunD((_) => {
 | |
|                     self.onNewDataLoaded(perLayer)
 | |
|                 })
 | |
|             },
 | |
|             newGeometry,
 | |
|             {
 | |
|                 handleLeftovers: (leftOvers) => {
 | |
|                     console.warn("Got some leftovers from the filteredLayers: ", leftOvers)
 | |
|                 },
 | |
|             }
 | |
|         )
 | |
| 
 | |
|         this.runningQuery = updater.runningQuery.map(
 | |
|             (overpass) => {
 | |
|                 console.log(
 | |
|                     "FeaturePipeline: runningQuery state changed: Overpass",
 | |
|                     overpass ? "is querying," : "is idle,",
 | |
|                     "osmFeatureSource is",
 | |
|                     osmFeatureSource.isRunning
 | |
|                         ? "is running and needs " +
 | |
|                               neededTilesFromOsm.data?.length +
 | |
|                               " tiles (already got " +
 | |
|                               osmFeatureSource.downloadedTiles.size +
 | |
|                               " tiles )"
 | |
|                         : "is idle"
 | |
|                 )
 | |
|                 return overpass || osmFeatureSource.isRunning.data
 | |
|             },
 | |
|             [osmFeatureSource.isRunning]
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     public GetAllFeaturesWithin(bbox: BBox): OsmFeature[][] {
 | |
|         const self = this
 | |
|         const tiles: OsmFeature[][] = []
 | |
|         Array.from(this.perLayerHierarchy.keys()).forEach((key) => {
 | |
|             const fetched: OsmFeature[][] = self.GetFeaturesWithin(key, bbox)
 | |
|             tiles.push(...fetched)
 | |
|         })
 | |
|         return tiles
 | |
|     }
 | |
| 
 | |
|     public GetAllFeaturesAndMetaWithin(
 | |
|         bbox: BBox,
 | |
|         layerIdWhitelist?: Set<string>
 | |
|     ): { features: OsmFeature[]; layer: string }[] {
 | |
|         const self = this
 | |
|         const tiles: { features: any[]; layer: string }[] = []
 | |
|         Array.from(this.perLayerHierarchy.keys()).forEach((key) => {
 | |
|             if (layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)) {
 | |
|                 return
 | |
|             }
 | |
|             return tiles.push({
 | |
|                 layer: key,
 | |
|                 features: [].concat(...self.GetFeaturesWithin(key, bbox)),
 | |
|             })
 | |
|         })
 | |
|         return tiles
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Gets all the tiles which overlap with the given BBOX.
 | |
|      * This might imply that extra features might be shown
 | |
|      */
 | |
|     public GetFeaturesWithin(layerId: string, bbox: BBox): OsmFeature[][] {
 | |
|         if (layerId === "*") {
 | |
|             return this.GetAllFeaturesWithin(bbox)
 | |
|         }
 | |
|         const requestedHierarchy = this.perLayerHierarchy.get(layerId)
 | |
|         if (requestedHierarchy === undefined) {
 | |
|             console.warn(
 | |
|                 "Layer ",
 | |
|                 layerId,
 | |
|                 "is not defined. Try one of ",
 | |
|                 Array.from(this.perLayerHierarchy.keys())
 | |
|             )
 | |
|             return undefined
 | |
|         }
 | |
|         return TileHierarchyTools.getTiles(requestedHierarchy, bbox)
 | |
|             .filter((featureSource) => featureSource.features?.data !== undefined)
 | |
|             .map((featureSource) => featureSource.features.data.map((fs) => fs.feature))
 | |
|     }
 | |
| 
 | |
|     public GetTilesPerLayerWithin(
 | |
|         bbox: BBox,
 | |
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void
 | |
|     ) {
 | |
|         Array.from(this.perLayerHierarchy.values()).forEach((hierarchy) => {
 | |
|             TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile)
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     private onNewDataLoaded(src: FeatureSource) {
 | |
|         this.newDataLoadedSignal.setData(src)
 | |
|     }
 | |
| 
 | |
|     private freshnessForVisibleLayers(z: number, x: number, y: number): Date {
 | |
|         let oldestDate = undefined
 | |
|         for (const flayer of this.state.filteredLayers.data) {
 | |
|             if (!flayer.isDisplayed.data && !flayer.layerDef.forceLoad) {
 | |
|                 continue
 | |
|             }
 | |
|             if (this.state.locationControl.data.zoom < flayer.layerDef.minzoom) {
 | |
|                 continue
 | |
|             }
 | |
|             if (flayer.layerDef.maxAgeOfCache === 0) {
 | |
|                 return undefined
 | |
|             }
 | |
|             const freshnessCalc = this.freshnesses.get(flayer.layerDef.id)
 | |
|             if (freshnessCalc === undefined) {
 | |
|                 console.warn("No freshness tracker found for ", flayer.layerDef.id)
 | |
|                 return undefined
 | |
|             }
 | |
|             const freshness = freshnessCalc.freshnessFor(z, x, y)
 | |
|             if (freshness === undefined) {
 | |
|                 // SOmething is undefined --> we return undefined as we have to download
 | |
|                 return undefined
 | |
|             }
 | |
|             if (oldestDate === undefined || oldestDate > freshness) {
 | |
|                 oldestDate = freshness
 | |
|             }
 | |
|         }
 | |
|         return oldestDate
 | |
|     }
 | |
| 
 | |
|     /*
 | |
|      * Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM
 | |
|      * */
 | |
|     private getNeededTilesFromOsm(isSufficientlyZoomed: Store<boolean>): Store<number[]> {
 | |
|         const self = this
 | |
|         return this.state.currentBounds.map(
 | |
|             (bbox) => {
 | |
|                 if (bbox === undefined) {
 | |
|                     return []
 | |
|                 }
 | |
|                 if (!isSufficientlyZoomed.data) {
 | |
|                     return []
 | |
|                 }
 | |
|                 const osmSourceZoomLevel = self.osmSourceZoomLevel
 | |
|                 const range = bbox.containingTileRange(osmSourceZoomLevel)
 | |
|                 const tileIndexes = []
 | |
|                 if (range.total >= 100) {
 | |
|                     // Too much tiles!
 | |
|                     return undefined
 | |
|                 }
 | |
|                 Tiles.MapRange(range, (x, y) => {
 | |
|                     const i = Tiles.tile_index(osmSourceZoomLevel, x, y)
 | |
|                     const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y)
 | |
|                     if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) {
 | |
|                         console.debug(
 | |
|                             "Skipping tile",
 | |
|                             osmSourceZoomLevel,
 | |
|                             x,
 | |
|                             y,
 | |
|                             "as a decently fresh one is available"
 | |
|                         )
 | |
|                         // The cached tiles contain decently fresh data
 | |
|                         return undefined
 | |
|                     }
 | |
|                     tileIndexes.push(i)
 | |
|                 })
 | |
|                 return tileIndexes
 | |
|             },
 | |
|             [isSufficientlyZoomed]
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     private initOverpassUpdater(
 | |
|         state: {
 | |
|             allElements: ElementStorage
 | |
|             layoutToUse: LayoutConfig
 | |
|             currentBounds: Store<BBox>
 | |
|             locationControl: Store<Loc>
 | |
|             readonly overpassUrl: Store<string[]>
 | |
|             readonly overpassTimeout: Store<number>
 | |
|             readonly overpassMaxZoom: Store<number>
 | |
|         },
 | |
|         useOsmApi: Store<boolean>
 | |
|     ): OverpassFeatureSource {
 | |
|         const minzoom = Math.min(...state.layoutToUse.layers.map((layer) => layer.minzoom))
 | |
|         const overpassIsActive = state.currentBounds.map(
 | |
|             (bbox) => {
 | |
|                 if (bbox === undefined) {
 | |
|                     console.debug("Disabling overpass source: no bbox")
 | |
|                     return false
 | |
|                 }
 | |
|                 let zoom = state.locationControl.data.zoom
 | |
|                 if (zoom < minzoom) {
 | |
|                     // We are zoomed out over the zoomlevel of any layer
 | |
|                     console.debug("Disabling overpass source: zoom < minzoom")
 | |
|                     return false
 | |
|                 }
 | |
| 
 | |
|                 const range = bbox.containingTileRange(zoom)
 | |
|                 if (range.total >= 5000) {
 | |
|                     // Let's assume we don't have so much data cached
 | |
|                     return true
 | |
|                 }
 | |
|                 const self = this
 | |
|                 const allFreshnesses = Tiles.MapRange(range, (x, y) =>
 | |
|                     self.freshnessForVisibleLayers(zoom, x, y)
 | |
|                 )
 | |
|                 return allFreshnesses.some(
 | |
|                     (freshness) => freshness === undefined || freshness < this.oldestAllowedDate
 | |
|                 )
 | |
|             },
 | |
|             [state.locationControl]
 | |
|         )
 | |
| 
 | |
|         const self = this
 | |
|         const updater = 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
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Builds upon 'GetAllFeaturesAndMetaWithin', but does stricter BBOX-checking and applies the filters
 | |
|      */
 | |
|     public getAllVisibleElementsWithmeta(
 | |
|         bbox: BBox
 | |
|     ): { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] {
 | |
|         if (bbox === undefined) {
 | |
|             console.warn("No bbox")
 | |
|             return []
 | |
|         }
 | |
| 
 | |
|         const layers = Utils.toIdRecord(this.state.layoutToUse.layers)
 | |
|         const elementsWithMeta: { features: OsmFeature[]; layer: string }[] =
 | |
|             this.GetAllFeaturesAndMetaWithin(bbox)
 | |
| 
 | |
|         let elements: { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] = []
 | |
|         let seenElements = new Set<string>()
 | |
|         for (const elementsWithMetaElement of elementsWithMeta) {
 | |
|             const layer = layers[elementsWithMetaElement.layer]
 | |
|             if (layer.title === undefined) {
 | |
|                 continue
 | |
|             }
 | |
|             const filtered = this.state.filteredLayers.data.find((fl) => fl.layerDef == layer)
 | |
|             for (let i = 0; i < elementsWithMetaElement.features.length; i++) {
 | |
|                 const element = elementsWithMetaElement.features[i]
 | |
|                 if (!filtered.isDisplayed.data) {
 | |
|                     continue
 | |
|                 }
 | |
|                 if (seenElements.has(element.properties.id)) {
 | |
|                     continue
 | |
|                 }
 | |
|                 seenElements.add(element.properties.id)
 | |
|                 if (!bbox.overlapsWith(BBox.get(element))) {
 | |
|                     continue
 | |
|                 }
 | |
|                 if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) {
 | |
|                     continue
 | |
|                 }
 | |
|                 const activeFilters: FilterState[] = Array.from(
 | |
|                     filtered.appliedFilters.data.values()
 | |
|                 )
 | |
|                 if (
 | |
|                     !activeFilters.every(
 | |
|                         (filter) =>
 | |
|                             filter?.currentFilter === undefined ||
 | |
|                             filter?.currentFilter?.matchesProperties(element.properties)
 | |
|                     )
 | |
|                 ) {
 | |
|                     continue
 | |
|                 }
 | |
|                 const center = GeoOperations.centerpointCoordinates(element)
 | |
|                 elements.push({
 | |
|                     element,
 | |
|                     center,
 | |
|                     layer: layers[elementsWithMetaElement.layer],
 | |
|                 })
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return elements
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Inject a new point
 | |
|      */
 | |
|     InjectNewPoint(geojson) {
 | |
|         this.newGeometryHandler.features.data.push({
 | |
|             feature: geojson,
 | |
|             freshness: new Date(),
 | |
|         })
 | |
|         this.newGeometryHandler.features.ping()
 | |
|     }
 | |
| }
 |