Skip to content

Commit 1756417

Browse files
authored
feat: upgrade map
feat: update nearest destination fetching to return all results and improve callback handling
2 parents 86c6399 + 796f6eb commit 1756417

22 files changed

Lines changed: 4127 additions & 587 deletions

README.md

Lines changed: 155 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -61,44 +61,44 @@ bun add @tracktor/map
6161
import { MapProvider, MarkerMap } from "@tracktor/map";
6262

6363
const markers = [
64-
{
65-
id: 1,
66-
lng: 2.3522,
67-
lat: 48.8566,
68-
Tooltip: <div>Paris</div>,
69-
color: "primary",
70-
variant: "default",
71-
},
72-
{
73-
id: 2,
74-
lng: -0.1276,
75-
lat: 51.5074,
76-
Tooltip: <div>London</div>,
77-
color: "secondary",
78-
variant: "default",
79-
},
64+
{
65+
id: 1,
66+
lng: 2.3522,
67+
lat: 48.8566,
68+
Tooltip: <div>Paris</div>,
69+
color: "primary",
70+
variant: "default",
71+
},
72+
{
73+
id: 2,
74+
lng: -0.1276,
75+
lat: 51.5074,
76+
Tooltip: <div>London</div>,
77+
color: "secondary",
78+
variant: "default",
79+
},
8080
];
8181

