import React, { useState, useEffect, useRef } from 'react'; import { cn } from '../../shared/utils'; import { useGoogleMapsLoader } from '../google-maps-loader'; export interface RouteMapProps extends React.HTMLAttributes { origin: { lat: number; lng: number }; destination: { lat: number; lng: number }; waypoints?: Array<{ lat: number; lng: number }>; travelMode?: 'DRIVING' | 'WALKING' | 'BICYCLING' | 'TRANSIT'; height?: string; mapId?: string; apiKey?: string; mapContainerClassName?: string; disableDefaultUI?: boolean; zoomControl?: boolean; streetViewControl?: boolean; mapTypeControl?: boolean; fullscreenControl?: boolean; onRouteCalculated?: (distance: string, duration: string) => void; } const RouteMapContent = React.forwardRef( ({ apiKey, ...props }, ref) => { const { isLoaded, loadError, load } = useGoogleMapsLoader(); const { origin, destination, waypoints = [], travelMode = 'DRIVING', height = '450px', mapContainerClassName, disableDefaultUI = false, zoomControl = true, streetViewControl = false, mapTypeControl = false, fullscreenControl = true, onRouteCalculated, className, ...divProps } = props; const mapRef = useRef(null); const gmpMapRef = useRef(null); // Ref for custom element const directionsRendererRef = useRef(null); const iInitializedRef = useRef(false); const isCalculatingRef = useRef(false); // Initialize map useEffect(() => { // Load API if needed if (!isLoaded && apiKey && !loadError && load) { load(apiKey).catch(console.error); } }, [isLoaded, apiKey, loadError, load]); // Handle gmp-map initialization and DirectionsRenderer useEffect(() => { if (!isLoaded || !gmpMapRef.current) return; const setupMap = (map: google.maps.Map) => { mapRef.current = map; // Create directions renderer const computedStyle = getComputedStyle(document.documentElement); const primaryColor = computedStyle.getPropertyValue('--primary').trim() || '#4F46E5'; if (!directionsRendererRef.current) { directionsRendererRef.current = new google.maps.DirectionsRenderer({ map: map, suppressMarkers: false, polylineOptions: { strokeColor: primaryColor, strokeWeight: 5, strokeOpacity: 0.8, }, }); } else { directionsRendererRef.current.setMap(map); } }; const gmpMap = gmpMapRef.current; if (gmpMap.innerMap) { setupMap(gmpMap.innerMap); } else { const interval = setInterval(() => { if (gmpMap.innerMap) { setupMap(gmpMap.innerMap); clearInterval(interval); } }, 100); return () => clearInterval(interval); } return () => { if (directionsRendererRef.current) { directionsRendererRef.current.setMap(null); // Don't nullify renderer here to allow reuse, or recreate } mapRef.current = null; }; }, [isLoaded]); // Sync properties with gmp-map custom element useEffect(() => { if (gmpMapRef.current && origin) { gmpMapRef.current.center = origin; } }, [origin.lat, origin.lng]); useEffect(() => { if (gmpMapRef.current) { gmpMapRef.current.zoom = 13; } }, []); // mapId is now set declaratively on the gmp-map element // Calculate route useEffect(() => { const map = mapRef.current; const renderer = directionsRendererRef.current; if (!map || !renderer || !isLoaded || isCalculatingRef.current) return; if (!origin || !destination) return; isCalculatingRef.current = true; const directionsService = new google.maps.DirectionsService(); const request: google.maps.DirectionsRequest = { origin: origin, destination: destination, waypoints: waypoints.map(wp => ({ location: wp, stopover: true, })), travelMode: google.maps.TravelMode[travelMode], }; directionsService.route(request, (result, status) => { isCalculatingRef.current = false; if (status === 'OK' && result) { renderer.setDirections(result); // Calculate total distance and duration const route = result.routes[0]; if (route?.legs?.length > 0 && onRouteCalculated) { let totalDistance = 0; let totalDuration = 0; route.legs.forEach(leg => { if (leg.distance) totalDistance += leg.distance.value; if (leg.duration) totalDuration += leg.duration.value; }); const distanceKm = (totalDistance / 1000).toFixed(1); const distanceText = `${distanceKm} km`; const hours = Math.floor(totalDuration / 3600); const minutes = Math.floor((totalDuration % 3600) / 60); const durationText = hours > 0 ? `${hours}h ${minutes}min` : `${minutes} min`; onRouteCalculated(distanceText, durationText); } } }); }, [ isLoaded, origin.lat, origin.lng, destination.lat, destination.lng, travelMode, mapRef.current, ]); // Added mapRef.current dep if (loadError) { // ... (Error state same as before) return (

Failed to load Google Maps

Check API key in Settings

); } if (!isLoaded) { return (
); } return (
{/* @ts-ignore */}
); } ); RouteMapContent.displayName = 'RouteMapContent'; /** * Specialized route display and calculation component for Google Maps. * * @description * Calculates and renders a route between an `origin` and `destination` point * using the Google Directions Service. Supports intermediate `waypoints` and * multiple `travelMode` options (DRIVING, WALKING, BICYCLING, TRANSIT). * * @ai-rules * 1. Provide `origin` and `destination` as LatLng objects `{ lat, lng }`. * 2. Use `onRouteCalculated` to receive the pre-formatted distance and duration strings. * 3. `travelMode` accepts 'DRIVING', 'WALKING', 'BICYCLING', or 'TRANSIT'. * 4. The Directions API must be enabled on your Google Maps API key. */ export const RouteMap = React.forwardRef((props, ref) => { const { isLoaded, loadError } = useGoogleMapsLoader(); const effectiveApiKey = props.apiKey || (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_GOOGLE_MAPS_API_KEY) || ''; const isValidKey = effectiveApiKey && effectiveApiKey !== 'YOUR_GOOGLE_MAPS_API_KEY_HERE' && effectiveApiKey.startsWith('AIza'); if (isLoaded || isValidKey || loadError) { return ; } // Check if the script is injected in the DOM (loading via provider) const isScriptInjected = typeof document !== 'undefined' && !!document.querySelector('script[src*="maps.googleapis.com/maps/api/js"]'); if (isScriptInjected) { return ; } const { origin, destination, waypoints, travelMode, height, apiKey, mapContainerClassName, disableDefaultUI, zoomControl, streetViewControl, mapTypeControl, fullscreenControl, onRouteCalculated, ...divProps } = props; return (

Configure Google Maps API Key in Settings

); }); RouteMap.displayName = 'RouteMap';