diff --git a/frontend/src/composables/useETA.ts b/frontend/src/composables/useETA.ts index 569cbc2..b71e6b2 100644 --- a/frontend/src/composables/useETA.ts +++ b/frontend/src/composables/useETA.ts @@ -31,12 +31,46 @@ export function useETA() { return R * c; } - const calcularETA = async (ruta_id: string, parada_cercana: BusStop) => { + const calcularETA = async (ruta_id: string, parada_cercana: BusStop | null) => { cargando.value = true; busesActivos.value = []; try { - // PASO 1: Obtener detalles de la ruta y todas sus paradas para calcular distancia real + // OPTIMIZACIÓN: PASO 1 - Verificar horarios primero (más rápido que cálculos geo) + // Esto permite dar feedback instantáneo de "No hay buses" sin esperar a la ubicación + const diaActual = new Date().getDay(); + const dias = ['domingo', 'lunes', 'martes', 'miercoles', 'jueves', 'viernes', 'sabado']; + const diaString = dias[diaActual]; + const tipoDia = (diaActual === 0 || diaActual === 6) ? 'weekend' : 'weekday'; + + const { data: horarios, error: hError } = await supabase + .from('bus_schedules') + .select('id, departure_time, dias_operacion, schedule_type') + .eq('route_id', ruta_id) + .eq('is_active', true) + .eq('is_published', true); + + if (hError) throw hError; + + const horariosHoy = (horarios ?? []).filter(h => { + if (h.dias_operacion) { + return h.dias_operacion.includes('todos') || h.dias_operacion.includes(diaString); + } + return h.schedule_type === tipoDia || h.schedule_type === 'todos' || !h.schedule_type; + }); + + if (horariosHoy.length === 0) { + // No hay horarios hoy, no perdemos tiempo en geo + busesActivos.value = []; + cargando.value = false; + return; + } + + // Si no tenemos parada aún, no podemos calcular ETA real, pero sabemos que SÍ hay buses. + // Mantenemos el estado 'cargando' hasta que llegue la parada. + if (!parada_cercana) return; + + // PASO 2: Obtener detalles de la ruta y todas sus paradas para calcular distancia const [routeRes, stopsRes] = await Promise.all([ supabase .from('routes') @@ -59,69 +93,27 @@ export function useETA() { const targetStopIndex = routeStops.findIndex(s => s.stop_id === parada_cercana.id); if (targetStopIndex === -1) return; - // CALCULAR DISTANCIA ACUMULADA (Terminal hasta Parada Destino) - // Usamos Haversine entre cada parada consecutiva para mayor precisión que una línea recta total + // CALCULAR DISTANCIA ACUMULADA let distanciaAcumuladaKm = 0; for (let i = 0; i < targetStopIndex; i++) { const stopA = routeStops[i]; const stopB = routeStops[i + 1]; - const start = stopA ? (stopA.bus_stops as any) : null; const end = stopB ? (stopB.bus_stops as any) : null; - if (start?.latitude != null && start?.longitude != null && end?.latitude != null && end?.longitude != null) { - distanciaAcumuladaKm += getDistanceKm( - start.latitude, start.longitude, - end.latitude, end.longitude - ); + distanciaAcumuladaKm += getDistanceKm(start.latitude, start.longitude, end.latitude, end.longitude); } } - // Aplicar factor de corrección de ruta (las calles no son rectas, aprox +20%) distanciaAcumuladaKm *= 1.2; - - // PASO 2: Aplicar Fórmula Mejorada (Requerida por el usuario) - // ETA = (Distancia / Velocidad) + (Nº Paradas * Tiempo Parada) - const velocidad = routeData.average_speed_kmh || VELOCIDAD_PROMEDIO_KMH; - - // Tiempo de viaje (en minutos) const tiempoViajeMinutos = (distanciaAcumuladaKm / velocidad) * 60; - - // Tiempo total en paradas (N paradas previas × tiempo promedio) - // numeroParadas = stops antes de llegar a la parada destino const numeroParadas = targetStopIndex; const tiempoParadasMinutos = (numeroParadas * TIEMPO_PARADA_SEGUNDOS) / 60; - - // Tiempo total hasta la parada del usuario const minutosHastaParada = tiempoViajeMinutos + tiempoParadasMinutos; - console.log(`SIBU ETA | Ruta: ${ruta_id} | Dist: ${distanciaAcumuladaKm.toFixed(2)}km | Paradas: ${numeroParadas} | Total: ${minutosHastaParada.toFixed(1)}min`); - - // PASO 3: Obtener horarios activos para hoy - const diaActual = new Date().getDay(); - const dias = ['domingo', 'lunes', 'martes', 'miercoles', 'jueves', 'viernes', 'sabado']; - const diaString = dias[diaActual]; - const tipoDia = (diaActual === 0 || diaActual === 6) ? 'weekend' : 'weekday'; - - const { data: horarios, error: hError } = await supabase - .from('bus_schedules') - .select('id, departure_time, dias_operacion, schedule_type') - .eq('route_id', ruta_id) - .eq('is_active', true) - .eq('is_published', true); - - if (hError) throw hError; - - const horariosHoy = (horarios ?? []).filter(h => { - if (h.dias_operacion) { - return h.dias_operacion.includes('todos') || h.dias_operacion.includes(diaString); - } - return h.schedule_type === tipoDia || h.schedule_type === 'todos' || !h.schedule_type; - }); - - // PASO 4: Calcular ETA para cada salida desde la terminal + // PASO 3: Calcular ETA para cada salida const ahora = new Date(); const minutosAhora = ahora.getHours() * 60 + ahora.getMinutes(); const resultados: BusETA[] = []; @@ -132,20 +124,14 @@ export function useETA() { const [hStr, mStr] = salida.split(':'); const minutosSalida = parseInt(hStr) * 60 + parseInt(mStr); - - // Hora estimada de llegada a la parada del usuario: const minutosLlegadaParada = minutosSalida + minutosHastaParada; - - // ETA = cuántos minutos faltan desde ahora: const etaMinutos = minutosLlegadaParada - minutosAhora; - const horaLlegada = minutosAHora(minutosLlegadaParada); const horaSalidaFormato = `${hStr.padStart(2, '0')}:${mStr.padStart(2, '0')}`; let estado: BusETA['estado']; if (etaMinutos > 5) { - const yaPartio = minutosAhora >= minutosSalida; - estado = yaPartio ? 'en_camino' : 'próximo'; + estado = (minutosAhora >= minutosSalida) ? 'en_camino' : 'próximo'; } else if (etaMinutos >= -2) { estado = 'en_camino'; } else { @@ -165,14 +151,11 @@ export function useETA() { resultados.sort((a, b) => { const prioridad = { 'en_camino': 0, 'próximo': 1, 'pasó': 2 }; - if (prioridad[a.estado] !== prioridad[b.estado]) { - return prioridad[a.estado] - prioridad[b.estado]; - } + if (prioridad[a.estado] !== prioridad[b.estado]) return prioridad[a.estado] - prioridad[b.estado]; return a.etaMinutos - b.etaMinutos; }); busesActivos.value = resultados.slice(0, 3); - } catch (e) { console.error('SIBU | Error calculando ETA:', e); } finally { diff --git a/frontend/src/composables/useParadaCercana.ts b/frontend/src/composables/useParadaCercana.ts index bd0ca6b..e1636f3 100644 --- a/frontend/src/composables/useParadaCercana.ts +++ b/frontend/src/composables/useParadaCercana.ts @@ -59,24 +59,14 @@ export function useParadaCercana() { const { RoutesService } = await google.maps.importLibrary("routes") as any; const routeService = new RoutesService(); - for (const stop of top5) { + const routePromises = top5.map(async (stop) => { try { const response = await routeService.computeRoutes({ origin: { - location: { - latLng: { - latitude: ubicacionUsuario.lat, - longitude: ubicacionUsuario.lng - } - } + location: { latLng: { latitude: ubicacionUsuario.lat, longitude: ubicacionUsuario.lng } } }, destination: { - location: { - latLng: { - latitude: stop.latitude, - longitude: stop.longitude - } - } + location: { latLng: { latitude: stop.latitude, longitude: stop.longitude } } }, travelMode: 'DRIVE', routingPreference: 'TRAFFIC_UNAWARE', @@ -86,31 +76,28 @@ export function useParadaCercana() { if (response.routes && response.routes.length > 0) { const route = response.routes[0]; - let distTotal = 0; - let durTotal = 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); - } - } + return { + stop, + distance: route.distanceMeters || Infinity, + duration: parseInt(route.duration || "0"), + points: route.polyline?.encodedPolyline ? google.maps.geometry.encoding.decodePath(route.polyline.encodedPolyline) : [] + }; } } catch (e) { console.warn('Error calculando ruta a parada', stop.name, e); } + return null; + }); + + const results = await Promise.all(routePromises); + + for (const res of results) { + if (res && res.distance < minimaDistanciaCalles) { + minimaDistanciaCalles = res.distance; + mejorDuracion = res.duration; + mejorParada = res.stop; + mejorRutaPuntos = res.points; + } } } catch (e) { console.error('Error cargando Routes API en useParadaCercana', e); diff --git a/frontend/src/views/MapView.vue b/frontend/src/views/MapView.vue index 4d2c54c..3fa33b2 100644 --- a/frontend/src/views/MapView.vue +++ b/frontend/src/views/MapView.vue @@ -276,10 +276,9 @@ function selectRouteAndClose(route: any) { } async function updateActiveUnits() { - if (!isLoaded.value) return; - if (routeStore.selectedRouteId && paradaCercana.value) { - await calcularETA(routeStore.selectedRouteId, paradaCercana.value as BusStop); - } + if (!isLoaded.value || !routeStore.selectedRouteId) return; + // Llamamos a calcularETA incluso si no hay paradaCercana aún para un chequeo rápido de disponibilidad + await calcularETA(routeStore.selectedRouteId, (paradaCercana.value as BusStop) || null); } function locateUser(): Promise { @@ -333,24 +332,56 @@ const sonarHtml = ` watch([etaCargando, () => busesActivos.value.length], ([loading, count]) => { if (!loading && count === 0 && routeStore.selectedRouteId && routeStore.wasSelectedFromMap) { showETACard.value = true; + + // PROBLEMA 2 & 3: Limpieza automática cuando no hay buses + // Reseteamos el estado de la ruta en el store para que el buscador se limpie + // y el mapa se limpie a través de los watchers existentes. + + // Pequeño delay para asegurar que ETACard capture los datos antes de limpiar el store + setTimeout(() => { + if (showETACard.value && busesActivos.value.length === 0 && routeStore.selectedRouteId) { + routeStore.clearSelection(); + router.replace({ query: {} }); + console.log("SIBU | Ruta autolimpiada por falta de buses"); + } + }, 300); } }); +// Watch for route selection changes watch(() => routeStore.selectedRouteId, (routeId) => { if (routeId) { if (routeStore.wasSelectedFromMap) { + // OPTIMIZACIÓN PROBLEMA 1: Paralelismo Total + // Iniciamos dibujo y búsqueda de disponibilidad en paralelo updateMapMarkers(false); + updateActiveUnits(); } else { clearMapMarkers(); } } else { clearMapMarkers(); + showETACard.value = false; } }); +// Watch for paradaCercana to recalculate ETA as soon as it's identified +watch(paradaCercana, (newStop) => { + if (newStop && routeStore.selectedRouteId) { + updateActiveUnits(); + } +}); + function handleImageError(event: Event) { (event.target as HTMLImageElement).src = getImageUrl(null, 'coupon'); } + +// AUTO-LOCATION: Watch for user profile to trigger location if preference is enabled +watch(() => authStore.userProfile?.auto_location, (canLocate) => { + if (canLocate && isLoaded.value && !userCoords.value) { + locateUser(); + } +}, { immediate: true });