From 21d8c599d3f92d83e0bc79e0533ed4db59be11cb Mon Sep 17 00:00:00 2001 From: Richard Sun Date: Sat, 13 Dec 2025 18:19:42 -0500 Subject: [PATCH 1/4] Chore: Fix indentation of location polling effect --- client/src/components/MapKitMap.tsx | 50 ++++++++++++++--------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/client/src/components/MapKitMap.tsx b/client/src/components/MapKitMap.tsx index a2bdfef4..b516b13b 100644 --- a/client/src/components/MapKitMap.tsx +++ b/client/src/components/MapKitMap.tsx @@ -157,33 +157,33 @@ export default function MapKitMap({ routeData, displayVehicles = true, generateR mapkitScript(); }, []); - // Fetch location data on component mount and set up polling - useEffect(() => { - if (!displayVehicles) return; - - const pollLocation = async () => { - try { - const response = await fetch('/api/locations'); - if (!response.ok) { - throw new Error('Network response was not ok'); - } - const data = await response.json(); - setVehicles(data); - } catch (error) { - console.error('Error fetching location:', error); + // Fetch location data on component mount and set up polling + useEffect(() => { + if (!displayVehicles) return; + + const pollLocation = async () => { + try { + const response = await fetch('/api/locations'); + if (!response.ok) { + throw new Error('Network response was not ok'); } + const data = await response.json(); + setVehicles(data); + } catch (error) { + console.error('Error fetching location:', error); } - - pollLocation(); - - // refresh location every 5 seconds - const refreshLocation = setInterval(pollLocation, 5000); - - return () => { - clearInterval(refreshLocation); - } - - }, []); + } + + pollLocation(); + + // refresh location every 5 seconds + const refreshLocation = setInterval(pollLocation, 5000); + + return () => { + clearInterval(refreshLocation); + } + + }, []); // create the map useEffect(() => { From 1bcddbd3c4523bc48fe2273eafcd49cabf664a6d Mon Sep 17 00:00:00 2001 From: Richard Sun Date: Sat, 13 Dec 2025 18:19:42 -0500 Subject: [PATCH 2/4] Fix: Ensure animation loop re-renders on route changes --- client/src/components/MapKitMap.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/MapKitMap.tsx b/client/src/components/MapKitMap.tsx index b516b13b..1d8ec887 100644 --- a/client/src/components/MapKitMap.tsx +++ b/client/src/components/MapKitMap.tsx @@ -721,7 +721,7 @@ export default function MapKitMap({ routeData, displayVehicles = true, generateR return () => { if (animationFrameId.current) cancelAnimationFrame(animationFrameId.current); }; - }, [vehicles]); // Restart loop if vehicles change? Not strictly necessary if refs are used, but ensures we have latest `vehicles` closure if needed. Actually with refs we don't need to dependency on vehicles often if we read from ref, but here we read `vehicles` prop. + }, [vehicles, flattenedRoutes]); // Include flattenedRoutes to ensure animation uses current route data From b5ba3ec12d108cfed56a97782d31b50b44c93312 Mon Sep 17 00:00:00 2001 From: Richard Sun Date: Sat, 13 Dec 2025 18:19:42 -0500 Subject: [PATCH 3/4] Feat: Implement off-route detection and handling for vehicles Fixes #266 --- client/src/components/MapKitMap.tsx | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/client/src/components/MapKitMap.tsx b/client/src/components/MapKitMap.tsx index 1d8ec887..b35a43be 100644 --- a/client/src/components/MapKitMap.tsx +++ b/client/src/components/MapKitMap.tsx @@ -531,12 +531,29 @@ export default function MapKitMap({ routeData, displayVehicles = true, generateR return; } + // Check if shuttle is off-route by finding distance to nearest polyline point + const nearestResult = findNearestPointOnPolyline(vehicleCoord, routePolyline); + const OFF_ROUTE_THRESHOLD_METERS = 100; // If >100m from polyline, consider off-route + + if (nearestResult.distance > OFF_ROUTE_THRESHOLD_METERS) { + // Shuttle is off-route - clear animation state so it shows at actual GPS position + // Delete the animation state so the annotation uses the direct coordinate update (line 494) + delete vehicleAnimationStates.current[key]; + + // Update annotation directly to GPS position + const annotation = vehicleOverlays.current[key]; + if (annotation) { + annotation.coordinate = new mapkit.Coordinate(vehicle.latitude, vehicle.longitude); + } + return; + } + const snapToPolyline = () => { - const { index, point } = findNearestPointOnPolyline(vehicleCoord, routePolyline); + // Reuse nearestResult from off-route check to avoid duplicate calculation vehicleAnimationStates.current[key] = { lastUpdateTime: now, - polylineIndex: index, - currentPoint: point, + polylineIndex: nearestResult.index, + currentPoint: nearestResult.point, targetDistance: 0, distanceTraveled: 0, lastServerTime: serverTime From 4bdaf7067ee2be221a2e6574639e1d585116a6eb Mon Sep 17 00:00:00 2001 From: Richard Sun Date: Sat, 13 Dec 2025 18:19:43 -0500 Subject: [PATCH 4/4] Refactor: Adjust animation thresholds and clean up state on vehicle removal --- client/src/components/MapKitMap.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/components/MapKitMap.tsx b/client/src/components/MapKitMap.tsx index b35a43be..1ea43005 100644 --- a/client/src/components/MapKitMap.tsx +++ b/client/src/components/MapKitMap.tsx @@ -630,9 +630,9 @@ export default function MapKitMap({ routeData, displayVehicles = true, generateR } // Step 6: Update animation state. - // If the gap is extremely large (>250m in either direction), snap to server position. + // If the gap is extremely large (>350m in either direction), snap to server position. // For smaller backward gaps, animate smoothly backward to correct the overprediction. - const MAX_REASONABLE_GAP_METERS = 250; + const MAX_REASONABLE_GAP_METERS = 350; if (Math.abs(distanceToTarget) > MAX_REASONABLE_GAP_METERS) { snapToPolyline(); } else { @@ -656,6 +656,8 @@ export default function MapKitMap({ routeData, displayVehicles = true, generateR if (!currentVehicleKeys.has(key)) { map.removeAnnotation(vehicleOverlays.current[key]); delete vehicleOverlays.current[key]; + // Also clean up animation state to prevent memory leaks + delete vehicleAnimationStates.current[key]; } }); }, [map, vehicles, routeData]);