Skip to content

Commit 910a37f

Browse files
enhancement(map): synchronize turn marker animation with route drawing
Creates a more dynamic and intuitive visual experience for the user. - The `animateRoute` function now tracks the animation progress and adds turn markers to the map in real-time as the corresponding route segment is displayed.
1 parent 42431b9 commit 910a37f

File tree

1 file changed

+59
-31
lines changed

1 file changed

+59
-31
lines changed

frontend/src/components/LeafletMap.tsx

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ interface RouteStep {
6969
export default function LeafletMap({ from, to, animateKey, isPredicting }: LeafletMapProps) {
7070
const [isRouteLoading, setIsRouteLoading] = useState(false)
7171
const [routeSteps, setRouteSteps] = useState<RouteStep[]>([])
72+
const [routeError, setRouteError] = useState<string | null>(null);
7273

7374
useEffect(() => {
7475
const map = L.map('route-map', {
@@ -147,50 +148,73 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl
147148
if (routeLayer) routeLayer.remove();
148149
clearTurnMarkers();
149150

151+
//NOTE: the geoJson array coordinate pair [lon, lat] is converted into a Leaflet LatLng object [lat, lon].
150152
const allCoords = geoJsonData.geometry.coordinates.flat(1).map((c: number[]) => L.latLng(c[1], c[0]));
153+
154+
// Create an empty polyline (a line with multiple points). This is what we will "draw" on.
155+
// We'll add coordinates to it over time to create the animation effect.
151156
const animatedPolyline = L.polyline([], { color: '#2563eb', weight: 4, opacity: 0.95 }).addTo(map);
152-
routeLayer = animatedPolyline;
153-
154-
const animationDuration = 750; // Animate over 750ms
157+
routeLayer = animatedPolyline; // Keep a reference to it so we can remove it later.
158+
159+
// --- Prepare Turn Markers ---
160+
// The route data also includes "steps" (like "turn left," "go straight").
161+
// We extract the coordinate index for each turn.
162+
const steps = geoJsonData.properties?.legs?.[0]?.steps;
163+
const turnPoints = (steps && steps.length > 1)
164+
// We skip the first step (the start) and map over the rest.
165+
? steps.slice(1).map((step: any) => ({
166+
// `from_index` tells us which point in `allCoords` corresponds to the start of this turn.
167+
index: step.from_index,
168+
// We get the actual LatLng object for that index.
169+
latlng: allCoords[step.from_index],
170+
})).filter((turn: any) => turn.latlng) // Make sure the coordinate exists.
171+
: [];
172+
173+
// --- Animation Setup ---
174+
let nextTurnIndex = 0; // This will track which turn marker we need to draw next.
175+
const turnIcon = createTurnIcon(); // A small white dot icon for the turns.
176+
const animationDuration = 750; // We want the animation to last 750 milliseconds.
155177
let startTime: number | null = null;
156178

179+
// The `step` function is the core of our animation. It will be called on every frame.
157180
const step = (timestamp: number) => {
181+
// On the very first frame, record the start time.
158182
if (!startTime) {
159183
startTime = timestamp;
160184
}
161185

186+
// Calculate how much time has passed since the animation started.
187+
// `progress` will be a value from 0 (start) to 1 (end).
162188
const progress = Math.min((timestamp - startTime) / animationDuration, 1);
189+
190+
// Based on the progress, calculate how many points of the route line should be visible.
163191
const pointsToShow = Math.floor(progress * allCoords.length);
164192

165-
// Only update if there are new points to show to avoid unnecessary re-renders
193+
// To avoid unnecessary work, we only update the map if new points need to be drawn.
166194
if (pointsToShow > animatedPolyline.getLatLngs().length) {
195+
// Update the polyline to show the new segment of the route.
167196
animatedPolyline.setLatLngs(allCoords.slice(0, pointsToShow));
197+
198+
// --- Synchronized Turn Marker Drawing ---
199+
// This loop checks if the line has reached or passed the next turn point.
200+
while (nextTurnIndex < turnPoints.length && turnPoints[nextTurnIndex].index <= pointsToShow) {
201+
// If it has, we get the turn's data...
202+
const turn = turnPoints[nextTurnIndex];
203+
// ...add a marker to the map at that turn's location...
204+
turnMarkers.push(L.marker(turn.latlng, { icon: turnIcon }).addTo(map));
205+
// ...and move on to the next turn in our list.
206+
nextTurnIndex++;
207+
}
168208
}
169209

210+
// If the animation is not yet finished (progress < 1), we request the next frame.
211+
// This creates a smooth loop.
170212
if (progress < 1) {
171213
requestAnimationFrame(step);
172214
} else {
173-
animatedPolyline.setLatLngs(allCoords); // Ensure the full route is drawn
174-
const properties = geoJsonData.properties;
175-
if (properties) {
176-
const distanceKm = (properties.distance / 1000).toFixed(1);
177-
const timeMinutes = Math.round(properties.time / 60);
178-
animatedPolyline.bindPopup(`<b>Route Details</b><br>Distance: ${distanceKm} km<br>Est. Time: ${timeMinutes} minutes`);
179-
}
180-
181-
// Draw turn markers after animation is complete
182-
const steps = geoJsonData.properties?.legs?.[0]?.steps;
183-
if (steps && steps.length > 1) {
184-
const turnIcon = createTurnIcon();
185-
// Start from the second step to get the first turn coordinate
186-
for (let i = 1; i < steps.length; i++) {
187-
const turnIndex = steps[i].from_index;
188-
if (turnIndex < allCoords.length) {
189-
const turnMarker = L.marker(allCoords[turnIndex], { icon: turnIcon }).addTo(map);
190-
turnMarkers.push(turnMarker);
191-
}
192-
}
193-
}
215+
// --- Animation Finished ---
216+
// Once the animation is complete, ensure the entire route is drawn.
217+
animatedPolyline.setLatLngs(allCoords);
194218
}
195219
};
196220
requestAnimationFrame(step);
@@ -205,13 +229,15 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl
205229
}
206230
setIsRouteLoading(true)
207231
clearTurnMarkers()
232+
setRouteError(null); // Clear previous errors
208233
setRouteSteps([]) // Clear previous steps
234+
if (routeLayer) routeLayer.remove(); // Clear previous route before fetching
209235
try {
210-
//EXAMPLE:https://api.geoapify.com/v1/routing?waypoints=40.7757145,-73.87336398511545|40.6604335,-73.8302749&mode=drive&apiKey=YOUR_API_KEY
211-
const url = `https://api.geoapify.com/v1/routing?waypoints=${from.lat},${from.lon}|${to.lat},${to.lon}&mode=drive&format=geojson&apiKey=${apiKey}`
236+
// Use waypoints.snapped=true to find the nearest routable point for each coordinate
237+
const url = `https://api.geoapify.com/v1/routing?waypoints=${from.lat},${from.lon}|${to.lat},${to.lon}&mode=drive&format=geojson&waypoints.snapped=true&apiKey=${apiKey}`
212238
console.log(url);
213239
const res = await fetch(url)
214-
if (!res.ok) throw new Error(`HTTP ${res.status}`)
240+
if (!res.ok) throw new Error(`Could not find a routable path. (HTTP ${res.status})`)
215241
const data = await res.json()
216242
console.log('Geoapify route data:', data);
217243
if (!data?.features?.[0]) throw new Error('No route')
@@ -222,16 +248,18 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl
222248
if (data.features[0]?.properties?.legs?.[0]?.steps) {
223249
setRouteSteps(data.features[0].properties.legs[0].steps)
224250
}
225-
} catch {
226-
drawStraight()
251+
} catch (error) {
252+
if (error instanceof Error) {
253+
setRouteError(error.message);
254+
}
227255
} finally {
228256
setIsRouteLoading(false)
229257
}
230258
}
231259

232260
drawMarkers()
233261
fitBoundsIfNeeded()
234-
// Always draw something quickly, then try to replace with routed geometry
262+
235263
if (from && to) {
236264
fetchRoute()
237265
}

0 commit comments

Comments
 (0)