fix: OSRM sparse data handling, frontend type mismatch, map jumping

- Rewrite OsrmService with smart dense/sparse segmentation:
  dense GPS traces use Match API, sparse gaps use Route API
  (turn-by-turn directions between waypoints)
- Filter stationary points before OSRM processing
- Fix critical frontend bug: LocationHistoryResponse type didn't
  match backend response shape (matchedRoute vs matched), so OSRM
  routes were never actually displaying
- Fix double distance conversion (backend sends miles, frontend
  was dividing by 1609.34 again)
- Fix map jumping: disable popup autoPan on marker data refresh
- Extend default history window from 4h to 12h

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 17:39:00 +01:00
parent 12b9361ae0
commit cb4a070ad9
4 changed files with 385 additions and 116 deletions

View File

@@ -157,21 +157,21 @@ export function GpsTracking() {
const history = locationHistory as LocationHistoryResponse;
// If we have matched coordinates, use those
if (history.matched && history.coordinates && history.coordinates.length > 1) {
// If we have OSRM matched route, use road-snapped coordinates
if (history.matchedRoute && history.matchedRoute.coordinates && history.matchedRoute.coordinates.length > 1) {
return {
positions: history.coordinates,
positions: history.matchedRoute.coordinates,
isMatched: true,
distance: history.distance,
duration: history.duration,
confidence: history.confidence,
distance: history.matchedRoute.distance, // already in miles from backend
duration: history.matchedRoute.duration,
confidence: history.matchedRoute.confidence,
};
}
// Fall back to raw positions
if (history.rawPositions && history.rawPositions.length > 1) {
// Fall back to raw GPS points
if (history.rawPoints && history.rawPoints.length > 1) {
return {
positions: history.rawPositions.map(loc => [loc.latitude, loc.longitude] as [number, number]),
positions: history.rawPoints.map(loc => [loc.latitude, loc.longitude] as [number, number]),
isMatched: false,
distance: undefined,
duration: undefined,
@@ -433,7 +433,7 @@ export function GpsTracking() {
position={[driver.location!.latitude, driver.location!.longitude]}
icon={createDriverIcon(true)}
>
<Popup>
<Popup autoPan={false}>
<div className="min-w-[180px]">
<h3 className="font-semibold text-base">{driver.driverName}</h3>
{driver.driverPhone && (
@@ -489,10 +489,10 @@ export function GpsTracking() {
<div className="w-12 h-0.5 bg-gray-400 opacity-40" style={{ borderTop: '2px dashed #94a3b8' }}></div>
<span className="text-xs text-muted-foreground">GPS Trail</span>
</div>
{routePolyline && routePolyline.distance && (
{routePolyline && routePolyline.distance != null && (
<div className="pt-1 border-t border-border">
<div className="text-xs text-muted-foreground">
Distance: <span className="font-medium text-foreground">{(routePolyline.distance / 1609.34).toFixed(1)} mi</span>
Distance: <span className="font-medium text-foreground">{routePolyline.distance.toFixed(1)} mi</span>
</div>
{routePolyline.confidence !== undefined && (
<div className="text-xs text-muted-foreground">

View File

@@ -108,10 +108,20 @@ export interface MyGpsStatus {
}
export interface LocationHistoryResponse {
matched: boolean;
coordinates?: [number, number][]; // road-snapped [lat, lng] pairs
distance?: number; // road distance in meters
duration?: number; // duration in seconds
confidence?: number; // 0-1 confidence score
rawPositions?: Array<{ latitude: number; longitude: number; speed: number; timestamp: Date }>;
rawPoints: Array<{
latitude: number;
longitude: number;
altitude?: number | null;
speed?: number | null;
course?: number | null;
accuracy?: number | null;
battery?: number | null;
timestamp: string;
}>;
matchedRoute: {
coordinates: [number, number][]; // road-snapped [lat, lng] pairs
distance: number; // road distance in miles
duration: number; // duration in seconds
confidence: number; // 0-1 confidence score
} | null;
}