From ece17875d4e454423f55f0623a456c0433ecd502 Mon Sep 17 00:00:00 2001
From: Ariel Costas Guerrero <ariel@costas.dev>
Date: Fri, 13 Mar 2026 17:12:12 +0100
Subject: 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.
---
 src/frontend/app/contexts/MapContext.tsx | 159 ++++++++++++++++++++++---------
 1 file changed, 116 insertions(+), 43 deletions(-)

(limited to 'src/frontend/app/contexts/MapContext.tsx')

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}
-- 
cgit v1.3

