'use client'; import { useEffect, useState, useRef } from 'react'; import dynamic from 'next/dynamic'; import L from 'leaflet'; // Fix pour les icônes Leaflet avec Next.js if (typeof window !== 'undefined') { delete (L.Icon.Default.prototype as any)._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png', iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png', }); } // Import dynamique pour éviter les problèmes SSR avec Leaflet const MapContainer = dynamic(() => import('react-leaflet').then((mod) => mod.MapContainer), { ssr: false }); const TileLayer = dynamic(() => import('react-leaflet').then((mod) => mod.TileLayer), { ssr: false }); const Marker = dynamic(() => import('react-leaflet').then((mod) => mod.Marker), { ssr: false }); const Popup = dynamic(() => import('react-leaflet').then((mod) => mod.Popup), { ssr: false }); const Polyline = dynamic(() => import('react-leaflet').then((mod) => mod.Polyline), { ssr: false }); interface TrajetMapProps { adresseDepart: string; adresseArrivee: string; adherentNom?: string; } interface Coordinates { lat: number; lng: number; } interface RouteInfo { distance: number; // en mètres duration: number; // en secondes } export default function TrajetMap({ adresseDepart, adresseArrivee, adherentNom }: TrajetMapProps) { const [departCoords, setDepartCoords] = useState(null); const [arriveeCoords, setArriveeCoords] = useState(null); const [routeInfo, setRouteInfo] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Cache simple pour éviter de regéocoder les mêmes adresses const geocodeCacheRef = useRef>(new Map()); useEffect(() => { // Réinitialiser les coordonnées quand les adresses changent setDepartCoords(null); setArriveeCoords(null); setRouteInfo(null); setError(null); if (adresseDepart && adresseArrivee && adresseDepart.length >= 5 && adresseArrivee.length >= 5) { // Délai réduit pour une réponse plus rapide const timeoutId = setTimeout(() => { geocodeAddresses(); }, 400); return () => clearTimeout(timeoutId); } }, [adresseDepart, adresseArrivee]); const geocodeAddresses = async () => { if (!adresseDepart || !adresseArrivee) { return; } // Vérifier que les adresses ont au moins 5 caractères if (adresseDepart.length < 5 || adresseArrivee.length < 5) { return; } setLoading(true); setError(null); try { // Géocoder les deux adresses en parallèle pour plus de rapidité // (l'API Nominatim permet généralement 1 req/sec, mais on peut essayer) const [departResult, arriveeResult] = await Promise.all([ geocodeAddress(adresseDepart).catch(() => null), // Petit délai pour la deuxième requête pour respecter les limites new Promise(resolve => setTimeout(resolve, 1000)).then(() => geocodeAddress(adresseArrivee).catch(() => null) ), ]); if (departResult && arriveeResult) { setDepartCoords(departResult); setArriveeCoords(arriveeResult); // Calculer la distance et le temps estimé const info = calculateRouteInfo(departResult, arriveeResult); setRouteInfo(info); } else if (!departResult) { setError(`Impossible de trouver l'adresse de départ: "${adresseDepart}"`); } else { setError(`Impossible de trouver l'adresse d'arrivée: "${adresseArrivee}"`); } } catch (err) { console.error('Erreur lors du géocodage:', err); setError('Erreur lors du chargement de la carte. Veuillez réessayer.'); } finally { setLoading(false); } }; const geocodeAddress = async (address: string): Promise => { if (!address || address.length < 5) { return null; } try { // Nettoyer l'adresse pour améliorer les résultats const cleanAddress = address.trim().toLowerCase(); // Vérifier le cache if (geocodeCacheRef.current.has(cleanAddress)) { return geocodeCacheRef.current.get(cleanAddress)!; } const response = await fetch( `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address.trim())}&limit=1&addressdetails=1&countrycodes=fr`, { headers: { 'User-Agent': 'MAD Platform', 'Accept-Language': 'fr-FR,fr;q=0.9', 'Referer': window.location.origin, }, } ); if (response.ok) { const data = await response.json(); if (data && data.length > 0 && data[0].lat && data[0].lon) { const lat = parseFloat(data[0].lat); const lng = parseFloat(data[0].lon); // Vérifier que les coordonnées sont valides if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { const coords = { lat, lng }; // Mettre en cache (limiter à 50 entrées pour éviter la surconsommation mémoire) if (geocodeCacheRef.current.size < 50) { geocodeCacheRef.current.set(cleanAddress, coords); } return coords; } } } else { console.warn('Réponse non OK de Nominatim:', response.status); } return null; } catch (error) { console.error('Erreur de géocodage:', error); return null; } }; const calculateRouteInfo = (depart: Coordinates, arrivee: Coordinates): RouteInfo => { // Calcul de la distance à vol d'oiseau (formule de Haversine) const R = 6371e3; // Rayon de la Terre en mètres const φ1 = (depart.lat * Math.PI) / 180; const φ2 = (arrivee.lat * Math.PI) / 180; const Δφ = ((arrivee.lat - depart.lat) * Math.PI) / 180; const Δλ = ((arrivee.lng - depart.lng) * 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)); const distance = R * c; // Distance en mètres // Estimation du temps : vitesse moyenne de 50 km/h en ville // On multiplie la distance par 1.3 pour tenir compte des détours const distanceWithDetour = distance * 1.3; const vitesseMoyenne = 50; // km/h const duration = (distanceWithDetour / 1000 / vitesseMoyenne) * 3600; // en secondes return { distance: distanceWithDetour, duration }; }; const formatDistance = (meters: number): string => { if (meters < 1000) { return `${Math.round(meters)} m`; } return `${(meters / 1000).toFixed(1)} km`; }; const formatDuration = (seconds: number): string => { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (hours > 0) { return `${hours}h${minutes > 0 ? minutes : ''}`; } return `${minutes} min`; }; if (!adresseDepart || !adresseArrivee) { return (

