Files
projet_gestion_commande/frontend-admin/src/components/TomTomMap.tsx
T
2026-02-21 15:51:56 +01:00

371 lines
12 KiB
TypeScript

import React, {
useRef,
useEffect,
useCallback,
forwardRef,
useImperativeHandle,
} from "react";
import { StyleSheet } from "react-native";
import { WebView } from "react-native-webview";
import { geocodeAddress, calculateRoute } from "../api/tomtom";
const TOMTOM_API_KEY = "MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB";
export interface TomTomMarker {
id: string;
latitude: number;
longitude: number;
color: string;
label?: string;
description?: string;
isSelected?: boolean;
}
export interface TomTomMapRef {
fitAllMarkers: () => void;
fitToCoordinates: (
coords: { latitude: number; longitude: number }[],
) => void;
calcRoute: (
origin: { latitude: number; longitude: number },
destination: { latitude: number; longitude: number },
) => void;
calcRouteFromAddress: (
origin: { latitude: number; longitude: number },
address: string,
) => void;
clearRoute: () => void;
}
interface TomTomMapProps {
style?: any;
markers?: TomTomMarker[];
initialCenter?: { latitude: number; longitude: number };
initialZoom?: number;
onMarkerPress?: (markerId: string) => void;
}
const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
(
{
style,
markers = [],
initialCenter,
initialZoom = 14,
onMarkerPress,
},
ref,
) => {
const webViewRef = useRef<WebView>(null);
const isReady = useRef(false);
const pendingMessages = useRef<string[]>([]);
const sendMessage = useCallback((msg: object) => {
const json = JSON.stringify(msg);
if (isReady.current) {
webViewRef.current?.injectJavaScript(
`(function(){ try { handleMessage(${json}); } catch(e) {} })(); true;`,
);
} else {
pendingMessages.current.push(json);
}
}, []);
useImperativeHandle(
ref,
() => ({
fitAllMarkers: () => sendMessage({ type: "fitAll" }),
fitToCoordinates: (coords) =>
sendMessage({ type: "fitCoords", coords }),
clearRoute: () => sendMessage({ type: "clearRoute" }),
calcRoute: (origin, dest) => {
calculateRoute(origin, dest).then((result) => {
if (result) {
sendMessage({
type: "drawRoute",
coordinates: result.route.coordinates,
destination: dest,
});
} else {
// Fallback: ligne droite
sendMessage({
type: "drawRoute",
coordinates: [origin, dest],
destination: dest,
});
}
}).catch(() => {
sendMessage({
type: "drawRoute",
coordinates: [origin, dest],
destination: dest,
});
});
},
calcRouteFromAddress: async (origin, address) => {
try {
const dest = await geocodeAddress(address);
if (!dest) return;
const result = await calculateRoute(origin, dest);
if (result) {
sendMessage({
type: "drawRoute",
coordinates: result.route.coordinates,
destination: dest,
});
} else {
sendMessage({
type: "drawRoute",
coordinates: [origin, dest],
destination: dest,
});
}
} catch {
/* silent */
}
},
}),
[sendMessage],
);
useEffect(() => {
sendMessage({ type: "updateMarkers", markers });
}, [markers, sendMessage]);
const onMessage = useCallback(
(event: any) => {
try {
const data = JSON.parse(event.nativeEvent.data);
if (data.type === "ready") {
isReady.current = true;
// Rejouer les messages en attente
const pending = pendingMessages.current.slice();
pendingMessages.current = [];
for (const msg of pending) {
webViewRef.current?.injectJavaScript(
`(function(){ try { handleMessage(${msg}); } catch(e) {} })(); true;`,
);
}
// Fit sur tous les markers après un court délai
setTimeout(() => {
webViewRef.current?.injectJavaScript(
`(function(){ try { handleMessage(${JSON.stringify({ type: "fitAll" })}); } catch(e) {} })(); true;`,
);
}, 500);
} else if (data.type === "markerPress" && onMarkerPress) {
onMarkerPress(data.id);
}
} catch {
/* ignore */
}
},
[onMarkerPress],
);
const center =
initialCenter ||
(markers.length > 0
? {
latitude: markers[0].latitude,
longitude: markers[0].longitude,
}
: { latitude: 48.8566, longitude: 2.3522 });
const html = buildHtml(center, initialZoom, TOMTOM_API_KEY);
return (
<WebView
ref={webViewRef}
style={[styles.map, style]}
source={{ html, baseUrl: "https://api.tomtom.com" }}
onMessage={onMessage}
javaScriptEnabled
domStorageEnabled
scrollEnabled={false}
bounces={false}
originWhitelist={["*"]}
mixedContentMode="always"
allowFileAccessFromFileURLs
allowUniversalAccessFromFileURLs
androidLayerType="hardware"
/>
);
},
);
function buildHtml(
center: { latitude: number; longitude: number },
zoom: number,
apiKey: string,
) {
return `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/6.25.0/maps/maps.css">
<script src="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/6.25.0/maps/maps-web.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body, #map { width: 100%; height: 100%; overflow: hidden; }
.marker-dot {
width: 28px; height: 28px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
border: 2px solid rgba(255,255,255,0.8);
cursor: pointer;
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
}
.marker-dot.selected {
width: 36px; height: 36px;
border: 3px solid #7c3aed;
box-shadow: 0 0 12px rgba(124,58,237,0.6);
}
.dest-marker {
width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
var map = tt.map({
key: '${apiKey}',
container: 'map',
center: [${center.longitude}, ${center.latitude}],
zoom: ${zoom},
stylesVisibility: { trafficFlow: false, trafficIncidents: false }
});
var markerObjects = {};
var destMarker = null;
var ROUTE_SOURCE = 'tt-route-src';
var ROUTE_LAYER = 'tt-route-lyr';
// File d'attente pour les messages reçus avant que le style soit chargé
var pendingRouteData = null;
var styleLoaded = false;
function clearRoute() {
try { if (map.getLayer(ROUTE_LAYER)) map.removeLayer(ROUTE_LAYER); } catch(e) {}
try { if (map.getSource(ROUTE_SOURCE)) map.removeSource(ROUTE_SOURCE); } catch(e) {}
if (destMarker) { destMarker.remove(); destMarker = null; }
}
function drawRoute(coordinates, destination) {
clearRoute();
if (!destination) return;
// Marker destination (pin rouge)
var pin = document.createElement('div');
pin.className = 'dest-marker';
pin.innerHTML = '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="#ef4444" d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5" fill="white"/></svg>';
destMarker = new tt.Marker({ element: pin })
.setLngLat([destination.longitude, destination.latitude])
.addTo(map);
if (!coordinates || coordinates.length < 2) {
map.flyTo({ center: [destination.longitude, destination.latitude], zoom: 15 });
return;
}
// Convertir les coords { latitude, longitude } en [lng, lat] pour GeoJSON
var lngLat = coordinates.map(function(c) {
return [c.longitude, c.latitude];
});
try {
map.addSource(ROUTE_SOURCE, {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: { type: 'LineString', coordinates: lngLat }
}
});
map.addLayer({
id: ROUTE_LAYER,
type: 'line',
source: ROUTE_SOURCE,
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { 'line-color': '#4285F4', 'line-width': 6, 'line-opacity': 0.9 }
});
var bounds = new tt.LngLatBounds();
lngLat.forEach(function(c) { bounds.extend(c); });
map.fitBounds(bounds, { padding: 60, maxZoom: 16, duration: 800 });
} catch(e) {}
}
}
function handleMessage(data) {
if (data.type === 'updateMarkers') {
Object.values(markerObjects).forEach(function(m) { m.remove(); });
markerObjects = {};
(data.markers || []).forEach(function(m) {
var el = document.createElement('div');
el.className = 'marker-dot' + (m.isSelected ? ' selected' : '');
el.style.backgroundColor = m.color || '#7c3aed';
el.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="white"><circle cx="12" cy="12" r="8"/></svg>';
el.addEventListener('click', function() {
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'markerPress', id: m.id }));
});
var popup = new tt.Popup({ offset: 20, closeButton: false })
.setHTML('<div style="padding:4px 8px;font-size:12px;"><b>' + (m.label || m.id) + '</b>' + (m.description ? '<br>' + m.description : '') + '</div>');
var marker = new tt.Marker({ element: el })
.setLngLat([m.longitude, m.latitude])
.setPopup(popup)
.addTo(map);
markerObjects[m.id] = marker;
});
}
if (data.type === 'drawRoute') {
if (!styleLoaded) {
pendingRouteData = data;
} else {
drawRoute(data.coordinates, data.destination);
}
}
if (data.type === 'clearRoute') {
pendingRouteData = null;
clearRoute();
}
if (data.type === 'fitAll') {
var all = Object.values(markerObjects);
if (all.length > 0) {
var bounds = new tt.LngLatBounds();
all.forEach(function(m) { bounds.extend(m.getLngLat()); });
if (destMarker) bounds.extend(destMarker.getLngLat());
map.fitBounds(bounds, { padding: 60, maxZoom: 15, duration: 800 });
}
}
if (data.type === 'fitCoords') {
if (data.coords && data.coords.length > 0) {
var bounds = new tt.LngLatBounds();
data.coords.forEach(function(c) { bounds.extend([c.longitude, c.latitude]); });
map.fitBounds(bounds, { padding: 80, maxZoom: 15, duration: 800 });
}
}
}
map.on('load', function() {
styleLoaded = true;
if (pendingRouteData) {
drawRoute(pendingRouteData.coordinates, pendingRouteData.destination);
pendingRouteData = null;
}
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'ready' }));
});
</script>
</body>
</html>`;
}
const styles = StyleSheet.create({
map: { flex: 1 },
});
export default TomTomMap;