feat(UI): actualización de colores de ruta a amarillo y fix navegación transporte
This commit is contained in:
@ -1,5 +1,7 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useMapState } from './useMapState';
|
||||
|
||||
export interface Parada {
|
||||
id: number;
|
||||
nombre: string;
|
||||
@ -11,15 +13,15 @@ export interface Parada {
|
||||
export function useDirectionsRoute() {
|
||||
const estasCargando = ref<boolean>(false);
|
||||
const errorRuta = ref<string | null>(null);
|
||||
const renderizadoresActivos = ref<google.maps.DirectionsRenderer[]>([]);
|
||||
const { registrarRenderer, renderers } = useMapState();
|
||||
|
||||
// Limpia los tramos anteriores dibujados en el mapa
|
||||
const limpiarRuta = () => {
|
||||
if (renderizadoresActivos.value.length > 0) {
|
||||
renderizadoresActivos.value.forEach((renderer) => {
|
||||
if (renderers.value.length > 0) {
|
||||
renderers.value.forEach((renderer) => {
|
||||
renderer.setMap(null);
|
||||
});
|
||||
renderizadoresActivos.value = [];
|
||||
renderers.value = [];
|
||||
}
|
||||
errorRuta.value = null;
|
||||
};
|
||||
@ -76,18 +78,18 @@ export function useDirectionsRoute() {
|
||||
suppressMarkers: true, // SIBU maneja los suyos propios
|
||||
preserveViewport: true, // No auto centrar en cada tramo para evitar parpadeos visuales
|
||||
polylineOptions: isPast ? {
|
||||
strokeColor: '#9CA3AF', // Gris Tailwind 400
|
||||
strokeColor: '#FDE68A', // amarillo muy tenue
|
||||
strokeWeight: 3,
|
||||
strokeOpacity: 0.4
|
||||
} : {
|
||||
strokeColor: '#1D4ED8', // Azul Tailwind 700
|
||||
strokeColor: '#FBBF24', // amarillo principal
|
||||
strokeWeight: 5,
|
||||
strokeOpacity: 0.95
|
||||
}
|
||||
});
|
||||
|
||||
renderer.setDirections(response);
|
||||
renderizadoresActivos.value.push(renderer);
|
||||
registrarRenderer(renderer);
|
||||
|
||||
} catch (err: any) {
|
||||
console.warn(`SIBU | Tramo ${i} falló: `, err);
|
||||
|
||||
203
frontend/src/composables/useFlujoPrincipal.ts
Normal file
203
frontend/src/composables/useFlujoPrincipal.ts
Normal file
@ -0,0 +1,203 @@
|
||||
import { ref } from 'vue'
|
||||
import { useMapState } from './useMapState'
|
||||
import { useDirectionsRoute } from './useDirectionsRoute'
|
||||
import { useParadaCercana } from './useParadaCercana'
|
||||
import type { BusStop } from '@/types'
|
||||
|
||||
export const useFlujoPrincipal = () => {
|
||||
const { limpiarMapa, registrarMarker } = useMapState()
|
||||
const { encontrarParadaCercana } = useParadaCercana()
|
||||
const { trazarRuta } = useDirectionsRoute()
|
||||
const cargando = ref(false)
|
||||
const errorMsg = ref('')
|
||||
|
||||
// Simulated obtenerUbicacion (since it was just navigator.geolocation)
|
||||
const obtenerUbicacion = (): Promise<{ lat: number, lng: number }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!navigator.geolocation) return reject(new Error('No geolocation'))
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude }),
|
||||
(err) => reject(err),
|
||||
{ enableHighAccuracy: true, timeout: 8000, maximumAge: 30000 }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const procesarSeleccionDeRuta = async (
|
||||
_ruta: { id: string },
|
||||
paradas: BusStop[],
|
||||
map: google.maps.Map | undefined
|
||||
) => {
|
||||
if (!map) return
|
||||
|
||||
try {
|
||||
// ── PASO 1: Limpiar todo lo que había antes ──────────
|
||||
limpiarMapa()
|
||||
cargando.value = true
|
||||
|
||||
// ── PASO 2: Obtener ubicación ──
|
||||
// Paradas ya vienen precargadas desde store para evitar doble fetch
|
||||
let ubicacion: { lat: number, lng: number } | null = null;
|
||||
try {
|
||||
ubicacion = await obtenerUbicacion();
|
||||
} catch (err) {
|
||||
console.warn('SIBU | No se pudo obtener ubicación', err);
|
||||
}
|
||||
|
||||
// Format Paradas for trazarRuta
|
||||
const paradasFormateadas = paradas.map((p, i) => ({
|
||||
id: typeof p.id === 'string' ? parseInt(p.id) || i : p.id || i,
|
||||
nombre: p.name,
|
||||
latitud: p.latitude,
|
||||
longitud: p.longitude,
|
||||
orden: i
|
||||
}));
|
||||
|
||||
// Si no hay paradas o muy pocas, abortar
|
||||
if (paradasFormateadas.length < 2) return;
|
||||
|
||||
if (!ubicacion) {
|
||||
// Fallback: solo dibujar la ruta sin parada cercana (o podríamos no hacer zoom guiado)
|
||||
await trazarRuta(paradasFormateadas, map, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── PASO 3: Dibujar ruta completa (fondo, gris tenue) ─
|
||||
await trazarRuta(paradasFormateadas, map, true);
|
||||
|
||||
// ── PASO 4: Calcular parada más cercana ───────────────
|
||||
await encontrarParadaCercana(ubicacion, paradas, map)
|
||||
// Buscamos manualmente porque encontrarParadaCercana guarda en su propio ref interno que no retornamos fácil
|
||||
// o podemos usar useParadaCercana().paradaCercana.value
|
||||
// Actually, refactor from useParadaCercana: finding the closest
|
||||
|
||||
const { paradaCercana } = useParadaCercana()
|
||||
await encontrarParadaCercana(ubicacion, paradas, undefined) // sin mapa para no dibujar polilínea vieja por encima
|
||||
|
||||
const paradaCercanaFound = paradaCercana.value
|
||||
|
||||
if (!paradaCercanaFound) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ── PASO 5: Dibujar tramo relevante (azul vivo) ───────
|
||||
const idx = paradasFormateadas.findIndex(p => p.longitud === paradaCercanaFound.longitude && p.latitud === paradaCercanaFound.latitude)
|
||||
if (idx !== -1) {
|
||||
const tramoRelevante = paradasFormateadas.slice(idx)
|
||||
if (tramoRelevante.length > 1) {
|
||||
await trazarRuta(tramoRelevante, map, false)
|
||||
}
|
||||
}
|
||||
|
||||
// ── PASO 6: Marcador de parada cercana ────────────────
|
||||
registrarMarker(
|
||||
new google.maps.Marker({
|
||||
position: { lat: paradaCercanaFound.latitude, lng: paradaCercanaFound.longitude },
|
||||
map,
|
||||
icon: {
|
||||
path: google.maps.SymbolPath.CIRCLE,
|
||||
fillColor: '#F59E0B',
|
||||
fillOpacity: 1,
|
||||
strokeColor: '#FFFFFF',
|
||||
strokeWeight: 3,
|
||||
scale: 12
|
||||
},
|
||||
title: paradaCercanaFound.name,
|
||||
zIndex: 10
|
||||
})
|
||||
)
|
||||
|
||||
// ── PASO 7: Pulso animado en ubicación del usuario ────
|
||||
dibujarPulsoUsuario(ubicacion, map)
|
||||
|
||||
// ── PASO 8: Zoom centrado en usuario + parada cercana ─
|
||||
hacerZoomAlTramoRelevante(ubicacion, paradaCercanaFound, map)
|
||||
|
||||
} catch (error) {
|
||||
console.error('SIBU | Error procesando ruta:', error)
|
||||
errorMsg.value = 'No se pudo cargar la ruta'
|
||||
} finally {
|
||||
cargando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// PULSO ANIMADO EN LA UBICACIÓN DEL USUARIO:
|
||||
const dibujarPulsoUsuario = (
|
||||
ubicacion: { lat: number, lng: number },
|
||||
map: google.maps.Map
|
||||
) => {
|
||||
const { registrarCircle, registrarMarker } = useMapState()
|
||||
|
||||
// Círculo exterior pulsante (efecto ripple)
|
||||
registrarCircle(
|
||||
new google.maps.Circle({
|
||||
map,
|
||||
center: ubicacion,
|
||||
radius: 80, // metros
|
||||
fillColor: '#3B82F6',
|
||||
fillOpacity: 0.15,
|
||||
strokeColor: '#3B82F6',
|
||||
strokeOpacity: 0.4,
|
||||
strokeWeight: 1,
|
||||
zIndex: 1
|
||||
})
|
||||
)
|
||||
|
||||
// Punto central sólido del usuario
|
||||
registrarMarker(
|
||||
new google.maps.Marker({
|
||||
position: ubicacion,
|
||||
map,
|
||||
icon: {
|
||||
path: google.maps.SymbolPath.CIRCLE,
|
||||
fillColor: '#3B82F6', // azul
|
||||
fillOpacity: 1,
|
||||
strokeColor: '#FFFFFF',
|
||||
strokeWeight: 3,
|
||||
scale: 9
|
||||
},
|
||||
title: 'Tu ubicación',
|
||||
zIndex: 20 // encima de todo
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// ZOOM QUE MUESTRA USUARIO Y PARADA CERCANA:
|
||||
const hacerZoomAlTramoRelevante = (
|
||||
ubicacion: { lat: number, lng: number },
|
||||
paradaCercana: BusStop,
|
||||
map: google.maps.Map
|
||||
) => {
|
||||
const bounds = new google.maps.LatLngBounds()
|
||||
|
||||
// Incluir solo estos dos puntos clave
|
||||
bounds.extend(new google.maps.LatLng(ubicacion.lat, ubicacion.lng))
|
||||
bounds.extend(
|
||||
new google.maps.LatLng(paradaCercana.latitude, paradaCercana.longitude)
|
||||
)
|
||||
|
||||
// Ajustar zoom con padding generoso para que se vean bien
|
||||
map.fitBounds(bounds, {
|
||||
top: 100, // espacio para la navbar
|
||||
bottom: 200, // espacio para el bottom sheet de ETA
|
||||
left: 60,
|
||||
right: 60
|
||||
})
|
||||
|
||||
// Asegurar zoom mínimo para que se vea el contexto de la calle
|
||||
google.maps.event.addListenerOnce(
|
||||
map,
|
||||
'bounds_changed',
|
||||
() => {
|
||||
if ((map.getZoom() ?? 0) > 17) {
|
||||
map.setZoom(17) // no acercar demasiado
|
||||
}
|
||||
if ((map.getZoom() ?? 0) < 14) {
|
||||
map.setZoom(14) // no alejar demasiado
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return { procesarSeleccionDeRuta, cargando, errorMsg }
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
/** Composable for Google Maps integration */
|
||||
import { ref, shallowRef, onMounted } from 'vue'
|
||||
import { setOptions, importLibrary } from '@googlemaps/js-api-loader'
|
||||
import { useMapState } from './useMapState'
|
||||
|
||||
const getApiKey = () => import.meta.env.VITE_GOOGLE_MAPS_API_KEY || ''
|
||||
|
||||
@ -13,6 +14,11 @@ export function useGoogleMaps() {
|
||||
const map = shallowRef<google.maps.Map | null>(null)
|
||||
const isLoaded = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const {
|
||||
registrarMarker,
|
||||
registrarPolyline,
|
||||
limpiarMapa: limpiarTodoCentralizado
|
||||
} = useMapState()
|
||||
|
||||
// Escuchar errores globales de autenticación de Google
|
||||
if (typeof window !== 'undefined') {
|
||||
@ -139,6 +145,8 @@ export function useGoogleMaps() {
|
||||
icon: options?.icon,
|
||||
})
|
||||
|
||||
registrarMarker(marker)
|
||||
|
||||
if (options?.onDragEnd) {
|
||||
marker.addListener('dragend', () => {
|
||||
const pos = marker.getPosition()
|
||||
@ -192,6 +200,7 @@ export function useGoogleMaps() {
|
||||
fontWeight: '900',
|
||||
},
|
||||
})
|
||||
registrarMarker(marker)
|
||||
|
||||
if (onClick) {
|
||||
marker.addListener('click', onClick)
|
||||
@ -211,7 +220,7 @@ export function useGoogleMaps() {
|
||||
function addCleanMarker(
|
||||
position: { lat: number; lng: number },
|
||||
title: string,
|
||||
type: 'normal' | 'cercana' | 'origen' | 'destino',
|
||||
type: 'normal' | 'cercana' | 'origen' | 'destino' | 'pasada',
|
||||
onClick?: () => void
|
||||
): google.maps.Marker | null {
|
||||
if (!map.value) {
|
||||
@ -221,25 +230,25 @@ export function useGoogleMaps() {
|
||||
|
||||
const iconoParadaNormal = {
|
||||
path: google.maps.SymbolPath.CIRCLE,
|
||||
fillColor: '#3B82F6', // azul
|
||||
fillColor: '#FBBF24', // amarillo
|
||||
fillOpacity: 1,
|
||||
strokeColor: '#FFFFFF', // borde blanco limpio
|
||||
strokeColor: '#FFFFFF',
|
||||
strokeWeight: 2,
|
||||
scale: 7
|
||||
};
|
||||
|
||||
const iconoParadaCercana = {
|
||||
path: google.maps.SymbolPath.CIRCLE,
|
||||
fillColor: '#F59E0B', // amarillo/naranja
|
||||
fillColor: '#F59E0B', // amarillo intenso
|
||||
fillOpacity: 1,
|
||||
strokeColor: '#FFFFFF',
|
||||
strokeWeight: 3,
|
||||
scale: 10
|
||||
scale: 12
|
||||
};
|
||||
|
||||
const iconoOrigen = {
|
||||
path: google.maps.SymbolPath.CIRCLE,
|
||||
fillColor: '#10B981', // verde
|
||||
fillColor: '#FBBF24', // amarillo
|
||||
fillOpacity: 1,
|
||||
strokeColor: '#FFFFFF',
|
||||
strokeWeight: 3,
|
||||
@ -248,18 +257,28 @@ export function useGoogleMaps() {
|
||||
|
||||
const iconoDestino = {
|
||||
path: google.maps.SymbolPath.CIRCLE,
|
||||
fillColor: '#EF4444', // rojo
|
||||
fillColor: '#D97706', // amarillo oscuro
|
||||
fillOpacity: 1,
|
||||
strokeColor: '#FFFFFF',
|
||||
strokeWeight: 3,
|
||||
scale: 10
|
||||
};
|
||||
|
||||
const iconoParadaPasada = {
|
||||
path: google.maps.SymbolPath.CIRCLE,
|
||||
fillColor: '#FDE68A', // amarillo tenue
|
||||
fillOpacity: 0.5,
|
||||
strokeColor: '#FFFFFF',
|
||||
strokeWeight: 1,
|
||||
scale: 5
|
||||
};
|
||||
|
||||
const iconos = {
|
||||
normal: iconoParadaNormal,
|
||||
cercana: iconoParadaCercana,
|
||||
origen: iconoOrigen,
|
||||
destino: iconoDestino
|
||||
destino: iconoDestino,
|
||||
pasada: iconoParadaPasada
|
||||
};
|
||||
|
||||
const marker = new google.maps.Marker({
|
||||
@ -268,6 +287,7 @@ export function useGoogleMaps() {
|
||||
title,
|
||||
icon: iconos[type],
|
||||
});
|
||||
registrarMarker(marker);
|
||||
|
||||
if (onClick) {
|
||||
const infoWindow = new google.maps.InfoWindow({
|
||||
@ -352,6 +372,7 @@ export function useGoogleMaps() {
|
||||
|
||||
const overlay = new CustomOverlay(new google.maps.LatLng(position.lat, position.lng));
|
||||
overlay.setMap(map.value);
|
||||
registrarMarker(overlay);
|
||||
|
||||
// Track for cleanup
|
||||
if (!globalOverlays.has(map.value)) {
|
||||
@ -376,6 +397,7 @@ export function useGoogleMaps() {
|
||||
strokeWeight: 5,
|
||||
map: map.value,
|
||||
})
|
||||
registrarPolyline(polyline)
|
||||
|
||||
// Track in global overlay tracker
|
||||
if (map.value) {
|
||||
@ -483,6 +505,7 @@ export function useGoogleMaps() {
|
||||
if (!map.value) {
|
||||
return
|
||||
}
|
||||
limpiarTodoCentralizado()
|
||||
clearAllOverlaysForMap(map.value)
|
||||
}
|
||||
|
||||
|
||||
116
frontend/src/composables/useMapState.ts
Normal file
116
frontend/src/composables/useMapState.ts
Normal file
@ -0,0 +1,116 @@
|
||||
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[]>([])
|
||||
|
||||
// Singleton pattern using composable
|
||||
export const useMapState = () => {
|
||||
|
||||
const registrarMarker = (marker: any) => {
|
||||
if (marker) markers.value.push(marker as google.maps.Marker)
|
||||
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) => {
|
||||
if (polyline) polylines.value.push(polyline)
|
||||
return polyline
|
||||
}
|
||||
|
||||
// Registrar un circle
|
||||
const registrarCircle = (circle: google.maps.Circle) => {
|
||||
if (circle) circles.value.push(circle)
|
||||
return circle
|
||||
}
|
||||
|
||||
// Registrar un InfoWindow
|
||||
const registrarInfoWindow = (infoWindow: google.maps.InfoWindow) => {
|
||||
if (infoWindow) infoWindows.value.push(infoWindow)
|
||||
return infoWindow
|
||||
}
|
||||
|
||||
// ⚠️ FUNCIÓN CRÍTICA: limpiar ABSOLUTAMENTE TODO del mapa
|
||||
const limpiarMapa = () => {
|
||||
// Eliminar markers
|
||||
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)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error limpiando marker', e)
|
||||
}
|
||||
})
|
||||
markers.value = []
|
||||
|
||||
// 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)
|
||||
} catch (e) {
|
||||
console.warn('Error limpiando renderer', e)
|
||||
}
|
||||
})
|
||||
renderers.value = []
|
||||
|
||||
// Eliminar polylines
|
||||
polylines.value.forEach(p => {
|
||||
try {
|
||||
if (p && typeof p.setMap === 'function') p.setMap(null)
|
||||
} catch (e) {
|
||||
console.warn('Error limpiando polyline', e)
|
||||
}
|
||||
})
|
||||
polylines.value = []
|
||||
|
||||
// Cerrar y limpiar infoWindows
|
||||
infoWindows.value.forEach(iw => {
|
||||
try {
|
||||
if (iw && typeof iw.close === 'function') iw.close()
|
||||
} catch (e) {
|
||||
console.warn('Error limpiando infowindow', e)
|
||||
}
|
||||
})
|
||||
infoWindows.value = []
|
||||
|
||||
// Eliminar circles (pulso de ubicación) // Added custom HTML markers too since they often act as circles
|
||||
circles.value.forEach(c => {
|
||||
try {
|
||||
if (c && typeof c.setMap === 'function') c.setMap(null)
|
||||
if (c && typeof (c as any).remove === 'function') (c as any).remove()
|
||||
} catch (e) {
|
||||
console.warn('Error limpiando circle', e)
|
||||
}
|
||||
})
|
||||
circles.value = []
|
||||
|
||||
console.log('SIBU | Mapa limpiado completamente ✓')
|
||||
}
|
||||
|
||||
return {
|
||||
markers,
|
||||
renderers,
|
||||
polylines,
|
||||
infoWindows,
|
||||
circles,
|
||||
registrarMarker,
|
||||
registrarRenderer,
|
||||
registrarPolyline,
|
||||
registrarCircle,
|
||||
registrarInfoWindow,
|
||||
limpiarMapa
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { ref } from 'vue';
|
||||
import type { BusStop } from '@/types';
|
||||
import { useMapState } from './useMapState';
|
||||
|
||||
// Fórmula Haversine para distancia en línea recta (km)
|
||||
function getHaversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||
@ -18,6 +19,7 @@ export function useParadaCercana() {
|
||||
const paradaCercana = ref<BusStop | null>(null);
|
||||
const distanciaMetros = ref<number>(0);
|
||||
const duracionCaminata = ref<number>(0);
|
||||
const { registrarPolyline } = useMapState();
|
||||
const caminandoPolyline = ref<google.maps.Polyline | null>(null);
|
||||
|
||||
const limpiarCaminata = () => {
|
||||
@ -103,20 +105,22 @@ export function useParadaCercana() {
|
||||
if (map && mejorRutaPuntos.length > 0) {
|
||||
caminandoPolyline.value = new google.maps.Polyline({
|
||||
path: mejorRutaPuntos,
|
||||
strokeColor: '#1E40AF',
|
||||
strokeColor: '#F59E0B',
|
||||
strokeOpacity: 0,
|
||||
strokeWeight: 4,
|
||||
strokeWeight: 3,
|
||||
icons: [{
|
||||
icon: {
|
||||
path: 'M 0,-1 0,1',
|
||||
strokeOpacity: 1,
|
||||
scale: 3
|
||||
scale: 3,
|
||||
strokeColor: '#F59E0B'
|
||||
},
|
||||
offset: '0',
|
||||
repeat: '20px'
|
||||
repeat: '12px'
|
||||
}],
|
||||
map: map
|
||||
});
|
||||
registrarPolyline(caminandoPolyline.value);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user