chore: add gps with tomtom API

This commit is contained in:
2026-01-24 21:33:29 +01:00
parent 325b1b7c10
commit db34640acc
5 changed files with 2580 additions and 10 deletions
+1124 -1
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
# TomTom API Key
# Obtenez votre cle API gratuite sur https://developer.tomtom.com/
VITE_TOMTOM_API_KEY=MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB
+513
View File
@@ -0,0 +1,513 @@
.tomtom-map-container {
position: relative;
width: 100%;
height: 400px;
border-radius: 16px;
overflow: hidden;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.tomtom-map-container.navigating {
height: 500px;
}
.tomtom-map {
width: 100%;
height: 100%;
}
/* Marqueur du livreur */
.driver-marker {
cursor: pointer;
}
.driver-marker-inner {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border-radius: 50%;
border: 3px solid white;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
color: white;
animation: pulse-driver 2s infinite;
}
.driver-marker.navigation-mode .driver-marker-inner {
width: 50px;
height: 50px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.5);
}
.driver-arrow {
width: 0;
height: 0;
border-left: 12px solid transparent;
border-right: 12px solid transparent;
border-bottom: 24px solid white;
}
@keyframes pulse-driver {
0% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
}
70% {
box-shadow: 0 0 0 15px rgba(16, 185, 129, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
/* Marqueur de destination */
.destination-marker {
cursor: pointer;
}
.destination-marker-inner {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
border-radius: 50% 50% 50% 0;
border: 3px solid white;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
color: white;
transform: rotate(-45deg);
}
.destination-marker-inner svg {
transform: rotate(45deg);
}
/* Panneau de navigation */
.navigation-panel {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
background: linear-gradient(
180deg,
rgba(26, 26, 46, 0.98) 0%,
rgba(26, 26, 46, 0.95) 100%
);
backdrop-filter: blur(10px);
border-bottom: 2px solid #10b981;
}
.current-instruction {
display: flex;
align-items: center;
padding: 1rem 1.25rem;
gap: 1rem;
}
.instruction-maneuver {
font-size: 2.5rem;
min-width: 60px;
text-align: center;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
}
.instruction-details {
flex: 1;
}
.instruction-text {
font-size: 1.1rem;
font-weight: 600;
color: white;
line-height: 1.3;
}
.instruction-street {
font-size: 0.9rem;
color: #10b981;
margin-top: 0.25rem;
font-weight: 500;
}
.instruction-distance {
font-size: 1.5rem;
font-weight: 700;
color: #10b981;
min-width: 80px;
text-align: right;
}
.next-instruction {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1.25rem;
background: rgba(255, 255, 255, 0.05);
border-top: 1px solid rgba(255, 255, 255, 0.1);
font-size: 0.85rem;
color: #9ca3af;
}
.next-label {
color: #6b7280;
font-weight: 500;
}
.next-maneuver {
font-size: 1.25rem;
}
.next-text {
flex: 1;
}
/* Bouton recentrer */
.recenter-btn {
position: absolute;
bottom: 80px;
right: 16px;
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(26, 26, 46, 0.95);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 5;
transition: all 0.2s ease;
}
.recenter-btn:hover {
background: #10b981;
transform: scale(1.1);
}
/* Chargement */
.tomtom-map-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 20;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
background: rgba(26, 26, 46, 0.95);
padding: 1.5rem 2rem;
border-radius: 12px;
color: white;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(255, 255, 255, 0.2);
border-top-color: #10b981;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Informations de route */
.tomtom-route-info {
position: absolute;
bottom: 16px;
left: 16px;
right: 16px;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
background: rgba(26, 26, 46, 0.95);
backdrop-filter: blur(10px);
padding: 1rem 1.5rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 5;
}
.route-info-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.route-info-label {
font-size: 0.75rem;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.route-info-value {
font-size: 1.25rem;
font-weight: 700;
color: #10b981;
}
.route-info-divider {
width: 1px;
height: 40px;
background: rgba(255, 255, 255, 0.2);
}
/* Bouton afficher instructions */
.show-instructions-btn {
padding: 0.5rem 1rem;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border: none;
border-radius: 8px;
color: white;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.show-instructions-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
/* Liste des instructions */
.instructions-list {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 70px;
background: rgba(26, 26, 46, 0.98);
z-index: 15;
display: flex;
flex-direction: column;
border-radius: 16px 16px 0 0;
}
.instructions-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.instructions-header h4 {
margin: 0;
color: white;
font-size: 1rem;
}
.close-instructions-btn {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #ef4444;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.close-instructions-btn:hover {
background: rgba(239, 68, 68, 0.3);
transform: scale(1.1);
}
.instructions-scroll {
flex: 1;
overflow-y: auto;
padding: 0.5rem 0;
}
.instruction-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.25rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
transition: background 0.2s;
}
.instruction-item.active {
background: rgba(16, 185, 129, 0.15);
border-left: 3px solid #10b981;
}
.instruction-item.passed {
opacity: 0.5;
}
.item-maneuver {
font-size: 1.5rem;
min-width: 40px;
text-align: center;
}
.item-details {
flex: 1;
}
.item-text {
color: white;
font-size: 0.95rem;
display: block;
}
.item-street {
color: #10b981;
font-size: 0.8rem;
display: block;
margin-top: 0.25rem;
}
.item-distance {
color: #9ca3af;
font-size: 0.85rem;
min-width: 60px;
text-align: right;
}
/* Erreur */
.tomtom-map-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 2rem;
text-align: center;
color: white;
}
.tomtom-map-error .error-icon {
color: #f59e0b;
margin-bottom: 1rem;
}
.tomtom-map-error h3 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
color: #f59e0b;
}
.tomtom-map-error p {
color: #9ca3af;
margin-bottom: 1rem;
}
.tomtom-map-error ol {
text-align: left;
color: #9ca3af;
font-size: 0.9rem;
line-height: 1.8;
}
.tomtom-map-error a {
color: #3b82f6;
text-decoration: underline;
}
.tomtom-map-error code {
background: rgba(59, 130, 246, 0.2);
padding: 0.125rem 0.375rem;
border-radius: 4px;
font-family: monospace;
color: #60a5fa;
}
/* Toast d'erreur */
.tomtom-map-error-toast {
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
background: rgba(239, 68, 68, 0.95);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
z-index: 20;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translate(-50%, -20px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}
/* Responsive */
@media (max-width: 640px) {
.tomtom-map-container {
height: 350px;
border-radius: 12px;
}
.tomtom-map-container.navigating {
height: 450px;
}
.current-instruction {
padding: 0.75rem 1rem;
}
.instruction-maneuver {
font-size: 2rem;
min-width: 50px;
}
.instruction-text {
font-size: 1rem;
}
.instruction-distance {
font-size: 1.25rem;
min-width: 60px;
}
.tomtom-route-info {
flex-wrap: wrap;
padding: 0.75rem 1rem;
gap: 0.5rem;
}
.route-info-value {
font-size: 1rem;
}
.route-info-divider {
height: 30px;
}
.recenter-btn {
bottom: 75px;
width: 40px;
height: 40px;
}
}
+827
View File
@@ -0,0 +1,827 @@
import { useEffect, useRef, useState, useCallback } from "react";
import tt from "@tomtom-international/web-sdk-maps";
import "@tomtom-international/web-sdk-maps/dist/maps.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faArrowLeft,
faArrowRight,
faArrowUp,
faArrowTurnUp,
faRotateLeft,
faRotateRight,
faCircleNotch,
faRoad,
faSignOutAlt,
faFlagCheckered,
faCar,
faLocationDot,
faTimes,
faRoute,
faLocationCrosshairs,
} from "@fortawesome/free-solid-svg-icons";
import "./TomTomMap.css";
const TOMTOM_API_KEY = import.meta.env.VITE_TOMTOM_API_KEY;
interface TomTomMapProps {
driverLocation: {
latitude: number;
longitude: number;
} | null;
destinationAddress: string | null;
onRouteCalculated?: (distance: string, duration: string) => void;
onError?: (error: string) => void;
isNavigating?: boolean;
}
interface RouteInfo {
distance: string;
duration: string;
distanceRemaining: number;
timeRemaining: number;
}
interface NavigationInstruction {
instruction: string;
distance: string;
maneuverIcon: any;
streetName?: string;
isActive: boolean;
}
// Mapping des maneuvres vers des icones Font Awesome
const maneuverIcons: Record<string, any> = {
TURN_LEFT: faArrowLeft,
TURN_RIGHT: faArrowRight,
TURN_SLIGHT_LEFT: faArrowTurnUp,
TURN_SLIGHT_RIGHT: faArrowTurnUp,
TURN_SHARP_LEFT: faRotateLeft,
TURN_SHARP_RIGHT: faRotateRight,
KEEP_LEFT: faArrowLeft,
KEEP_RIGHT: faArrowRight,
STRAIGHT: faArrowUp,
ENTER_ROUNDABOUT: faCircleNotch,
EXIT_ROUNDABOUT: faArrowRight,
MOTORWAY_ENTER: faRoad,
MOTORWAY_EXIT: faSignOutAlt,
ARRIVE: faFlagCheckered,
ARRIVE_LEFT: faFlagCheckered,
ARRIVE_RIGHT: faFlagCheckered,
DEPART: faCar,
U_TURN: faRotateLeft,
FOLLOW: faArrowUp,
WAYPOINT_REACHED: faLocationDot,
DEFAULT: faArrowUp,
};
// Mapping des maneuvres vers des textes en francais
const maneuverTranslations: Record<string, string> = {
TURN_LEFT: "Tournez a gauche",
TURN_RIGHT: "Tournez a droite",
TURN_SLIGHT_LEFT: "Tournez legerement a gauche",
TURN_SLIGHT_RIGHT: "Tournez legerement a droite",
TURN_SHARP_LEFT: "Tournez fortement a gauche",
TURN_SHARP_RIGHT: "Tournez fortement a droite",
KEEP_LEFT: "Restez a gauche",
KEEP_RIGHT: "Restez a droite",
STRAIGHT: "Continuez tout droit",
ENTER_ROUNDABOUT: "Entrez dans le rond-point",
EXIT_ROUNDABOUT: "Sortez du rond-point",
MOTORWAY_ENTER: "Entrez sur l'autoroute",
MOTORWAY_EXIT: "Sortez de l'autoroute",
ARRIVE: "Vous etes arrive",
ARRIVE_LEFT: "Destination a gauche",
ARRIVE_RIGHT: "Destination a droite",
DEPART: "Depart",
U_TURN: "Faites demi-tour",
FOLLOW: "Suivez",
WAYPOINT_REACHED: "Point de passage atteint",
};
function TomTomMap({
driverLocation,
destinationAddress,
onRouteCalculated,
onError,
isNavigating = false,
}: TomTomMapProps) {
const mapContainer = useRef<HTMLDivElement>(null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mapInstance = useRef<any>(null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const driverMarker = useRef<any>(null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const destinationMarker = useRef<any>(null);
const routeLayerId = useRef<string>("route-layer");
const routeCoordinates = useRef<[number, number][]>([]);
const [isMapReady, setIsMapReady] = useState(false);
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
const [destinationCoords, setDestinationCoords] = useState<{
lat: number;
lng: number;
} | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [instructions, setInstructions] = useState<NavigationInstruction[]>(
[],
);
const [currentInstructionIndex, setCurrentInstructionIndex] = useState(0);
const [showAllInstructions, setShowAllInstructions] = useState(false);
// Calculer la distance entre deux points (formule Haversine)
const calculateDistance = useCallback(
(lat1: number, lon1: number, lat2: number, lon2: number): number => {
const R = 6371e3; // Rayon de la Terre en metres
const φ1 = (lat1 * Math.PI) / 180;
const φ2 = (lat2 * Math.PI) / 180;
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) *
Math.cos(φ2) *
Math.sin(Δλ / 2) *
Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
},
[],
);
// Formater la distance
const formatDistance = useCallback((meters: number): string => {
if (meters < 1000) {
return `${Math.round(meters)} m`;
}
return `${(meters / 1000).toFixed(1)} km`;
}, []);
// Initialiser la carte
useEffect(() => {
if (
!mapContainer.current ||
!TOMTOM_API_KEY ||
TOMTOM_API_KEY === "YOUR_TOMTOM_API_KEY_HERE"
) {
setError(
"Cle API TomTom non configuree. Veuillez ajouter VITE_TOMTOM_API_KEY dans .env",
);
onError?.("Cle API TomTom non configuree");
return;
}
const map = tt.map({
key: TOMTOM_API_KEY,
container: mapContainer.current,
center: driverLocation
? [driverLocation.longitude, driverLocation.latitude]
: [2.3522, 48.8566],
zoom: 15,
language: "fr-FR",
});
map.addControl(new tt.NavigationControl());
map.on("load", () => {
console.log("TomTom Map chargee");
setIsMapReady(true);
});
mapInstance.current = map;
return () => {
if (mapInstance.current) {
mapInstance.current.remove();
mapInstance.current = null;
}
};
}, []);
// Creer le marqueur du livreur (style navigation)
const createDriverMarkerElement = useCallback(() => {
const el = document.createElement("div");
el.className = "driver-marker navigation-mode";
el.innerHTML = `
<div class="driver-marker-inner">
<div class="driver-arrow"></div>
</div>
`;
return el;
}, []);
// Creer le marqueur de destination
const createDestinationMarkerElement = useCallback(() => {
const el = document.createElement("div");
el.className = "destination-marker";
el.innerHTML = `
<div class="destination-marker-inner">
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
<path 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-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>
</div>
`;
return el;
}, []);
// Mettre a jour la position du livreur et centrer la carte en mode navigation
useEffect(() => {
if (!isMapReady || !mapInstance.current || !driverLocation) return;
const { latitude, longitude } = driverLocation;
if (driverMarker.current) {
driverMarker.current.setLngLat([longitude, latitude]);
} else {
driverMarker.current = new tt.Marker({
element: createDriverMarkerElement(),
})
.setLngLat([longitude, latitude])
.addTo(mapInstance.current);
}
// En mode navigation, suivre le livreur
if (isNavigating && destinationCoords) {
mapInstance.current.easeTo({
center: [longitude, latitude],
zoom: 17,
pitch: 60,
bearing: calculateBearing(
latitude,
longitude,
destinationCoords.lat,
destinationCoords.lng,
),
});
// Mettre a jour l'instruction active en fonction de la position
updateCurrentInstruction(latitude, longitude);
} else if (!destinationCoords) {
mapInstance.current.flyTo({
center: [longitude, latitude],
zoom: 15,
});
}
}, [
isMapReady,
driverLocation,
isNavigating,
destinationCoords,
createDriverMarkerElement,
]);
// Calculer l'angle de direction (bearing)
const calculateBearing = (
lat1: number,
lon1: number,
lat2: number,
lon2: number,
): number => {
const φ1 = (lat1 * Math.PI) / 180;
const φ2 = (lat2 * Math.PI) / 180;
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
const y = Math.sin(Δλ) * Math.cos(φ2);
const x =
Math.cos(φ1) * Math.sin(φ2) -
Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
const θ = Math.atan2(y, x);
return ((θ * 180) / Math.PI + 360) % 360;
};
// Mettre a jour l'instruction courante basee sur la position
const updateCurrentInstruction = useCallback(
(lat: number, lng: number) => {
if (instructions.length === 0) return;
// Trouver l'instruction la plus proche
let minDistance = Infinity;
let closestIndex = currentInstructionIndex;
// Chercher parmi les instructions restantes
for (
let i = currentInstructionIndex;
i < instructions.length;
i++
) {
// Utiliser les coordonnees de la route pour trouver le point le plus proche
if (routeCoordinates.current.length > 0) {
const segmentStart = Math.floor(
(i / instructions.length) *
routeCoordinates.current.length,
);
if (segmentStart < routeCoordinates.current.length) {
const [pointLng, pointLat] =
routeCoordinates.current[segmentStart];
const dist = calculateDistance(
lat,
lng,
pointLat,
pointLng,
);
if (dist < minDistance) {
minDistance = dist;
closestIndex = i;
}
}
}
}
// Si on est a moins de 50m d'une instruction, passer a la suivante
if (minDistance < 50 && closestIndex > currentInstructionIndex) {
setCurrentInstructionIndex(closestIndex);
// Mettre a jour les instructions pour marquer l'active
setInstructions((prev) =>
prev.map((inst, idx) => ({
...inst,
isActive: idx === closestIndex,
})),
);
}
},
[instructions, currentInstructionIndex, calculateDistance],
);
// Geocoder l'adresse de destination
useEffect(() => {
if (
!destinationAddress ||
!TOMTOM_API_KEY ||
TOMTOM_API_KEY === "YOUR_TOMTOM_API_KEY_HERE"
)
return;
setIsLoading(true);
setError(null);
// Utiliser l'API REST directement pour le geocodage
const searchUrl = `https://api.tomtom.com/search/2/geocode/${encodeURIComponent(destinationAddress)}.json?key=${TOMTOM_API_KEY}&countrySet=FR&limit=1`;
fetch(searchUrl)
.then((response) => response.json())
.then((data) => {
if (data.results && data.results.length > 0) {
const result = data.results[0];
if (
result.position?.lat !== undefined &&
result.position?.lon !== undefined
) {
const coords = {
lat: result.position.lat,
lng: result.position.lon,
};
console.log("Destination geocodee:", coords);
setDestinationCoords(coords);
} else {
setError("Position de destination invalide");
onError?.("Position de destination invalide");
}
} else {
setError("Adresse de destination introuvable");
onError?.("Adresse de destination introuvable");
}
})
.catch((err) => {
console.error("Erreur geocodage:", err);
setError("Erreur lors du geocodage de l'adresse");
onError?.("Erreur lors du geocodage de l'adresse");
})
.finally(() => {
setIsLoading(false);
});
}, [destinationAddress, onError]);
// Ajouter le marqueur de destination et calculer l'itineraire
useEffect(() => {
if (!isMapReady || !mapInstance.current || !destinationCoords) return;
if (destinationMarker.current) {
destinationMarker.current.setLngLat([
destinationCoords.lng,
destinationCoords.lat,
]);
} else {
destinationMarker.current = new tt.Marker({
element: createDestinationMarkerElement(),
})
.setLngLat([destinationCoords.lng, destinationCoords.lat])
.addTo(mapInstance.current);
}
if (driverLocation) {
calculateRoute();
} else {
mapInstance.current.flyTo({
center: [destinationCoords.lng, destinationCoords.lat],
zoom: 15,
});
}
}, [
isMapReady,
destinationCoords,
driverLocation,
createDestinationMarkerElement,
]);
// Calculer l'itineraire avec instructions via API REST directe
const calculateRoute = useCallback(async () => {
if (
!mapInstance.current ||
!driverLocation ||
!destinationCoords ||
!TOMTOM_API_KEY ||
TOMTOM_API_KEY === "YOUR_TOMTOM_API_KEY_HERE"
)
return;
setIsLoading(true);
try {
// Utiliser l'API REST directement pour avoir les points de la route
const startPoint = `${driverLocation.latitude},${driverLocation.longitude}`;
const endPoint = `${destinationCoords.lat},${destinationCoords.lng}`;
const routeUrl = `https://api.tomtom.com/routing/1/calculateRoute/${startPoint}:${endPoint}/json?key=${TOMTOM_API_KEY}&instructionsType=text&language=fr-FR&traffic=true&travelMode=car`;
console.log("Appel API TomTom Routing...");
const response = await fetch(routeUrl);
const routeData = await response.json();
console.log("Route response:", routeData);
if (!routeData.routes || routeData.routes.length === 0) {
setError("Aucun itineraire trouve");
onError?.("Aucun itineraire trouve");
setIsLoading(false);
return;
}
const route = routeData.routes[0];
let coordinates: [number, number][] = [];
// Extraire les coordonnees des legs
if (route.legs && route.legs.length > 0) {
route.legs.forEach((leg: any) => {
if (leg.points && leg.points.length > 0) {
leg.points.forEach((point: any) => {
coordinates.push([point.longitude, point.latitude]);
});
}
});
}
console.log("Coordonnees extraites:", coordinates.length, "points");
// Fallback: ligne droite si pas de points
if (coordinates.length === 0) {
console.log("Fallback: utilisation ligne droite");
coordinates = [
[driverLocation.longitude, driverLocation.latitude],
[destinationCoords.lng, destinationCoords.lat],
];
}
routeCoordinates.current = coordinates;
// Extraire les instructions de navigation
const navInstructions: NavigationInstruction[] = [];
if (
route.guidance?.instructions &&
route.guidance.instructions.length > 0
) {
route.guidance.instructions.forEach(
(inst: any, index: number) => {
const maneuverKey = inst.maneuver || "STRAIGHT";
const icon =
maneuverIcons[maneuverKey] || maneuverIcons.DEFAULT;
const text =
maneuverTranslations[maneuverKey] || "Continuez";
navInstructions.push({
instruction: inst.message || text,
distance: formatDistance(
inst.routeOffsetInMeters || 0,
),
maneuverIcon: icon,
streetName: inst.street,
isActive: index === 0,
});
},
);
}
// Si pas d'instructions, creer des instructions basiques
if (navInstructions.length === 0) {
navInstructions.push({
instruction: "Dirigez-vous vers votre destination",
distance: formatDistance(
route.summary?.lengthInMeters || 0,
),
maneuverIcon: faCar,
isActive: true,
});
navInstructions.push({
instruction: "Vous etes arrive a destination",
distance: "0 m",
maneuverIcon: faFlagCheckered,
isActive: false,
});
}
setInstructions(navInstructions);
setCurrentInstructionIndex(0);
// Construire le GeoJSON pour la ligne
const geojson = {
type: "Feature" as const,
properties: {},
geometry: {
type: "LineString" as const,
coordinates: coordinates,
},
};
console.log("GeoJSON cree avec", coordinates.length, "points");
// Supprimer l'ancien itineraire s'il existe
try {
if (mapInstance.current?.getLayer(routeLayerId.current)) {
mapInstance.current.removeLayer(routeLayerId.current);
}
if (mapInstance.current?.getSource(routeLayerId.current)) {
mapInstance.current.removeSource(routeLayerId.current);
}
} catch (e) {
console.log("Pas d'ancien itineraire a supprimer");
}
// Ajouter le nouvel itineraire
mapInstance.current?.addSource(routeLayerId.current, {
type: "geojson",
data: geojson,
});
mapInstance.current?.addLayer({
id: routeLayerId.current,
type: "line",
source: routeLayerId.current,
layout: {
"line-join": "round",
"line-cap": "round",
},
paint: {
"line-color": "#4285F4",
"line-width": 6,
"line-opacity": 0.8,
},
});
console.log("Itineraire ajoute a la carte");
// Informations de l'itineraire
const summary = route.summary;
const distanceKm = (summary.lengthInMeters / 1000).toFixed(1);
const durationMin = Math.round(summary.travelTimeInSeconds / 60);
const info: RouteInfo = {
distance: `${distanceKm} km`,
duration: `${durationMin} min`,
distanceRemaining: summary.lengthInMeters,
timeRemaining: summary.travelTimeInSeconds,
};
setRouteInfo(info);
onRouteCalculated?.(info.distance, info.duration);
// Ajuster la vue pour montrer tout l'itineraire
const bounds = new tt.LngLatBounds();
bounds.extend([driverLocation.longitude, driverLocation.latitude]);
bounds.extend([destinationCoords.lng, destinationCoords.lat]);
mapInstance.current?.fitBounds(bounds, {
padding: 80,
});
console.log(
"Itineraire calcule avec",
navInstructions.length,
"instructions",
);
} catch (err) {
console.error("Erreur calcul itineraire:", err);
setError("Erreur lors du calcul de l'itineraire");
onError?.("Erreur lors du calcul de l'itineraire");
} finally {
setIsLoading(false);
}
}, [
driverLocation,
destinationCoords,
onRouteCalculated,
onError,
formatDistance,
]);
// Recentrer sur le livreur
const centerOnDriver = () => {
if (mapInstance.current && driverLocation) {
mapInstance.current.flyTo({
center: [driverLocation.longitude, driverLocation.latitude],
zoom: 17,
});
}
};
if (
error &&
(!TOMTOM_API_KEY || TOMTOM_API_KEY === "YOUR_TOMTOM_API_KEY_HERE")
) {
return (
<div className="tomtom-map-container">
<div className="tomtom-map-error">
<div className="error-icon">
<svg
viewBox="0 0 24 24"
fill="currentColor"
width="48"
height="48"
>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
</svg>
</div>
<h3>Configuration requise</h3>
<p>{error}</p>
<ol>
<li>
Allez sur{" "}
<a
href="https://developer.tomtom.com/"
target="_blank"
rel="noopener noreferrer"
>
developer.tomtom.com
</a>
</li>
<li>Creez un compte gratuit</li>
<li>Obtenez une cle API</li>
<li>
Ajoutez-la dans le fichier <code>.env</code>
</li>
</ol>
</div>
</div>
);
}
const currentInstruction = instructions[currentInstructionIndex];
const nextInstruction = instructions[currentInstructionIndex + 1];
return (
<div
className={`tomtom-map-container ${isNavigating ? "navigating" : ""}`}
>
{isLoading && (
<div className="tomtom-map-loading">
<div className="loading-spinner"></div>
<span>Calcul de l'itineraire...</span>
</div>
)}
{/* Panneau d'instruction principale */}
{isNavigating && currentInstruction && (
<div className="navigation-panel">
<div className="current-instruction">
<div className="instruction-maneuver">
<FontAwesomeIcon
icon={currentInstruction.maneuverIcon}
/>
</div>
<div className="instruction-details">
<div className="instruction-text">
{currentInstruction.instruction}
</div>
{currentInstruction.streetName && (
<div className="instruction-street">
{currentInstruction.streetName}
</div>
)}
</div>
<div className="instruction-distance">
{currentInstruction.distance}
</div>
</div>
{nextInstruction && (
<div className="next-instruction">
<span className="next-label">Puis</span>
<span className="next-maneuver">
<FontAwesomeIcon
icon={nextInstruction.maneuverIcon}
/>
</span>
<span className="next-text">
{nextInstruction.instruction}
</span>
</div>
)}
</div>
)}
<div ref={mapContainer} className="tomtom-map" />
{/* Bouton recentrer */}
{driverLocation && (
<button
className="recenter-btn"
onClick={centerOnDriver}
title="Recentrer"
>
<FontAwesomeIcon icon={faLocationCrosshairs} />
</button>
)}
{/* Infos de route */}
{routeInfo && (
<div className="tomtom-route-info">
<div className="route-info-item">
<span className="route-info-label">Distance</span>
<span className="route-info-value">
{routeInfo.distance}
</span>
</div>
<div className="route-info-divider" />
<div className="route-info-item">
<span className="route-info-label">Duree</span>
<span className="route-info-value">
{routeInfo.duration}
</span>
</div>
{instructions.length > 0 && (
<>
<div className="route-info-divider" />
<button
className="show-instructions-btn"
onClick={() =>
setShowAllInstructions(!showAllInstructions)
}
>
<FontAwesomeIcon
icon={faRoute}
style={{ marginRight: "0.5rem" }}
/>
{showAllInstructions ? "Masquer" : "Etapes"} (
{instructions.length})
</button>
</>
)}
</div>
)}
{/* Liste des instructions */}
{showAllInstructions && instructions.length > 0 && (
<div className="instructions-list">
<div className="instructions-header">
<h4>Etapes de l'itineraire</h4>
<button
className="close-instructions-btn"
onClick={() => setShowAllInstructions(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
</div>
<div className="instructions-scroll">
{instructions.map((inst, index) => (
<div
key={index}
className={`instruction-item ${index === currentInstructionIndex ? "active" : ""} ${index < currentInstructionIndex ? "passed" : ""}`}
>
<span className="item-maneuver">
<FontAwesomeIcon icon={inst.maneuverIcon} />
</span>
<div className="item-details">
<span className="item-text">
{inst.instruction}
</span>
{inst.streetName && (
<span className="item-street">
{inst.streetName}
</span>
)}
</div>
<span className="item-distance">
{inst.distance}
</span>
</div>
))}
</div>
</div>
)}
{error && <div className="tomtom-map-error-toast">{error}</div>}
</div>
);
}
export default TomTomMap;
@@ -32,6 +32,7 @@ import {
endAlert, endAlert,
getMyAlerts, getMyAlerts,
} from "../../api/api_delivery"; } from "../../api/api_delivery";
import TomTomMap from "../../components/TomTomMap";
interface DeliveryStats { interface DeliveryStats {
todayDeliveries: number; todayDeliveries: number;
@@ -117,6 +118,9 @@ function DeliveryDashboard() {
const [isAlertTriggered, setIsAlertTriggered] = useState(false); const [isAlertTriggered, setIsAlertTriggered] = useState(false);
const [policeAlertLoading, setPoliceAlertLoading] = useState(false); const [policeAlertLoading, setPoliceAlertLoading] = useState(false);
const [showMap, setShowMap] = useState(false);
const [routeDistance, setRouteDistance] = useState<string | null>(null);
const [routeDuration, setRouteDuration] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const checkAuth = () => { const checkAuth = () => {
@@ -409,6 +413,11 @@ function DeliveryDashboard() {
const clientInfo = const clientInfo =
detailsResult.delivery.client_info; detailsResult.delivery.client_info;
const deliveryStatus =
delivery.status === "assigned"
? "assigned"
: "in_route";
setCurrentDelivery({ setCurrentDelivery({
id: delivery.id, id: delivery.id,
orderNumber: `CMD-${delivery.id}`, orderNumber: `CMD-${delivery.id}`,
@@ -420,15 +429,17 @@ function DeliveryDashboard() {
"+33 X XX XX XX XX", "+33 X XX XX XX XX",
deliveryAddress: deliveryAddress:
delivery.adresse || "Adresse de livraison", delivery.adresse || "Adresse de livraison",
status: status: deliveryStatus,
delivery.status === "assigned"
? "assigned"
: "in_route",
distance: "N/A", distance: "N/A",
// ✅ FIX: Cast to any pour accéder à items qui peut exister mais n'est pas typé // eslint-disable-next-line @typescript-eslint/no-explicit-any
items: (currentDeliveryData as any).items || [], items: (currentDeliveryData as any).items || [],
totalPrice: currentDeliveryData.total_prix || 0, totalPrice: currentDeliveryData.total_prix || 0,
}); });
// Afficher automatiquement la carte si livraison en cours
if (deliveryStatus === "in_route") {
setShowMap(true);
}
} }
} else { } else {
setCurrentDelivery(null); setCurrentDelivery(null);
@@ -516,10 +527,14 @@ function DeliveryDashboard() {
} }
console.log("✅ [DELIVERY_DASHBOARD] Livraison démarrée"); console.log("✅ [DELIVERY_DASHBOARD] Livraison démarrée");
// Afficher automatiquement la carte en mode navigation
setShowMap(true);
showStyledAlert( showStyledAlert(
"success", "success",
"Livraison démarrée", "Livraison démarrée",
"La livraison a été démarrée avec succès !", "La livraison a été démarrée avec succès ! Suivez l'itinéraire sur la carte.",
() => window.location.reload(), () => window.location.reload(),
); );
} catch (error) { } catch (error) {
@@ -592,6 +607,20 @@ function DeliveryDashboard() {
}; };
const handleNavigate = () => { const handleNavigate = () => {
setShowMap((prev) => !prev);
};
const handleRouteCalculated = (distance: string, duration: string) => {
setRouteDistance(distance);
setRouteDuration(duration);
console.log(`Route calculee: ${distance}, ${duration}`);
};
const handleMapError = (error: string) => {
console.error("Erreur carte TomTom:", error);
};
const openExternalNavigation = () => {
if (currentDelivery) { if (currentDelivery) {
const address = encodeURIComponent(currentDelivery.deliveryAddress); const address = encodeURIComponent(currentDelivery.deliveryAddress);
window.open( window.open(
@@ -1273,7 +1302,7 @@ function DeliveryDashboard() {
onClick={handleStartDelivery} onClick={handleStartDelivery}
> >
<Package size={20} /> <Package size={20} />
Démarrer la Livraison Demarrer la Livraison
</button> </button>
)} )}
@@ -1283,7 +1312,7 @@ function DeliveryDashboard() {
onClick={handleCompleteDelivery} onClick={handleCompleteDelivery}
> >
<CheckCircle size={20} /> <CheckCircle size={20} />
Marquer comme Livrée Marquer comme Livree
</button> </button>
)} )}
@@ -1292,9 +1321,84 @@ function DeliveryDashboard() {
onClick={handleNavigate} onClick={handleNavigate}
> >
<Navigation size={20} /> <Navigation size={20} />
Navigation GPS {showMap
? "Masquer la carte"
: "Afficher la carte"}
</button> </button>
</div> </div>
{/* Carte TomTom GPS */}
{showMap && (
<div style={{ marginTop: "1.5rem" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: "1rem",
}}
>
<h4
style={{
margin: 0,
color: "#fff",
fontSize: "1.1rem",
fontWeight: "600",
display: "flex",
alignItems: "center",
gap: "0.5rem",
}}
>
<Navigation size={20} color="#10b981" />
Navigation GPS TomTom
</h4>
{routeDistance && routeDuration && (
<div
style={{
display: "flex",
gap: "1rem",
fontSize: "0.875rem",
}}
>
<span style={{ color: "#9ca3af" }}>
Distance:{" "}
<strong
style={{ color: "#10b981" }}
>
{routeDistance}
</strong>
</span>
<span style={{ color: "#9ca3af" }}>
Temps:{" "}
<strong
style={{ color: "#10b981" }}
>
{routeDuration}
</strong>
</span>
</div>
)}
</div>
<TomTomMap
driverLocation={
location.latitude && location.longitude
? {
latitude: location.latitude,
longitude: location.longitude,
}
: null
}
destinationAddress={
currentDelivery.deliveryAddress
}
onRouteCalculated={handleRouteCalculated}
onError={handleMapError}
isNavigating={
currentDelivery.status === "in_route"
}
/>
</div>
)}
</div> </div>
) : ( ) : (
<div className="current-delivery-card no-delivery-card"> <div className="current-delivery-card no-delivery-card">