Remplissez les adresses pour voir la carte

); } if (loading) { return (

Chargement de la carte...

Géocodage des adresses en cours

); } if (error) { return (

{error}

Vérifiez que les adresses sont correctes et complètes

); } if (!departCoords || !arriveeCoords) { return (

Chargement de la carte...

); } // Calculer le centre et le zoom optimal pour afficher les deux points const centerLat = departCoords && arriveeCoords ? (departCoords.lat + arriveeCoords.lat) / 2 : 48.8566; const centerLng = departCoords && arriveeCoords ? (departCoords.lng + arriveeCoords.lng) / 2 : 2.3522; // Calculer le zoom optimal pour voir les deux points let optimalZoom = 12; if (departCoords && arriveeCoords) { const latDiff = Math.abs(departCoords.lat - arriveeCoords.lat); const lngDiff = Math.abs(departCoords.lng - arriveeCoords.lng); const maxDiff = Math.max(latDiff, lngDiff); if (maxDiff > 0.5) optimalZoom = 7; else if (maxDiff > 0.2) optimalZoom = 8; else if (maxDiff > 0.1) optimalZoom = 9; else if (maxDiff > 0.05) optimalZoom = 10; else if (maxDiff > 0.02) optimalZoom = 11; else optimalZoom = 12; } if (typeof window === 'undefined') { return null; } return (
{/* Informations du trajet */} {routeInfo && (
Distance
{formatDistance(routeInfo.distance)}
Temps estimé
{formatDuration(routeInfo.duration)}
)} {/* Carte */}
A
`, iconSize: [40, 40], iconAnchor: [20, 40], popupAnchor: [0, -40], })} >
Départ
{adresseDepart}
{adherentNom &&
{adherentNom}
}
B
`, iconSize: [40, 40], iconAnchor: [20, 40], popupAnchor: [0, -40], })} >
Arrivée
{adresseArrivee}
); }