diff --git a/src/App.jsx b/src/App.jsx
index 0d02c8f..3bdea6e 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,8 +1,7 @@
import { useEffect, useRef, useCallback } from 'react'
import { useStore } from './store'
import { useTheme } from './hooks/useTheme'
-import { requestRoute, fetchAuthState } from './api'
-import { decodePolyline } from './utils/decode'
+import { fetchAuthState } from './api'
import MapView from './components/MapView'
import Panel from './components/Panel'
@@ -12,20 +11,10 @@ import LocateButton from './components/LocateButton'
export default function App() {
const mapViewRef = useRef(null)
- const routeDebounceRef = useRef(null)
// Initialize theme system
useTheme()
- const stops = useStore((s) => s.stops)
- const mode = useStore((s) => s.mode)
- const route = useStore((s) => s.route)
- const gpsOrigin = useStore((s) => s.gpsOrigin)
- const geoPermission = useStore((s) => s.geoPermission)
- const setRoute = useStore((s) => s.setRoute)
- const setRouteLoading = useStore((s) => s.setRouteLoading)
- const setRouteError = useStore((s) => s.setRouteError)
- const clearRoute = useStore((s) => s.clearRoute)
const setAuth = useStore((s) => s.setAuth)
// Initialize auth state on app load (single fetch, no polling)
@@ -33,70 +22,18 @@ export default function App() {
fetchAuthState().then(setAuth)
}, [setAuth])
- // Fetch route when stops, mode, gpsOrigin, or geoPermission change (debounced 500ms)
- useEffect(() => {
- if (routeDebounceRef.current) clearTimeout(routeDebounceRef.current)
-
- routeDebounceRef.current = setTimeout(async () => {
- const { userLocation } = useStore.getState()
-
- let effective = stops.map((s) => ({ lat: s.lat, lon: s.lon }))
- if (gpsOrigin && geoPermission === 'granted' && userLocation) {
- effective = [{ lat: userLocation.lat, lon: userLocation.lon }, ...effective]
- }
-
- if (effective.length < 2) {
- clearRoute()
- return
- }
-
- setRouteLoading(true)
-
- try {
- const data = await requestRoute(effective, mode)
- if (data.trip) {
- setRoute(data.trip)
- } else {
- setRouteError('No route returned')
- }
- } catch (e) {
- setRouteError(e.message || 'Route request failed')
- } finally {
- setRouteLoading(false)
- }
- }, 500)
-
- return () => {
- if (routeDebounceRef.current) clearTimeout(routeDebounceRef.current)
- }
- }, [stops, mode, gpsOrigin, geoPermission, clearRoute, setRoute, setRouteLoading, setRouteError])
-
- // Handle maneuver click
- const handleManeuverClick = useCallback(
- (maneuver) => {
- if (!route || !route.legs) return
-
- const legIdx = maneuver._legIndex || 0
- const leg = route.legs[legIdx]
- if (!leg || !leg.shape) return
-
- const coords = decodePolyline(leg.shape, 6)
- const idx = maneuver.begin_shape_index
- if (idx >= 0 && idx < coords.length) {
- const [lng, lat] = coords[idx]
- mapViewRef.current?.flyTo(lat, lng, 15)
- }
- },
- [route]
- )
+ // Handle clear route from panel
+ const handleClearRoute = useCallback(() => {
+ mapViewRef.current?.clearRoute?.()
+ }, [])
return (
-
-
+
+
-
+
{/* Bottom-right map controls */}
diff --git a/src/api.js b/src/api.js
index fe8fd02..47d5861 100644
--- a/src/api.js
+++ b/src/api.js
@@ -321,3 +321,69 @@ export async function fetchAuthState() {
return { authenticated: false, username: null }
}
}
+
+// ── Offroute API ──
+
+const OFFROUTE_URL = "/api/offroute"
+const MVUM_URL = "/api/mvum"
+
+/**
+ * Request an offroute route from the pathfinder API.
+ * @param {object} start - { lat, lon }
+ * @param {object} end - { lat, lon }
+ * @param {string} mode - foot | mtb | atv | vehicle
+ * @param {string} boundaryMode - strict | pragmatic | emergency
+ * @returns {Promise