feat(map): clean stop markers and route dimming
This commit is contained in:
@ -25,7 +25,7 @@ const mapStore = useMapStore();
|
||||
const busStopStore = useBusStopStore();
|
||||
const couponStore = useCouponStore();
|
||||
|
||||
const { map, isLoaded, error: mapsError, initMap, addNumberedMarker, addHtmlMarker, fitBounds, setCenter, setZoom, addMarker, clearAllOverlays } = useGoogleMaps();
|
||||
const { map, isLoaded, error: mapsError, initMap, addCleanMarker, addHtmlMarker, setCenter, setZoom, addMarker, clearAllOverlays } = useGoogleMaps();
|
||||
const { trazarRuta, limpiarRuta, estasCargando: estasCargandoRuta, errorRuta } = useDirectionsRoute();
|
||||
const { encontrarParadaCercana, limpiarCaminata, paradaCercana, distanciaMetros, duracionCaminata } = useParadaCercana();
|
||||
const { calcularETA, busesActivos, cargando: etaCargando } = useETA();
|
||||
@ -39,7 +39,6 @@ const polyline = ref<google.maps.Polyline | null>(null);
|
||||
const walkingPolyline = ref<google.maps.Polyline | null>(null);
|
||||
const walkingPolylineBorder = ref<google.maps.Polyline | null>(null); // Borde blanco estilo Google Maps
|
||||
const optimalStopPulse = ref<any>(null); // Radar para la parada óptima
|
||||
const navigationInfo = ref<{ distance: string, duration: string, targetName: string } | null>(null);
|
||||
const showRouteDropdown = ref(false);
|
||||
const routeCardRef = ref<HTMLElement | null>(null);
|
||||
const isUpdatingMarkers = ref(false);
|
||||
@ -49,7 +48,7 @@ const userCoords = ref<{ lat: number; lng: number } | null>(null); // Store last
|
||||
const currentMarkerMode = ref<'dot' | 'pin' | null>(null);
|
||||
const mappingSequenceId = ref(0); // Atomic ID to prevent race conditions
|
||||
|
||||
|
||||
const alturaNavbar = ref(64);
|
||||
// Search state
|
||||
const stopSearchQuery = ref("");
|
||||
const destinationQuery = ref("");
|
||||
@ -109,7 +108,6 @@ async function clearAllMapData() {
|
||||
showRoutesToggle.value = false;
|
||||
destinationQuery.value = "";
|
||||
stopSearchQuery.value = "";
|
||||
navigationInfo.value = null;
|
||||
|
||||
// 2. Invalidar cualquier hilo de dibujo en curso
|
||||
mappingSequenceId.value++;
|
||||
@ -218,8 +216,14 @@ async function claimPromo() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
const navbar = document.querySelector('#navbar-admin') ?? document.querySelector('nav') ?? document.querySelector('header');
|
||||
if (navbar) {
|
||||
alturaNavbar.value = navbar.getBoundingClientRect().height;
|
||||
} else {
|
||||
alturaNavbar.value = 64;
|
||||
}
|
||||
|
||||
analyticsService.logEvent({ event_name: 'screen_view', properties: { screen_name: 'Map' } })
|
||||
// Add click outside listener
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
@ -343,7 +347,7 @@ async function initializeMap() {
|
||||
updatePromoMarkers();
|
||||
|
||||
// Apply initial styles based on current zoom
|
||||
updateMarkersStyles(true);
|
||||
updateMarkersStyles();
|
||||
}
|
||||
|
||||
// Watch for route selection changes
|
||||
@ -392,12 +396,13 @@ watch(
|
||||
if (!oldStops || newStops.length !== oldStops.length ||
|
||||
newStops.some((stop, idx) => stop.id !== oldStops[idx]?.id)) {
|
||||
console.log('Route stops changed - updating markers')
|
||||
await updateMapMarkers();
|
||||
|
||||
// FLOW REFINEMENT: After markers are loaded, find the optimal entrance stop
|
||||
// FLOW REFINEMENT: Find the optimal entrance stop first
|
||||
if (newStops.length > 0) {
|
||||
highlightOptimalStopForRoute();
|
||||
await highlightOptimalStopForRoute();
|
||||
}
|
||||
|
||||
await updateMapMarkers();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -445,9 +450,6 @@ function clearMapMarkers() {
|
||||
walkingPolylineBorder.value = null;
|
||||
}
|
||||
|
||||
// Clear navigation info
|
||||
navigationInfo.value = null;
|
||||
|
||||
// Clear optimal pulse
|
||||
if (optimalStopPulse.value) {
|
||||
if (typeof optimalStopPulse.value.setMap === 'function') {
|
||||
@ -474,6 +476,7 @@ async function updateMapMarkers() {
|
||||
|
||||
if (!currentRequestRouteId || stops.length === 0) {
|
||||
clearMapMarkers();
|
||||
limpiarRuta();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -489,121 +492,83 @@ async function updateMapMarkers() {
|
||||
return;
|
||||
}
|
||||
|
||||
const newMarkers: any[] = [];
|
||||
const path: Array<{ lat: number; lng: number }> = [];
|
||||
|
||||
for (let i = 0; i < stops.length; i++) {
|
||||
const stop = stops[i];
|
||||
if (!stop) continue;
|
||||
|
||||
// Verificación atómica en cada paso
|
||||
if (mappingSequenceId.value !== thisSeq || !routeStore.selectedRouteId) {
|
||||
newMarkers.forEach(m => { if (m.setMap) m.setMap(null); });
|
||||
return;
|
||||
}
|
||||
|
||||
const marker = addNumberedMarker(
|
||||
{ lat: stop.latitude, lng: stop.longitude },
|
||||
i + 1,
|
||||
stop.name,
|
||||
() => handleBusStopClick(stop)
|
||||
);
|
||||
|
||||
if (marker) newMarkers.push(marker);
|
||||
path.push({ lat: stop.latitude, lng: stop.longitude });
|
||||
}
|
||||
|
||||
// Final check before committing to the map
|
||||
if (mappingSequenceId.value !== thisSeq || !routeStore.selectedRouteId) {
|
||||
newMarkers.forEach(m => { if (m.setMap) m.setMap(null); });
|
||||
return;
|
||||
}
|
||||
|
||||
clearMapMarkers();
|
||||
markers.value = newMarkers;
|
||||
limpiarRuta();
|
||||
|
||||
if (path.length > 0) fitBounds(path);
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ JARVIS: Error en updateMapMarkers:', err);
|
||||
} finally {
|
||||
if (mappingSequenceId.value === thisSeq) {
|
||||
isUpdatingMarkers.value = false;
|
||||
if (routeStore.selectedRouteId) updateMarkersStyles(true);
|
||||
let pastStops: any[] = [];
|
||||
let relevantStops: any[] = [...stops];
|
||||
|
||||
if (paradaCercana.value) {
|
||||
const idx = stops.findIndex(s => s.id === paradaCercana.value?.id);
|
||||
if (idx > 0) {
|
||||
pastStops = stops.slice(0, idx + 1); // overlap that 1 point for continuous mapping
|
||||
relevantStops = stops.slice(idx);
|
||||
}
|
||||
}
|
||||
|
||||
const newMarkers: any[] = [];
|
||||
|
||||
// Paradas del tramo relevante: mostrar con clean markers
|
||||
relevantStops.forEach((stop, index) => {
|
||||
let tipo: 'normal' | 'cercana' | 'origen' | 'destino' = 'normal';
|
||||
|
||||
if (paradaCercana.value && stop.id === paradaCercana.value.id) tipo = 'cercana';
|
||||
else if (index === relevantStops.length - 1) tipo = 'destino';
|
||||
else if (!paradaCercana.value && index === 0) tipo = 'origen';
|
||||
|
||||
const marker = addCleanMarker(
|
||||
{ lat: stop.latitude, lng: stop.longitude },
|
||||
stop.name,
|
||||
tipo,
|
||||
() => handleBusStopClick(stop)
|
||||
);
|
||||
if (marker) newMarkers.push(marker);
|
||||
});
|
||||
|
||||
markers.value = newMarkers;
|
||||
|
||||
// Dibujar en paralelo ambos tramos
|
||||
const renderPromises = [];
|
||||
if (pastStops.length > 1 && map.value) {
|
||||
renderPromises.push(trazarRuta(pastStops.map((p, i) => ({
|
||||
id: i, nombre: p.name, latitud: p.latitude, longitud: p.longitude, orden: i
|
||||
})), map.value, true));
|
||||
}
|
||||
if (relevantStops.length > 1 && map.value) {
|
||||
renderPromises.push(trazarRuta(relevantStops.map((p, i) => ({
|
||||
id: i, nombre: p.name, latitud: p.latitude, longitud: p.longitude, orden: i
|
||||
})), map.value, false));
|
||||
}
|
||||
await Promise.all(renderPromises);
|
||||
|
||||
// Zoom automático al tramo que le importa al usuario
|
||||
if (map.value) {
|
||||
const bounds = new google.maps.LatLngBounds();
|
||||
if (userCoords.value) {
|
||||
bounds.extend(userCoords.value);
|
||||
}
|
||||
relevantStops.forEach(p => bounds.extend({ lat: p.latitude, lng: p.longitude }));
|
||||
|
||||
// Timeout para que los directions renderers también ajusten bounds si preserveViewport estaba false (actualmente es true)
|
||||
setTimeout(() => {
|
||||
if (map.value && bounds.getNorthEast() && bounds.getSouthWest()) {
|
||||
map.value.fitBounds(bounds, { top: 80, bottom: 120, left: 20, right: 20 });
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ JARVIS: Error en updateMapMarkers:', err);
|
||||
} finally {
|
||||
if (mappingSequenceId.value === thisSeq) {
|
||||
isUpdatingMarkers.value = false;
|
||||
// updateMarkersStyles NO hace falta para "clean markers". Lo mantenemos en caso sea forzado.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimización de rendimiento: Solo actualiza los iconos si cambiamos de modo (punto vs pin)
|
||||
* o si se fuerza la actualización (ej: al cargar nueva ruta)
|
||||
*/
|
||||
function updateMarkersStyles(force = false) {
|
||||
if (!map.value || markers.value.length === 0 || !routeStore.selectedRouteId) return;
|
||||
|
||||
const currentZoom = map.value.getZoom() || 12;
|
||||
const newMode = currentZoom >= 15 ? 'pin' : 'dot';
|
||||
|
||||
if (!force && currentMarkerMode.value === newMode) return;
|
||||
|
||||
currentMarkerMode.value = newMode;
|
||||
const showNumbers = newMode === 'pin';
|
||||
|
||||
console.log(`🤖 JARVIS: Actualizando estilos de marcadores a modo: ${newMode}`);
|
||||
|
||||
markers.value.forEach((marker: any, index: number) => {
|
||||
if (!marker) return;
|
||||
|
||||
// Si la secuencia cambió o la ruta desapareció mientras hacíamos esto, abortamos
|
||||
if (!routeStore.selectedRouteId) {
|
||||
if (marker.setMap) marker.setMap(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (showNumbers) {
|
||||
// MODO PREMIUM: Círculo Amarillo con Borde Negro y Numero Negro
|
||||
marker.setIcon({
|
||||
path: google.maps.SymbolPath.CIRCLE,
|
||||
fillColor: '#FEE715',
|
||||
fillOpacity: 1,
|
||||
strokeColor: '#101820',
|
||||
strokeWeight: 2.5,
|
||||
scale: 14, // Tamaño ideal para leer el número dentro
|
||||
});
|
||||
marker.setLabel({
|
||||
text: (index + 1).toString(),
|
||||
color: '#101820',
|
||||
fontSize: '13px',
|
||||
fontWeight: '900',
|
||||
});
|
||||
} else {
|
||||
// MODO COMPACTO: Punto Amarillo brillante con anillo de profundidad
|
||||
marker.setIcon({
|
||||
path: google.maps.SymbolPath.CIRCLE,
|
||||
fillColor: '#FEE715',
|
||||
fillOpacity: 1,
|
||||
strokeColor: '#101820',
|
||||
strokeWeight: 1.5,
|
||||
scale: 7,
|
||||
});
|
||||
marker.setLabel(null);
|
||||
}
|
||||
});
|
||||
|
||||
// Dibujar la ruta usando Directions API cuando se actualicen los marcadores
|
||||
if (routeStore.selectedRouteId && map.value) {
|
||||
const stopsForDirections = markers.value.map((m, i) => {
|
||||
const pos = m.getPosition();
|
||||
return {
|
||||
id: i, // ID temporal para trazar logic
|
||||
nombre: `Stop ${i+1}`,
|
||||
latitud: pos.lat(),
|
||||
longitud: pos.lng(),
|
||||
orden: i
|
||||
};
|
||||
});
|
||||
trazarRuta(stopsForDirections, map.value);
|
||||
}
|
||||
function updateMarkersStyles() {
|
||||
// Empty space: Clean markers are static and distinct per requirement.
|
||||
}
|
||||
|
||||
// La función drawRouteOnRoad ha sido eliminada por petición del usuario (línea azul removida)
|
||||
@ -652,11 +617,6 @@ function selectRouteAndClose(routeId: string, routeName: string) {
|
||||
routeStore.selectRoute(routeId, routeName);
|
||||
showRouteDropdown.value = false;
|
||||
showUberSearch.value = false; // Close the expanded search panel
|
||||
|
||||
// Si no tenemos ubicación, la pedimos para poder calcular la parada óptima automáticamente
|
||||
if (!userCoords.value) {
|
||||
locateUser();
|
||||
}
|
||||
}
|
||||
async function updateActiveUnits() {
|
||||
if (!isLoaded.value) return;
|
||||
@ -690,8 +650,13 @@ const optimalSonarHtml = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
function locateUser() {
|
||||
if (navigator.geolocation) {
|
||||
function locateUser(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (!navigator.geolocation) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const { latitude, longitude } = position.coords;
|
||||
@ -715,13 +680,19 @@ function locateUser() {
|
||||
sonarHtml,
|
||||
{ x: -30, y: -30 }
|
||||
);
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
console.error("Error getting location", error);
|
||||
alert("No se pudo obtener tu ubicación. Por favor, verifica tus permisos de GPS.");
|
||||
console.warn("SIBU | Geolocalización denegada:", error.message);
|
||||
resolve();
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 8000,
|
||||
maximumAge: 30000
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -730,6 +701,10 @@ function locateUser() {
|
||||
* y la resalta para el usuario.
|
||||
*/
|
||||
async function highlightOptimalStopForRoute() {
|
||||
if (!userCoords.value) {
|
||||
await locateUser();
|
||||
}
|
||||
|
||||
if (!userCoords.value || routeStore.selectedRouteStops.length === 0) {
|
||||
console.warn('🤖 JARVIS: Sin ubicación o paradas para calcular parada óptima.');
|
||||
return;
|
||||
@ -744,9 +719,7 @@ async function highlightOptimalStopForRoute() {
|
||||
const stopObj = paradaCercana.value as BusStop;
|
||||
console.log(`🤖 JARVIS: Parada óptima detectada: ${stopObj.name}`);
|
||||
|
||||
// Centrar mapa
|
||||
setCenter(stopObj.latitude, stopObj.longitude);
|
||||
setZoom(17);
|
||||
// Ya no centramos o hacemos zoom aquí manual porque la nueva gráfica de updateMapMarkers ajusta bounds y engloba location.
|
||||
|
||||
// Añadir el PULSO NARANJA
|
||||
if (optimalStopPulse.value && typeof optimalStopPulse.value.setMap === 'function') {
|
||||
@ -759,13 +732,6 @@ async function highlightOptimalStopForRoute() {
|
||||
{ x: -30, y: -30 }
|
||||
);
|
||||
|
||||
// Mini-notificación (Opcional, se cubre ahora también con ETA card)
|
||||
navigationInfo.value = {
|
||||
distance: distanciaMetros.value < 1000 ? `${distanciaMetros.value.toFixed(0)} m` : `${(distanciaMetros.value/1000).toFixed(1)} km`,
|
||||
duration: "Calculada",
|
||||
targetName: stopObj.name
|
||||
};
|
||||
|
||||
// Calcular ETAs
|
||||
await calcularETA(routeStore.selectedRouteId!, stopObj);
|
||||
showETACard.value = true;
|
||||
@ -837,13 +803,9 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
|
||||
const route = dirResult.routes[0];
|
||||
const leg = route.legs?.[0];
|
||||
|
||||
// Guardar info de navegación (ETA y Distancia)
|
||||
// Guardar info de navegación (ETA y Distancia) (Retirado a favor de ETA Card / Parada Cercana Banner)
|
||||
if (leg) {
|
||||
navigationInfo.value = {
|
||||
distance: leg.distance?.text || '---',
|
||||
duration: leg.duration?.text || '---',
|
||||
targetName: targetStop.name
|
||||
};
|
||||
// console.log('Distancia', leg.distance?.text);
|
||||
}
|
||||
|
||||
if (walkingPolyline.value) walkingPolyline.value.setMap(null);
|
||||
@ -881,11 +843,6 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
|
||||
});
|
||||
}
|
||||
|
||||
function clearNavigation() {
|
||||
clearMapMarkers();
|
||||
navigationInfo.value = null;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -893,7 +850,7 @@ function clearNavigation() {
|
||||
<!-- Main Map Container -->
|
||||
<div class="map-side">
|
||||
<div class="map-view">
|
||||
<!-- Status overlay para SIBU Directions API -->
|
||||
<!-- Status overlay para SIBU Directions API -->
|
||||
<div v-if="estasCargandoRuta || errorRuta" class="status-indicator">
|
||||
<div v-if="estasCargandoRuta" class="loading-pill">
|
||||
Calculando ruta real...
|
||||
@ -903,6 +860,32 @@ function clearNavigation() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Banner de Parada Más Cercana Inteligente -->
|
||||
<div
|
||||
v-if="paradaCercana && routeStore.selectedRouteId"
|
||||
class="fixed left-0 right-0 z-40 px-3 transition-transform duration-300 pointer-events-none"
|
||||
:style="{ top: alturaNavbar + 'px' }"
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-900 rounded-b-2xl shadow-xl border-t-4 border-blue-600 border-t-blue-600 p-3 flex items-center gap-3 pointer-events-auto">
|
||||
<div class="bg-blue-100 dark:bg-blue-900/40 rounded-full p-2 shrink-0">
|
||||
<span class="material-icons text-blue-600 dark:text-blue-400">directions_bus</span>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-[11px] text-gray-500 font-bold uppercase">Parada más cercana</p>
|
||||
<p class="text-sm font-bold text-gray-800 dark:text-white truncate">
|
||||
{{ paradaCercana?.name }}
|
||||
</p>
|
||||
<p class="text-xs text-blue-600 dark:text-blue-400 font-medium whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{{ (distanciaMetros && distanciaMetros < 1000) ? Math.round(distanciaMetros) + ' m' : (distanciaMetros ? (distanciaMetros / 1000).toFixed(1) + ' km' : '') }}
|
||||
<span v-if="duracionCaminata">· {{ Math.round(duracionCaminata / 60) }} min caminando</span>
|
||||
</p>
|
||||
</div>
|
||||
<button @click="paradaCercana = null" class="text-gray-400 hover:text-gray-600 shrink-0 p-1">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="map-container">
|
||||
<!-- Floating Offers Button at exact location -->
|
||||
<div v-if="mapsError" class="error">
|
||||
@ -975,27 +958,6 @@ function clearNavigation() {
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Google Maps Style Navigation Summary Card -->
|
||||
<Transition name="uber-slide">
|
||||
<div v-if="navigationInfo" class="navigation-summary-card">
|
||||
<div class="nav-card-accent"></div>
|
||||
<div class="nav-content">
|
||||
<div class="nav-left">
|
||||
<div class="nav-stats">
|
||||
<span class="nav-time">{{ navigationInfo.duration }}</span>
|
||||
<span class="nav-dist">{{ navigationInfo.distance }}</span>
|
||||
</div>
|
||||
<div class="nav-destination">Parada: {{ navigationInfo.targetName }}</div>
|
||||
</div>
|
||||
<div class="nav-actions">
|
||||
<button class="nav-btn-close" @click="clearNavigation">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Uber-style Search Panel -->
|
||||
<Transition name="uber-slide">
|
||||
<div v-if="showUberSearch" class="uber-search-panel" :class="{ 'is-focused': isInputFocused }">
|
||||
|
||||
Reference in New Issue
Block a user