diff --git a/frontend/src/composables/useDirectionsRoute.ts b/frontend/src/composables/useDirectionsRoute.ts index bd92f1b..32918b9 100644 --- a/frontend/src/composables/useDirectionsRoute.ts +++ b/frontend/src/composables/useDirectionsRoute.ts @@ -13,15 +13,15 @@ export interface Parada { export function useDirectionsRoute() { const estasCargando = ref(false); const errorRuta = ref(null); - const { registrarRenderer, renderers } = useMapState(); + const { registrarPolyline, polylines } = useMapState(); // Limpia los tramos anteriores dibujados en el mapa const limpiarRuta = () => { - if (renderers.value.length > 0) { - renderers.value.forEach((renderer) => { - renderer.setMap(null); + if (polylines.value.length > 0) { + polylines.value.forEach((polyline) => { + polyline.setMap(null); }); - renderers.value = []; + polylines.value = []; } errorRuta.value = null; }; @@ -40,64 +40,81 @@ export function useDirectionsRoute() { errorRuta.value = null; try { - const directionsService = new google.maps.DirectionsService(); - // Límite de la API de Google Maps: Origen, Destino, y hasta 23 waypoints (25 puntos total por request) + // Importar librerías necesarias de la nueva API + const { Route } = await google.maps.importLibrary("routes") as any; + + // Límite de la API de Google Maps Routes: Origen, Destino, y hasta 25 intermediates const maxPuntosPorChunk = 25; const overlaps = 1; - // Iteramos sobre las paradas dividiéndolas en chunks con 1 punto en común ("overlap") - // para asegurar que las secciones se conecten correctamente. for (let i = 0; i < paradas.length - 1; i += (maxPuntosPorChunk - overlaps)) { const chunk = paradas.slice(i, i + maxPuntosPorChunk); - - // Si el chunk es muy pequeño (último fragmento o vector final), detenemos if (chunk.length < 2) break; - const origen = new google.maps.LatLng(chunk[0]!.latitud, chunk[0]!.longitud); - const destino = new google.maps.LatLng(chunk[chunk.length - 1]!.latitud, chunk[chunk.length - 1]!.longitud); - - // Excluimos el primero y último para que sean los waypoints intermedios - const waypoints: google.maps.DirectionsWaypoint[] = chunk.slice(1, -1).map(p => ({ - location: new google.maps.LatLng(p.latitud, p.longitud), - stopover: true - })); - - const request: google.maps.DirectionsRequest = { - origin: origen, - destination: destino, - waypoints: waypoints, - travelMode: google.maps.TravelMode.DRIVING, - optimizeWaypoints: false + const origin = { + location: { + latLng: { + latitude: chunk[0]!.latitud, + longitude: chunk[0]!.longitud + } + } }; - try { - const response = await directionsService.route(request); - - const renderer = new google.maps.DirectionsRenderer({ - map: map, - suppressMarkers: true, // SIBU maneja los suyos propios - preserveViewport: true, // No auto centrar en cada tramo para evitar parpadeos visuales - polylineOptions: isPast ? { - strokeColor: '#FDE68A', // amarillo muy tenue - strokeWeight: 3, - strokeOpacity: 0.4 - } : { - strokeColor: '#FBBF24', // amarillo principal - strokeWeight: 5, - strokeOpacity: 0.95 + const destination = { + location: { + latLng: { + latitude: chunk[chunk.length - 1]!.latitud, + longitude: chunk[chunk.length - 1]!.longitud } + } + }; + + const intermediates = chunk.slice(1, -1).map(p => ({ + location: { + latLng: { + latitude: p.latitud, + longitude: p.longitud + } + } + })); + + try { + const response = await Route.computeRoutes({ + origin, + destination, + intermediates, + travelMode: 'DRIVE' as any, // 'DRIVE' es el nuevo estandar en computeRoutes + routingPreference: 'TRAFFIC_UNAWARE' as any, + polylineQuality: 'HIGH_QUALITY' as any, + polylineEncoding: 'ENCODED_POLYLINE' as any, }); - renderer.setDirections(response); - registrarRenderer(renderer); + if (response.routes && response.routes.length > 0) { + const route = response.routes[0]; + if (route.polyline && route.polyline.encodedPolyline) { + const path = google.maps.geometry.encoding.decodePath(route.polyline.encodedPolyline); + const polyline = new google.maps.Polyline({ + path: path, + map: map, + strokeColor: isPast ? '#FDE68A' : '#FBBF24', + strokeWeight: isPast ? 3 : 5, + strokeOpacity: isPast ? 0.4 : 0.95, + icons: isPast ? [{ + icon: { path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 2 }, + offset: '0', + repeat: '10px' + }] : [] + }); + + registrarPolyline(polyline); + } + } } catch (err: any) { - console.warn(`SIBU | Tramo ${i} falló: `, err); - // La ruta continúa renderizando los siguientes tramos disponibles, no paramos todo. + console.warn(`SIBU | Tramo ${i} falló con Routes API: `, err); } - // Retardo para evitar sobrecargar a la API y el error "OVER_QUERY_LIMIT" - await delay(300); + await delay(200); } } catch (err: any) { errorRuta.value = `Error crítico al trazar la ruta: ${err.message || String(err)}`; diff --git a/frontend/src/composables/useGoogleMaps.ts b/frontend/src/composables/useGoogleMaps.ts index bd3c2b5..f5b3210 100644 --- a/frontend/src/composables/useGoogleMaps.ts +++ b/frontend/src/composables/useGoogleMaps.ts @@ -16,7 +16,6 @@ export function useGoogleMaps() { const error = ref(null) const { registrarMarker, - registrarRenderer, registrarPolyline, registrarCallbackLimpieza, limpiarMapa: limpiarTodoCentralizado @@ -58,6 +57,7 @@ export function useGoogleMaps() { await importLibrary('maps'); await importLibrary('places'); await importLibrary('geometry'); + await importLibrary('routes'); if (typeof google === 'undefined' || !google.maps) { throw new Error('Google Maps se cargó pero el espacio de nombres "google.maps" no está disponible.'); @@ -398,76 +398,104 @@ export function useGoogleMaps() { async function addRoutePolyline(paradas: Array<{ lat: number; lng: number }>) { if (!map.value) { - console.error('Map not initialized') - return [] + console.error('Map not initialized'); + return []; } if (!paradas || paradas.length < 2) { console.warn("Se necesitan al menos 2 paradas para trazar una ruta."); - return [] + return []; } - // Limpiar antes de dibujar una nueva ruta para evitar acumulación - limpiarTodoCentralizado() + // Limpiar antes de dibujar una nueva ruta + limpiarTodoCentralizado(); if (map.value && globalOverlays.has(map.value)) { - clearAllOverlaysForMap(map.value) + clearAllOverlaysForMap(map.value); } - const directionsService = new google.maps.DirectionsService(); - const renderizadoresActivos: google.maps.DirectionsRenderer[] = []; - const tamañoChunk = 25; + const polylinesCreadas: google.maps.Polyline[] = []; 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; + try { + // Cargar ruta + const { Route } = await google.maps.importLibrary("routes") as any; - 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 tamañoChunk = 25; - const waypoints = chunk.slice(1, -1).map(p => ({ - location: { lat: p.lat, lng: p.lng }, - stopover: true - })); + 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 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 + const origin = { + location: { + latLng: { + latitude: chunk[0]!.lat, + longitude: chunk[0]!.lng + } } - }); + }; + const destination = { + location: { + latLng: { + latitude: chunk[chunk.length - 1]!.lat, + longitude: chunk[chunk.length - 1]!.lng + } + } + }; + const intermediates = chunk.slice(1, -1).map(p => ({ + location: { + latLng: { + latitude: p.lat, + longitude: p.lng + } + } + })); - renderer.setDirections(response); - renderizadoresActivos.push(renderer); - registrarRenderer(renderer); // Registrar para limpieza centralizada + try { + const response = await Route.computeRoutes({ + origin, + destination, + intermediates, + travelMode: 'DRIVE', + routingPreference: 'TRAFFIC_UNAWARE', + polylineQuality: 'HIGH_QUALITY', + polylineEncoding: 'ENCODED_POLYLINE', + }); - // Registrar en global overlays para limpiarlos después - if (!globalOverlays.has(map.value)) { - globalOverlays.set(map.value, new Set()) + if (response.routes && response.routes.length > 0) { + const route = response.routes[0]; + if (route.polyline && route.polyline.encodedPolyline) { + const path = google.maps.geometry.encoding.decodePath(route.polyline.encodedPolyline); + + const polyline = new google.maps.Polyline({ + path: path, + map: map.value, + strokeColor: '#FBBF24', + strokeWeight: 5, + strokeOpacity: 0.95, + geodesic: true + }); + + polylinesCreadas.push(polyline); + registrarPolyline(polyline); + + // Registrar en global overlays + if (!globalOverlays.has(map.value)) { + globalOverlays.set(map.value, new Set()); + } + globalOverlays.get(map.value)!.add(polyline); + } + } + } catch (error) { + console.error(`Error trazando el tramo (Paradas ${i} a ${i + chunk.length}):`, error); } - 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); } - - await delay(200); + } catch (e) { + console.error('Error cargando Routes API:', e); } - return renderizadoresActivos; + + return polylinesCreadas; } function fitBounds(path: Array<{ lat: number; lng: number }>) { diff --git a/frontend/src/composables/useMapState.ts b/frontend/src/composables/useMapState.ts index 7867d35..45926f7 100644 --- a/frontend/src/composables/useMapState.ts +++ b/frontend/src/composables/useMapState.ts @@ -2,7 +2,6 @@ import { ref } from 'vue' // Registro global de todo lo que está en el mapa const markers = ref([]) -const renderers = ref([]) const polylines = ref([]) const infoWindows = ref([]) const circles = ref([]) @@ -15,11 +14,7 @@ export const useMapState = () => { return marker } - // Registrar un renderer - const registrarRenderer = (renderer: google.maps.DirectionsRenderer) => { - if (renderer) renderers.value.push(renderer) - return renderer - } + // Registrar una polyline const registrarPolyline = (polyline: google.maps.Polyline) => { @@ -48,7 +43,7 @@ export const useMapState = () => { // ⚠️ FUNCIÓN CRÍTICA: limpiar ABSOLUTAMENTE TODO del mapa const limpiarMapa = () => { - console.log(`SIBU | Iniciando limpieza de ${markers.value.length} markers, ${renderers.value.length} renderers, ${polylines.value.length} polylines...`) + console.log(`SIBU | Iniciando limpieza de ${markers.value.length} markers, ${polylines.value.length} polylines...`) // Eliminar markers y overlays HTML markers.value.forEach(m => { @@ -67,18 +62,7 @@ export const useMapState = () => { }) markers.value = [] - // Eliminar renderers de Directions - renderers.value.forEach(r => { - try { - if (r) { - if (typeof r.setMap === 'function') r.setMap(null); - if (typeof r.setDirections === 'function') r.setDirections({ routes: [] } as any); - } - } catch (e) { - console.warn('Error limpiando renderer', e) - } - }) - renderers.value = [] + // Eliminar polylines polylines.value.forEach(p => { @@ -125,12 +109,10 @@ export const useMapState = () => { return { markers, - renderers, polylines, infoWindows, circles, registrarMarker, - registrarRenderer, registrarPolyline, registrarCircle, registrarInfoWindow, diff --git a/frontend/src/composables/useParadaCercana.ts b/frontend/src/composables/useParadaCercana.ts index 57c80f7..442ebe3 100644 --- a/frontend/src/composables/useParadaCercana.ts +++ b/frontend/src/composables/useParadaCercana.ts @@ -49,45 +49,70 @@ export function useParadaCercana() { paradasConDistLineal.sort((a, b) => a.distancia - b.distancia); const top5 = paradasConDistLineal.slice(0, 5).map(item => item.parada); - // 2. Usar Directions API para encontrar la más cercana por calles reales + // 2. Usar Routes API para encontrar la más cercana por calles reales let mejorParada: BusStop | null = null; let minimaDistanciaCalles = Infinity; let mejorDuracion = 0; let mejorRutaPuntos: google.maps.LatLng[] = []; - const directionsService = new google.maps.DirectionsService(); + try { + const { Route } = await google.maps.importLibrary("routes") as any; - for (const stop of top5) { - try { - const response = await directionsService.route({ - origin: new google.maps.LatLng(ubicacionUsuario.lat, ubicacionUsuario.lng), - destination: new google.maps.LatLng(stop.latitude, stop.longitude), - travelMode: google.maps.TravelMode.DRIVING // Calles reales - }); + for (const stop of top5) { + try { + const response = await Route.computeRoutes({ + origin: { + location: { + latLng: { + latitude: ubicacionUsuario.lat, + longitude: ubicacionUsuario.lng + } + } + }, + destination: { + location: { + latLng: { + latitude: stop.latitude, + longitude: stop.longitude + } + } + }, + travelMode: 'DRIVE', + routingPreference: 'TRAFFIC_UNAWARE', + polylineQuality: 'HIGH_QUALITY', + polylineEncoding: 'ENCODED_POLYLINE', + }); - if (response.routes && response.routes.length > 0) { - const route = response.routes[0]; - if (!route) continue; - let distTotal = 0; - let durTotal = 0; + if (response.routes && response.routes.length > 0) { + const route = response.routes[0]; + let distTotal = 0; + let durTotal = 0; - if (route.legs) { - for (const leg of route.legs) { - distTotal += leg.distance?.value || 0; - durTotal += leg.duration?.value || 0; + if (route.distanceMeters) { + distTotal = route.distanceMeters; + } + + if (route.duration) { + // La duración viene como string "123s" + durTotal = parseInt(route.duration); + } + + if (distTotal < minimaDistanciaCalles) { + minimaDistanciaCalles = distTotal; + mejorDuracion = durTotal; + mejorParada = stop; + + if (route.polyline && route.polyline.encodedPolyline) { + mejorRutaPuntos = google.maps.geometry.encoding.decodePath(route.polyline.encodedPolyline); + } } } - - if (distTotal < minimaDistanciaCalles) { - minimaDistanciaCalles = distTotal; - mejorDuracion = durTotal; - mejorParada = stop; - mejorRutaPuntos = route.overview_path; - } + } catch (e) { + console.warn('Error calculando ruta a parada', stop.name, e); } - } catch (e) { - console.warn('Error calculando ruta a parada', stop.name, e); } + } catch (e) { + console.error('Error cargando Routes API en useParadaCercana', e); } // 3. Fallback a la más cercana lineal si falla API diff --git a/frontend/src/views/MapView.vue b/frontend/src/views/MapView.vue index 9545b5c..1c51d5b 100644 --- a/frontend/src/views/MapView.vue +++ b/frontend/src/views/MapView.vue @@ -642,7 +642,7 @@ function drawInternalWalkingRoute(targetStop: BusStop, originOverride?: { lat: n } } -function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop: BusStop) { +async function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop: BusStop) { // 1. Limpiar pulso anterior si existe if (optimalStopPulse.value) { if (typeof optimalStopPulse.value.setMap === 'function') { @@ -678,58 +678,71 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop: } // 2. Trazar línea de puntos verde siguiendo RED VIAL PRINCIPAL - const directionsService = new google.maps.DirectionsService(); - directionsService.route({ - origin: origin, - destination: { lat: targetStop.latitude, lng: targetStop.longitude }, - travelMode: google.maps.TravelMode.DRIVING, - }, (dirResult, dirStatus) => { - if (dirStatus === 'OK' && dirResult && dirResult.routes && dirResult.routes[0]) { - const route = dirResult.routes[0]; - const leg = route.legs?.[0]; + try { + const { Route } = await google.maps.importLibrary("routes") as any; + const response = await Route.computeRoutes({ + origin: { + location: { + latLng: { latitude: origin.lat, longitude: origin.lng } + } + }, + destination: { + location: { + latLng: { latitude: targetStop.latitude, longitude: targetStop.longitude } + } + }, + travelMode: 'DRIVE', + routingPreference: 'TRAFFIC_UNAWARE', + polylineQuality: 'HIGH_QUALITY', + polylineEncoding: 'ENCODED_POLYLINE', + }); - // Guardar info de navegación (ETA y Distancia) (Retirado a favor de ETA Card / Parada Cercana Banner) - if (leg) { - // console.log('Distancia', leg.distance?.text); - } - - if (walkingPolyline.value) walkingPolyline.value.setMap(null); - if (walkingPolylineBorder.value) walkingPolylineBorder.value.setMap(null); + if (response.routes && response.routes.length > 0) { + const route = response.routes[0]; - const { registrarPolyline: regPoly } = useMapState(); + if (route.polyline && route.polyline.encodedPolyline) { + const path = google.maps.geometry.encoding.decodePath(route.polyline.encodedPolyline); - // CAPA 1: Borde blanco (Para dar contraste estilo Google Maps) - walkingPolylineBorder.value = new google.maps.Polyline({ - path: route.overview_path, - geodesic: true, - strokeColor: '#FFFFFF', - strokeOpacity: 0.9, - strokeWeight: 10, // Un poco más grueso para el borde - map: map.value, - zIndex: 5 - }); - regPoly(walkingPolylineBorder.value); + if (walkingPolyline.value) walkingPolyline.value.setMap(null); + if (walkingPolylineBorder.value) walkingPolylineBorder.value.setMap(null); + + const { registrarPolyline: regPoly } = useMapState(); - // CAPA 2: Línea Indigo Central (La ruta principal) - walkingPolyline.value = new google.maps.Polyline({ - path: route.overview_path, - geodesic: true, - strokeColor: '#4285F4', // Azul Google Maps - strokeOpacity: 1.0, - strokeWeight: 5, - map: map.value, - zIndex: 10 - }); - regPoly(walkingPolyline.value); + // CAPA 1: Borde blanco (Para dar contraste estilo Google Maps) + walkingPolylineBorder.value = new google.maps.Polyline({ + path: path, + geodesic: true, + strokeColor: '#FFFFFF', + strokeOpacity: 0.9, + strokeWeight: 10, + map: map.value, + zIndex: 5 + }); + regPoly(walkingPolylineBorder.value); - // Ajustar zoom para mostrar toda la ruta de caminata - if (map.value) { - const bounds = new google.maps.LatLngBounds(); - route.overview_path.forEach(p => bounds.extend(p)); - map.value.fitBounds(bounds, { top: 100, bottom: 200, left: 50, right: 50 }); + // CAPA 2: Línea Indigo Central (La ruta principal) + walkingPolyline.value = new google.maps.Polyline({ + path: path, + geodesic: true, + strokeColor: '#4285F4', // Azul Google Maps + strokeOpacity: 1.0, + strokeWeight: 5, + map: map.value, + zIndex: 10 + }); + regPoly(walkingPolyline.value); + + // Ajustar zoom para mostrar toda la ruta de caminata + if (map.value) { + const bounds = new google.maps.LatLngBounds(); + path.forEach(p => bounds.extend(p)); + map.value.fitBounds(bounds, { top: 100, bottom: 200, left: 50, right: 50 }); + } } } - }); + } catch (error) { + console.warn('SIBU | Error trazando ruta a pie con Routes API:', error); + } }