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,7 +553,6 @@ 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
@ -545,8 +564,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.selectedElement.setData(undefined) this.selectedElement.setData(undefined)
} }
}) })
}
{
this.selectedElement.addCallback((selected) => { this.selectedElement.addCallback((selected) => {
if (selected === undefined) { if (selected === undefined) {
// We did _unselect_ an item - we always remove the lastclick-object // We did _unselect_ an item - we always remove the lastclick-object
@ -554,7 +572,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.selectedLayer.setData(undefined) 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

@ -49,6 +49,8 @@
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
@ -72,7 +74,7 @@
} }
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]
) )
@ -88,7 +90,7 @@
} }
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]
) )
@ -109,9 +111,10 @@
</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}>
@ -127,14 +130,14 @@
<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
@ -155,13 +158,18 @@
<div class="alert w-fit">Testmode</div> <div class="alert w-fit">Testmode</div>
</If> </If>
</div> </div>
<div class="flex justify-center w-full"> <!-- Flex and w-full are needed for the positioning -->
<!-- Centermessage -->
<StateIndicator {state}/>
</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">
<!-- bottom controls -->
<div class="flex w-full items-end justify-between px-4"> <div class="flex w-full items-end justify-between px-4">
<div class="flex"> <div class="flex">
<!-- bottom left elements --> <!-- bottom left elements -->
<OpenBackgroundSelectorButton {state} hideTooltip={true}/> <OpenBackgroundSelectorButton hideTooltip={true} {state}/>
<a <a
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" 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={() => { on:click={() => {
@ -185,10 +193,10 @@
</div> </div>
</If> </If>
<MapControlButton on:click={() => mapproperties.zoom.update((z) => z + 1)}> <MapControlButton on:click={() => mapproperties.zoom.update((z) => z + 1)}>
<ToSvelte construct={Svg.plus_svg().SetClass("w-8 h-8")} /> <ToSvelte construct={Svg.plus_svg().SetClass("w-8 h-8")}/>
</MapControlButton> </MapControlButton>
<MapControlButton on:click={() => mapproperties.zoom.update((z) => z - 1)}> <MapControlButton on:click={() => mapproperties.zoom.update((z) => z - 1)}>
<ToSvelte construct={Svg.min_svg().SetClass("w-8 h-8")} /> <ToSvelte construct={Svg.min_svg().SetClass("w-8 h-8")}/>
</MapControlButton> </MapControlButton>
<If condition={featureSwitches.featureSwitchGeolocation}> <If condition={featureSwitches.featureSwitchGeolocation}>
<MapControlButton> <MapControlButton>
@ -210,6 +218,7 @@
[selectedLayer] [selectedLayer]
)} )}
> >
<!-- right modal with the selected element view -->
<ModalRight <ModalRight
on:close={() => { on:close={() => {
selectedElement.setData(undefined) selectedElement.setData(undefined)
@ -233,12 +242,13 @@
[selectedLayer] [selectedLayer]
)} )}
> >
<!-- Floatover with the selected element, if applicable -->
<FloatOver <FloatOver
on:close={() => { on:close={() => {
selectedElement.setData(undefined) selectedElement.setData(undefined)
}} }}
> >
<ToSvelte construct={new VariableUiElement(selectedElementView)} /> <ToSvelte construct={new VariableUiElement(selectedElementView)}/>
</FloatOver> </FloatOver>
</If> </If>
@ -255,18 +265,18 @@
</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>
@ -289,25 +299,25 @@
</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>
@ -318,10 +328,10 @@
<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}
mapproperties={state.mapProperties}
map={state.map}
{availableLayers} {availableLayers}
map={state.map}
mapproperties={state.mapProperties}
userstate={state.userRelatedState}
visible={state.guistate.backgroundLayerSelectionIsOpened} visible={state.guistate.backgroundLayerSelectionIsOpened}
/> />
</div> </div>
@ -340,50 +350,50 @@
/> />
</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}
@ -400,30 +410,30 @@
</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>

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",