diff --git a/src/App.tsx b/src/App.tsx
index f3c47eb7..2f539c80 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -11,6 +11,7 @@ import {
getQueryStore,
getRouteStore,
getSettingsStore,
+ getCurrentLocationStore,
} from '@/stores/Stores'
import MapComponent from '@/map/MapComponent'
import MapOptions from '@/map/MapOptions'
@@ -22,6 +23,7 @@ import { QueryStoreState, RequestState } from '@/stores/QueryStore'
import { RouteStoreState } from '@/stores/RouteStore'
import { MapOptionsStoreState } from '@/stores/MapOptionsStore'
import { ErrorStoreState } from '@/stores/ErrorStore'
+import { CurrentLocationStoreState } from '@/stores/CurrentLocationStore'
import Search from '@/sidebar/search/Search'
import ErrorMessage from '@/sidebar/ErrorMessage'
import useBackgroundLayer from '@/layers/UseBackgroundLayer'
@@ -45,6 +47,7 @@ import useExternalMVTLayer from '@/layers/UseExternalMVTLayer'
import LocationButton from '@/map/LocationButton'
import { SettingsContext } from '@/contexts/SettingsContext'
import usePOIsLayer from '@/layers/UsePOIsLayer'
+import useCurrentLocationLayer from '@/layers/UseCurrentLocationLayer'
export const POPUP_CONTAINER_ID = 'popup-container'
export const SIDEBAR_CONTENT_ID = 'sidebar-content'
@@ -59,6 +62,7 @@ export default function App() {
const [pathDetails, setPathDetails] = useState(getPathDetailsStore().state)
const [mapFeatures, setMapFeatures] = useState(getMapFeatureStore().state)
const [pois, setPOIs] = useState(getPOIsStore().state)
+ const [currentLocation, setCurrentLocation] = useState(getCurrentLocationStore().state)
const map = getMap()
@@ -72,6 +76,7 @@ export default function App() {
const onPathDetailsChanged = () => setPathDetails(getPathDetailsStore().state)
const onMapFeaturesChanged = () => setMapFeatures(getMapFeatureStore().state)
const onPOIsChanged = () => setPOIs(getPOIsStore().state)
+ const onCurrentLocationChanged = () => setCurrentLocation(getCurrentLocationStore().state)
getSettingsStore().register(onSettingsChanged)
getQueryStore().register(onQueryChanged)
@@ -82,6 +87,7 @@ export default function App() {
getPathDetailsStore().register(onPathDetailsChanged)
getMapFeatureStore().register(onMapFeaturesChanged)
getPOIsStore().register(onPOIsChanged)
+ getCurrentLocationStore().register(onCurrentLocationChanged)
onQueryChanged()
onInfoChanged()
@@ -91,6 +97,7 @@ export default function App() {
onPathDetailsChanged()
onMapFeaturesChanged()
onPOIsChanged()
+ onCurrentLocationChanged()
return () => {
getSettingsStore().deregister(onSettingsChanged)
@@ -102,6 +109,7 @@ export default function App() {
getPathDetailsStore().deregister(onPathDetailsChanged)
getMapFeatureStore().deregister(onMapFeaturesChanged)
getPOIsStore().deregister(onPOIsChanged)
+ getCurrentLocationStore().deregister(onCurrentLocationChanged)
}
}, [])
@@ -116,6 +124,7 @@ export default function App() {
useQueryPointsLayer(map, query.queryPoints)
usePathDetailsLayer(map, pathDetails)
usePOIsLayer(map, pois)
+ useCurrentLocationLayer(map, currentLocation)
const isSmallScreen = useMediaQuery({ query: '(max-width: 44rem)' })
return (
@@ -138,6 +147,7 @@ export default function App() {
error={error}
encodedValues={info.encoded_values}
drawAreas={settings.drawAreasEnabled}
+ currentLocation={currentLocation}
/>
) : (
)}
@@ -160,12 +171,22 @@ interface LayoutProps {
route: RouteStoreState
map: Map
mapOptions: MapOptionsStoreState
+ currentLocation: CurrentLocationStoreState
error: ErrorStoreState
encodedValues: object[]
drawAreas: boolean
}
-function LargeScreenLayout({ query, route, map, error, mapOptions, encodedValues, drawAreas }: LayoutProps) {
+function LargeScreenLayout({
+ query,
+ route,
+ map,
+ error,
+ mapOptions,
+ encodedValues,
+ drawAreas,
+ currentLocation,
+}: LayoutProps) {
const [showSidebar, setShowSidebar] = useState(true)
const [showCustomModelBox, setShowCustomModelBox] = useState(false)
return (
@@ -216,7 +237,7 @@ function LargeScreenLayout({ query, route, map, error, mapOptions, encodedValues
-
+
@@ -229,7 +250,16 @@ function LargeScreenLayout({ query, route, map, error, mapOptions, encodedValues
)
}
-function SmallScreenLayout({ query, route, map, error, mapOptions, encodedValues, drawAreas }: LayoutProps) {
+function SmallScreenLayout({
+ query,
+ route,
+ map,
+ error,
+ mapOptions,
+ encodedValues,
+ drawAreas,
+ currentLocation,
+}: LayoutProps) {
return (
<>
@@ -248,7 +278,7 @@ function SmallScreenLayout({ query, route, map, error, mapOptions, encodedValues
diff --git a/src/actions/Actions.ts b/src/actions/Actions.ts
index 466b1282..bc20514a 100644
--- a/src/actions/Actions.ts
+++ b/src/actions/Actions.ts
@@ -189,7 +189,7 @@ export class ToggleExternalMVTLayer implements Action {
export class MapIsLoaded implements Action {}
-export class ZoomMapToPoint implements Action {
+export class MoveMapToPoint implements Action {
readonly coordinate: Coordinate
constructor(coordinate: Coordinate) {
@@ -272,3 +272,35 @@ export class SetPOIs implements Action {
this.pois = pois
}
}
+
+/**
+ * Start watching the location and synchronizing the view.
+ */
+export class StartWatchCurrentLocation implements Action {}
+export class StopWatchCurrentLocation implements Action {}
+
+/**
+ * Start synchronizing the view again.
+ */
+export class StartSyncCurrentLocation implements Action {}
+export class StopSyncCurrentLocation implements Action {}
+
+export class CurrentLocationError implements Action {
+ readonly error: string
+
+ constructor(error: string) {
+ this.error = error
+ }
+}
+
+export class CurrentLocation implements Action {
+ readonly coordinate: Coordinate
+ readonly accuracy: number
+ readonly heading: number | null
+
+ constructor(coordinate: Coordinate, accuracy: number, heading: number | null) {
+ this.coordinate = coordinate
+ this.accuracy = accuracy
+ this.heading = heading
+ }
+}
diff --git a/src/index.tsx b/src/index.tsx
index ee8937a4..81842587 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -13,6 +13,7 @@ import {
getQueryStore,
getRouteStore,
getSettingsStore,
+ getCurrentLocationStore,
setStores,
} from '@/stores/Stores'
import Dispatcher from '@/stores/Dispatcher'
@@ -31,6 +32,7 @@ import MapFeatureStore from '@/stores/MapFeatureStore'
import SettingsStore from '@/stores/SettingsStore'
import { ErrorAction, InfoReceived } from '@/actions/Actions'
import POIsStore from '@/stores/POIsStore'
+import CurrentLocationStore from '@/stores/CurrentLocationStore'
import { setDistanceFormat } from '@/Converters'
import { AddressParseResult } from '@/pois/AddressParseResult'
@@ -61,6 +63,7 @@ setStores({
pathDetailsStore: new PathDetailsStore(),
mapFeatureStore: new MapFeatureStore(),
poisStore: new POIsStore(),
+ currentLocationStore: new CurrentLocationStore(),
})
setMap(createMap())
@@ -75,6 +78,7 @@ Dispatcher.register(getMapOptionsStore())
Dispatcher.register(getPathDetailsStore())
Dispatcher.register(getMapFeatureStore())
Dispatcher.register(getPOIsStore())
+Dispatcher.register(getCurrentLocationStore())
// register map action receiver
const smallScreenMediaQuery = window.matchMedia('(max-width: 44rem)')
diff --git a/src/layers/UseCurrentLocationLayer.tsx b/src/layers/UseCurrentLocationLayer.tsx
new file mode 100644
index 00000000..bd0d5f8c
--- /dev/null
+++ b/src/layers/UseCurrentLocationLayer.tsx
@@ -0,0 +1,129 @@
+import { Feature, Map } from 'ol'
+import { useEffect, useRef } from 'react'
+import VectorLayer from 'ol/layer/Vector'
+import VectorSource from 'ol/source/Vector'
+import { Circle, Circle as CircleGeom, Point } from 'ol/geom'
+import { Circle as CircleStyle, Fill, RegularShape, Stroke, Style } from 'ol/style'
+import { CurrentLocationStoreState } from '@/stores/CurrentLocationStore'
+import { fromLonLat } from 'ol/proj'
+
+export default function useCurrentLocationLayer(map: Map, locationState: CurrentLocationStoreState) {
+ const layerRef = useRef
| null>(null)
+ const positionFeatureRef = useRef(null)
+ const accuracyFeatureRef = useRef(null)
+ const headingFeatureRef = useRef(null)
+
+ useEffect(() => {
+ if (!locationState.enabled) {
+ if (layerRef.current) {
+ map.removeLayer(layerRef.current)
+ layerRef.current = null
+ }
+ return
+ } else if (!layerRef.current) {
+ const layer = createLocationLayer()
+ layer.getSource()?.addFeature((positionFeatureRef.current = new Feature()))
+ layer.getSource()?.addFeature((accuracyFeatureRef.current = new Feature()))
+ layer.getSource()?.addFeature((headingFeatureRef.current = new Feature()))
+ map.addLayer(layer)
+
+ layerRef.current = layer
+ }
+
+ return () => {
+ if (layerRef.current) {
+ map.removeLayer(layerRef.current)
+ layerRef.current = null
+ }
+ }
+ }, [locationState.enabled])
+
+ useEffect(() => {
+ if (
+ !locationState.enabled ||
+ !locationState.coordinate ||
+ !layerRef.current ||
+ // typescript complaints without the following
+ !positionFeatureRef.current ||
+ !accuracyFeatureRef.current ||
+ !headingFeatureRef.current
+ )
+ return
+
+ const coord = fromLonLat([locationState.coordinate.lng, locationState.coordinate.lat])
+ positionFeatureRef.current.setGeometry(new Point(coord))
+ accuracyFeatureRef.current.setGeometry(new Circle(coord, locationState.accuracy))
+
+ // set heading feature position (style will handle the triangle and rotation)
+ if (locationState.heading != null) {
+ headingFeatureRef.current.setGeometry(new Point(coord))
+ headingFeatureRef.current.set('heading', locationState.heading)
+ } else {
+ headingFeatureRef.current.setGeometry(undefined)
+ headingFeatureRef.current.unset('heading') // not strictly necessary
+ }
+
+ if (locationState.syncView) {
+ const currentZoom = map.getView().getZoom()
+ const targetZoom = currentZoom == undefined || currentZoom < 16 ? 16 : currentZoom
+ const zoomDifference = Math.abs(targetZoom - (currentZoom || 0))
+ if (zoomDifference > 0.1) {
+ map.getView().animate({ zoom: targetZoom, center: coord, duration: 400 })
+ } else {
+ // for smaller zoom changes set center without animation to avoid pulsing of map
+ map.getView().setCenter(coord)
+ }
+ }
+ }, [
+ locationState.coordinate,
+ locationState.accuracy,
+ locationState.heading,
+ locationState.syncView,
+ locationState.enabled,
+ ])
+}
+
+function createLocationLayer(): VectorLayer {
+ return new VectorLayer({
+ source: new VectorSource(),
+ style: feature => {
+ const geometry = feature.getGeometry()
+ if (geometry instanceof Point) {
+ const heading = feature.get('heading')
+ if (heading !== undefined) {
+ // triangle style for heading direction
+ return new Style({
+ image: new RegularShape({
+ points: 3,
+ radius: 8,
+ displacement: [0, 9],
+ rotation: (heading * Math.PI) / 180, // convert degrees to radians
+ fill: new Fill({ color: '#368fe8' }),
+ stroke: new Stroke({ color: '#FFFFFF', width: 1 }),
+ }),
+ zIndex: 1,
+ })
+ } else {
+ // blue dot style for position
+ return new Style({
+ image: new CircleStyle({
+ radius: 8,
+ fill: new Fill({ color: '#368fe8' }),
+ stroke: new Stroke({ color: '#FFFFFF', width: 2 }),
+ }),
+ zIndex: 2, // above the others
+ })
+ }
+ } else if (geometry instanceof CircleGeom) {
+ // accuracy circle style
+ return new Style({
+ fill: new Fill({ color: 'rgba(66, 133, 244, 0.1)' }),
+ stroke: new Stroke({ color: 'rgba(66, 133, 244, 0.3)', width: 1 }),
+ zIndex: 0, // behind the others
+ })
+ }
+ return []
+ },
+ zIndex: 4, // layer itself should be above paths and query points
+ })
+}
diff --git a/src/layers/UseQueryPointsLayer.tsx b/src/layers/UseQueryPointsLayer.tsx
index 48973ea8..d21e4f1a 100644
--- a/src/layers/UseQueryPointsLayer.tsx
+++ b/src/layers/UseQueryPointsLayer.tsx
@@ -85,7 +85,7 @@ function removeDragInteractions(map: Map) {
.forEach(i => map.removeInteraction(i))
}
-function addDragInteractions(map: Map, queryPointsLayer: VectorLayer>>) {
+function addDragInteractions(map: Map, queryPointsLayer: VectorLayer) {
let tmp = queryPointsLayer.getSource()
if (tmp == null) throw new Error('source must not be null') // typescript requires this
const modify = new Modify({
diff --git a/src/map/ContextMenuContent.tsx b/src/map/ContextMenuContent.tsx
index 9c142725..4fdcc011 100644
--- a/src/map/ContextMenuContent.tsx
+++ b/src/map/ContextMenuContent.tsx
@@ -3,7 +3,7 @@ import { coordinateToText } from '@/Converters'
import styles from './ContextMenuContent.module.css'
import QueryStore, { QueryPoint, QueryPointType } from '@/stores/QueryStore'
import Dispatcher from '@/stores/Dispatcher'
-import { AddPoint, SetPoint, ZoomMapToPoint } from '@/actions/Actions'
+import { AddPoint, SetPoint, MoveMapToPoint } from '@/actions/Actions'
import { RouteStoreState } from '@/stores/RouteStore'
import { findNextWayPoint } from '@/map/findNextWayPoint'
import { tr } from '@/translation/Translation'
@@ -143,7 +143,7 @@ export function ContextMenuContent({
className={styles.entry}
onClick={() => {
onSelect()
- Dispatcher.dispatch(new ZoomMapToPoint(coordinate))
+ Dispatcher.dispatch(new MoveMapToPoint(coordinate))
}}
>
{tr('center_map')}
diff --git a/src/map/LocationButton.tsx b/src/map/LocationButton.tsx
index 4bbdd108..fa9ddd72 100644
--- a/src/map/LocationButton.tsx
+++ b/src/map/LocationButton.tsx
@@ -1,48 +1,36 @@
import styles from './LocationButton.module.css'
-import { onCurrentLocationButtonClicked } from '@/map/MapComponent'
import Dispatcher from '@/stores/Dispatcher'
-import { SetBBox, SetPoint, ZoomMapToPoint } from '@/actions/Actions'
-import { QueryPoint, QueryPointType } from '@/stores/QueryStore'
+import { StartSyncCurrentLocation, StartWatchCurrentLocation, StopWatchCurrentLocation } from '@/actions/Actions'
import LocationError from '@/map/location_error.svg'
import LocationSearching from '@/map/location_searching.svg'
import LocationOn from '@/map/location_on.svg'
-import { useState } from 'react'
-import { tr } from '@/translation/Translation'
-import { getBBoxFromCoord } from '@/utils'
+import Location from '@/map/location.svg'
+import LocationNotInSync from '@/map/location_not_in_sync.svg'
+import { CurrentLocationStoreState } from '@/stores/CurrentLocationStore'
-export default function LocationButton(props: { queryPoints: QueryPoint[] }) {
- const [locationSearch, setLocationSearch] = useState('synched_map_or_initial')
+export default function LocationButton(props: { currentLocation: CurrentLocationStoreState }) {
return (
{
- setLocationSearch('search')
- onCurrentLocationButtonClicked(coordinate => {
- if (coordinate) {
- if (props.queryPoints[0] && !props.queryPoints[0].isInitialized)
- Dispatcher.dispatch(
- new SetPoint(
- {
- ...props.queryPoints[0],
- coordinate,
- queryText: tr('current_location'),
- isInitialized: true,
- type: QueryPointType.From,
- },
- false
- )
- )
- Dispatcher.dispatch(new ZoomMapToPoint(coordinate))
- // We do not reset state of this button when map is moved, so we do not know if
- // the map is currently showing the location.
- setLocationSearch('synched_map_or_initial')
- } else setLocationSearch('error')
- })
+ if (props.currentLocation.enabled && !props.currentLocation.syncView && !props.currentLocation.error) {
+ Dispatcher.dispatch(new StartSyncCurrentLocation())
+ } else {
+ if (props.currentLocation.enabled) {
+ Dispatcher.dispatch(new StopWatchCurrentLocation())
+ } else {
+ Dispatcher.dispatch(new StartWatchCurrentLocation())
+ }
+ }
}}
>
- {locationSearch == 'error' && }
- {locationSearch == 'search' && }
- {locationSearch == 'synched_map_or_initial' && }
+ {(() => {
+ if (props.currentLocation.error) return
+ if (!props.currentLocation.enabled) return
+ if (!props.currentLocation.syncView) return
+ if (props.currentLocation.coordinate != null) return
+ return
+ })()}
)
}
diff --git a/src/map/MapComponent.tsx b/src/map/MapComponent.tsx
index ac2d39b3..18f5dab7 100644
--- a/src/map/MapComponent.tsx
+++ b/src/map/MapComponent.tsx
@@ -43,23 +43,3 @@ export function onCurrentLocationSelected(
{ timeout: 300_000, enableHighAccuracy: true }
)
}
-
-export function onCurrentLocationButtonClicked(onSelect: (coordinate: Coordinate | undefined) => void) {
- if (!navigator.geolocation) {
- Dispatcher.dispatch(new ErrorAction('Geolocation is not supported in this browser'))
- onSelect(undefined)
- return
- }
-
- navigator.geolocation.getCurrentPosition(
- position => {
- onSelect({ lat: position.coords.latitude, lng: position.coords.longitude })
- },
- error => {
- Dispatcher.dispatch(new ErrorAction(tr('searching_location_failed') + ': ' + error.message))
- onSelect(undefined)
- },
- // DO NOT use e.g. maximumAge: 5_000 -> getCurrentPosition will then never return on mobile firefox!?
- { timeout: 300_000, enableHighAccuracy: true }
- )
-}
diff --git a/src/map/location.svg b/src/map/location.svg
new file mode 100644
index 00000000..0a1871ad
--- /dev/null
+++ b/src/map/location.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/src/map/location_not_in_sync.svg b/src/map/location_not_in_sync.svg
new file mode 100644
index 00000000..6534e601
--- /dev/null
+++ b/src/map/location_not_in_sync.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/map/location_on.svg b/src/map/location_on.svg
index 0a1871ad..643c3be1 100644
--- a/src/map/location_on.svg
+++ b/src/map/location_on.svg
@@ -1,6 +1,6 @@
\ No newline at end of file
diff --git a/src/map/map.ts b/src/map/map.ts
index b0aefc25..8cc35182 100644
--- a/src/map/map.ts
+++ b/src/map/map.ts
@@ -1,7 +1,7 @@
import Dispatcher from '@/stores/Dispatcher'
import { Map, View } from 'ol'
import { fromLonLat } from 'ol/proj'
-import { MapIsLoaded } from '@/actions/Actions'
+import { MapIsLoaded, StopSyncCurrentLocation } from '@/actions/Actions'
import { defaults as defaultControls } from 'ol/control'
import styles from '@/map/Map.module.css'
@@ -31,12 +31,18 @@ export function createMap(): Map {
map.once('postrender', () => {
Dispatcher.dispatch(new MapIsLoaded())
})
+
+ map.on('pointerdrag', () => {
+ if (!getMap().getView().getAnimating()) Dispatcher.dispatch(new StopSyncCurrentLocation())
+ })
+
return map
}
export function setMap(m: Map) {
map = m
}
+
export function getMap(): Map {
if (!map) throw Error('Map must be initialized before it can be used. Use "createMap" when starting the app')
return map
diff --git a/src/sidebar/search/Search.tsx b/src/sidebar/search/Search.tsx
index fd4d8649..17faeb8f 100644
--- a/src/sidebar/search/Search.tsx
+++ b/src/sidebar/search/Search.tsx
@@ -2,7 +2,16 @@ import { useState } from 'react'
import Dispatcher from '@/stores/Dispatcher'
import styles from '@/sidebar/search/Search.module.css'
import { QueryPoint } from '@/stores/QueryStore'
-import { AddPoint, ClearRoute, InvalidatePoint, MovePoint, RemovePoint, SetBBox, SetPoint } from '@/actions/Actions'
+import {
+ AddPoint,
+ ClearRoute,
+ InvalidatePoint,
+ MovePoint,
+ RemovePoint,
+ SetBBox,
+ SetPoint,
+ StopSyncCurrentLocation,
+} from '@/actions/Actions'
import RemoveIcon from './minus-circle-solid.svg'
import AddIcon from './plus-circle-solid.svg'
import TargetIcon from './send.svg'
@@ -34,6 +43,7 @@ export default function Search({ points, profile, map }: { points: QueryPoint[];
onChange={() => {
Dispatcher.dispatch(new ClearRoute())
Dispatcher.dispatch(new InvalidatePoint(point))
+ Dispatcher.dispatch(new StopSyncCurrentLocation())
}}
showTargetIcons={showTargetIcons}
moveStartIndex={moveStartIndex}
diff --git a/src/stores/CurrentLocationStore.ts b/src/stores/CurrentLocationStore.ts
new file mode 100644
index 00000000..a95b7639
--- /dev/null
+++ b/src/stores/CurrentLocationStore.ts
@@ -0,0 +1,134 @@
+import Store from '@/stores/Store'
+import Dispatcher, { Action } from '@/stores/Dispatcher'
+import {
+ CurrentLocation,
+ CurrentLocationError,
+ StartSyncCurrentLocation,
+ StartWatchCurrentLocation,
+ StopSyncCurrentLocation,
+ StopWatchCurrentLocation,
+} from '@/actions/Actions'
+import { tr } from '@/translation/Translation'
+import { Coordinate } from '@/utils'
+
+export interface CurrentLocationStoreState {
+ error: string | null
+ enabled: boolean
+ syncView: boolean
+ accuracy: number // meters
+ heading: number | null
+ coordinate: Coordinate | null
+}
+
+export default class CurrentLocationStore extends Store {
+ private watchId: number | null = null
+
+ constructor() {
+ super({
+ error: null,
+ enabled: false,
+ syncView: false,
+ accuracy: 0,
+ heading: null,
+ coordinate: null,
+ })
+ }
+
+ reduce(state: CurrentLocationStoreState, action: Action): CurrentLocationStoreState {
+ // console.log('NOW ', action.constructor.name, action)
+ // console.log('NOW state ', state)
+
+ if (action instanceof StartWatchCurrentLocation) {
+ if (state.enabled) {
+ console.log('NOW cannot start as already started. ID = ' + this.watchId)
+ return state
+ }
+
+ this.start()
+ return {
+ ...state,
+ error: null,
+ enabled: true,
+ syncView: true,
+ heading: null,
+ coordinate: null,
+ }
+ } else if (action instanceof StopWatchCurrentLocation) {
+ this.stop()
+ return {
+ ...state,
+ error: null,
+ enabled: false,
+ heading: null,
+ syncView: false,
+ }
+ } else if (action instanceof CurrentLocationError) {
+ return {
+ ...state,
+ enabled: false,
+ syncView: false,
+ error: action.error,
+ heading: null,
+ coordinate: null,
+ }
+ } else if (action instanceof CurrentLocation) {
+ return {
+ ...state,
+ heading: action.heading,
+ accuracy: action.accuracy,
+ coordinate: action.coordinate,
+ }
+ } else if (action instanceof StartSyncCurrentLocation) {
+ if (!state.enabled) {
+ console.warn('cannot start synchronizing view as current location not enabled')
+ return state
+ }
+
+ return {
+ ...state,
+ error: null,
+ enabled: true,
+ syncView: true,
+ }
+ } else if (action instanceof StopSyncCurrentLocation) {
+ if (!state.enabled) return state
+
+ return {
+ ...state,
+ error: null,
+ syncView: false,
+ }
+ }
+ return state
+ }
+
+ start() {
+ if (!navigator.geolocation) {
+ Dispatcher.dispatch(new CurrentLocationError('Geolocation is not supported in this browser'))
+ this.watchId = null
+ return
+ }
+
+ this.watchId = navigator.geolocation.watchPosition(
+ position => {
+ Dispatcher.dispatch(
+ new CurrentLocation(
+ { lng: position.coords.longitude, lat: position.coords.latitude },
+ position.coords.accuracy,
+ // heading is in degrees from north, clockwise
+ position.coords.heading
+ )
+ )
+ },
+ error => {
+ Dispatcher.dispatch(new CurrentLocationError(tr('searching_location_failed') + ': ' + error.message))
+ },
+ // DO NOT use e.g. maximumAge: 5_000 -> getCurrentPosition will then never return on mobile firefox!?
+ { timeout: 300_000, enableHighAccuracy: true }
+ )
+ }
+
+ stop() {
+ if (this.watchId) navigator.geolocation.clearWatch(this.watchId)
+ }
+}
diff --git a/src/stores/MapActionReceiver.ts b/src/stores/MapActionReceiver.ts
index a2191be7..6361e3c9 100644
--- a/src/stores/MapActionReceiver.ts
+++ b/src/stores/MapActionReceiver.ts
@@ -7,7 +7,7 @@ import {
RouteRequestSuccess,
SetBBox,
SetSelectedPath,
- ZoomMapToPoint,
+ MoveMapToPoint,
} from '@/actions/Actions'
import RouteStore from '@/stores/RouteStore'
import { Bbox } from '@/api/graphhopper'
@@ -30,7 +30,7 @@ export default class MapActionReceiver implements ActionReceiver {
// we estimate the map size to be equal to the window size. we don't know better at this point, because
// the map has not been rendered for the first time yet
fitBounds(this.map, action.bbox, isSmallScreen, [window.innerWidth, window.innerHeight])
- } else if (action instanceof ZoomMapToPoint) {
+ } else if (action instanceof MoveMapToPoint) {
let zoom = this.map.getView().getZoom()
if (zoom == undefined || zoom < 8) zoom = 8
this.map.getView().animate({
diff --git a/src/stores/Stores.ts b/src/stores/Stores.ts
index b7ba16d3..2660b7ab 100644
--- a/src/stores/Stores.ts
+++ b/src/stores/Stores.ts
@@ -7,6 +7,7 @@ import PathDetailsStore from '@/stores/PathDetailsStore'
import MapFeatureStore from '@/stores/MapFeatureStore'
import SettingsStore from '@/stores/SettingsStore'
import POIsStore from '@/stores/POIsStore'
+import CurrentLocationStore from '@/stores/CurrentLocationStore'
let settingsStore: SettingsStore
let queryStore: QueryStore
@@ -17,6 +18,7 @@ let mapOptionsStore: MapOptionsStore
let pathDetailsStore: PathDetailsStore
let mapFeatureStore: MapFeatureStore
let poisStore: POIsStore
+let currentLocationStore: CurrentLocationStore
interface StoresInput {
settingsStore: SettingsStore
@@ -28,6 +30,7 @@ interface StoresInput {
pathDetailsStore: PathDetailsStore
mapFeatureStore: MapFeatureStore
poisStore: POIsStore
+ currentLocationStore: CurrentLocationStore
}
export const setStores = function (stores: StoresInput) {
@@ -40,6 +43,7 @@ export const setStores = function (stores: StoresInput) {
pathDetailsStore = stores.pathDetailsStore
mapFeatureStore = stores.mapFeatureStore
poisStore = stores.poisStore
+ currentLocationStore = stores.currentLocationStore
}
export const getSettingsStore = () => settingsStore
@@ -51,3 +55,4 @@ export const getMapOptionsStore = () => mapOptionsStore
export const getPathDetailsStore = () => pathDetailsStore
export const getMapFeatureStore = () => mapFeatureStore
export const getPOIsStore = () => poisStore
+export const getCurrentLocationStore = () => currentLocationStore