forked from MapComplete/MapComplete
		
	Invalidate cache if a point has been deleted or changed geometry. Fix #865; review cache retention times and disable cache for external geojson datasets, fix #1660
This commit is contained in:
		
							parent
							
								
									adaff94dbd
								
							
						
					
					
						commit
						a399260bf0
					
				
					 6 changed files with 68 additions and 39 deletions
				
			
		|  | @ -1,26 +1,12 @@ | |||
| /** | ||||
|  * This actor will download the latest version of the selected element from OSM and update the tags if necessary. | ||||
|  */ | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import { Changes } from "../Osm/Changes" | ||||
| import { OsmConnection } from "../Osm/OsmConnection" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import SimpleMetaTagger from "../SimpleMetaTagger" | ||||
| import { Feature } from "geojson" | ||||
| import { OsmTags } from "../../Models/OsmFeature" | ||||
| import OsmObjectDownloader from "../Osm/OsmObjectDownloader" | ||||
| import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| interface TagsUpdaterState { | ||||
|     selectedElement: UIEventSource<Feature> | ||||
|     featureProperties: { getStore: (id: string) => UIEventSource<Record<string, string>> } | ||||
|     changes: Changes | ||||
|     osmConnection: OsmConnection | ||||
|     layout: LayoutConfig | ||||
|     osmObjectDownloader: OsmObjectDownloader | ||||
|     indexedFeatures: IndexedFeatureSource | ||||
| } | ||||
| import ThemeViewState from "../../Models/ThemeViewState" | ||||
| import { BBox } from "../BBox" | ||||
| import { Feature } from "geojson" | ||||
| 
 | ||||
