11import { useEffect , useState } from 'react'
22import L from 'leaflet'
33import 'leaflet/dist/leaflet.css'
4- import { List , Loader2 } from 'lucide-react'
4+ import { Loader2 } from 'lucide-react'
55
66
77// Fix for default markers not showing in bundled environments
@@ -32,6 +32,21 @@ const createCustomIcon = (color: string) => {
3232 } )
3333}
3434
35+ // Create a small white dot icon for turn-by-turn markers
36+ const createTurnIcon = ( ) => {
37+ const markerHtml = `
38+ <svg viewBox="0 0 12 12" width="12" height="12" style="filter: drop-shadow(0 1px 2px rgba(0,0,0,0.4));">
39+ <circle cx="6" cy="6" r="4" fill="#FFFFFF" stroke="#333333" stroke-width="1.5" />
40+ </svg>`
41+
42+ return L . divIcon ( {
43+ className : 'leaflet-turn-icon' ,
44+ html : markerHtml ,
45+ iconSize : [ 12 , 12 ] ,
46+ iconAnchor : [ 6 , 6 ] ,
47+ } )
48+ }
49+
3550export type GeoPoint = {
3651 name ?: string
3752 lat : number
@@ -42,6 +57,7 @@ interface LeafletMapProps {
4257 from ?: GeoPoint | null
4358 to ?: GeoPoint | null
4459 animateKey ?: string | number
60+ isPredicting ?: boolean
4561}
4662
4763interface RouteStep {
@@ -50,7 +66,7 @@ interface RouteStep {
5066 }
5167}
5268
53- export default function LeafletMap ( { from, to, animateKey } : LeafletMapProps ) {
69+ export default function LeafletMap ( { from, to, animateKey, isPredicting } : LeafletMapProps ) {
5470 const [ isRouteLoading , setIsRouteLoading ] = useState ( false )
5571 const [ routeSteps , setRouteSteps ] = useState < RouteStep [ ] > ( [ ] )
5672
@@ -65,7 +81,7 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) {
6581 : 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
6682
6783
68-
84+
6985 const tiles = L . tileLayer ( tileUrl , {
7086 maxZoom : 19 ,
7187 attribution :
@@ -77,6 +93,7 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) {
7793
7894 let markers : L . Marker [ ] = [ ]
7995 let routeLayer : L . Polyline | L . GeoJSON | null = null
96+ let turnMarkers : L . Marker [ ] = [ ]
8097
8198 const fitBoundsIfNeeded = ( ) => {
8299 const points : L . LatLngExpression [ ] = [ ]
@@ -109,6 +126,11 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) {
109126 }
110127 }
111128
129+ const clearTurnMarkers = ( ) => {
130+ turnMarkers . forEach ( m => m . remove ( ) )
131+ turnMarkers = [ ]
132+ }
133+
112134 const drawStraight = ( ) => {
113135 if ( ! from || ! to ) return
114136 if ( routeLayer ) routeLayer . remove ( )
@@ -123,30 +145,54 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) {
123145
124146 const animateRoute = ( geoJsonData : any ) => {
125147 if ( routeLayer ) routeLayer . remove ( ) ;
148+ clearTurnMarkers ( ) ;
126149
127150 const allCoords = geoJsonData . geometry . coordinates . flat ( 1 ) . map ( ( c : number [ ] ) => L . latLng ( c [ 1 ] , c [ 0 ] ) ) ;
128151 const animatedPolyline = L . polyline ( [ ] , { color : '#2563eb' , weight : 4 , opacity : 0.95 } ) . addTo ( map ) ;
129152 routeLayer = animatedPolyline ;
130153
131- let i = 0 ;
132- const step = ( ) => {
133- if ( i < allCoords . length ) {
134- animatedPolyline . addLatLng ( allCoords [ i ] ) ;
135- i ++ ;
154+ const animationDuration = 750 ; // Animate over 750ms
155+ let startTime : number | null = null ;
156+
157+ const step = ( timestamp : number ) => {
158+ if ( ! startTime ) {
159+ startTime = timestamp ;
160+ }
161+
162+ const progress = Math . min ( ( timestamp - startTime ) / animationDuration , 1 ) ;
163+ const pointsToShow = Math . floor ( progress * allCoords . length ) ;
164+
165+ // Only update if there are new points to show to avoid unnecessary re-renders
166+ if ( pointsToShow > animatedPolyline . getLatLngs ( ) . length ) {
167+ animatedPolyline . setLatLngs ( allCoords . slice ( 0 , pointsToShow ) ) ;
168+ }
169+
170+ if ( progress < 1 ) {
136171 requestAnimationFrame ( step ) ;
137172 } else {
138- // Animation finished, bind the popup
173+ animatedPolyline . setLatLngs ( allCoords ) ; // Ensure the full route is drawn
139174 const properties = geoJsonData . properties ;
140175 if ( properties ) {
141176 const distanceKm = ( properties . distance / 1000 ) . toFixed ( 1 ) ;
142177 const timeMinutes = Math . round ( properties . time / 60 ) ;
143- animatedPolyline . bindPopup (
144- `<b>Route Details</b><br>Distance: ${ distanceKm } km<br>Est. Time: ${ timeMinutes } minutes`
145- ) ;
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+ }
146193 }
147194 }
148195 } ;
149-
150196 requestAnimationFrame ( step ) ;
151197 } ;
152198
@@ -158,17 +204,18 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) {
158204 return
159205 }
160206 setIsRouteLoading ( true )
207+ clearTurnMarkers ( )
161208 setRouteSteps ( [ ] ) // Clear previous steps
162209 try {
163- //EXAMPLE:https://api.geoapify.com/v1/routing?waypoints=40.7757145,-73.87336398511545|40.6604335,-73.8302749&mode=drive&apiKey=YOUR_API_KEY
210+ //EXAMPLE:https://api.geoapify.com/v1/routing?waypoints=40.7757145,-73.87336398511545|40.6604335,-73.8302749&mode=drive&apiKey=YOUR_API_KEY
164211 const url = `https://api.geoapify.com/v1/routing?waypoints=${ from . lat } ,${ from . lon } |${ to . lat } ,${ to . lon } &mode=drive&format=geojson&apiKey=${ apiKey } `
165212 console . log ( url ) ;
166213 const res = await fetch ( url )
167214 if ( ! res . ok ) throw new Error ( `HTTP ${ res . status } ` )
168215 const data = await res . json ( )
169216 console . log ( 'Geoapify route data:' , data ) ;
170217 if ( ! data ?. features ?. [ 0 ] ) throw new Error ( 'No route' )
171-
218+
172219 animateRoute ( data . features [ 0 ] ) ;
173220
174221 // Extract and set turn-by-turn instructions
@@ -186,11 +233,11 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) {
186233 fitBoundsIfNeeded ( )
187234 // Always draw something quickly, then try to replace with routed geometry
188235 if ( from && to ) {
189- drawStraight ( )
190236 fetchRoute ( )
191237 }
192238
193239 return ( ) => {
240+ clearTurnMarkers ( )
194241 map . remove ( )
195242 }
196243 // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -201,7 +248,11 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) {
201248 < div className = "flex items-center justify-between border-b border-border px-4 py-3 text-sm text-foreground/70" >
202249 < div className = "flex items-center gap-2" >
203250 < span className = "inline-flex h-2 w-2 rounded-full bg-primary" />
204- < span > Route Preview</ span >
251+ { isPredicting ? (
252+ < span > Calculating route...</ span >
253+ ) : (
254+ < span > Route Preview</ span >
255+ ) }
205256 </ div >
206257 { from && to ? (
207258 < span className = "truncate" > { from . name ?? 'Start' } → { to . name ?? 'End' } </ span >
@@ -217,21 +268,7 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) {
217268 </ div >
218269 ) }
219270 </ div >
220- { routeSteps . length > 0 && (
221- < div className = "border-t border-border" >
222- < div className = "flex items-center gap-2 px-4 py-2 text-sm font-medium" >
223- < List className = "h-4 w-4" />
224- < span > Turn-by-Turn Directions</ span >
225- </ div >
226- < ol className = "max-h-48 overflow-y-auto list-decimal list-inside bg-background/50 px-4 pb-3 text-sm" >
227- { routeSteps . map ( ( step , index ) => (
228- < li key = { index } className = "py-1.5 border-b border-border/50 last:border-b-0" >
229- { step . instruction . text }
230- </ li >
231- ) ) }
232- </ ol >
233- </ div >
234- ) }
271+
235272 </ div >
236273 )
237274}
0 commit comments