From 0f32f7aec67cfc77aa0fdf9fc513cecfca447e50 Mon Sep 17 00:00:00 2001 From: Anurag-Bansode Date: Wed, 15 Oct 2025 22:58:10 +0530 Subject: [PATCH 01/11] feat(ui): enhance user experience with loading states and animations UI/UX improvements - **Button Loading State**: The "Predict Travel Time" button now displays a loading spinner and "Predicting..." text - **Map Loading State**: A loading overlay with a spinner is now shown on the map while the route is being fetched - **Animated Route Drawing**: The route path is now drawn with a "snaking" animation from the start to the end point, offering a more dynamic and engaging visual. - **Turn-by-Turn Directions**: After a route is calculated, a scrollable list of turn-by-turn directions is displayed below the map, providing valuable trip details. - **Custom Map Markers**: Replaced default Leaflet markers with custom, visually appealing SVG pins for start (blue) and end (red) locations. - **Default Date/Time**: The date and time input now defaults to the user's current local time, streamlining the form-filling process. --- frontend/src/components/LeafletMap.tsx | 82 +++++++++++++++++++++++--- frontend/src/pages/Home.tsx | 42 +++++++++---- 2 files changed, 107 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/LeafletMap.tsx b/frontend/src/components/LeafletMap.tsx index dc4593e..36711c9 100644 --- a/frontend/src/components/LeafletMap.tsx +++ b/frontend/src/components/LeafletMap.tsx @@ -1,6 +1,7 @@ -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import L from 'leaflet' import 'leaflet/dist/leaflet.css' +import { List, Loader2 } from 'lucide-react' // Fix for default markers not showing in bundled environments @@ -43,7 +44,16 @@ interface LeafletMapProps { animateKey?: string | number } +interface RouteStep { + instruction: { + text: string + } +} + export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { + const [isRouteLoading, setIsRouteLoading] = useState(false) + const [routeSteps, setRouteSteps] = useState([]) + useEffect(() => { const map = L.map('route-map', { zoomControl: true, @@ -111,6 +121,35 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { ).addTo(map) } + const animateRoute = (geoJsonData: any) => { + if (routeLayer) routeLayer.remove(); + + const allCoords = geoJsonData.geometry.coordinates.flat(1).map((c: number[]) => L.latLng(c[1], c[0])); + const animatedPolyline = L.polyline([], { color: '#2563eb', weight: 4, opacity: 0.95 }).addTo(map); + routeLayer = animatedPolyline; + + let i = 0; + const step = () => { + if (i < allCoords.length) { + animatedPolyline.addLatLng(allCoords[i]); + i++; + requestAnimationFrame(step); + } else { + // Animation finished, bind the popup + const properties = geoJsonData.properties; + if (properties) { + const distanceKm = (properties.distance / 1000).toFixed(1); + const timeMinutes = Math.round(properties.time / 60); + animatedPolyline.bindPopup( + `Route Details
Distance: ${distanceKm} km
Est. Time: ${timeMinutes} minutes` + ); + } + } + }; + + requestAnimationFrame(step); + }; + const fetchRoute = async () => { if (!from || !to) return const apiKey = import.meta.env.VITE_GEOAPIFY_API_KEY @@ -118,6 +157,8 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { drawStraight() return } + setIsRouteLoading(true) + setRouteSteps([]) // Clear previous steps try { //EXAMPLE:https://api.geoapify.com/v1/routing?waypoints=40.7757145,-73.87336398511545|40.6604335,-73.8302749&mode=drive&apiKey=YOUR_API_KEY const url = `https://api.geoapify.com/v1/routing?waypoints=${from.lat},${from.lon}|${to.lat},${to.lon}&mode=drive&format=geojson&apiKey=${apiKey}` @@ -127,12 +168,17 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { const data = await res.json() console.log('Geoapify route data:', data); if (!data?.features?.[0]) throw new Error('No route') - if (routeLayer) routeLayer.remove() - routeLayer = L.geoJSON(data.features[0], { - style: { color: '#2563eb', weight: 4, opacity: 0.95 }, - }).addTo(map) + + animateRoute(data.features[0]); + + // Extract and set turn-by-turn instructions + if (data.features[0]?.properties?.legs?.[0]?.steps) { + setRouteSteps(data.features[0].properties.legs[0].steps) + } } catch { drawStraight() + } finally { + setIsRouteLoading(false) } } @@ -151,7 +197,7 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { }, [from?.lat, from?.lon, to?.lat, to?.lon, animateKey]) return ( -
+
@@ -163,7 +209,29 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { Select locations to preview )}
-
+
+
+ {isRouteLoading && ( +
+ +
+ )} +
+ {routeSteps.length > 0 && ( +
+
+ + Turn-by-Turn Directions +
+
    + {routeSteps.map((step, index) => ( +
  1. + {step.instruction.text} +
  2. + ))} +
+
+ )}
) } \ No newline at end of file diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 00f455e..16053b2 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -4,7 +4,7 @@ import LeafletMap from "@/components/LeafletMap"; import { Button } from "@/components/ui/button"; import { ThemeToggle } from "@/components/ui/theme-toggle"; import { predictTravelTime } from "@/lib/api"; -import { Clock, MapPin, Car, Calendar, AlertTriangle } from "lucide-react"; +import { Clock, MapPin, Car, Calendar, AlertTriangle ,Loader2} from "lucide-react"; import Footer from "@/components/Footer"; import { motion, AnimatePresence } from "framer-motion"; @@ -45,9 +45,19 @@ export default function Home() { const [toId, setToId] = useState(""); const [fromLocation, setFromLocation] = useState(null); const [toLocation, setToLocation] = useState(null); - const [dateStr, setDateStr] = useState(""); const [predicted, setPredicted] = useState(null); const [animKey, setAnimKey] = useState(0); + + const getInitialDateTime = () => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}`; + }; + const [dateStr, setDateStr] = useState(getInitialDateTime()); const [currentCity, setCurrentCity] = useState<"new_york" | "san_francisco">( "new_york" ); @@ -146,6 +156,7 @@ export default function Home() { city: currentCity, }); + console.log('Prediction API response:', response); if (typeof response.minutes === "number" && isFinite(response.minutes)) { setPredicted(response.minutes); if (isMobile) { @@ -466,17 +477,21 @@ export default function Home() { id="start_time" type="datetime-local" value={dateStr} - onChange={(e) => setDateStr(e.target.value)} - className="absolute inset-0 w-full opacity-0 h-full cursor-pointer z-1000" + onChange={(e) => { + console.log('Date and time selected:', e.target.value); + setDateStr(e.target.value); + setWarning(''); + }} + className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0" />
- + shadow-soft transition focus-within:border-primary focus-within:ring-2 + focus-within:ring-primary/30 flex justify-between items-center"> + {dateStr ? formatDateTime(dateStr) : 'dd-mm-yyyy --:--'} - +
@@ -490,9 +505,16 @@ export default function Home() { From f2f3d98394c1742167401cda7c978862a8304b8e Mon Sep 17 00:00:00 2001 From: Anurag-Bansode Date: Thu, 16 Oct 2025 21:06:36 +0530 Subject: [PATCH 02/11] fix(DatePicker): Datepicker now displays accurate data --- frontend/src/components/DateTimePicker.css | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/frontend/src/components/DateTimePicker.css b/frontend/src/components/DateTimePicker.css index 6bfe7e9..55d8613 100644 --- a/frontend/src/components/DateTimePicker.css +++ b/frontend/src/components/DateTimePicker.css @@ -128,3 +128,31 @@ .react-datepicker__time-list::-webkit-scrollbar { display: none; /* Safari and Chrome */ } + +/* Center the datepicker on small screens */ +@media (max-width: 480px) { + .react-datepicker-portal { + @apply fixed inset-0 z-30 flex items-center justify-center bg-black/50; + } + .react-datepicker-portal .react-datepicker-popper { + /* Reset popper.js inline styles to allow flex centering */ + @apply static transform-none; + } + + /* Reduce calendar size on mobile */ + .react-datepicker { + @apply p-1; + } + .react-datepicker__header { + @apply p-1; + } + .react-datepicker__current-month { + @apply text-sm; + } + .react-datepicker__day-name { + @apply m-0.5 w-7; + } + .react-datepicker__day { + @apply m-0.5 h-7 w-7; + } +} From 42431b9e0bb5afe3f642d8ecfa134fb7ea09df7f Mon Sep 17 00:00:00 2001 From: Anurag-Bansode Date: Fri, 17 Oct 2025 10:29:45 +0530 Subject: [PATCH 03/11] feat(map): enhance route animation and add turn markers Improves the user experience of the route preview by introducing a smoother, time-based animation and adding visual cues for turns. - Adds more fluid and consistent drawing effect. - Adds an `isPredicting` state to display a "Calculating route..." message, providing better feedback during API calls. - Introduces small, white circular markers on the map to visually indicate each turn along the calculated route. --- frontend/src/components/LeafletMap.tsx | 101 +++++++++++++++++-------- frontend/src/pages/Home.tsx | 18 ++--- 2 files changed, 76 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/LeafletMap.tsx b/frontend/src/components/LeafletMap.tsx index 36711c9..697fc6d 100644 --- a/frontend/src/components/LeafletMap.tsx +++ b/frontend/src/components/LeafletMap.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import L from 'leaflet' import 'leaflet/dist/leaflet.css' -import { List, Loader2 } from 'lucide-react' +import {Loader2 } from 'lucide-react' // Fix for default markers not showing in bundled environments @@ -32,6 +32,21 @@ const createCustomIcon = (color: string) => { }) } +// Create a small white dot icon for turn-by-turn markers +const createTurnIcon = () => { + const markerHtml = ` + + + ` + + return L.divIcon({ + className: 'leaflet-turn-icon', + html: markerHtml, + iconSize: [12, 12], + iconAnchor: [6, 6], + }) +} + export type GeoPoint = { name?: string lat: number @@ -42,6 +57,7 @@ interface LeafletMapProps { from?: GeoPoint | null to?: GeoPoint | null animateKey?: string | number + isPredicting?: boolean } interface RouteStep { @@ -50,7 +66,7 @@ interface RouteStep { } } -export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { +export default function LeafletMap({ from, to, animateKey, isPredicting }: LeafletMapProps) { const [isRouteLoading, setIsRouteLoading] = useState(false) const [routeSteps, setRouteSteps] = useState([]) @@ -65,7 +81,7 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { : 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' - + const tiles = L.tileLayer(tileUrl, { maxZoom: 19, attribution: @@ -77,6 +93,7 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { let markers: L.Marker[] = [] let routeLayer: L.Polyline | L.GeoJSON | null = null + let turnMarkers: L.Marker[] = [] const fitBoundsIfNeeded = () => { const points: L.LatLngExpression[] = [] @@ -109,6 +126,11 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { } } + const clearTurnMarkers = () => { + turnMarkers.forEach(m => m.remove()) + turnMarkers = [] + } + const drawStraight = () => { if (!from || !to) return if (routeLayer) routeLayer.remove() @@ -123,30 +145,54 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { const animateRoute = (geoJsonData: any) => { if (routeLayer) routeLayer.remove(); + clearTurnMarkers(); const allCoords = geoJsonData.geometry.coordinates.flat(1).map((c: number[]) => L.latLng(c[1], c[0])); const animatedPolyline = L.polyline([], { color: '#2563eb', weight: 4, opacity: 0.95 }).addTo(map); routeLayer = animatedPolyline; - let i = 0; - const step = () => { - if (i < allCoords.length) { - animatedPolyline.addLatLng(allCoords[i]); - i++; + const animationDuration = 750; // Animate over 750ms + let startTime: number | null = null; + + const step = (timestamp: number) => { + if (!startTime) { + startTime = timestamp; + } + + const progress = Math.min((timestamp - startTime) / animationDuration, 1); + const pointsToShow = Math.floor(progress * allCoords.length); + + // Only update if there are new points to show to avoid unnecessary re-renders + if (pointsToShow > animatedPolyline.getLatLngs().length) { + animatedPolyline.setLatLngs(allCoords.slice(0, pointsToShow)); + } + + if (progress < 1) { requestAnimationFrame(step); } else { - // Animation finished, bind the popup + animatedPolyline.setLatLngs(allCoords); // Ensure the full route is drawn const properties = geoJsonData.properties; if (properties) { const distanceKm = (properties.distance / 1000).toFixed(1); const timeMinutes = Math.round(properties.time / 60); - animatedPolyline.bindPopup( - `Route Details
Distance: ${distanceKm} km
Est. Time: ${timeMinutes} minutes` - ); + animatedPolyline.bindPopup(`Route Details
Distance: ${distanceKm} km
Est. Time: ${timeMinutes} minutes`); + } + + // Draw turn markers after animation is complete + const steps = geoJsonData.properties?.legs?.[0]?.steps; + if (steps && steps.length > 1) { + const turnIcon = createTurnIcon(); + // Start from the second step to get the first turn coordinate + for (let i = 1; i < steps.length; i++) { + const turnIndex = steps[i].from_index; + if (turnIndex < allCoords.length) { + const turnMarker = L.marker(allCoords[turnIndex], { icon: turnIcon }).addTo(map); + turnMarkers.push(turnMarker); + } + } } } }; - requestAnimationFrame(step); }; @@ -158,9 +204,10 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { return } setIsRouteLoading(true) + clearTurnMarkers() setRouteSteps([]) // Clear previous steps try { - //EXAMPLE:https://api.geoapify.com/v1/routing?waypoints=40.7757145,-73.87336398511545|40.6604335,-73.8302749&mode=drive&apiKey=YOUR_API_KEY + //EXAMPLE:https://api.geoapify.com/v1/routing?waypoints=40.7757145,-73.87336398511545|40.6604335,-73.8302749&mode=drive&apiKey=YOUR_API_KEY const url = `https://api.geoapify.com/v1/routing?waypoints=${from.lat},${from.lon}|${to.lat},${to.lon}&mode=drive&format=geojson&apiKey=${apiKey}` console.log(url); const res = await fetch(url) @@ -168,7 +215,7 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { const data = await res.json() console.log('Geoapify route data:', data); if (!data?.features?.[0]) throw new Error('No route') - + animateRoute(data.features[0]); // Extract and set turn-by-turn instructions @@ -186,11 +233,11 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) { fitBoundsIfNeeded() // Always draw something quickly, then try to replace with routed geometry if (from && to) { - drawStraight() fetchRoute() } return () => { + clearTurnMarkers() map.remove() } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -201,7 +248,11 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) {
- Route Preview + {isPredicting ? ( + Calculating route... + ) : ( + Route Preview + )}
{from && to ? ( {from.name ?? 'Start'} → {to.name ?? 'End'} @@ -217,21 +268,7 @@ export default function LeafletMap({ from, to, animateKey }: LeafletMapProps) {
)}
- {routeSteps.length > 0 && ( -
-
- - Turn-by-Turn Directions -
-
    - {routeSteps.map((step, index) => ( -
  1. - {step.instruction.text} -
  2. - ))} -
-
- )} +
) } \ No newline at end of file diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index aa2db4d..e83a095 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -5,7 +5,7 @@ import { DateTimePicker } from "@/components/DateTimePicker"; import { Button } from "@/components/ui/button"; import { ThemeToggle } from "@/components/ui/theme-toggle"; import { predictTravelTime } from "@/lib/api"; -import { Clock, MapPin, Car, Calendar, AlertTriangle ,Loader2} from "lucide-react"; +import { Clock, MapPin, Car, AlertTriangle ,Loader2} from "lucide-react"; import Footer from "@/components/Footer"; import { motion, AnimatePresence } from "framer-motion"; @@ -50,20 +50,11 @@ export default function Home() { const [predicted, setPredicted] = useState(null); const [animKey, setAnimKey] = useState(0); - const getInitialDateTime = () => { - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - const day = String(now.getDate()).padStart(2, '0'); - const hours = String(now.getHours()).padStart(2, '0'); - const minutes = String(now.getMinutes()).padStart(2, '0'); - return `${year}-${month}-${day}T${hours}:${minutes}`; - }; - const [dateStr, setDateStr] = useState(getInitialDateTime()); const [currentCity, setCurrentCity] = useState<"new_york" | "san_francisco">( "new_york" ); const [isLoading, setIsLoading] = useState(false); + const [isPredicting, setIsPredicting] = useState(false); const [warning, setWarning] = useState(""); // Add this line // Update city when location changes @@ -118,6 +109,7 @@ export default function Home() { } setIsLoading(true); + setIsPredicting(true); const isMobile = window.innerWidth <= 768; // Validate that both locations are within the same city @@ -131,6 +123,7 @@ export default function Home() { "Cross-city travel is not supported. Please select locations within the same city (New York or San Francisco)" ); setIsLoading(false); + setIsPredicting(false); return; } @@ -151,6 +144,7 @@ export default function Home() { setAnimKey((k) => k + 1); } setIsLoading(false); + setIsPredicting(false); return; } } catch (error) { @@ -167,6 +161,7 @@ export default function Home() { setAnimKey((k) => k + 1); } setIsLoading(false); + setIsPredicting(false); }; const resultRef = useRef(null); @@ -359,6 +354,7 @@ export default function Home() { from={fromLocation} to={toLocation} animateKey={`${animKey}-${fromLocation?.id}-${toLocation?.id}`} + isPredicting={isPredicting} />
From 910a37fe6e1dffedc422e6e6598b929809aed396 Mon Sep 17 00:00:00 2001 From: Anurag-Bansode Date: Fri, 17 Oct 2025 10:55:23 +0530 Subject: [PATCH 04/11] 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. --- frontend/src/components/LeafletMap.tsx | 90 +++++++++++++++++--------- 1 file changed, 59 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/LeafletMap.tsx b/frontend/src/components/LeafletMap.tsx index 697fc6d..f5bbf13 100644 --- a/frontend/src/components/LeafletMap.tsx +++ b/frontend/src/components/LeafletMap.tsx @@ -69,6 +69,7 @@ interface RouteStep { export default function LeafletMap({ from, to, animateKey, isPredicting }: LeafletMapProps) { const [isRouteLoading, setIsRouteLoading] = useState(false) const [routeSteps, setRouteSteps] = useState([]) + const [routeError, setRouteError] = useState(null); useEffect(() => { const map = L.map('route-map', { @@ -147,50 +148,73 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl if (routeLayer) routeLayer.remove(); clearTurnMarkers(); + //NOTE: the geoJson array coordinate pair [lon, lat] is converted into a Leaflet LatLng object [lat, lon]. const allCoords = geoJsonData.geometry.coordinates.flat(1).map((c: number[]) => L.latLng(c[1], c[0])); + + // Create an empty polyline (a line with multiple points). This is what we will "draw" on. + // We'll add coordinates to it over time to create the animation effect. const animatedPolyline = L.polyline([], { color: '#2563eb', weight: 4, opacity: 0.95 }).addTo(map); - routeLayer = animatedPolyline; - - const animationDuration = 750; // Animate over 750ms + routeLayer = animatedPolyline; // Keep a reference to it so we can remove it later. + + // --- Prepare Turn Markers --- + // The route data also includes "steps" (like "turn left," "go straight"). + // We extract the coordinate index for each turn. + const steps = geoJsonData.properties?.legs?.[0]?.steps; + const turnPoints = (steps && steps.length > 1) + // We skip the first step (the start) and map over the rest. + ? steps.slice(1).map((step: any) => ({ + // `from_index` tells us which point in `allCoords` corresponds to the start of this turn. + index: step.from_index, + // We get the actual LatLng object for that index. + latlng: allCoords[step.from_index], + })).filter((turn: any) => turn.latlng) // Make sure the coordinate exists. + : []; + + // --- Animation Setup --- + let nextTurnIndex = 0; // This will track which turn marker we need to draw next. + const turnIcon = createTurnIcon(); // A small white dot icon for the turns. + const animationDuration = 750; // We want the animation to last 750 milliseconds. let startTime: number | null = null; + // The `step` function is the core of our animation. It will be called on every frame. const step = (timestamp: number) => { + // On the very first frame, record the start time. if (!startTime) { startTime = timestamp; } + // Calculate how much time has passed since the animation started. + // `progress` will be a value from 0 (start) to 1 (end). const progress = Math.min((timestamp - startTime) / animationDuration, 1); + + // Based on the progress, calculate how many points of the route line should be visible. const pointsToShow = Math.floor(progress * allCoords.length); - // Only update if there are new points to show to avoid unnecessary re-renders + // To avoid unnecessary work, we only update the map if new points need to be drawn. if (pointsToShow > animatedPolyline.getLatLngs().length) { + // Update the polyline to show the new segment of the route. animatedPolyline.setLatLngs(allCoords.slice(0, pointsToShow)); + + // --- Synchronized Turn Marker Drawing --- + // This loop checks if the line has reached or passed the next turn point. + while (nextTurnIndex < turnPoints.length && turnPoints[nextTurnIndex].index <= pointsToShow) { + // If it has, we get the turn's data... + const turn = turnPoints[nextTurnIndex]; + // ...add a marker to the map at that turn's location... + turnMarkers.push(L.marker(turn.latlng, { icon: turnIcon }).addTo(map)); + // ...and move on to the next turn in our list. + nextTurnIndex++; + } } + // If the animation is not yet finished (progress < 1), we request the next frame. + // This creates a smooth loop. if (progress < 1) { requestAnimationFrame(step); } else { - animatedPolyline.setLatLngs(allCoords); // Ensure the full route is drawn - const properties = geoJsonData.properties; - if (properties) { - const distanceKm = (properties.distance / 1000).toFixed(1); - const timeMinutes = Math.round(properties.time / 60); - animatedPolyline.bindPopup(`Route Details
Distance: ${distanceKm} km
Est. Time: ${timeMinutes} minutes`); - } - - // Draw turn markers after animation is complete - const steps = geoJsonData.properties?.legs?.[0]?.steps; - if (steps && steps.length > 1) { - const turnIcon = createTurnIcon(); - // Start from the second step to get the first turn coordinate - for (let i = 1; i < steps.length; i++) { - const turnIndex = steps[i].from_index; - if (turnIndex < allCoords.length) { - const turnMarker = L.marker(allCoords[turnIndex], { icon: turnIcon }).addTo(map); - turnMarkers.push(turnMarker); - } - } - } + // --- Animation Finished --- + // Once the animation is complete, ensure the entire route is drawn. + animatedPolyline.setLatLngs(allCoords); } }; requestAnimationFrame(step); @@ -205,13 +229,15 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl } setIsRouteLoading(true) clearTurnMarkers() + setRouteError(null); // Clear previous errors setRouteSteps([]) // Clear previous steps + if (routeLayer) routeLayer.remove(); // Clear previous route before fetching try { - //EXAMPLE:https://api.geoapify.com/v1/routing?waypoints=40.7757145,-73.87336398511545|40.6604335,-73.8302749&mode=drive&apiKey=YOUR_API_KEY - const url = `https://api.geoapify.com/v1/routing?waypoints=${from.lat},${from.lon}|${to.lat},${to.lon}&mode=drive&format=geojson&apiKey=${apiKey}` + // Use waypoints.snapped=true to find the nearest routable point for each coordinate + 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}` console.log(url); const res = await fetch(url) - if (!res.ok) throw new Error(`HTTP ${res.status}`) + if (!res.ok) throw new Error(`Could not find a routable path. (HTTP ${res.status})`) const data = await res.json() console.log('Geoapify route data:', data); if (!data?.features?.[0]) throw new Error('No route') @@ -222,8 +248,10 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl if (data.features[0]?.properties?.legs?.[0]?.steps) { setRouteSteps(data.features[0].properties.legs[0].steps) } - } catch { - drawStraight() + } catch (error) { + if (error instanceof Error) { + setRouteError(error.message); + } } finally { setIsRouteLoading(false) } @@ -231,7 +259,7 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl drawMarkers() fitBoundsIfNeeded() - // Always draw something quickly, then try to replace with routed geometry + if (from && to) { fetchRoute() } From dda3654eae1c66bccc64f02ac3ecfb0d33e64026 Mon Sep 17 00:00:00 2001 From: Anurag-Bansode Date: Fri, 17 Oct 2025 18:46:30 +0530 Subject: [PATCH 05/11] perf(map): refactor map component and implement robust routing refactoring of the mapping and location logic to improve maintainability, scalability, and user experience. - Implements a robust two-step routing strategy. If the primary routing request fails, the component now uses a reverse geocoding fallback to find the nearest accessible roads and automatically retries the request. --- .../{ => DateTimePicker}/DateTimePicker.css | 0 .../{ => DateTimePicker}/DateTimePicker.tsx | 0 frontend/src/components/LeafletMap.tsx | 372 ++++++++---------- frontend/src/components/LocationSearch.tsx | 16 +- frontend/src/pages/Home.tsx | 27 +- frontend/src/types/LeafletMaps.ts | 19 + 6 files changed, 215 insertions(+), 219 deletions(-) rename frontend/src/components/{ => DateTimePicker}/DateTimePicker.css (100%) rename frontend/src/components/{ => DateTimePicker}/DateTimePicker.tsx (100%) create mode 100644 frontend/src/types/LeafletMaps.ts diff --git a/frontend/src/components/DateTimePicker.css b/frontend/src/components/DateTimePicker/DateTimePicker.css similarity index 100% rename from frontend/src/components/DateTimePicker.css rename to frontend/src/components/DateTimePicker/DateTimePicker.css diff --git a/frontend/src/components/DateTimePicker.tsx b/frontend/src/components/DateTimePicker/DateTimePicker.tsx similarity index 100% rename from frontend/src/components/DateTimePicker.tsx rename to frontend/src/components/DateTimePicker/DateTimePicker.tsx diff --git a/frontend/src/components/LeafletMap.tsx b/frontend/src/components/LeafletMap.tsx index f5bbf13..cf6c805 100644 --- a/frontend/src/components/LeafletMap.tsx +++ b/frontend/src/components/LeafletMap.tsx @@ -1,7 +1,8 @@ -import { useEffect, useState } from 'react' +import { useEffect, useState, useRef, useCallback } from 'react' import L from 'leaflet' import 'leaflet/dist/leaflet.css' import {Loader2 } from 'lucide-react' +import { RouteStep,LeafletMapProps} from '@/types/LeafletMaps'; // Fix for default markers not showing in bundled environments @@ -47,229 +48,200 @@ const createTurnIcon = () => { }) } -export type GeoPoint = { - name?: string - lat: number - lon: number -} - -interface LeafletMapProps { - from?: GeoPoint | null - to?: GeoPoint | null - animateKey?: string | number - isPredicting?: boolean -} - -interface RouteStep { - instruction: { - text: string - } -} - export default function LeafletMap({ from, to, animateKey, isPredicting }: LeafletMapProps) { const [isRouteLoading, setIsRouteLoading] = useState(false) const [routeSteps, setRouteSteps] = useState([]) const [routeError, setRouteError] = useState(null); - useEffect(() => { - const map = L.map('route-map', { - zoomControl: true, - }) - - const apiKey = import.meta.env.VITE_GEOAPIFY_API_KEY - const tileUrl = apiKey - ? `https://maps.geoapify.com/v1/tile/osm-bright/{z}/{x}/{y}.png?apiKey=${apiKey}` - : 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' - - + // Refs to hold Leaflet instances, preventing re-initialization on re-renders + const mapRef = useRef(null); + const routeLayerRef = useRef(null); + const markersRef = useRef([]); + const turnMarkersRef = useRef([]); + + // --- Map Drawing and Animation Functions (SRP) --- + + const clearTurnMarkers = useCallback(() => { + turnMarkersRef.current.forEach(m => m.remove()); + turnMarkersRef.current = []; + }, []); + + const animateRoute = useCallback((geoJsonData: any) => { + const map = mapRef.current; + if (!map) return; + + if (routeLayerRef.current) routeLayerRef.current.remove(); + clearTurnMarkers(); + + const allCoords = geoJsonData.geometry.coordinates.flat(1).map((c: number[]) => L.latLng(c[1], c[0])); + const animatedPolyline = L.polyline([], { color: '#2563eb', weight: 4, opacity: 0.95 }).addTo(map); + routeLayerRef.current = animatedPolyline; + + const steps = geoJsonData.properties?.legs?.[0]?.steps; + const turnPoints = (steps && steps.length > 1) + ? steps.slice(1).map((step: any) => ({ + index: step.from_index, + latlng: allCoords[step.from_index], + })).filter((turn: any) => turn.latlng) + : []; + + let nextTurnIndex = 0; + const turnIcon = createTurnIcon(); + const animationDuration = 750; + let startTime: number | null = null; + + const step = (timestamp: number) => { + if (!startTime) startTime = timestamp; + const progress = Math.min((timestamp - startTime) / animationDuration, 1); + const pointsToShow = Math.floor(progress * allCoords.length); + + if (pointsToShow > animatedPolyline.getLatLngs().length) { + animatedPolyline.setLatLngs(allCoords.slice(0, pointsToShow)); + + while (nextTurnIndex < turnPoints.length && turnPoints[nextTurnIndex].index <= pointsToShow) { + const turn = turnPoints[nextTurnIndex]; + turnMarkersRef.current.push(L.marker(turn.latlng, { icon: turnIcon }).addTo(map)); + nextTurnIndex++; + } + } - const tiles = L.tileLayer(tileUrl, { - maxZoom: 19, - attribution: - apiKey - ? '© OpenMapTiles © OpenStreetMap contributors | © Geoapify' - : '© OpenStreetMap contributors', - }) - tiles.addTo(map) - - let markers: L.Marker[] = [] - let routeLayer: L.Polyline | L.GeoJSON | null = null - let turnMarkers: L.Marker[] = [] - - const fitBoundsIfNeeded = () => { - const points: L.LatLngExpression[] = [] - if (from) points.push([from.lat, from.lon]) - if (to) points.push([to.lat, to.lon]) - if (points.length) { - const bounds = L.latLngBounds(points) - map.fitBounds(bounds.pad(0.25)) + if (progress < 1) { + requestAnimationFrame(step); } else { - map.setView([40.7128, -74.006], 11) + animatedPolyline.setLatLngs(allCoords); + const properties = geoJsonData.properties; + if (properties) { + const distanceKm = (properties.distance / 1000).toFixed(1); + const timeMinutes = Math.round(properties.time / 60); + animatedPolyline.bindPopup(`Route Details
Distance: ${distanceKm} km
Est. Time: ${timeMinutes} minutes`); + } } + }; + requestAnimationFrame(step); + }, [clearTurnMarkers]); + + const fetchRoute = useCallback(async () => { + if (!from || !to) return; + + const apiKey = import.meta.env.VITE_GEOAPIFY_API_KEY; + if (!apiKey) { + // Fallback to straight line if no API key + if (mapRef.current) { // Guard against mapRef.current being null + if (routeLayerRef.current) routeLayerRef.current.remove(); + routeLayerRef.current = L.polyline([[from.lat, from.lon], [to.lat, to.lon]], { color: '#2563eb', weight: 3, opacity: 0.85 }).addTo(mapRef.current); + } + return; } - const drawMarkers = () => { - markers.forEach(m => m.remove()) - markers = [] - if (from) { - const startMarker = L.marker([from.lat, from.lon], { - icon: createCustomIcon('#2563eb'), // Blue pin for start - }).addTo(map) - startMarker.bindPopup(`Start: ${from.name || 'Start Location'}`) - markers.push(startMarker) - } - if (to) { - const endMarker = L.marker([to.lat, to.lon], { - icon: createCustomIcon('#ef4444'), // Red pin for end - }).addTo(map) - endMarker.bindPopup(`End: ${to.name || 'End Location'}`) - markers.push(endMarker) + setIsRouteLoading(true); + clearTurnMarkers(); + setRouteError(null); + setRouteSteps([]); + if (routeLayerRef.current) routeLayerRef.current.remove(); + + try { + const primaryRouteResponse = await fetch( + `https://api.geoapify.com/v1/routing?waypoints=${from.lat},${from.lon}|${to.lat},${to.lon}&mode=drive&format=geojson&waypoints.snapped=true&apiKey=${apiKey}` + ); + let routeData = await primaryRouteResponse.json(); + + if (routeData.statusCode === 400) { + console.warn("Initial routing failed. Attempting fallback with reverse geocoding."); + const fromPromise = fetch(`https://api.geoapify.com/v1/geocode/reverse?lat=${from.lat}&lon=${from.lon}&apiKey=${apiKey}`).then(res => res.json()); + const toPromise = fetch(`https://api.geoapify.com/v1/geocode/reverse?lat=${to.lat}&lon=${to.lon}&apiKey=${apiKey}`).then(res => res.json()); + const [fromRev, toRev] = await Promise.all([fromPromise, toPromise]); + + const correctedFrom = fromRev?.features?.[0]?.properties; + const correctedTo = toRev?.features?.[0]?.properties; + + if (!correctedFrom || !correctedTo) throw new Error("Reverse geocoding failed."); + + const fallbackRouteResponse = await fetch( + `https://api.geoapify.com/v1/routing?waypoints=${correctedFrom.lat},${correctedFrom.lon}|${correctedTo.lat},${correctedTo.lon}&mode=drive&format=geojson&apiKey=${apiKey}` + ); + if (!fallbackRouteResponse.ok) throw new Error(`Fallback routing failed.`); + routeData = await fallbackRouteResponse.json(); } - } - const clearTurnMarkers = () => { - turnMarkers.forEach(m => m.remove()) - turnMarkers = [] - } + if (!routeData?.features?.[0]) throw new Error("No route feature found."); - const drawStraight = () => { - if (!from || !to) return - if (routeLayer) routeLayer.remove() - routeLayer = L.polyline( - [ - [from.lat, from.lon], - [to.lat, to.lon], - ], - { color: '#2563eb', weight: 3, opacity: 0.85 } - ).addTo(map) + animateRoute(routeData.features[0]); + setRouteSteps(routeData.features[0]?.properties?.legs?.[0]?.steps || []); + } catch (error) { + if (error instanceof Error) { + console.error("Final routing error:", error.message); + setRouteError(error.message); + } + } finally { + setIsRouteLoading(false); } + }, [from, to, animateRoute, clearTurnMarkers]); - const animateRoute = (geoJsonData: any) => { - if (routeLayer) routeLayer.remove(); - clearTurnMarkers(); + // --- useEffect Hooks for Lifecycle Management --- - //NOTE: the geoJson array coordinate pair [lon, lat] is converted into a Leaflet LatLng object [lat, lon]. - const allCoords = geoJsonData.geometry.coordinates.flat(1).map((c: number[]) => L.latLng(c[1], c[0])); - - // Create an empty polyline (a line with multiple points). This is what we will "draw" on. - // We'll add coordinates to it over time to create the animation effect. - const animatedPolyline = L.polyline([], { color: '#2563eb', weight: 4, opacity: 0.95 }).addTo(map); - routeLayer = animatedPolyline; // Keep a reference to it so we can remove it later. - - // --- Prepare Turn Markers --- - // The route data also includes "steps" (like "turn left," "go straight"). - // We extract the coordinate index for each turn. - const steps = geoJsonData.properties?.legs?.[0]?.steps; - const turnPoints = (steps && steps.length > 1) - // We skip the first step (the start) and map over the rest. - ? steps.slice(1).map((step: any) => ({ - // `from_index` tells us which point in `allCoords` corresponds to the start of this turn. - index: step.from_index, - // We get the actual LatLng object for that index. - latlng: allCoords[step.from_index], - })).filter((turn: any) => turn.latlng) // Make sure the coordinate exists. - : []; - - // --- Animation Setup --- - let nextTurnIndex = 0; // This will track which turn marker we need to draw next. - const turnIcon = createTurnIcon(); // A small white dot icon for the turns. - const animationDuration = 750; // We want the animation to last 750 milliseconds. - let startTime: number | null = null; - - // The `step` function is the core of our animation. It will be called on every frame. - const step = (timestamp: number) => { - // On the very first frame, record the start time. - if (!startTime) { - startTime = timestamp; - } + // Effect for map initialization (runs once) + useEffect(() => { + if (mapRef.current) return; // Initialize map only once - // Calculate how much time has passed since the animation started. - // `progress` will be a value from 0 (start) to 1 (end). - const progress = Math.min((timestamp - startTime) / animationDuration, 1); - - // Based on the progress, calculate how many points of the route line should be visible. - const pointsToShow = Math.floor(progress * allCoords.length); - - // To avoid unnecessary work, we only update the map if new points need to be drawn. - if (pointsToShow > animatedPolyline.getLatLngs().length) { - // Update the polyline to show the new segment of the route. - animatedPolyline.setLatLngs(allCoords.slice(0, pointsToShow)); - - // --- Synchronized Turn Marker Drawing --- - // This loop checks if the line has reached or passed the next turn point. - while (nextTurnIndex < turnPoints.length && turnPoints[nextTurnIndex].index <= pointsToShow) { - // If it has, we get the turn's data... - const turn = turnPoints[nextTurnIndex]; - // ...add a marker to the map at that turn's location... - turnMarkers.push(L.marker(turn.latlng, { icon: turnIcon }).addTo(map)); - // ...and move on to the next turn in our list. - nextTurnIndex++; - } - } + mapRef.current = L.map('route-map', { zoomControl: true }); + const apiKey = import.meta.env.VITE_GEOAPIFY_API_KEY; + const tileUrl = apiKey + ? `https://maps.geoapify.com/v1/tile/osm-bright/{z}/{x}/{y}.png?apiKey=${apiKey}` + : 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; - // If the animation is not yet finished (progress < 1), we request the next frame. - // This creates a smooth loop. - if (progress < 1) { - requestAnimationFrame(step); - } else { - // --- Animation Finished --- - // Once the animation is complete, ensure the entire route is drawn. - animatedPolyline.setLatLngs(allCoords); - } - }; - requestAnimationFrame(step); + L.tileLayer(tileUrl, { + maxZoom: 19, + attribution: apiKey ? '© OpenMapTiles © OpenStreetMap contributors | © Geoapify' : '© OpenStreetMap contributors', + }).addTo(mapRef.current); + + // Cleanup function to remove map on component unmount + return () => { + mapRef.current?.remove(); + mapRef.current = null; }; + }, []); - const fetchRoute = async () => { - if (!from || !to) return - const apiKey = import.meta.env.VITE_GEOAPIFY_API_KEY - if (!apiKey) { - drawStraight() - return - } - setIsRouteLoading(true) - clearTurnMarkers() - setRouteError(null); // Clear previous errors - setRouteSteps([]) // Clear previous steps - if (routeLayer) routeLayer.remove(); // Clear previous route before fetching - try { - // Use waypoints.snapped=true to find the nearest routable point for each coordinate - 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}` - console.log(url); - const res = await fetch(url) - if (!res.ok) throw new Error(`Could not find a routable path. (HTTP ${res.status})`) - const data = await res.json() - console.log('Geoapify route data:', data); - if (!data?.features?.[0]) throw new Error('No route') - - animateRoute(data.features[0]); - - // Extract and set turn-by-turn instructions - if (data.features[0]?.properties?.legs?.[0]?.steps) { - setRouteSteps(data.features[0].properties.legs[0].steps) - } - } catch (error) { - if (error instanceof Error) { - setRouteError(error.message); - } - } finally { - setIsRouteLoading(false) - } + // Effect for updating markers and view when 'from' or 'to' change + useEffect(() => { + const map = mapRef.current; + if (!map) return; + + // Draw start/end markers + markersRef.current.forEach(m => m.remove()); + markersRef.current = []; + if (from) { + const startMarker = L.marker([from.lat, from.lon], { icon: createCustomIcon('#2563eb') }).addTo(map); + startMarker.bindPopup(`Start: ${from.name || 'Start Location'}`); + markersRef.current.push(startMarker); + } + if (to) { + const endMarker = L.marker([to.lat, to.lon], { icon: createCustomIcon('#ef4444') }).addTo(map); + endMarker.bindPopup(`End: ${to.name || 'End Location'}`); + markersRef.current.push(endMarker); } - drawMarkers() - fitBoundsIfNeeded() - - if (from && to) { - fetchRoute() + // Adjust map view + const points: L.LatLngExpression[] = []; + if (from) points.push([from.lat, from.lon]); + if (to) points.push([to.lat, to.lon]); + + if (points.length > 0) { + map.fitBounds(L.latLngBounds(points).pad(0.25)); + } else { + map.setView([40.7128, -74.006], 11); // Default view } + }, [from, to]); - return () => { - clearTurnMarkers() - map.remove() + // Effect for fetching and drawing the route + useEffect(() => { + if (from && to) { + fetchRoute(); + } else { + // Clear route if 'from' or 'to' is missing + if (routeLayerRef.current) routeLayerRef.current.remove(); + clearTurnMarkers(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [from?.lat, from?.lon, to?.lat, to?.lon, animateKey]) + }, [from, to, animateKey, fetchRoute, clearTurnMarkers]); return (
@@ -279,6 +251,8 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl {isPredicting ? ( Calculating route... ) : ( + routeError ? + Route not found : Route Preview )}
diff --git a/frontend/src/components/LocationSearch.tsx b/frontend/src/components/LocationSearch.tsx index bb335f8..047ed07 100644 --- a/frontend/src/components/LocationSearch.tsx +++ b/frontend/src/components/LocationSearch.tsx @@ -16,7 +16,7 @@ interface LocationSearchProps { label: string; value: string; onChange: (val: string) => void; - onLocationSelect: (location: Location) => void; + onLocationSelect: (location: Location | null) => void; selectedLocation?: Location | null; placeholder?: string; className?: string; @@ -46,7 +46,13 @@ const SF_LOCATIONS: Location[] = [ { id: "sf_mission", name: "Mission District, SF", lat: 37.7599, lon: -122.4148 }, { id: "sf_soma", name: "SoMa, SF", lat: 37.7749, lon: -122.4194 }, { id: "sf_marina", name: "Marina District, SF", lat: 37.8024, lon: -122.4368 }, - { id: "sf_sfo_airport", name: "SFO Airport, SF", lat: 37.6213, lon: -122.3790 }, +//? Wrong data possibly { id: "sf_sfo_airport", name: "SFO Airport, SF", lat: 37.6213, lon: -122.3790 }, +{ id: "sf_sfo_airport", name: "SFO Airport, SF", lat: 37.6163, lon: -122.3863 }, //! had coorinates of runway which is not publicaly accessible + + + + + { id: "sf_oakland_airport", name: "Oakland Airport, SF", lat: 37.7126, lon: -122.2196 }, ]; @@ -109,7 +115,7 @@ export function LocationSearch({ // If user clears the input, clear the selected location if (!newValue.trim()) { - onLocationSelect(null as any); + onLocationSelect(null); } }; @@ -174,7 +180,7 @@ export function LocationSearch({ className="absolute right-1 top-1/2 h-6 w-6 -translate-y-1/2 p-0" onClick={() => { onChange(''); - onLocationSelect(null as any); + onLocationSelect(null); }} > × @@ -200,7 +206,7 @@ export function LocationSearch({
{location.name}
- {location.lat.toFixed(4)}, {location.lon.toFixed(4)} + {location.lat.toFixed(8)}, {location.lon.toFixed(8)}
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index e83a095..12b26c0 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef } from "react"; import { LocationSearch } from "@/components/LocationSearch"; import LeafletMap from "@/components/LeafletMap"; -import { DateTimePicker } from "@/components/DateTimePicker"; +import { DateTimePicker } from "@/components/DateTimePicker/DateTimePicker"; import { Button } from "@/components/ui/button"; import { ThemeToggle } from "@/components/ui/theme-toggle"; import { predictTravelTime } from "@/lib/api"; @@ -143,25 +143,22 @@ export default function Home() { } else { setAnimKey((k) => k + 1); } - setIsLoading(false); - setIsPredicting(false); return; } } catch (error) { console.error("Prediction API error:", error); + // Clear previous prediction if API fails, so fallback is used or it's reset + setPredicted(null); + } finally { + // Fallback calculation if prediction is still null after API call + if (predicted === null) { + const km = haversineKm(fromLocation, toLocation); + const minutes = estimateMinutes(km, travelDate); + setPredicted(minutes); + } + setIsLoading(false); + setIsPredicting(false); } - - // Fallback calculation - const km = haversineKm(fromLocation, toLocation); - const minutes = estimateMinutes(km, travelDate); - setPredicted(minutes); - if (isMobile) { - setTimeout(() => setAnimKey((k) => k + 1), 900); - } else { - setAnimKey((k) => k + 1); - } - setIsLoading(false); - setIsPredicting(false); }; const resultRef = useRef(null); diff --git a/frontend/src/types/LeafletMaps.ts b/frontend/src/types/LeafletMaps.ts new file mode 100644 index 0000000..767a250 --- /dev/null +++ b/frontend/src/types/LeafletMaps.ts @@ -0,0 +1,19 @@ + +type GeoPoint = { + name?: string + lat: number + lon: number +} + +export interface LeafletMapProps { + from?: GeoPoint | null + to?: GeoPoint | null + animateKey?: string | number + isPredicting?: boolean +} + +export interface RouteStep { + instruction: { + text: string + } +} From c32f7dda4a5000d25c6711850aca9c59e98bd885 Mon Sep 17 00:00:00 2001 From: Anurag-Bansode Date: Fri, 17 Oct 2025 19:00:44 +0530 Subject: [PATCH 06/11] refactor(routes): Removed unused routestep --- frontend/src/components/LeafletMap.tsx | 5 +---- frontend/src/types/LeafletMaps.ts | 6 ------ 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/frontend/src/components/LeafletMap.tsx b/frontend/src/components/LeafletMap.tsx index cf6c805..98602c9 100644 --- a/frontend/src/components/LeafletMap.tsx +++ b/frontend/src/components/LeafletMap.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useRef, useCallback } from 'react' import L from 'leaflet' import 'leaflet/dist/leaflet.css' import {Loader2 } from 'lucide-react' -import { RouteStep,LeafletMapProps} from '@/types/LeafletMaps'; +import {LeafletMapProps} from '@/types/LeafletMaps'; // Fix for default markers not showing in bundled environments @@ -50,7 +50,6 @@ const createTurnIcon = () => { export default function LeafletMap({ from, to, animateKey, isPredicting }: LeafletMapProps) { const [isRouteLoading, setIsRouteLoading] = useState(false) - const [routeSteps, setRouteSteps] = useState([]) const [routeError, setRouteError] = useState(null); // Refs to hold Leaflet instances, preventing re-initialization on re-renders @@ -136,7 +135,6 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl setIsRouteLoading(true); clearTurnMarkers(); setRouteError(null); - setRouteSteps([]); if (routeLayerRef.current) routeLayerRef.current.remove(); try { @@ -166,7 +164,6 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl if (!routeData?.features?.[0]) throw new Error("No route feature found."); animateRoute(routeData.features[0]); - setRouteSteps(routeData.features[0]?.properties?.legs?.[0]?.steps || []); } catch (error) { if (error instanceof Error) { console.error("Final routing error:", error.message); diff --git a/frontend/src/types/LeafletMaps.ts b/frontend/src/types/LeafletMaps.ts index 767a250..cbf8016 100644 --- a/frontend/src/types/LeafletMaps.ts +++ b/frontend/src/types/LeafletMaps.ts @@ -11,9 +11,3 @@ export interface LeafletMapProps { animateKey?: string | number isPredicting?: boolean } - -export interface RouteStep { - instruction: { - text: string - } -} From 83f42327bd2f9af3cb3f4e8b8c7f320434644629 Mon Sep 17 00:00:00 2001 From: Anurag-Bansode Date: Fri, 17 Oct 2025 21:41:02 +0530 Subject: [PATCH 07/11] test(deployment) --- .gitignore | 2 ++ backend/routes/predict.js | 11 ++++++- frontend/src/components/LeafletMap.tsx | 41 ++++++++++++++++++-------- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index ec6b88b..db6e4e8 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,5 @@ frontend/.vite-temp/ # Project specific notebooks/gmaps/ data/processed/ +.bak +/netlify \ No newline at end of file diff --git a/backend/routes/predict.js b/backend/routes/predict.js index 4d9a5ba..23f8e4c 100644 --- a/backend/routes/predict.js +++ b/backend/routes/predict.js @@ -59,11 +59,15 @@ function estimateTravelTime(distanceKm, startTime, city) { return minutes > 5 ? minutes : 5; } +// This file now exclusively serves as an Express route handler for local development or a traditional server. export const predictRoute = (req, res) => { + const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; + console.log(`[predict.js] [${requestId}] Received prediction request.`); try { const { from, to, startTime, city } = req.body || {}; if (!from || !to || !startTime || !city) { + console.warn(`[predict.js] [${requestId}] Bad Request: Missing required fields. Body:`, req.body); return res.status(400).json({ error: 'Missing required fields: from, to, startTime, city' }); } @@ -73,6 +77,7 @@ export const predictRoute = (req, res) => { from.lat < -90 || from.lat > 90 || to.lat < -90 || to.lat > 90 || from.lon < -180 || from.lon > 180 || to.lon < -180 || to.lon > 180 ) { + console.warn(`[predict.js] [${requestId}] Bad Request: Invalid coordinates. From:`, from, "To:", to); return res.status(400).json({ error: 'Invalid coordinates' }); } @@ -81,9 +86,11 @@ export const predictRoute = (req, res) => { const cacheKey = createCacheKey(from, to, startTime, cityKey); const cached = predictionCache.get(cacheKey); if (cached !== undefined) { + console.log(`[predict.js] [${requestId}] Cache HIT for key: ${cacheKey}`); return res.json({ ...cached, cached: true }); } + console.log(`[predict.js] [${requestId}] Cache MISS for key: ${cacheKey}. Calculating new prediction.`); const distanceKm = calculateDistance(from.lat, from.lon, to.lat, to.lon); const minutes = estimateTravelTime(distanceKm, startTime, cityKey); @@ -98,9 +105,11 @@ export const predictRoute = (req, res) => { }; predictionCache.set(cacheKey, prediction); + console.log(`[predict.js] [${requestId}] Prediction successful. Result:`, prediction); return res.json({ ...prediction, cached: false }); } catch (error) { - console.error('Prediction error:', error); + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[predict.js] [${requestId}] Prediction error: ${errorMessage}`, { error }); return res.status(500).json({ error: 'Internal server error', message: error?.message ?? String(error), diff --git a/frontend/src/components/LeafletMap.tsx b/frontend/src/components/LeafletMap.tsx index 98602c9..f31d747 100644 --- a/frontend/src/components/LeafletMap.tsx +++ b/frontend/src/components/LeafletMap.tsx @@ -120,6 +120,7 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl }, [clearTurnMarkers]); const fetchRoute = useCallback(async () => { + console.log(`[LeafletMap] Fetching route from ${from?.name ?? 'start'} to ${to?.name ?? 'end'}.`); if (!from || !to) return; const apiKey = import.meta.env.VITE_GEOAPIFY_API_KEY; @@ -127,6 +128,7 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl // Fallback to straight line if no API key if (mapRef.current) { // Guard against mapRef.current being null if (routeLayerRef.current) routeLayerRef.current.remove(); + console.warn("[LeafletMap] No API key. Drawing a straight line as a fallback."); routeLayerRef.current = L.polyline([[from.lat, from.lon], [to.lat, to.lon]], { color: '#2563eb', weight: 3, opacity: 0.85 }).addTo(mapRef.current); } return; @@ -138,35 +140,46 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl if (routeLayerRef.current) routeLayerRef.current.remove(); try { - const primaryRouteResponse = await fetch( - `https://api.geoapify.com/v1/routing?waypoints=${from.lat},${from.lon}|${to.lat},${to.lon}&mode=drive&format=geojson&waypoints.snapped=true&apiKey=${apiKey}` - ); + const primaryRouteUrl = `https://api.geoapify.com/v1/routing?waypoints=${from.lat},${from.lon}|${to.lat},${to.lon}&mode=drive&format=geojson&waypoints.snapped=true&apiKey=${apiKey}`; + console.log("[LeafletMap] Primary API Request:", primaryRouteUrl); + const primaryRouteResponse = await fetch(primaryRouteUrl); let routeData = await primaryRouteResponse.json(); + console.log("[LeafletMap] Primary API Response:", { status: primaryRouteResponse.status, ok: primaryRouteResponse.ok, data: routeData }); if (routeData.statusCode === 400) { - console.warn("Initial routing failed. Attempting fallback with reverse geocoding."); - const fromPromise = fetch(`https://api.geoapify.com/v1/geocode/reverse?lat=${from.lat}&lon=${from.lon}&apiKey=${apiKey}`).then(res => res.json()); - const toPromise = fetch(`https://api.geoapify.com/v1/geocode/reverse?lat=${to.lat}&lon=${to.lon}&apiKey=${apiKey}`).then(res => res.json()); + console.warn("[LeafletMap] Initial routing failed (status 400). Attempting fallback with reverse geocoding."); + const fromRevUrl = `https://api.geoapify.com/v1/geocode/reverse?lat=${from.lat}&lon=${from.lon}&apiKey=${apiKey}`; + const toRevUrl = `https://api.geoapify.com/v1/geocode/reverse?lat=${to.lat}&lon=${to.lon}&apiKey=${apiKey}`; + console.log("[LeafletMap] Fallback Geocode Requests:", { from: fromRevUrl, to: toRevUrl }); + + const fromPromise = fetch(fromRevUrl).then(res => res.json()); + const toPromise = fetch(toRevUrl).then(res => res.json()); const [fromRev, toRev] = await Promise.all([fromPromise, toPromise]); + console.log("[LeafletMap] Fallback Geocode Responses:", { fromRev, toRev }); const correctedFrom = fromRev?.features?.[0]?.properties; const correctedTo = toRev?.features?.[0]?.properties; - if (!correctedFrom || !correctedTo) throw new Error("Reverse geocoding failed."); + if (!correctedFrom || !correctedTo) { + console.error("[LeafletMap] Reverse geocoding failed for fallback.", { fromRev, toRev }); + throw new Error("Reverse geocoding failed."); + } - const fallbackRouteResponse = await fetch( - `https://api.geoapify.com/v1/routing?waypoints=${correctedFrom.lat},${correctedFrom.lon}|${correctedTo.lat},${correctedTo.lon}&mode=drive&format=geojson&apiKey=${apiKey}` - ); + const fallbackRouteUrl = `https://api.geoapify.com/v1/routing?waypoints=${correctedFrom.lat},${correctedFrom.lon}|${correctedTo.lat},${correctedTo.lon}&mode=drive&format=geojson&apiKey=${apiKey}`; + console.log("[LeafletMap] Fallback API Request:", fallbackRouteUrl); + const fallbackRouteResponse = await fetch(fallbackRouteUrl); if (!fallbackRouteResponse.ok) throw new Error(`Fallback routing failed.`); routeData = await fallbackRouteResponse.json(); + console.log("[LeafletMap] Fallback API Response:", { status: fallbackRouteResponse.status, ok: fallbackRouteResponse.ok, data: routeData }); } - if (!routeData?.features?.[0]) throw new Error("No route feature found."); + if (!routeData?.features?.[0]) throw new Error("No route feature found in API response."); + console.log("[LeafletMap] Route data received, starting animation."); animateRoute(routeData.features[0]); } catch (error) { if (error instanceof Error) { - console.error("Final routing error:", error.message); + console.error("[LeafletMap] Final routing error:", error); setRouteError(error.message); } } finally { @@ -180,6 +193,7 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl useEffect(() => { if (mapRef.current) return; // Initialize map only once + console.log("[LeafletMap] Initializing Leaflet map component."); mapRef.current = L.map('route-map', { zoomControl: true }); const apiKey = import.meta.env.VITE_GEOAPIFY_API_KEY; const tileUrl = apiKey @@ -193,6 +207,7 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl // Cleanup function to remove map on component unmount return () => { + console.log("[LeafletMap] Cleaning up and removing map instance."); mapRef.current?.remove(); mapRef.current = null; }; @@ -203,6 +218,8 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl const map = mapRef.current; if (!map) return; + console.log("[LeafletMap] Updating start/end markers and map view."); + // Draw start/end markers markersRef.current.forEach(m => m.remove()); markersRef.current = []; From 0eb73c3b833c3218b927f142c96ed128ff82a9a7 Mon Sep 17 00:00:00 2001 From: Anurag-Bansode Date: Fri, 17 Oct 2025 21:58:59 +0530 Subject: [PATCH 08/11] test(deployment) --- backend/server.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/server.js b/backend/server.js index 9261793..32d2759 100644 --- a/backend/server.js +++ b/backend/server.js @@ -9,10 +9,12 @@ dotenv.config(); const app = express(); const PORT = process.env.PORT || 8000; +const allowedOrigin = process.env.FRONTEND_URL || 'http://localhost:3000'; + // Middleware app.use(cors({ - origin: process.env.FRONTEND_URL || 'http://localhost:3000', - credentials: true + origin: allowedOrigin, + credentials: true, })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); @@ -66,6 +68,7 @@ app.use('*', (req, res) => { if (process.env.NODE_ENV !== 'test') { app.listen(PORT, () => { console.log(`🚀 GoPredict API Server running on port ${PORT}`); + console.log(`✅ CORS enabled for origin: ${allowedOrigin}`); console.log(`📊 Health check: http://localhost:${PORT}/api/health`); console.log(`🔮 Prediction endpoint: http://localhost:${PORT}/api/predict`); }); From 6ac466c5277071082ce77d42665cd0492e43eee2 Mon Sep 17 00:00:00 2001 From: Anurag-Bansode Date: Fri, 17 Oct 2025 22:36:07 +0530 Subject: [PATCH 09/11] test(deployment) --- backend/server.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/server.js b/backend/server.js index 32d2759..f25b703 100644 --- a/backend/server.js +++ b/backend/server.js @@ -9,11 +9,21 @@ dotenv.config(); const app = express(); const PORT = process.env.PORT || 8000; -const allowedOrigin = process.env.FRONTEND_URL || 'http://localhost:3000'; +const allowedOrigins = (process.env.FRONTEND_URL || 'http://localhost:3000') + .split(',') + .map(origin => origin.trim()); +console.log(`[server.js]:${allowedOrigins}`) // Middleware app.use(cors({ - origin: allowedOrigin, + origin: (origin, callback) => { + // Allow requests with no origin (like mobile apps or curl requests) + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, credentials: true, })); app.use(express.json()); @@ -68,7 +78,7 @@ app.use('*', (req, res) => { if (process.env.NODE_ENV !== 'test') { app.listen(PORT, () => { console.log(`🚀 GoPredict API Server running on port ${PORT}`); - console.log(`✅ CORS enabled for origin: ${allowedOrigin}`); + console.log(`✅ CORS enabled for origins: ${allowedOrigins.join(', ')}`); console.log(`📊 Health check: http://localhost:${PORT}/api/health`); console.log(`🔮 Prediction endpoint: http://localhost:${PORT}/api/predict`); }); From ecd442077abc53678eda5b490cea057bed2490ca Mon Sep 17 00:00:00 2001 From: Anurag-Bansode Date: Fri, 17 Oct 2025 22:55:49 +0530 Subject: [PATCH 10/11] reset after deployment testing --- .gitignore | 2 ++ frontend/src/components/LeafletMap.tsx | 41 ++++++++++++++++++-------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index ec6b88b..db6e4e8 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,5 @@ frontend/.vite-temp/ # Project specific notebooks/gmaps/ data/processed/ +.bak +/netlify \ No newline at end of file diff --git a/frontend/src/components/LeafletMap.tsx b/frontend/src/components/LeafletMap.tsx index 98602c9..f31d747 100644 --- a/frontend/src/components/LeafletMap.tsx +++ b/frontend/src/components/LeafletMap.tsx @@ -120,6 +120,7 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl }, [clearTurnMarkers]); const fetchRoute = useCallback(async () => { + console.log(`[LeafletMap] Fetching route from ${from?.name ?? 'start'} to ${to?.name ?? 'end'}.`); if (!from || !to) return; const apiKey = import.meta.env.VITE_GEOAPIFY_API_KEY; @@ -127,6 +128,7 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl // Fallback to straight line if no API key if (mapRef.current) { // Guard against mapRef.current being null if (routeLayerRef.current) routeLayerRef.current.remove(); + console.warn("[LeafletMap] No API key. Drawing a straight line as a fallback."); routeLayerRef.current = L.polyline([[from.lat, from.lon], [to.lat, to.lon]], { color: '#2563eb', weight: 3, opacity: 0.85 }).addTo(mapRef.current); } return; @@ -138,35 +140,46 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl if (routeLayerRef.current) routeLayerRef.current.remove(); try { - const primaryRouteResponse = await fetch( - `https://api.geoapify.com/v1/routing?waypoints=${from.lat},${from.lon}|${to.lat},${to.lon}&mode=drive&format=geojson&waypoints.snapped=true&apiKey=${apiKey}` - ); + const primaryRouteUrl = `https://api.geoapify.com/v1/routing?waypoints=${from.lat},${from.lon}|${to.lat},${to.lon}&mode=drive&format=geojson&waypoints.snapped=true&apiKey=${apiKey}`; + console.log("[LeafletMap] Primary API Request:", primaryRouteUrl); + const primaryRouteResponse = await fetch(primaryRouteUrl); let routeData = await primaryRouteResponse.json(); + console.log("[LeafletMap] Primary API Response:", { status: primaryRouteResponse.status, ok: primaryRouteResponse.ok, data: routeData }); if (routeData.statusCode === 400) { - console.warn("Initial routing failed. Attempting fallback with reverse geocoding."); - const fromPromise = fetch(`https://api.geoapify.com/v1/geocode/reverse?lat=${from.lat}&lon=${from.lon}&apiKey=${apiKey}`).then(res => res.json()); - const toPromise = fetch(`https://api.geoapify.com/v1/geocode/reverse?lat=${to.lat}&lon=${to.lon}&apiKey=${apiKey}`).then(res => res.json()); + console.warn("[LeafletMap] Initial routing failed (status 400). Attempting fallback with reverse geocoding."); + const fromRevUrl = `https://api.geoapify.com/v1/geocode/reverse?lat=${from.lat}&lon=${from.lon}&apiKey=${apiKey}`; + const toRevUrl = `https://api.geoapify.com/v1/geocode/reverse?lat=${to.lat}&lon=${to.lon}&apiKey=${apiKey}`; + console.log("[LeafletMap] Fallback Geocode Requests:", { from: fromRevUrl, to: toRevUrl }); + + const fromPromise = fetch(fromRevUrl).then(res => res.json()); + const toPromise = fetch(toRevUrl).then(res => res.json()); const [fromRev, toRev] = await Promise.all([fromPromise, toPromise]); + console.log("[LeafletMap] Fallback Geocode Responses:", { fromRev, toRev }); const correctedFrom = fromRev?.features?.[0]?.properties; const correctedTo = toRev?.features?.[0]?.properties; - if (!correctedFrom || !correctedTo) throw new Error("Reverse geocoding failed."); + if (!correctedFrom || !correctedTo) { + console.error("[LeafletMap] Reverse geocoding failed for fallback.", { fromRev, toRev }); + throw new Error("Reverse geocoding failed."); + } - const fallbackRouteResponse = await fetch( - `https://api.geoapify.com/v1/routing?waypoints=${correctedFrom.lat},${correctedFrom.lon}|${correctedTo.lat},${correctedTo.lon}&mode=drive&format=geojson&apiKey=${apiKey}` - ); + const fallbackRouteUrl = `https://api.geoapify.com/v1/routing?waypoints=${correctedFrom.lat},${correctedFrom.lon}|${correctedTo.lat},${correctedTo.lon}&mode=drive&format=geojson&apiKey=${apiKey}`; + console.log("[LeafletMap] Fallback API Request:", fallbackRouteUrl); + const fallbackRouteResponse = await fetch(fallbackRouteUrl); if (!fallbackRouteResponse.ok) throw new Error(`Fallback routing failed.`); routeData = await fallbackRouteResponse.json(); + console.log("[LeafletMap] Fallback API Response:", { status: fallbackRouteResponse.status, ok: fallbackRouteResponse.ok, data: routeData }); } - if (!routeData?.features?.[0]) throw new Error("No route feature found."); + if (!routeData?.features?.[0]) throw new Error("No route feature found in API response."); + console.log("[LeafletMap] Route data received, starting animation."); animateRoute(routeData.features[0]); } catch (error) { if (error instanceof Error) { - console.error("Final routing error:", error.message); + console.error("[LeafletMap] Final routing error:", error); setRouteError(error.message); } } finally { @@ -180,6 +193,7 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl useEffect(() => { if (mapRef.current) return; // Initialize map only once + console.log("[LeafletMap] Initializing Leaflet map component."); mapRef.current = L.map('route-map', { zoomControl: true }); const apiKey = import.meta.env.VITE_GEOAPIFY_API_KEY; const tileUrl = apiKey @@ -193,6 +207,7 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl // Cleanup function to remove map on component unmount return () => { + console.log("[LeafletMap] Cleaning up and removing map instance."); mapRef.current?.remove(); mapRef.current = null; }; @@ -203,6 +218,8 @@ export default function LeafletMap({ from, to, animateKey, isPredicting }: Leafl const map = mapRef.current; if (!map) return; + console.log("[LeafletMap] Updating start/end markers and map view."); + // Draw start/end markers markersRef.current.forEach(m => m.remove()); markersRef.current = []; From 1023bb194b6ac1020ac9c88dc8c608e30d050ee2 Mon Sep 17 00:00:00 2001 From: Anurag-Bansode Date: Fri, 17 Oct 2025 22:59:47 +0530 Subject: [PATCH 11/11] test(deployment) --- netlify.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netlify.toml b/netlify.toml index 04a0239..fce6c72 100644 --- a/netlify.toml +++ b/netlify.toml @@ -5,7 +5,7 @@ SECRETS_SCAN_OMIT_KEYS = "VITE_API_URL,VITE_GEOAPIFY_API_KEY" [build] base = "frontend" command = "pnpm install && pnpm run build" -publish = "dist" +publish = "/dist" [[redirects]] from = "/*"