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:
Pieter Vander Vennet 2024-02-26 15:52:40 +01:00
parent adaff94dbd
commit a399260bf0
6 changed files with 68 additions and 39 deletions

View file

@ -1,26 +1,12 @@
/** /**
* This actor will download the latest version of the selected element from OSM and update the tags if necessary. * 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 SimpleMetaTagger from "../SimpleMetaTagger"
import { Feature } from "geojson"
import { OsmTags } from "../../Models/OsmFeature" import { OsmTags } from "../../Models/OsmFeature"
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import ThemeViewState from "../../Models/ThemeViewState"
interface TagsUpdaterState { import { BBox } from "../BBox"
selectedElement: UIEventSource<Feature> import { Feature } from "geojson"
featureProperties: { getStore: (id: string) => UIEventSource<Record<string, string>> }
changes: Changes
osmConnection: OsmConnection
layout: LayoutConfig
osmObjectDownloader: OsmObjectDownloader
indexedFeatures: IndexedFeatureSource
}
export default class SelectedElementTagsUpdater { export default class SelectedElementTagsUpdater {
private static readonly metatags = new Set([ private static readonly metatags = new Set([
@ -31,19 +17,21 @@ export default class SelectedElementTagsUpdater {
"uid", "uid",
"id", "id",
]) ])
private readonly state: ThemeViewState
constructor(state: TagsUpdaterState) { constructor(state: ThemeViewState) {
this.state = state
state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => {
if (!isLoggedIn && !Utils.runningFromConsole) { if (!isLoggedIn && !Utils.runningFromConsole) {
return return
} }
this.installCallback(state) this.installCallback()
// We only have to do this once... // We only have to do this once...
return true return true
}) })
} }
public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) { public static applyUpdate(latestTags: OsmTags, id: string, state: ThemeViewState) {
try { try {
const leftRightSensitive = state.layout.isLeftRightSensitive() 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) console.error("Updating the tags of selected element ", id, "failed due to", e)
} }
} }
private invalidateCache(s: Feature) {
private installCallback(state: TagsUpdaterState) { 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) => { state.selectedElement.addCallbackAndRunD(async (s) => {
let id = s.properties?.id let id = s.properties?.id
if (!id) { if (!id) {
@ -146,9 +139,9 @@ export default class SelectedElementTagsUpdater {
const osmObject = await state.osmObjectDownloader.DownloadObjectAsync(id) const osmObject = await state.osmObjectDownloader.DownloadObjectAsync(id)
if (osmObject === "deleted") { if (osmObject === "deleted") {
console.debug("The current selected element has been deleted upstream!", id) console.debug("The current selected element has been deleted upstream!", id)
this.invalidateCache(s)
const currentTagsSource = state.featureProperties.getStore(id) const currentTagsSource = state.featureProperties.getStore(id)
currentTagsSource.data["_deleted"] = "yes" currentTagsSource.data["_deleted"] = "yes"
currentTagsSource.addCallbackAndRun((tags) => console.trace("Tags are", tags))
currentTagsSource.ping() currentTagsSource.ping()
return return
} }
@ -158,6 +151,7 @@ export default class SelectedElementTagsUpdater {
const oldGeometry = oldFeature?.geometry const oldGeometry = oldFeature?.geometry
if (oldGeometry !== undefined && !Utils.SameObject(newGeometry, oldGeometry)) { if (oldGeometry !== undefined && !Utils.SameObject(newGeometry, oldGeometry)) {
console.log("Detected a difference in geometry for ", id) console.log("Detected a difference in geometry for ", id)
this.invalidateCache(s)
oldFeature.geometry = newGeometry oldFeature.geometry = newGeometry
state.featureProperties.getStore(id)?.ping() state.featureProperties.getStore(id)?.ping()
} }

View file

@ -5,6 +5,8 @@ import { GeoOperations } from "../../GeoOperations"
import FeaturePropertiesStore from "./FeaturePropertiesStore" import FeaturePropertiesStore from "./FeaturePropertiesStore"
import { UIEventSource } from "../../UIEventSource" import { UIEventSource } from "../../UIEventSource"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import { Tiles } from "../../../Models/TileRange"
import { BBox } from "../../BBox"
class SingleTileSaver { class SingleTileSaver {
private readonly _storage: UIEventSource<Feature[]> private readonly _storage: UIEventSource<Feature[]>
@ -54,6 +56,8 @@ class SingleTileSaver {
* Also see the sibling class * Also see the sibling class
*/ */
export default class SaveFeatureSourceToLocalStorage { export default class SaveFeatureSourceToLocalStorage {
public readonly storage: TileLocalStorage<Feature[]>
private zoomlevel: number
constructor( constructor(
backend: string, backend: string,
layername: string, layername: string,
@ -62,7 +66,9 @@ export default class SaveFeatureSourceToLocalStorage {
featureProperties: FeaturePropertiesStore, featureProperties: FeaturePropertiesStore,
maxCacheAge: number maxCacheAge: number
) { ) {
this.zoomlevel = zoomlevel
const storage = TileLocalStorage.construct<Feature[]>(backend, layername, maxCacheAge) const storage = TileLocalStorage.construct<Feature[]>(backend, layername, maxCacheAge)
this.storage = storage
const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>() const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>()
features.features.addCallbackAndRunD((features) => { features.features.addCallbackAndRunD((features) => {
const sliced = GeoOperations.slice(zoomlevel, 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)
})
}
} }

View file

@ -1,5 +1,6 @@
import { IdbLocalStorage } from "../../Web/IdbLocalStorage" import { IdbLocalStorage } from "../../Web/IdbLocalStorage"
import { UIEventSource } from "../../UIEventSource" import { UIEventSource } from "../../UIEventSource"
import { Tiles } from "../../../Models/TileRange"
/** /**
* A class which allows to read/write a tile to local storage. * 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") await IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex + "_date")
) )
const maxAge = this._maxAgeSeconds const maxAge = this._maxAgeSeconds
const timeDiff = Date.now() - date const timeDiff = (Date.now() - date) / 1000
if (timeDiff >= maxAge) { 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) await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, undefined)
return undefined return undefined
@ -102,7 +111,8 @@ export default class TileLocalStorage<T> {
return <any>data return <any>data
} }
invalidate(zoomlevel: number, tileIndex) { public invalidate(tileIndex: number) {
console.log("Invalidated tile", tileIndex)
this.getTileSource(tileIndex).setData(undefined) this.getTileSource(tileIndex).setData(undefined)
} }
} }

View file

@ -27,7 +27,7 @@ export default class LayoutSource extends FeatureSourceMerger {
private readonly supportsForceDownload: UpdatableFeatureSource[] private readonly supportsForceDownload: UpdatableFeatureSource[]
private readonly fromCache: Map<string, LocalStorageFeatureSource> private readonly fromCache: Map<string, LocalStorageFeatureSource>
private static readonly fromCacheZoomLevel = 15 public static readonly fromCacheZoomLevel = 15
constructor( constructor(
layers: LayerConfig[], layers: LayerConfig[],
featureSwitches: FeatureSwitchState, featureSwitches: FeatureSwitchState,

View file

@ -27,14 +27,14 @@ export default class LocalStorageFeatureSource extends DynamicTileSource {
options?.maxAge ?? 24 * 60 * 60 options?.maxAge ?? 24 * 60 * 60
) )
super( super(
new ImmutableStore(zoomlevel), new ImmutableStore(zoomlevel),
layer.minzoom, layer.minzoom,
(tileIndex) => (tileIndex) =>
new StaticFeatureSource( new StaticFeatureSource(
storage.getTileSource(tileIndex).mapD((features) => { storage.getTileSource(tileIndex).mapD((features) => {
if (features.length === undefined) { if (features.length === undefined) {
console.trace("These are not features:", features) console.trace("These are not features:", features)
storage.invalidate(zoomlevel, tileIndex) storage.invalidate(tileIndex)
return [] return []
} }
return features.filter((f) => !f.properties.id.match(/(node|way)\/-[0-9]+/)) return features.filter((f) => !f.properties.id.match(/(node|way)\/-[0-9]+/))

View file

@ -146,6 +146,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
* Triggered by navigating the map with arrows or by pressing 'space' or 'enter' * Triggered by navigating the map with arrows or by pressing 'space' or 'enter'
*/ */
public readonly visualFeedback: UIEventSource<boolean> = new UIEventSource<boolean>(false) public readonly visualFeedback: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public readonly toCacheSavers: ReadonlyMap<string, SaveFeatureSourceToLocalStorage>
constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) { constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) {
Utils.initDomPurify() Utils.initDomPurify()
@ -295,16 +296,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
) )
this.perLayer = perLayer.perLayer 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) => { this.floors = this.featuresInView.features.stabilized(500).map((features) => {
if (!features) { if (!features) {
@ -366,6 +357,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.favourites = new FavouritesFeatureSource(this) this.favourites = new FavouritesFeatureSource(this)
this.featureSummary = this.setupSummaryLayer() this.featureSummary = this.setupSummaryLayer()
this.toCacheSavers = this.initSaveToLocalStorage()
this.initActors() this.initActors()
this.drawSpecialLayers() this.drawSpecialLayers()
this.initHotkeys() 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> { public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> {
const filteringFeatureSource = new Map<string, FilteringFeatureSource>() const filteringFeatureSource = new Map<string, FilteringFeatureSource>()
this.perLayer.forEach((fs, layerName) => { this.perLayer.forEach((fs, layerName) => {