feat: add OSRM road-snapping for GPS routes and mileage (#21)

Routes now follow actual roads instead of cutting through buildings:
- New OsrmService calls free OSRM Match API to snap GPS points to roads
- Position history endpoint accepts ?matched=true for road-snapped geometry
- Stats use OSRM road distance instead of Haversine crow-flies distance
- Frontend shows solid blue polylines for matched routes, dashed for raw
- Handles chunking (100 coord limit), rate limiting, graceful fallback
- Distance badge shows accurate road miles on route trails

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 17:03:47 +01:00
parent d93919910b
commit 33fda57cc6
6 changed files with 351 additions and 16 deletions

View File

@@ -9,6 +9,7 @@ import type {
EnrollmentResponse,
MyGpsStatus,
DeviceQrInfo,
LocationHistoryResponse,
} from '@/types/gps';
import toast from 'react-hot-toast';
import { queryKeys } from '@/lib/query-keys';
@@ -125,19 +126,22 @@ export function useDriverLocation(driverId: string) {
/**
* Get driver location history (for route trail display)
* By default, requests road-snapped routes from OSRM map matching
*/
export function useDriverLocationHistory(driverId: string | null, from?: string, to?: string) {
return useQuery<Array<{ latitude: number; longitude: number; speed: number; timestamp: Date }>>({
return useQuery({
queryKey: ['gps', 'locations', driverId, 'history', from, to],
queryFn: async () => {
const params = new URLSearchParams();
if (from) params.append('from', from);
if (to) params.append('to', to);
params.append('matched', 'true'); // Request road-snapped route
const { data } = await api.get(`/gps/locations/${driverId}/history?${params}`);
return data;
},
enabled: !!driverId,
refetchInterval: 30000, // Refresh every 30 seconds
refetchInterval: 60000, // Match routes less frequently (60s) since OSRM has rate limits
staleTime: 30000, // Consider data fresh for 30s
});
}