);
};
const ItineraryDetail = ({
itinerary,
onClose,
}: {
itinerary: Itinerary;
onClose: () => void;
}) => {
const { t } = useTranslation();
const mapRef = useRef(null);
const { destination: userDestination } = usePlanner();
const [nextArrivals, setNextArrivals] = useState<
Record
>({});
const routeGeoJson = {
type: "FeatureCollection",
features: itinerary.legs.map((leg) => ({
type: "Feature" as const,
geometry: {
type: "LineString" as const,
coordinates: leg.geometry?.coordinates || [],
},
properties: {
mode: leg.mode,
color:
leg.mode === "WALK"
? "#9ca3af"
: leg.routeColor
? `#${leg.routeColor}`
: "#2563eb",
},
})),
};
// Create GeoJSON for all markers
const markersGeoJson = useMemo(() => {
const features: any[] = [];
// Add points for each leg transition
itinerary.legs.forEach((leg, idx) => {
// Add "from" point of the leg
if (leg.from?.lat && leg.from?.lon) {
features.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: [leg.from.lon, leg.from.lat],
},
properties: {
type: idx === 0 ? "origin" : "transfer",
name: leg.from.name || "",
index: idx.toString(),
},
});
}
// If it's the last leg, also add the "to" point
if (idx === itinerary.legs.length - 1 && leg.to?.lat && leg.to?.lon) {
features.push({
type: "Feature",
geometry: { type: "Point", coordinates: [leg.to.lon, leg.to.lat] },
properties: {
type: "destination",
name: leg.to.name || "",
index: (idx + 1).toString(),
},
});
}
// Add intermediate stops
leg.intermediateStops?.forEach((stop) => {
features.push({
type: "Feature",
geometry: { type: "Point", coordinates: [stop.lon, stop.lat] },
properties: {
type: "intermediate",
name: stop.name || "Intermediate stop",
},
});
});
});
return { type: "FeatureCollection", features };
}, [itinerary]);
// Get origin and destination coordinates
const origin = itinerary.legs[0]?.from;
const destination = itinerary.legs[itinerary.legs.length - 1]?.to;
useEffect(() => {
if (!mapRef.current) return;
// Small delay to ensure map is fully loaded
const timer = setTimeout(() => {
if (mapRef.current && itinerary.legs.length > 0) {
const bounds = new maplibregl.LngLatBounds();
// Add all route coordinates to bounds
itinerary.legs.forEach((leg) => {
leg.geometry?.coordinates.forEach((coord) =>
bounds.extend([coord[0], coord[1]])
);
});
// Also include markers (origin, destination, transfers, intermediate) so all are visible
markersGeoJson.features.forEach((feature: any) => {
if (
feature.geometry?.type === "Point" &&
Array.isArray(feature.geometry.coordinates)
) {
const [lng, lat] = feature.geometry.coordinates as [number, number];
bounds.extend([lng, lat]);
}
});
// Ensure bounds are valid before fitting
if (!bounds.isEmpty()) {
mapRef.current.fitBounds(bounds, { padding: 80, duration: 1000 });
}
}
}, 100);
return () => clearTimeout(timer);
}, [mapRef.current, itinerary]);
// Fetch next arrivals for bus legs
useEffect(() => {
const fetchArrivals = async () => {
const arrivalsByStop: Record = {};
for (const leg of itinerary.legs) {
if (leg.mode !== "WALK" && leg.from?.stopId) {
const stopKey = leg.from.name || leg.from.stopId;
if (!arrivalsByStop[stopKey]) {
try {
//TODO: Allow multiple stops one request
const resp = await fetch(
`/api/stops/arrivals?id=${encodeURIComponent(leg.from.stopId)}`,
{ headers: { Accept: "application/json" } }
);
if (resp.ok) {
arrivalsByStop[stopKey] = await resp.json() satisfies ConsolidatedCirculation[];
}
} catch (err) {
console.warn(
`Failed to fetch arrivals for ${leg.from.stopId}:`,
err
);
}
}
}
}
setNextArrivals(arrivalsByStop);
};
fetchArrivals();
}, [itinerary]);
return (
{/* Map Section */}
{/* All markers as GeoJSON layers */}
{/* Intermediate stops (smaller white dots) - rendered first to be at the bottom */}
{/* Outer circle for all numbered markers */}
{/* Numbers for markers */}