diff --git a/src/app/docs/_components/examples/osrm-route-example.tsx b/src/app/docs/_components/examples/osrm-route-example.tsx index 72e6228..52e2046 100644 --- a/src/app/docs/_components/examples/osrm-route-example.tsx +++ b/src/app/docs/_components/examples/osrm-route-example.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import type { Map as MapLibreMap, MapMouseEvent } from "maplibre-gl"; import { Map, MapMarker, @@ -8,63 +9,179 @@ import { MapRoute, MarkerLabel, } from "@/registry/map"; -import { Loader2 } from "lucide-react"; +import { Loader2, Clock, Route } from "lucide-react"; +import { Button } from "@/components/ui/button"; -const start = { name: "Times Square", lng: -73.9855, lat: 40.758 }; -const end = { name: "Central Park", lng: -73.9654, lat: 40.7829 }; +const getRouteId = (index: number) => `route-${index}`; + +const start = { name: "Amsterdam", lng: 4.9041, lat: 52.3676 }; +const end = { name: "Rotterdam", lng: 4.4777, lat: 51.9244 }; + +interface RouteData { + coordinates: [number, number][]; + duration: number; // seconds + distance: number; // meters +} + +function formatDuration(seconds: number): string { + const mins = Math.round(seconds / 60); + if (mins < 60) return `${mins} min`; + const hours = Math.floor(mins / 60); + const remainingMins = mins % 60; + return `${hours}h ${remainingMins}m`; +} + +function formatDistance(meters: number): string { + if (meters < 1000) return `${Math.round(meters)} m`; + return `${(meters / 1000).toFixed(1)} km`; +} export function OsrmRouteExample() { - const [route, setRoute] = useState<[number, number][] | null>(null); + const [routes, setRoutes] = useState([]); + const [selectedIndex, setSelectedIndex] = useState(0); const [isLoading, setIsLoading] = useState(true); + const mapRef = useRef(null); + // Fetch routes useEffect(() => { - async function fetchRoute() { + async function fetchRoutes() { try { const response = await fetch( - `https://router.project-osrm.org/route/v1/driving/${start.lng},${start.lat};${end.lng},${end.lat}?overview=full&geometries=geojson` + `https://router.project-osrm.org/route/v1/driving/${start.lng},${start.lat};${end.lng},${end.lat}?overview=full&geometries=geojson&alternatives=true` ); const data = await response.json(); - if (data.routes?.[0]?.geometry?.coordinates) { - setRoute(data.routes[0].geometry.coordinates); + if (data.routes?.length > 0) { + const routeData: RouteData[] = data.routes.map( + (route: { + geometry: { coordinates: [number, number][] }; + duration: number; + distance: number; + }) => ({ + coordinates: route.geometry.coordinates, + duration: route.duration, + distance: route.distance, + }) + ); + setRoutes(routeData); } } catch (error) { - console.error("Failed to fetch route:", error); + console.error("Failed to fetch routes:", error); } finally { setIsLoading(false); } } - fetchRoute(); + fetchRoutes(); }, []); + // Handle route click - only select topmost route when overlapping + useEffect(() => { + const map = mapRef.current; + if (!map || routes.length === 0) return; + + const routeIds = routes.map((_, i) => getRouteId(i)); + + const handleClick = (e: MapMouseEvent) => { + const layers = routeIds.filter((id) => map.getLayer(id)); + if (layers.length === 0) return; + + const features = map.queryRenderedFeatures(e.point, { layers }); + if (features.length === 0) return; + + // Only select the topmost route + const topLayerId = features[0].layer.id; + const clickedIndex = routes.findIndex( + (_, i) => getRouteId(i) === topLayerId + ); + if (clickedIndex !== -1 && clickedIndex !== selectedIndex) { + setSelectedIndex(clickedIndex); + } + }; + + map.on("click", handleClick); + return () => { + map.off("click", handleClick); + }; + }, [routes, selectedIndex]); + + // Sort routes so selected is always rendered last (on top) + const sortedRoutes = useMemo( + () => + routes + .map((route, index) => ({ route, index })) + .sort((a, b) => + a.index === selectedIndex ? 1 : b.index === selectedIndex ? -1 : 0 + ), + [routes, selectedIndex] + ); + return ( -
- - {route && ( - - )} +
+ + {sortedRoutes.map(({ route, index }) => { + const isSelected = index === selectedIndex; + return ( + + ); + })}
- Start: {start.name} + {start.name}
- End: {end.name} + {end.name} + {routes.length > 0 && ( +
+ {routes.map((route, index) => { + const isActive = index === selectedIndex; + const isFastest = index === 0; + return ( + + ); + })} +
+ )} + {isLoading && (
diff --git a/src/app/docs/api-reference/page.tsx b/src/app/docs/api-reference/page.tsx index ecde318..ee43181 100644 --- a/src/app/docs/api-reference/page.tsx +++ b/src/app/docs/api-reference/page.tsx @@ -448,6 +448,13 @@ export default function ApiReferencePage() {

- +

- Fetch real driving directions from the OSRM API and display them on - the map. + Display multiple route options and let users select between them. This + example fetches real driving directions from the{" "} + + OSRM API + + . Click on a route or use the buttons to switch.

- + diff --git a/src/registry/map.tsx b/src/registry/map.tsx index da46a50..b87384c 100644 --- a/src/registry/map.tsx +++ b/src/registry/map.tsx @@ -834,6 +834,8 @@ function MapPopup({ } type MapRouteProps = { + /** Optional unique identifier for the route layer */ + id?: string; /** Array of [longitude, latitude] coordinate pairs defining the route */ coordinates: [number, number][]; /** Line color as CSS color value (default: "#4285F4") */ @@ -855,6 +857,7 @@ type MapRouteProps = { }; function MapRoute({ + id, coordinates, color = "#4285F4", width = 3, @@ -867,8 +870,8 @@ function MapRoute({ }: MapRouteProps) { const { map, isLoaded } = useMap(); const autoId = useId(); - const sourceId = `route-source-${autoId}`; - const layerId = `route-layer-${autoId}`; + const sourceId = id ?? `route-source-${autoId}`; + const layerId = id ?? `route-layer-${autoId}`; // Add source and layer on mount useEffect(() => {