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.
|
* 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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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]+/))
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
Loading…
Reference in a new issue