From 1e56eb550373f5f5e1233c44741954435d4d109e Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 15 Jun 2023 02:42:12 +0200 Subject: [PATCH] Usertests: add 'centermessage' again which indicates if data is loading or is present, see #1457 --- Logic/Actors/NoElementsInViewDetector.ts | 90 +++ Logic/FeatureSource/Sources/LayoutSource.ts | 2 +- Logic/UIEventSource.ts | 1 - Models/ThemeConfig/LayoutConfig.ts | 11 + Models/ThemeViewState.ts | 69 +- UI/BigComponents/StateIndicator.svelte | 40 ++ UI/ThemeViewGUI.svelte | 702 ++++++++++---------- langs/en.json | 4 +- 8 files changed, 544 insertions(+), 375 deletions(-) create mode 100644 Logic/Actors/NoElementsInViewDetector.ts create mode 100644 UI/BigComponents/StateIndicator.svelte diff --git a/Logic/Actors/NoElementsInViewDetector.ts b/Logic/Actors/NoElementsInViewDetector.ts new file mode 100644 index 000000000..8721348f0 --- /dev/null +++ b/Logic/Actors/NoElementsInViewDetector.ts @@ -0,0 +1,90 @@ +import { BBox } from "../BBox" +import { Store } from "../UIEventSource" +import ThemeViewState from "../../Models/ThemeViewState" +import Constants from "../../Models/Constants" + +export type FeatureViewState = + | "no-data" + | "zoom-to-low" + | "has-visible-feature" + | "all-filtered-away" +export default class NoElementsInViewDetector { + public readonly hasFeatureInView: Store + + constructor(themeViewState: ThemeViewState) { + const state = themeViewState + const minZoom = Math.min( + ...themeViewState.layout.layers + .filter((l) => Constants.priviliged_layers.indexOf(l.id) < 0) + .map((l) => l.minzoom) + ) + const mapProperties = themeViewState.mapProperties + + const priviliged: Set = new Set(Constants.priviliged_layers) + + this.hasFeatureInView = state.mapProperties.bounds.stabilized(100).map( + (bbox) => { + if (mapProperties.zoom.data < minZoom) { + // Not a single layer will display anything as the zoom is to low + return "zoom-to-low" + } + + for (const [layerName, source] of themeViewState.perLayerFiltered) { + if (priviliged.has(layerName)) { + continue + } + if ( + mapProperties.zoom.data < themeViewState.layout.getLayer(layerName).minzoom + ) { + continue + } + if (!state.layerState.filteredLayers.get(layerName).isDisplayed.data) { + continue + } + const feats = source.features.data + if (!(feats?.length > 0)) { + // Nope, no data loaded + continue + } + + for (const feat of feats) { + if (BBox.get(feat).overlapsWith(bbox)) { + // We found at least one item which has visible data + return "has-visible-feature" + } + } + } + + // If we arrive here, data might have been filtered away + for (const [layerName, source] of themeViewState.perLayer) { + if (priviliged.has(layerName)) { + continue + } + if ( + mapProperties.zoom.data < themeViewState.layout.getLayer(layerName).minzoom + ) { + continue + } + const feats = source.features.data + if (!(feats?.length > 0)) { + // Nope, no data loaded + continue + } + + for (const feat of feats) { + if (BBox.get(feat).overlapsWith(bbox)) { + // We found at least one item, but as we didn't find it before, it is filtered away + return "all-filtered-away" + } + } + } + return "no-data" + }, + [ + ...Array.from(themeViewState.perLayerFiltered.values()).map((f) => f.features), + mapProperties.zoom, + ...Array.from(state.layerState.filteredLayers.values()).map((fl) => fl.isDisplayed), + ] + ) + } +} diff --git a/Logic/FeatureSource/Sources/LayoutSource.ts b/Logic/FeatureSource/Sources/LayoutSource.ts index 899b50f15..c8e73d217 100644 --- a/Logic/FeatureSource/Sources/LayoutSource.ts +++ b/Logic/FeatureSource/Sources/LayoutSource.ts @@ -69,7 +69,7 @@ export default class LayoutSource extends FeatureSourceMerger { const self = this function setIsLoading() { - const loading = overpassSource?.runningQuery?.data && osmApiSource?.isRunning?.data + const loading = overpassSource?.runningQuery?.data || osmApiSource?.isRunning?.data self._isLoading.setData(loading) } diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index 50543fb83..dd1171caa 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -384,7 +384,6 @@ class ListenerTracker { /** * The mapped store is a helper type which does the mapping of a function. - * It'll fuse */ class MappedStore extends Store { private readonly _upstream: Store diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index be5cda3ec..9781734dd 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -70,6 +70,8 @@ export default class LayoutConfig implements LayoutInformation { public readonly definedAtUrl?: string public readonly definitionRaw?: string + private readonly layersDict: Map + constructor( json: LayoutConfigJson, official = true, @@ -209,6 +211,11 @@ export default class LayoutConfig implements LayoutInformation { this.overpassTimeout = json.overpassTimeout ?? 30 this.overpassMaxZoom = json.overpassMaxZoom ?? 16 this.osmApiTileSize = json.osmApiTileSize ?? this.overpassMaxZoom + 1 + + this.layersDict = new Map() + for (const layer of this.layers) { + this.layersDict.set(layer.id, layer) + } } public CustomCodeSnippets(): string[] { @@ -228,6 +235,10 @@ export default class LayoutConfig implements LayoutInformation { return custom } + public getLayer(id: string) { + return this.layersDict.get(id) + } + public isLeftRightSensitive() { return this.layers.some((l) => l.isLeftRightSensitive()) } diff --git a/Models/ThemeViewState.ts b/Models/ThemeViewState.ts index f4d0d7e16..7cd7ca1b5 100644 --- a/Models/ThemeViewState.ts +++ b/Models/ThemeViewState.ts @@ -50,6 +50,9 @@ import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter" import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor" +import NoElementsInViewDetector, { + FeatureViewState, +} from "../Logic/Actors/NoElementsInViewDetector" /** * @@ -75,8 +78,13 @@ export default class ThemeViewState implements SpecialVisualizationState { readonly osmObjectDownloader: OsmObjectDownloader readonly dataIsLoading: Store + /** + * Indicates if there is _some_ data in view, even if it is not shown due to the filters + */ + readonly hasDataInView: Store + readonly guistate: MenuState - readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO + readonly fullNodeDatabase?: FullNodeDatabaseSource readonly historicalUserLocations: WritableFeatureSource> readonly indexedFeatures: IndexedFeatureSource & LayoutSource @@ -85,6 +93,8 @@ export default class ThemeViewState implements SpecialVisualizationState { readonly newFeatures: WritableFeatureSource readonly layerState: LayerState readonly perLayer: ReadonlyMap + readonly perLayerFiltered: ReadonlyMap + readonly availableLayers: Store readonly selectedLayer: UIEventSource readonly userRelatedState: UserRelatedState @@ -175,6 +185,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.fullNodeDatabase ) this.indexedFeatures = layoutSource + const empty = [] let currentViewIndex = 0 this.currentView = new StaticFeatureSource( @@ -194,6 +205,9 @@ export default class ThemeViewState implements SpecialVisualizationState { ) this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) this.dataIsLoading = layoutSource.isLoading + this.dataIsLoading.addCallbackAndRunD((loading) => + console.log("Data is loading?", loading) + ) const indexedElements = this.indexedFeatures this.featureProperties = new FeaturePropertiesStore(indexedElements) @@ -288,7 +302,10 @@ export default class ThemeViewState implements SpecialVisualizationState { this.changes ) - this.showNormalDataOn(this.map) + this.perLayerFiltered = this.showNormalDataOn(this.map) + + this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView + this.initActors() this.addLastClick(lastClick) this.drawSpecialLayers() @@ -299,8 +316,9 @@ export default class ThemeViewState implements SpecialVisualizationState { } } - public showNormalDataOn(map: Store) { - this.perLayer.forEach((fs) => { + public showNormalDataOn(map: Store): ReadonlyMap { + const filteringFeatureSource = new Map() + this.perLayer.forEach((fs, layerName) => { const doShowLayer = this.mapProperties.zoom.map( (z) => (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), @@ -323,6 +341,7 @@ export default class ThemeViewState implements SpecialVisualizationState { (id) => this.featureProperties.getStore(id), this.layerState.globalFilters ) + filteringFeatureSource.set(layerName, filtered) new ShowDataLayer(map, { layer: fs.layer.layerDef, @@ -333,6 +352,7 @@ export default class ThemeViewState implements SpecialVisualizationState { fetchStore: (id) => this.featureProperties.getStore(id), }) }) + return filteringFeatureSource } /** @@ -533,28 +553,25 @@ export default class ThemeViewState implements SpecialVisualizationState { * Setup various services for which no reference are needed */ private initActors() { - { - // Unselect the selected element if it is panned out of view - this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => { - const selected = this.selectedElement.data - if (selected === undefined) { - return - } - const bbox = BBox.get(selected) - if (!bbox.overlapsWith(bounds)) { - this.selectedElement.setData(undefined) - } - }) - } - { - this.selectedElement.addCallback((selected) => { - if (selected === undefined) { - // We did _unselect_ an item - we always remove the lastclick-object - this.lastClickObject.features.setData([]) - this.selectedLayer.setData(undefined) - } - }) - } + // Unselect the selected element if it is panned out of view + this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => { + const selected = this.selectedElement.data + if (selected === undefined) { + return + } + const bbox = BBox.get(selected) + if (!bbox.overlapsWith(bounds)) { + this.selectedElement.setData(undefined) + } + }) + + this.selectedElement.addCallback((selected) => { + if (selected === undefined) { + // We did _unselect_ an item - we always remove the lastclick-object + this.lastClickObject.features.setData([]) + this.selectedLayer.setData(undefined) + } + }) new ThemeViewStateHashActor(this) new MetaTagging(this) new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this) diff --git a/UI/BigComponents/StateIndicator.svelte b/UI/BigComponents/StateIndicator.svelte new file mode 100644 index 000000000..0c37aa7bc --- /dev/null +++ b/UI/BigComponents/StateIndicator.svelte @@ -0,0 +1,40 @@ + +{#if $currentState === "has-visible-features"} + +{:else if $currentState === "zoom-to-low"} +
+ +
+{:else if $currentState === "all-filtered-away"} +
+ +
+{:else if $dataIsLoading} +
+ + + +
+{:else if $currentState === "no-data"} +
+ +
+{/if} diff --git a/UI/ThemeViewGUI.svelte b/UI/ThemeViewGUI.svelte index 9635f5f92..a05b7e8c2 100644 --- a/UI/ThemeViewGUI.svelte +++ b/UI/ThemeViewGUI.svelte @@ -1,430 +1,440 @@
- +
+
- - -
- -
-
-
- state.guistate.themeIsOpened.setData(true)}> -
- - - - -
-
- state.guistate.menuIsOpened.setData(true)}> - - - {#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()} - { + + +
+ +
+
+
+ state.guistate.themeIsOpened.setData(true)}> +
+ + + + +
+
+ state.guistate.menuIsOpened.setData(true)}> + + + {#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()} + { selectedLayer.setData(currentViewLayer) selectedElement.setData(state.currentView.features?.data?.[0]) }} - > + > + currentViewLayer.defaultIcon().SetClass("w-8 h-8 cursor-pointer")} + /> + + {/if} currentViewLayer.defaultIcon().SetClass("w-8 h-8 cursor-pointer")} + construct={() => new ExtraLinkButton(state, layout.extraLink).SetClass("pointer-events-auto")} /> - - {/if} - new ExtraLinkButton(state, layout.extraLink).SetClass("pointer-events-auto")} - /> - -
Testmode
-
-
+ +
Testmode
+
+
+
+ + +
-
-
- - - { + +
+ - -
- - f.length > 1)}> -
- + > + © OpenStreetMap, {rasterLayerName} +
-
- mapproperties.zoom.update((z) => z + 1)}> - - - mapproperties.zoom.update((z) => z - 1)}> - - - - - + + f.length > 1)}> +
+ +
+
+ mapproperties.zoom.update((z) => z + 1)}> + + + mapproperties.zoom.update((z) => z - 1)}> + + + + + - - + /> +
+
+
-
v !== undefined && selectedLayer.data !== undefined && !selectedLayer.data.popupInFloatover, [selectedLayer] )} > - { + + { selectedElement.setData(undefined) }} - > -
- - - - - - -
-
+ > +
+ + + + + + +
+
v !== undefined && selectedLayer.data !== undefined && selectedLayer.data.popupInFloatover, [selectedLayer] )} > - { + + { selectedElement.setData(undefined) }} - > - - + > + + - - - - -
- state.guistate.themeIsOpened.setData(false)} - /> -
+ + + + +
+ state.guistate.themeIsOpened.setData(false)} + /> +
-
- - -
+
+ + +
-
- -
+
+ +
-
- - - - -
+
+ + + + +
-
- {#each layout.layers as layer} - - {/each} - {#each layout.tileLayerSources as tilesource} - - {/each} -
-
- - - - -
-
- -
+
+ {#each layout.layers as layer} + + {/each} + {#each layout.tileLayerSources as tilesource} + + {/each} +
+
+ + + + +
+
+ +
-
- -
+
+ +
- new CopyrightPanel(state)} slot="content3" /> + new CopyrightPanel(state)} slot="content3"/> -
- -
-
- new ShareScreen(state)} /> -
-
-
+
+ +
+
+ new ShareScreen(state)}/> +
+
+
- - state.guistate.backgroundLayerSelectionIsOpened.setData(false)}> -
- -
-
+ + state.guistate.backgroundLayerSelectionIsOpened.setData(false)}> +
+ +
+
- - - - -
- state.guistate.menuIsOpened.setData(false)} - /> -
-
- -
+ + + + +
+ state.guistate.menuIsOpened.setData(false)} + /> +
+
+ +
- -
- - -
+
+ + +
- + {state} + tags={state.userRelatedState.preferencesAsTags} + /> + +
-
- - Get in touch with others -
-
- -
+
+ + Get in touch with others +
+
+ +
-
- - -
-
- new PrivacyPolicy()} /> -
+
+ + +
+
+ new PrivacyPolicy()}/> +
- -
- - + +
+ + new OpenJosm(state.osmConnection, state.mapProperties.bounds).SetClass("w-full")} - /> - - -
- - + /> + + +
+ + diff --git a/langs/en.json b/langs/en.json index 3bc3f4b96..fa8189a65 100644 --- a/langs/en.json +++ b/langs/en.json @@ -3,7 +3,9 @@ "title": "Advanced features" }, "centerMessage": { + "allFilteredAway": "No feature in view meets all filters", "loadingData": "Loading data…", + "noData": "There are no relevant features in the current view", "ready": "Done!", "retrying": "Loading data failed. Trying again in {count} seconds…", "zoomIn": "Zoom in to view or edit the data" @@ -400,7 +402,7 @@ "geolocate": "Pan the map to the current location or zoom the map to the current location. Requests geopermission", "intro": "MapComplete supports the following keys:", "key": "Key combination", - "openLayersPanel": "Opens the Background, layers and filters panel", + "openLayersPanel": "Opens the layers and filters panel", "selectAerial": "Set the background to aerial or satellite imagery. Toggles between the two best, available layers", "selectMap": "Set the background to a map from external sources. Toggles between the two best, available layers", "selectMapnik": "Set the background layer to OpenStreetMap-carto",