chore: migrate Google Maps to Routes API and clean up deprecated code

This commit is contained in:
2026-02-27 22:15:58 -05:00
parent 8084032f25
commit d73926cd77
5 changed files with 257 additions and 192 deletions

View File

@ -13,15 +13,15 @@ export interface Parada {
export function useDirectionsRoute() { export function useDirectionsRoute() {
const estasCargando = ref<boolean>(false); const estasCargando = ref<boolean>(false);
const errorRuta = ref<string | null>(null); const errorRuta = ref<string | null>(null);
const { registrarRenderer, renderers } = useMapState(); const { registrarPolyline, polylines } = useMapState();
// Limpia los tramos anteriores dibujados en el mapa // Limpia los tramos anteriores dibujados en el mapa
const limpiarRuta = () => { const limpiarRuta = () => {
if (renderers.value.length > 0) { if (polylines.value.length > 0) {
renderers.value.forEach((renderer) => { polylines.value.forEach((polyline) => {
renderer.setMap(null); polyline.setMap(null);
}); });
renderers.value = []; polylines.value = [];
} }
errorRuta.value = null; errorRuta.value = null;
}; };
@ -40,64 +40,81 @@ export function useDirectionsRoute() {
errorRuta.value = null; errorRuta.value = null;
try { try {
const directionsService = new google.maps.DirectionsService(); // Importar librerías necesarias de la nueva API
// Límite de la API de Google Maps: Origen, Destino, y hasta 23 waypoints (25 puntos total por request) 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 maxPuntosPorChunk = 25;
const overlaps = 1; 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)) { for (let i = 0; i < paradas.length - 1; i += (maxPuntosPorChunk - overlaps)) {
const chunk = paradas.slice(i, i + maxPuntosPorChunk); 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; if (chunk.length < 2) break;
const origen = new google.maps.LatLng(chunk[0]!.latitud, chunk[0]!.longitud); const origin = {
const destino = new google.maps.LatLng(chunk[chunk.length - 1]!.latitud, chunk[chunk.length - 1]!.longitud); location: {
latLng: {
// Excluimos el primero y último para que sean los waypoints intermedios latitude: chunk[0]!.latitud,
const waypoints: google.maps.DirectionsWaypoint[] = chunk.slice(1, -1).map(p => ({ longitude: chunk[0]!.longitud
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
}; };
try { const destination = {
const response = await directionsService.route(request); location: {
latLng: {
const renderer = new google.maps.DirectionsRenderer({ latitude: chunk[chunk.length - 1]!.latitud,
map: map, longitude: chunk[chunk.length - 1]!.longitud
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 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); if (response.routes && response.routes.length > 0) {
registrarRenderer(renderer); 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) { } catch (err: any) {
console.warn(`SIBU | Tramo ${i} falló: `, err); console.warn(`SIBU | Tramo ${i} falló con Routes API: `, err);
// La ruta continúa renderizando los siguientes tramos disponibles, no paramos todo.
} }
// Retardo para evitar sobrecargar a la API y el error "OVER_QUERY_LIMIT" await delay(200);
await delay(300);
} }
} catch (err: any) { } catch (err: any) {
errorRuta.value = `Error crítico al trazar la ruta: ${err.message || String(err)}`; errorRuta.value = `Error crítico al trazar la ruta: ${err.message || String(err)}`;

View File

@ -16,7 +16,6 @@ export function useGoogleMaps() {
const error = ref<string | null>(null) const error = ref<string | null>(null)
const { const {
registrarMarker, registrarMarker,
registrarRenderer,
registrarPolyline, registrarPolyline,
registrarCallbackLimpieza, registrarCallbackLimpieza,
limpiarMapa: limpiarTodoCentralizado limpiarMapa: limpiarTodoCentralizado
@ -58,6 +57,7 @@ export function useGoogleMaps() {
await importLibrary('maps'); await importLibrary('maps');
await importLibrary('places'); await importLibrary('places');
await importLibrary('geometry'); await importLibrary('geometry');
await importLibrary('routes');
if (typeof google === 'undefined' || !google.maps) { if (typeof google === 'undefined' || !google.maps) {
throw new Error('Google Maps se cargó pero el espacio de nombres "google.maps" no está disponible.'); 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 }>) { async function addRoutePolyline(paradas: Array<{ lat: number; lng: number }>) {
if (!map.value) { if (!map.value) {
console.error('Map not initialized') console.error('Map not initialized');
return [] return [];
} }
if (!paradas || paradas.length < 2) { if (!paradas || paradas.length < 2) {
console.warn("Se necesitan al menos 2 paradas para trazar una ruta."); 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 // Limpiar antes de dibujar una nueva ruta
limpiarTodoCentralizado() limpiarTodoCentralizado();
if (map.value && globalOverlays.has(map.value)) { if (map.value && globalOverlays.has(map.value)) {
clearAllOverlaysForMap(map.value) clearAllOverlaysForMap(map.value);
} }
const directionsService = new google.maps.DirectionsService(); const polylinesCreadas: google.maps.Polyline[] = [];
const renderizadoresActivos: google.maps.DirectionsRenderer[] = [];
const tamañoChunk = 25;
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
for (let i = 0; i < paradas.length - 1; i += (tamañoChunk - 1)) { try {
const chunk = paradas.slice(i, i + tamañoChunk); // Cargar ruta
if (chunk.length < 2) break; const { Route } = await google.maps.importLibrary("routes") as any;
const origen = { lat: chunk[0]!.lat, lng: chunk[0]!.lng }; const tamañoChunk = 25;
const destino = { lat: chunk[chunk.length - 1]!.lat, lng: chunk[chunk.length - 1]!.lng };
const waypoints = chunk.slice(1, -1).map(p => ({ for (let i = 0; i < paradas.length - 1; i += (tamañoChunk - 1)) {
location: { lat: p.lat, lng: p.lng }, const chunk = paradas.slice(i, i + tamañoChunk);
stopover: true if (chunk.length < 2) break;
}));
const request = { const origin = {
origin: origen, location: {
destination: destino, latLng: {
waypoints: waypoints, latitude: chunk[0]!.lat,
travelMode: google.maps.TravelMode.DRIVING, longitude: chunk[0]!.lng
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 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); try {
renderizadoresActivos.push(renderer); const response = await Route.computeRoutes({
registrarRenderer(renderer); // Registrar para limpieza centralizada origin,
destination,
intermediates,
travelMode: 'DRIVE',
routingPreference: 'TRAFFIC_UNAWARE',
polylineQuality: 'HIGH_QUALITY',
polylineEncoding: 'ENCODED_POLYLINE',
});
// Registrar en global overlays para limpiarlos después if (response.routes && response.routes.length > 0) {
if (!globalOverlays.has(map.value)) { const route = response.routes[0];
globalOverlays.set(map.value, new Set()) 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) { await delay(200);
console.error(`Error trazando el tramo (Paradas ${i} a ${i + chunk.length}):`, error);
} }
} catch (e) {
await delay(200); console.error('Error cargando Routes API:', e);
} }
return renderizadoresActivos;
return polylinesCreadas;
} }
function fitBounds(path: Array<{ lat: number; lng: number }>) { function fitBounds(path: Array<{ lat: number; lng: number }>) {

View File

@ -2,7 +2,6 @@ import { ref } from 'vue'
// Registro global de todo lo que está en el mapa // Registro global de todo lo que está en el mapa
const markers = ref<google.maps.Marker[]>([]) const markers = ref<google.maps.Marker[]>([])
const renderers = ref<google.maps.DirectionsRenderer[]>([])
const polylines = ref<google.maps.Polyline[]>([]) const polylines = ref<google.maps.Polyline[]>([])
const infoWindows = ref<google.maps.InfoWindow[]>([]) const infoWindows = ref<google.maps.InfoWindow[]>([])
const circles = ref<google.maps.Circle[]>([]) const circles = ref<google.maps.Circle[]>([])
@ -15,11 +14,7 @@ export const useMapState = () => {
return marker return marker
} }
// Registrar un renderer
const registrarRenderer = (renderer: google.maps.DirectionsRenderer) => {
if (renderer) renderers.value.push(renderer)
return renderer
}
// Registrar una polyline // Registrar una polyline
const registrarPolyline = (polyline: google.maps.Polyline) => { const registrarPolyline = (polyline: google.maps.Polyline) => {
@ -48,7 +43,7 @@ export const useMapState = () => {
// ⚠️ FUNCIÓN CRÍTICA: limpiar ABSOLUTAMENTE TODO del mapa // ⚠️ FUNCIÓN CRÍTICA: limpiar ABSOLUTAMENTE TODO del mapa
const limpiarMapa = () => { 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 // Eliminar markers y overlays HTML
markers.value.forEach(m => { markers.value.forEach(m => {
@ -67,18 +62,7 @@ export const useMapState = () => {
}) })
markers.value = [] 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 // Eliminar polylines
polylines.value.forEach(p => { polylines.value.forEach(p => {
@ -125,12 +109,10 @@ export const useMapState = () => {
return { return {
markers, markers,
renderers,
polylines, polylines,
infoWindows, infoWindows,
circles, circles,
registrarMarker, registrarMarker,
registrarRenderer,
registrarPolyline, registrarPolyline,
registrarCircle, registrarCircle,
registrarInfoWindow, registrarInfoWindow,

View File

@ -49,45 +49,70 @@ export function useParadaCercana() {
paradasConDistLineal.sort((a, b) => a.distancia - b.distancia); paradasConDistLineal.sort((a, b) => a.distancia - b.distancia);
const top5 = paradasConDistLineal.slice(0, 5).map(item => item.parada); 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 mejorParada: BusStop | null = null;
let minimaDistanciaCalles = Infinity; let minimaDistanciaCalles = Infinity;
let mejorDuracion = 0; let mejorDuracion = 0;
let mejorRutaPuntos: google.maps.LatLng[] = []; 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) { for (const stop of top5) {
try { try {
const response = await directionsService.route({ const response = await Route.computeRoutes({
origin: new google.maps.LatLng(ubicacionUsuario.lat, ubicacionUsuario.lng), origin: {
destination: new google.maps.LatLng(stop.latitude, stop.longitude), location: {
travelMode: google.maps.TravelMode.DRIVING // Calles reales 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) { if (response.routes && response.routes.length > 0) {
const route = response.routes[0]; const route = response.routes[0];
if (!route) continue; let distTotal = 0;
let distTotal = 0; let durTotal = 0;
let durTotal = 0;
if (route.legs) { if (route.distanceMeters) {
for (const leg of route.legs) { distTotal = route.distanceMeters;
distTotal += leg.distance?.value || 0; }
durTotal += leg.duration?.value || 0;
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);
}
} }
} }
} catch (e) {
if (distTotal < minimaDistanciaCalles) { console.warn('Error calculando ruta a parada', stop.name, e);
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.error('Error cargando Routes API en useParadaCercana', e);
} }
// 3. Fallback a la más cercana lineal si falla API // 3. Fallback a la más cercana lineal si falla API

View File

@ -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 // 1. Limpiar pulso anterior si existe
if (optimalStopPulse.value) { if (optimalStopPulse.value) {
if (typeof optimalStopPulse.value.setMap === 'function') { 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 // 2. Trazar línea de puntos verde siguiendo RED VIAL PRINCIPAL
const directionsService = new google.maps.DirectionsService(); try {
directionsService.route({ const { Route } = await google.maps.importLibrary("routes") as any;
origin: origin, const response = await Route.computeRoutes({
destination: { lat: targetStop.latitude, lng: targetStop.longitude }, origin: {
travelMode: google.maps.TravelMode.DRIVING, location: {
}, (dirResult, dirStatus) => { latLng: { latitude: origin.lat, longitude: origin.lng }
if (dirStatus === 'OK' && dirResult && dirResult.routes && dirResult.routes[0]) { }
const route = dirResult.routes[0]; },
const leg = route.legs?.[0]; 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 (response.routes && response.routes.length > 0) {
if (leg) { const route = response.routes[0];
// console.log('Distancia', leg.distance?.text);
}
if (walkingPolyline.value) walkingPolyline.value.setMap(null); if (route.polyline && route.polyline.encodedPolyline) {
if (walkingPolylineBorder.value) walkingPolylineBorder.value.setMap(null); const path = google.maps.geometry.encoding.decodePath(route.polyline.encodedPolyline);
const { registrarPolyline: regPoly } = useMapState(); if (walkingPolyline.value) walkingPolyline.value.setMap(null);
if (walkingPolylineBorder.value) walkingPolylineBorder.value.setMap(null);
// CAPA 1: Borde blanco (Para dar contraste estilo Google Maps) const { registrarPolyline: regPoly } = useMapState();
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);
// CAPA 2: Línea Indigo Central (La ruta principal) // CAPA 1: Borde blanco (Para dar contraste estilo Google Maps)
walkingPolyline.value = new google.maps.Polyline({ walkingPolylineBorder.value = new google.maps.Polyline({
path: route.overview_path, path: path,
geodesic: true, geodesic: true,
strokeColor: '#4285F4', // Azul Google Maps strokeColor: '#FFFFFF',
strokeOpacity: 1.0, strokeOpacity: 0.9,
strokeWeight: 5, strokeWeight: 10,
map: map.value, map: map.value,
zIndex: 10 zIndex: 5
}); });
regPoly(walkingPolyline.value); regPoly(walkingPolylineBorder.value);
// Ajustar zoom para mostrar toda la ruta de caminata // CAPA 2: Línea Indigo Central (La ruta principal)
if (map.value) { walkingPolyline.value = new google.maps.Polyline({
const bounds = new google.maps.LatLngBounds(); path: path,
route.overview_path.forEach(p => bounds.extend(p)); geodesic: true,
map.value.fitBounds(bounds, { top: 100, bottom: 200, left: 50, right: 50 }); 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);
}
} }
</script> </script>