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() {
const estasCargando = ref<boolean>(false);
const errorRuta = ref<string | null>(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)}`;

View File

@ -16,7 +16,6 @@ export function useGoogleMaps() {
const error = ref<string | null>(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 }>) {

View File

@ -2,7 +2,6 @@ import { ref } from 'vue'
// Registro global de todo lo que está en el mapa
const markers = ref<google.maps.Marker[]>([])
const renderers = ref<google.maps.DirectionsRenderer[]>([])
const polylines = ref<google.maps.Polyline[]>([])
const infoWindows = ref<google.maps.InfoWindow[]>([])
const circles = ref<google.maps.Circle[]>([])
@ -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,

View File

@ -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