diff --git a/frontend/src/composables/useGoogleMaps.ts b/frontend/src/composables/useGoogleMaps.ts index 8fcde51..bd3c2b5 100644 --- a/frontend/src/composables/useGoogleMaps.ts +++ b/frontend/src/composables/useGoogleMaps.ts @@ -323,67 +323,44 @@ export function useGoogleMaps() { } function addHtmlMarker( - position: { lat: number; lng: number }, - htmlContent: string, - offset: { x: number; y: number } = { x: 0, y: 0 } + position: { lat: number, lng: number }, + html: string, + offset: { x: number, y: number } = { x: 0, y: 0 } ) { if (!map.value) return null; - class CustomOverlay extends google.maps.OverlayView { - private div: HTMLElement | null = null; - private pos: google.maps.LatLng; + const overlay = new google.maps.OverlayView(); + let container: HTMLDivElement | null = null; - constructor(pos: google.maps.LatLng) { - super(); - this.pos = pos; + overlay.onAdd = function () { + container = document.createElement('div'); + container.style.position = 'absolute'; + container.innerHTML = html; + const panes = this.getPanes(); + if (panes) panes.overlayMouseTarget.appendChild(container); + }; + + overlay.draw = function () { + const projection = this.getProjection(); + if (!projection || !container) return; + const pos = projection.fromLatLngToDivPixel(new google.maps.LatLng(position.lat, position.lng)); + if (pos) { + container.style.left = (pos.x + offset.x) + 'px'; + container.style.top = (pos.y + offset.y) + 'px'; } + }; - onAdd() { - const div = document.createElement('div'); - div.style.position = 'absolute'; - div.style.cursor = 'pointer'; - div.innerHTML = htmlContent; - this.div = div; - const panes = this.getPanes(); - panes?.overlayMouseTarget.appendChild(div); + overlay.onRemove = function () { + if (container && container.parentNode) { + container.parentNode.removeChild(container); } + container = null; + }; - draw() { - const overlayProjection = this.getProjection(); - const point = overlayProjection.fromLatLngToDivPixel(this.pos); - if (point && this.div) { - this.div.style.left = (point.x + offset.x) + 'px'; - this.div.style.top = (point.y + offset.y) + 'px'; - } - } - - onRemove() { - if (this.div) { - try { - // Safer element removal - if (this.div.parentNode) { - this.div.parentNode.removeChild(this.div); - } else { - this.div.remove(); - } - } catch (e) { - console.warn('CustomOverlay: element already removed or parent mismatch', e); - } - this.div = null; - } - } - - setPosition(newPos: { lat: number; lng: number }) { - this.pos = new google.maps.LatLng(newPos.lat, newPos.lng); - this.draw(); - } - } - - const overlay = new CustomOverlay(new google.maps.LatLng(position.lat, position.lng)); overlay.setMap(map.value); - registrarMarker(overlay); + registrarMarker(overlay as any); - // Track for cleanup + // Track in global overlay tracker if (!globalOverlays.has(map.value)) { globalOverlays.set(map.value, new Set()); } diff --git a/frontend/src/composables/useMapState.ts b/frontend/src/composables/useMapState.ts index 0c4333e..7867d35 100644 --- a/frontend/src/composables/useMapState.ts +++ b/frontend/src/composables/useMapState.ts @@ -48,13 +48,18 @@ export const useMapState = () => { // ⚠️ FUNCIÓN CRÍTICA: limpiar ABSOLUTAMENTE TODO del mapa const limpiarMapa = () => { - // Eliminar markers + console.log(`SIBU | Iniciando limpieza de ${markers.value.length} markers, ${renderers.value.length} renderers, ${polylines.value.length} polylines...`) + + // Eliminar markers y overlays HTML markers.value.forEach(m => { try { - if (m && typeof m.setMap === 'function') m.setMap(null) - if (m && typeof (m as any).remove === 'function') (m as any).remove() // para HTML markers custom - if (m && google?.maps?.event?.clearInstanceListeners) { - google.maps.event.clearInstanceListeners(m) + if (m) { + if (typeof m.setMap === 'function') m.setMap(null); + if (typeof (m as any).remove === 'function') (m as any).remove(); + if (typeof (m as any).onRemove === 'function') (m as any).onRemove(); + if (google?.maps?.event?.clearInstanceListeners) { + google.maps.event.clearInstanceListeners(m); + } } } catch (e) { console.warn('Error limpiando marker', e) @@ -65,8 +70,10 @@ export const useMapState = () => { // Eliminar renderers de Directions renderers.value.forEach(r => { try { - if (r && typeof r.setMap === 'function') r.setMap(null) - if (r && typeof r.setDirections === 'function') r.setDirections({ routes: [] } as any) + 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) } @@ -93,7 +100,7 @@ export const useMapState = () => { }) infoWindows.value = [] - // Eliminar circles (pulso de ubicación) // Added custom HTML markers too since they often act as circles + // Eliminar circles circles.value.forEach(c => { try { if (c && typeof c.setMap === 'function') c.setMap(null) @@ -106,7 +113,9 @@ export const useMapState = () => { // Ejecutar callback de limpieza externa si existe if (onLimpiarCallback.value) { - try { onLimpiarCallback.value() } catch (e) { + try { + onLimpiarCallback.value() + } catch (e) { console.warn('Error en callback de limpieza externa', e) } } diff --git a/frontend/src/views/MapView.vue b/frontend/src/views/MapView.vue index 7536c20..9545b5c 100644 --- a/frontend/src/views/MapView.vue +++ b/frontend/src/views/MapView.vue @@ -27,9 +27,9 @@ const mapStore = useMapStore(); const busStopStore = useBusStopStore(); const couponStore = useCouponStore(); -const { map, isLoaded, error: mapsError, initMap, addCleanMarker, addHtmlMarker, setCenter, setZoom, addMarker, clearAllOverlays } = useGoogleMaps(); +const { map, isLoaded, error: mapsError, initMap, addCleanMarker, addHtmlMarker, setCenter, setZoom, addMarker } = useGoogleMaps(); const { estasCargando: estasCargandoRuta, errorRuta } = useDirectionsRoute(); -const { encontrarParadaCercana, limpiarCaminata, paradaCercana, distanciaMetros, duracionCaminata } = useParadaCercana(); +const { encontrarParadaCercana, paradaCercana, distanciaMetros, duracionCaminata } = useParadaCercana(); const { calcularETA, busesActivos, cargando: etaCargando } = useETA(); const { procesarSeleccionDeRuta } = useFlujoPrincipal(); @@ -107,7 +107,7 @@ function closeUberSearch() { } async function clearAllMapData() { - console.log('🤖 JARVIS: Iniciando PURGA nuclear...'); + console.log('🤖 JARVIS: Iniciando PURGA nuclear centralizada...'); // 1. UI inmediata showUberSearch.value = false; @@ -124,85 +124,23 @@ async function clearAllMapData() { routeStore.clearSelection(); lastProcessedRouteId.value = null; - // 4. Limpiar markers locales - const sweep = (arrayRef: any) => { - if (!arrayRef.value) return; - arrayRef.value.forEach((m: any) => { - try { if (m && m.setMap) m.setMap(null); } catch (e) {} - }); - arrayRef.value = []; - }; - - sweep(markers); - sweep(promoMarkers); - - // Limpiar unidades de transporte - if (unitMarkers.value) { - unitMarkers.value.forEach((m: any) => { - try { if (m && m.setMap) m.setMap(null); } catch (e) {} - }); - unitMarkers.value.clear(); - } - - // 5. Limpiar polilíneas (CORREGIDO: agregar walkingPolylineBorder) - if (polyline.value) { - polyline.value.setMap(null); - polyline.value = null; - } - if (walkingPolyline.value) { - walkingPolyline.value.setMap(null); - walkingPolyline.value = null; - } - // ✅ NUEVO: limpiar el borde blanco de la ruta caminando - if (walkingPolylineBorder.value) { - walkingPolylineBorder.value.setMap(null); - walkingPolylineBorder.value = null; - } - - // 6. Limpiar pulso de parada óptima (CORREGIDO) - if (optimalStopPulse.value) { - try { - // Intentar setMap primero - if (typeof optimalStopPulse.value.setMap === 'function') { - optimalStopPulse.value.setMap(null); - } - // Si es un overlay HTML, también intentar remove() - if (typeof optimalStopPulse.value.remove === 'function') { - optimalStopPulse.value.remove(); - } - // Si tiene onRemove (OverlayView pattern) - if (typeof optimalStopPulse.value.onRemove === 'function') { - optimalStopPulse.value.onRemove(); - } - } catch(e) { - console.warn('SIBU | No se pudo limpiar optimalStopPulse:', e); - } - optimalStopPulse.value = null; - } - - // 7. Limpiar composables - limpiarCaminata(); - - // 8. Barrido profundo de Google Maps overlays - if (typeof clearAllOverlays === 'function') { - try { clearAllOverlays(); } catch (e) {} - } - - // 9. Purgación centralizada (useMapState) + // 4. LIMPIEZA CENTRALIZADA (useMapState) + // Esto limpia markers, renderers, polylines, overlays HTML, etc. limpiarTodoCentralizado(); - // 10. Restaurar SOLO el marcador del usuario + // 5. Limpiar referencias locales del componente (aunque no tengan mapa asignado ya) + markers.value = []; + promoMarkers.value = []; + unitMarkers.value.clear(); + polyline.value = null; + walkingPolyline.value = null; + walkingPolylineBorder.value = null; + optimalStopPulse.value = null; + + // 6. Restaurar SOLO el marcador del usuario si tenemos ubicación await nextTick(); if (userCoords.value) { const { lat, lng } = userCoords.value; - // Limpiar marcador anterior del usuario - if (userMarker.value) { - try { - if (userMarker.value.setMap) userMarker.value.setMap(null); - if (userMarker.value.remove) userMarker.value.remove(); - } catch(e) {} - } - // Redibujar solo el sonar del usuario userMarker.value = addHtmlMarker( { lat, lng }, sonarHtml, @@ -210,7 +148,7 @@ async function clearAllMapData() { ); } - console.log('🤖 JARVIS: Purga completada. Solo queda el usuario ✓'); + console.log('🤖 JARVIS: Purga completada ✓'); } catch (err) { console.error('❌ JARVIS: Error en purga:', err); } @@ -758,6 +696,8 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop: if (walkingPolyline.value) walkingPolyline.value.setMap(null); if (walkingPolylineBorder.value) walkingPolylineBorder.value.setMap(null); + const { registrarPolyline: regPoly } = useMapState(); + // CAPA 1: Borde blanco (Para dar contraste estilo Google Maps) walkingPolylineBorder.value = new google.maps.Polyline({ path: route.overview_path, @@ -768,6 +708,7 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop: map: map.value, zIndex: 5 }); + regPoly(walkingPolylineBorder.value); // CAPA 2: Línea Indigo Central (La ruta principal) walkingPolyline.value = new google.maps.Polyline({ @@ -779,6 +720,7 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop: map: map.value, zIndex: 10 }); + regPoly(walkingPolyline.value); // Ajustar zoom para mostrar toda la ruta de caminata if (map.value) { diff --git a/frontend/src/views/SchedulesView.vue b/frontend/src/views/SchedulesView.vue index 9aae3f3..e85d14c 100644 --- a/frontend/src/views/SchedulesView.vue +++ b/frontend/src/views/SchedulesView.vue @@ -39,42 +39,69 @@ function getBusStatus(timeStr: string): 'departing' | 'ontime' | 'upcoming' | 'p return 'upcoming' } +// ── Calcular si el horario es "hoy" o "mañana" según tipo de día // ── Calcular si el horario es "hoy" o "mañana" según tipo de día function getScheduleDay(schedule: any): 'today' | 'tomorrow' | 'other' { const now = new Date() - const dow = now.getDay() // 0=Dom, 6=Sab - const isWeekend = dow === 0 || dow === 6 - const isTomorrow = (!isWeekend && schedule.schedule_type === 'weekend') || - (isWeekend && schedule.schedule_type === 'weekday') + const tomorrow = new Date(now) + tomorrow.setDate(now.getDate() + 1) + + const getDayType = (date: Date) => { + const dow = date.getDay() // 0=Dom, 6=Sab + return (dow === 0 || dow === 6) ? 'weekend' : 'weekday' + } + + const todayType = getDayType(now) + const tomorrowType = getDayType(tomorrow) + + // Comparar con el tipo del horario + // Nota: Si el horario es 'todos', cuenta para hoy y mañana (pero priorizamos hoy si pides hoy) + const type = schedule.schedule_type || 'todos' + + const isToday = type === todayType || type === 'todos' + const isTomorrow = type === tomorrowType || type === 'todos' + + if (isToday) return 'today' if (isTomorrow) return 'tomorrow' - if (schedule.schedule_type === 'holiday') return 'other' - return 'today' + if (type === 'holiday') return 'other' + + return 'other' } function getDayLabel(schedule: any): string { - const d = getScheduleDay(schedule) - if (d === 'today') return 'Hoy' - if (d === 'tomorrow') return 'Mañana' - return 'Próximo' + const type = schedule.schedule_type || 'todos' + const now = new Date() + const todayType = (now.getDay() === 0 || now.getDay() === 6) ? 'weekend' : 'weekday' + + if (type === 'todos') return 'Diario' + if (type === todayType) return 'Hoy' + return 'Mañana' } // ── Filtrado de horarios const filteredSchedules = computed(() => { + const now = new Date() + const hhmmAhora = now.getHours() * 100 + now.getMinutes() + return scheduleStore.schedules.filter(s => { const d = getScheduleDay(s) - const status = getBusStatus(s.departure_time) + const [hStr, mStr] = (s.departure_time || '00:00').split(':') + const h = parseInt(hStr || '0') + const m = parseInt(mStr || '0') + const hhmmSched = h * 100 + m + const isPassed = hhmmSched < hhmmAhora - 2 // margen de 2 min - // Filtro Hoy: Solo buses de hoy que NO han pasado + // Filtro Hoy: Es hoy Y no ha pasado (o es de los que dice salir en este rango) if (dayFilter.value === 'today') { - return d === 'today' && status !== 'passed' + return d === 'today' && !isPassed } - // Filtro Mañana: Solo buses de mañana + // Filtro Mañana: Es mañana if (dayFilter.value === 'tomorrow') { return d === 'tomorrow' } - // Filtro Todos: Mostrar todo + // Filtro Todos: Mostrar todo sin importar si pasó o es otro día return true }) })