Files
SIB/frontend/src/composables/useETA.ts

177 lines
7.5 KiB
TypeScript

import { ref } from 'vue';
import { supabase } from '@/supabase';
import type { BusStop } from '@/types';
export interface BusETA {
horario_id: string;
hora_salida: string; // "HH:mm" para mostrar en UI
horaLlegadaParada: string; // "HH:mm" hora estimada de llegada a la parada
etaMinutos: number; // minutos hasta que llega (negativo = ya pasó)
estado: 'próximo' | 'en_camino' | 'pasó';
}
export function useETA() {
const busesActivos = ref<BusETA[]>([]);
const cargando = ref<boolean>(false);
// Configuración para el cálculo del ETA mejorado
const VELOCIDAD_PROMEDIO_KMH = 35; // km/h (promedio ciudad/carretera)
const TIEMPO_PARADA_SEGUNDOS = 45; // segundos detenido por parada
// Fórmula Haversine para distancia en línea recta (km)
function getDistanceKm(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLon = (lon2 - lon1) * (Math.PI / 180);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
const calcularETA = async (ruta_id: string, parada_cercana: BusStop | null) => {
cargando.value = true;
busesActivos.value = [];
try {
// 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')
.select('distance_km, average_speed_kmh')
.eq('id', ruta_id)
.single(),
supabase
.from('route_stops')
.select('stop_id, stop_order, bus_stops(latitude, longitude)')
.eq('route_id', ruta_id)
.order('stop_order', { ascending: true })
]);
if (routeRes.error || !stopsRes.data) throw new Error('Error al cargar datos de ruta');
const routeData = routeRes.data;
const routeStops = stopsRes.data;
// Encontrar el orden de la parada donde está el usuario
const targetStopIndex = routeStops.findIndex(s => s.stop_id === parada_cercana.id);
if (targetStopIndex === -1) return;
// 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 *= 1.2;
const velocidad = routeData.average_speed_kmh || VELOCIDAD_PROMEDIO_KMH;
const tiempoViajeMinutos = (distanciaAcumuladaKm / velocidad) * 60;
const numeroParadas = targetStopIndex;
const tiempoParadasMinutos = (numeroParadas * TIEMPO_PARADA_SEGUNDOS) / 60;
const minutosHastaParada = tiempoViajeMinutos + tiempoParadasMinutos;
// PASO 3: Calcular ETA para cada salida
const ahora = new Date();
const minutosAhora = ahora.getHours() * 60 + ahora.getMinutes();
const resultados: BusETA[] = [];
for (const h of horariosHoy) {
const salida = h.departure_time;
if (!salida) continue;
const [hStr, mStr] = salida.split(':');
const minutosSalida = parseInt(hStr) * 60 + parseInt(mStr);
const minutosLlegadaParada = minutosSalida + minutosHastaParada;
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) {
estado = (minutosAhora >= minutosSalida) ? 'en_camino' : 'próximo';
} else if (etaMinutos >= -2) {
estado = 'en_camino';
} else {
estado = 'pasó';
}
if (etaMinutos < -60) continue;
resultados.push({
horario_id: h.id,
hora_salida: horaSalidaFormato,
horaLlegadaParada: horaLlegada,
etaMinutos: Math.round(etaMinutos),
estado
});
}
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];
return a.etaMinutos - b.etaMinutos;
});
busesActivos.value = resultados.slice(0, 3);
} catch (e) {
console.error('SIBU | Error calculando ETA:', e);
} finally {
cargando.value = false;
}
};
// Helper: convierte minutos desde medianoche a "HH:mm"
function minutosAHora(minutos: number): string {
const totalMinutes = Math.round(minutos);
const m = ((totalMinutes % 1440) + 1440) % 1440; // normalizar 0-1439
const h = Math.floor(m / 60);
const min = m % 60;
return `${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}`;
}
return { calcularETA, busesActivos, cargando };
}