MapComplete/src/Logic/State/GeoLocationState.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

196 lines
7.7 KiB
TypeScript
Raw Normal View History

import { Store, UIEventSource } from "../UIEventSource"
2023-09-28 23:50:27 +02:00
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { QueryParameters } from "../Web/QueryParameters"
2023-05-14 03:24:13 +02:00
export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied"
export interface GeoLocationPointProperties extends GeolocationCoordinates {
2023-09-28 23:50:27 +02:00
id: "gps"
"user:location": "yes"
date: string
}
/**
* An abstract representation of the current state of the geolocation.
*/
export class GeoLocationState {
/**
* What do we know about the current state of having access to the GPS?
* If 'prompt', then we just started and didn't request access yet
* 'requested' means the user tapped the 'locate me' button at least once
* 'granted' means that it is granted
* 'denied' means that we don't have access
*/
public readonly permission: UIEventSource<GeolocationPermissionState> = new UIEventSource(
"prompt"
2023-09-28 23:50:27 +02:00
)
/**
* 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
2023-02-09 03:12:21 +01:00
/**
* Important to determine e.g. if we move automatically on fix or not
*/
2023-09-28 23:50:27 +02:00
public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined)
/**
* If true: the map will center (and re-center) to the current GPS-location
*/
2023-09-28 23:50:27 +02:00
public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true)
2023-06-07 23:55:02 +02:00
/**
* The latest GeoLocationCoordinates, as given by the WebAPI
*/
public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> =
2023-09-28 23:50:27 +02:00
new UIEventSource<GeolocationCoordinates | undefined>(undefined)
/**
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
* On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions.
*
* Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately.
* If the user denies the geolocation this time, we unset this flag
* @private
*/
private readonly _previousLocationGrant: UIEventSource<boolean> = LocalStorageSource.GetParsed<boolean>("geolocation-permissions", false)
/**
* Used to detect a permission retraction
*/
2023-09-28 23:50:27 +02:00
private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false)
constructor() {
2023-09-28 23:50:27 +02:00
const self = this
this.permission.addCallbackAndRunD(async (state) => {
if (state === "granted") {
self._previousLocationGrant.setData(true)
2023-09-28 23:50:27 +02:00
self._grantedThisSession.setData(true)
}
if (state === "prompt" && self._grantedThisSession.data) {
// This is _really_ weird: we had a grant earlier, but it's 'prompt' now?
// This means that the rights have been revoked again!
self._previousLocationGrant.setData(false)
2023-09-28 23:50:27 +02:00
self.permission.setData("denied")
self.currentGPSLocation.setData(undefined)
console.warn("Detected a downgrade in permissions!")
}
if (state === "denied") {
self._previousLocationGrant.setData(false)
}
2023-09-28 23:50:27 +02:00
})
console.log("Previous location grant:", this._previousLocationGrant.data)
if (this._previousLocationGrant.data) {
// 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
this._previousLocationGrant.setData(false)
2023-09-28 23:50:27 +02:00
console.log("Requesting access to GPS as this was previously granted")
const latLonGivenViaUrl =
2023-09-28 23:50:27 +02:00
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon")
if (!latLonGivenViaUrl) {
2023-09-28 23:50:27 +02:00
this.requestMoment.setData(new Date())
}
2023-09-28 23:50:27 +02:00
this.requestPermission()
}
}
/**
* Requests the user to allow access to their position.
* When granted, will be written to the 'geolocationState'.
* This class will start watching
*/
public requestPermission() {
2023-09-30 15:30:11 +02:00
this.requestPermissionAsync()
}
2023-10-30 13:44:27 +01:00
public static isSafari(): boolean {
return navigator.permissions === undefined && navigator.geolocation !== undefined
}
2023-09-30 15:30:11 +02:00
/**
* Requests the user to allow access to their position.
* When granted, will be written to the 'geolocationState'.
* This class will start watching
*/
public async requestPermissionAsync() {
if (typeof navigator === "undefined") {
// Not compatible with this browser
2023-09-28 23:50:27 +02:00
this.permission.setData("denied")
return
}
if (this.permission.data !== "prompt" && this.permission.data !== "requested") {
// If the user denies the first prompt, revokes the deny and then tries again, we have to run the flow as well
// Hence that we continue the flow if it is "requested"
2023-09-28 23:50:27 +02:00
return
}
if (GeoLocationState.isSafari()) {
/*
This is probably safari
Safari does not support the 'permissions'-API for geolocation,
so we just start watching right away
*/
this.permission.setData("requested")
2023-10-17 16:43:50 +02:00
this.startWatching()
return
}
2023-09-28 23:50:27 +02:00
this.permission.setData("requested")
try {
2023-09-30 15:30:11 +02:00
const status = await navigator?.permissions?.query({ name: "geolocation" })
const self = this
console.log("Got geolocation state", status.state)
if (status.state === "granted" || status.state === "denied") {
self.permission.setData(status.state)
self.startWatching()
return
}
status.addEventListener("change", () => {
2023-09-30 15:30:11 +02:00
self.permission.setData(status.state)
})
// The code above might have reset it to 'prompt', but we _did_ request permission!
this.permission.setData("requested")
// We _must_ call 'startWatching', as that is the actual trigger for the popup...
self.startWatching()
} catch (e) {
2023-09-28 23:50:27 +02:00
console.error("Could not get permission:", e)
}
}
/**
* Installs the listener for updates
* @private
*/
private async startWatching() {
2023-09-28 23:50:27 +02:00
const self = this
navigator.geolocation.watchPosition(
2023-09-28 23:50:27 +02:00
function (position) {
self._gpsAvailable.set(true)
2023-09-28 23:50:27 +02:00
self.currentGPSLocation.setData(position.coords)
self._previousLocationGrant.setData(true)
},
2023-10-17 17:17:19 +02:00
function (e) {
if(e.code === 2 || e.code === 3){
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
}
2023-10-17 17:17:19 +02:00
console.warn("Could not get location with navigator.geolocation due to", e)
},
{
2023-09-28 23:50:27 +02:00
enableHighAccuracy: true,
}
2023-09-28 23:50:27 +02:00
)
}
}