@@ -69,6 +69,7 @@ interface RouteStep {
6969export 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