se crea ra un componente ContenedorMapas.tsx que tendra toda la logica, a continuacion el codigo:
import { useState } from 'react';
import DraggableMapContainer from './ContenedorMapasAnimado';
// Componente funcional ContenedorMapas
function ContenedorMapas() {
// Estado para almacenar la lista de mapas. Inicialmente contiene un mapa con ID 1.
const [maps, setMaps] = useState([{ id: 1 }]);
// Estado para rastrear el ID del mapa que está actualmente activo o en primer plano.
const [activeMapId, setActiveMapId] = useState(1);
// Estado para generar el ID único del próximo mapa que se agregue.
const [nextId, setNextId] = useState(2);
// Función para añadir un nuevo mapa a la lista.
const addMap = () => {
// Obtiene el próximo ID disponible.
const newId = nextId;
// Actualiza el estado de 'maps' añadiendo un nuevo objeto de mapa con el nuevo ID.
// Se utiliza el spread operator (...) para incluir los mapas existentes y luego añadir el nuevo.
setMaps([...maps, { id: newId }]);
// Establece el nuevo mapa como el mapa activo.
setActiveMapId(newId);
// Incrementa el 'nextId' para el siguiente mapa que se añada.
setNextId(newId + 1);
};
// Función para eliminar un mapa de la lista basado en su ID.
const removeMap = (id) => {
// Crea un nuevo array de mapas que no incluye el mapa con el ID proporcionado.
const newMaps = maps.filter(map => map.id !== id);
// Actualiza el estado de 'maps' con el nuevo array filtrado.
setMaps(newMaps);
// Si el mapa que se eliminó era el mapa activo,
if (id === activeMapId && newMaps.length > 0) {
// y si todavía quedan mapas en la lista, activa el último mapa disponible.
setActiveMapId(newMaps[newMaps.length - 1].id);
}
};
// Renderiza la estructura del componente.
return (
<div style={{
width: '100vw', // Ancho del 100% del viewport.
height: '100vh', // Alto del 100% del viewport.
position: 'relative', // Permite posicionar elementos hijos de forma absoluta o relativa a este contenedor.
background: '#f0f0f0', // Color de fondo gris claro.
overflow: 'hidden' // Oculta cualquier contenido que se salga de los límites del contenedor.
}}>
{/* Botón para añadir nuevos mapas */}
<button
onClick={addMap} // Llama a la función 'addMap' cuando se hace clic.
style={{
position: 'fixed', // Se mantiene en una posición fija en la ventana del navegador.
top: '20px', // A 20 píxeles del borde superior.
right: '20px', // A 20 píxeles del borde derecho.
zIndex: 2000, // Asegura que el botón esté por encima de otros elementos (mayor valor de z-index).
padding: '10px 15px', // Espaciado interno del botón.
background: '#4CAF50', // Color de fondo verde.
color: 'white', // Color del texto blanco.
border: 'none', // Sin borde.
borderRadius: '4px', // Bordes ligeramente redondeados.
cursor: 'pointer', // Cambia el cursor al pasar por encima, indicando que es interactivo.
fontSize: '16px' // Tamaño de la fuente.
}}
>
+ Añadir Mapa
</button>
{/* Contador de mapas */}
<div style={{
position: 'fixed', // Se mantiene en una posición fija en la ventana del navegador.
top: '20px', // A 20 píxeles del borde superior.
left: '20px', // A 20 píxeles del borde izquierdo.
zIndex: 2000, // Asegura que el contador esté por encima de otros elementos.
background: 'rgba(0,0,0,0.7)', // Fondo negro semi-transparente.
color: 'white', // Color del texto blanco.
padding: '10px 15px', // Espaciado interno.
borderRadius: '4px' // Bordes redondeados.
}}>
Mapas abiertos: {maps.length} {/* Muestra la cantidad de mapas en el estado 'maps'. */}
</div>
{/* Renderizar todos los mapas */}
{maps.map((map) => (
<DraggableMapContainer
key={map.id} // Propiedad 'key' necesaria para que React pueda identificar de forma única cada elemento en la lista.
isActive={activeMapId === map.id} // Pasa un booleano para indicar si este mapa es el activo.
onActivate={() => setActiveMapId(map.id)} // Función que se llama cuando este mapa se activa. Actualiza el 'activeMapId'.
onClose={() => removeMap(map.id)} // Función que se llama cuando se solicita cerrar este mapa. Llama a 'removeMap' con el ID del mapa.
/>
))}
</div>
);
}
export default ContenedorMapas;
Siguiente componente :
import { useState, useRef, useEffect } from 'react';
import MapView from '../componentsAnimacionMaps/MapViewAnimado';
// Componente funcional DraggableMapContainer que permite arrastrar y redimensionar un contenedor de mapa.
const DraggableMapContainer = ({ isActive, onActivate, onClose }) => {
// Estado para la posición del contenedor del mapa. Se inicializa con coordenadas aleatorias dentro del 20% del ancho y alto de la ventana.
const [position, setPosition] = useState({
x: Math.random() * window.innerWidth * 0.2,
y: Math.random() * window.innerHeight * 0.2
});
// Estado para el tamaño del contenedor del mapa. Se inicializa con un 60% del ancho y alto del viewport.
const [size, setSize] = useState({ width: '60vw', height: '60vh' });
// Estado para indicar si el contenedor está siendo arrastrado.
const [isDragging, setIsDragging] = useState(false);
// Estado para indicar si el contenedor está siendo redimensionado.
const [isResizing, setIsResizing] = useState(false);
// Referencia mutable para almacenar el desplazamiento del ratón al iniciar el arrastre.
const dragOffset = useRef({ x: 0, y: 0 });
// Referencia mutable para almacenar las dimensiones y la posición inicial al iniciar el redimensionamiento.
const resizeStart = useRef({ x: 0, y: 0, width: 0, height: 0 });
// Referencia al elemento div del contenedor del mapa.
const containerRef = useRef(null);
// Efecto para resetear los estados de arrastre y redimensionamiento cuando el mapa se desactiva.
useEffect(() => {
// Si el mapa ya no está activo y estaba siendo arrastrado o redimensionado,
if (!isActive && (isDragging || isResizing)) {
// se detiene el arrastre y el redimensionamiento.
setIsDragging(false);
setIsResizing(false);
// Se restablece el cursor del body al estado por defecto.
document.body.style.cursor = '';
}
}, [isActive, isDragging, isResizing]); // Dependencias del efecto: se ejecuta cuando cambia alguna de estas variables.
// Función que se ejecuta al hacer clic en el control de redimensionamiento.
const handleResizeMouseDown = (e) => {
// Si el mapa no está activo, no se permite el redimensionamiento.
if (!isActive) return;
// Detiene la propagación del evento para evitar que se active el arrastre del mapa.
e.stopPropagation();
// Establece el estado de redimensionamiento a verdadero.
setIsResizing(true);
// Almacena la posición del cursor y las dimensiones actuales del contenedor al inicio del redimensionamiento.
resizeStart.current = {
x: e.clientX,
y: e.clientY,
width: parseInt(size.width),
height: parseInt(size.height)
};
// Cambia el cursor del body para indicar que se está redimensionando.
document.body.style.cursor = 'nwse-resize';
};
// Función que se ejecuta al hacer clic con el botón secundario (por defecto) en el contenedor del mapa para iniciar el arrastre.
const handleMouseDown = (e) => {
// Llama a la función 'onActivate' proporcionada por el componente padre para indicar que este mapa se ha activado.
onActivate();
// Solo permite el arrastre con el botón secundario del ratón.
if (e.button !== 2) return;
// Previene el comportamiento predeterminado del botón secundario (menú contextual).
e.preventDefault();
// Establece el estado de arrastre a verdadero.
setIsDragging(true);
// Calcula la diferencia entre la posición del cursor y la posición actual del contenedor para mantener el punto de agarre.
dragOffset.current = {
x: e.clientX - position.x,
y: e.clientY - position.y
};
// Cambia el cursor del body para indicar que se está arrastrando.
document.body.style.cursor = 'grabbing';
};
// Función que se ejecuta cuando se mueve el ratón.
const handleMouseMove = (e) => {
// Si se está redimensionando y el mapa está activo,
if (isResizing && isActive) {
// calcula la diferencia en la posición del cursor desde el inicio del redimensionamiento.
const deltaX = e.clientX - resizeStart.current.x;
const deltaY = e.clientY - resizeStart.current.y;
// Actualiza el estado del tamaño del contenedor, asegurando que no sea menor a ciertos límites.
setSize({
width: `${Math.max(300, resizeStart.current.width + deltaX)}px`,
height: `${Math.max(200, resizeStart.current.height + deltaY)}px`
});
}
// Si se está arrastrando y el mapa está activo,
else if (isDragging && isActive) {
// actualiza el estado de la posición del contenedor basándose en la posición actual del cursor y el desplazamiento inicial.
setPosition({
x: e.clientX - dragOffset.current.x,
y: e.clientY - dragOffset.current.y
});
}
};
// Función que se ejecuta cuando se suelta el botón del ratón.
const handleMouseUp = () => {
// Detiene el arrastre y el redimensionamiento.
setIsDragging(false);
setIsResizing(false);
// Restablece el cursor del body a 'grab' si el mapa está activo, o a 'default' si no lo está.
document.body.style.cursor = isActive ? 'grab' : 'default';
};
// Efecto para manejar los eventos globales de mouseup y mousemove.
useEffect(() => {
// Función interna para manejar el evento global de mouseup.
const handleGlobalMouseUp = () => {
// Si se estaba arrastrando o redimensionando, llama a la función local handleMouseUp para detenerlo.
if (isDragging || isResizing) {
handleMouseUp();
}
};
// Añade event listeners para los eventos 'mouseup' y 'mousemove' en la ventana.
window.addEventListener('mouseup', handleGlobalMouseUp);
window.addEventListener('mousemove', handleMouseMove);
// Función de limpieza del efecto: se ejecuta cuando el componente se desmonta o antes de que el efecto se vuelva a ejecutar.
return () => {
// Remueve los event listeners para evitar fugas de memoria.
window.removeEventListener('mouseup', handleGlobalMouseUp);
window.removeEventListener('mousemove', handleMouseMove);
};
}, [isDragging, isResizing, isActive]); // Dependencias del efecto: se vuelve a ejecutar si cambia alguno de estos estados.
// Renderiza el contenedor del mapa.
return (
<div
ref={containerRef} // Asigna la referencia al elemento div.
className="draggable-map"
style={{
position: 'fixed', // Permite la manipulación de la posición con 'top' y 'left'.
left: `${position.x}px`, // Posición horizontal del contenedor.
top: `${position.y}px`, // Posición vertical del contenedor.
width: size.width, // Ancho del contenedor.
height: size.height, // Alto del contenedor.
cursor: isActive ? (isDragging ? 'grabbing' : 'grab') : 'default', // Cambia el cursor dependiendo del estado de actividad y arrastre.
boxShadow: isActive // Aplica una sombra más prominente si el mapa está activo.
? '0 0 0 3px #4a90e2, 0 4px 20px rgba(0,0,0,0.3)'
: '0 4px 10px rgba(0,0,0,0.2)',
borderRadius: '8px', // Bordes redondeados.
overflow: 'hidden', // Oculta el contenido que se desborda.
zIndex: isActive ? 1000 : 100, // Asegura que el mapa activo esté por encima de los demás.
transition: (isDragging || isResizing) ? 'none' : 'all 0.2s ease', // Aplica una transición suave a los cambios de estilo, excepto durante el arrastre o redimensionamiento.
opacity: isActive ? 1 : 0.9, // Reduce ligeramente la opacidad si el mapa no está activo.
minWidth: '300px', // Ancho mínimo del contenedor.
minHeight: '200px' // Alto mínimo del contenedor.
}}
onMouseDown={handleMouseDown} // Asigna la función para iniciar el arrastre al evento mousedown.
onContextMenu={(e) => e.preventDefault()} // Previene la aparición del menú contextual al hacer clic derecho.
>
{/* Componente que renderiza el mapa */}
<MapView />
{/* Barra de título del mapa */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '40px',
background: 'rgba(0,0,0,0.7)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0 10px',
color: 'white',
zIndex: 10 // Asegura que la barra de título esté por encima del mapa.
}}>
<span>{isActive ? "Mapa activo" : "Mapa"}</span> {/* Muestra un texto diferente si el mapa está activo. */}
<button
onClick={onClose} // Llama a la función 'onClose' proporcionada por el padre para eliminar el mapa.
style={{
background: '#ff4444',
color: 'white',
border: 'none',
borderRadius: '4px',
padding: '4px 8px',
cursor: 'pointer'
}}
>
Cerrar
</button>
</div>
{/* Control de redimensionamiento (solo visible si el mapa está activo) */}
{isActive && (
<div
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: '20px',
height: '20px',
background: '#4a90e2',
cursor: 'nwse-resize',
zIndex: 10 // Asegura que el control de redimensionamiento esté por encima del mapa.
}}
onMouseDown={handleResizeMouseDown} // Asigna la función para iniciar el redimensionamiento al evento mousedown.
/>
)}
</div>
);
};
export default DraggableMapContainer;
siguiente componente
import React, { memo, lazy, Suspense, Profiler, useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { MapContainer, TileLayer, LayersControl } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
// 1. Error Boundary optimizado
// ErrorBoundary es un componente que captura errores de renderizado en sus hijos.
// 'memo' se utiliza para optimizar el componente, evitando re-renderizados innecesarios si las props no cambian.
const ErrorBoundary = memo(
class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
// Define el estado inicial del componente, indicando si ha ocurrido un error.
state = { hasError: false };
// Método estático invocado cuando un error ocurre durante el renderizado de un componente hijo.
static getDerivedStateFromError(error: Error) {
// Registra el mensaje del error en la consola.
console.error('MapError:', error.message);
// Actualiza el estado para indicar que ha ocurrido un error.
return { hasError: true };
}
// Método invocado después de que un error ha sido lanzado por un componente descendiente.
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Registra la información de la pila del componente donde ocurrió el error.
console.error('Component stack:', errorInfo.componentStack);
}
// Método de renderizado del componente.
render() {
// Si el estado 'hasError' es verdadero, renderiza una interfaz de usuario de fallback.
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Error en el mapa</h2>
{/* Botón para intentar restablecer el estado de error y re-renderizar los hijos. */}
<button onClick={() => this.setState({ hasError: false })}>
Reintentar
</button>
</div>
);
}
// Si no hay error, renderiza los componentes hijos que fueron pasados como props.
return this.props.children;
}
}
);
// 2. Precarga de CapaBase
// Variable para controlar si la CapaBase ya ha sido precargada.
let capaBasePreloaded = false;
// Función para iniciar la carga del módulo CapaBase de forma asíncrona.
const preloadCapaBase = () => {
// Si la CapaBase aún no ha sido precargada,
if (!capaBasePreloaded) {
// Inicia la importación del módulo. Esto dispara la carga del código en segundo plano.
import('./CapaBase');
// Marca la CapaBase como precargada para evitar importaciones duplicadas.
capaBasePreloaded = true;
}
};
// 3. Componentes lazy con precarga
// 'lazy' permite cargar componentes solo cuando son necesarios, mejorando el rendimiento inicial.
// CapaBase se carga de forma lazy, y antes de la carga real, se llama a 'preloadCapaBase' para iniciar la descarga anticipada.
const CapaBase = lazy(() => {
preloadCapaBase();
return import('./CapaBase');
});
// CapaFlotante también se carga de forma lazy.
const CapaFlotante = lazy(() => import('./CapaFlotante'));
// 4. Componente principal optimizado
// Componente funcional MapView que renderiza el mapa y sus capas.
const MapView = () => {
// Estado para controlar si la CapaBase ha sido "cargada" (para la lógica de renderizado condicional).
const [baseLoaded, setBaseLoaded] = useState(false);
// Estado para controlar si otras capas (como CapaFlotante) han sido "cargadas".
const [otherLayersLoaded, setOtherLayersLoaded] = useState(false);
// Referencia para acceder a la instancia del componente MapContainer de Leaflet.
const mapRef = useRef<any>(null);
// Referencia para acceder a la instancia del componente TileLayer de Leaflet.
const tileLayerRef = useRef<any>(null);
// Referencia para almacenar el ID del frame de animación para CapaBase.
const animationFrameIdBase = useRef<number | null>(null);
// Referencia para almacenar el ID del frame de animación para CapaFlotante.
const animationFrameIdFlotante = useRef<number | null>(null);
// Efecto para precargar CapaBase y simular una carga inicial rápida.
useEffect(() => {
// Inicia la precarga de CapaBase al montar el componente.
preloadCapaBase();
// Simula una carga rápida de la capa base para habilitar la carga de otras capas.
const timer = setTimeout(() => setBaseLoaded(true), 10);
// Limpia el timeout si el componente se desmonta antes de que termine.
return () => clearTimeout(timer);
}, []); // Se ejecuta solo una vez al montar el componente.
// Efecto para iniciar la carga de otras capas una vez que la capa base se considera "cargada".
useEffect(() => {
// Si la capa base ha sido marcada como cargada,
if (baseLoaded) {
// Simula un pequeño retraso antes de marcar otras capas como cargadas.
const timer = setTimeout(() => setOtherLayersLoaded(true), 100);
// Limpia el timeout si el componente se desmonta antes de que termine.
return () => clearTimeout(timer);
}
}, [baseLoaded]); // Se ejecuta cuando el estado 'baseLoaded' cambia.
// Configuraciones del MapContainer memoizadas para evitar re-creaciones innecesarias.
const mapOptions = useMemo(() => ({
center: [19.285653, -99.147809] as [number, number], // Centro inicial del mapa (Ciudad de México).
zoom: 5, // Nivel de zoom inicial.
preferCanvas: true, // Utiliza Canvas para renderizar los tiles (puede mejorar el rendimiento).
fadeAnimation: false, // Deshabilita la animación de fundido al cargar nuevos tiles.
attributionControl: false, // Oculta el control de atribución de Leaflet.
zoomControl: false, // Oculta los controles de zoom por defecto.
maxZoom: 18, // Nivel máximo de zoom permitido.
minZoom: 2, // Nivel mínimo de zoom permitido.
maxBoundsViscosity: 1.0, // Fuerza el mapa a mantenerse dentro de los límites máximos.
maxBounds: [
[-85, -175], // Suroeste de los límites del mapa.
[85, 175] // Noreste de los límites del mapa.
],
worldCopyJump: false // Deshabilita el salto de copia del mundo al hacer zoom out.
}), []);
// Props del TileLayer memoizadas.
const tileLayerProps = useMemo(() => ({
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', // URL de los tiles de OpenStreetMap.
attribution: '© OpenStreetMap contributors', // Atribución requerida por OpenStreetMap.
updateWhenIdle: true, // Actualiza los tiles solo cuando el mapa está quieto.
updateWhenZooming: false, // No actualiza los tiles durante el zoom (mejora la fluidez).
keepBuffer: 4, // Número de tiles a mantener en el buffer alrededor de la vista actual.
maxNativeZoom: 19, // Nivel de zoom nativo máximo de los tiles.
minZoom: 2, // Nivel mínimo de zoom para mostrar los tiles.
noWrap: true, // Evita la repetición horizontal de los tiles.
detectRetina: false, // Deshabilita la detección de pantallas Retina.
bounds: [
[-85, -175], // Suroeste de los límites de los tiles.
[85, 175] // Noreste de los límites de los tiles.
]
}), []);
// Estilos del contenedor del mapa memoizados.
const mapStyles = useMemo(() => ({
position: 'absolute', // Permite que el mapa ocupe todo el contenedor padre.
top: 0,
left: 0,
right: 0,
bottom: 0,
overflow: 'hidden', // Oculta cualquier contenido que se salga de los límites.
touchAction: 'none', // Deshabilita las acciones táctiles predeterminadas (mejora el control del mapa).
zIndex: 0, // Asegura que el mapa esté en la capa base.
willChange: 'transform', // Indica al navegador que la propiedad 'transform' va a cambiar (optimización).
backfaceVisibility: 'hidden', // Oculta la parte posterior del elemento durante las transformaciones 3D (optimización).
backgroundColor: '#f0f0f0' // Color de fondo sólido del mapa.
}), []);
// Función para manejar la animación de CapaBase (actualmente vacía).
const animateBaseLayers = (timestamp: number) => {
// Aquí puedes agregar la lógica específica para CapaBase
animationFrameIdBase.current = requestAnimationFrame(animateBaseLayers);
};
// Función para manejar la animación de CapaFlotante.
const animateFlotanteLayers = (timestamp: number) => {
try {
// Aquí puedes agregar la lógica específica para CapaFlotante
} catch (error) {
console.error('Error en requestAnimationFrame de CapaFlotante:', error);
}
animationFrameIdFlotante.current = requestAnimationFrame(animateFlotanteLayers);
};
// Efecto para iniciar el loop de animación de CapaBase al montar el componente.
useEffect(() => {
animationFrameIdBase.current = requestAnimationFrame(animateBaseLayers);
// Función de limpieza para cancelar el frame de animación cuando el componente se desmonta.
return () => {
if (animationFrameIdBase.current !== null) {
cancelAnimationFrame(animationFrameIdBase.current);
}
};
}, []); // Se ejecuta solo una vez al montar.
// Efecto para iniciar el loop de animación de CapaFlotante cuando otras capas se consideran cargadas.
useEffect(() => {
if (otherLayersLoaded) {
animationFrameIdFlotante.current = requestAnimationFrame(animateFlotanteLayers);
}
// Función de limpieza para cancelar el frame de animación.
return () => {
if (animationFrameIdFlotante.current !== null) {
cancelAnimationFrame(animationFrameIdFlotante.current);
}
};
}, [otherLayersLoaded]); // Se ejecuta cuando 'otherLayersLoaded' cambia.
// Función para manejar los cambios en la vista del mapa (zoom, pan).
const handleViewportChanged = useCallback(() => {
// Puedes agregar lógica aquí para responder a los cambios en la vista del mapa.
// Por ejemplo, actualizar el estado o realizar otras acciones.
}, []); // 'useCallback' memoiza la función para evitar re-creaciones innecesarias.
return (
// Envuelve el contenido con el ErrorBoundary para capturar cualquier error en los componentes hijos.
<ErrorBoundary>
<div className="map-container">
{/* Componente principal de react-leaflet que contiene el mapa. */}
<MapContainer
{...mapOptions} // Pasa las opciones memoizadas al MapContainer.
ref={mapRef} // Asigna la referencia al componente MapContainer.
style={mapStyles} // Aplica los estilos memoizados.
whenCreated={(map) => {
mapRef.current = map; // Guarda la instancia del mapa en la referencia.
map.on('moveend', handleViewportChanged); // Asigna el listener para el evento 'moveend' del mapa.
}}
>
{/* Control para agrupar las capas (base y overlay). */}
<LayersControl position="topright">
{/* Capa base del mapa (OpenStreetMap). */}
<LayersControl.BaseLayer name="OpenStreetMap" checked>
{/* Componente para renderizar los tiles del mapa. */}
<TileLayer ref={tileLayerRef} {...tileLayerProps} /> {/* Pasa las props memoizadas al TileLayer. */}
</LayersControl.BaseLayer>
{/* Renderiza el componente CapaBase solo cuando 'baseLoaded' es verdadero. */}
{baseLoaded && (
// Suspense permite mostrar un fallback mientras el componente lazy se carga.
<Suspense fallback={null}>
{/* Profiler mide el tiempo de renderizado del componente hijo. */}
<Profiler
id="CapaBase"
// Función llamada después de que CapaBase se renderiza.
onRender={(id, phase, actualDuration) => {
// Registra en la consola si el tiempo de renderizado excede un umbral.
if (actualDuration > 16) {
console.log(`CapaBase render time: ${actualDuration.toFixed(2)}ms`);
}
}}
>
{/* Componente CapaBase cargado de forma lazy. */}
<CapaBase />
</Profiler>
</Suspense>
)}
{/* Renderiza otras capas (CapaFlotante) solo cuando 'otherLayersLoaded' es verdadero. */}
{otherLayersLoaded && (
<Suspense fallback={null}>
{/* Envuelve CapaFlotante con otro ErrorBoundary para manejar errores específicos de esta capa. */}
<ErrorBoundary>
<Profiler
id="CapaFlotante"
onRender={(id, phase, actualDuration) => {
if (actualDuration > 16) {
console.log(`CapaFlotante render time: ${actualDuration.toFixed(2)}ms`);
}
}}
>
{/* Componente CapaFlotante cargado de forma lazy. */}
<CapaFlotante />
</Profiler>
</ErrorBoundary>
</Suspense>
)}
</LayersControl>
</MapContainer>
</div>
</ErrorBoundary>
);
};
// Estilos globales aplicados dinámicamente al head del documento.
if (typeof document !== 'undefined') {
const style = document.createElement('style');
style.textContent = `
.map-container {
width: 100vw;
height: 100vh;
position: relative;
background: #f0f0f0;
overflow: hidden;
isolation: isolate; /* Crea un nuevo contexto de apilamiento para evitar problemas con elementos externos. */
}
.error-fallback {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(255,255,255,0.9);
z-index: 1000; /* Asegura que el fallback esté por encima del mapa. */
}
`;
document.head.append(style);
}
// 'memo' se utiliza nuevamente para optimizar el componente MapView, evitando re-renderizados innecesarios.
export default memo(MapView);
siguiente componente :
import { memo } from 'react';
import { lazy, Suspense, useState, useEffect, useRef, useCallback } from 'react';
import { LayersControl, MapContainer } from 'react-leaflet';
import { arrayGeoserverBaseLayers } from './geoserverBaselay'; // Importa la lista de configuraciones de capas base.
import OfflineModal from './modal/OfflineModal'; // Importa el modal para indicar que la aplicación está offline.
import ErrorModal from './modal/OnlineModal'; // Importa el modal para mostrar errores de conexión o carga.
// Tiempo de espera en milisegundos para la verificación del estado del servidor.
const SERVER_CHECK_TIMEOUT = 3000;
// Umbral de tiempo en milisegundos para mostrar el modal de carga durante la verificación inicial.
const REQUEST_ANIMATION_FRAME_THRESHOLD = 50;
// Constantes de configuración para los reintentos de carga de capas fallidas.
const RETRY_CONFIG = {
MAX_IMMEDIATE_RETRIES: 3, // Número máximo de reintentos inmediatos.
RETRY_DELAY_BASE: 1000, // Tiempo base en milisegundos para el primer reintento.
LONG_RETRY_DELAY: 3 * 60 * 1000, // Tiempo de espera largo en milisegundos para reintentos posteriores o errores de red (3 minutos).
NETWORK_ERRORS: [ // Lista de mensajes de error que indican problemas de red.
'net::ERR_INTERNET_DISCONNECTED',
'net::ERR_NETWORK_CHANGED',
'net::ERR_CONNECTION_REFUSED',
'net::ERR_CONNECTION_TIMED_OUT',
'net::ERR_CONNECTION_RESET',
'net::ERR_FAILED',
'net::ERR_INSUFFICIENT_RESOURCES',
'net::ERR_HTTP2_PROTOCOL_ERROR' // Añadido un tipo de error HTTP/2.
]
};
// Componente funcional CapaBase, responsable de gestionar y renderizar las capas base del mapa.
const CapaBase = () => {
// Estado para almacenar el estado de conexión de cada servidor de capas (online, error, retrying, loaded).
const [serverStatus, setServerStatus] = useState({});
// Estado para indicar si la verificación inicial del servidor está en curso.
const [loading, setLoading] = useState(true);
// Estado para rastrear si la aplicación tiene conexión a internet.
const [isOnline, setIsOnline] = useState(navigator.onLine);
// Estado para controlar la visibilidad del modal de carga inicial.
const [showLoadingModal, setShowLoadingModal] = useState(false);
// Estado para controlar la visibilidad del modal de error de conexión general.
const [showOnModal, setShowOnModal] = useState(false);
// Estado para llevar la cuenta de los reintentos para cada capa.
const [retryCount, setRetryCount] = useState({});
// Estado para almacenar la última vez que ocurrió un error para cada capa.
const [lastErrorTime, setLastErrorTime] = useState({});
// Estado para controlar la visibilidad del modal de desconexión.
const [showOfflineModal, setShowOfflineModal] = useState(!isOnline);
// Referencia para el ID del frame de animación utilizado para controlar la visualización del modal de carga inicial.
const animationFrameId = useRef(null);
// Efecto para escuchar los eventos de conexión (online y offline) del navegador.
useEffect(() => {
// Función que se ejecuta cuando la aplicación vuelve a estar online.
const handleOnline = () => {
setIsOnline(true); // Actualiza el estado de conexión.
setShowOfflineModal(false); // Oculta el modal de desconexión.
};
// Función que se ejecuta cuando la aplicación pierde la conexión a internet.
const handleOffline = () => {
setIsOnline(false); // Actualiza el estado de conexión.
setShowOfflineModal(true); // Muestra el modal de desconexión.
console.error('Error de conexión: net::ERR_INTERNET_DISCONNECTED - No hay conexión a internet');
};
// Añade los listeners para los eventos 'online' y 'offline' en la ventana.
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
// Función de limpieza del efecto: remueve los listeners cuando el componente se desmonta.
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []); // El array de dependencias vacío asegura que este efecto se ejecute solo una vez al montar y desmontar.
// Efecto para verificar el estado de conexión de cada servidor de capas.
useEffect(() => {
// Si no hay conexión a internet, no se realiza la verificación del servidor.
if (!isOnline) return;
let isMounted = true; // Bandera para evitar actualizaciones de estado en un componente desmontado.
const abortController = new AbortController(); // Controlador para abortar las peticiones fetch si el componente se desmonta.
// Función asíncrona para verificar el estado de un servidor.
const checkServerStatus = async () => {
const status = {}; // Objeto para almacenar el estado de cada capa.
// Itera sobre la lista de capas base configuradas.
for (const layer of arrayGeoserverBaseLayers) {
try {
// Construye una URL de prueba para verificar si el servidor WMS está respondiendo.
const testUrl = `${layer.url}?service=WMS&request=GetCapabilities&version=1.1.1`;
// Crea una promesa para un timeout. Si la petición no responde en el tiempo definido, se rechaza.
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout exceeded')), SERVER_CHECK_TIMEOUT)
);
// Realiza la petición fetch para verificar el estado del servidor.
const fetchPromise = fetch(testUrl, {
signal: abortController.signal // Asocia el signal del abortController a la petición.
});
// Espera la primera promesa que se resuelva o rechace (la petición fetch o el timeout).
const response = await Promise.race([fetchPromise, timeoutPromise]);
// Si la respuesta no es exitosa (código de estado HTTP no OK), lanza un error.
if (!response.ok) {
throw new Error(`HTTP status ${response.status}`);
}
// Si el componente sigue montado, marca el servidor de esta capa como 'online'.
if (isMounted) status[layer.key] = 'online';
} catch (error) {
// Si ocurre un error durante la verificación.
if (isMounted) {
status[layer.key] = 'error'; // Marca el servidor de esta capa como 'error'.
logServerError(layer, error); // Registra el error en la consola.
// Detecta específicamente errores de conexión a internet.
if (error.message.includes('Failed to fetch') ||
error.message.includes('net::ERR_INTERNET_DISCONNECTED')) {
setShowOnModal(true); // Muestra el modal de error de conexión general.
}
}
}
}
// Si el componente sigue montado, actualiza el estado del servidor y marca la carga inicial como completada.
if (isMounted) {
setServerStatus(status);
setLoading(false);
}
};
// Llama a la función para verificar el estado del servidor.
checkServerStatus();
// Función de limpieza del efecto: establece la bandera a falso y aborta cualquier petición fetch pendiente.
return () => {
isMounted = false;
abortController.abort();
};
}, [isOnline]); // Se ejecuta cuando cambia el estado de conexión a internet.
// Función para registrar información detallada sobre los errores de carga de capas.
const logServerError = (layer, error) => {
console.groupCollapsed(`Error en capa WMS: ${layer.name}`);
console.error('Tipo:', error.name);
console.error('Mensaje:', error.message);
console.error('URL:', layer.url);
if (error.message.includes('net::ERR_INTERNET_DISCONNECTED')) {
console.error('Estado:', 'Sin conexión a internet');
} else if (error.message.includes('Timeout exceeded')) {
console.error('Estado:', 'Timeout - Servidor no responde');
} else {
console.error('Estado:', 'Error en el servidor');
}
console.groupEnd();
};
// Función que se llama cuando ocurre un error al cargar un tile de una capa.
const handleTileError = (layer, error) => {
setServerStatus(prev => ({ ...prev, [layer.key]: 'error' })); // Marca el estado del servidor de la capa como 'error'.
logServerError(layer, error); // Registra el error.
handleRetry(layer, error); // Intenta reintentar la carga de la capa.
};
// Función para manejar los reintentos de carga de capas fallidas.
const handleRetry = useCallback((layer, error) => {
const layerKey = layer.key;
const currentRetry = retryCount[layerKey] || 0; // Obtiene el número actual de reintentos para la capa.
const now = Date.now(); // Obtiene la marca de tiempo actual.
const lastError = lastErrorTime[layerKey] || 0; // Obtiene la marca de tiempo del último error para la capa.
// Si es un error de red y no ha pasado el tiempo de espera largo, no reintentar inmediatamente.
if (isNetworkError(error) && (now - lastError < RETRY_CONFIG.LONG_RETRY_DELAY)) {
console.log(`[Capa ${layer.name}] Error de red detectado. Esperando 3 minutos...`);
return;
}
let retryDelay;
// Determina el tiempo de espera para el próximo reintento basado en el tipo de error y el número de reintentos.
if (isNetworkError(error)) {
retryDelay = RETRY_CONFIG.LONG_RETRY_DELAY;
console.log(`[Capa ${layer.name}] Error de red grave. Próximo intento en ${retryDelay / 1000 / 60} minutos`);
} else if (currentRetry >= RETRY_CONFIG.MAX_IMMEDIATE_RETRIES) {
retryDelay = RETRY_CONFIG.LONG_RETRY_DELAY;
console.log(`[Capa ${layer.name}] Máximo de reintentos inmediatos alcanzado. Próximo intento en ${retryDelay / 1000 / 60} minutos`);
} else {
retryDelay = RETRY_CONFIG.RETRY_DELAY_BASE * (currentRetry + 1);
console.log(`[Capa ${layer.name}] Reintento ${currentRetry + 1}/${RETRY_CONFIG.MAX_IMMEDIATE_RETRIES} en ${retryDelay / 1000}s`);
}
// Configura un timeout para reintentar la carga de la capa después del tiempo de espera determinado.
setTimeout(() => {
setServerStatus(prev => ({ ...prev, [layerKey]: 'retrying' })); // Marca el estado del servidor como 'retrying'.
setRetryCount(prev => ({ ...prev, [layerKey]: currentRetry + 1 })); // Incrementa el contador de reintentos.
setLastErrorTime(prev => ({ ...prev, [layerKey]: now })); // Actualiza la marca de tiempo del último error.
}, retryDelay);
}, [retryCount, lastErrorTime]); // Dependencias del useCallback: se recrea si cambian retryCount o lastErrorTime.
// Función para verificar si un error es considerado un error de red.
const isNetworkError = (error) => {
return RETRY_CONFIG.NETWORK_ERRORS.some(netError =>
error.message.includes(netError) || error.toString().includes(netError)
);
};
// Carga lazy del componente WMSTileLayer de react-leaflet.
const LazyWMSTileLayer = lazy(() =>
import('react-leaflet').then(module => ({ default: module.WMSTileLayer }))
);
// Función para controlar la visualización del modal de carga inicial mediante requestAnimationFrame.
const animateLoadingCheck = (timestamp) => {
const startTime = performance.now();
arrayGeoserverBaseLayers.forEach((layer) => {
// Si el estado de la capa no es 'loaded' y ha pasado el umbral de tiempo, muestra el modal de carga.
if (serverStatus[layer.key] !== 'loaded' && performance.now() - startTime > REQUEST_ANIMATION_FRAME_THRESHOLD) {
setShowLoadingModal(true);
}
});
animationFrameId.current = requestAnimationFrame(animateLoadingCheck);
};
// Efecto para iniciar y limpiar el loop de animación de la verificación de carga inicial.
useEffect(() => {
animationFrameId.current = requestAnimationFrame(animateLoadingCheck);
return () => {
if (animationFrameId.current) {
cancelAnimationFrame(animationFrameId.current);
}
};
}, [serverStatus]); // Se ejecuta cuando cambia el estado del servidor de alguna capa.
// Si la carga inicial está en curso, no renderiza las capas.
if (loading) return null;
return (
<>
{/* Renderiza el modal de desconexión si la aplicación está offline. */}
{showOfflineModal && (
<OfflineModal onClose={() => setShowOfflineModal(false)} />
)}
{/* Renderiza el modal de carga inicial si showLoadingModal es true. */}
{showLoadingModal && (
<ErrorModal
message="Cargando capas... Por favor, espere."
onClose={() => setShowLoadingModal(false)}
/>
)}
{/* Renderiza el modal de error de conexión general si showOnModal es true. */}
{showOnModal && (
<ErrorModal
message="Error de conexión. Verifique su conexión a internet y vuelva a intentarlo."
onClose={() => setShowOnModal(false)}
/>
)}
{/* Mapea la lista de capas base para renderizar cada una como una capa base en el control de capas. */}
{arrayGeoserverBaseLayers?.map((layer, index) => (
<LayersControl.BaseLayer
name={layer?.name} // Nombre de la capa que se mostrará en el control.
key={`baseLayer-${layer?.key || index}`} // Clave única para el elemento de la lista.
checked={layer?.checked ?? false} // Indica si la capa está activa por defecto.
>
{/* Si el estado del servidor para esta capa es 'error', no renderiza la capa (la oculta). */}
{serverStatus[layer.key] === 'error' ? (
<div style={{ display: 'none' }} aria-hidden="true" />
) : (
// Si no hay error, carga el componente WMSTileLayer de forma lazy.
<Suspense fallback={null}>
<LazyWMSTileLayer
layers={layer?.layers} // Nombre de las capas WMS a solicitar.
url={layer?.url} // URL del servicio WMS.
transparent={layer?.transparent ?? true} // Indica si las capas deben ser transparentes.
format={layer?.format || 'image/png'} // Formato de imagen solicitado.
opacity={layer?.opacity ?? 1} // Opacidad de la capa.
maxZoom={layer?.maxZoom ?? 19} // Nivel de zoom máximo para la capa.
minZoom={layer?.minZoom ?? 0} // Nivel de zoom mínimo para la capa.
eventHandlers={{
// Manejador de eventos para errores de carga de tiles.
error: (e) => handleTileError(layer, e.error),
// Manejador de eventos para el inicio de la carga de tiles (solo para logging).
loading: () => console.log(`Cargando capa ${layer.name}...`)
}}
// Función que se llama cuando la capa se añade al mapa.
onAdd={() => {
setServerStatus(prev => ({ ...prev, [layer.key]: 'loaded' })); // Marca el estado del servidor como 'loaded'.
setShowLoadingModal(false); // Oculta el modal de carga inicial.
}}
/>
</Suspense>
)}
</LayersControl.BaseLayer>
))}
</>
);
};
// 'memo' se utiliza para optimizar el componente CapaBase, evitando re-renderizados innecesarios
// si las props (que en este caso no recibe directamente) no cambian. Sin embargo, dado que este
// componente tiene estado interno y se suscribe a eventos globales, su comportamiento de re-renderizado
// dependerá de los cambios de estado.
export default memo(CapaBase);
siguiente componente :
// Definición de constantes para los niveles de zoom máximo y mínimo permitidos en el mapa.
const ZoomMaximo = 18;
const ZoomMinimo = 4;
// Clave de API para el servicio de mapas Thunderforest.
const ThunderforestApiKey = '7c352c8ff1244dd8b732e349e0b0fe8d';
// Función que genera la URL base para las capas de mapa de Thunderforest,
// interpolando el estilo de mapa deseado.
const ThunderforestURL = (style: string) =>
`https://{s}.tile.thunderforest.com/${style}/{z}/{x}/{y}{r}.png?apikey=${ThunderforestApiKey}`;
// Identificador de la aplicación y código de la aplicación para el servicio de mapas HERE.
const HereAppId = 'eAdkWGYRoc4RfxVo0Z4B';
const HereAppCode = 'TrLJuXVK62IQk0vuXFzaig';
// Función que genera la URL base para las capas de mapa de HERE,
// interpolando el tipo de mapa deseado ('terrain' o 'satellite').
const HereURL = (type: 'terrain' | 'satellite') =>
`https://2.aerial.maps.ls.hereapi.com/maptile/2.1/maptile/newest/${type}.day/{z}/{x}/{y}/256/png8?app_id=${HereAppId}&app_code=${HereAppCode}&lg=eng`;
// Función utilitaria para crear un objeto de configuración de capa de mapa.
// Recibe el nombre de la capa, una clave única, la URL de los tiles,
// un indicador si la capa está marcada como activa por defecto,
// y los niveles de zoom máximo y mínimo permitidos para esta capa.
const makeLayer = (
name: string,
key: string,
url: string,
checked = false,
maxZoom = ZoomMaximo,
minZoom = ZoomMinimo
) => ({
name, // Nombre legible de la capa.
key, // Clave única para identificar la capa en la aplicación.
url, // URL de los tiles del mapa, con marcadores de posición para {s} (subdominio), {z} (zoom), {x} (columna), {y} (fila) y opcionalmente {r} (para tiles de alta resolución).
checked, // Booleano que indica si la capa está seleccionada o visible por defecto.
maxZoom, // Nivel de zoom máximo para esta capa.
minZoom // Nivel de zoom mínimo para esta capa.
});
// Array que contiene la configuración de múltiples capas base de mapa,
// listas para ser utilizadas en un control de capas de un mapa (como en Leaflet).
export const arrayGeoserverBaseLayers = [
// Grupo de capas de Thunderforest
makeLayer('Transporte Thunderforest (*20)', 'thunderforest_mobile_atlas', ThunderforestURL('mobile-atlas')),
makeLayer('Topografía Thunderforest (*20)', 'thunderforest_landscape', ThunderforestURL('landscape')),
makeLayer('Calle Thunderforest (*20)', 'thunderforest_transport', ThunderforestURL('transport')),
makeLayer('Calle Thunderforest Atlas (*20)', 'thunderforest_atlas', 'https://{s}.tile.thunderforest.com/atlas/{z}/{x}/{y}{r}.png?apikey=6a53e8b25d114a5e9216df5bf9b5e9c8'),
// Grupo de capas de ESRI
makeLayer('Topografía ESRI (*15)', 'esri_topo', 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', false, 15),
makeLayer('Satélite ESRI (*18)', 'esri_satellite', 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'),
// Grupo de capas de OpenStreetMap
makeLayer('Calle OpenStreetMap (*19)', 'osm_standard', 'http://a.tile.openstreetmap.org/{z}/{x}/{y}.png'),
makeLayer('Calle OpenStreetMap DE (*20)', 'osm_de', 'https://tile.openstreetmap.de/{z}/{x}/{y}.png'),
makeLayer('Topografía OpenTopoMap (*15)', 'opentopomap', 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', false, 15),
makeLayer('Topografía Cyclosm (*15)', 'osm_cyclosm', 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', false, 15),
// Grupo de capas de Here Wego
makeLayer('Topografía Here Wego Terrain (*20)', 'here_terrain', HereURL('terrain')),
makeLayer('Satélite Here Wego (*20)', 'here_satellite', HereURL('satellite')),
// Grupo de capas de Google Maps
makeLayer('Satélite Google Maps (*20)', 'google_satellite', 'http://www.google.cn/maps/vt?lyrs=s@189&gl=cn&x={x}&y={y}&z={z}'),
makeLayer('Calles Google Maps (*20)', 'google_maps', 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}'),
makeLayer('Tráfico Google Maps (*18)', 'google_traffic', 'https://mt1.google.com/vt?lyrs=h@159000000,traffic|seconds_into_week:-1&style=3&x={x}&y={y}&z={z}', false, 18),
// Grupo de capas de Mapbox
makeLayer('Satélite Mapbox (*20)', 'mapbox_satellite', 'https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.webp?sku=101S0AiAdllT3&access_token=pk.eyJ1Ijoiam9uY2hlbWxhIiwiYSI6IjdXUzRocmsifQ.acEmRifqE4Bh2Xz-IY_4Bw'),
// Grupo de capas de Carto
makeLayer('Negro Carto (*20)', 'carto_dark', 'http://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'),
makeLayer('Blanco Fastly Carto (*20)', 'carto_light', 'https://cartodb-basemaps-a.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png', true),
// Grupo de capas de Waze
makeLayer('Calle Waze World (*20)', 'waze_world', 'https://worldtiles3.waze.com/tiles/{z}/{x}/{y}.png'),
];
suguiente componente :
import React, {
memo, lazy, Suspense, useState, useEffect, useRef,
useCallback, Profiler
} from 'react';
import { LayersControl } from 'react-leaflet';
import { arrayGeoserverOverlay } from './geoserverOverlay'; // Importa la lista de configuraciones para las capas overlay.
// Componentes lazy para cargar los tipos de capas de forma diferida.
const LazyWMSTileLayer = lazy(() =>
import('react-leaflet').then(m => ({ default: m.WMSTileLayer }))
);
const LazyTileLayer = lazy(() =>
import('react-leaflet').then(m => ({ default: m.TileLayer }))
);
// Configuración para los reintentos de carga de capas fallidas.
const RETRY_CONFIG = {
MAX_IMMEDIATE_RETRIES: 3, // Número máximo de reintentos inmediatos.
RETRY_DELAY_BASE: 1000, // Tiempo base en milisegundos para el primer reintento (1 segundo).
LONG_RETRY_DELAY: 5 * 60 * 1000, // Tiempo de espera largo en milisegundos para reintentos posteriores o errores de red (5 minutos).
NETWORK_ERRORS: [ // Lista de mensajes de error que indican problemas de red.
'net::ERR_INTERNET_DISCONNECTED',
'net::ERR_NETWORK_CHANGED',
'net::ERR_CONNECTION_REFUSED',
'net::ERR_CONNECTION_TIMED_OUT',
'net::ERR_CONNECTION_RESET'
]
};
// Callback para el Profiler de React, utilizado para medir el rendimiento de la renderización.
const onRenderCallback = (
id: string, phase: string, actualDuration: number,
baseDuration: number, startTime: number,
commitTime: number, interactions: Set<any>
) => {
console.log(`Profiler [${id}]:`, { phase, actualDuration, baseDuration });
};
// Componente funcional CapaFlotante, responsable de gestionar y renderizar las capas overlay del mapa.
const CapaFlotante = () => {
// Estado para rastrear si la aplicación tiene conexión a internet.
const [isOnline, setIsOnline] = useState(navigator.onLine);
// Estado para almacenar el estado de carga de cada capa overlay ('loading', 'loaded', 'error', 'retrying').
const [layerStatus, setLayerStatus] = useState<Record<string, string>>({});
// Estado para llevar la cuenta del número de reintentos para cada capa.
const [retryCount, setRetryCount] = useState<Record<string, number>>({});
// Estado para almacenar la marca de tiempo del último error para cada capa.
const [lastErrorTime, setLastErrorTime] = useState<Record<string, number>>({});
// Ref para almacenar un Set de las claves de las capas que se han cargado exitosamente.
const loadedLayers = useRef<Set<string>>(new Set());
// Ref para almacenar un objeto con los timers de reintento para cada capa.
const retryTimers = useRef<Record<string, NodeJS.Timeout>>({});
// useCallback para verificar si un error dado es un error de red.
const isNetworkError = useCallback(
(error: any) =>
RETRY_CONFIG.NETWORK_ERRORS.some(e =>
error?.message?.includes(e) || error?.toString().includes(e)
),
[] // El array de dependencias vacío significa que esta función no se recreará en cada renderizado.
);
// useCallback para limpiar todos los timers de reintento activos.
const clearRetryTimers = useCallback(() => {
Object.values(retryTimers.current).forEach(clearTimeout);
retryTimers.current = {};
}, []); // El array de dependencias vacío significa que esta función no se recreará en cada renderizado.
// Efecto para realizar una precarga de 'react-leaflet' y para limpiar los timers al desmontar el componente.
useEffect(() => {
import('react-leaflet'); // Precarga el módulo 'react-leaflet' para mejorar la carga de los componentes lazy.
return clearRetryTimers; // Retorna la función para limpiar los timers al desmontar el componente.
}, [clearRetryTimers]); // La dependencia es 'clearRetryTimers', pero como no cambia, este efecto solo se ejecuta al montar y desmontar.
// Efecto para escuchar los eventos de conexión (online y offline) del navegador.
useEffect(() => {
const handleOnline = () => {
console.log('[Conexión] Red en línea');
setIsOnline(true); // Actualiza el estado de conexión.
const now = Date.now();
// Itera sobre las capas que tuvieron errores y, si ha pasado el tiempo de espera largo, intenta reintentarlas.
for (const [key, time] of Object.entries(lastErrorTime)) {
if (now - time >= RETRY_CONFIG.LONG_RETRY_DELAY) {
console.log(`[Reintento] Capa ${key} después de 5 minutos`);
handleRetry(key);
}
}
};
const handleOffline = () => {
console.error('[Conexión] Sin internet');
setIsOnline(false); // Actualiza el estado de conexión.
clearRetryTimers(); // Limpia los timers de reintento al perder la conexión.
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, [lastErrorTime, clearRetryTimers]); // Este efecto se ejecuta cuando cambian 'lastErrorTime' o 'clearRetryTimers'.
// useCallback para manejar el reintento de carga de una capa específica.
const handleRetry = useCallback((layerKey: string, layerName = '', error?: any) => {
const now = Date.now();
const retry = retryCount[layerKey] || 0;
const lastError = lastErrorTime[layerKey] || 0;
// Si hay un error, es un error de red y no ha pasado el tiempo de espera largo, no reintenta inmediatamente.
if (error && isNetworkError(error) && now - lastError < RETRY_CONFIG.LONG_RETRY_DELAY) {
console.log(`[${layerName}] Error de red, esperando 5 min`);
return;
}
clearTimeout(retryTimers.current[layerKey]); // Limpia cualquier timer de reintento previo para esta capa.
// Determina el tiempo de espera para el próximo reintento.
const retryDelay = error && isNetworkError(error)
? RETRY_CONFIG.LONG_RETRY_DELAY
: retry >= RETRY_CONFIG.MAX_IMMEDIATE_RETRIES
? RETRY_CONFIG.LONG_RETRY_DELAY
: RETRY_CONFIG.RETRY_DELAY_BASE * (retry + 1);
console.log(`[${layerName}] Reintento en ${retryDelay / 1000}s`);
// Establece un timer para intentar recargar la capa después del tiempo de espera.
retryTimers.current[layerKey] = setTimeout(() => {
loadedLayers.current.delete(layerKey); // Elimina la capa del conjunto de capas cargadas.
setLayerStatus(prev => ({ ...prev, [layerKey]: 'retrying' })); // Actualiza el estado de la capa a 'retrying'.
setRetryCount(prev => ({ ...prev, [layerKey]: retry + 1 })); // Incrementa el contador de reintentos.
}, retryDelay);
}, [retryCount, lastErrorTime, isNetworkError]); // Este useCallback depende de 'retryCount', 'lastErrorTime' e 'isNetworkError'.
// useCallback para renderizar una capa WMS.
const renderWMSLayer = useCallback((layer: any) => {
const key = layer?.key || layer?.name;
const name = layer?.name || 'Sin nombre';
const props = {
layers: layer?.layers,
url: layer?.url,
transparent: layer?.transparent ?? true,
format: layer?.format || 'image/png',
opacity: layer?.opacity ?? 0.7,
maxZoom: layer?.maxZoom ?? 18,
minZoom: layer?.minZoom ?? 0,
zIndex: layer?.zIndex || 1,
className: `wms-layer ${key}`
};
return (
<LazyWMSTileLayer
{...props}
eventHandlers={{
load: () => {
console.log(`[${name}] Carga exitosa`);
setLayerStatus(prev => ({ ...prev, [key]: 'loaded' })); // Actualiza el estado de la capa a 'loaded'.
setRetryCount(prev => ({ ...prev, [key]: 0 })); // Resetea el contador de reintentos.
setLastErrorTime(prev => ({ ...prev, [key]: 0 })); // Resetea la marca de tiempo del último error.
},
error: (e: any) => {
console.error(`[${name}] Error:`, e.error);
loadedLayers.current.delete(key); // Elimina la capa del conjunto de capas cargadas.
setLayerStatus(prev => ({ ...prev, [key]: 'error' })); // Actualiza el estado de la capa a 'error'.
setLastErrorTime(prev => ({ ...prev, [key]: Date.now() })); // Actualiza la marca de tiempo del último error.
handleRetry(key, name, e.error); // Intenta reintentar la carga de la capa.
}
}}
/>
);
}, [handleRetry]); // Este useCallback depende de 'handleRetry'.
// useCallback para renderizar una capa WMTS (convertida a TileLayer estándar).
const renderWMTSTileLayer = useCallback((layer: any) => {
const key = layer?.key || layer?.name;
const name = layer?.name || 'Sin nombre';
// Convertir la URL de WMTS a una URL de teselas estándar reemplazando los marcadores.
const tileUrl = layer?.url
.replace('{z}', '{z}')
.replace('{x}', '{x}')
.replace('{y}', '{y}');
return (
<LazyTileLayer
url={tileUrl}
opacity={layer?.opacity ?? 0.7}
maxZoom={layer?.maxZoom ?? 18}
minZoom={layer?.minZoom ?? 0}
zIndex={layer?.zIndex || 1}
className={`wmts-layer ${key}`}
eventHandlers={{
load: () => {
console.log(`[${name}] Carga exitosa`);
setLayerStatus(prev => ({ ...prev, [key]: 'loaded' })); // Actualiza el estado de la capa a 'loaded'.
setRetryCount(prev => ({ ...prev, [key]: 0 })); // Resetea el contador de reintentos.
setLastErrorTime(prev => ({ ...prev, [key]: 0 })); // Resetea la marca de tiempo del último error.
},
error: (e: any) => {
console.error(`[${name}] Error:`, e.error);
loadedLayers.current.delete(key); // Elimina la capa del conjunto de capas cargadas.
setLayerStatus(prev => ({ ...prev, [key]: 'error' })); // Actualiza el estado de la capa a 'error'.
setLastErrorTime(prev => ({ ...prev, [key]: Date.now() })); // Actualiza la marca de tiempo del último error.
handleRetry(key, name, e.error); // Intenta reintentar la carga de la capa.
}
}}
/>
);
}, [handleRetry]); // Este useCallback depende de 'handleRetry'.
// useCallback para renderizar una capa, decidiendo qué tipo de capa renderizar.
const renderLayer = useCallback((layer: any) => {
const key = layer?.key || layer?.name;
const status = layerStatus[key];
// Si el estado de la capa es 'error', no la renderiza.
if (status === 'error') return null;
// Si la capa aún no se ha marcado como cargada, lo hace y loguea la solicitud.
if (!loadedLayers.current.has(key)) {
loadedLayers.current.add(key);
console.log(`[${layer?.name}] Solicitando datos`);
}
return (
<Suspense fallback={null}>
{/* Renderiza el componente de capa adecuado según el tipo de capa. */}
{layer.type === 'wms' ? renderWMSLayer(layer) : renderWMTSTileLayer(layer)}
</Suspense>
);
}, [layerStatus, renderWMSLayer, renderWMTSTileLayer]); // Este useCallback depende de 'layerStatus', 'renderWMSLayer' y 'renderWMTSTileLayer'.
return (
<>
{/* Mapea la lista de capas overlay para renderizar cada una dentro de un control de capas. */}
{arrayGeoserverOverlay.map((layer, index) => {
const key = layer?.key || `layer-${index}`;
const name = layer?.name || `Capa ${index}`;
const status = layerStatus[key];
// Decide si mostrar la capa: si no está en error y está online, o si está en proceso de reintento.
const show = (status !== 'error' && isOnline) || status === 'retrying';
return (
<Profiler key={`profiler-${key}`} id={`Layer-${key}`} onRender={onRenderCallback}>
<LayersControl.Overlay
name={name}
key={`overlay-${key}`}
checked={layer?.checked ?? false}
>
{/* Envuelve la renderización de la capa con un div condicional para mostrarla u ocultarla. */}
<div style={{ display: show ? 'block' : 'none' }}>
{renderLayer(layer)}
</div>
</LayersControl.Overlay>
</Profiler>
);
})}
</>
);
};
export default memo(CapaFlotante); // 'memo' se utiliza para optimizar el componente funcional CapaFlotante, evitando re-renderizados innecesarios si las props no cambian (aunque este componente no recibe props directamente).
siguiente componente :
// URL base para el servicio de mapas WMS de NASA GIBS.
const ResourceNasa = 'https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi';
// URL base para la obtención de leyendas de capas de NASA GIBS.
const ResourceLegends = 'https://gitc.earthdata.nasa.gov/legends';
// Nivel de zoom máximo permitido para las capas.
const ZoomMaximo = 18;
// Nivel de zoom mínimo permitido para las capas.
const ZoomMinimo = 1;
// Opacidad por defecto para las capas (30%).
const Opacity = 0.3;
// Indica si la transparencia está habilitada por defecto para las capas.
const Transparent = true;
// Formato de imagen por defecto para las capas.
const Format = 'image/png';
// Indica si la capa está marcada como visible por defecto.
const Checked = false;
// Tipo de capa, en este caso WMS (Web Map Service).
const type = 'wms';
// Función para construir un objeto de configuración de capa overlay.
// Recibe el nombre de la capa, una clave única, el nombre de las capas WMS a solicitar,
// la ruta al archivo de leyenda (si existe), la URL del servicio (por defecto NASA),
// y la opacidad (por defecto el valor definido).
const buildLayer = (name: string, key: string, layers: string, legendPath: string, url = ResourceNasa, opacity = Opacity) => ({
name, // Nombre legible de la capa.
key, // Clave única para identificar la capa.
layers, // Nombre de la(s) capa(s) dentro del servicio WMS.
url, // URL del servicio WMS para esta capa.
transparent: Transparent, // Indica si la capa debe ser transparente.
opacity, // Nivel de opacidad de la capa.
type: type, // Tipo de capa (WMS).
format: Format, // Formato de imagen de la capa.
checked: Checked, // Indica si la capa está visible por defecto.
maxZoom: ZoomMaximo, // Nivel de zoom máximo para esta capa.
minZoom: ZoomMinimo, // Nivel de zoom mínimo para esta capa.
legends: legendPath ? `${ResourceLegends}/${legendPath}` : '', // URL completa a la leyenda de la capa, si se proporciona la ruta.
});
// Array que contiene la configuración de múltiples capas overlay para ser mostradas en el mapa.
export const arrayGeoserverOverlay = [
// Capas de temperatura VIIRS (NOAA20 y SNPP) para la noche.
buildLayer('TEMPERATURA VIIRS NOAA20', 'TEMPERATURA NOCHE', 'VIIRS_NOAA20_Brightness_Temp_BandI5_Night', 'VIIRS_Brightness_Temp_BandI5_H.png'),
buildLayer('TEMPERATURA VIIRS SNPP', 'TEMPERATURA NOCHE SNPP', 'VIIRS_SNPP_Brightness_Temp_BandI5_Night', 'VIIRS_Brightness_Temp_BandI5_H.png'),
// Capas de temperatura de la superficie terrestre MODIS (día y noche).
buildLayer('TEMPERATURA DE LA SUPERFICIE DEL TERRESTRE NOCHE', 'TEMPERATURA DE LA SUPERFICIE DEL TERRESTRE NOCHE', 'MODIS_Terra_L3_Land_Surface_Temp_Monthly_Night', 'MODIS_Land_Surface_Temp_H.png'),
buildLayer('TEMPERATURA DE LA SUPERFICIE DEL TERRESTRE DIA', 'TEMPERATURA DE LA SUPERFICIE DEL TERRESTRE DIA', 'MODIS_Terra_L3_Land_Surface_Temp_Monthly_Day', 'MODIS_Land_Surface_Temp_H.png'),
// Capa de cirros VIIRS SNPP.
buildLayer('CIRRUS SNPP', 'CIRRUS SNPP', 'VIIRS_SNPP_Apparent_Reflectance_VNP02MOD_M09', 'VIIRS_Apparent_Reflectance_H.png'),
// Capas de confianza de cielo limpio VIIRS (NOAA20 y SNPP) para la noche.
buildLayer('CIELO LIMPIO VIIRS SNPP', 'CIELO LIMPIO VIIRS SNPP', 'VIIRS_SNPP_Clear_Sky_Confidence_Night', 'VIIRS_Clear_Sky_Confidence_H.png'),
buildLayer('CIELO LIMPIO VIIRS NOAA20', 'CIELO LIMPIO VIIRS NOAA20', 'VIIRS_NOAA20_Clear_Sky_Confidence_Night', 'VIIRS_Clear_Sky_Confidence_H.png'),
// Capas de radio efectivo de nubes VIIRS (NOAA20 y SNPP).
buildLayer('NUBE RADIO EFECTIVO VIIRS SNPP', 'NUBE RADIO EFECTIVO VIIRS SNPP', 'VIIRS_SNPP_Cloud_Effective_Radius', 'MODIS_VIIRS_Cloud_Effective_Radius_H.png'),
buildLayer('NUBE RADIO EFECTIVO VIIRS NOAA20', 'NUBE RADIO EFECTIVO VIIRS NOAA20', 'VIIRS_NOAA20_Cloud_Effective_Radius', 'MODIS_VIIRS_Cloud_Effective_Radius_H.png'),
// Capas de espesor óptico de nubes VIIRS (NOAA20 y SNPP).
buildLayer('ESPESOR ÓPTICO DE LA NUBE VIIRS SNPP', 'ESPESOR ÓPTICO DE LA NUBE VIIRS SNPP', 'VIIRS_SNPP_Cloud_Optical_Thickness', 'MODIS_VIIRS_Cloud_Optical_Thickness_H.png'),
buildLayer('ESPESOR ÓPTICO DE LA NUBE VIIRS NOAA20', 'ESPESOR ÓPTICO DE LA NUBE VIIRS NOAA20', 'VIIRS_NOAA20_Cloud_Optical_Thickness', 'MODIS_VIIRS_Cloud_Optical_Thickness_H.png'),
// Capa de deposición de polvo mensual MERRA2.
buildLayer('POLVO MENSUAL', 'POLVO MENSUAL', 'MERRA2_Total_Dust_Deposition_Dry_Wet_Monthly', 'MERRA2_Total_Dust_Deposition_Dry_Wet_Monthly_H.png'),
// Capa de evaporación terrestre mensual MERRA2.
buildLayer('EVAPORACION', 'EVAPORACION', 'MERRA2_Evaporation_Land_Monthly', 'MERRA2_Evaporation_Land_Monthly_H.png'),
// Capas infrarrojas limpias de satélites geoestacionarios (Este y Oeste).
buildLayer('GEOSTACIONARIA ESTE LIMPIO INFRARROJO', 'GEOSTACIONARIA ESTE LIMPIO INFRARROJO', 'GOES-East_ABI_Band13_Clean_Infrared', 'Clean_Longwave_Infrared_Window_Band_H.png'),
// Capa de color de satélite geoestacionario Este.
buildLayer('GEOSTACIONARIA ESTE COLOR', 'GEOSTACIONARIA ESTE COLOR', 'GOES-East_ABI_GeoColor', ''),
// Capas infrarrojas limpias de satélites geoestacionarios (Este y Oeste).
buildLayer('GEOSTACIONARIA OESTE LIMPIO INFRARROJO', 'GEOSTACIONARIA OESTE LIMPIO INFRARROJO', 'GOES-West_ABI_Band13_Clean_Infrared', 'Clean_Longwave_Infrared_Window_Band_H.png'),
// Capa de color de satélite geoestacionario Oeste.
buildLayer('GEOSTACIONARIA OESTE COLOR', 'GEOSTACIONARIA OESTE COLOR', 'GOES-West_ABI_GeoColor', ''),
// Capa de superficie impermeable global Landsat.
buildLayer('SUPERFICIE IMPERMEABLE', 'SUPERFICIE IMPERMEABLE', 'Landsat_Global_Man-made_Impervious_Surface', 'Landsat_Global_Man-made_Impervious_Surface_H.png'),
// Capa de tasa de precipitación IMERG.
buildLayer('PRECIPITACIÓN', 'PRECIPITACIÓN', 'IMERG_Precipitation_Rate', 'GPM_Precipitation_Rate_H.png'),
// Capas de humedad relativa superficial mensual AIRS (día y noche).
buildLayer('HUMEDAD RELATIVA DIA', 'HUMEDAD RELATIVA DIA', 'AIRS_L3_Surface_Relative_Humidity_Monthly_Day', 'AIRS_Surface_Relative_Humidity_Monthly_Day_H.png'),
buildLayer('HUMEDAD RELATIVA NOCHE', 'HUMEDAD RELATIVA NOCHE', 'AIRS_L3_Surface_Relative_Humidity_Monthly_Night', 'AIRS_Surface_Relative_Humidity_Monthly_Night_H.png'),
// Capa de velocidad del viento superficial mensual MERRA2.
buildLayer('VIENTO', 'VIENTO', 'MERRA2_Surface_Wind_Speed_Monthly', 'MERRA2_Surface_Wind_Speed_Monthly_H.png'),
// Capas de TELCEL (con URL específica y opacidad 1).
buildLayer('telcel 5g', 'telcel 5g', '', 'MERRA2_Surface_Wind_Speed_Monthly_H.png', 'https://lbsva.telcel.com/tiles/gwc/service/wmts?layer=telcel:cdmx_no_garantizada&style=&tilematrixset=EPSG:900913&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/png&TileMatrix=EPSG:900913:{z}&TileCol={x}&TileRow={y}', 1),
buildLayer('telcel 5g garantizado', 'telcel 5g garantizado', '', 'MERRA2_Surface_Wind_Speed_Monthly_H.png', 'https://lbsva.telcel.com/tiles/gwc/service/wmts?layer=telcel:cdmx_garantizada&style=&tilematrixset=EPSG:900913&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/png&TileMatrix=EPSG:900913:{z}&TileCol={x}&TileRow={y}', 1),
buildLayer('telcel 4g', 'telcel 4g', '', 'MERRA2_Surface_Wind_Speed_Monthly_H.png', 'https://lbsva.telcel.com/tiles/gwc/service/wmts?layer=telcel:lte_no_garantizada&style=&tilematrixset=EPSG:900913&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/png&TileMatrix=EPSG:900913:{z}&TileCol={x}&TileRow={y}', 1),
buildLayer('telcel 4g garantizado', 'telcel 4g garantizado', '', 'MERRA2_Surface_Wind_Speed_Monthly_H.png', 'https://lbsva.telcel.com/tiles/gwc/service/wmts?layer=telcel:lte_garantizada&style=&tilematrixset=EPSG:900913&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/png&TileMatrix=EPSG:900913:{z}&TileCol={x}&TileRow={y}', 1),
];
codigo de modales --------------
/* OfflineModal.css */
/* Estilos para la capa de superposición oscura que cubre toda la ventana */
.modal-overlay {
position: fixed; /* Fija el elemento a la ventana del navegador */
top: 0;
left: 0;
width: 100%; /* Ocupa todo el ancho de la ventana */
height: 100%; /* Ocupa toda la altura de la ventana */
background: rgba(0, 0, 0, 0.5); /* Fondo negro semitransparente */
display: flex; /* Utiliza Flexbox para centrar el contenido */
justify-content: center; /* Centra horizontalmente */
align-items: center; /* Centra verticalmente */
z-index: 1000; /* Asegura que el modal esté por encima de otros elementos */
}
/* Estilos para el contenedor del contenido del modal */
.modal-content {
background: white; /* Fondo blanco del modal */
padding: 20px; /* Espacio interno alrededor del contenido */
border-radius: 8px; /* Bordes redondeados */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* Sombra suave para elevar el modal */
}
/* Estilos para el encabezado h2 dentro del contenido del modal */
.modal-content h2 {
margin-top: 0; /* Elimina el margen superior por defecto del h2 */
}
/* Estilos para el botón dentro del contenido del modal */
.modal-content button {
margin-top: 20px; /* Espacio superior entre el botón y otros elementos */
padding: 10px 20px; /* Espacio interno del botón (arriba/abajo, izquierda/derecha) */
background: #007bff; /* Color de fondo azul */
color: white; /* Color del texto blanco */
border: none; /* Elimina el borde por defecto del botón */
border-radius: 4px; /* Bordes ligeramente redondeados */
cursor: pointer; /* Cambia el cursor a una mano al pasar por encima */
}
/* Estilos para el botón cuando el cursor está sobre él */
.modal-content button:hover {
background: #005bb5; /* Color de fondo azul más oscuro al pasar el ratón */
}
codigo de modal
// OfflineModal.js
import React from 'react';
import './OfflineModal.css'; // Asegúrate de crear este archivo CSS para estilos
// Componente funcional OfflineModal que se encarga de mostrar un mensaje cuando no hay conexión a internet.
// Recibe una prop 'onClose' que es una función para cerrar el modal.
const OfflineModal = ({ onClose }) => {
return (
// Contenedor principal que actúa como la capa de superposición oscura.
<div className="modal-overlay">
{/* Contenedor para el contenido del modal (fondo blanco, mensaje, botón). */}
<div className="modal-content">
{/* Título del modal */}
<h2>Algunas capas base estan sin Conexión a Internet</h2>
{/* Mensaje informativo sobre la falta de conexión. */}
<p>Sin conexión a internet. Algunas funciones como capas pueden no estar disponibles.</p>
{/* Botón para cerrar el modal. Al hacer clic, se ejecuta la función 'onClose' pasada como prop. */}
<button onClick={onClose}>Cerrar</button>
</div>
</div>
);
};
// Exporta el componente OfflineModal para que pueda ser utilizado en otras partes de la aplicación.
export default OfflineModal;
siguiente modal :
// OfflineModal.js
import React from 'react';
import './OfflineModal.css'; // Importa los estilos definidos en el archivo OfflineModal.css.
// Componente funcional OfflineModal.
// Este componente recibe una prop llamada 'onClose', que es una función
// que se ejecutará cuando el usuario quiera cerrar el modal.
const OfflineModal = ({ onClose }) => {
return (
// Contenedor principal con la clase 'modal-overlay'.
// Esta clase probablemente define el fondo oscuro y la posición fija del modal.
<div className="modal-overlay">
{/* Contenedor para el contenido real del modal (fondo blanco, texto, botón).
La clase 'modal-content' definirá su estilo visual. */}
<div className="modal-content">
{/* Encabezado del modal, indicando el estado de la conexión a Internet. */}
<h2>Conexión a Internet</h2>
{/* Párrafo con un mensaje indicando que el usuario tiene conexión a Internet.
Aunque el nombre del componente es 'OfflineModal', este mensaje sugiere
que se está mostrando cuando hay conexión. Podría haber una confusión en el nombre. */}
<p>Estás trabajando con conexión a Internet.</p>
{/* Botón para cerrar el modal.
Al hacer clic en este botón, se llama a la función 'onClose' que se recibió como prop.
Esto permite al componente padre controlar el cierre del modal. */}
<button onClick={onClose}>Cerrar</button>
</div>
</div>
);
};
// Exporta el componente OfflineModal para que pueda ser utilizado en otras partes de la aplicación.
// A pesar de su nombre, el mensaje actual indica que se muestra cuando hay conexión a Internet.
export default OfflineModal;