8282
function App() {
83-
return (
84-
<MapProvider
85-
licenseMuiX="your-muix-license"
86-
licenceMapbox="your-mapbox-token"
87-
>
88-
<MarkerMap
89-
markers={markers}
90-
center={[2.3522, 48.8566]}
91-
zoom={5}
92-
fitBounds
93-
height={500}
94-
width="100%"
95-
onMapClick={(lng, lat, marker) => {
96-
console.log("Clicked at:", lng, lat);
97-
if (marker) console.log("Marker clicked:", marker);
98-
}}
99-
/>
100-
</MapProvider>
101-
);
83+
return (
84+
<MapProvider
85+
licenseMuiX="your-muix-license"
86+
licenceMapbox="your-mapbox-token"
87+
>
88+
<MarkerMap
89+
markers={markers}
90+
center={[2.3522, 48.8566]}
91+
zoom={5}
92+
fitBounds
93+
height={500}
94+
width="100%"
95+
onMapClick={(lng, lat, marker) => {
96+
console.log("Clicked at:", lng, lat);
97+
if (marker) console.log("Marker clicked:", marker);
98+
}}
99+
/>
100+
</MapProvider>
101+
);
102102
}
103103
```
104104

@@ -115,10 +115,10 @@ Wraps your map components and injects required providers (theme, tokens, MUI X l
115115
- `licenceMapbox` — Your Mapbox access token
116116
```tsx
117117
<MapProvider
118-
licenseMuiX="your-license"
119-
licenceMapbox="your-token"
118+
licenseMuiX="your-license"
119+
licenceMapbox="your-token"
120120
>
121-
{/* Your map components */}
121+
{/* Your map components */}
122122
</MapProvider>
123123
```
124124

@@ -147,7 +147,7 @@ Main map component that handles:
147147

148148
| Prop | Type | Default | Description |
149149
|------|------|---------|-------------|
150-
| `center` | `[lng, lat]` | `[2.3522, 48.8566]` | Initial map center coordinates |
150+
| `center` | `LngLatLike \| number[]` | `[2.3522, 48.8566]` | Initial map center coordinates [lng, lat] |
151151
| `zoom` | `number` | `5` | Initial zoom level (0-22) |
152152
| `width` | `string \| number` | `"100%"` | Map container width |
153153
| `height` | `string \| number` | `300` | Map container height |
@@ -170,13 +170,14 @@ Main map component that handles:
170170
|------|------|---------|-------------|
171171
| `cooperativeGestures` | `boolean` | `true` | Require modifier key for zoom/pan |
172172
| `doubleClickZoom` | `boolean` | `true` | Enable double-click to zoom |
173-
| `onMapClick` | `(lng, lat, marker?) => void` | - | Callback for map clicks |
173+
| `onMapClick` | `(lng, lat, marker?) => void` | - | Callback for map clicks (includes clicked marker if applicable) |
174174

175175
#### Marker Props
176176

177177
| Prop | Type | Default | Description |
178178
|------|------|---------|-------------|
179179
| `markers` | `MarkerProps[]` | `[]` | Array of markers to display |
180+
| `markerImageURL` | `string` | - | Custom marker icon URL |
180181
| `openPopup` | `string \| number` | `undefined` | ID of marker with open popup |
181182
| `openPopupOnHover` | `boolean` | `false` | Open popups on hover instead of click |
182183
| `popupMaxWidth` | `string` | `"300px"` | Maximum popup width |
@@ -222,76 +223,104 @@ const marker = {
222223

223224
---
224225

225-
### Routing Props
226+
### Itinerary Props (`itineraryParams`)
226227

227-
Add a route between two points by providing `from` and `to` coordinates.
228+
Draw a route between two points with customizable styling and routing engines.
228229

229230
| Prop | Type | Default | Description |
230231
|------|------|---------|-------------|
231-
| `from` | `[lng, lat]` | - | Route starting point |
232-
| `to` | `[lng, lat]` | - | Route ending point |
232+
| `from` | `[number, number]` | - | Route starting point [lng, lat] |
233+
| `to` | `[number, number]` | - | Route ending point [lng, lat] |
233234
| `profile` | `"driving" \| "walking" \| "cycling"` | `"driving"` | Transportation mode |
234235
| `engine` | `"OSRM" \| "Mapbox"` | `"OSRM"` | Routing service to use |
235-
| `itineraryLineStyle` | `{ color, width, opacity }` | `{ color: "#3b82f6", width: 4, opacity: 0.8 }` | Route line appearance |
236+
| `itineraryLineStyle` | `Partial<ItineraryLineStyle>` | `{ color: "#3b82f6", width: 4, opacity: 0.8 }` | Route line appearance |
237+
| `initialRoute` | `Feature<LineString>` | - | Precomputed GeoJSON route |
238+
| `onRouteComputed` | `(route) => void` | - | Callback fired when route is computed |
239+
| `itineraryLabel` | `ReactNode` | - | Label displayed along the route (e.g., "12 min") |
236240

237241
**Example:**
238242
```tsx
239243
<MapView
240-
from={[2.3522, 48.8566]} // Paris
241-
to={[-0.1276, 51.5074]} // London
242-
profile="driving"
243-
engine="OSRM"
244-
itineraryLineStyle={{
245-
color: "#10b981",
246-
width: 5,
247-
opacity: 0.9
244+
itineraryParams={{
245+
from: [2.3522, 48.8566], // Paris
246+
to: [-0.1276, 51.5074], // London
247+
profile: "driving",
248+
engine: "OSRM",
249+
itineraryLineStyle: {
250+
color: "#10b981",
251+
width: 5,
252+
opacity: 0.9
253+
},
254+
itineraryLabel: <span>Route principale</span>,
255+
onRouteComputed: (route) => {
256+
console.log("Route computed:", route);
257+
}
248258
}}
249259
/>
250260
```
251261

252262
---
253263

254-
### Nearest Marker Search
264+
### Nearest Marker Search (`findNearestMarker`)
255265

256266
Find and highlight the closest marker to a given point within a maximum distance.
257267

258-
| Prop | Type | Description |
259-
|------|------|-------------|
260-
| `findNearestMarker.origin` | `[lng, lat]` | Starting point for search |
261-
| `findNearestMarker.destinations` | `Array<{lng, lat, id}>` | Candidate destinations |
262-
| `findNearestMarker.maxDistanceMeters` | `number` | Maximum search radius |
263-
| `onNearestFound` | `(id, coords, distance) => void` | Callback with nearest result |
268+
| Prop | Type | Default | Description |
269+
|------|------|---------|-------------|
270+
| `origin` | `[number, number]` | - | Starting point for search [lng, lat] |
271+
| `destinations` | `Array<{id, lng, lat}>` | - | Candidate destinations |
272+
| `maxDistanceMeters` | `number` | - | Maximum search radius in meters |
273+
| `profile` | `"driving" \| "walking" \| "cycling"` | `"driving"` | Routing profile for distance calculation |
274+
| `engine` | `"OSRM" \| "Mapbox"` | `"OSRM"` | Routing engine to use |
275+
| `onNearestFound` | `(results) => void` | - | Callback with all nearest results |
276+
| `initialNearestResults` | `NearestResult[]` | - | Precomputed nearest results |
277+
| `itineraryLineStyle` | `Partial<ItineraryLineStyle>` | - | Style override for auto-generated itinerary |
278+
279+
**NearestResult Type:**
280+
```tsx
281+
interface NearestResult {
282+
id: number | string;
283+
point: [number, number]; // [lng, lat]
284+
distance: number; // in meters
285+
routeFeature?: Feature<LineString> | null;
286+
}
287+
```
264288

265289
**Example:**
266290
```tsx
267291
<MapView
268292
findNearestMarker={{
269293
origin: [2.3522, 48.8566],
270294
destinations: markers.map(m => ({
295+
id: m.id,
271296
lng: m.lng,
272-
lat: m.lat,
273-
id: m.id
297+
lat: m.lat
274298
})),
275299
maxDistanceMeters: 5000,
276-
}}
277-
onNearestFound={(id, coords, distance) => {
278-
console.log(`Nearest: ${id} at ${distance}m`);
300+
profile: "walking",
301+
engine: "OSRM",
302+
onNearestFound: (results) => {
303+
console.log(`Found ${results.length} markers within range`);
304+
results.forEach(r => {
305+
console.log(`Marker ${r.id} at ${r.distance}m`);
306+
});
307+
}
279308
}}
280309
/>
281310
```
282311

283312
---
284313

285-
### Isochrone Props
314+
### Isochrone Props (`isochrone`)
286315

287316
Compute and display areas reachable within specific time intervals.
288317

289-
| Prop | Type | Description |
290-
|------|------|-------------|
291-
| `isochrone.origin` | `[lng, lat]` | Center point for isochrone |
292-
| `isochrone.profile` | `"driving" \| "walking" \| "cycling"` | Transportation mode |
293-
| `isochrone.intervals` | `number[]` | Time intervals in minutes |
294-
| `isochrone.onIsochroneLoaded` | `(data) => void` | Callback with GeoJSON result |
318+
| Prop | Type | Default | Description |
319+
|------|------|---------|-------------|
320+
| `origin` | `[number, number]` | - | Center point for isochrone [lng, lat] |
321+
| `profile` | `"driving" \| "walking" \| "cycling"` | `"driving"` | Transportation mode |
322+
| `intervals` | `number[]` | `[5, 10, 15]` | Time intervals in minutes |
323+
| `onIsochroneLoaded` | `(data) => void` | - | Callback with GeoJSON result |
295324

296325
**Example:**
297326
```tsx
@@ -309,7 +338,7 @@ Compute and display areas reachable within specific time intervals.
309338

310339
---
311340

312-
### GeoJSON Features
341+
### GeoJSON Features (`features`)
313342

314343
Display custom vector features like polygons, lines, or points.
315344

@@ -472,6 +501,44 @@ function InteractiveMap() {
472501
}
473502
```
474503

504+
### 🚗 Combined Routing & Nearest Search
505+
```tsx
506+
function DeliveryMap() {
507+
const [origin] = useState([2.3522, 48.8566]);
508+
const [destinations] = useState([
509+
{ id: 1, lng: 2.35, lat: 48.86 },
510+
{ id: 2, lng: 2.36, lat: 48.85 },
511+
{ id: 3, lng: 2.34, lat: 48.87 }
512+
]);
513+
514+
return (
515+
<MapView
516+
markers={destinations.map(d => ({
517+
id: d.id,
518+
lng: d.lng,
519+
lat: d.lat,
520+
Tooltip: <div>Destination {d.id}</div>
521+
}))}
522+
findNearestMarker={{
523+
origin,
524+
destinations,
525+
maxDistanceMeters: 10000,
526+
profile: "driving",
527+
engine: "OSRM",
528+
itineraryLineStyle: {
529+
color: "#22c55e",
530+
width: 4,
531+
opacity: 0.8
532+
},
533+
onNearestFound: (results) => {
534+
console.log("Nearest destinations:", results);
535+
}
536+
}}
537+
/>
538+
);
539+
}
540+
```
541+
475542
---
476543

477544
## 💡 Tips & Best Practices
@@ -482,20 +549,23 @@ function InteractiveMap() {
482549
- **Use `fitBoundsAnimationKey`** to control when bounds recalculate
483550
- **Disable animations** for large datasets: `disableAnimation={true}`
484551
- **Debounce dynamic updates** when tracking real-time data
552+
- **Use `initialRoute` and `initialNearestResults`** to avoid redundant API calls
485553

486554
### UX Improvements
487555

488556
- Combine `openPopupOnHover` and `disableAnimation` for smooth interactions
489557
- Use `fitBoundsPadding` to ensure markers aren't at screen edges
490558
- Set appropriate `popupMaxWidth` for mobile responsiveness
491559
- Provide visual feedback with custom `IconComponent` states
560+
- Use `itineraryLabel` to display route duration or distance
492561

493562
### Routing Best Practices
494563

495564
- **Use OSRM** (free) for basic routing needs
496565
- **Use Mapbox** for production apps requiring SLA and support
497-
- Cache route results to minimize API calls
498-
- Handle network errors gracefully
566+
- Cache route results with `initialRoute` to minimize API calls
567+
- Handle network errors gracefully with `onRouteComputed` callback
568+
- Combine `findNearestMarker` with `itineraryParams` for optimal routing workflows
499569

500570
---
501571

@@ -647,10 +717,17 @@ This will:
647717
- Reduce marker count or use clustering
648718
- Disable animations for large datasets
649719
- Memoize marker data
720+
- Use `initialRoute` and `initialNearestResults` for cached data
721+
722+
**Routing not working:**
723+
- Verify coordinates are in [lng, lat] format (not lat, lng)
724+
- Check that routing engine is accessible
725+
- Ensure profile matches your use case
726+
- Verify maxDistanceMeters is reasonable for nearest search
650727

651728
### Getting Help
652729

653-
- 📖 Check the [documentation](https://tracktor.github.io/map)
730+
- 📖 Check the [documentation](https://github.com/Tracktor/map)
654731
- 🐛 [Report bugs](https://github.com/tracktor-tech/tracktor-map/issues)
655732
- 💬 Join discussions in GitHub Discussions
656733

@@ -666,9 +743,10 @@ This will:
666743
## 🧭 Links
667744

668745
- 📦 **npm**: [@tracktor/map](https://www.npmjs.com/package/@tracktor/map)
669-
- 💻 **GitHub**: [tracktor-tech/tracktor-map](https://github.com/tracktor-tech/tracktor-map)
746+
- 💻 **GitHub**: [@tracktor/map](https://github.com/Tracktor/map)
670747
- 🌐 **Docs**: [tracktor.github.io/map](https://tracktor.github.io/map)
671748
- 🎨 **Design System**: [@tracktor/design-system](https://www.npmjs.com/package/@tracktor/design-system)
749+
- Sandbox Demo: [tracktor.github.io/map/sandbox](https://tracktor.github.io/map)
672750

673751
---
674752

sandbox/examples/IsochroneExample.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const IsochroneExample = () => {
7676
{/* 🗺️ MAP */}
7777
<Box sx={{ flex: 1 }}>
7878
<MapView
79+
key={`${cooperativeGestures}-${doubleClickZoom}-${projection.name}-${profile}-${intervals.join(",")}`}
7980
height="100%"
8081
width="100%"
8182
markers={allMarkers}

sandbox/examples/MarkersExample.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ const MarkersExample = () => {
112112
{/* 🗺️ Map */}
113113
<Box sx={{ flex: 1 }}>
114114
<MapView
115+
key={`${cooperativeGestures}-${doubleClickZoom}-${projection}-${projection}`}
115116
openPopup={openPopupId}
116117
markers={markers}
117118
height="100%"

0 commit comments

Comments
 (0)