chore: add gps with tomtom API
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
# TomTom API Key
|
||||||
|
# Obtenez votre cle API gratuite sur https://developer.tomtom.com/
|
||||||
|
VITE_TOMTOM_API_KEY=MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user