@navigoo/map-components 1.0.8 → 1.1.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/package.json +1 -1
- package/src/components/MapView.tsx +45 -175
package/package.json
CHANGED
|
@@ -24,17 +24,16 @@ const MapView: React.FC<MapViewProps> = ({
|
|
|
24
24
|
}) => {
|
|
25
25
|
const mapRef = useRef<L.Map | null>(null);
|
|
26
26
|
const mapContainerRef = useRef<HTMLDivElement>(null);
|
|
27
|
-
const markerRef = useRef<L.Marker | null>(null);
|
|
28
27
|
const routeLayerRef = useRef<L.LayerGroup | null>(null);
|
|
29
|
-
const clickMarkerRef = useRef<L.Marker | null>(null);
|
|
30
28
|
const routePolylinesRef = useRef<L.Polyline[]>([]);
|
|
31
29
|
|
|
32
|
-
//
|
|
30
|
+
// ✅ Bornes élargies du Cameroun
|
|
33
31
|
const CAMEROON_BOUNDS = L.latLngBounds(
|
|
34
|
-
[1.
|
|
35
|
-
[13.
|
|
32
|
+
[1.0, 7.8], // Sud-Ouest
|
|
33
|
+
[13.5, 16.5] // Nord-Est
|
|
36
34
|
);
|
|
37
35
|
|
|
36
|
+
// 🔍 Fonction pour parser les géométries WKT
|
|
38
37
|
const parseWKTLineString = (wkt: string): [number, number][] => {
|
|
39
38
|
try {
|
|
40
39
|
const geo = parse(wkt);
|
|
@@ -42,7 +41,7 @@ const MapView: React.FC<MapViewProps> = ({
|
|
|
42
41
|
return geo.coordinates.map(([lng, lat]: [number, number]) => [lat, lng] as [number, number]);
|
|
43
42
|
}
|
|
44
43
|
} catch (error) {
|
|
45
|
-
console.error('
|
|
44
|
+
console.error('Erreur de parsing WKT:', error);
|
|
46
45
|
}
|
|
47
46
|
const match = wkt.match(/LINESTRING\s*\(([^)]+)\)/);
|
|
48
47
|
if (match) {
|
|
@@ -56,31 +55,27 @@ const MapView: React.FC<MapViewProps> = ({
|
|
|
56
55
|
return [];
|
|
57
56
|
};
|
|
58
57
|
|
|
58
|
+
// 🌍 Initialisation de la carte
|
|
59
59
|
useEffect(() => {
|
|
60
60
|
if (mapContainerRef.current && !mapRef.current) {
|
|
61
61
|
const maxZoom = 18;
|
|
62
|
-
|
|
63
|
-
// Configuration de la carte avec limites strictes
|
|
62
|
+
|
|
64
63
|
mapRef.current = L.map(mapContainerRef.current, {
|
|
65
|
-
center: [
|
|
66
|
-
zoom: 6,
|
|
67
|
-
minZoom:
|
|
68
|
-
maxZoom
|
|
64
|
+
center: [7.3697, 12.3547], // Centre du Cameroun
|
|
65
|
+
zoom: 6,
|
|
66
|
+
minZoom: 5,
|
|
67
|
+
maxZoom,
|
|
69
68
|
maxBounds: CAMEROON_BOUNDS,
|
|
70
|
-
maxBoundsViscosity:
|
|
69
|
+
maxBoundsViscosity: 0.4, // ✅ Souple — permet un léger glissement
|
|
71
70
|
});
|
|
72
71
|
|
|
73
|
-
//
|
|
74
|
-
mapRef.current.setMaxBounds(CAMEROON_BOUNDS);
|
|
75
|
-
|
|
72
|
+
// 🗺 Couche OpenStreetMap
|
|
76
73
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
77
|
-
attribution: '©
|
|
78
|
-
maxZoom
|
|
79
|
-
tileSize: 256,
|
|
80
|
-
zoomOffset: 0,
|
|
81
|
-
bounds: CAMEROON_BOUNDS, // Limiter les tuiles aux bounds du Cameroun
|
|
74
|
+
attribution: '© OpenStreetMap contributors',
|
|
75
|
+
maxZoom,
|
|
82
76
|
}).addTo(mapRef.current);
|
|
83
77
|
|
|
78
|
+
// 🧭 Icônes par défaut
|
|
84
79
|
L.Icon.Default.mergeOptions({
|
|
85
80
|
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
|
|
86
81
|
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
|
|
@@ -89,188 +84,63 @@ const MapView: React.FC<MapViewProps> = ({
|
|
|
89
84
|
|
|
90
85
|
routeLayerRef.current = L.layerGroup().addTo(mapRef.current);
|
|
91
86
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!CAMEROON_BOUNDS.contains(currentCenter)) {
|
|
99
|
-
mapRef.current.panInsideBounds(CAMEROON_BOUNDS, { animate: true });
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
mapRef.current.on('click', async (e: L.LeafletMouseEvent) => {
|
|
105
|
-
const { lat, lng } = e.latlng;
|
|
106
|
-
|
|
107
|
-
// Vérifier que le clic est dans les limites du Cameroun
|
|
108
|
-
if (!CAMEROON_BOUNDS.contains([lat, lng])) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const closestPlace = await apiClient.findClosestPlace(lat, lng);
|
|
113
|
-
const placeName = closestPlace?.name || 'Position sélectionnée';
|
|
114
|
-
|
|
115
|
-
if (clickMarkerRef.current) {
|
|
116
|
-
mapRef.current?.removeLayer(clickMarkerRef.current);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
clickMarkerRef.current = L.marker([lat, lng])
|
|
120
|
-
.addTo(mapRef.current!)
|
|
121
|
-
.bindPopup(`<b>${placeName}</b><br>Lat: ${lat.toFixed(6)}<br>Lng: ${lng.toFixed(6)}`)
|
|
122
|
-
.openPopup();
|
|
123
|
-
});
|
|
87
|
+
// ✅ Recentrage automatique après affichage
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
mapRef.current?.invalidateSize();
|
|
90
|
+
mapRef.current?.fitBounds(CAMEROON_BOUNDS, { animate: true, padding: [20, 20] });
|
|
91
|
+
}, 300);
|
|
124
92
|
}
|
|
125
93
|
|
|
126
94
|
return () => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
mapRef.current = null;
|
|
130
|
-
}
|
|
95
|
+
mapRef.current?.remove();
|
|
96
|
+
mapRef.current = null;
|
|
131
97
|
};
|
|
132
98
|
}, [apiClient]);
|
|
133
99
|
|
|
100
|
+
// 🚗 Affichage des itinéraires
|
|
134
101
|
useEffect(() => {
|
|
135
102
|
if (!mapRef.current) return;
|
|
136
103
|
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
routeLayerRef.current.clearLayers();
|
|
140
|
-
}
|
|
141
|
-
if (markerRef.current) {
|
|
142
|
-
markerRef.current.remove();
|
|
143
|
-
markerRef.current = null;
|
|
144
|
-
}
|
|
145
|
-
if (clickMarkerRef.current) {
|
|
146
|
-
clickMarkerRef.current.remove();
|
|
147
|
-
clickMarkerRef.current = null;
|
|
148
|
-
}
|
|
149
|
-
|
|
104
|
+
// Nettoyage avant rendu
|
|
105
|
+
routeLayerRef.current?.clearLayers();
|
|
150
106
|
routePolylinesRef.current = [];
|
|
151
107
|
|
|
152
|
-
// Fonction pour centrer la carte sur un point avec un marqueur
|
|
153
|
-
const centerOnPoint = async (lat: number, lng: number, placeName: string, zoom: number = 16) => {
|
|
154
|
-
// Vérifier que le point est dans les limites du Cameroun
|
|
155
|
-
if (!CAMEROON_BOUNDS.contains([lat, lng])) {
|
|
156
|
-
console.warn('Le point est en dehors des limites du Cameroun');
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
let displayName = placeName;
|
|
161
|
-
if (placeName === 'Votre position') {
|
|
162
|
-
const closestPlace = await apiClient.findClosestPlace(lat, lng);
|
|
163
|
-
displayName = closestPlace?.name || placeName;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Supprimer l'ancien marqueur s'il existe
|
|
167
|
-
if (markerRef.current) {
|
|
168
|
-
markerRef.current.remove();
|
|
169
|
-
markerRef.current = null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
mapRef.current!.setView([lat, lng], zoom, { animate: true });
|
|
173
|
-
markerRef.current = L.marker([lat, lng])
|
|
174
|
-
.addTo(mapRef.current!)
|
|
175
|
-
.bindPopup(`<b>${displayName}</b><br>Lat: ${lat.toFixed(6)}<br>Lng: ${lng.toFixed(6)}`)
|
|
176
|
-
.openPopup();
|
|
177
|
-
};
|
|
178
|
-
|
|
179
108
|
if (routes && routes.length > 0) {
|
|
180
|
-
// Gérer les itinéraires
|
|
181
109
|
let allCoordinates: [number, number][] = [];
|
|
110
|
+
|
|
182
111
|
routes.forEach((route, index) => {
|
|
183
112
|
const coordinates: [number, number][] = [];
|
|
184
|
-
route.steps.forEach(
|
|
113
|
+
route.steps.forEach(step => {
|
|
185
114
|
const latLngs = parseWKTLineString(step.geometry);
|
|
186
115
|
if (latLngs.length > 0) coordinates.push(...latLngs);
|
|
187
116
|
});
|
|
188
117
|
|
|
189
118
|
if (coordinates.length > 0) {
|
|
190
119
|
const color = index === selectedRouteIndex ? 'green' : 'black';
|
|
191
|
-
const
|
|
192
|
-
const opacity = index === selectedRouteIndex ? 1.0 : 0.5;
|
|
193
|
-
|
|
194
|
-
const polyline = L.polyline(coordinates, { color, weight, opacity })
|
|
120
|
+
const polyline = L.polyline(coordinates, { color, weight: 4, opacity: 0.8 })
|
|
195
121
|
.addTo(routeLayerRef.current!)
|
|
196
|
-
.on('click', (
|
|
197
|
-
L.DomEvent.stopPropagation(e);
|
|
198
|
-
setSelectedRouteIndex(index);
|
|
199
|
-
routePolylinesRef.current.forEach((pl, i) => {
|
|
200
|
-
pl.setStyle({
|
|
201
|
-
color: i === index ? 'green' : 'black',
|
|
202
|
-
weight: i === index ? 5 : 3,
|
|
203
|
-
opacity: i === index ? 1.0 : 0.5,
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
const bounds = polyline.getBounds();
|
|
207
|
-
const center = bounds.getCenter();
|
|
208
|
-
L.popup()
|
|
209
|
-
.setLatLng(center)
|
|
210
|
-
.setContent(`
|
|
211
|
-
<b>Route ${index + 1}</b><br>
|
|
212
|
-
Distance: ${route.distance.toFixed(2)} m<br>
|
|
213
|
-
Durée: ${(route.duration / 60).toFixed(2)} min<br>
|
|
214
|
-
Départ: ${route.startPlaceName || 'Départ'}<br>
|
|
215
|
-
Destination: ${route.endPlaceName || 'Destination'}
|
|
216
|
-
`)
|
|
217
|
-
.openOn(mapRef.current!);
|
|
218
|
-
});
|
|
219
|
-
|
|
122
|
+
.on('click', () => setSelectedRouteIndex(index));
|
|
220
123
|
routePolylinesRef.current.push(polyline);
|
|
221
|
-
allCoordinates
|
|
222
|
-
|
|
223
|
-
if (index === selectedRouteIndex) {
|
|
224
|
-
const startPoint = coordinates[0];
|
|
225
|
-
const endPoint = coordinates[coordinates.length - 1];
|
|
226
|
-
(async () => {
|
|
227
|
-
let startPlaceName = route.startPlaceName || 'Départ';
|
|
228
|
-
if (route.startPlaceName === 'Votre position') {
|
|
229
|
-
const closestStartPlace = await apiClient.findClosestPlace(startPoint[1], startPoint[0]);
|
|
230
|
-
startPlaceName = closestStartPlace?.name || route.startPlaceName;
|
|
231
|
-
}
|
|
232
|
-
L.marker(startPoint).addTo(routeLayerRef.current!).bindPopup(`
|
|
233
|
-
<b>${startPlaceName}</b><br>Lat: ${startPoint[0].toFixed(6)}<br>Lng: ${startPoint[1].toFixed(6)}
|
|
234
|
-
`);
|
|
235
|
-
L.marker(endPoint).addTo(routeLayerRef.current!).bindPopup(`
|
|
236
|
-
<b>${route.endPlaceName || 'Destination'}</b><br>Lat: ${endPoint[0].toFixed(6)}<br>Lng: ${endPoint[1].toFixed(6)}
|
|
237
|
-
`);
|
|
238
|
-
})();
|
|
239
|
-
}
|
|
124
|
+
allCoordinates.push(...coordinates);
|
|
240
125
|
}
|
|
241
126
|
});
|
|
242
127
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// S'assurer que les bounds ajustées sont dans les limites du Cameroun
|
|
249
|
-
const finalBounds = L.latLngBounds(
|
|
250
|
-
[
|
|
251
|
-
Math.max(paddedBounds.getSouth(), CAMEROON_BOUNDS.getSouth()),
|
|
252
|
-
Math.max(paddedBounds.getWest(), CAMEROON_BOUNDS.getWest())
|
|
253
|
-
],
|
|
254
|
-
[
|
|
255
|
-
Math.min(paddedBounds.getNorth(), CAMEROON_BOUNDS.getNorth()),
|
|
256
|
-
Math.min(paddedBounds.getEast(), CAMEROON_BOUNDS.getEast())
|
|
257
|
-
]
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
mapRef.current!.fitBounds(finalBounds);
|
|
261
|
-
}
|
|
262
|
-
} else if (searchedPlace && searchedPlace.coordinates) {
|
|
263
|
-
centerOnPoint(searchedPlace.coordinates.lat, searchedPlace.coordinates.lng, searchedPlace.name);
|
|
264
|
-
} else if (userLocation) {
|
|
265
|
-
centerOnPoint(userLocation.latitude, userLocation.longitude, 'Votre position');
|
|
128
|
+
// 🗺 Ajustement intelligent de la vue sans sortir du Cameroun
|
|
129
|
+
const routeBounds = L.latLngBounds(allCoordinates);
|
|
130
|
+
const mergedBounds = routeBounds.extend(CAMEROON_BOUNDS);
|
|
131
|
+
mapRef.current.fitBounds(mergedBounds, { padding: [30, 30] });
|
|
266
132
|
} else {
|
|
267
|
-
//
|
|
268
|
-
mapRef.current
|
|
269
|
-
mapRef.current!.setMaxBounds(CAMEROON_BOUNDS);
|
|
133
|
+
// 🌍 Recentrage sur le Cameroun si aucun itinéraire
|
|
134
|
+
mapRef.current.fitBounds(CAMEROON_BOUNDS, { animate: true });
|
|
270
135
|
}
|
|
271
|
-
}, [
|
|
136
|
+
}, [routes, selectedRouteIndex]);
|
|
272
137
|
|
|
273
|
-
return
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
ref={mapContainerRef}
|
|
141
|
+
className="w-full h-screen rounded-xl overflow-hidden shadow-md"
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
274
144
|
};
|
|
275
145
|
|
|
276
|
-
export default
|
|
146
|
+
export default MapView;
|