Usertests: add 'centermessage' again which indicates if data is loading or is present, see #1457

This commit is contained in:
Pieter Vander Vennet 2023-06-15 02:42:12 +02:00
parent a55b84dba1
commit 1e56eb5503
8 changed files with 544 additions and 375 deletions

View file

@ -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<FeatureViewState>
constructor(themeViewState: ThemeViewState) {
const state = themeViewState
const minZoom = Math.min(
...themeViewState.layout.layers
.filter((l) => Constants.priviliged_layers.indexOf(<any>l.id) < 0)
.map((l) => l.minzoom)
)
const mapProperties = themeViewState.mapProperties
const priviliged: Set<string> = 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),
]
)
}
}

View file

@ -69,7 +69,7 @@ export default class LayoutSource extends FeatureSourceMerger {
const self = this const self = this
function setIsLoading() { function setIsLoading() {
const loading = overpassSource?.runningQuery?.data && osmApiSource?.isRunning?.data const loading = overpassSource?.runningQuery?.data || osmApiSource?.isRunning?.data
self._isLoading.setData(loading) self._isLoading.setData(loading)
} }

View file

@ -384,7 +384,6 @@ class ListenerTracker<T> {
/** /**
* The mapped store is a helper type which does the mapping of a function. * The mapped store is a helper type which does the mapping of a function.
* It'll fuse
*/ */
class MappedStore<TIn, T> extends Store<T> { class MappedStore<TIn, T> extends Store<T> {
private readonly _upstream: Store<TIn> private readonly _upstream: Store<TIn>

View file

@ -70,6 +70,8 @@ export default class LayoutConfig implements LayoutInformation {
public readonly definedAtUrl?: string public readonly definedAtUrl?: string
public readonly definitionRaw?: string public readonly definitionRaw?: string
private readonly layersDict: Map<string, LayerConfig>
constructor( constructor(
json: LayoutConfigJson, json: LayoutConfigJson,
official = true, official = true,
@ -209,6 +211,11 @@ export default class LayoutConfig implements LayoutInformation {
this.overpassTimeout = json.overpassTimeout ?? 30 this.overpassTimeout = json.overpassTimeout ?? 30
this.overpassMaxZoom = json.overpassMaxZoom ?? 16 this.overpassMaxZoom = json.overpassMaxZoom ?? 16
this.osmApiTileSize = json.osmApiTileSize ?? this.overpassMaxZoom + 1 this.osmApiTileSize = json.osmApiTileSize ?? this.overpassMaxZoom + 1
this.layersDict = new Map<string, LayerConfig>()
for (const layer of this.layers) {
this.layersDict.set(layer.id, layer)
}
} }
public CustomCodeSnippets(): string[] { public CustomCodeSnippets(): string[] {
@ -228,6 +235,10 @@ export default class LayoutConfig implements LayoutInformation {
return custom return custom
} }
public getLayer(id: string) {
return this.layersDict.get(id)
}
public isLeftRightSensitive() { public isLeftRightSensitive() {
return this.layers.some((l) => l.isLeftRightSensitive()) return this.layers.some((l) => l.isLeftRightSensitive())
} }

View file

@ -50,6 +50,9 @@ import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor" 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 osmObjectDownloader: OsmObjectDownloader
readonly dataIsLoading: Store<boolean> readonly dataIsLoading: Store<boolean>
/**
* Indicates if there is _some_ data in view, even if it is not shown due to the filters
*/
readonly hasDataInView: Store<FeatureViewState>
readonly guistate: MenuState readonly guistate: MenuState
readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO readonly fullNodeDatabase?: FullNodeDatabaseSource
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>> readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
readonly indexedFeatures: IndexedFeatureSource & LayoutSource readonly indexedFeatures: IndexedFeatureSource & LayoutSource
@ -85,6 +93,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
readonly newFeatures: WritableFeatureSource readonly newFeatures: WritableFeatureSource
readonly layerState: LayerState readonly layerState: LayerState
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource>
readonly availableLayers: Store<RasterLayerPolygon[]> readonly availableLayers: Store<RasterLayerPolygon[]>
readonly selectedLayer: UIEventSource<LayerConfig> readonly selectedLayer: UIEventSource<LayerConfig>
readonly userRelatedState: UserRelatedState readonly userRelatedState: UserRelatedState
@ -175,6 +185,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.fullNodeDatabase this.fullNodeDatabase
) )
this.indexedFeatures = layoutSource this.indexedFeatures = layoutSource
const empty = [] const empty = []
let currentViewIndex = 0 let currentViewIndex = 0
this.currentView = new StaticFeatureSource( this.currentView = new StaticFeatureSource(
@ -194,6 +205,9 @@ export default class ThemeViewState implements SpecialVisualizationState {
) )
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
this.dataIsLoading = layoutSource.isLoading this.dataIsLoading = layoutSource.isLoading
this.dataIsLoading.addCallbackAndRunD((loading) =>
console.log("Data is loading?", loading)
)
const indexedElements = this.indexedFeatures const indexedElements = this.indexedFeatures
this.featureProperties = new FeaturePropertiesStore(indexedElements) this.featureProperties = new FeaturePropertiesStore(indexedElements)
@ -288,7 +302,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.changes this.changes
) )
this.showNormalDataOn(this.map) this.perLayerFiltered = this.showNormalDataOn(this.map)
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
this.initActors() this.initActors()
this.addLastClick(lastClick) this.addLastClick(lastClick)
this.drawSpecialLayers() this.drawSpecialLayers()
@ -299,8 +316,9 @@ export default class ThemeViewState implements SpecialVisualizationState {
} }
} }
public showNormalDataOn(map: Store<MlMap>) { public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> {
this.perLayer.forEach((fs) => { const filteringFeatureSource = new Map<string, FilteringFeatureSource>()
this.perLayer.forEach((fs, layerName) => {
const doShowLayer = this.mapProperties.zoom.map( const doShowLayer = this.mapProperties.zoom.map(
(z) => (z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), (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), (id) => this.featureProperties.getStore(id),
this.layerState.globalFilters this.layerState.globalFilters
) )
filteringFeatureSource.set(layerName, filtered)
new ShowDataLayer(map, { new ShowDataLayer(map, {
layer: fs.layer.layerDef, layer: fs.layer.layerDef,
@ -333,6 +352,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
fetchStore: (id) => this.featureProperties.getStore(id), 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 * Setup various services for which no reference are needed
*/ */
private initActors() { private initActors() {
{ // Unselect the selected element if it is panned out of view
// Unselect the selected element if it is panned out of view this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => {
this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => { const selected = this.selectedElement.data
const selected = this.selectedElement.data if (selected === undefined) {
if (selected === undefined) { return
return }
} const bbox = BBox.get(selected)
const bbox = BBox.get(selected) if (!bbox.overlapsWith(bounds)) {
if (!bbox.overlapsWith(bounds)) { this.selectedElement.setData(undefined)
this.selectedElement.setData(undefined) }
} })
})
} this.selectedElement.addCallback((selected) => {
{ if (selected === undefined) {
this.selectedElement.addCallback((selected) => { // We did _unselect_ an item - we always remove the lastclick-object
if (selected === undefined) { this.lastClickObject.features.setData([])
// We did _unselect_ an item - we always remove the lastclick-object this.selectedLayer.setData(undefined)
this.lastClickObject.features.setData([]) }
this.selectedLayer.setData(undefined) })
}
})
}
new ThemeViewStateHashActor(this) new ThemeViewStateHashActor(this)
new MetaTagging(this) new MetaTagging(this)
new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this) new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this)

View file

@ -0,0 +1,40 @@
<script lang="ts">
import ThemeViewState from "../../Models/ThemeViewState";
import Translations from "../i18n/Translations";
import Tr from "../Base/Tr.svelte";
import Loading from "../Base/Loading.svelte";
export let state: ThemeViewState
/**
* Gives the contributor some feedback based on the current state:
* - is data loading?
* - Is all data hidden due to filters?
* - Is no data in view?
*/
let dataIsLoading = state.dataIsLoading
let currentState = state.hasDataInView
currentState.data === ""
const t = Translations.t.centerMessage
</script>
{#if $currentState === "has-visible-features"}
<!-- don't show anything -->
{:else if $currentState === "zoom-to-low"}
<div class="w-fit p-4 alert">
<Tr t={t.zoomIn}/>
</div>
{:else if $currentState === "all-filtered-away"}
<div class="w-fit p-4 alert">
<Tr t={t.allFilteredAway}/>
</div>
{:else if $dataIsLoading}
<div class="w-fit p-4 alert">
<Loading>
<Tr t={Translations.t.centerMessage.loadingData}/>
</Loading>
</div>
{:else if $currentState === "no-data"}
<div class="w-fit p-4 alert">
<Tr t={t.noData}/>
</div>
{/if}

View file

@ -1,430 +1,440 @@
<script lang="ts"> <script lang="ts">
import {Store, UIEventSource} from "../Logic/UIEventSource" import {Store, UIEventSource} from "../Logic/UIEventSource"
import {Map as MlMap} from "maplibre-gl" import {Map as MlMap} from "maplibre-gl"
import MaplibreMap from "./Map/MaplibreMap.svelte" import MaplibreMap from "./Map/MaplibreMap.svelte"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState" import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import MapControlButton from "./Base/MapControlButton.svelte" import MapControlButton from "./Base/MapControlButton.svelte"
import ToSvelte from "./Base/ToSvelte.svelte" import ToSvelte from "./Base/ToSvelte.svelte"
import If from "./Base/If.svelte" import If from "./Base/If.svelte"
import {GeolocationControl} from "./BigComponents/GeolocationControl" import {GeolocationControl} from "./BigComponents/GeolocationControl"
import type {Feature} from "geojson" import type {Feature} from "geojson"
import SelectedElementView from "./BigComponents/SelectedElementView.svelte" import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
import LayerConfig from "../Models/ThemeConfig/LayerConfig" import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import Filterview from "./BigComponents/Filterview.svelte" import Filterview from "./BigComponents/Filterview.svelte"
import ThemeViewState from "../Models/ThemeViewState" import ThemeViewState from "../Models/ThemeViewState"
import type {MapProperties} from "../Models/MapProperties" import type {MapProperties} from "../Models/MapProperties"
import Geosearch from "./BigComponents/Geosearch.svelte" import Geosearch from "./BigComponents/Geosearch.svelte"
import Translations from "./i18n/Translations" import Translations from "./i18n/Translations"
import {CogIcon, EyeIcon, MenuIcon, XCircleIcon} from "@rgossiaux/svelte-heroicons/solid" import {CogIcon, EyeIcon, MenuIcon, XCircleIcon} from "@rgossiaux/svelte-heroicons/solid"
import Tr from "./Base/Tr.svelte" import Tr from "./Base/Tr.svelte"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte" import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
import FloatOver from "./Base/FloatOver.svelte" import FloatOver from "./Base/FloatOver.svelte"
import PrivacyPolicy from "./BigComponents/PrivacyPolicy" import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
import Constants from "../Models/Constants" import Constants from "../Models/Constants"
import TabbedGroup from "./Base/TabbedGroup.svelte" import TabbedGroup from "./Base/TabbedGroup.svelte"
import UserRelatedState from "../Logic/State/UserRelatedState" import UserRelatedState from "../Logic/State/UserRelatedState"
import LoginToggle from "./Base/LoginToggle.svelte" import LoginToggle from "./Base/LoginToggle.svelte"
import LoginButton from "./Base/LoginButton.svelte" import LoginButton from "./Base/LoginButton.svelte"
import CopyrightPanel from "./BigComponents/CopyrightPanel" import CopyrightPanel from "./BigComponents/CopyrightPanel"
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte" import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
import ModalRight from "./Base/ModalRight.svelte" import ModalRight from "./Base/ModalRight.svelte"
import {Utils} from "../Utils" import {Utils} from "../Utils"
import Hotkeys from "./Base/Hotkeys" import Hotkeys from "./Base/Hotkeys"
import {VariableUiElement} from "./Base/VariableUIElement" import {VariableUiElement} from "./Base/VariableUIElement"
import SvelteUIElement from "./Base/SvelteUIElement" import SvelteUIElement from "./Base/SvelteUIElement"
import OverlayToggle from "./BigComponents/OverlayToggle.svelte" import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
import LevelSelector from "./BigComponents/LevelSelector.svelte" import LevelSelector from "./BigComponents/LevelSelector.svelte"
import ExtraLinkButton from "./BigComponents/ExtraLinkButton" import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte" import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
import Svg from "../Svg" import Svg from "../Svg"
import {ShareScreen} from "./BigComponents/ShareScreen" import {ShareScreen} from "./BigComponents/ShareScreen"
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte" import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
import type {RasterLayerPolygon} from "../Models/RasterLayers" import type {RasterLayerPolygon} from "../Models/RasterLayers"
import {AvailableRasterLayers} from "../Models/RasterLayers" import {AvailableRasterLayers} from "../Models/RasterLayers"
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte" import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
import IfHidden from "./Base/IfHidden.svelte" import IfHidden from "./Base/IfHidden.svelte"
import {onDestroy} from "svelte" import {onDestroy} from "svelte"
import {OpenJosm} from "./BigComponents/OpenJosm" import {OpenJosm} from "./BigComponents/OpenJosm"
import MapillaryLink from "./BigComponents/MapillaryLink.svelte" import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte" import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"; import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte";
import Loading from "./Base/Loading.svelte";
import StateIndicator from "./BigComponents/StateIndicator.svelte";
export let state: ThemeViewState export let state: ThemeViewState
let layout = state.layout let layout = state.layout
let maplibremap: UIEventSource<MlMap> = state.map let maplibremap: UIEventSource<MlMap> = state.map
let selectedElement: UIEventSource<Feature> = state.selectedElement let selectedElement: UIEventSource<Feature> = state.selectedElement
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer
const selectedElementView = selectedElement.map( const selectedElementView = selectedElement.map(
(selectedElement) => { (selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements // Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected // As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick // This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data const layer = selectedLayer.data
if (selectedElement === undefined || layer === undefined) { if (selectedElement === undefined || layer === undefined) {
return undefined return undefined
} }
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) { if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
return undefined return undefined
} }
const tags = state.featureProperties.getStore(selectedElement.properties.id) const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags }) return new SvelteUIElement(SelectedElementView, {state, layer, selectedElement, tags})
}, },
[selectedLayer] [selectedLayer]
) )
const selectedElementTitle = selectedElement.map( const selectedElementTitle = selectedElement.map(
(selectedElement) => { (selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements // Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected // As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick // This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data const layer = selectedLayer.data
if (selectedElement === undefined || layer === undefined) { if (selectedElement === undefined || layer === undefined) {
return undefined return undefined
} }
const tags = state.featureProperties.getStore(selectedElement.properties.id) const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags }) return new SvelteUIElement(SelectedElementTitle, {state, layer, selectedElement, tags})
}, },
[selectedLayer] [selectedLayer]
) )
let mapproperties: MapProperties = state.mapProperties let mapproperties: MapProperties = state.mapProperties
let featureSwitches: FeatureSwitchState = state.featureSwitches let featureSwitches: FeatureSwitchState = state.featureSwitches
let availableLayers = state.availableLayers let availableLayers = state.availableLayers
let userdetails = state.osmConnection.userDetails let userdetails = state.osmConnection.userDetails
let currentViewLayer = layout.layers.find((l) => l.id === "current_view") let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
let rasterLayerName = let rasterLayerName =
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maplibre.properties.name rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maplibre.properties.name
onDestroy( onDestroy(
rasterLayer.addCallbackAndRunD((l) => { rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name rasterLayerName = l.properties.name
}) })
) )
</script> </script>
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden"> <div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
<MaplibreMap map={maplibremap} /> <MaplibreMap map={maplibremap}/>
</div> </div>
<div class="pointer-events-none absolute top-0 left-0 w-full"> <div class="pointer-events-none absolute top-0 left-0 w-full">
<!-- Top components --> <!-- Top components -->
<If condition={state.featureSwitches.featureSwitchSearch}> <If condition={state.featureSwitches.featureSwitchSearch}>
<div class="pointer-events-auto float-right mt-1 px-1 max-[480px]:w-full sm:m-2"> <div class="pointer-events-auto float-right mt-1 px-1 max-[480px]:w-full sm:m-2">
<Geosearch <Geosearch
bounds={state.mapProperties.bounds} bounds={state.mapProperties.bounds}
perLayer={state.perLayer} perLayer={state.perLayer}
{selectedElement} {selectedElement}
{selectedLayer} {selectedLayer}
/> />
</div> </div>
</If> </If>
<div class="float-left m-1 flex flex-col sm:mt-2"> <div class="float-left m-1 flex flex-col sm:mt-2">
<MapControlButton on:click={() => state.guistate.themeIsOpened.setData(true)}> <MapControlButton on:click={() => state.guistate.themeIsOpened.setData(true)}>
<div class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 md:mx-2"> <div class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 md:mx-2">
<img class="mr-0.5 block h-6 w-6 sm:mr-1 md:mr-2 md:h-8 md:w-8" src={layout.icon} /> <img class="mr-0.5 block h-6 w-6 sm:mr-1 md:mr-2 md:h-8 md:w-8" src={layout.icon}/>
<b class="mr-1"> <b class="mr-1">
<Tr t={layout.title} /> <Tr t={layout.title}/>
</b> </b>
</div> </div>
</MapControlButton> </MapControlButton>
<MapControlButton on:click={() => state.guistate.menuIsOpened.setData(true)}> <MapControlButton on:click={() => state.guistate.menuIsOpened.setData(true)}>
<MenuIcon class="h-8 w-8 cursor-pointer" /> <MenuIcon class="h-8 w-8 cursor-pointer"/>
</MapControlButton> </MapControlButton>
{#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()} {#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()}
<MapControlButton <MapControlButton
on:click={() => { on:click={() => {
selectedLayer.setData(currentViewLayer) selectedLayer.setData(currentViewLayer)
selectedElement.setData(state.currentView.features?.data?.[0]) selectedElement.setData(state.currentView.features?.data?.[0])
}} }}
> >
<ToSvelte
construct={() => currentViewLayer.defaultIcon().SetClass("w-8 h-8 cursor-pointer")}
/>
</MapControlButton>
{/if}
<ToSvelte <ToSvelte
construct={() => currentViewLayer.defaultIcon().SetClass("w-8 h-8 cursor-pointer")} construct={() => new ExtraLinkButton(state, layout.extraLink).SetClass("pointer-events-auto")}
/> />
</MapControlButton> <If condition={state.featureSwitchIsTesting}>
{/if} <div class="alert w-fit">Testmode</div>
<ToSvelte </If>
construct={() => new ExtraLinkButton(state, layout.extraLink).SetClass("pointer-events-auto")} </div>
/> <div class="flex justify-center w-full"> <!-- Flex and w-full are needed for the positioning -->
<If condition={state.featureSwitchIsTesting}> <!-- Centermessage -->
<div class="alert w-fit">Testmode</div> <StateIndicator {state}/>
</If> </div>
</div>
</div> </div>
<div class="pointer-events-none absolute bottom-0 left-0 mb-4 w-screen"> <div class="pointer-events-none absolute bottom-0 left-0 mb-4 w-screen">
<div class="flex w-full items-end justify-between px-4"> <!-- bottom controls -->
<div class="flex"> <div class="flex w-full items-end justify-between px-4">
<!-- bottom left elements --> <div class="flex">
<OpenBackgroundSelectorButton {state} hideTooltip={true}/> <!-- bottom left elements -->
<a <OpenBackgroundSelectorButton hideTooltip={true} {state}/>
class="bg-black-transparent pointer-events-auto h-fit max-h-12 cursor-pointer self-end overflow-hidden rounded-2xl pl-1 pr-2 text-white opacity-50 hover:opacity-100" <a
on:click={() => { class="bg-black-transparent pointer-events-auto h-fit max-h-12 cursor-pointer self-end overflow-hidden rounded-2xl pl-1 pr-2 text-white opacity-50 hover:opacity-100"
on:click={() => {
state.guistate.themeViewTab.setData("copyright") state.guistate.themeViewTab.setData("copyright")
state.guistate.themeIsOpened.setData(true) state.guistate.themeIsOpened.setData(true)
}} }}
> >
© OpenStreetMap, <span class="w-24">{rasterLayerName}</span> © OpenStreetMap, <span class="w-24">{rasterLayerName}</span>
</a> </a>
</div>
<div class="flex flex-col items-end">
<!-- bottom right elements -->
<If condition={state.floors.map((f) => f.length > 1)}>
<div class="pointer-events-auto mr-0.5">
<LevelSelector
floors={state.floors}
layerState={state.layerState}
zoom={state.mapProperties.zoom}
/>
</div> </div>
</If>
<MapControlButton on:click={() => mapproperties.zoom.update((z) => z + 1)}> <div class="flex flex-col items-end">
<ToSvelte construct={Svg.plus_svg().SetClass("w-8 h-8")} /> <!-- bottom right elements -->
</MapControlButton> <If condition={state.floors.map((f) => f.length > 1)}>
<MapControlButton on:click={() => mapproperties.zoom.update((z) => z - 1)}> <div class="pointer-events-auto mr-0.5">
<ToSvelte construct={Svg.min_svg().SetClass("w-8 h-8")} /> <LevelSelector
</MapControlButton> floors={state.floors}
<If condition={featureSwitches.featureSwitchGeolocation}> layerState={state.layerState}
<MapControlButton> zoom={state.mapProperties.zoom}
<ToSvelte />
construct={new GeolocationControl(state.geolocation, mapproperties).SetClass( </div>
</If>
<MapControlButton on:click={() => mapproperties.zoom.update((z) => z + 1)}>
<ToSvelte construct={Svg.plus_svg().SetClass("w-8 h-8")}/>
</MapControlButton>
<MapControlButton on:click={() => mapproperties.zoom.update((z) => z - 1)}>
<ToSvelte construct={Svg.min_svg().SetClass("w-8 h-8")}/>
</MapControlButton>
<If condition={featureSwitches.featureSwitchGeolocation}>
<MapControlButton>
<ToSvelte
construct={new GeolocationControl(state.geolocation, mapproperties).SetClass(
"block w-8 h-8" "block w-8 h-8"
)} )}
/> />
</MapControlButton> </MapControlButton>
</If> </If>
</div>
</div> </div>
</div>
</div> </div>
<If <If
condition={selectedElementView.map( condition={selectedElementView.map(
(v) => (v) =>
v !== undefined && selectedLayer.data !== undefined && !selectedLayer.data.popupInFloatover, v !== undefined && selectedLayer.data !== undefined && !selectedLayer.data.popupInFloatover,
[selectedLayer] [selectedLayer]
)} )}
> >
<ModalRight <!-- right modal with the selected element view -->
on:close={() => { <ModalRight
on:close={() => {
selectedElement.setData(undefined) selectedElement.setData(undefined)
}} }}
> >
<div class="normal-background absolute flex h-full w-full flex-col"> <div class="normal-background absolute flex h-full w-full flex-col">
<ToSvelte construct={new VariableUiElement(selectedElementTitle)}> <ToSvelte construct={new VariableUiElement(selectedElementTitle)}>
<!-- Title --> <!-- Title -->
</ToSvelte> </ToSvelte>
<ToSvelte construct={new VariableUiElement(selectedElementView).SetClass("overflow-auto")}> <ToSvelte construct={new VariableUiElement(selectedElementView).SetClass("overflow-auto")}>
<!-- Main view --> <!-- Main view -->
</ToSvelte> </ToSvelte>
</div> </div>
</ModalRight> </ModalRight>
</If> </If>
<If <If
condition={selectedElementView.map( condition={selectedElementView.map(
(v) => (v) =>
v !== undefined && selectedLayer.data !== undefined && selectedLayer.data.popupInFloatover, v !== undefined && selectedLayer.data !== undefined && selectedLayer.data.popupInFloatover,
[selectedLayer] [selectedLayer]
)} )}
> >
<FloatOver <!-- Floatover with the selected element, if applicable -->
on:close={() => { <FloatOver
on:close={() => {
selectedElement.setData(undefined) selectedElement.setData(undefined)
}} }}
> >
<ToSvelte construct={new VariableUiElement(selectedElementView)} /> <ToSvelte construct={new VariableUiElement(selectedElementView)}/>
</FloatOver> </FloatOver>
</If> </If>
<If condition={state.guistate.themeIsOpened}> <If condition={state.guistate.themeIsOpened}>
<!-- Theme menu --> <!-- Theme menu -->
<FloatOver> <FloatOver>
<span slot="close-button"><!-- Disable the close button --></span> <span slot="close-button"><!-- Disable the close button --></span>
<TabbedGroup tab={state.guistate.themeViewTabIndex}> <TabbedGroup tab={state.guistate.themeViewTabIndex}>
<div slot="post-tablist"> <div slot="post-tablist">
<XCircleIcon <XCircleIcon
class="mr-2 h-8 w-8" class="mr-2 h-8 w-8"
on:click={() => state.guistate.themeIsOpened.setData(false)} on:click={() => state.guistate.themeIsOpened.setData(false)}
/> />
</div> </div>
<div class="flex" slot="title0"> <div class="flex" slot="title0">
<img class="block h-4 w-4" src={layout.icon} /> <img class="block h-4 w-4" src={layout.icon}/>
<Tr t={layout.title} /> <Tr t={layout.title}/>
</div> </div>
<div class="m-4" slot="content0"> <div class="m-4" slot="content0">
<ThemeIntroPanel {state} /> <ThemeIntroPanel {state}/>
</div> </div>
<div class="flex" slot="title1"> <div class="flex" slot="title1">
<If condition={state.featureSwitches.featureSwitchFilter}> <If condition={state.featureSwitches.featureSwitchFilter}>
<ToSvelte construct={Svg.filter_svg().SetClass("w-4 h-4")} /> <ToSvelte construct={Svg.filter_svg().SetClass("w-4 h-4")}/>
<Tr t={Translations.t.general.menu.filter} /> <Tr t={Translations.t.general.menu.filter}/>
</If> </If>
</div> </div>
<div class="m-2 flex flex-col" slot="content1"> <div class="m-2 flex flex-col" slot="content1">
{#each layout.layers as layer} {#each layout.layers as layer}
<Filterview <Filterview
zoomlevel={state.mapProperties.zoom} zoomlevel={state.mapProperties.zoom}
filteredLayer={state.layerState.filteredLayers.get(layer.id)} filteredLayer={state.layerState.filteredLayers.get(layer.id)}
highlightedLayer={state.guistate.highlightedLayerInFilters} highlightedLayer={state.guistate.highlightedLayerInFilters}
/> />
{/each} {/each}
{#each layout.tileLayerSources as tilesource} {#each layout.tileLayerSources as tilesource}
<OverlayToggle <OverlayToggle
layerproperties={tilesource} layerproperties={tilesource}
state={state.overlayLayerStates.get(tilesource.id)} state={state.overlayLayerStates.get(tilesource.id)}
highlightedLayer={state.guistate.highlightedLayerInFilters} highlightedLayer={state.guistate.highlightedLayerInFilters}
zoomlevel={state.mapProperties.zoom} zoomlevel={state.mapProperties.zoom}
/> />
{/each} {/each}
</div> </div>
<div class="flex" slot="title2"> <div class="flex" slot="title2">
<If condition={state.featureSwitches.featureSwitchEnableExport}> <If condition={state.featureSwitches.featureSwitchEnableExport}>
<ToSvelte construct={Svg.download_svg().SetClass("w-4 h-4")} /> <ToSvelte construct={Svg.download_svg().SetClass("w-4 h-4")}/>
<Tr t={Translations.t.general.download.title} /> <Tr t={Translations.t.general.download.title}/>
</If> </If>
</div> </div>
<div class="m-4" slot="content2"> <div class="m-4" slot="content2">
<DownloadPanel {state} /> <DownloadPanel {state}/>
</div> </div>
<div slot="title3"> <div slot="title3">
<Tr t={Translations.t.general.attribution.title} /> <Tr t={Translations.t.general.attribution.title}/>
</div> </div>
<ToSvelte construct={() => new CopyrightPanel(state)} slot="content3" /> <ToSvelte construct={() => new CopyrightPanel(state)} slot="content3"/>
<div slot="title4"> <div slot="title4">
<Tr t={Translations.t.general.sharescreen.title} /> <Tr t={Translations.t.general.sharescreen.title}/>
</div> </div>
<div class="m-2" slot="content4"> <div class="m-2" slot="content4">
<ToSvelte construct={() => new ShareScreen(state)} /> <ToSvelte construct={() => new ShareScreen(state)}/>
</div> </div>
</TabbedGroup> </TabbedGroup>
</FloatOver> </FloatOver>
</If> </If>
<IfHidden condition={state.guistate.backgroundLayerSelectionIsOpened}> <IfHidden condition={state.guistate.backgroundLayerSelectionIsOpened}>
<!-- background layer selector --> <!-- background layer selector -->
<FloatOver on:close={() => state.guistate.backgroundLayerSelectionIsOpened.setData(false)}> <FloatOver on:close={() => state.guistate.backgroundLayerSelectionIsOpened.setData(false)}>
<div class="h-full p-2"> <div class="h-full p-2">
<RasterLayerOverview <RasterLayerOverview
userstate={state.userRelatedState} {availableLayers}
mapproperties={state.mapProperties} map={state.map}
map={state.map} mapproperties={state.mapProperties}
{availableLayers} userstate={state.userRelatedState}
visible={state.guistate.backgroundLayerSelectionIsOpened} visible={state.guistate.backgroundLayerSelectionIsOpened}
/> />
</div> </div>
</FloatOver> </FloatOver>
</IfHidden> </IfHidden>
<If condition={state.guistate.menuIsOpened}> <If condition={state.guistate.menuIsOpened}>
<!-- Menu page --> <!-- Menu page -->
<FloatOver> <FloatOver>
<span slot="close-button"><!-- Hide the default close button --></span> <span slot="close-button"><!-- Hide the default close button --></span>
<TabbedGroup tab={state.guistate.menuViewTabIndex}> <TabbedGroup tab={state.guistate.menuViewTabIndex}>
<div slot="post-tablist"> <div slot="post-tablist">
<XCircleIcon <XCircleIcon
class="mr-2 h-8 w-8" class="mr-2 h-8 w-8"
on:click={() => state.guistate.menuIsOpened.setData(false)} on:click={() => state.guistate.menuIsOpened.setData(false)}
/> />
</div> </div>
<div class="flex" slot="title0"> <div class="flex" slot="title0">
<Tr t={Translations.t.general.menu.aboutMapComplete} /> <Tr t={Translations.t.general.menu.aboutMapComplete}/>
</div> </div>
<div class="links-as-button links-w-full m-2 flex flex-col gap-y-1" slot="content0"> <div class="links-as-button links-w-full m-2 flex flex-col gap-y-1" slot="content0">
<Tr t={Translations.t.general.aboutMapComplete.intro} /> <Tr t={Translations.t.general.aboutMapComplete.intro}/>
<a class="flex" href={Utils.HomepageLink()}> <a class="flex" href={Utils.HomepageLink()}>
<img class="h-6 w-6" src="./assets/svg/add.svg" /> <img class="h-6 w-6" src="./assets/svg/add.svg"/>
<Tr t={Translations.t.general.backToIndex} /> <Tr t={Translations.t.general.backToIndex}/>
</a> </a>
<a class="flex" href="https://github.com/pietervdvn/MapComplete/issues" target="_blank"> <a class="flex" href="https://github.com/pietervdvn/MapComplete/issues" target="_blank">
<img class="h-6 w-6" src="./assets/svg/bug.svg" /> <img class="h-6 w-6" src="./assets/svg/bug.svg"/>
<Tr t={Translations.t.general.attribution.openIssueTracker} /> <Tr t={Translations.t.general.attribution.openIssueTracker}/>
</a> </a>
<a class="flex" href="https://en.osm.town/@MapComplete" target="_blank"> <a class="flex" href="https://en.osm.town/@MapComplete" target="_blank">
<img class="h-6 w-6" src="./assets/svg/mastodon.svg" /> <img class="h-6 w-6" src="./assets/svg/mastodon.svg"/>
<Tr t={Translations.t.general.attribution.followOnMastodon} /> <Tr t={Translations.t.general.attribution.followOnMastodon}/>
</a> </a>
<a class="flex" href="https://liberapay.com/pietervdvn/" target="_blank"> <a class="flex" href="https://liberapay.com/pietervdvn/" target="_blank">
<img class="h-6 w-6" src="./assets/svg/liberapay.svg" /> <img class="h-6 w-6" src="./assets/svg/liberapay.svg"/>
<Tr t={Translations.t.general.attribution.donate} /> <Tr t={Translations.t.general.attribution.donate}/>
</a> </a>
<a class="flex" href={Utils.OsmChaLinkFor(7)} target="_blank"> <a class="flex" href={Utils.OsmChaLinkFor(7)} target="_blank">
<img class="h-6 w-6" src="./assets/svg/statistics.svg" /> <img class="h-6 w-6" src="./assets/svg/statistics.svg"/>
<Tr t={Translations.t.general.attribution.openOsmcha.Subs({ theme: "MapComplete" })} /> <Tr t={Translations.t.general.attribution.openOsmcha.Subs({ theme: "MapComplete" })}/>
</a> </a>
{Constants.vNumber} {Constants.vNumber}
</div> </div>
<div class="flex" slot="title1"> <div class="flex" slot="title1">
<CogIcon class="h-6 w-6" /> <CogIcon class="h-6 w-6"/>
<Tr t={UserRelatedState.usersettingsConfig.title.GetRenderValue({})} /> <Tr t={UserRelatedState.usersettingsConfig.title.GetRenderValue({})}/>
</div> </div>
<div class="links-as-button" slot="content1"> <div class="links-as-button" slot="content1">
<!-- All shown components are set by 'usersettings.json', which happily uses some special visualisations created specifically for it --> <!-- All shown components are set by 'usersettings.json', which happily uses some special visualisations created specifically for it -->
<LoginToggle {state}> <LoginToggle {state}>
<div class="flex flex-col" slot="not-logged-in"> <div class="flex flex-col" slot="not-logged-in">
<Tr class="alert" t={Translations.t.userinfo.notLoggedIn} /> <Tr class="alert" t={Translations.t.userinfo.notLoggedIn}/>
<LoginButton clss="primary" osmConnection={state.osmConnection} /> <LoginButton clss="primary" osmConnection={state.osmConnection}/>
</div> </div>
<SelectedElementView <SelectedElementView
highlightedRendering={state.guistate.highlightedUserSetting} highlightedRendering={state.guistate.highlightedUserSetting}
layer={UserRelatedState.usersettingsConfig} layer={UserRelatedState.usersettingsConfig}
selectedElement={{ selectedElement={{
type: "Feature", type: "Feature",
properties: {}, properties: {},
geometry: { type: "Point", coordinates: [0, 0] }, geometry: { type: "Point", coordinates: [0, 0] },
}} }}
{state} {state}
tags={state.userRelatedState.preferencesAsTags} tags={state.userRelatedState.preferencesAsTags}
/> />
</LoginToggle> </LoginToggle>
</div> </div>
<div class="flex" slot="title2"> <div class="flex" slot="title2">
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} /> <ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")}/>
Get in touch with others Get in touch with others
</div> </div>
<div class="m-2" slot="content2"> <div class="m-2" slot="content2">
<CommunityIndexView location={state.mapProperties.location} /> <CommunityIndexView location={state.mapProperties.location}/>
</div> </div>
<div class="flex" slot="title3"> <div class="flex" slot="title3">
<EyeIcon class="w-6" /> <EyeIcon class="w-6"/>
<Tr t={Translations.t.privacy.title} /> <Tr t={Translations.t.privacy.title}/>
</div> </div>
<div class="m-2" slot="content3"> <div class="m-2" slot="content3">
<ToSvelte construct={() => new PrivacyPolicy()} /> <ToSvelte construct={() => new PrivacyPolicy()}/>
</div> </div>
<Tr slot="title4" t={Translations.t.advanced.title} /> <Tr slot="title4" t={Translations.t.advanced.title}/>
<div class="m-2 flex flex-col" slot="content4"> <div class="m-2 flex flex-col" slot="content4">
<OpenIdEditor mapProperties={state.mapProperties} /> <OpenIdEditor mapProperties={state.mapProperties}/>
<ToSvelte <ToSvelte
construct={() => construct={() =>
new OpenJosm(state.osmConnection, state.mapProperties.bounds).SetClass("w-full")} new OpenJosm(state.osmConnection, state.mapProperties.bounds).SetClass("w-full")}
/> />
<MapillaryLink mapProperties={state.mapProperties} /> <MapillaryLink mapProperties={state.mapProperties}/>
<ToSvelte construct={Hotkeys.generateDocumentationDynamic} /> <ToSvelte construct={Hotkeys.generateDocumentationDynamic}/>
</div> </div>
</TabbedGroup> </TabbedGroup>
</FloatOver> </FloatOver>
</If> </If>

View file

@ -3,7 +3,9 @@
"title": "Advanced features" "title": "Advanced features"
}, },
"centerMessage": { "centerMessage": {
"allFilteredAway": "No feature in view meets all filters",
"loadingData": "Loading data…", "loadingData": "Loading data…",
"noData": "There are no relevant features in the current view",
"ready": "Done!", "ready": "Done!",
"retrying": "Loading data failed. Trying again in {count} seconds…", "retrying": "Loading data failed. Trying again in {count} seconds…",
"zoomIn": "Zoom in to view or edit the data" "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", "geolocate": "Pan the map to the current location or zoom the map to the current location. Requests geopermission",
"intro": "MapComplete supports the following keys:", "intro": "MapComplete supports the following keys:",
"key": "Key combination", "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", "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", "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", "selectMapnik": "Set the background layer to OpenStreetMap-carto",