Skip to content

Commit 6da9ebe

Browse files
committed
feat: add isochrone functionality with Mapbox API and integrate into MarkerMap
1 parent eb0db68 commit 6da9ebe

27 files changed

+666
-454
lines changed

example/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ThemeProvider } from "@tracktor/design-system";
22
import FeaturesExample from "example/FeaturesExample.tsx";
3+
import IsochroneExample from "example/IsochroneExample.tsx";
34
import LandingPage from "example/LandingPage.tsx";
45
import MarkersExample from "example/MarkersExample";
56
import NearestMarkerExample from "example/NearestMarkerExample.tsx";
@@ -20,6 +21,7 @@ const App = () => {
2021
<Route path="/features" element={<FeaturesExample themeMode={themeMode} setThemeMode={setThemeMode} />} />
2122
<Route path="/route" element={<RouteExample themeMode={themeMode} setThemeMode={setThemeMode} />} />
2223
<Route path="/nearest-marker" element={<NearestMarkerExample themeMode={themeMode} setThemeMode={setThemeMode} />} />
24+
<Route path="/isochrone" element={<IsochroneExample themeMode={themeMode} setThemeMode={setThemeMode} />} />
2325
</Routes>
2426
</MapProvider>
2527
</ThemeProvider>

example/IsochroneExample.tsx

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { Box, Button, MenuItem, Select, Stack, Switch, Typography } from "@tracktor/design-system";
2+
import Navbar from "example/Navbar.tsx";
3+
import { useEffect, useMemo, useState } from "react";
4+
import type { ProjectionSpecification } from "react-map-gl";
5+
import MarkerMap from "@/features/MarkerMap/MarkerMap";
6+
import type { RoutingProfile } from "@/services/core/interface";
7+
import isPointInGeoJSON from "@/utils/isPointInGeoJSON"; // ✅ ta version maison
8+
9+
const predefinedOrigins = [
10+
{ coords: [2.3522, 48.8566], id: "paris", name: "Paris" },
11+
{ coords: [4.8357, 45.764], id: "lyon", name: "Lyon" },
12+
{ coords: [-0.5792, 44.8378], id: "bordeaux", name: "Bordeaux" },
13+
];
14+
15+
interface IsochroneExampleProps {
16+
themeMode: "light" | "dark";
17+
setThemeMode: (mode: "light" | "dark") => void;
18+
}
19+
20+
const IsochroneExample = ({ themeMode, setThemeMode }: IsochroneExampleProps) => {
21+
const [projection, setProjection] = useState<ProjectionSpecification>({ name: "mercator" });
22+
const [profile, setProfile] = useState<RoutingProfile>("driving");
23+
const [origin, setOrigin] = useState(predefinedOrigins[0]);
24+
const [intervals, setIntervals] = useState([5, 10, 15]);
25+
const [isochroneData, setIsochroneData] = useState<GeoJSON.FeatureCollection<GeoJSON.Polygon> | null>(null);
26+
const [cooperativeGestures, setCooperativeGestures] = useState(true);
27+
const [doubleClickZoom, setDoubleClickZoom] = useState(true);
28+
29+
// ✅ markers autour de l’origine
30+
const generateMarkers = (base: typeof origin) =>
31+
Array.from({ length: 12 }, (_, i) => {
32+
const offsetLng = (Math.random() - 0.5) * 0.5;
33+
const offsetLat = (Math.random() - 0.5) * 0.5;
34+
return {
35+
color: "#f59e0b", // orange par défaut
36+
id: i + 1,
37+
lat: base.coords[1] + offsetLat,
38+
lng: base.coords[0] + offsetLng,
39+
name: `Point ${i + 1}`,
40+
};
41+
});
42+
43+
const [markers, setMarkers] = useState(() => generateMarkers(origin));
44+
45+
useEffect(() => {
46+
if (!isochroneData) {
47+
return;
48+
}
49+
50+
setMarkers((prev) =>
51+
prev.map((m) => {
52+
const inside = isochroneData.features.some((f) => isPointInGeoJSON([m.lng, m.lat], f));
53+
return {
54+
...m,
55+
color: inside ? "#22c55e" : "#9ca3af",
56+
};
57+
}),
58+
);
59+
}, [isochroneData]);
60+
61+
const originMarker = useMemo(
62+
() => [
63+
{
64+
id: "origin",
65+
lat: origin.coords[1],
66+
lng: origin.coords[0],
67+
Tooltip: <div>📍 {origin.name} (origin)</div>,
68+
variant: "success",
69+
},
70+
],
71+
[origin],
72+
);
73+
74+
const allMarkers = useMemo(() => [...markers, ...originMarker], [markers, originMarker]);
75+
76+
return (
77+
<>
78+
<Navbar />
79+
<Stack direction="row" sx={{ height: "100vh", overflow: "hidden", width: "100vw" }}>
80+
{/* 🗺️ MAP */}
81+
<Box sx={{ flex: 1 }}>
82+
<MarkerMap
83+
height="100%"
84+
width="100%"
85+
markers={allMarkers}
86+
cooperativeGestures={cooperativeGestures}
87+
doubleClickZoom={doubleClickZoom}
88+
projection={projection}
89+
fitBounds
90+
isochrone={{
91+
intervals,
92+
onIsochroneLoaded: setIsochroneData,
93+
origin: origin.coords as [number, number],
94+
profile,
95+
}}
96+
/>
97+
</Box>
98+
99+
{/* ⚙️ PANNEAU DE CONTRÔLE */}
100+
<Box
101+
sx={{
102+
backgroundColor: "background.paper",
103+
borderColor: "divider",
104+
borderLeft: "1px solid",
105+
color: "text.primary",
106+
display: "flex",
107+
flexDirection: "column",
108+
gap: 2,
109+
overflowY: "auto",
110+
p: 2,
111+
width: 300,
112+
}}
113+
>
114+
<Typography variant="h6">🕒 Isochrone + Reachability</Typography>
115+
116+
<Button variant="outlined" onClick={() => setThemeMode(themeMode === "dark" ? "light" : "dark")}>
117+
{themeMode === "dark" ? "Light mode" : "Dark mode"}
118+
</Button>
119+
120+
{/* 🔽 Origine */}
121+
<Typography variant="body2" color="text.secondary">
122+
Origin
123+
</Typography>
124+
<Select
125+
value={origin.id}
126+
onChange={(e) => {
127+
const newOrigin = predefinedOrigins.find((o) => o.id === e.target.value);
128+
if (!newOrigin) {
129+
return;
130+
}
131+
132+
setOrigin(newOrigin);
133+
setMarkers(generateMarkers(newOrigin));
134+
}}
135+
size="small"
136+
>
137+
{predefinedOrigins.map((o) => (
138+
<MenuItem key={o.id} value={o.id}>
139+
{o.name}
140+
</MenuItem>
141+
))}
142+
</Select>
143+
144+
{/* 🛣️ Profile */}
145+
<Typography variant="body2" color="text.secondary">
146+
Profile
147+
</Typography>
148+
<Select value={profile} onChange={(e) => setProfile(e.target.value as RoutingProfile)} size="small">
149+
<MenuItem value="driving">🚗 Driving</MenuItem>
150+
<MenuItem value="walking">🚶 Walking</MenuItem>
151+
<MenuItem value="cycling">🚴 Cycling</MenuItem>
152+
</Select>
153+
154+
{/* ⏱️ Intervalles */}
155+
<Typography variant="body2" color="text.secondary">
156+
Isochrone intervals (minutes)
157+
</Typography>
158+
<Select
159+
value={intervals.join(",")}
160+
onChange={(e) =>
161+
setIntervals(
162+
e.target.value
163+
.toString()
164+
.split(",")
165+
.map((v) => Number(v.trim())),
166+
)
167+
}
168+
size="small"
169+
>
170+
<MenuItem value="5,10,15">5 / 10 / 15 min</MenuItem>
171+
<MenuItem value="10,20,30">10 / 20 / 30 min</MenuItem>
172+
<MenuItem value="15,30,45">15 / 30 / 45 min</MenuItem>
173+
</Select>
174+
175+
{/* 🌍 Projection */}
176+
<Typography variant="body2" color="text.secondary">
177+
Projection
178+
</Typography>
179+
<Select
180+
value={projection.name}
181+
onChange={(e) => setProjection({ name: e.target.value as ProjectionSpecification["name"] })}
182+
size="small"
183+
>
184+
<MenuItem value="mercator">Mercator</MenuItem>
185+
<MenuItem value="globe">Globe</MenuItem>
186+
<MenuItem value="naturalEarth">Natural Earth</MenuItem>
187+
</Select>
188+
189+
{/* ✋ Interactions */}
190+
<Typography variant="body2" color="text.secondary">
191+
Interactions
192+
</Typography>
193+
<Stack spacing={1}>
194+
<Stack direction="row" alignItems="center" justifyContent="space-between">
195+
<Typography variant="body2">Cooperative gestures</Typography>
196+
<Switch checked={cooperativeGestures} onChange={(e) => setCooperativeGestures(e.target.checked)} />
197+
</Stack>
198+
<Stack direction="row" alignItems="center" justifyContent="space-between">
199+
<Typography variant="body2">Double click zoom</Typography>
200+
<Switch checked={doubleClickZoom} onChange={(e) => setDoubleClickZoom(e.target.checked)} />
201+
</Stack>
202+
</Stack>
203+
204+
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
205+
🟢 inside isochrone | ⚫ outside | 🟠 pending
206+
</Typography>
207+
</Box>
208+
</Stack>
209+
</>
210+
);
211+
};
212+
213+
export default IsochroneExample;

0 commit comments

Comments
 (0)