forked from MapComplete/MapComplete
Refactor geolocation handler into multiple parts, fix lock behaviour
This commit is contained in:
parent
e10c9e87ca
commit
4de76136de
7 changed files with 362 additions and 372 deletions
|
@ -1,218 +1,123 @@
|
|||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import Svg from "../../Svg"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { VariableUiElement } from "../../UI/Base/VariableUIElement"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import { BBox } from "../BBox"
|
||||
import Constants from "../../Models/Constants"
|
||||
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"
|
||||
import { GeoLocationPointProperties, GeoLocationState } from "../State/GeoLocationState"
|
||||
import State from "../../State"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
|
||||
export interface GeoLocationPointProperties {
|
||||
id: "gps"
|
||||
"user:location": "yes"
|
||||
date: string
|
||||
latitude: number
|
||||
longitude: number
|
||||
speed: number
|
||||
accuracy: number
|
||||
heading: number
|
||||
altitude: number
|
||||
}
|
||||
/**
|
||||
* The geolocation-handler takes a map-location and a geolocation state.
|
||||
* It'll move the map as appropriate given the state of the geolocation-API
|
||||
*/
|
||||
export default class GeoLocationHandler {
|
||||
public readonly geolocationState: GeoLocationState
|
||||
private readonly _state: State
|
||||
public readonly mapHasMoved: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
|
||||
export default class GeoLocationHandler extends VariableUiElement {
|
||||
private readonly currentLocation?: SimpleFeatureSource
|
||||
|
||||
/**
|
||||
* Wether or not the geolocation is active, aka the user requested the current location
|
||||
*/
|
||||
private readonly _isActive: UIEventSource<boolean>
|
||||
|
||||
/**
|
||||
* Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user
|
||||
*/
|
||||
private readonly _isLocked: UIEventSource<boolean>
|
||||
|
||||
/**
|
||||
* The callback over the permission API
|
||||
* @private
|
||||
*/
|
||||
private readonly _permission: UIEventSource<string>
|
||||
/**
|
||||
* Literally: _currentGPSLocation.data != undefined
|
||||
* @private
|
||||
*/
|
||||
private readonly _hasLocation: Store<boolean>
|
||||
private readonly _currentGPSLocation: UIEventSource<GeolocationCoordinates>
|
||||
/**
|
||||
* Kept in order to update the marker
|
||||
* @private
|
||||
*/
|
||||
private readonly _leafletMap: UIEventSource<L.Map>
|
||||
|
||||
/**
|
||||
* The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs
|
||||
*/
|
||||
private _lastUserRequest: UIEventSource<Date>
|
||||
|
||||
/**
|
||||
* 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<string>
|
||||
private readonly _layoutToUse: LayoutConfig
|
||||
|
||||
constructor(state: {
|
||||
selectedElement: UIEventSource<any>
|
||||
currentUserLocation?: SimpleFeatureSource
|
||||
leafletMap: UIEventSource<any>
|
||||
layoutToUse: LayoutConfig
|
||||
featureSwitchGeolocation: UIEventSource<boolean>
|
||||
}) {
|
||||
const currentGPSLocation = new UIEventSource<GeolocationCoordinates>(
|
||||
undefined,
|
||||
"GPS-coordinate"
|
||||
)
|
||||
const leafletMap = state.leafletMap
|
||||
const initedAt = new Date()
|
||||
let autozoomDone = false
|
||||
const hasLocation = currentGPSLocation.map((location) => location !== undefined)
|
||||
const previousLocationGrant = LocalStorageSource.Get("geolocation-permissions")
|
||||
const isActive = new UIEventSource<boolean>(false)
|
||||
const isLocked = new UIEventSource<boolean>(false)
|
||||
const permission = new UIEventSource<string>("")
|
||||
const lastClick = new UIEventSource<Date>(undefined)
|
||||
const lastClickWithinThreeSecs = lastClick.map((lastClick) => {
|
||||
if (lastClick === undefined) {
|
||||
return false
|
||||
constructor(
|
||||
geolocationState: GeoLocationState,
|
||||
state: State // { locationControl: UIEventSource<Loc>, selectedElement: UIEventSource<any>, leafletMap?: UIEventSource<any> })
|
||||
) {
|
||||
this.geolocationState = geolocationState
|
||||
this._state = state
|
||||
const mapLocation = state.locationControl
|
||||
// Did an interaction move the map?
|
||||
let self = this
|
||||
let initTime = new Date()
|
||||
mapLocation.addCallbackD((_) => {
|
||||
if (new Date().getTime() - initTime.getTime() < 250) {
|
||||
return
|
||||
}
|
||||
const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000
|
||||
return timeDiff <= 3
|
||||
self.mapHasMoved.setData(true)
|
||||
return true // Unsubscribe
|
||||
})
|
||||
|
||||
const latLonGiven =
|
||||
QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon")
|
||||
const willFocus = lastClick.map((lastUserRequest) => {
|
||||
const timeDiffInited = (new Date().getTime() - initedAt.getTime()) / 1000
|
||||
if (!latLonGiven && !autozoomDone && timeDiffInited < Constants.zoomToLocationTimeout) {
|
||||
return true
|
||||
}
|
||||
if (lastUserRequest === undefined) {
|
||||
return false
|
||||
}
|
||||
const timeDiff = (new Date().getTime() - lastUserRequest.getTime()) / 1000
|
||||
return timeDiff <= Constants.zoomToLocationTimeout
|
||||
})
|
||||
const latLonGivenViaUrl =
|
||||
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon")
|
||||
if (latLonGivenViaUrl) {
|
||||
// The URL counts as a 'user interaction'
|
||||
this.mapHasMoved.setData(true)
|
||||
}
|
||||
|
||||
lastClick.addCallbackAndRunD((_) => {
|
||||
window.setTimeout(() => {
|
||||
if (lastClickWithinThreeSecs.data || willFocus.data) {
|
||||
lastClick.ping()
|
||||
this.geolocationState.currentGPSLocation.addCallbackAndRunD((newLocation) => {
|
||||
const timeSinceLastRequest =
|
||||
(new Date().getTime() - geolocationState.requestMoment.data?.getTime() ?? 0) / 1000
|
||||
if (!this.mapHasMoved.data) {
|
||||
// The map hasn't moved yet; we received our first coordinates, so let's move there!
|
||||
console.log(
|
||||
"Moving the map to an initial location; time since last request is",
|
||||
timeSinceLastRequest
|
||||
)
|
||||
if (timeSinceLastRequest < Constants.zoomToLocationTimeout) {
|
||||
self.MoveMapToCurrentLocation()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
if (this.geolocationState.isLocked.data) {
|
||||
// Jup, the map is locked to the bound location: move automatically
|
||||
self.MoveMapToCurrentLocation()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
super(
|
||||
hasLocation.map(
|
||||
(hasLocationData) => {
|
||||
if (permission.data === "denied") {
|
||||
return Svg.location_refused_svg()
|
||||
}
|
||||
|
||||
if (!isActive.data) {
|
||||
return Svg.location_empty_svg()
|
||||
}
|
||||
if (!hasLocationData) {
|
||||
// Position not yet found but we are active: we spin to indicate activity
|
||||
// If will focus is active too, we indicate this differently
|
||||
const icon = willFocus.data ? Svg.location_svg() : Svg.location_empty_svg()
|
||||
icon.SetStyle("animation: spin 4s linear infinite;")
|
||||
return icon
|
||||
}
|
||||
if (isLocked.data) {
|
||||
return Svg.location_locked_svg()
|
||||
}
|
||||
if (lastClickWithinThreeSecs.data) {
|
||||
return Svg.location_unlocked_svg()
|
||||
}
|
||||
|
||||
// We have a location, so we show a dot in the center
|
||||
return Svg.location_svg()
|
||||
},
|
||||
[isActive, isLocked, permission, lastClickWithinThreeSecs, willFocus]
|
||||
)
|
||||
)
|
||||
this.SetClass("mapcontrol")
|
||||
this._isActive = isActive
|
||||
this._isLocked = isLocked
|
||||
this._permission = permission
|
||||
this._previousLocationGrant = previousLocationGrant
|
||||
this._currentGPSLocation = currentGPSLocation
|
||||
this._leafletMap = leafletMap
|
||||
this._layoutToUse = state.layoutToUse
|
||||
this._hasLocation = hasLocation
|
||||
this._lastUserRequest = lastClick
|
||||
const self = this
|
||||
|
||||
const currentPointer = this._isActive.map(
|
||||
(isActive) => {
|
||||
if (isActive && !self._hasLocation.data) {
|
||||
return "cursor-wait"
|
||||
}
|
||||
return "cursor-pointer"
|
||||
},
|
||||
[this._hasLocation]
|
||||
)
|
||||
currentPointer.addCallbackAndRun((pointerClass) => {
|
||||
self.RemoveClass("cursor-wait")
|
||||
self.RemoveClass("cursor-pointer")
|
||||
self.SetClass(pointerClass)
|
||||
})
|
||||
|
||||
this.onClick(() => {
|
||||
/*
|
||||
* If the previous click was within 3 seconds (and we have an active location), then we lock to the location
|
||||
*/
|
||||
if (self._hasLocation.data) {
|
||||
if (isLocked.data) {
|
||||
isLocked.setData(false)
|
||||
} else if (lastClick.data !== undefined) {
|
||||
const timeDiff = (new Date().getTime() - lastClick.data.getTime()) / 1000
|
||||
if (timeDiff <= 3) {
|
||||
isLocked.setData(true)
|
||||
lastClick.setData(undefined)
|
||||
} else {
|
||||
lastClick.setData(new Date())
|
||||
}
|
||||
geolocationState.isLocked.map(
|
||||
(isLocked) => {
|
||||
if (isLocked) {
|
||||
state.leafletMap?.data?.dragging?.disable()
|
||||
} else {
|
||||
lastClick.setData(new Date())
|
||||
state.leafletMap?.data?.dragging?.enable()
|
||||
}
|
||||
},
|
||||
[state.leafletMap]
|
||||
)
|
||||
|
||||
this.CopyGeolocationIntoMapstate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the map to the GPS-location, except:
|
||||
* - If there is a selected element
|
||||
* - The location is out of the locked bound
|
||||
* - The GPS-location iss NULL-island
|
||||
* @constructor
|
||||
*/
|
||||
public MoveMapToCurrentLocation() {
|
||||
const newLocation = this.geolocationState.currentGPSLocation.data
|
||||
const mapLocation = this._state.locationControl
|
||||
const state = this._state
|
||||
// We got a new location.
|
||||
// Do we move the map to it?
|
||||
|
||||
if (state.selectedElement.data !== undefined) {
|
||||
// Nope, there is something selected, so we don't move to the current GPS-location
|
||||
return
|
||||
}
|
||||
if (newLocation.latitude === 0 && newLocation.longitude === 0) {
|
||||
console.debug("Not moving to GPS-location: it is null island")
|
||||
return
|
||||
}
|
||||
|
||||
// We check that the GPS location is not out of bounds
|
||||
const bounds = state.layoutToUse.lockLocation
|
||||
if (bounds && bounds !== true) {
|
||||
// B is an array with our lock-location
|
||||
const inRange = new BBox(bounds).contains([newLocation.longitude, newLocation.latitude])
|
||||
if (!inRange) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.init(true, true)
|
||||
mapLocation.setData({
|
||||
zoom: mapLocation.data.zoom,
|
||||
lon: newLocation.longitude,
|
||||
lat: newLocation.latitude,
|
||||
})
|
||||
this.mapHasMoved.setData(true)
|
||||
}
|
||||
|
||||
const doAutoZoomToLocation =
|
||||
!latLonGiven &&
|
||||
state.featureSwitchGeolocation.data &&
|
||||
state.selectedElement.data !== undefined
|
||||
this.init(false, doAutoZoomToLocation)
|
||||
|
||||
isLocked.addCallbackAndRunD((isLocked) => {
|
||||
if (isLocked) {
|
||||
leafletMap.data?.dragging?.disable()
|
||||
} else {
|
||||
leafletMap.data?.dragging?.enable()
|
||||
}
|
||||
})
|
||||
|
||||
this.currentLocation = state.currentUserLocation
|
||||
this._currentGPSLocation.addCallback((location) => {
|
||||
self._previousLocationGrant.setData("granted")
|
||||
private CopyGeolocationIntoMapstate() {
|
||||
const state = this._state
|
||||
this.geolocationState.currentGPSLocation.addCallback((location) => {
|
||||
const feature = {
|
||||
type: "Feature",
|
||||
properties: <GeoLocationPointProperties>{
|
||||
|
@ -232,164 +137,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
},
|
||||
}
|
||||
|
||||
self.currentLocation?.features?.setData([{ feature, freshness: new Date() }])
|
||||
|
||||
if (willFocus.data) {
|
||||
console.log("Zooming to user location: willFocus is set")
|
||||
lastClick.setData(undefined)
|
||||
autozoomDone = true
|
||||
self.MoveToCurrentLocation(16)
|
||||
} else if (self._isLocked.data) {
|
||||
self.MoveToCurrentLocation()
|
||||
}
|
||||
state.currentUserLocation?.features?.setData([{ feature, freshness: new Date() }])
|
||||
})
|
||||
}
|
||||
|
||||
private init(askPermission: boolean, zoomToLocation: boolean) {
|
||||
const self = this
|
||||
|
||||
if (self._isActive.data) {
|
||||
self.MoveToCurrentLocation(16)
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof navigator === "undefined") {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
navigator?.permissions?.query({ name: "geolocation" })?.then(function (status) {
|
||||
console.log("Geolocation permission is ", status.state)
|
||||
if (status.state === "granted") {
|
||||
self.StartGeolocating(zoomToLocation)
|
||||
}
|
||||
self._permission.setData(status.state)
|
||||
status.onchange = function () {
|
||||
self._permission.setData(status.state)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
if (askPermission) {
|
||||
self.StartGeolocating(zoomToLocation)
|
||||
} else if (this._previousLocationGrant.data === "granted") {
|
||||
this._previousLocationGrant.setData("")
|
||||
self.StartGeolocating(zoomToLocation)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves to the currently loaded location.
|
||||
*
|
||||
* // Should move to any location
|
||||
* let resultingLocation = undefined
|
||||
* let resultingzoom = 1
|
||||
* const state = {
|
||||
* selectedElement: new UIEventSource<any>(undefined);
|
||||
* currentUserLocation: undefined ,
|
||||
* leafletMap: new UIEventSource<any>({getZoom: () => resultingzoom; setView: (loc, zoom) => {resultingLocation = loc; resultingzoom = zoom}),
|
||||
* layoutToUse: new LayoutConfig(<any>{
|
||||
* id: 'test',
|
||||
* title: {"en":"test"}
|
||||
* description: "A testing theme",
|
||||
* layers: []
|
||||
* }),
|
||||
* featureSwitchGeolocation : new UIEventSource<boolean>(true)
|
||||
* }
|
||||
* const handler = new GeoLocationHandler(state)
|
||||
* handler._currentGPSLocation.setData(<any> {latitude : 51.3, longitude: 4.1})
|
||||
* handler.MoveToCurrentLocation()
|
||||
* resultingLocation // => [51.3, 4.1]
|
||||
* handler._currentGPSLocation.setData(<any> {latitude : 60, longitude: 60) // out of bounds
|
||||
* handler.MoveToCurrentLocation()
|
||||
* resultingLocation // => [60, 60]
|
||||
*
|
||||
* // should refuse to move if out of bounds
|
||||
* let resultingLocation = undefined
|
||||
* let resultingzoom = 1
|
||||
* const state = {
|
||||
* selectedElement: new UIEventSource<any>(undefined);
|
||||
* currentUserLocation: undefined ,
|
||||
* leafletMap: new UIEventSource<any>({getZoom: () => resultingzoom; setView: (loc, zoom) => {resultingLocation = loc; resultingzoom = zoom}),
|
||||
* layoutToUse: new LayoutConfig(<any>{
|
||||
* id: 'test',
|
||||
* title: {"en":"test"}
|
||||
* "lockLocation": [ [ 2.1, 50.4], [6.4, 51.54 ]],
|
||||
* description: "A testing theme",
|
||||
* layers: []
|
||||
* }),
|
||||
* featureSwitchGeolocation : new UIEventSource<boolean>(true)
|
||||
* }
|
||||
* const handler = new GeoLocationHandler(state)
|
||||
* handler._currentGPSLocation.setData(<any> {latitude : 51.3, longitude: 4.1})
|
||||
* handler.MoveToCurrentLocation()
|
||||
* resultingLocation // => [51.3, 4.1]
|
||||
* handler._currentGPSLocation.setData(<any> {latitude : 60, longitude: 60) // out of bounds
|
||||
* handler.MoveToCurrentLocation()
|
||||
* resultingLocation // => [51.3, 4.1]
|
||||
*/
|
||||
private MoveToCurrentLocation(targetZoom?: number) {
|
||||
const location = this._currentGPSLocation.data
|
||||
this._lastUserRequest.setData(undefined)
|
||||
|
||||
if (
|
||||
this._currentGPSLocation.data.latitude === 0 &&
|
||||
this._currentGPSLocation.data.longitude === 0
|
||||
) {
|
||||
console.debug("Not moving to GPS-location: it is null island")
|
||||
return
|
||||
}
|
||||
|
||||
// We check that the GPS location is not out of bounds
|
||||
const b = this._layoutToUse.lockLocation
|
||||
let inRange = true
|
||||
if (b) {
|
||||
if (b !== true) {
|
||||
// B is an array with our locklocation
|
||||
inRange = new BBox(b).contains([location.longitude, location.latitude])
|
||||
}
|
||||
}
|
||||
if (!inRange) {
|
||||
console.log("Not zooming to GPS location: out of bounds", b, location)
|
||||
} else {
|
||||
const currentZoom = this._leafletMap.data.getZoom()
|
||||
this._leafletMap.data.setView(
|
||||
[location.latitude, location.longitude],
|
||||
Math.max(targetZoom ?? 0, currentZoom)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private StartGeolocating(zoomToGPS = true) {
|
||||
const self = this
|
||||
|
||||
this._lastUserRequest.setData(zoomToGPS ? new Date() : new Date(0))
|
||||
if (self._permission.data === "denied") {
|
||||
self._previousLocationGrant.setData("")
|
||||
self._isActive.setData(false)
|
||||
return ""
|
||||
}
|
||||
if (this._currentGPSLocation.data !== undefined) {
|
||||
this.MoveToCurrentLocation(16)
|
||||
}
|
||||
|
||||
if (self._isActive.data) {
|
||||
return
|
||||
}
|
||||
self._isActive.setData(true)
|
||||
|
||||
navigator.geolocation.watchPosition(
|
||||
function (position) {
|
||||
self._currentGPSLocation.setData(position.coords)
|
||||
},
|
||||
function () {
|
||||
console.warn("Could not get location with navigator.geolocation")
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { LocalStorageSource } from "../Web/LocalStorageSource"
|
|||
import SimpleMetaTagger from "../SimpleMetaTagger"
|
||||
import FeatureSource from "../FeatureSource/FeatureSource"
|
||||
import { ElementStorage } from "../ElementStorage"
|
||||
import { GeoLocationPointProperties } from "../Actors/GeoLocationHandler"
|
||||
import { GeoLocationPointProperties } from "../State/GeoLocationState"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler"
|
||||
import { OsmConnection } from "./OsmConnection"
|
||||
|
|
138
Logic/State/GeoLocationState.ts
Normal file
138
Logic/State/GeoLocationState.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
import { UIEventSource } from "../UIEventSource"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
|
||||
type GeolocationState = "prompt" | "requested" | "granted" | "denied"
|
||||
|
||||
export interface GeoLocationPointProperties extends GeolocationCoordinates {
|
||||
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<GeolocationState> = new UIEventSource("prompt")
|
||||
|
||||
public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined)
|
||||
/**
|
||||
* If true: the map will center (and re-center) to this location
|
||||
*/
|
||||
public readonly isLocked: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
|
||||
public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> =
|
||||
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<"true" | "false"> = <any>(
|
||||
LocalStorageSource.Get("geolocation-permissions")
|
||||
)
|
||||
|
||||
/**
|
||||
* Used to detect a permission retraction
|
||||
*/
|
||||
private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
constructor() {
|
||||
const self = this
|
||||
|
||||
this.permission.addCallbackAndRunD(async (state) => {
|
||||
if (state === "granted") {
|
||||
self._previousLocationGrant.setData("true")
|
||||
self._grantedThisSession.setData(true)
|
||||
await self.startWatching()
|
||||
}
|
||||
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.permission.setData("denied")
|
||||
self._previousLocationGrant.setData("false")
|
||||
self.permission.setData("denied")
|
||||
console.warn("Detected a downgrade in permissions!")
|
||||
}
|
||||
if (state === "denied") {
|
||||
self._previousLocationGrant.setData("false")
|
||||
}
|
||||
})
|
||||
|
||||
if (this._previousLocationGrant.data === "true") {
|
||||
// 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")
|
||||
console.log("Requesting access to GPS as this was previously granted")
|
||||
this.requestPermission()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the listener for updates
|
||||
* @private
|
||||
*/
|
||||
private async startWatching() {
|
||||
const self = this
|
||||
navigator.geolocation.watchPosition(
|
||||
function (position) {
|
||||
self.currentGPSLocation.setData(position.coords)
|
||||
},
|
||||
function () {
|
||||
console.warn("Could not get location with navigator.geolocation")
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the user to allow access to their position.
|
||||
* When granted, will be written to the 'geolocationState'.
|
||||
* This class will start watching
|
||||
*/
|
||||
public requestPermission() {
|
||||
if (typeof navigator === "undefined") {
|
||||
// Not compatible with this browser
|
||||
this.permission.setData("denied")
|
||||
return
|
||||
}
|
||||
if (this.permission.data !== "prompt") {
|
||||
return
|
||||
}
|
||||
this.requestMoment.setData(new Date())
|
||||
this.permission.setData("requested")
|
||||
try {
|
||||
navigator?.permissions
|
||||
?.query({ name: "geolocation" })
|
||||
.then((status) => {
|
||||
console.log("Geolocation permission is ", status.state)
|
||||
this.permission.setData(status.state)
|
||||
const self = this
|
||||
status.onchange = function () {
|
||||
self.permission.setData(status.state)
|
||||
}
|
||||
// We _must_ call 'startWatching', as that is the actual trigger for the popup...
|
||||
self.startWatching()
|
||||
})
|
||||
.catch((e) => console.error("Could not get geopermission", e))
|
||||
} catch (e) {
|
||||
console.error("Could not get permission:", e)
|
||||
}
|
||||
}
|
||||
}
|
99
UI/BigComponents/GeolocationControl.ts
Normal file
99
UI/BigComponents/GeolocationControl.ts
Normal file
|
@ -0,0 +1,99 @@
|
|||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Svg from "../../Svg"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
|
||||
|
||||
/**
|
||||
* Displays an icon depending on the state of the geolocation.
|
||||
* Will set the 'lock' if clicked twice
|
||||
*/
|
||||
export class GeolocationControl extends VariableUiElement {
|
||||
constructor(geolocationHandler: GeoLocationHandler) {
|
||||
const lastClick = new UIEventSource<Date>(undefined)
|
||||
const lastClickWithinThreeSecs = lastClick.map((lastClick) => {
|
||||
if (lastClick === undefined) {
|
||||
return false
|
||||
}
|
||||
const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000
|
||||
return timeDiff <= 3
|
||||
})
|
||||
const geolocationState = geolocationHandler.geolocationState
|
||||
super(
|
||||
geolocationState.permission.map(
|
||||
(permission) => {
|
||||
if (permission === "denied") {
|
||||
return Svg.location_refused_svg()
|
||||
}
|
||||
if (geolocationState.isLocked.data) {
|
||||
return Svg.location_locked_svg()
|
||||
}
|
||||
|
||||
if (permission === "prompt") {
|
||||
return Svg.location_empty_svg()
|
||||
}
|
||||
if (geolocationState.currentGPSLocation === undefined) {
|
||||
// Position not yet found, but permission is either requested or granted: we spin to indicate activity
|
||||
const icon = !geolocationHandler.mapHasMoved.data
|
||||
? Svg.location_svg()
|
||||
: Svg.location_empty_svg()
|
||||
return icon
|
||||
.SetClass("cursor-wait")
|
||||
.SetStyle("animation: spin 4s linear infinite;")
|
||||
}
|
||||
|
||||
if (
|
||||
lastClickWithinThreeSecs.data &&
|
||||
geolocationState.permission.data === "granted"
|
||||
) {
|
||||
return Svg.location_unlocked_svg()
|
||||
}
|
||||
|
||||
// We have a location, so we show a dot in the center
|
||||
return Svg.location_svg()
|
||||
},
|
||||
[
|
||||
geolocationState.currentGPSLocation,
|
||||
geolocationState.isLocked,
|
||||
geolocationHandler.mapHasMoved,
|
||||
lastClickWithinThreeSecs,
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
this.onClick(async () => {
|
||||
if (geolocationState.permission.data !== "granted") {
|
||||
await geolocationState.requestPermission()
|
||||
}
|
||||
|
||||
if (geolocationState.isLocked.data === true) {
|
||||
// Unlock
|
||||
geolocationState.isLocked.setData(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (geolocationState.currentGPSLocation.data === undefined) {
|
||||
// No location is known yet, not much we can do
|
||||
return
|
||||
}
|
||||
|
||||
// A location _is_ known! Let's zoom to this location
|
||||
geolocationHandler.MoveMapToCurrentLocation()
|
||||
|
||||
if (lastClickWithinThreeSecs.data && geolocationState.permission.data === "granted") {
|
||||
geolocationState.isLocked.setData(true)
|
||||
lastClick.setData(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
lastClick.setData(new Date())
|
||||
})
|
||||
|
||||
lastClick.addCallbackAndRunD((_) => {
|
||||
window.setTimeout(() => {
|
||||
if (lastClickWithinThreeSecs.data) {
|
||||
lastClick.ping()
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -5,18 +5,16 @@ import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
|
|||
import Svg from "../../Svg"
|
||||
import MapState from "../../Logic/State/MapState"
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
||||
import { Utils } from "../../Utils"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { OsmFeature } from "../../Models/OsmFeature"
|
||||
import LevelSelector from "./LevelSelector"
|
||||
import { GeolocationControl } from "./GeolocationControl"
|
||||
|
||||
export default class RightControls extends Combine {
|
||||
constructor(state: MapState & { featurePipeline: FeaturePipeline }) {
|
||||
const geolocatioHandler = new GeoLocationHandler(state)
|
||||
|
||||
constructor(
|
||||
state: MapState & { featurePipeline: FeaturePipeline },
|
||||
geolocationHandler: GeoLocationHandler
|
||||
) {
|
||||
const geolocationButton = new Toggle(
|
||||
new MapControlButton(geolocatioHandler, {
|
||||
new MapControlButton(new GeolocationControl(geolocationHandler), {
|
||||
dontStyle: true,
|
||||
}).SetClass("p-1"),
|
||||
undefined,
|
||||
|
|
|
@ -29,6 +29,8 @@ import Img from "./Base/Img"
|
|||
import UserInformationPanel from "./BigComponents/UserInformation"
|
||||
import { LoginToggle } from "./Popup/LoginButton"
|
||||
import { FixedUiElement } from "./Base/FixedUiElement"
|
||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||
import { GeoLocationState } from "../Logic/State/GeoLocationState"
|
||||
|
||||
/**
|
||||
* The default MapComplete GUI initializer
|
||||
|
@ -38,10 +40,14 @@ import { FixedUiElement } from "./Base/FixedUiElement"
|
|||
export default class DefaultGUI {
|
||||
private readonly guiState: DefaultGuiState
|
||||
private readonly state: FeaturePipelineState
|
||||
private readonly geolocationHandler: GeoLocationHandler | undefined
|
||||
|
||||
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
|
||||
this.state = state
|
||||
this.guiState = guiState
|
||||
if (this.state.featureSwitchGeolocation.data) {
|
||||
this.geolocationHandler = new GeoLocationHandler(new GeoLocationState(), state)
|
||||
}
|
||||
}
|
||||
|
||||
public setup() {
|
||||
|
@ -231,15 +237,16 @@ export default class DefaultGUI {
|
|||
.SetClass("flex items-center justify-center normal-background h-full")
|
||||
.AttachTo("on-small-screen")
|
||||
|
||||
new Combine([Toggle.If(state.featureSwitchSearch, () =>
|
||||
new SearchAndGo(state).SetClass(
|
||||
"shadow rounded-full h-min w-full overflow-hidden sm:max-w-sm pointer-events-auto"
|
||||
)
|
||||
)])
|
||||
.AttachTo("top-right")
|
||||
new Combine([
|
||||
Toggle.If(state.featureSwitchSearch, () =>
|
||||
new SearchAndGo(state).SetClass(
|
||||
"shadow rounded-full h-min w-full overflow-hidden sm:max-w-sm pointer-events-auto"
|
||||
)
|
||||
),
|
||||
]).AttachTo("top-right")
|
||||
|
||||
new LeftControls(state, guiState).AttachTo("bottom-left")
|
||||
new RightControls(state).AttachTo("bottom-right")
|
||||
new RightControls(state, this.geolocationHandler).AttachTo("bottom-right")
|
||||
|
||||
new CenterMessageBox(state).AttachTo("centermessage")
|
||||
document.getElementById("centermessage").classList.add("pointer-events-none")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import { Feature } from "geojson"
|
||||
import { Point } from "@turf/turf"
|
||||
import { GeoLocationPointProperties } from "../../Logic/Actors/GeoLocationHandler"
|
||||
import { GeoLocationPointProperties } from "../../Logic/State/GeoLocationState"
|
||||
import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
|
||||
|
|
Loading…
Reference in a new issue