| export default class SelectedElementTagsUpdater { | ||||
|     private static readonly metatags = new Set([ | ||||
|  | @ -31,19 +17,21 @@ export default class SelectedElementTagsUpdater { | |||
|         "uid", | ||||
|         "id", | ||||
|     ]) | ||||
|     private readonly state: ThemeViewState | ||||
| 
 | ||||
|     constructor(state: TagsUpdaterState) { | ||||
|     constructor(state: ThemeViewState) { | ||||
|         this.state = state | ||||
|         state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { | ||||
|             if (!isLoggedIn && !Utils.runningFromConsole) { | ||||
|                 return | ||||
|             } | ||||
|             this.installCallback(state) | ||||
|             this.installCallback() | ||||
|             // We only have to do this once...
 | ||||
|             return true | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) { | ||||
|     public static applyUpdate(latestTags: OsmTags, id: string, state: ThemeViewState) { | ||||
|         try { | ||||
|             const leftRightSensitive = state.layout.isLeftRightSensitive() | ||||
| 
 | ||||
|  | @ -120,8 +108,13 @@ export default class SelectedElementTagsUpdater { | |||
|             console.error("Updating the tags of selected element ", id, "failed due to", e) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private installCallback(state: TagsUpdaterState) { | ||||
|     private invalidateCache(s: Feature) { | ||||
|         const state = this.state | ||||
|         const wasPartOfLayer = state.layout.getMatchingLayer(s.properties) | ||||
|         state.toCacheSavers.get(wasPartOfLayer.id).invalidateCacheAround(BBox.get(s)) | ||||
|     } | ||||
|     private installCallback() { | ||||
|         const state = this.state | ||||
|         state.selectedElement.addCallbackAndRunD(async (s) => { | ||||
|             let id = s.properties?.id | ||||
|             if (!id) { | ||||
|  | @ -146,9 +139,9 @@ export default class SelectedElementTagsUpdater { | |||
|                 const osmObject = await state.osmObjectDownloader.DownloadObjectAsync(id) | ||||
|                 if (osmObject === "deleted") { | ||||
|                     console.debug("The current selected element has been deleted upstream!", id) | ||||
|                     this.invalidateCache(s) | ||||
|                     const currentTagsSource = state.featureProperties.getStore(id) | ||||
|                     currentTagsSource.data["_deleted"] = "yes" | ||||
|                     currentTagsSource.addCallbackAndRun((tags) => console.trace("Tags are", tags)) | ||||
|                     currentTagsSource.ping() | ||||
|                     return | ||||
|                 } | ||||
|  | @ -158,6 +151,7 @@ export default class SelectedElementTagsUpdater { | |||
|                 const oldGeometry = oldFeature?.geometry | ||||
|                 if (oldGeometry !== undefined && !Utils.SameObject(newGeometry, oldGeometry)) { | ||||
|                     console.log("Detected a difference in geometry for ", id) | ||||
|                     this.invalidateCache(s) | ||||
|                     oldFeature.geometry = newGeometry | ||||
|                     state.featureProperties.getStore(id)?.ping() | ||||
|                 } | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ import { GeoOperations } from "../../GeoOperations" | |||
| import FeaturePropertiesStore from "./FeaturePropertiesStore" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import { Utils } from "../../../Utils" | ||||
| import { Tiles } from "../../../Models/TileRange" | ||||
| import { BBox } from "../../BBox" | ||||
| 
 | ||||
| class SingleTileSaver { | ||||
|     private readonly _storage: UIEventSource<Feature[]> | ||||
|  | @ -54,6 +56,8 @@ class SingleTileSaver { | |||
|  * Also see the sibling class | ||||
|  */ | ||||
| export default class SaveFeatureSourceToLocalStorage { | ||||
|     public readonly storage: TileLocalStorage<Feature[]> | ||||
|     private zoomlevel: number | ||||
|     constructor( | ||||
|         backend: string, | ||||
|         layername: string, | ||||
|  | @ -62,7 +66,9 @@ export default class SaveFeatureSourceToLocalStorage { | |||
|         featureProperties: FeaturePropertiesStore, | ||||
|         maxCacheAge: number | ||||
|     ) { | ||||
|         this.zoomlevel = zoomlevel | ||||
|         const storage = TileLocalStorage.construct<Feature[]>(backend, layername, maxCacheAge) | ||||
|         this.storage = storage | ||||
|         const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>() | ||||
|         features.features.addCallbackAndRunD((features) => { | ||||
|             const sliced = GeoOperations.slice(zoomlevel, features) | ||||
|  | @ -80,4 +86,12 @@ export default class SaveFeatureSourceToLocalStorage { | |||
|             }) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public invalidateCacheAround(bbox: BBox) { | ||||
|         const range = Tiles.tileRangeFrom(bbox, this.zoomlevel) | ||||
|         Tiles.MapRange(range, (x, y) => { | ||||
|             const index = Tiles.tile_index(this.zoomlevel, x, y) | ||||
|             this.storage.invalidate(index) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import { IdbLocalStorage } from "../../Web/IdbLocalStorage" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import { Tiles } from "../../../Models/TileRange" | ||||
| 
 | ||||
| /** | ||||
|  * A class which allows to read/write a tile to local storage. | ||||
|  | @ -91,9 +92,17 @@ export default class TileLocalStorage<T> { | |||
|             await IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex + "_date") | ||||
|         ) | ||||
|         const maxAge = this._maxAgeSeconds | ||||
|         const timeDiff = Date.now() - date | ||||
|         const timeDiff = (Date.now() - date) / 1000 | ||||
|         if (timeDiff >= maxAge) { | ||||
|             console.debug("Dropping cache for", this._layername, tileIndex, "out of date") | ||||
|             console.debug( | ||||
|                 "Dropping cache for", | ||||
|                 this._layername, | ||||
|                 tileIndex, | ||||
|                 "out of date. Max allowed age is", | ||||
|                 maxAge, | ||||
|                 "current age is", | ||||
|                 timeDiff | ||||
|             ) | ||||
|             await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, undefined) | ||||
| 
 | ||||
|             return undefined | ||||
|  | @ -102,7 +111,8 @@ export default class TileLocalStorage<T> { | |||
|         return <any>data | ||||
|     } | ||||
| 
 | ||||
|     invalidate(zoomlevel: number, tileIndex) { | ||||
|     public invalidate(tileIndex: number) { | ||||
|         console.log("Invalidated tile", tileIndex) | ||||
|         this.getTileSource(tileIndex).setData(undefined) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ export default class LayoutSource extends FeatureSourceMerger { | |||
|     private readonly supportsForceDownload: UpdatableFeatureSource[] | ||||
| 
 | ||||
|     private readonly fromCache: Map<string, LocalStorageFeatureSource> | ||||
|     private static readonly fromCacheZoomLevel = 15 | ||||
|     public static readonly fromCacheZoomLevel = 15 | ||||
|     constructor( | ||||
|         layers: LayerConfig[], | ||||
|         featureSwitches: FeatureSwitchState, | ||||
|  |  | |||
|  | @ -27,14 +27,14 @@ export default class LocalStorageFeatureSource extends DynamicTileSource { | |||
|             options?.maxAge ?? 24 * 60 * 60 | ||||
|         ) | ||||
|         super( | ||||
|            new ImmutableStore(zoomlevel), | ||||
|             new ImmutableStore(zoomlevel), | ||||
|             layer.minzoom, | ||||
|             (tileIndex) => | ||||
|                 new StaticFeatureSource( | ||||
|                     storage.getTileSource(tileIndex).mapD((features) => { | ||||
|                         if (features.length === undefined) { | ||||
|                             console.trace("These are not features:", features) | ||||
|                             storage.invalidate(zoomlevel, tileIndex) | ||||
|                             storage.invalidate(tileIndex) | ||||
|                             return [] | ||||
|                         } | ||||
|                         return features.filter((f) => !f.properties.id.match(/(node|way)\/-[0-9]+/)) | ||||
|  |  | |||
|  | @ -146,6 +146,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|      * Triggered by navigating the map with arrows or by pressing 'space' or 'enter' | ||||
|      */ | ||||
|     public readonly visualFeedback: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
|     public readonly toCacheSavers: ReadonlyMap<string, SaveFeatureSourceToLocalStorage> | ||||
| 
 | ||||
|     constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) { | ||||
|         Utils.initDomPurify() | ||||
|  | @ -295,16 +296,6 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|             ) | ||||
|             this.perLayer = perLayer.perLayer | ||||
|         } | ||||
|         this.perLayer.forEach((fs) => { | ||||
|             new SaveFeatureSourceToLocalStorage( | ||||
|                 this.osmConnection.Backend(), | ||||
|                 fs.layer.layerDef.id, | ||||
|                 15, | ||||
|                 fs, | ||||
|                 this.featureProperties, | ||||
|                 fs.layer.layerDef.maxAgeOfCache | ||||
|             ) | ||||
|         }) | ||||
| 
 | ||||
|         this.floors = this.featuresInView.features.stabilized(500).map((features) => { | ||||
|             if (!features) { | ||||
|  | @ -366,6 +357,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         this.favourites = new FavouritesFeatureSource(this) | ||||
| 
 | ||||
|         this.featureSummary = this.setupSummaryLayer() | ||||
|         this.toCacheSavers = this.initSaveToLocalStorage() | ||||
|         this.initActors() | ||||
|         this.drawSpecialLayers() | ||||
|         this.initHotkeys() | ||||
|  | @ -391,6 +383,25 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public initSaveToLocalStorage() { | ||||
|         const toLocalStorage = new Map<string, SaveFeatureSourceToLocalStorage>() | ||||
|         this.perLayer.forEach((fs, layerId) => { | ||||
|             if (fs.layer.layerDef.source.geojsonSource !== undefined) { | ||||
|                 return // We don't cache external data layers
 | ||||
|             } | ||||
|             console.log("Setting up a local store feature sink for", layerId) | ||||
|             const storage = new SaveFeatureSourceToLocalStorage( | ||||
|                 this.osmConnection.Backend(), | ||||
|                 fs.layer.layerDef.id, | ||||
|                 LayoutSource.fromCacheZoomLevel, | ||||
|                 fs, | ||||
|                 this.featureProperties, | ||||
|                 fs.layer.layerDef.maxAgeOfCache | ||||
|             ) | ||||
|             toLocalStorage.set(layerId, storage) | ||||
|         }) | ||||
|         return toLocalStorage | ||||
|     } | ||||
|     public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> { | ||||
|         const filteringFeatureSource = new Map<string, FilteringFeatureSource>() | ||||
|         this.perLayer.forEach((fs, layerName) => { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue