UX: update gps-label indication if GPS is not available physically

This commit is contained in:
Pieter Vander Vennet 2024-08-09 10:53:09 +02:00
parent 0c3cfdc034
commit d41afe7688
6 changed files with 79 additions and 29 deletions

View file

@ -274,6 +274,7 @@
"background": "Change background", "background": "Change background",
"filter": "Filter data", "filter": "Filter data",
"jumpToLocation": "Go to your current location", "jumpToLocation": "Go to your current location",
"locationNotAvailable": "GPS location not available. Does this device have location or are you in a tunnel?",
"menu": "Menu", "menu": "Menu",
"zoomIn": "Zoom in", "zoomIn": "Zoom in",
"zoomOut": "Zoom out" "zoomOut": "Zoom out"

View file

@ -99,7 +99,8 @@ export default class InitialMapPositioning {
Utils.downloadJson<{ latitude: number; longitude: number }>( Utils.downloadJson<{ latitude: number; longitude: number }>(
Constants.GeoIpServer + "ip" Constants.GeoIpServer + "ip"
).then(({ longitude, latitude }) => { ).then(({ longitude, latitude }) => {
if (geolocationState.currentGPSLocation.data !== undefined) { const gpsLoc = geolocationState.currentGPSLocation.data
if (gpsLoc !== undefined) {
return // We got a geolocation by now, abort return // We got a geolocation by now, abort
} }
console.log("Setting location based on geoip", longitude, latitude) console.log("Setting location based on geoip", longitude, latitude)

View file

@ -1,4 +1,4 @@
import { UIEventSource } from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
import { LocalStorageSource } from "../Web/LocalStorageSource" import { LocalStorageSource } from "../Web/LocalStorageSource"
import { QueryParameters } from "../Web/QueryParameters" import { QueryParameters } from "../Web/QueryParameters"
@ -25,6 +25,13 @@ export class GeoLocationState {
"prompt" "prompt"
) )
/**
* If an error occurs with a code indicating "gps unavailable", this will be set to "false".
* This is about the physical availability of the GPS-signal; this might e.g. become false if the user is in a tunnel
*/
private readonly _gpsAvailable: UIEventSource<boolean> = new UIEventSource<boolean>(true)
public readonly gpsAvailable: Store<boolean> = this._gpsAvailable
/** /**
* Important to determine e.g. if we move automatically on fix or not * Important to determine e.g. if we move automatically on fix or not
*/ */
@ -48,9 +55,7 @@ export class GeoLocationState {
* If the user denies the geolocation this time, we unset this flag * If the user denies the geolocation this time, we unset this flag
* @private * @private
*/ */
private readonly _previousLocationGrant: UIEventSource<"true" | "false"> = <any>( private readonly _previousLocationGrant: UIEventSource<boolean> = LocalStorageSource.GetParsed<boolean>("geolocation-permissions", false)
LocalStorageSource.Get("geolocation-permissions")
)
/** /**
* Used to detect a permission retraction * Used to detect a permission retraction
@ -62,27 +67,27 @@ export class GeoLocationState {
this.permission.addCallbackAndRunD(async (state) => { this.permission.addCallbackAndRunD(async (state) => {
if (state === "granted") { if (state === "granted") {
self._previousLocationGrant.setData("true") self._previousLocationGrant.setData(true)
self._grantedThisSession.setData(true) self._grantedThisSession.setData(true)
} }
if (state === "prompt" && self._grantedThisSession.data) { if (state === "prompt" && self._grantedThisSession.data) {
// This is _really_ weird: we had a grant earlier, but it's 'prompt' now? // This is _really_ weird: we had a grant earlier, but it's 'prompt' now?
// This means that the rights have been revoked again! // This means that the rights have been revoked again!
self._previousLocationGrant.setData("false") self._previousLocationGrant.setData(false)
self.permission.setData("denied") self.permission.setData("denied")
self.currentGPSLocation.setData(undefined) self.currentGPSLocation.setData(undefined)
console.warn("Detected a downgrade in permissions!") console.warn("Detected a downgrade in permissions!")
} }
if (state === "denied") { if (state === "denied") {
self._previousLocationGrant.setData("false") self._previousLocationGrant.setData(false)
} }
}) })
console.log("Previous location grant:", this._previousLocationGrant.data) console.log("Previous location grant:", this._previousLocationGrant.data)
if (this._previousLocationGrant.data === "true") { if (this._previousLocationGrant.data) {
// A previous visit successfully granted permission. Chance is high that we are allowed to use it again! // A previous visit successfully granted permission. Chance is high that we are allowed to use it again!
// We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them // We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them
this._previousLocationGrant.setData("false") this._previousLocationGrant.setData(false)
console.log("Requesting access to GPS as this was previously granted") console.log("Requesting access to GPS as this was previously granted")
const latLonGivenViaUrl = const latLonGivenViaUrl =
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon") QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon")
@ -124,9 +129,11 @@ export class GeoLocationState {
} }
if (GeoLocationState.isSafari()) { if (GeoLocationState.isSafari()) {
// This is probably safari /*
// Safari does not support the 'permissions'-API for geolocation, This is probably safari
// so we just start watching right away Safari does not support the 'permissions'-API for geolocation,
so we just start watching right away
*/
this.permission.setData("requested") this.permission.setData("requested")
this.startWatching() this.startWatching()
@ -143,7 +150,7 @@ export class GeoLocationState {
self.startWatching() self.startWatching()
return return
} }
status.addEventListener("change", (e) => { status.addEventListener("change", () => {
self.permission.setData(status.state) self.permission.setData(status.state)
}) })
// The code above might have reset it to 'prompt', but we _did_ request permission! // The code above might have reset it to 'prompt', but we _did_ request permission!
@ -163,10 +170,22 @@ export class GeoLocationState {
const self = this const self = this
navigator.geolocation.watchPosition( navigator.geolocation.watchPosition(
function (position) { function (position) {
self._gpsAvailable.set(true)
self.currentGPSLocation.setData(position.coords) self.currentGPSLocation.setData(position.coords)
self._previousLocationGrant.setData("true") self._previousLocationGrant.setData(true)
}, },
function (e) { function (e) {
if(e.code === 2 || e.code === 3){
console.log("Could not get location with navigator.geolocation due to unavailable or timeout", e)
self._gpsAvailable.set(false)
return
}
self._gpsAvailable.set(true) // We go back to the default assumption that the location is physically available
if(e.code === 1) {
self.permission.set("denied")
self._grantedThisSession.setData(false)
return
}
console.warn("Could not get location with navigator.geolocation due to", e) console.warn("Could not get location with navigator.geolocation due to", e)
}, },
{ {

View file

@ -2,7 +2,7 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { twJoin } from "tailwind-merge" import { twJoin } from "tailwind-merge"
import { Translation } from "../i18n/Translation" import { Translation } from "../i18n/Translation"
import { ariaLabel } from "../../Utils/ariaLabel" import { ariaLabel, ariaLabelStore } from "../../Utils/ariaLabel"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
/** /**
@ -12,18 +12,21 @@
export let cls = "m-0.5 p-0.5 sm:p-1 md:m-1" export let cls = "m-0.5 p-0.5 sm:p-1 md:m-1"
export let enabled: Store<boolean> = new ImmutableStore(true) export let enabled: Store<boolean> = new ImmutableStore(true)
export let arialabel: Translation = undefined export let arialabel: Translation = undefined
export let arialabelDynamic : Store<Translation> = new ImmutableStore(arialabel)
let arialabelString = arialabelDynamic.bind(tr => tr?.current)
export let htmlElem: UIEventSource<HTMLElement> = undefined export let htmlElem: UIEventSource<HTMLElement> = undefined
let _htmlElem: HTMLElement let _htmlElem: HTMLElement
$: { $: {
htmlElem?.setData(_htmlElem) htmlElem?.setData(_htmlElem)
} }
</script> </script>
<button <button
bind:this={_htmlElem} bind:this={_htmlElem}
on:click={(e) => dispatch("click", e)} on:click={(e) => dispatch("click", e)}
on:keydown on:keydown
use:ariaLabel={arialabel} use:ariaLabelStore={arialabelString}
class={twJoin( class={twJoin(
"pointer-events-auto relative h-fit w-fit rounded-full", "pointer-events-auto relative h-fit w-fit rounded-full",
cls, cls,

View file

@ -14,6 +14,7 @@
let allowMoving = geolocationstate.allowMoving let allowMoving = geolocationstate.allowMoving
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation
let geolocationControlState = state.geolocationControl let geolocationControlState = state.geolocationControl
let isAvailable = state.geolocation.geolocationState.gpsAvailable
let lastClickWasRecent = geolocationControlState.lastClickWithinThreeSecs let lastClickWasRecent = geolocationControlState.lastClickWithinThreeSecs
</script> </script>
@ -31,7 +32,7 @@
{:else if $geopermission === "requested"} {:else if $geopermission === "requested"}
<!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup --> <!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup -->
<Location class="h-8 w-8" style="animation: 3s linear 0s infinite normal none running spin;" /> <Location class="h-8 w-8" style="animation: 3s linear 0s infinite normal none running spin;" />
{:else if $geopermission === "denied"} {:else if $geopermission === "denied" || !$isAvailable}
<Location_refused class="h-8 w-8" /> <Location_refused class="h-8 w-8" />
{:else} {:else}
<Location class="h-8 w-8" style="animation: 3s linear 0s infinite normal none running spin;" /> <Location class="h-8 w-8" style="animation: 3s linear 0s infinite normal none running spin;" />

View file

@ -19,7 +19,7 @@
EyeIcon, EyeIcon,
HeartIcon, HeartIcon,
MenuIcon, MenuIcon,
XCircleIcon XCircleIcon,
} from "@rgossiaux/svelte-heroicons/solid" } 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"
@ -120,15 +120,15 @@
let visualFeedback = state.visualFeedback let visualFeedback = state.visualFeedback
let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined) let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined)
let mapproperties: MapProperties = state.mapProperties let mapproperties: MapProperties = state.mapProperties
let usersettingslayer = new LayerConfig(<LayerConfigJson> usersettings, "usersettings", true) let usersettingslayer = new LayerConfig(<LayerConfigJson>usersettings, "usersettings", true)
state.mapProperties.installCustomKeyboardHandler(viewport) state.mapProperties.installCustomKeyboardHandler(viewport)
let canZoomIn = mapproperties.maxzoom.map( let canZoomIn = mapproperties.maxzoom.map(
(mz) => mapproperties.zoom.data < mz, (mz) => mapproperties.zoom.data < mz,
[mapproperties.zoom] [mapproperties.zoom],
) )
let canZoomOut = mapproperties.minzoom.map( let canZoomOut = mapproperties.minzoom.map(
(mz) => mapproperties.zoom.data > mz, (mz) => mapproperties.zoom.data > mz,
[mapproperties.zoom] [mapproperties.zoom],
) )
function updateViewport() { function updateViewport() {
@ -144,7 +144,7 @@
const bottomRight = mlmap.unproject([rect.right, rect.bottom]) const bottomRight = mlmap.unproject([rect.right, rect.bottom])
const bbox = new BBox([ const bbox = new BBox([
[topLeft.lng, topLeft.lat], [topLeft.lng, topLeft.lat],
[bottomRight.lng, bottomRight.lat] [bottomRight.lng, bottomRight.lat],
]) ])
state.visualFeedbackViewportBounds.setData(bbox) state.visualFeedbackViewportBounds.setData(bbox)
} }
@ -165,7 +165,7 @@
onDestroy( onDestroy(
rasterLayer.addCallbackAndRunD((l) => { rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name rasterLayerName = l.properties.name
}) }),
) )
let previewedImage = state.previewedImage let previewedImage = state.previewedImage
@ -196,7 +196,7 @@
let openMapButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined) let openMapButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
let openMenuButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined) let openMenuButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
let openCurrentViewLayerButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>( let openCurrentViewLayerButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(
undefined undefined,
) )
let _openNewElementButton: HTMLButtonElement let _openNewElementButton: HTMLButtonElement
let openNewElementButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined) let openNewElementButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
@ -207,6 +207,31 @@
let openFilterButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined) let openFilterButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
let openBackgroundButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined) let openBackgroundButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
let addNewFeatureMode = state.userRelatedState.addNewFeatureMode let addNewFeatureMode = state.userRelatedState.addNewFeatureMode
let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsAvailable.map(available => {
if (!available) {
return Translations.t.general.labels.locationNotAvailable
}
if (state.geolocation.geolocationState.permission.data === "denied") {
return Translations.t.general.geopermissionDenied
}
if (state.geolocation.geolocationState.permission.data === "requested") {
return Translations.t.general.waitingForGeopermission
}
if (!state.geolocation.geolocationState.allowMoving.data) {
return Translations.t.general.visualFeedback.islocked
}
if (state.geolocation.geolocationState.currentGPSLocation.data === undefined) {
return Translations.t.general.waitingForLocation
}
return Translations.t.general.labels.jumpToLocation
}, [state.geolocation.geolocationState.allowMoving, state.geolocation.geolocationState.permission])
</script> </script>
<main> <main>
@ -408,7 +433,7 @@
<If condition={featureSwitches.featureSwitchGeolocation}> <If condition={featureSwitches.featureSwitchGeolocation}>
<div class="relative m-0"> <div class="relative m-0">
<MapControlButton <MapControlButton
arialabel={Translations.t.general.labels.jumpToLocation} arialabelDynamic={gpsButtonAriaLabel}
on:click={() => state.geolocationControl.handleClick()} on:click={() => state.geolocationControl.handleClick()}
on:keydown={forwardEventToMap} on:keydown={forwardEventToMap}
> >
@ -465,7 +490,7 @@
}} }}
> >
<span slot="close-button" /> <span slot="close-button" />
<SelectedElementPanel absolute={false} {state} selected={$selectedElement} /> <SelectedElementPanel absolute={false} {state} selected={$selectedElement} />
</FloatOver> </FloatOver>
{:else} {:else}
<FloatOver <FloatOver
@ -525,7 +550,7 @@
</div> </div>
<div slot="content2" class="m-2 flex flex-col"> <div slot="content2" class="m-2 flex flex-col">
<CopyrightPanel {state}/> <CopyrightPanel {state} />
</div> </div>
<div class="flex" slot="title3"> <div class="flex" slot="title3">
@ -659,7 +684,7 @@
<Tr t={Translations.t.general.menu.aboutMapComplete} /> <Tr t={Translations.t.general.menu.aboutMapComplete} />
</h2> </h2>
<AboutMapComplete {state} /> <AboutMapComplete {state} />
<CopyrightPanel {state}/> <CopyrightPanel {state} />
</div> </div>
</div> </div>
</FloatOver> </FloatOver>