@tracktor/map 1.6.4 β 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +723 -78
- package/dist/components/FeatureCollection/FeatureCollection.d.ts +10 -2
- package/dist/components/FeatureCollection/LineLabel.d.ts +7 -0
- package/dist/features/Itinerary/Itinerary.d.ts +10 -27
- package/dist/features/MapView/MapView.d.ts +1 -1
- package/dist/features/NearestPointItinerary/NearestPointItinary.d.ts +2 -9
- package/dist/main.js +473 -464
- package/dist/main.umd.cjs +4 -4
- package/dist/services/Mapbox/findNearestDestination.d.ts +2 -10
- package/dist/services/OSRM/findNearestDestination.d.ts +3 -11
- package/dist/services/core/fetchNearestInChunkFactory.d.ts +9 -5
- package/dist/services/core/interface.d.ts +1 -1
- package/dist/types/MapViewProps.d.ts +89 -156
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,114 +1,759 @@
|
|
|
1
|
-
#
|
|
1
|
+
# πΊοΈ @tracktor/map
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A modern, lightweight React map library built on top of Mapbox GL JS and react-map-gl. Designed for simplicity, flexibility, and visual elegance.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Easily combine markers, routes, GeoJSON features, isochrones, and nearest-point calculations β all with a declarative, type-safe API.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@tracktor/map)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
---
|
|
6
11
|
|
|
12
|
+
## π Installation
|
|
7
13
|
```bash
|
|
8
14
|
npm install @tracktor/map
|
|
9
15
|
```
|
|
10
16
|
|
|
11
17
|
or
|
|
12
|
-
|
|
13
18
|
```bash
|
|
14
19
|
yarn add @tracktor/map
|
|
15
20
|
```
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
or
|
|
23
|
+
```bash
|
|
24
|
+
bun add @tracktor/map
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## βοΈ Requirements
|
|
30
|
+
|
|
31
|
+
| Dependency | Version | Purpose |
|
|
32
|
+
|------------|---------|---------|
|
|
33
|
+
| `react` | 17+ / 18+ / 19+ | Core React runtime |
|
|
34
|
+
| `react-dom` | 17+ / 18+ / 19+ | React DOM rendering |
|
|
35
|
+
| `mapbox-gl` | β₯3.0.0 | Map rendering engine |
|
|
36
|
+
| `@tracktor/design-system` | β₯4.0.0 | UI theming and components |
|
|
37
|
+
| `@mui/icons-material` | * | Material UI icons |
|
|
38
|
+
| `@mui/x-license` | * | MUI X license integration |
|
|
18
39
|
|
|
19
|
-
|
|
40
|
+
πͺΆ **You'll also need a Mapbox access token** to render maps. Get one at [mapbox.com](https://account.mapbox.com/access-tokens/).
|
|
20
41
|
|
|
21
|
-
|
|
22
|
-
- `mapbox-gl` for map rendering
|
|
23
|
-
- React 19+
|
|
42
|
+
---
|
|
24
43
|
|
|
25
|
-
##
|
|
44
|
+
## β¨ Features
|
|
26
45
|
|
|
46
|
+
β
**Declarative API** β manage complex map interactions with simple props
|
|
47
|
+
β
**Markers & Popups** β customizable React components or image-based icons
|
|
48
|
+
β
**Routing & Isochrones** β visualize travel-time areas or compute optimal routes
|
|
49
|
+
β
**GeoJSON Layers** β render vector data dynamically
|
|
50
|
+
β
**Nearest Marker Search** β find and highlight closest points instantly
|
|
51
|
+
β
**Type-safe API** β full TypeScript support with smart IntelliSense
|
|
52
|
+
β
**Responsive Design** β automatically adapts to any container or screen size
|
|
53
|
+
β
**Built for performance** β minimal re-renders, efficient map updates
|
|
54
|
+
β
**Multiple Routing Engines** β supports both OSRM (free) and Mapbox Directions API
|
|
55
|
+
β
**Flexible Map Styles** β works with Mapbox styles, OpenStreetMap, or custom raster tiles
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## π§© Quick Start
|
|
27
60
|
```tsx
|
|
28
61
|
import { MapProvider, MarkerMap } from "@tracktor/map";
|
|
29
62
|
|
|
30
63
|
const markers = [
|
|
31
|
-
|
|
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
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
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
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## π§ Components Overview
|
|
108
|
+
|
|
109
|
+
### `MapProvider`
|
|
110
|
+
|
|
111
|
+
Wraps your map components and injects required providers (theme, tokens, MUI X license).
|
|
112
|
+
|
|
113
|
+
**Required Props:**
|
|
114
|
+
- `licenseMuiX` β Your MUI X license key
|
|
115
|
+
- `licenceMapbox` β Your Mapbox access token
|
|
116
|
+
```tsx
|
|
117
|
+
<MapProvider
|
|
118
|
+
licenseMuiX="your-license"
|
|
119
|
+
licenceMapbox="your-token"
|
|
120
|
+
>
|
|
121
|
+
{/* Your map components */}
|
|
122
|
+
</MapProvider>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `MapView` / `MarkerMap`
|
|
126
|
+
|
|
127
|
+
Main map component that handles:
|
|
128
|
+
- Marker rendering with custom icons or React components
|
|
129
|
+
- Interactive popups with hover/click modes
|
|
130
|
+
- Automatic bounds fitting
|
|
131
|
+
- Map click events
|
|
132
|
+
- Responsive container sizing
|
|
133
|
+
|
|
134
|
+
### Specialized Components
|
|
135
|
+
|
|
136
|
+
- **`RouteMap`** β Draw routes between two points using OSRM or Mapbox
|
|
137
|
+
- **`IsochroneMap`** β Compute and display travel-time polygons
|
|
138
|
+
- **`FeatureMap`** β Display custom GeoJSON layers with styling
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## π§± Props Reference
|
|
143
|
+
|
|
144
|
+
### `MapView` Props
|
|
145
|
+
|
|
146
|
+
#### Core Map Props
|
|
147
|
+
|
|
148
|
+
| Prop | Type | Default | Description |
|
|
149
|
+
|------|------|---------|-------------|
|
|
150
|
+
| `center` | `LngLatLike \| number[]` | `[2.3522, 48.8566]` | Initial map center coordinates [lng, lat] |
|
|
151
|
+
| `zoom` | `number` | `5` | Initial zoom level (0-22) |
|
|
152
|
+
| `width` | `string \| number` | `"100%"` | Map container width |
|
|
153
|
+
| `height` | `string \| number` | `300` | Map container height |
|
|
154
|
+
| `loading` | `boolean` | `false` | Show skeleton loader |
|
|
155
|
+
| `square` | `boolean` | `false` | Enforce 1:1 aspect ratio |
|
|
156
|
+
| `containerStyle` | `SxProps` | `undefined` | Custom MUI sx styles |
|
|
157
|
+
|
|
158
|
+
#### Map Appearance
|
|
159
|
+
|
|
160
|
+
| Prop | Type | Default | Description |
|
|
161
|
+
|------|------|---------|-------------|
|
|
162
|
+
| `theme` | `"light" \| "dark"` | `"light"` | Map color theme |
|
|
163
|
+
| `baseMapView` | `"street" \| "satellite"` | `"street"` | Base map layer type |
|
|
164
|
+
| `mapStyle` | `string` | - | Custom Mapbox style URL |
|
|
165
|
+
| `projection` | `ProjectionSpecification` | `"mercator"` | Map projection system |
|
|
166
|
+
|
|
167
|
+
#### Interaction Props
|
|
168
|
+
|
|
169
|
+
| Prop | Type | Default | Description |
|
|
170
|
+
|------|------|---------|-------------|
|
|
171
|
+
| `cooperativeGestures` | `boolean` | `true` | Require modifier key for zoom/pan |
|
|
172
|
+
| `doubleClickZoom` | `boolean` | `true` | Enable double-click to zoom |
|
|
173
|
+
| `onMapClick` | `(lng, lat, marker?) => void` | - | Callback for map clicks (includes clicked marker if applicable) |
|
|
174
|
+
|
|
175
|
+
#### Marker Props
|
|
176
|
+
|
|
177
|
+
| Prop | Type | Default | Description |
|
|
178
|
+
|------|------|---------|-------------|
|
|
179
|
+
| `markers` | `MarkerProps[]` | `[]` | Array of markers to display |
|
|
180
|
+
| `markerImageURL` | `string` | - | Custom marker icon URL |
|
|
181
|
+
| `openPopup` | `string \| number` | `undefined` | ID of marker with open popup |
|
|
182
|
+
| `openPopupOnHover` | `boolean` | `false` | Open popups on hover instead of click |
|
|
183
|
+
| `popupMaxWidth` | `string` | `"300px"` | Maximum popup width |
|
|
184
|
+
|
|
185
|
+
#### Bounds & Animation
|
|
186
|
+
|
|
187
|
+
| Prop | Type | Default | Description |
|
|
188
|
+
|------|------|---------|-------------|
|
|
189
|
+
| `fitBounds` | `boolean` | `true` | Auto-fit map to show all markers |
|
|
190
|
+
| `fitBoundsPadding` | `number` | `0` | Padding (px) around fitted bounds |
|
|
191
|
+
| `fitBoundDuration` | `number` | `500` | Animation duration (ms) |
|
|
192
|
+
| `disableAnimation` | `boolean` | `false` | Disable all animations |
|
|
193
|
+
| `fitBoundsAnimationKey` | `unknown` | - | Change to re-trigger fit bounds |
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### Marker Props (`MarkerProps`)
|
|
198
|
+
|
|
199
|
+
| Prop | Type | Required | Description |
|
|
200
|
+
|------|------|----------|-------------|
|
|
201
|
+
| `id` | `string \| number` | β
| Unique marker identifier |
|
|
202
|
+
| `lng` | `number` | β
| Longitude coordinate |
|
|
203
|
+
| `lat` | `number` | β
| Latitude coordinate |
|
|
204
|
+
| `Tooltip` | `ReactNode` | - | Content for popup/tooltip |
|
|
205
|
+
| `IconComponent` | `React.ComponentType` | - | Custom React icon component |
|
|
206
|
+
| `iconProps` | `object` | - | Props passed to IconComponent |
|
|
207
|
+
| `color` | `string` | - | Marker color (MUI palette) |
|
|
208
|
+
| `variant` | `string` | - | Marker style variant |
|
|
209
|
+
|
|
210
|
+
**Example with custom icon:**
|
|
211
|
+
```tsx
|
|
212
|
+
import LocationOnIcon from '@mui/icons-material/LocationOn';
|
|
213
|
+
|
|
214
|
+
const marker = {
|
|
215
|
+
id: 1,
|
|
216
|
+
lng: 2.3522,
|
|
217
|
+
lat: 48.8566,
|
|
218
|
+
IconComponent: LocationOnIcon,
|
|
219
|
+
iconProps: { fontSize: 'large', color: 'error' },
|
|
220
|
+
Tooltip: <div>Custom Icon Marker</div>
|
|
221
|
+
};
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### Itinerary Props (`itineraryParams`)
|
|
227
|
+
|
|
228
|
+
Draw a route between two points with customizable styling and routing engines.
|
|
229
|
+
|
|
230
|
+
| Prop | Type | Default | Description |
|
|
231
|
+
|------|------|---------|-------------|
|
|
232
|
+
| `from` | `[number, number]` | - | Route starting point [lng, lat] |
|
|
233
|
+
| `to` | `[number, number]` | - | Route ending point [lng, lat] |
|
|
234
|
+
| `profile` | `"driving" \| "walking" \| "cycling"` | `"driving"` | Transportation mode |
|
|
235
|
+
| `engine` | `"OSRM" \| "Mapbox"` | `"OSRM"` | Routing service to use |
|
|
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") |
|
|
240
|
+
|
|
241
|
+
**Example:**
|
|
242
|
+
```tsx
|
|
243
|
+
<MapView
|
|
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
|
+
}
|
|
258
|
+
}}
|
|
259
|
+
/>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### Nearest Marker Search (`findNearestMarker`)
|
|
265
|
+
|
|
266
|
+
Find and highlight the closest marker to a given point within a maximum distance.
|
|
267
|
+
|
|
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
|
+
```
|
|
288
|
+
|
|
289
|
+
**Example:**
|
|
290
|
+
```tsx
|
|
291
|
+
<MapView
|
|
292
|
+
findNearestMarker={{
|
|
293
|
+
origin: [2.3522, 48.8566],
|
|
294
|
+
destinations: markers.map(m => ({
|
|
295
|
+
id: m.id,
|
|
296
|
+
lng: m.lng,
|
|
297
|
+
lat: m.lat
|
|
298
|
+
})),
|
|
299
|
+
maxDistanceMeters: 5000,
|
|
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
|
+
}
|
|
308
|
+
}}
|
|
309
|
+
/>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
### Isochrone Props (`isochrone`)
|
|
315
|
+
|
|
316
|
+
Compute and display areas reachable within specific time intervals.
|
|
317
|
+
|
|
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 |
|
|
324
|
+
|
|
325
|
+
**Example:**
|
|
326
|
+
```tsx
|
|
327
|
+
<MapView
|
|
328
|
+
isochrone={{
|
|
329
|
+
origin: [2.3522, 48.8566],
|
|
330
|
+
profile: "driving",
|
|
331
|
+
intervals: [5, 10, 15, 20], // 5, 10, 15, 20 minutes
|
|
332
|
+
onIsochroneLoaded: (geojson) => {
|
|
333
|
+
console.log("Isochrone data:", geojson);
|
|
334
|
+
}
|
|
335
|
+
}}
|
|
336
|
+
/>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
### GeoJSON Features (`features`)
|
|
342
|
+
|
|
343
|
+
Display custom vector features like polygons, lines, or points.
|
|
344
|
+
|
|
345
|
+
| Prop | Type | Description |
|
|
346
|
+
|------|------|-------------|
|
|
347
|
+
| `features` | `Feature \| Feature[] \| FeatureCollection` | GeoJSON data to render |
|
|
348
|
+
|
|
349
|
+
**Example:**
|
|
350
|
+
```tsx
|
|
351
|
+
<MapView
|
|
352
|
+
features={{
|
|
353
|
+
type: "Feature",
|
|
354
|
+
geometry: {
|
|
355
|
+
type: "LineString",
|
|
356
|
+
coordinates: [
|
|
357
|
+
[2.3, 48.8],
|
|
358
|
+
[2.4, 48.9],
|
|
359
|
+
[2.5, 48.85]
|
|
360
|
+
]
|
|
361
|
+
},
|
|
362
|
+
properties: {
|
|
363
|
+
color: "#ef4444"
|
|
364
|
+
}
|
|
365
|
+
}}
|
|
366
|
+
/>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## π§ Advanced Use Cases
|
|
372
|
+
|
|
373
|
+
### π§ Real-time GPS Tracking
|
|
374
|
+
```tsx
|
|
375
|
+
function LiveTracking() {
|
|
376
|
+
const [position, setPosition] = useState([2.3522, 48.8566]);
|
|
377
|
+
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
const watchId = navigator.geolocation.watchPosition((pos) => {
|
|
380
|
+
setPosition([pos.coords.longitude, pos.coords.latitude]);
|
|
381
|
+
});
|
|
382
|
+
return () => navigator.geolocation.clearWatch(watchId);
|
|
383
|
+
}, []);
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<MapView
|
|
387
|
+
markers={[{
|
|
388
|
+
id: 'current',
|
|
389
|
+
lng: position[0],
|
|
390
|
+
lat: position[1],
|
|
391
|
+
Tooltip: <div>You are here</div>
|
|
392
|
+
}]}
|
|
393
|
+
center={position}
|
|
394
|
+
zoom={15}
|
|
395
|
+
fitBounds={false}
|
|
396
|
+
/>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### π Dynamic Data with React Query
|
|
402
|
+
```tsx
|
|
403
|
+
import { useQuery } from '@tanstack/react-query';
|
|
404
|
+
|
|
405
|
+
function DynamicMarkers() {
|
|
406
|
+
const { data: markers } = useQuery({
|
|
407
|
+
queryKey: ['locations'],
|
|
408
|
+
queryFn: fetchLocations,
|
|
409
|
+
refetchInterval: 5000 // Refresh every 5s
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
<MapView
|
|
414
|
+
markers={markers}
|
|
415
|
+
fitBounds
|
|
416
|
+
fitBoundsAnimationKey={markers?.length}
|
|
417
|
+
/>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### π¨ Custom Marker Components
|
|
423
|
+
```tsx
|
|
424
|
+
function CustomMarker({ isActive, count }) {
|
|
425
|
+
return (
|
|
426
|
+
<Box
|
|
427
|
+
sx={{
|
|
428
|
+
width: 40,
|
|
429
|
+
height: 40,
|
|
430
|
+
borderRadius: '50%',
|
|
431
|
+
bgcolor: isActive ? 'success.main' : 'grey.500',
|
|
432
|
+
display: 'flex',
|
|
433
|
+
alignItems: 'center',
|
|
434
|
+
justifyContent: 'center',
|
|
435
|
+
color: 'white',
|
|
436
|
+
fontWeight: 'bold',
|
|
437
|
+
border: '2px solid white',
|
|
438
|
+
boxShadow: 2
|
|
439
|
+
}}
|
|
440
|
+
>
|
|
441
|
+
{count}
|
|
442
|
+
</Box>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
<MapView
|
|
447
|
+
markers={[{
|
|
32
448
|
id: 1,
|
|
33
449
|
lng: 2.3522,
|
|
34
450
|
lat: 48.8566,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
451
|
+
IconComponent: CustomMarker,
|
|
452
|
+
iconProps: { isActive: true, count: 5 }
|
|
453
|
+
}]}
|
|
454
|
+
/>
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### πΊοΈ Multi-Route Comparison
|
|
458
|
+
```tsx
|
|
459
|
+
<MapView
|
|
460
|
+
features={[
|
|
461
|
+
{
|
|
462
|
+
type: "Feature",
|
|
463
|
+
geometry: {
|
|
464
|
+
type: "LineString",
|
|
465
|
+
coordinates: route1
|
|
466
|
+
},
|
|
467
|
+
properties: { color: "#3b82f6" }
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
type: "Feature",
|
|
471
|
+
geometry: {
|
|
472
|
+
type: "LineString",
|
|
473
|
+
coordinates: route2
|
|
474
|
+
},
|
|
475
|
+
properties: { color: "#ef4444" }
|
|
476
|
+
}
|
|
477
|
+
]}
|
|
478
|
+
/>
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### π― Click-to-Add Markers
|
|
482
|
+
```tsx
|
|
483
|
+
function InteractiveMap() {
|
|
484
|
+
const [markers, setMarkers] = useState([]);
|
|
41
485
|
|
|
42
|
-
|
|
43
|
-
|
|
486
|
+
const handleMapClick = (lng, lat) => {
|
|
487
|
+
setMarkers(prev => [...prev, {
|
|
488
|
+
id: Date.now(),
|
|
489
|
+
lng,
|
|
490
|
+
lat,
|
|
491
|
+
Tooltip: <div>Point {markers.length + 1}</div>
|
|
492
|
+
}]);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
return (
|
|
496
|
+
<MapView
|
|
44
497
|
markers={markers}
|
|
45
|
-
|
|
46
|
-
zoom={10}
|
|
47
|
-
fitBounds
|
|
48
|
-
height={400}
|
|
49
|
-
width="100%"
|
|
498
|
+
onMapClick={handleMapClick}
|
|
50
499
|
/>
|
|
51
|
-
|
|
500
|
+
);
|
|
501
|
+
}
|
|
52
502
|
```
|
|
53
503
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
| `fitBounds` | `boolean` | Automatically fit the map to the bounds of all markers |
|
|
64
|
-
| `fitBoundsPadding` | `number` | Padding around markers when fitting bounds |
|
|
65
|
-
| `zoomFlyFrom` | `number` | Zoom level to fly in from |
|
|
66
|
-
| `popupMaxWidth` | `string` | Max width for popups |
|
|
67
|
-
| `width` | `number` or `string` | Map width |
|
|
68
|
-
| `height` | `number` or `string` | Map height |
|
|
69
|
-
| `loading` | `boolean` | Whether to show a loading state |
|
|
70
|
-
| `markerImageURL` | `string` | Default marker image URL |
|
|
71
|
-
| `containerStyle` | `SxProps` | Custom styles for the map container |
|
|
72
|
-
| `disableFlyTo` | `boolean` | Disable flyTo animation on marker click |
|
|
73
|
-
| `flyToDuration` | `number` | Duration of the flyTo animation |
|
|
74
|
-
| `fitBoundDuration` | `number` | Duration of the fitBounds animation |
|
|
75
|
-
| `square` | `boolean` | Forces the map to be square-shaped |
|
|
76
|
-
| `openPopup` | `number` or `string` | ID of the marker with an open popup |
|
|
77
|
-
| `openPopupOnHover` | `boolean` | Automatically open popups on marker hover |
|
|
78
|
-
| `onMapClick` | `(lng: number, lat: number) => void` | Callback triggered when clicking on the map |
|
|
79
|
-
|
|
80
|
-
### MarkerProps
|
|
81
|
-
|
|
82
|
-
| Prop | Type | Description |
|
|
83
|
-
|-------------|----------------------|---------------------------------------------------------------|
|
|
84
|
-
| `id` | `number` or `string` | Unique marker identifier |
|
|
85
|
-
| `lng` | `number` | Longitude |
|
|
86
|
-
| `lat` | `number` | Latitude |
|
|
87
|
-
| `Tooltip` | `ReactNode` | Tooltip content displayed in a popup |
|
|
88
|
-
| `iconImage` | `string` | Image URL used as marker icon |
|
|
89
|
-
| `size` | `number` | Marker size in pixels |
|
|
90
|
-
| `zIndex` | `number` | Z-index to control stacking order |
|
|
91
|
-
| `onClick` | `() => void` | Function triggered on marker click |
|
|
92
|
-
| `type` | `string` | Custom marker type (optional, for filtering or styling) |
|
|
93
|
-
| `name` | `string` | Name of the marker |
|
|
94
|
-
| `Icon` | `ReactNode` | Custom React component to render instead of default image |
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
## π§βπ» Contributing
|
|
98
|
-
|
|
99
|
-
Contributions are welcome! Please follow the coding conventions and include tests when necessary.
|
|
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
|
+
]);
|
|
100
513
|
|
|
101
|
-
|
|
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
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## π‘ Tips & Best Practices
|
|
545
|
+
|
|
546
|
+
### Performance Optimization
|
|
547
|
+
|
|
548
|
+
- **Memoize marker data** to prevent unnecessary re-renders
|
|
549
|
+
- **Use `fitBoundsAnimationKey`** to control when bounds recalculate
|
|
550
|
+
- **Disable animations** for large datasets: `disableAnimation={true}`
|
|
551
|
+
- **Debounce dynamic updates** when tracking real-time data
|
|
552
|
+
- **Use `initialRoute` and `initialNearestResults`** to avoid redundant API calls
|
|
553
|
+
|
|
554
|
+
### UX Improvements
|
|
555
|
+
|
|
556
|
+
- Combine `openPopupOnHover` and `disableAnimation` for smooth interactions
|
|
557
|
+
- Use `fitBoundsPadding` to ensure markers aren't at screen edges
|
|
558
|
+
- Set appropriate `popupMaxWidth` for mobile responsiveness
|
|
559
|
+
- Provide visual feedback with custom `IconComponent` states
|
|
560
|
+
- Use `itineraryLabel` to display route duration or distance
|
|
561
|
+
|
|
562
|
+
### Routing Best Practices
|
|
563
|
+
|
|
564
|
+
- **Use OSRM** (free) for basic routing needs
|
|
565
|
+
- **Use Mapbox** for production apps requiring SLA and support
|
|
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
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## π§βπ» Development
|
|
573
|
+
|
|
574
|
+
### Prerequisites
|
|
575
|
+
|
|
576
|
+
- **Bun** β₯1.1.0 (recommended) or Node.js 18+
|
|
577
|
+
- Git
|
|
578
|
+
|
|
579
|
+
### Setup
|
|
580
|
+
```bash
|
|
581
|
+
# Clone the repository
|
|
582
|
+
git clone https://github.com/tracktor-tech/tracktor-map.git
|
|
583
|
+
cd tracktor-map
|
|
584
|
+
|
|
585
|
+
# Install dependencies
|
|
586
|
+
bun install
|
|
587
|
+
|
|
588
|
+
# Start development sandbox
|
|
589
|
+
bun run sandbox
|
|
590
|
+
# or
|
|
591
|
+
bun run dev:sandbox
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Available Scripts
|
|
595
|
+
|
|
596
|
+
| Command | Description |
|
|
597
|
+
|---------|-------------|
|
|
598
|
+
| `bun run sandbox` | Start interactive development playground |
|
|
599
|
+
| `bun run build` | Build library for production |
|
|
600
|
+
| `bun run build:sandbox` | Build sandbox demo site |
|
|
601
|
+
| `bun run deploy:sandbox` | Deploy sandbox to GitHub Pages |
|
|
602
|
+
| `bun run lint` | Check code quality and types |
|
|
603
|
+
| `bun run lint:fix` | Auto-fix linting issues |
|
|
604
|
+
| `bun run test` | Run test suite |
|
|
605
|
+
| `bun run test:watch` | Run tests in watch mode |
|
|
606
|
+
| `bun run version` | Bump version with changelog |
|
|
607
|
+
| `bun run release` | Build and publish to npm |
|
|
608
|
+
|
|
609
|
+
### Project Structure
|
|
610
|
+
```
|
|
611
|
+
@tracktor/map/
|
|
612
|
+
βββ src/
|
|
613
|
+
β βββ components/ # Reusable map and UI components (Marker, Popup, etc.)
|
|
614
|
+
β βββ constants/ # Shared configuration values and styling constants
|
|
615
|
+
β βββ context/ # React context providers (e.g. map state)
|
|
616
|
+
β βββ features/ # Core map features (routes, isochrones, nearest, etc.)
|
|
617
|
+
β βββ services/ # External APIs and utility services
|
|
618
|
+
β βββ types/ # TypeScript interfaces and types
|
|
619
|
+
β βββ utils/ # Generic helpers and formatting functions
|
|
620
|
+
β βββ main.ts # Library entry point
|
|
621
|
+
β
|
|
622
|
+
βββ sandbox/ # Development playground (example app & live demos)
|
|
623
|
+
β βββ context/ # Demo context providers
|
|
624
|
+
β βββ examples/ # Interactive usage examples
|
|
625
|
+
β βββ features/ # Components used in the docs/demo
|
|
626
|
+
β βββ public/ # Static assets (images, previews, etc.)
|
|
627
|
+
β βββ App.tsx # Sandbox root component
|
|
628
|
+
β βββ index.tsx # Sandbox entry file
|
|
629
|
+
β
|
|
630
|
+
βββ test/ # Unit and integration tests
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Testing
|
|
634
|
+
```bash
|
|
635
|
+
# Run all tests
|
|
636
|
+
bun test
|
|
637
|
+
|
|
638
|
+
# Watch mode
|
|
639
|
+
bun test:watch
|
|
640
|
+
|
|
641
|
+
# Run specific test file
|
|
642
|
+
bun test src/components/MapView.test.tsx
|
|
643
|
+
```
|
|
102
644
|
|
|
103
|
-
|
|
645
|
+
### Contributing
|
|
104
646
|
|
|
647
|
+
We welcome contributions! Please:
|
|
105
648
|
|
|
106
|
-
|
|
107
|
-
|
|
649
|
+
1. Fork the repository
|
|
650
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
651
|
+
3. Make your changes
|
|
652
|
+
4. Run tests and linting (`bun run test && bun run lint`)
|
|
653
|
+
5. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
654
|
+
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
655
|
+
7. Open a Pull Request
|
|
656
|
+
|
|
657
|
+
**Code Style:**
|
|
658
|
+
- Follow existing patterns and conventions
|
|
659
|
+
- Use TypeScript for all new code
|
|
660
|
+
- Add tests for new features
|
|
661
|
+
- Update documentation as needed
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## π Documentation & Examples
|
|
666
|
+
|
|
667
|
+
Explore interactive examples and comprehensive API documentation:
|
|
668
|
+
|
|
669
|
+
π **[Live Documentation & Sandbox](https://tracktor.github.io/map)**
|
|
670
|
+
|
|
671
|
+
The sandbox includes:
|
|
672
|
+
- Interactive code examples
|
|
673
|
+
- Live preview of all features
|
|
674
|
+
- Copy-paste ready snippets
|
|
675
|
+
- API reference with search
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
## π¦ Publishing & Deployment
|
|
680
|
+
|
|
681
|
+
### Publish to npm
|
|
682
|
+
```bash
|
|
683
|
+
# Update version and generate changelog
|
|
684
|
+
bun run version
|
|
108
685
|
|
|
686
|
+
# Build and publish
|
|
687
|
+
bun run release
|
|
109
688
|
```
|
|
110
|
-
|
|
689
|
+
|
|
690
|
+
### Deploy Sandbox to GitHub Pages
|
|
691
|
+
```bash
|
|
692
|
+
bun run deploy:sandbox
|
|
111
693
|
```
|
|
112
|
-
This will build the documentation and push it to the `gh-pages` branch.
|
|
113
694
|
|
|
114
|
-
|
|
695
|
+
This will:
|
|
696
|
+
1. Build the sandbox with production optimizations
|
|
697
|
+
2. Generate a 404.html for client-side routing
|
|
698
|
+
3. Push to the `gh-pages` branch
|
|
699
|
+
4. Update the live documentation site
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
## π§ Troubleshooting
|
|
704
|
+
|
|
705
|
+
### Common Issues
|
|
706
|
+
|
|
707
|
+
**Map not rendering:**
|
|
708
|
+
- Verify your Mapbox token is valid
|
|
709
|
+
- Check browser console for errors
|
|
710
|
+
- Ensure mapbox-gl CSS is imported
|
|
711
|
+
|
|
712
|
+
**TypeScript errors:**
|
|
713
|
+
- Run `bun install` to update type definitions
|
|
714
|
+
- Check peer dependency versions match
|
|
715
|
+
|
|
716
|
+
**Performance issues:**
|
|
717
|
+
- Reduce marker count or use clustering
|
|
718
|
+
- Disable animations for large datasets
|
|
719
|
+
- 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
|
|
727
|
+
|
|
728
|
+
### Getting Help
|
|
729
|
+
|
|
730
|
+
- π Check the [documentation](https://github.com/Tracktor/map)
|
|
731
|
+
- π [Report bugs](https://github.com/tracktor-tech/tracktor-map/issues)
|
|
732
|
+
- π¬ Join discussions in GitHub Discussions
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## π License
|
|
737
|
+
|
|
738
|
+
**UNLICENSED** β This package is proprietary software.
|
|
739
|
+
Β© [Tracktor β Kevin Graff]
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
## π§ Links
|
|
744
|
+
|
|
745
|
+
- π¦ **npm**: [@tracktor/map](https://www.npmjs.com/package/@tracktor/map)
|
|
746
|
+
- π» **GitHub**: [@tracktor/map](https://github.com/Tracktor/map)
|
|
747
|
+
- π **Docs**: [tracktor.github.io/map](https://tracktor.github.io/map)
|
|
748
|
+
- π¨ **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)
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
## π Acknowledgments
|
|
754
|
+
|
|
755
|
+
Built with:
|
|
756
|
+
- [Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/) β Powerful map rendering
|
|
757
|
+
- [react-map-gl](https://visgl.github.io/react-map-gl/) β React wrapper for Mapbox
|
|
758
|
+
- [OSRM](http://project-osrm.org/) β Free routing engine
|
|
759
|
+
- [@tracktor/design-system](https://www.npmjs.com/package/@tracktor/design-system) β UI components
|