/** Composable for Google Maps integration */ import { ref, shallowRef, onMounted } from 'vue' import { setOptions, importLibrary } from '@googlemaps/js-api-loader' import { useMapState } from './useMapState' const getApiKey = () => import.meta.env.VITE_GOOGLE_MAPS_API_KEY || '' let mapsLoaded = false // Global overlay tracker - persists across all composable instances const globalOverlays = new Map>() export function useGoogleMaps() { const map = shallowRef(null) const isLoaded = ref(false) const error = ref(null) const { registrarMarker, registrarRenderer, registrarPolyline, registrarCallbackLimpieza, limpiarMapa: limpiarTodoCentralizado } = useMapState() // Escuchar errores globales de autenticación de Google if (typeof window !== 'undefined') { (window as any).gm_auth_failure = () => { error.value = '⚠️ Error de Autenticación de Google: Revisa que la API de Mapas esté activada y que la facturación de Google Cloud sea válida.'; console.error('❌ Google Maps Auth Failure detected'); }; } async function loadMaps() { if (mapsLoaded) { isLoaded.value = true error.value = null return } const apiKey = getApiKey() if (!apiKey || apiKey.length < 10) { error.value = '❌ Error: VITE_GOOGLE_MAPS_API_KEY no detectada o es inválida.' console.error(error.value) return } console.log('🌐 Usando Nueva API Funcional de Google Maps...'); try { // Configuramos las opciones globales como pide el error setOptions({ key: apiKey, v: 'weekly' }); // Cargamos las librerías necesarias una por una console.log('🛰️ Cargando librerías...'); await importLibrary('maps'); await importLibrary('places'); await importLibrary('geometry'); if (typeof google === 'undefined' || !google.maps) { throw new Error('Google Maps se cargó pero el espacio de nombres "google.maps" no está disponible.'); } mapsLoaded = true isLoaded.value = true error.value = null console.log('✅ Google Maps (New API) cargado con éxito'); } catch (e: any) { console.error('❌ Error crítico en Nueva API:', e) let msg = 'Error de carga.' const errStr = String(e).toLowerCase() if (errStr.includes('apiprojectmaperror')) { msg = 'Error de Proyecto: API no habilitada o llave incorrecta.' } else if (errStr.includes('billing')) { msg = 'Facturación: Revisa tu cuenta en Google Cloud Console.' } else if (errStr.includes('referer') || errStr.includes('origin')) { msg = 'Restricción de Origen: La llave no permite peticiones desde esta App.' } else { msg = `Detalle: ${e.message || e}` } error.value = `⚠️ Google Maps: ${msg}` } } function initMap( containerId: string, center: { lat: number; lng: number }, zoom: number = 12 ) { if (!isLoaded.value) { console.error('Google Maps not loaded yet') return } const container = document.getElementById(containerId) if (!container) { console.error(`Map container with id "${containerId}" not found`) return } // Clear any existing overlays for this map before creating a new one if (map.value && globalOverlays.has(map.value)) { clearAllOverlaysForMap(map.value) } try { map.value = new google.maps.Map(container, { center, zoom, disableDefaultUI: true, }) } catch (e: any) { console.error('❌ Error inicializando el objeto Map:', e); error.value = `Error de inicialización: ${e.message || e}`; } // Initialize overlay tracking for this map if (map.value && !globalOverlays.has(map.value)) { globalOverlays.set(map.value, new Set()) } // Registrar callback para limpiar globalOverlays cuando useMapState.limpiarMapa() sea llamado registrarCallbackLimpieza(() => { if (map.value && globalOverlays.has(map.value)) { clearAllOverlaysForMap(map.value) } }) } function addMarker( position: { lat: number; lng: number }, options?: { title?: string draggable?: boolean icon?: google.maps.Icon | google.maps.Symbol | string onDragEnd?: (pos: { lat: number; lng: number }) => void } ): google.maps.Marker | null { if (!map.value) { console.error('Map not initialized') return null } const marker = new google.maps.Marker({ position, map: map.value, title: options?.title, draggable: options?.draggable, icon: options?.icon, }) registrarMarker(marker) if (options?.onDragEnd) { marker.addListener('dragend', () => { const pos = marker.getPosition() if (pos) { options.onDragEnd!({ lat: pos.lat(), lng: pos.lng() }) } }) } // Track in global overlay tracker if (map.value) { if (!globalOverlays.has(map.value)) { globalOverlays.set(map.value, new Set()) } globalOverlays.get(map.value)!.add(marker) } return marker } function addNumberedMarker( position: { lat: number; lng: number }, number: number, title?: string, onClick?: () => void ): google.maps.Marker | null { if (!map.value) { console.error('Map not initialized') return null } // Note: google.maps.Marker is deprecated but still works // We'll keep using it for now as AdvancedMarkerElement requires additional setup // TODO: Migrate to google.maps.marker.AdvancedMarkerElement in the future const marker = new google.maps.Marker({ position, map: map.value, title, icon: { path: google.maps.SymbolPath.CIRCLE, fillColor: '#FEE715', // Amarillo marca fillOpacity: 1, strokeColor: '#101820', // Negro marca strokeWeight: 2, scale: 14, }, label: { text: number.toString(), color: '#101820', fontSize: '13px', fontWeight: '900', }, }) registrarMarker(marker) if (onClick) { marker.addListener('click', onClick) } // Track in global overlay tracker if (map.value) { if (!globalOverlays.has(map.value)) { globalOverlays.set(map.value, new Set()) } globalOverlays.get(map.value)!.add(marker) } return marker } function addCleanMarker( position: { lat: number; lng: number }, title: string, type: 'normal' | 'cercana' | 'origen' | 'destino' | 'pasada', onClick?: () => void ): google.maps.Marker | null { if (!map.value) { console.error('Map not initialized'); return null; } const iconoParadaNormal = { path: google.maps.SymbolPath.CIRCLE, fillColor: '#FBBF24', // amarillo fillOpacity: 1, strokeColor: '#FFFFFF', strokeWeight: 2, scale: 7 }; const iconoParadaCercana = { path: google.maps.SymbolPath.CIRCLE, fillColor: '#F59E0B', // amarillo intenso fillOpacity: 1, strokeColor: '#FFFFFF', strokeWeight: 3, scale: 12 }; const iconoOrigen = { path: google.maps.SymbolPath.CIRCLE, fillColor: '#FBBF24', // amarillo fillOpacity: 1, strokeColor: '#FFFFFF', strokeWeight: 3, scale: 10 }; const iconoDestino = { path: google.maps.SymbolPath.CIRCLE, fillColor: '#D97706', // amarillo oscuro fillOpacity: 1, strokeColor: '#FFFFFF', strokeWeight: 3, scale: 10 }; const iconoParadaPasada = { path: google.maps.SymbolPath.CIRCLE, fillColor: '#FDE68A', // amarillo tenue fillOpacity: 0.5, strokeColor: '#FFFFFF', strokeWeight: 1, scale: 5 }; const iconos = { normal: iconoParadaNormal, cercana: iconoParadaCercana, origen: iconoOrigen, destino: iconoDestino, pasada: iconoParadaPasada }; const marker = new google.maps.Marker({ position, map: map.value, title, icon: iconos[type], }); registrarMarker(marker); if (onClick) { const infoWindow = new google.maps.InfoWindow({ content: `
🚌 ${title}
` }); marker.addListener('click', () => { infoWindow.open(map.value, marker); onClick(); }); } if (map.value) { if (!globalOverlays.has(map.value)) { globalOverlays.set(map.value, new Set()); } globalOverlays.get(map.value)!.add(marker); } return marker; } function addHtmlMarker( position: { lat: number, lng: number }, html: string, offset: { x: number, y: number } = { x: 0, y: 0 } ) { if (!map.value) return null; const overlay = new google.maps.OverlayView(); let container: HTMLDivElement | null = null; overlay.onAdd = function () { container = document.createElement('div'); container.style.position = 'absolute'; container.innerHTML = html; const panes = this.getPanes(); if (panes) panes.overlayMouseTarget.appendChild(container); }; overlay.draw = function () { const projection = this.getProjection(); if (!projection || !container) return; const pos = projection.fromLatLngToDivPixel(new google.maps.LatLng(position.lat, position.lng)); if (pos) { container.style.left = (pos.x + offset.x) + 'px'; container.style.top = (pos.y + offset.y) + 'px'; } }; overlay.onRemove = function () { if (container && container.parentNode) { container.parentNode.removeChild(container); } container = null; }; overlay.setMap(map.value); registrarMarker(overlay as any); // Track in global overlay tracker if (!globalOverlays.has(map.value)) { globalOverlays.set(map.value, new Set()); } globalOverlays.get(map.value)!.add(overlay as any); return overlay; } function addPolyline(path: Array<{ lat: number; lng: number }>): google.maps.Polyline | null { if (!map.value) { console.error('Map not initialized') return null } const polyline = new google.maps.Polyline({ path, geodesic: true, strokeColor: '#101820', // Negro premium strokeOpacity: 0.8, strokeWeight: 5, map: map.value, }) registrarPolyline(polyline) // Track in global overlay tracker if (map.value) { if (!globalOverlays.has(map.value)) { globalOverlays.set(map.value, new Set()) } globalOverlays.get(map.value)!.add(polyline) } return polyline } async function addRoutePolyline(paradas: Array<{ lat: number; lng: number }>) { if (!map.value) { console.error('Map not initialized') return [] } if (!paradas || paradas.length < 2) { console.warn("Se necesitan al menos 2 paradas para trazar una ruta."); return [] } // Limpiar antes de dibujar una nueva ruta para evitar acumulación limpiarTodoCentralizado() if (map.value && globalOverlays.has(map.value)) { clearAllOverlaysForMap(map.value) } const directionsService = new google.maps.DirectionsService(); const renderizadoresActivos: google.maps.DirectionsRenderer[] = []; const tamañoChunk = 25; const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); for (let i = 0; i < paradas.length - 1; i += (tamañoChunk - 1)) { const chunk = paradas.slice(i, i + tamañoChunk); if (chunk.length < 2) break; const origen = { lat: chunk[0]!.lat, lng: chunk[0]!.lng }; const destino = { lat: chunk[chunk.length - 1]!.lat, lng: chunk[chunk.length - 1]!.lng }; const waypoints = chunk.slice(1, -1).map(p => ({ location: { lat: p.lat, lng: p.lng }, stopover: true })); const request = { origin: origen, destination: destino, waypoints: waypoints, travelMode: google.maps.TravelMode.DRIVING, optimizeWaypoints: false }; try { const response = await directionsService.route(request); const renderer = new google.maps.DirectionsRenderer({ map: map.value, suppressMarkers: true, preserveViewport: true, // Siempre conservar la vista ya que trazamos fragmentos polylineOptions: { strokeColor: '#FBBF24', // Amarillo consistente con paradas strokeWeight: 5, strokeOpacity: 0.95 } }); renderer.setDirections(response); renderizadoresActivos.push(renderer); registrarRenderer(renderer); // Registrar para limpieza centralizada // Registrar en global overlays para limpiarlos después if (!globalOverlays.has(map.value)) { globalOverlays.set(map.value, new Set()) } globalOverlays.get(map.value)!.add(renderer as any); } catch (error) { console.error(`Error trazando el tramo (Paradas ${i} a ${i + chunk.length}):`, error); } await delay(200); } return renderizadoresActivos; } function fitBounds(path: Array<{ lat: number; lng: number }>) { if (!map.value || path.length === 0) { return } const bounds = new google.maps.LatLngBounds() path.forEach((point) => { bounds.extend(new google.maps.LatLng(point.lat, point.lng)) }) map.value.fitBounds(bounds) } function setCenter(lat: number, lng: number) { if (map.value) { map.value.setCenter({ lat, lng }) } } function setZoom(zoom: number) { if (map.value) { map.value.setZoom(zoom) } } function clearAllOverlays() { if (!map.value) { return } limpiarTodoCentralizado() clearAllOverlaysForMap(map.value) } function clearAllOverlaysForMap(targetMap: google.maps.Map) { const overlays = globalOverlays.get(targetMap) // Remove all tracked overlays from the map if (overlays) { const overlayCount = overlays.size overlays.forEach((overlay) => { if (overlay) { try { if ('setMap' in overlay && typeof overlay.setMap === 'function') { overlay.setMap(null) } if ('remove' in overlay && typeof overlay.remove === 'function') { overlay.remove() } if ('onRemove' in overlay && typeof overlay.onRemove === 'function') { overlay.onRemove() } } catch (e) { // Ignore errors when removing overlays console.warn('Error removing overlay:', e) } } }) // Clear the set overlays.clear() console.log(`Cleared ${overlayCount} tracked overlays`) } // Manual DOM scraping fallback removed as it causes "removeChild" errors // with Google Maps' native OverlayView management. } onMounted(() => { loadMaps() }) return { map, isLoaded, error, loadMaps, initMap, addMarker, addHtmlMarker, addNumberedMarker, addCleanMarker, addPolyline, addRoutePolyline, fitBounds, setCenter, setZoom, clearAllOverlays, } }