diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2026-03-13 17:12:12 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2026-03-13 17:12:12 +0100 |
| commit | ece17875d4e454423f55f0623a456c0433ecd502 (patch) | |
| tree | 732c0432cbf32757344c51b8c01bb18e83e9c0c0 /src/frontend/app/contexts | |
| parent | 5c670f1b4a237b7a5197dfcf94de92095da95463 (diff) | |
feat: integrate geolocation functionality and enhance map interactions
- Added useGeolocation hook to manage user location and permissions.
- Updated PlannerOverlay to utilize geolocation for setting origin.
- Enhanced NavBar with a new planner route.
- Introduced context menu for map interactions to set routes from current location.
- Improved search functionality in the map with a dedicated search bar.
- Updated localization files with new strings for routing and search features.
Diffstat (limited to 'src/frontend/app/contexts')
| -rw-r--r-- | src/frontend/app/contexts/MapContext.tsx | 159 |
1 files changed, 116 insertions, 43 deletions
diff --git a/src/frontend/app/contexts/MapContext.tsx b/src/frontend/app/contexts/MapContext.tsx index f888f34..75851f4 100644 --- a/src/frontend/app/contexts/MapContext.tsx +++ b/src/frontend/app/contexts/MapContext.tsx @@ -1,8 +1,10 @@ import { type LngLatLike } from "maplibre-gl"; import { createContext, + useCallback, useContext, useEffect, + useRef, useState, type ReactNode, } from "react"; @@ -18,6 +20,7 @@ interface MapContextProps { setUserLocation: (location: LngLatLike | null) => void; setLocationPermission: (hasPermission: boolean) => void; updateMapState: (center: LngLatLike, zoom: number, path: string) => void; + requestLocation: () => void; } const MapContext = createContext<MapContextProps | undefined>(undefined); @@ -28,9 +31,6 @@ export const MapProvider = ({ children }: { children: ReactNode }) => { if (savedMapState) { try { const parsed = JSON.parse(savedMapState); - // Validate that the saved center is valid if needed, or just trust it. - // We might want to ensure we have a fallback if the region changed while the app was closed? - // But for now, let's stick to the existing logic. return { paths: parsed.paths || {}, userLocation: parsed.userLocation || null, @@ -47,58 +47,130 @@ export const MapProvider = ({ children }: { children: ReactNode }) => { }; }); - const setUserLocation = (userLocation: LngLatLike | null) => { + const watchIdRef = useRef<number | null>(null); + + const setUserLocation = useCallback((userLocation: LngLatLike | null) => { setMapState((prev) => { const newState = { ...prev, userLocation }; localStorage.setItem("mapState", JSON.stringify(newState)); return newState; }); - }; + }, []); - const setLocationPermission = (hasLocationPermission: boolean) => { - setMapState((prev) => { - const newState = { ...prev, hasLocationPermission }; - localStorage.setItem("mapState", JSON.stringify(newState)); - return newState; - }); - }; + const setLocationPermission = useCallback( + (hasLocationPermission: boolean) => { + setMapState((prev) => { + const newState = { ...prev, hasLocationPermission }; + localStorage.setItem("mapState", JSON.stringify(newState)); + return newState; + }); + }, + [] + ); - const updateMapState = (center: LngLatLike, zoom: number, path: string) => { - setMapState((prev) => { - const newState = { - ...prev, - paths: { - ...prev.paths, - [path]: { center, zoom }, - }, - }; - localStorage.setItem("mapState", JSON.stringify(newState)); - return newState; - }); - }; + const updateMapState = useCallback( + (center: LngLatLike, zoom: number, path: string) => { + setMapState((prev) => { + const newState = { + ...prev, + paths: { + ...prev.paths, + [path]: { center, zoom }, + }, + }; + localStorage.setItem("mapState", JSON.stringify(newState)); + return newState; + }); + }, + [] + ); + + const startWatching = useCallback(() => { + if (!navigator.geolocation || watchIdRef.current !== null) return; + watchIdRef.current = navigator.geolocation.watchPosition( + (position) => { + const { latitude, longitude } = position.coords; + setUserLocation([latitude, longitude]); + setLocationPermission(true); + }, + (error) => { + if (error.code === GeolocationPositionError.PERMISSION_DENIED) { + setLocationPermission(false); + } + }, + { enableHighAccuracy: false, maximumAge: 30000, timeout: 15000 } + ); + }, [setUserLocation, setLocationPermission]); + + const requestLocation = useCallback(() => { + if (typeof window === "undefined" || !("geolocation" in navigator)) return; + navigator.geolocation.getCurrentPosition( + (pos) => { + setUserLocation([pos.coords.latitude, pos.coords.longitude]); + setLocationPermission(true); + startWatching(); + }, + () => { + setLocationPermission(false); + }, + { enableHighAccuracy: false, maximumAge: 60000, timeout: 10000 } + ); + }, [setUserLocation, setLocationPermission, startWatching]); - // Try to get user location on load if permission was granted + const hasPermissionRef = useRef(mapState.hasLocationPermission); + + // On mount: subscribe to permission changes and auto-start watching if already granted useEffect(() => { - if (mapState.hasLocationPermission && !mapState.userLocation) { - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition( - (position) => { - const { latitude, longitude } = position.coords; - setUserLocation([latitude, longitude]); - }, - (error) => { - console.error("Error getting location:", error); + if (typeof window === "undefined" || !("geolocation" in navigator)) return; + + let permissionStatus: PermissionStatus | null = null; + + const onPermChange = () => { + if (permissionStatus?.state === "granted") { + setLocationPermission(true); + startWatching(); + } else if (permissionStatus?.state === "denied") { + setLocationPermission(false); + if (watchIdRef.current !== null) { + navigator.geolocation.clearWatch(watchIdRef.current); + watchIdRef.current = null; + } + } + }; + + const init = async () => { + try { + if (navigator.permissions?.query) { + permissionStatus = await navigator.permissions.query({ + name: "geolocation", + }); + if (permissionStatus.state === "granted") { + setLocationPermission(true); + startWatching(); + } else if (permissionStatus.state === "denied") { setLocationPermission(false); - }, - { - enableHighAccuracy: true, - maximumAge: Infinity, - timeout: 10000, } - ); + permissionStatus.addEventListener("change", onPermChange); + } else if (hasPermissionRef.current) { + startWatching(); + } + } catch { + if (hasPermissionRef.current) { + startWatching(); + } } - } - }, [mapState.hasLocationPermission, mapState.userLocation]); + }; + + init(); + + return () => { + if (watchIdRef.current !== null) { + navigator.geolocation.clearWatch(watchIdRef.current); + watchIdRef.current = null; + } + permissionStatus?.removeEventListener("change", onPermChange); + }; + }, [startWatching, setLocationPermission]); return ( <MapContext.Provider @@ -107,6 +179,7 @@ export const MapProvider = ({ children }: { children: ReactNode }) => { setUserLocation, setLocationPermission, updateMapState, + requestLocation, }} > {children} |
