Add caching into local storage for a faster map experience
This commit is contained in:
parent
3a2d654ac3
commit
f33fe081d0
12 changed files with 128 additions and 41 deletions
|
@ -27,21 +27,16 @@ import Img from "./UI/Base/Img";
|
|||
import UserDetails from "./Logic/Osm/OsmConnection";
|
||||
import Attribution from "./UI/BigComponents/Attribution";
|
||||
import MetaTagging from "./Logic/MetaTagging";
|
||||
import FeatureSourceMerger from "./Logic/FeatureSource/FeatureSourceMerger";
|
||||
import RememberingSource from "./Logic/FeatureSource/RememberingSource";
|
||||
import FilteringFeatureSource from "./Logic/FeatureSource/FilteringFeatureSource";
|
||||
import WayHandlingApplyingFeatureSource from "./Logic/FeatureSource/WayHandlingApplyingFeatureSource";
|
||||
import NoOverlapSource from "./Logic/FeatureSource/NoOverlapSource";
|
||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||
import LayerResetter from "./Logic/Actors/LayerResetter";
|
||||
import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs";
|
||||
import LayerControlPanel from "./UI/BigComponents/LayerControlPanel";
|
||||
import FeatureSwitched from "./UI/Base/FeatureSwitched";
|
||||
import FeatureDuplicatorPerLayer from "./Logic/FeatureSource/FeatureDuplicatorPerLayer";
|
||||
import LayerConfig from "./Customizations/JSON/LayerConfig";
|
||||
import ShowDataLayer from "./UI/ShowDataLayer";
|
||||
import Hash from "./Logic/Web/Hash";
|
||||
import HistoryHandling from "./Logic/Actors/HistoryHandling";
|
||||
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
||||
|
||||
export class InitUiElements {
|
||||
|
||||
|
@ -294,7 +289,7 @@ export class InitUiElements {
|
|||
const fullOptions2 = new FullWelcomePaneWithTabs();
|
||||
if (Hash.hash.data === undefined) {
|
||||
State.state.fullScreenMessage.setData({content: fullOptions2, hashText: "welcome"})
|
||||
|
||||
|
||||
}
|
||||
|
||||
Svg.help_svg()
|
||||
|
@ -405,20 +400,9 @@ export class InitUiElements {
|
|||
|
||||
const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap);
|
||||
State.state.layerUpdater = updater;
|
||||
const source = new FeaturePipeline(flayers, updater);
|
||||
|
||||
|
||||
const source =
|
||||
new FilteringFeatureSource(
|
||||
flayers,
|
||||
State.state.locationControl,
|
||||
new FeatureSourceMerger([
|
||||
new RememberingSource(new WayHandlingApplyingFeatureSource(flayers,
|
||||
new NoOverlapSource(flayers, new FeatureDuplicatorPerLayer(flayers, updater))
|
||||
)),
|
||||
new FeatureDuplicatorPerLayer(flayers, State.state.changes)
|
||||
])
|
||||
);
|
||||
|
||||
source.features.addCallback((featuresFreshness: { feature: any, freshness: Date }[]) => {
|
||||
let features = featuresFreshness.map(ff => ff.feature);
|
||||
features.forEach(feature => {
|
||||
|
|
|
@ -130,7 +130,7 @@ export default class UpdateFromOverpass implements FeatureSource{
|
|||
const w = Math.max(-180, bounds.getWest() - diff);
|
||||
const queryBounds = {north: n, east: e, south: s, west: w};
|
||||
|
||||
const z = Math.floor(this._location.data.zoom);
|
||||
const z = Math.floor(this._location.data.zoom ?? 0);
|
||||
|
||||
this.runningQuery.setData(true);
|
||||
const self = this;
|
||||
|
|
48
Logic/FeatureSource/FeaturePipeline.ts
Normal file
48
Logic/FeatureSource/FeaturePipeline.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import FilteringFeatureSource from "../FeatureSource/FilteringFeatureSource";
|
||||
import State from "../../State";
|
||||
import FeatureSourceMerger from "../FeatureSource/FeatureSourceMerger";
|
||||
import RememberingSource from "../FeatureSource/RememberingSource";
|
||||
import WayHandlingApplyingFeatureSource from "../FeatureSource/WayHandlingApplyingFeatureSource";
|
||||
import NoOverlapSource from "../FeatureSource/NoOverlapSource";
|
||||
import FeatureDuplicatorPerLayer from "../FeatureSource/FeatureDuplicatorPerLayer";
|
||||
import FeatureSource from "../FeatureSource/FeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import LocalStorageSaver from "./LocalStorageSaver";
|
||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||
import LocalStorageSource from "./LocalStorageSource";
|
||||
|
||||
export default class FeaturePipeline implements FeatureSource {
|
||||
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
|
||||
constructor(flayers: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[], updater: FeatureSource) {
|
||||
|
||||
const overpassSource = new WayHandlingApplyingFeatureSource(flayers,
|
||||
new NoOverlapSource(flayers, new FeatureDuplicatorPerLayer(flayers, updater))
|
||||
);
|
||||
|
||||
const amendedOverpassSource =
|
||||
new RememberingSource(new LocalStorageSaver(
|
||||
overpassSource
|
||||
));
|
||||
|
||||
const merged = new FeatureSourceMerger([
|
||||
amendedOverpassSource,
|
||||
new FeatureDuplicatorPerLayer(flayers, State.state.changes),
|
||||
new LocalStorageSource()
|
||||
]);
|
||||
merged.features.addCallbackAndRun(feats => console.log("Merged has",feats?.length))
|
||||
|
||||
const source =
|
||||
new FilteringFeatureSource(
|
||||
flayers,
|
||||
State.state.locationControl,
|
||||
merged
|
||||
);
|
||||
source.features.addCallbackAndRun(feats => console.log("Filtered has",feats?.length))
|
||||
|
||||
|
||||
this.features = source.features;
|
||||
}
|
||||
|
||||
}
|
|
@ -9,15 +9,20 @@ export default class FeatureSourceMerger implements FeatureSource {
|
|||
constructor(sources: FeatureSource[]) {
|
||||
this._sources = sources;
|
||||
const self = this;
|
||||
for (const source of sources) {
|
||||
source.features.addCallback(() => self.Update());
|
||||
for (let i = 0; i < sources.length; i++){
|
||||
let source = sources[i];
|
||||
source.features.addCallback(() => {
|
||||
self.Update();
|
||||
});
|
||||
}
|
||||
this.Update();
|
||||
}
|
||||
|
||||
private Update() {
|
||||
let all = {}; // Mapping 'id' -> {feature, freshness}
|
||||
for (const source of this._sources) {
|
||||
if(source?.features?.data === undefined){
|
||||
console.log("Not defined");
|
||||
continue;
|
||||
}
|
||||
for (const f of source.features.data) {
|
||||
|
|
|
@ -74,6 +74,7 @@ export default class FilteringFeatureSource implements FeatureSource {
|
|||
update();
|
||||
});
|
||||
|
||||
update();
|
||||
|
||||
}
|
||||
|
||||
|
|
35
Logic/FeatureSource/LocalStorageSaver.ts
Normal file
35
Logic/FeatureSource/LocalStorageSaver.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/***
|
||||
* Saves all the features that are passed in to localstorage, so they can be retrieved on the next run
|
||||
*
|
||||
* Technically, more an Actor then a featuresource, but it fits more neatly this ay
|
||||
*/
|
||||
import FeatureSource from "./FeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
|
||||
export default class LocalStorageSaver implements FeatureSource {
|
||||
public static readonly storageKey: string = "cached-features";
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
|
||||
constructor(source: FeatureSource) {
|
||||
this.features = source.features;
|
||||
|
||||
this.features.addCallbackAndRun(features => {
|
||||
if (features === undefined) {
|
||||
return;
|
||||
}
|
||||
if(features.length == 0){
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(LocalStorageSaver.storageKey, JSON.stringify(features));
|
||||
} catch (e) {
|
||||
console.warn("Could not save the features to local storage:", e)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
22
Logic/FeatureSource/LocalStorageSource.ts
Normal file
22
Logic/FeatureSource/LocalStorageSource.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import FeatureSource from "./FeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import LocalStorageSaver from "./LocalStorageSaver";
|
||||
|
||||
export default class LocalStorageSource implements FeatureSource {
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
|
||||
constructor() {
|
||||
this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
try {
|
||||
const fromStorage = localStorage.getItem(LocalStorageSaver.storageKey);
|
||||
if (fromStorage == null) {
|
||||
return;
|
||||
}
|
||||
const loaded = JSON.parse(fromStorage);
|
||||
this.features.setData(loaded);
|
||||
} catch (e) {
|
||||
console.log("Could not load features from localStorage:", e)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ import { Utils } from "../Utils";
|
|||
|
||||
export default class Constants {
|
||||
|
||||
public static vNumber = "0.4.8";
|
||||
public static vNumber = "0.4.9";
|
||||
|
||||
// The user journey states thresholds when a new feature gets unlocked
|
||||
public static userJourney = {
|
||||
|
|
21
State.ts
21
State.ts
|
@ -125,11 +125,11 @@ export default class State {
|
|||
this.layoutToUse.setData(layoutToUse);
|
||||
|
||||
const zoom = State.asFloat(
|
||||
QueryParameters.GetQueryParameter("z", "" + layoutToUse?.startZoom, "The initial/current zoom level")
|
||||
QueryParameters.GetQueryParameter("z", "" +(layoutToUse?.startZoom ?? 1), "The initial/current zoom level")
|
||||
.syncWith(LocalStorageSource.Get("zoom")));
|
||||
const lat = State.asFloat(QueryParameters.GetQueryParameter("lat", "" + layoutToUse?.startLat, "The initial/current latitude")
|
||||
const lat = State.asFloat(QueryParameters.GetQueryParameter("lat", "" + (layoutToUse?.startLat ?? 0), "The initial/current latitude")
|
||||
.syncWith(LocalStorageSource.Get("lat")));
|
||||
const lon = State.asFloat(QueryParameters.GetQueryParameter("lon", "" + layoutToUse?.startLon, "The initial/current longitude of the app")
|
||||
const lon = State.asFloat(QueryParameters.GetQueryParameter("lon", "" + (layoutToUse?.startLon ?? 0), "The initial/current longitude of the app")
|
||||
.syncWith(LocalStorageSource.Get("lon")));
|
||||
|
||||
|
||||
|
@ -188,6 +188,9 @@ export default class State {
|
|||
|
||||
const testParam = QueryParameters.GetQueryParameter("test", "false",
|
||||
"If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org").data;
|
||||
|
||||
|
||||
|
||||
this.osmConnection = new OsmConnection(
|
||||
testParam === "true",
|
||||
QueryParameters.GetQueryParameter("oauth_token", undefined,
|
||||
|
@ -245,18 +248,6 @@ export default class State {
|
|||
|
||||
this.allElements = new ElementStorage();
|
||||
this.changes = new Changes();
|
||||
|
||||
if (State.runningFromConsole) {
|
||||
console.warn("running from console - not initializing map. Assuming test.html");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (document.getElementById("leafletDiv") === null) {
|
||||
console.warn("leafletDiv not found - not initializing map. Assuming test.html");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static asFloat(source: UIEventSource<string>): UIEventSource<number> {
|
||||
|
|
|
@ -18,7 +18,7 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
State.state.selectedElement.setData(undefined);
|
||||
}).SetClass("only-on-mobile")
|
||||
.SetClass("featureinfobox-back-to-the-map")
|
||||
title.SetClass("featureinfobox-title")
|
||||
title.SetStyle("width: 100%; display: block;")
|
||||
const ornament = new Combine([new Ornament().SetStyle("height:5em;")]).SetClass("only-on-mobile")
|
||||
|
||||
this._component = new Combine([
|
||||
|
|
|
@ -55,7 +55,6 @@ export default class EditableTagRendering extends UIElement {
|
|||
}
|
||||
if (this._configuration.multiAnswer) {
|
||||
const atLeastOneMatch = this._configuration.mappings.some(mp =>TagUtils.MatchesMultiAnswer(mp.if, this._tags.data));
|
||||
console.log("SOME MATCH?", atLeastOneMatch)
|
||||
if (!atLeastOneMatch) {
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ export default class ShowDataLayer {
|
|||
const mp = leafletMap.data;
|
||||
|
||||
const feats = features.data.map(ff => ff.feature);
|
||||
|
||||
let geoLayer = self.CreateGeojsonLayer(feats)
|
||||
if (layoutToUse.clustering.minNeededElements <= features.data.length) {
|
||||
const cl = window["L"]; // This is a dirty workaround, the clustering plugin binds to the L of the window, not of the namespace or something
|
||||
|
@ -79,6 +80,7 @@ export default class ShowDataLayer {
|
|||
}
|
||||
})
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
|
||||
|
@ -94,7 +96,7 @@ export default class ShowDataLayer {
|
|||
// We have to convert them to the appropriate icon
|
||||
// Click handling is done in the next step
|
||||
|
||||
const tagSource = State.state.allElements.getEventSourceFor(feature);
|
||||
const tagSource = State.state.allElements.addOrGetElement(feature)
|
||||
const layer: LayerConfig = this._layerDict[feature._matching_layer_id];
|
||||
|
||||
const style = layer.GenerateLeafletStyle(tagSource, !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0));
|
||||
|
|
Loading…
Reference in a new issue