GeoCercas
import React, { useRef, useState, memo } from "react";
import { MapContainer, TileLayer } from "react-leaflet";
// Importaci贸n de componentes personalizados para manejar diferentes elementos del mapa
import ManejoSujetoPiloto from './componentes/ManejadorSujetoPiloto';
import ManejoLineas from './componentes/ManejoLineas';
import ManejoPoligono from './componentes/ManejoPoligono';
import ManejoMarcadores from './componentes/ManejoMarcadores';
import ManejoCircunferencia from './componentes/ManejoCirculo';
// 馃敼 Componente funcional memoizado para un bot贸n de toggle reutilizable
// memo() se utiliza para optimizar el rendimiento evitando re-renderizaciones innecesarias
const ToggleButton = memo(({ children, label, top, left, isActive, onClick }) => (
<>
{/* Bot贸n HTML con estilos inline para posicionamiento y apariencia */}
<button
onClick={onClick} // Funci贸n que se ejecuta al hacer clic en el bot贸n
style={{
position: 'absolute', // Permite posicionar el bot贸n exactamente en las coordenadas top y left
top, // Posici贸n vertical del bot贸n
left, // Posici贸n horizontal del bot贸n
zIndex: 1000, // Asegura que el bot贸n est茅 por encima de otros elementos del mapa
backgroundColor: isActive ? '#4CAF50' : '#f44336', // Cambia el color de fondo seg煤n si el bot贸n est谩 activo o no
color: 'white', // Color del texto del bot贸n
padding: '5px 10px', // Espaciado interno del bot贸n
border: 'none', // Elimina el borde predeterminado del bot贸n
borderRadius: '4px', // Redondea las esquinas del bot贸n
cursor: 'pointer', // Cambia el cursor al pasar sobre el bot贸n para indicar que es interactivo
}}
>
{label} {/* Texto que se muestra en el bot贸n */}
</button>
{/* Si el bot贸n est谩 activo (isActive es true), renderiza los componentes hijos */}
{isActive && children}
</>
));
// Componente funcional principal que contiene el mapa y los controles de toggle
const MapViewGeoCer = () => {
// useRef para acceder a la instancia del componente MapContainer
const mapRef = useRef(null);
// Estado para controlar qu茅 bot贸n de toggle est谩 actualmente activo
const [activeButton, setActiveButton] = useState(null);
// Funci贸n para manejar el clic en los botones de toggle
// Cambia el estado de activeButton para mostrar u ocultar el componente asociado
const handleToggleButton = (key) => {
setActiveButton(prev => (prev === key ? null : key));
};
return (
// Componente contenedor del mapa de Leaflet
<MapContainer
center={[19.285653, -99.147809]} // Coordenadas iniciales del centro del mapa (Ciudad de M茅xico)
zoom={13} // Nivel de zoom inicial del mapa
ref={mapRef} // Asigna la referencia al elemento MapContainer
style={{ height: "100vh", width: "100vw" }} // Establece la altura y el ancho del mapa para ocupar toda la ventana
>
{/* Componente para la capa de tiles del mapa (OpenStreetMap en este caso) */}
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' // Atribuci贸n requerida por OpenStreetMap
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" // URL del servicio de tiles de OpenStreetMap
/>
{/* Bot贸n de toggle para el manejo del sujeto piloto */}
<ToggleButton
label="Sujeto Piloto" // Texto del bot贸n
top={80} // Posici贸n vertical del bot贸n
left={10} // Posici贸n horizontal del bot贸n
isActive={activeButton === 'Sujetopiloto'} // Indica si este bot贸n est谩 activo
onClick={() => handleToggleButton('Sujetopiloto')} // Funci贸n a ejecutar al hacer clic
>
{/* Si el bot贸n est谩 activo, renderiza el componente ManejoSujetoPiloto */}
<ManejoSujetoPiloto />
</ToggleButton>
{/* Bot贸n de toggle para el manejo de l铆neas */}
<ToggleButton
label="L铆neas"
top={120}
left={10}
isActive={activeButton === 'lineas'}
onClick={() => handleToggleButton('lineas')}
>
<ManejoLineas />
</ToggleButton>
{/* Bot贸n de toggle para el manejo de pol铆gonos */}
<ToggleButton
label="Pol铆gonos"
top={160}
left={10}
isActive={activeButton === 'poligonos'}
onClick={() => handleToggleButton('poligonos')}
>
<ManejoPoligono />
</ToggleButton>
{/* Bot贸n de toggle para el manejo de marcadores */}
<ToggleButton
label="Marcadores"
top={200}
left={10}
isActive={activeButton === 'marcadores'}
onClick={() => handleToggleButton('marcadores')}
>
<ManejoMarcadores />
</ToggleButton>
{/* Bot贸n de toggle para el manejo de c铆rculos */}
<ToggleButton
label="C铆rculos"
top={240}
left={10}
isActive={activeButton === 'circulos'}
onClick={() => handleToggleButton('circulos')}
>
<ManejoCircunferencia />
</ToggleButton>
</MapContainer>
);
};
// Exporta el componente MapViewGeoCer para su uso en otras partes de la aplicaci贸n
export default MapViewGeoCer;
siguiente componente :
import React, { useRef, useState, useCallback, useEffect } from "react";
import { TileLayer, useMapEvents, Marker, Popup, Circle, Polyline, Polygon, LayerGroup } from "react-leaflet";
import L from "leaflet"; // Importaci贸n de la biblioteca Leaflet base
import icon from "./icon"; // Importaci贸n de un icono personalizado para los marcadores
import * as turf from "@turf/turf"; // Importaci贸n de la biblioteca turf.js para an谩lisis geoespacial
import ManejoSujetoPilotoAviso from "./ManejadorSujetoPilotoAviso"; // Importaci贸n de un componente para mostrar avisos relacionados con el sujeto piloto
// Componente funcional MapEvents que utiliza el hook useMapEvents para manejar eventos del mapa
const MapEvents = ({ onMapClick }) => {
// useMapEvents permite adjuntar listeners a los eventos del mapa
useMapEvents({
// Escucha el evento de 'click' en el mapa
click: (e) => {
// Cuando se hace clic en el mapa, llama a la funci贸n onMapClick proporcionando las coordenadas (latitud y longitud) del clic
onMapClick(e.latlng);
}
});
// Este componente no renderiza nada visualmente, solo adjunta el event listener
return null;
};
// Componente funcional ManejoSujetoPiloto que gestiona la visualizaci贸n e interacci贸n con marcadores en el mapa
const ManejoSujetoPiloto = () => {
// Estado para almacenar la lista de datos geogr谩ficos (marcadores)
const [datosGeograficos, setDatosGeograficos] = useState([]);
// Estado para almacenar la lista de l铆neas dibujadas en el mapa
const [lineas, setLineas] = useState([]);
// Estado para almacenar el ID del marcador actualmente seleccionado (para mostrar informaci贸n o realizar acciones sobre 茅l)
const [marcadorSeleccionado, setMarcadorSeleccionado] = useState(null);
// Estado para almacenar la informaci贸n del punto m谩s cercano a un marcador seleccionado
const [puntoMasCercano, setPuntoMasCercano] = useState(null);
// Estado para controlar la visibilidad de un aviso relacionado con el punto m谩s cercano
const [puntoMasCercanoAviso, setPuntoMasCercanoAviso] = useState(false);
// useRef para mantener una referencia a los popups de los marcadores (aunque no se est茅 utilizando expl铆citamente aqu铆 para controlar la apertura/cierre)
const popupRefs = useRef([]);
// useRef para mantener una referencia al LayerGroup que contiene los marcadores y otros elementos relacionados
const layerGroupRef = useRef(null); // A帽adida referencia faltante
// useCallback para memoizar la funci贸n que agrega un nuevo marcador
const agregarMarcador = useCallback((latlng) => {
// Crea un nuevo objeto de marcador con un ID 煤nico (timestamp) y las coordenadas con precisi贸n de 6 decimales
const nuevoMarcador = {
id: Date.now(),
lat: parseFloat(latlng.lat.toFixed(6)),
lng: parseFloat(latlng.lng.toFixed(6))
};
// Actualiza el estado de datosGeograficos a帽adiendo el nuevo marcador al array anterior
setDatosGeograficos((prev) => [...prev, nuevoMarcador]);
}, []); // La funci贸n solo depende de setDatosGeograficos, por lo que el array de dependencias est谩 vac铆o
// useCallback para memoizar la funci贸n que elimina un marcador por su ID
const eliminarMarcador = useCallback((id) => {
// Actualiza el estado de datosGeograficos filtrando el array para excluir el marcador con el ID proporcionado
setDatosGeograficos(prev => prev.filter(marker => marker.id !== id));
// Si el marcador eliminado era el seleccionado, resetea el estado de marcadorSeleccionado y puntoMasCercano
if (marcadorSeleccionado === id) {
setMarcadorSeleccionado(null);
setPuntoMasCercano(null);
}
}, [marcadorSeleccionado]); // La funci贸n depende de marcadorSeleccionado
// useCallback para memoizar la funci贸n que actualiza la posici贸n de un marcador existente
const actualizarPosicionMarcador = useCallback((id, latlng) => {
// Actualiza el estado de datosGeograficos mapeando sobre el array y modificando el marcador con el ID proporcionado
setDatosGeograficos(prev =>
prev.map(marker =>
marker.id === id ? {
...marker,
lat: parseFloat(latlng.lat.toFixed(6)),
lng: parseFloat(latlng.lng.toFixed(6))
} : marker
)
);
}, []); // La funci贸n solo depende de setDatosGeograficos
// useCallback para memoizar la funci贸n que agrega una nueva l铆nea al estado de l铆neas
const agregarLinea = useCallback((coordenadas) => {
// Actualiza el estado de l铆neas a帽adiendo las nuevas coordenadas de la l铆nea al array anterior
setLineas(prev => [...prev, coordenadas]);
}, []); // La funci贸n solo depende de setLineas
// useCallback para memoizar la funci贸n que calcula el punto m谩s cercano a un marcador seleccionado utilizando la biblioteca turf.js
const calcularPuntoMasCercano = useCallback((markerId) => {
// Si hay menos de dos marcadores, no se puede calcular un punto m谩s cercano
if (datosGeograficos.length < 2) {
setPuntoMasCercano(null);
return;
}
// Encuentra el marcador para el cual se va a calcular el punto m谩s cercano
const marcador = datosGeograficos.find(m => m.id === markerId);
if (!marcador) return;
// Crea un objeto de punto turf.js para el marcador actual
const punto = turf.point([marcador.lng, marcador.lat]);
let puntoMasCercano = null;
let distanciaMinima = Infinity;
let coordenadasCercano = null;
// Itera sobre todos los dem谩s marcadores para encontrar el m谩s cercano
datosGeograficos.forEach(otroMarker => {
// Excluye el marcador actual de la comparaci贸n
if (otroMarker.id !== markerId) {
// Crea un objeto de punto turf.js para el otro marcador
const otroPunto = turf.point([otroMarker.lng, otroMarker.lat]);
// Calcula la distancia en metros entre los dos puntos usando turf.distance
const distancia = turf.distance(punto, otroPunto, { units: 'meters' });
const otraCoordenadas = [otroMarker.lng, otroMarker.lat];
// Si la distancia actual es menor que la distancia m铆nima encontrada hasta ahora, actualiza el punto m谩s cercano y la distancia m铆nima
if (distancia < distanciaMinima) {
distanciaMinima = distancia;
puntoMasCercano = otroMarker;
coordenadasCercano = otraCoordenadas;
setPuntoMasCercanoAviso(true); // Activa el aviso del punto m谩s cercano
}
}
});
// Actualiza el estado de puntoMasCercano con la informaci贸n del marcador m谩s cercano y la distancia
setPuntoMasCercano(puntoMasCercano ? {
id: puntoMasCercano.id,
distancia: distanciaMinima,
coordenadas: coordenadasCercano
} : null);
}, [datosGeograficos]); // La funci贸n depende del estado de datosGeograficos
// useEffect para recalcular el punto m谩s cercano cuando cambia el marcador seleccionado
useEffect(() => {
if (marcadorSeleccionado) {
calcularPuntoMasCercano(marcadorSeleccionado);
} else {
setPuntoMasCercano(null);
}
}, [marcadorSeleccionado, calcularPuntoMasCercano]); // Se ejecuta cuando cambia marcadorSeleccionado o calcularPuntoMasCercano
// useCallback para memoizar el handler del evento de clic en el mapa
const handleMapClick = useCallback((latlng) => {
agregarMarcador(latlng); // Llama a la funci贸n para agregar un nuevo marcador en la ubicaci贸n del clic
}, [agregarMarcador]); // Depende de la funci贸n agregarMarcador
// useCallback para memoizar el handler del evento de dragend de un marcador
const handleDragEnd = useCallback((markerId) => (e) => {
actualizarPosicionMarcador(markerId, e.target.getLatLng()); // Llama a la funci贸n para actualizar la posici贸n del marcador arrastrado
}, [actualizarPosicionMarcador]); // Depende de la funci贸n actualizarPosicionMarcador
// useCallback para memoizar el handler del evento de clic en el bot贸n de eliminar de un popup
const handleDeleteMarker = useCallback((markerId, e) => {
e.stopPropagation(); // Evita que el clic en el bot贸n se propague al mapa y cause otros eventos
eliminarMarcador(markerId); // Llama a la funci贸n para eliminar el marcador
}, [eliminarMarcador]); // Depende de la funci贸n eliminarMarcador
// Funci贸n para obtener la posici贸n (latitud y longitud) de un marcador dado su ID
const getMarkerPosition = (id) => {
const marker = datosGeograficos.find(m => m.id === id);
return marker ? [marker.lat, marker.lng] : null;
};
return (
<>
{/* LayerGroup para agrupar los marcadores y otros elementos relacionados */}
<LayerGroup ref={layerGroupRef}>
{/* Mapea sobre el array de datosGeograficos para renderizar un Marker para cada objeto */}
{datosGeograficos.map((marker, index) => {
// Determina si el marcador actual es el seleccionado
const esSeleccionado = marcadorSeleccionado === marker.id;
// Determina si el marcador seleccionado tiene un punto m谩s cercano calculado
const tieneCercano = puntoMasCercano && esSeleccionado;
return (
// React.Fragment para agrupar elementos sin agregar un nodo extra al DOM
<React.Fragment key={marker.id}>
{/* Componente Marker de react-leaflet */}
<Marker
position={[marker.lat, marker.lng]} // Posici贸n del marcador
icon={icon} // Icono personalizado para el marcador
draggable // Permite que el marcador sea arrastrable
eventHandlers={{
// Handler para el evento dragend (cuando se suelta el marcador despu茅s de arrastrar)
dragend: handleDragEnd(marker.id),
// Handler para el evento popupopen (cuando se abre el popup del marcador)
popupopen: () => setMarcadorSeleccionado(marker.id),
// Handler para el evento popupclose (cuando se cierra el popup del marcador)
popupclose: () => setMarcadorSeleccionado(null)
}}
// Referencia al elemento Marker (puede ser 煤til para acceder a la instancia de Leaflet subyacente)
ref={(ref) => {
if (ref) popupRefs.current[index] = ref;
}}
>
{/* Componente Popup que se muestra al hacer clic en el marcador */}
<Popup>
<div>
<p>ID: {marker.id}</p>
{/* Si el marcador est谩 seleccionado y se ha encontrado un punto m谩s cercano, muestra la informaci贸n */}
{tieneCercano && puntoMasCercano?.coordenadas && (
<div>
<h4>Punto m谩s cercano:</h4>
<p>Distancia: {puntoMasCercano.distancia.toFixed(2)} metros</p>
</div>
)}
{/* Bot贸n para eliminar el marcador */}
<button
onClick={(e) => handleDeleteMarker(marker.id, e)}
style={{
marginTop: '10px',
padding: '5px 10px',
backgroundColor: '#ff4444',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Eliminar marcador
</button>
</div>
</Popup>
</Marker>
{/* Si se ha activado el aviso del punto m谩s cercano y hay coordenadas disponibles, renderiza el componente ManejoSujetoPilotoAviso */}
{puntoMasCercanoAviso && puntoMasCercano?.coordenadas && (
<ManejoSujetoPilotoAviso
coordenadasAviso={[puntoMasCercano.coordenadas[1], puntoMasCercano.coordenadas[0]]} // [lat, lng]
coordenadasOrigen={[marker.lat, marker.lng]}
layerGroupRef={layerGroupRef}
/>
)}
</React.Fragment>
);
})}
</LayerGroup>
{/* Si hay un marcador seleccionado y se ha encontrado un punto m谩s cercano, renderiza una Polyline para conectar ambos puntos */}
{marcadorSeleccionado && puntoMasCercano?.id && (
<Polyline
positions={[
getMarkerPosition(marcadorSeleccionado),
getMarkerPosition(puntoMasCercano.id)
].filter(Boolean)} // Filtra valores nulos en caso de que las posiciones no se encuentren
color="#ff7800"
weight={4}
dashArray="10, 5" // Estilo de l铆nea punteada
/>
)}
{/* Mapea sobre el array de l铆neas para renderizar componentes Polyline */}
{lineas.map((linea, index) => (
<Polyline key={`linea-${index}`} positions={linea} color="red" />
))}
{/* Componente MapEvents para manejar los clics en el mapa y agregar nuevos marcadores */}
<MapEvents onMapClick={handleMapClick} />
</>
);
};
// Exporta el componente ManejoSujetoPiloto para su uso en otras partes de la aplicaci贸n
export default ManejoSujetoPiloto;
siguiente componente
import React, { useEffect, useRef } from "react"; // Importa React y los hooks useEffect y useRef
import { Circle } from "react-leaflet"; // Importa el componente Circle de react-leaflet (aunque no se usa directamente en este c贸digo)
import L from "leaflet"; // Importa la biblioteca principal de Leaflet
import * as turf from "@turf/turf"; // Importa la biblioteca turf.js para an谩lisis espacial (aunque no se usa en este c贸digo)
// Define el componente funcional ManejoSujetoPilotoAviso
const ManejoSujetoPilotoAviso = ({
coordenadasAviso, // Propiedad que recibe un array con las coordenadas [latitud, longitud] del aviso
coordenadasOrigen, // Propiedad que recibe las coordenadas de origen (no se utiliza en este componente)
layerGroupRef // Propiedad que recibe una referencia mutable a un grupo de capas de Leaflet
}) => {
// Crea una referencia mutable para el objeto L.Circle de Leaflet
const circleRef = useRef<L.Circle | null>(null);
// Hook useEffect que se ejecuta despu茅s de cada renderizado (similar a componentDidMount y componentWillUnmount)
useEffect(() => {
// Verifica si la referencia al grupo de capas no es v谩lida o si las coordenadas del aviso no est谩n definidas o no son un array
if (!layerGroupRef.current || !coordenadasAviso || !Array.isArray(coordenadasAviso)) {
return; // Sale de la funci贸n si alguna de las dependencias no es v谩lida
}
// Valida las coordenadas del aviso
const [lat, lng] = coordenadasAviso; // Desestructura el array de coordenadas en latitud y longitud
if (typeof lat !== 'number' || typeof lng !== 'number' || isNaN(lat) || isNaN(lng)) {
console.error('Coordenadas inv谩lidas:', coordenadasAviso);
return; // Sale de la funci贸n si las coordenadas no son n煤meros v谩lidos
}
// Crear un nuevo c铆rculo de Leaflet
const circle = new L.Circle([lat, lng], {
radius: 50, // Define el radio del c铆rculo en metros
color: 'blue', // Define el color del borde del c铆rculo
fillColor: 'blue', // Define el color de relleno del c铆rculo
fillOpacity: 0.2 // Define la opacidad del relleno del c铆rculo (0.2 es 20% de opacidad)
});
// Agregar el c铆rculo al grupo de capas proporcionado por la referencia
layerGroupRef.current.addLayer(circle);
// Guarda la instancia del c铆rculo en la referencia mutable para poder acceder a ella despu茅s
circleRef.current = circle;
// Imprime en la consola las coordenadas del aviso
console.log("Coordenadas del aviso:", coordenadasAviso);
// Funci贸n de limpieza que se ejecuta cuando el componente se desmonta o antes de que el useEffect se vuelva a ejecutar
return () => {
// Verifica si la referencia al c铆rculo y al grupo de capas son v谩lidas
if (circleRef.current && layerGroupRef.current) {
// Elimina el c铆rculo del grupo de capas para evitar fugas de memoria y elementos duplicados
layerGroupRef.current.removeLayer(circleRef.current);
}
};
// El useEffect se ejecutar谩 cada vez que cambien las coordenadas del aviso o la referencia al grupo de capas
}, [coordenadasAviso, layerGroupRef]);
// El componente no renderiza ning煤n elemento visual directamente
return null;
};
// Exporta el componente ManejoSujetoPilotoAviso para que pueda ser utilizado en otras partes de la aplicaci贸n
export default ManejoSujetoPilotoAviso;
siguiente componente
import React, { useState, useEffect, useCallback, useRef, createContext, useContext } from "react";
import {
MapContainer, // Componente principal del mapa de Leaflet
TileLayer, // Componente para agregar una capa de tiles (im谩genes del mapa)
useMapEvents, // Hook para acceder a los eventos del mapa
Polyline, // Componente para dibujar l铆neas en el mapa
Polygon, // Componente para dibujar pol铆gonos en el mapa
Popup, // Componente para mostrar ventanas emergentes en el mapa
Marker, // Componente para mostrar marcadores en el mapa
LayerGroup // Componente para agrupar capas de Leaflet
} from "react-leaflet";
import * as turf from "@turf/turf"; // Importa la biblioteca turf.js para an谩lisis geoespacial (buffer, etc.)
import L from "leaflet"; // Importa la biblioteca Leaflet base
import PuntoMonitoreadoLinea from "./PuntoMonitoreadoLinea"; // Importa un componente personalizado para puntos de monitoreo de l铆nea
// Define un icono personalizado para los v茅rtices de las l铆neas editables
const vertexIcon = new L.Icon({
iconUrl: 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-512.png',
iconSize: [20, 20],
iconAnchor: [10, 10]
});
// Crea un Context de React para compartir datos geogr谩ficos entre componentes
const DatosGeograficosContext = createContext();
// Hook personalizado para acceder f谩cilmente al contexto de datos geogr谩ficos
export const useDatosGeograficos = () => {
const context = useContext(DatosGeograficosContext);
if (!context) {
throw new Error("useDatosGeograficos debe ser usado dentro de un DatosGeograficosProvider");
}
return context;
};
// Provider del Context de DatosGeograficos que envuelve a los componentes que necesitan acceder a estos datos
const DatosGeograficosProvider = ({ children }) => {
// Estado para almacenar la lista de l铆neas dibujadas, cada l铆nea contiene sus puntos y su buffer
const [lineas, setLineas] = useState([]);
// Estado para almacenar los buffers (pol铆gonos alrededor de las l铆neas)
const [buffers, setBuffers] = useState([]);
// Estado para almacenar los puntos temporales que el usuario est谩 dibujando para crear una nueva l铆nea
const [puntosTemporales, setPuntosTemporales] = useState([]);
// Estado para controlar el radio del buffer alrededor de las l铆neas (en kil贸metros)
const [radioBuffer, setRadioBuffer] = useState(0.1);
// Estado para almacenar el 铆ndice de la l铆nea actualmente seleccionada
const [lineaSeleccionada, setLineaSeleccionada] = useState(null);
// Estado para indicar si el mapa est谩 en modo de edici贸n de una l铆nea
const [editando, setEditando] = useState(false);
// useCallback para memoizar la funci贸n que agrega un punto temporal al dibujar una nueva l铆nea
const agregarPuntoTemporal = useCallback((latlng) => {
setPuntosTemporales((prev) => [...prev, { lat: latlng.lat, lng: latlng.lng }]);
}, []);
// useCallback para memoizar la funci贸n que calcula el buffer alrededor de una l铆nea utilizando turf.js
const calcularBuffer = useCallback((linea) => {
if (!Array.isArray(linea) || linea.length < 2) return null;
// Crea un objeto LineString de turf a partir de las coordenadas de la l铆nea
const lineString = turf.lineString(linea.map(point => [point.lng, point.lat]));
// Calcula el buffer alrededor de la LineString con el radio especificado
const buffered = turf.buffer(lineString, radioBuffer, { units: 'kilometers' });
return buffered;
}, [radioBuffer]); // Depende del radio del buffer
// useCallback para memoizar la funci贸n que finaliza el dibujo de una nueva l铆nea
const finalizarLinea = useCallback(() => {
if (puntosTemporales.length > 1) {
// Crea una nueva l铆nea con los puntos temporales actuales
const nuevaLinea = [...puntosTemporales];
// Calcula el buffer para la nueva l铆nea
const nuevoBuffer = calcularBuffer(nuevaLinea);
// Agrega la nueva l铆nea y su buffer al estado
setLineas((prev) => [...prev, { puntos: nuevaLinea, buffer: nuevoBuffer }]);
setBuffers((prev) => [...prev, nuevoBuffer]);
// Limpia los puntos temporales y desactiva el modo de edici贸n
setPuntosTemporales([]);
setEditando(false);
return true;
}
// Si no hay suficientes puntos para crear una l铆nea, limpia los puntos temporales y deselecciona la l铆nea
setPuntosTemporales([]);
setLineaSeleccionada(null);
setEditando(false);
return false;
}, [puntosTemporales, calcularBuffer]); // Depende de los puntos temporales y la funci贸n para calcular el buffer
// useCallback para memoizar la funci贸n que selecciona una l铆nea por su 铆ndice
const seleccionarLinea = useCallback((index) => {
setLineaSeleccionada(index);
}, []);
// useCallback para memoizar la funci贸n que elimina la l铆nea actualmente seleccionada
const eliminarLineaSeleccionada = useCallback(() => {
if (lineaSeleccionada !== null) {
// Filtra la lista de l铆neas y buffers para eliminar la l铆nea seleccionada
setLineas(prev => prev.filter((_, i) => i !== lineaSeleccionada));
setBuffers(prev => prev.filter((_, i) => i !== lineaSeleccionada));
// Resetea el estado de la l铆nea seleccionada, puntos temporales y modo de edici贸n
setLineaSeleccionada(null);
setPuntosTemporales([]);
setEditando(false);
}
}, [lineaSeleccionada]); // Depende de la l铆nea seleccionada
// useCallback para memoizar la funci贸n que actualiza el radio del buffer
const actualizarRadioBuffer = useCallback((nuevoRadio) => {
setRadioBuffer(nuevoRadio);
// Recalcula los buffers para todas las l铆neas existentes con el nuevo radio
setBuffers(
lineas.map((linea) => calcularBuffer(linea.puntos || linea))
);
}, [lineas, calcularBuffer]); // Depende de la lista de l铆neas y la funci贸n para calcular el buffer
// useCallback para memoizar la funci贸n que actualiza la posici贸n de un v茅rtice de una l铆nea editable
const actualizarVertice = useCallback((lineaIndex, verticeIndex, newLatLng) => {
setLineas(prev => prev.map((linea, i) => {
if (i === lineaIndex) {
// Crea una copia de los puntos de la l铆nea y actualiza el v茅rtice espec铆fico
const nuevosPuntos = [...linea.puntos];
nuevosPuntos[verticeIndex] = { lat: newLatLng.lat, lng: newLatLng.lng };
return { ...linea, puntos: nuevosPuntos };
}
return linea;
}));
// Actualiza los buffers despu茅s de modificar los v茅rtices
}, []);
// useCallback para memoizar la funci贸n que regenera los buffers de todas las l铆neas
const regenerarBuffers = useCallback(() => {
setBuffers(lineas.map(linea => calcularBuffer(linea.puntos || linea)));
}, [lineas, calcularBuffer]); // Depende de la lista de l铆neas y la funci贸n para calcular el buffer
// Objeto con los valores y funciones que se proporcionar谩n a los componentes consumidores
const value = {
lineas,
buffers,
puntosTemporales,
radioBuffer,
lineaSeleccionada,
editando,
agregarPuntoTemporal,
finalizarLinea,
seleccionarLinea,
eliminarLineaSeleccionada,
actualizarRadioBuffer,
actualizarVertice,
setEditando,
regenerarBuffers
};
// Proporciona el contexto a los componentes hijos
return (
<DatosGeograficosContext.Provider value={value}>
{children}
</DatosGeograficosContext.Provider>
);
};
// Componente funcional MapEvents para manejar eventos del mapa (clic y clic derecho)
const MapEvents = ({ onMapClick, onMapRightClick }) => {
useMapEvents({
click: (e) => onMapClick(e.latlng), // Llama a la funci贸n onMapClick con las coordenadas del clic
contextmenu: (e) => {
e.originalEvent.preventDefault(); // Evita el men煤 contextual predeterminado del navegador
onMapRightClick(e); // Llama a la funci贸n onMapRightClick con el evento
}
});
return null; // Este componente no renderiza nada visualmente
};
// Componente funcional ManejoLineas que utiliza el contexto para gestionar la creaci贸n y edici贸n de l铆neas
const ManejoLineas = () => {
// Accede a los valores y funciones del contexto de datos geogr谩ficos
const {
lineas,
buffers,
puntosTemporales,
radioBuffer,
lineaSeleccionada,
editando,
agregarPuntoTemporal,
finalizarLinea,
seleccionarLinea,
eliminarLineaSeleccionada,
actualizarRadioBuffer,
actualizarVertice,
setEditando,
regenerarBuffers
} = useDatosGeograficos();
// Estado para controlar qu茅 l铆nea muestra el componente PuntoMonitoreadoLinea
const [mostrarPunto, setMostrarPunto] = useState<number | null>(null);
// useRef para mantener una referencia al LayerGroup que contiene las l铆neas y buffers
const layerGroupRef = useRef(null);
// useRef para mantener una referencia a los popups de las l铆neas
const popupRefs = useRef([]);
// Estado para controlar la visibilidad del di谩logo de confirmaci贸n de eliminaci贸n
const [mostrarConfirmacion, setMostrarConfirmacion] = useState(false);
// useCallback para memoizar el handler del evento de clic en el mapa
const handleMapClick = useCallback((latlng) => {
// Si no se est谩 editando una l铆nea, agrega un punto temporal
if (!editando) {
agregarPuntoTemporal(latlng);
}
}, [agregarPuntoTemporal, editando]); // Depende de la funci贸n para agregar puntos temporales y el estado de edici贸n
// useCallback para memoizar el handler del evento de clic derecho en el mapa
const handleMapRightClick = useCallback(() => {
// Si no se est谩 editando, finaliza la l铆nea actual
if (!editando) {
const seCreo = finalizarLinea();
// Si se cre贸 una nueva l铆nea, la selecciona y abre su popup despu茅s de un peque帽o retraso
if (seCreo) {
setTimeout(() => {
const nuevaLineaIndex = lineas.length;
seleccionarLinea(nuevaLineaIndex);
const ref = popupRefs.current[nuevaLineaIndex];
if (ref && ref._map) {
ref.openOn(ref._map);
}
}, 100);
}
}
}, [editando, finalizarLinea, lineas.length, seleccionarLinea]); // Depende del estado de edici贸n, la funci贸n para finalizar la l铆nea, la longitud de las l铆neas y la funci贸n para seleccionar una l铆nea
// Handler para el cambio en el input del radio del buffer
const handleRadioChange = (e) => {
const nuevoRadio = parseFloat(e.target.value);
if (!isNaN(nuevoRadio)) {
actualizarRadioBuffer(nuevoRadio);
}
};
// Handler para seleccionar una l铆nea al hacer clic en ella
const handleSeleccionarLinea = (index) => {
seleccionarLinea(index);
// Abre el popup de la l铆nea seleccionada
const ref = popupRefs.current[index];
if (ref && ref._map) {
ref.openOn(ref._map);
}
};
// Funci贸n para iniciar el modo de edici贸n de una l铆nea
const iniciarEdicion = (index) => {
setEditando(true);
seleccionarLinea(index);
};
// Funci贸n para finalizar el modo de edici贸n de una l铆nea
const finalizarEdicion = () => {
setEditando(false);
regenerarBuffers(); // Recalcula los buffers despu茅s de la edici贸n
};
return (
<>
<LayerGroup ref={layerGroupRef}>
{/* Mapea sobre la lista de l铆neas para renderizar cada una */}
{lineas.map((linea, index) => (
<React.Fragment key={`linea-${index}`}>
{/* Renderiza el buffer de la l铆nea como un Pol铆gono */}
{buffers[index] && (
<Polygon
positions={buffers[index].geometry.coordinates[0].map(coord => ({ lat: coord[1], lng: coord[0] }))}
color={lineaSeleccionada === index ? "#ff6347" : "#009688"} // Cambia el color si la l铆nea est谩 seleccionada
fillOpacity={0.3}
stroke={false}
>
{/* Popup asociado al buffer para acciones espec铆ficas de la l铆nea */}
<Popup ref={(ref) => popupRefs.current[index] = ref}>
<button
onClick={() => setMostrarPunto(index)}
style={{
padding: '8px 16px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginRight: '10px',
marginBottom: '10px'
}}
>
Agregar Punto Monitoreo
</button>
</Popup>
</Polygon>
)}
{/* Renderiza el componente para agregar puntos de monitoreo a la l铆nea */}
{mostrarPunto === index && (
<PuntoMonitoreadoLinea layerGroupRef={layerGroupRef} buffer={buffers[index]}/>
)}
{/* Renderiza una Polyline invisible pero interactiva para facilitar la selecci贸n de la l铆nea */}
<Polyline
positions={linea.puntos || linea}
color="transparent"
weight={15}
opacity={0}
interactive={true}
eventHandlers={{
click: () => handleSeleccionarLinea(index)
}}
/>
{/* Renderiza la Polyline visible que representa la l铆nea */}
<Polyline
positions={linea.puntos || linea}
color={lineaSeleccionada === index ? "red" : "blue"} // Cambia el color si la l铆nea est谩 seleccionada
weight={6}
>
{/* Popup asociado a la l铆nea para mostrar opciones de edici贸n y eliminaci贸n */}
<Popup ref={(ref) => popupRefs.current[index] = ref}>
<div style={{ padding: '10px' }}>
<h4 style={{ marginTop: 0 }}>L铆nea {index + 1}</h4>
{/* Botones de Editar y Eliminar si no se est谩 editando */}
{!editando && (
<>
<button
onClick={() => iniciarEdicion(index)}
style={{
padding: '8px 16px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginRight: '10px',
marginBottom: '10px'
}}
>
Editar
</button>
<button
onClick={(e) => {
e.stopPropagation();
setMostrarConfirmacion(true);
}}
style={{
padding: '8px 16px',
backgroundColor: '#ff4444',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginBottom: '10px'
}}
>
Eliminar
</button>
</>
)}
{/* Bot贸n de Finalizar Edici贸n si se est谩 editando la l铆nea actual */}
{editando && lineaSeleccionada === index && (
<button
onClick={() => finalizarEdicion()}
style={{
padding: '8px 16px',
backgroundColor: '#2196F3',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginBottom: '10px'
}}
>
Finalizar Edici贸n
</button>
)}
{/* Input para cambiar el radio del buffer */}
<div style={{ marginTop: '10px' }}>
<label>
Radio del buffer (km):
<input
type="number"
step="0.01"
min="0.01"
max="10"
value={radioBuffer}
onChange={handleRadioChange}
style={{ width: '60px', marginLeft: '5px' }}
/>
</label>
</div>
</div>
</Popup>
</Polyline>
{/* Renderiza Markers para los v茅rtices de la l铆nea si se est谩 editando la l铆nea actual */}
{editando && lineaSeleccionada === index && (linea.puntos || linea).map((punto, verticeIndex) => (
<Marker
key={`vertice-${index}-${verticeIndex}`}
position={[punto.lat, punto.lng]}
icon={vertexIcon}
draggable={true}
eventHandlers={{
dragend: (e) => {
actualizarVertice(index, verticeIndex, e.target.getLatLng());
}
}}
/>
))}
</React.Fragment>
))}
{/* Renderiza la l铆nea temporal que se est谩 dibujando */}
{puntosTemporales.length > 1 && (
<Polyline positions={puntosTemporales} color="red" dashArray="4" />
)}
</LayerGroup>
{/* Componente MapEvents para manejar clics en el mapa */}
<MapEvents
onMapClick={handleMapClick}
onMapRightClick={handleMapRightClick}
/>
{/* Di谩logo de confirmaci贸n para eliminar una l铆nea */}
{mostrarConfirmacion && (
<div style={{
position: 'fixed',
top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex', justifyContent: 'center', alignItems: 'center',
zIndex: 1000
}}>
<div style={{
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
width: '300px',
boxShadow: '0 2px 10px rgba(0,0,0,0.3)'
}}>
<h3>¿Est谩s seguro?</h3>
<p>¿Deseas eliminar la l铆nea seleccionada?</p>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '20px' }}>
<button
onClick={() => setMostrarConfirmacion(false)}
style={{ marginRight: '10px', padding: '6px 12px' }}
>
Cancelar
</button>
<button
onClick={() => {
eliminarLineaSeleccionada();
setMostrarConfirmacion(false);
}}
style={{
backgroundColor: '#ff4444',
color: 'white',
padding: '6px 12px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Eliminar
</button>
</div>
</div>
</div>
)}
</>
);
};
// Componente Provider que envuelve la aplicaci贸n y proporciona el contexto de datos geogr谩ficos
export const LineasProvider = ({ children }) => (
<DatosGeograficosProvider>
{children}
<ManejoLineas />
</DatosGeograficosProvider>
);
export default LineasProvider;
suguiente componennte :
import React, { useState, useEffect, useRef } from "react";
import { useMapEvents } from "react-leaflet"; // Hook para acceder a los eventos del mapa de Leaflet
import L from "leaflet"; // Importa la biblioteca principal de Leaflet
import icon from "./icon"; // Importa el icono personalizado para los marcadores
import * as turf from "@turf/turf"; // Importa la biblioteca turf.js para an谩lisis espacial
// Define la interfaz para las coordenadas de latitud y longitud
interface Coordenadas {
lat: number;
lng: number;
}
// Define la interfaz para las propiedades del componente PuntoMonitoreadoProps
interface PuntoMonitoreadoProps {
layerGroupRef: React.RefObject<L.LayerGroup>; // Referencia mutable a un grupo de capas de Leaflet
buffer: turf.helpers.Feature<turf.helpers.Polygon | turf.helpers.MultiPolygon>; // Objeto GeoJSON que representa un buffer (pol铆gono o multipol铆gono)
}
/**
* Funci贸n para corregir la geometr铆a de un buffer, asegurando que el primer y 煤ltimo punto de cada anillo sean iguales
* para formar pol铆gonos cerrados v谩lidos.
* @param buffer Objeto GeoJSON del buffer a corregir.
* @returns Objeto GeoJSON del buffer corregido o null si la geometr铆a no es v谩lida.
*/
function corregirGeometria(
buffer: turf.helpers.Feature<turf.helpers.Polygon | turf.helpers.MultiPolygon>
): turf.helpers.Feature<turf.helpers.Polygon | turf.helpers.MultiPolygon> | null {
// Verifica si el buffer tiene geometr铆a y coordenadas definidas
if (!buffer.geometry || !buffer.geometry.coordinates) {
console.error("Buffer no tiene geometr铆a o coordenadas definidas.");
return null;
}
// Obtiene el tipo de geometr铆a del buffer
const tipo = buffer.geometry.type;
// Procesa si la geometr铆a es un Pol铆gono
if (tipo === "Polygon") {
// Itera sobre cada anillo (exterior e interior) del pol铆gono
const coords = buffer.geometry.coordinates.map(anillo => {
// Obtiene el primer y 煤ltimo punto del anillo
const primero = anillo[0];
const ultimo = anillo[anillo.length - 1];
// Verifica si el primer y 煤ltimo punto son diferentes
if (primero[0] !== ultimo[0] || primero[1] !== ultimo[1]) {
// Si son diferentes, agrega el primer punto al final para cerrar el pol铆gono
return [...anillo, primero];
}
// Si ya est谩n cerrados, devuelve el anillo sin cambios
return anillo;
});
// Crea un nuevo objeto GeoJSON de tipo Pol铆gono con las coordenadas corregidas y las propiedades originales
return turf.polygon(coords, buffer.properties || {});
}
// Procesa si la geometr铆a es un MultiPol铆gono
if (tipo === "MultiPolygon") {
// Itera sobre cada pol铆gono del MultiPol铆gono
const coords = buffer.geometry.coordinates.map(poligono =>
// Itera sobre cada anillo de cada pol铆gono
poligono.map(anillo => {
// Obtiene el primer y 煤ltimo punto del anillo
const primero = anillo[0];
const ultimo = anillo[anillo.length - 1];
// Verifica si el primer y 煤ltimo punto son diferentes
if (primero[0] !== ultimo[0] || primero[1] !== ultimo[1]) {
// Si son diferentes, agrega el primer punto al final para cerrar el pol铆gono
return [...anillo, primero];
}
// Si ya est谩n cerrados, devuelve el anillo sin cambios
return anillo;
})
);
// Crea un nuevo objeto GeoJSON de tipo MultiPol铆gono con las coordenadas corregidas y las propiedades originales
return turf.multiPolygon(coords, buffer.properties || {});
}
// Si el tipo de geometr铆a no es Pol铆gono ni MultiPol铆gono, muestra un error
console.error("Tipo de geometr铆a no soportado.");
return null;
}
// Componente funcional de React para mostrar y gestionar un punto de monitoreo en el mapa
const PuntoMonitoreadoLinea: React.FC<PuntoMonitoreadoProps> = ({ layerGroupRef, buffer }) => {
// Estado para almacenar las coordenadas del punto de monitoreo
const [coordenadas, setCoordenadas] = useState<Coordenadas | null>(null);
// Referencia mutable para el marcador de Leaflet
const markerRef = useRef<L.Marker | null>(null);
// Hook para manejar eventos del mapa (doble clic y clic derecho)
useMapEvents({
// Evento de doble clic en el mapa
dblclick: (e) => {
// Establece las coordenadas del clic como las coordenadas del punto de monitoreo
setCoordenadas(e.latlng);
},
// Evento de clic con el bot贸n derecho del rat贸n (men煤 contextual)
contextmenu: (e) => {
// Evita el comportamiento predeterminado del men煤 contextual del navegador
e.originalEvent.preventDefault();
// Establece las coordenadas del clic como las coordenadas del punto de monitoreo
setCoordenadas(e.latlng);
}
});
// Hook useEffect que se ejecuta cuando cambian las coordenadas, la referencia al grupo de capas o el buffer
useEffect(() => {
// Verifica si hay coordenadas, una referencia v谩lida al grupo de capas y un buffer
if (coordenadas && layerGroupRef.current && buffer) {
// Si ya existe un marcador, lo elimina del grupo de capas
if (markerRef.current) {
layerGroupRef.current.removeLayer(markerRef.current);
}
// Crea un nuevo marcador de Leaflet en las coordenadas actuales
const marker = L.marker([coordenadas.lat, coordenadas.lng], {
icon: icon, // Asigna el icono personalizado al marcador
draggable: true // Permite que el marcador sea arrastrable
});
// Agrega el marcador al grupo de capas
layerGroupRef.current.addLayer(marker);
// Actualiza la referencia al marcador
markerRef.current = marker;
// Crea un objeto turf.js de tipo Punto con las coordenadas
const punto = turf.point([coordenadas.lng, coordenadas.lat]);
// Corrige la geometr铆a del buffer utilizando la funci贸n corregirGeometria
const bufferCorregido = corregirGeometria(buffer);
// Variable para almacenar el mensaje de estado (dentro o fuera del buffer)
let mensajeEstado = "";
// Verifica si el buffer corregido es v谩lido
if (bufferCorregido) {
// Utiliza turf.booleanPointInPolygon para verificar si el punto est谩 dentro del buffer
const dentro = turf.booleanPointInPolygon(punto, bufferCorregido);
// Establece el mensaje de estado basado en si el punto est谩 dentro o fuera
mensajeEstado = dentro
? "✅ El punto est谩 <b>dentro</b> del buffer."
: "❌ El punto est谩 <b>fuera</b> del buffer.";
// Imprime el estado en la consola (sin etiquetas HTML)
console.log(mensajeEstado.replace(/<[^>]*>/g, ""));
} else {
// Si el buffer no es v谩lido, establece un mensaje de error
mensajeEstado = "⚠️ Buffer no v谩lido.";
console.error(mensajeEstado);
}
// Define el contenido del popup que se mostrar谩 al hacer clic en el marcador
const popupContent = `
<div>
<p><b>Ubicaci贸n:</b> ${coordenadas.lat.toFixed(5)}, ${coordenadas.lng.toFixed(5)}</p>
<p>${mensajeEstado}</p>
<button id="eliminar-marcador" style="padding: 5px 10px; background-color: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer;">Eliminar</button>
</div>
`;
// Asocia el popup al marcador y lo abre inicialmente
marker.bindPopup(popupContent).openPopup();
// Agrega un evento 'dragend' al marcador para actualizar las coordenadas cuando se arrastra
marker.on("dragend", (e) => {
const position = e.target.getLatLng();
setCoordenadas({ lat: position.lat, lng: position.lng });
});
// Agrega un evento 'popupopen' al marcador para manejar el bot贸n de eliminar dentro del popup
marker.on("popupopen", () => {
// Obtiene la referencia al bot贸n de eliminar por su ID
const btn = document.getElementById("eliminar-marcador");
// Si el bot贸n existe
if (btn) {
// Define la funci贸n que se ejecutar谩 al hacer clic en el bot贸n de eliminar
btn.onclick = () => {
// Verifica si la referencia al marcador actual es v谩lida
if (markerRef.current) {
// Elimina el marcador del grupo de capas
layerGroupRef.current?.removeLayer(markerRef.current);
// Limpia la referencia al marcador
markerRef.current = null;
// Limpia el estado de las coordenadas
setCoordenadas(null);
}
};
}
});
}
// El useEffect se ejecutar谩 cada vez que cambien las coordenadas, la referencia al grupo de capas o el buffer
}, [coordenadas, layerGroupRef, buffer]);
// El componente no renderiza nada visualmente por s铆 mismo, ya que manipula directamente las capas del mapa
return null;
};
// Exporta el componente PuntoMonitoreadoLinea para su uso en otras partes de la aplicaci贸n
export default PuntoMonitoreadoLinea;
siguiente componente
import React, { useState, useCallback, useRef } from "react"; // Importa React y los hooks useState, useCallback y useRef
import {
useMapEvents, // Hook para acceder a los eventos del mapa de Leaflet
Polygon, // Componente para renderizar pol铆gonos en el mapa
Polyline, // Componente para renderizar l铆neas poligonales en el mapa
Popup, // Componente para mostrar ventanas emergentes en el mapa
Marker, // Componente para renderizar marcadores en el mapa
LayerGroup // Componente para agrupar capas de Leaflet
} from "react-leaflet"; // Importa componentes espec铆ficos de react-leaflet
import L from "leaflet"; // Importa la biblioteca principal de Leaflet
import * as turf from "@turf/turf"; // Importa la biblioteca turf.js para an谩lisis espacial
import PuntoMonitoreadoPoligono from './PuntoMonitoreadoPoligono'; // Importa un componente personalizado para el manejo de puntos de monitoreo dentro de un pol铆gono
// Define una interfaz para representar coordenadas de latitud y longitud
interface LatLng {
lat: number;
lng: number;
}
// Define una interfaz para representar un pol铆gono con su ID y sus v茅rtices
interface Poligono {
id: number;
vertices: LatLng[];
}
// Define las propiedades del componente MapEvents
interface MapEventsProps {
onMapClick: (latlng: LatLng) => void; // Funci贸n que se llama al hacer clic en el mapa
onMapRightClick: (e: any) => void; // Funci贸n que se llama al hacer clic derecho en el mapa
}
// Componente funcional MapEvents que utiliza el hook useMapEvents para manejar eventos del mapa
const MapEvents: React.FC<MapEventsProps> = ({ onMapClick, onMapRightClick }) => {
useMapEvents({
click: (e) => onMapClick(e.latlng), // Asocia el evento 'click' del mapa a la funci贸n onMapClick, pasando las coordenadas del clic
contextmenu: (e) => { // Asocia el evento 'contextmenu' (clic derecho) del mapa
e.originalEvent.preventDefault(); // Previene el men煤 contextual predeterminado del navegador
onMapRightClick(e); // Llama a la funci贸n onMapRightClick, pasando el evento
}
});
return null; // Este componente no renderiza nada visualmente, solo maneja eventos del mapa
};
// Define un icono personalizado para los v茅rtices del pol铆gono
const vertexIcon = new L.Icon({
iconUrl: 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-512.png', // URL de la imagen del icono
iconSize: [20, 20], // Tama帽o del icono en p铆xeles
iconAnchor: [10, 10] // Punto del icono que corresponde a la ubicaci贸n del marcador
});
// Componente funcional ManejoPoligono que gestiona la creaci贸n, selecci贸n, eliminaci贸n y edici贸n de pol铆gonos
const ManejoPoligono: React.FC = () => {
// Estado para almacenar la lista de pol铆gonos creados
const [poligonos, setPoligonos] = useState<Poligono[]>([]);
// Estado para almacenar los puntos temporales que forman un pol铆gono en proceso de creaci贸n
const [puntosTemporales, setPuntosTemporales] = useState<LatLng[]>([]);
// Estado para almacenar el ID del pol铆gono actualmente seleccionado
const [poligonoSeleccionado, setPoligonoSeleccionado] = useState<number | null>(null);
// Estado para controlar la visibilidad del cuadro de confirmaci贸n de eliminaci贸n
const [mostrarConfirmacion, setMostrarConfirmacion] = useState(false);
// Estado para almacenar el ID del pol铆gono que se va a eliminar
const [poligonoAEliminar, setPoligonoAEliminar] = useState<number | null>(null);
// Estado para controlar la visibilidad del componente PuntoMonitoreadoPoligono para un pol铆gono espec铆fico
const [mostrarPunto, setMostrarPunto] = useState<number | null>(null);
// Referencia mutable para almacenar las referencias a los componentes Popup de cada pol铆gono
const popupRefs = useRef<any[]>([]);
// Referencia mutable para el grupo de capas donde se renderizar谩n los pol铆gonos y otros elementos
const layerGroupRef = useRef<L.LayerGroup | null>(null);
// Funci贸n useCallback para agregar un nuevo punto temporal a la lista de puntos temporales
const agregarPuntoTemporal = useCallback((latlng: LatLng) => {
setPuntosTemporales((prev) => [...prev, latlng]);
}, []);
// Funci贸n useCallback para finalizar la creaci贸n de un pol铆gono
const finalizarPoligono = useCallback(() => {
// Verifica si hay al menos 3 puntos temporales para formar un pol铆gono
if (puntosTemporales.length > 2) {
const nuevoPoligono: Poligono = {
id: Date.now(), // Genera un ID 煤nico basado en la marca de tiempo actual
vertices: [...puntosTemporales, puntosTemporales[0]] // Cierra el pol铆gono agregando el primer punto al final
};
// Actualiza el estado de los pol铆gonos agregando el nuevo pol铆gono a la lista
setPoligonos((prev) => [...prev, nuevoPoligono]);
}
// Limpia la lista de puntos temporales para la creaci贸n del siguiente pol铆gono
setPuntosTemporales([]);
// Desselecciona cualquier pol铆gono previamente seleccionado
setPoligonoSeleccionado(null);
}, [puntosTemporales]);
// Funci贸n useCallback para seleccionar un pol铆gono por su ID
const seleccionarPoligono = useCallback((id: number) => {
setPoligonoSeleccionado(id);
}, []);
// Funci贸n useCallback para eliminar el pol铆gono actualmente seleccionado
const eliminarPoligonoSeleccionado = useCallback(() => {
if (poligonoSeleccionado !== null) {
// Filtra la lista de pol铆gonos, manteniendo solo aquellos cuyo ID no coincide con el pol铆gono seleccionado
setPoligonos(prev => prev.filter(p => p.id !== poligonoSeleccionado));
// Desselecciona el pol铆gono
setPoligonoSeleccionado(null);
// Limpia los puntos temporales
setPuntosTemporales([]);
}
}, [poligonoSeleccionado]);
// Funci贸n useCallback para actualizar la posici贸n de un v茅rtice de un pol铆gono
const actualizarVertice = useCallback((poligonoId: number, verticeIndex: number, newLatLng: LatLng) => {
// Actualiza el estado de los pol铆gonos mapeando la lista y modificando el pol铆gono correspondiente
setPoligonos(prev =>
prev.map(poligono => {
if (poligono.id === poligonoId) {
const nuevosVertices = [...poligono.vertices];
nuevosVertices[verticeIndex] = newLatLng;
// Si se mueve el primer v茅rtice, tambi茅n se actualiza el 煤ltimo para mantener el pol铆gono cerrado
if (verticeIndex === 0) {
nuevosVertices[nuevosVertices.length - 1] = newLatLng;
}
return { ...poligono, vertices: nuevosVertices };
}
return poligono;
})
);
}, []);
// Funci贸n useCallback que se llama al hacer clic en el mapa para agregar un punto temporal
const handleMapClick = useCallback((latlng: LatLng) => {
agregarPuntoTemporal(latlng);
}, [agregarPuntoTemporal]);
// Funci贸n useCallback que se llama al hacer clic derecho en el mapa para finalizar el pol铆gono actual
const handleMapRightClick = useCallback(() => {
finalizarPoligono();
}, [finalizarPoligono]);
// Funci贸n para seleccionar un pol铆gono y abrir su Popup
const handleSeleccionarPoligono = (id: number) => {
seleccionarPoligono(id);
// Encuentra el 铆ndice del pol铆gono seleccionado en la lista
const index = poligonos.findIndex(p => p.id === id);
// Obtiene la referencia al Popup del pol铆gono
const ref = popupRefs.current[index];
// Si la referencia y el mapa asociado al Popup existen, abre el Popup
if (ref && ref._map) {
ref.openOn(ref._map);
}
};
// Funci贸n para convertir un array de v茅rtices (LatLng) a un objeto turf.js Polygon
const convertirAPoligonoTurf = (vertices: LatLng[]): turf.helpers.Feature<turf.helpers.Polygon> => {
// Mapea los v茅rtices para convertirlos al formato [longitude, latitude] requerido por turf.js
const coords = vertices.map(vertice => [vertice.lng, vertice.lat]);
// Crea un objeto turf.js Polygon con las coordenadas
return turf.polygon([coords]);
};
return (
<>
{/* LayerGroup para contener todos los pol铆gonos y otros elementos relacionados */}
<LayerGroup ref={layerGroupRef}>
{/* Mapea la lista de pol铆gonos para renderizar cada uno */}
{poligonos.map((poligono, index) => {
// Determina si el pol铆gono actual est谩 seleccionado
const esSeleccionado = poligonoSeleccionado === poligono.id;
return (
<React.Fragment key={`poligono-${poligono.id}`}>
{/* Renderiza el componente Polygon de react-leaflet */}
<Polygon
positions={poligono.vertices} // Pasa los v茅rtices del pol铆gono
color={esSeleccionado ? "red" : "blue"} // Cambia el color del borde si est谩 seleccionado
fillOpacity={0.3} // Define la opacidad del relleno
eventHandlers={{
click: () => handleSeleccionarPoligono(poligono.id) // Asocia un evento de clic para seleccionar el pol铆gono
}}
>
{/* Renderiza un Popup asociado al pol铆gono */}
<Popup ref={(ref) => popupRefs.current[index] = ref}>
<div style={{ padding: '10px' }}>
<h4 style={{ marginTop: 0 }}>Pol铆gono {index + 1}</h4>
{/* Bot贸n para eliminar el pol铆gono */}
<button
onClick={(e) => {
e.stopPropagation(); // Evita que el clic en el bot贸n se propague al pol铆gono subyacente
setPoligonoAEliminar(poligono.id); // Guarda el ID del pol铆gono a eliminar
setMostrarConfirmacion(true); // Muestra el cuadro de confirmaci贸n
}}
style={{
padding: '8px 16px',
backgroundColor: '#ff4444',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginBottom: '10px'
}}
>
Eliminar Pol铆gono
</button>
{/* Bot贸n para agregar un punto de monitoreo al pol铆gono */}
<button
onClick={() => setMostrarPunto(index)}
style={{
padding: '8px 16px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginRight: '10px',
marginBottom: '10px'
}}
>
Agregar Punto Monitoreo
</button>
</div>
</Popup>
</Polygon>
{/* Renderiza el componente PuntoMonitoreadoPoligono si mostrarPunto coincide con el 铆ndice del pol铆gono */}
{mostrarPunto === index && (
<PuntoMonitoreadoPoligono
layerGroupRef={layerGroupRef}
buffer={convertirAPoligonoTurf(poligono.vertices)} // Pasa el pol铆gono convertido a formato turf.js como buffer
/>
)}
{/* Si el pol铆gono est谩 seleccionado, renderiza marcadores en cada v茅rtice para permitir la edici贸n */}
{esSeleccionado && poligono.vertices.map((vertice, verticeIndex) => (
<Marker
key={`vertice-${poligono.id}-${verticeIndex}`}
position={[vertice.lat, vertice.lng]}
icon={vertexIcon} // Usa el icono personalizado para los v茅rtices
draggable // Permite que el marcador sea arrastrable
eventHandlers={{
dragend: (e) => {
// Llama a la funci贸n para actualizar la posici贸n del v茅rtice al finalizar el arrastre
actualizarVertice(poligono.id, verticeIndex, e.target.getLatLng());
}
}}
/>
))}
</React.Fragment>
);
})}
</LayerGroup>
{/* Renderiza una Polyline para mostrar la l铆nea temporal mientras se dibuja un nuevo pol铆gono */}
{puntosTemporales.length > 1 && (
<Polyline
positions={puntosTemporales}
color="red"
dashArray="4" // Estilo de l铆nea punteada
/>
)}
{/* Componente MapEvents para manejar los clics en el mapa */}
<MapEvents
onMapClick={handleMapClick}
onMapRightClick={handleMapRightClick}
/>
{/* Renderiza un cuadro de confirmaci贸n para la eliminaci贸n del pol铆gono */}
{mostrarConfirmacion && (
<div style={{
position: 'fixed',
top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex', justifyContent: 'center', alignItems: 'center',
zIndex: 1000
}}>
<div style={{
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
width: '300px',
boxShadow: '0 2px 10px rgba(0,0,0,0.3)'
}}>
<h3>¿Est谩s seguro?</h3>
<p>¿Deseas eliminar el pol铆gono seleccionado?</p>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '20px' }}>
{/* Bot贸n para cancelar la eliminaci贸n */}
<button
onClick={() => {
setMostrarConfirmacion(false);
setPoligonoAEliminar(null);
}}
style={{ marginRight: '10px', padding: '6px 12px' }}
>
Cancelar
</button>
{/* Bot贸n para confirmar y eliminar el pol铆gono */}
<button
onClick={() => {
seleccionarPoligono(poligonoAEliminar!);
eliminarPoligonoSeleccionado();
setMostrarConfirmacion(false);
setPoligonoAEliminar(null);
}}
style={{
backgroundColor: '#ff4444',
color: 'white',
padding: '6px 12px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Eliminar
</button>
</div>
</div>
</div>
)}
</>
);
};
export default ManejoPoligono;
siguiente componente
import React, { useState, useEffect, useRef } from "react"; // Importa React y los hooks useState, useEffect y useRef
import { useMapEvents } from "react-leaflet"; // Hook para acceder a los eventos del mapa de Leaflet
import L from "leaflet"; // Importa la biblioteca principal de Leaflet
import icon from "./icon"; // Importa el icono personalizado para los marcadores (asumiendo que existe un archivo llamado "icon" en la misma carpeta)
import * as turf from "@turf/turf"; // Importa la biblioteca turf.js para an谩lisis espacial
// Define la interfaz para las coordenadas de latitud y longitud
interface Coordenadas {
lat: number;
lng: number;
}
// Define la interfaz para las propiedades del componente PuntoMonitoreadoProps
interface PuntoMonitoreadoProps {
layerGroupRef: React.RefObject<L.LayerGroup>; // Referencia mutable a un grupo de capas de Leaflet
buffer: turf.helpers.Feature<turf.helpers.Polygon | turf.helpers.MultiPolygon>; // Objeto GeoJSON que representa un buffer (pol铆gono o multipol铆gono)
}
/**
* Funci贸n para corregir la geometr铆a de un buffer, asegurando que el primer y 煤ltimo punto de cada anillo sean iguales
* para formar pol铆gonos cerrados v谩lidos.
* @param buffer Objeto GeoJSON del buffer a corregir.
* @returns Objeto GeoJSON del buffer corregido o null si la geometr铆a no es v谩lida.
*/
function corregirGeometria(
buffer: turf.helpers.Feature<turf.helpers.Polygon | turf.helpers.MultiPolygon>
): turf.helpers.Feature<turf.helpers.Polygon | turf.helpers.MultiPolygon> | null {
// Verifica si el buffer tiene geometr铆a y coordenadas definidas
if (!buffer.geometry || !buffer.geometry.coordinates) {
console.error("Buffer no tiene geometr铆a o coordenadas definidas.");
return null;
}
// Obtiene el tipo de geometr铆a del buffer
const tipo = buffer.geometry.type;
// Procesa si la geometr铆a es un Pol铆gono
if (tipo === "Polygon") {
// Itera sobre cada anillo (exterior e interior) del pol铆gono
const coords = buffer.geometry.coordinates.map(anillo => {
// Obtiene el primer y 煤ltimo punto del anillo
const primero = anillo[0];
const ultimo = anillo[anillo.length - 1];
// Verifica si el primer y 煤ltimo punto son diferentes
if (primero[0] !== ultimo[0] || primero[1] !== ultimo[1]) {
// Si son diferentes, agrega el primer punto al final para cerrar el pol铆gono
return [...anillo, primero];
}
// Si ya est谩n cerrados, devuelve el anillo sin cambios
return anillo;
});
// Crea un nuevo objeto GeoJSON de tipo Pol铆gono con las coordenadas corregidas y las propiedades originales
return turf.polygon(coords, buffer.properties || {});
}
// Procesa si la geometr铆a es un MultiPol铆gono
if (tipo === "MultiPolygon") {
// Itera sobre cada pol铆gono del MultiPol铆gono
const coords = buffer.geometry.coordinates.map(poligono =>
// Itera sobre cada anillo de cada pol铆gono
poligono.map(anillo => {
// Obtiene el primer y 煤ltimo punto del anillo
const primero = anillo[0];
const ultimo = anillo[anillo.length - 1];
// Verifica si el primer y 煤ltimo punto son diferentes
if (primero[0] !== ultimo[0] || primero[1] !== ultimo[1]) {
// Si son diferentes, agrega el primer punto al final para cerrar el pol铆gono
return [...anillo, primero];
}
// Si ya est谩n cerrados, devuelve el anillo sin cambios
return anillo;
})
);
// Crea un nuevo objeto GeoJSON de tipo MultiPol铆gono con las coordenadas corregidas y las propiedades originales
return turf.multiPolygon(coords, buffer.properties || {});
}
// Si el tipo de geometr铆a no es Pol铆gono ni MultiPol铆gono, muestra un error
console.error("Tipo de geometr铆a no soportado.");
return null;
}
// Componente funcional de React para mostrar y gestionar un punto de monitoreo dentro de un pol铆gono
const PuntoMonitoreadoPoligono: React.FC<PuntoMonitoreadoProps> = ({ layerGroupRef, buffer }) => {
// Estado para almacenar las coordenadas del punto de monitoreo
const [coordenadas, setCoordenadas] = useState<Coordenadas | null>(null);
// Referencia mutable para el marcador de Leaflet
const markerRef = useRef<L.Marker | null>(null);
/**
* Funci贸n para crear un objeto GeoJSON de tipo Pol铆gono a partir de un array de objetos LatLng.
* Asegura que el pol铆gono tenga al menos 3 puntos y est茅 cerrado.
* @param puntos Array de objetos con propiedades lat y lng.
* @returns Objeto GeoJSON de tipo Pol铆gono o null si no se puede crear.
*/
const crearPoligonoDesdeLatLng = (puntos: { lat: number, lng: number }[]): turf.helpers.Feature<turf.helpers.Polygon> | null => {
// Verifica si el array de puntos es v谩lido y tiene al menos 3 puntos
if (!Array.isArray(puntos) || puntos.length < 3) {
console.error("Se requieren al menos 3 puntos para formar un pol铆gono.");
return null;
}
// Convierte las coordenadas al formato [longitude, latitude] que espera Turf.js
const coords: [number, number][] = puntos.map(p => [p.lng, p.lat]);
// Asegura que el pol铆gono est茅 cerrado (el primer punto debe ser igual al 煤ltimo)
const primero = coords[0];
const ultimo = coords[coords.length - 1];
if (primero[0] !== ultimo[0] || primero[1] !== ultimo[1]) {
coords.push(primero); // Agrega el primer punto al final para cerrar el anillo
}
// Crea el pol铆gono como un objeto Feature de Turf.js
return turf.polygon([coords]);
}
// Hook para manejar eventos del mapa (doble clic y clic derecho)
useMapEvents({
// Evento de doble clic en el mapa
dblclick: (e) => {
// Establece las coordenadas del clic como las coordenadas del punto de monitoreo
setCoordenadas(e.latlng);
},
// Evento de clic con el bot贸n derecho del rat贸n (men煤 contextual)
contextmenu: (e) => {
// Evita el comportamiento predeterminado del men煤 contextual del navegador
e.originalEvent.preventDefault();
// Establece las coordenadas del clic como las coordenadas del punto de monitoreo
setCoordenadas(e.latlng);
}
});
// Hook useEffect que se ejecuta cuando cambian las coordenadas, la referencia al grupo de capas o el buffer
useEffect(() => {
// Verifica si hay coordenadas, una referencia v谩lida al grupo de capas y un buffer
if (coordenadas && layerGroupRef.current && buffer) {
// Si ya existe un marcador, lo elimina del grupo de capas
if (markerRef.current) {
layerGroupRef.current.removeLayer(markerRef.current);
}
// Crea un nuevo marcador de Leaflet en las coordenadas actuales
const marker = L.marker([coordenadas.lat, coordenadas.lng], {
icon: icon, // Asigna el icono personalizado al marcador
draggable: true // Permite que el marcador sea arrastrable
});
// Agrega el marcador al grupo de capas
layerGroupRef.current.addLayer(marker);
// Actualiza la referencia al marcador
markerRef.current = marker;
// Crea un objeto turf.js de tipo Punto con las coordenadas
const punto = turf.point([coordenadas.lng, coordenadas.lat]);
// Corrige la geometr铆a del buffer utilizando la funci贸n corregirGeometria
const bufferCorregido = corregirGeometria(buffer);
// Variable para almacenar el mensaje de estado (dentro o fuera del buffer)
let mensajeEstado = "";
// Verifica si el buffer corregido es v谩lido
if (bufferCorregido) {
// Utiliza turf.booleanPointInPolygon para verificar si el punto est谩 dentro del buffer
const dentro = turf.booleanPointInPolygon(punto, bufferCorregido);
// Establece el mensaje de estado basado en si el punto est谩 dentro o fuera
mensajeEstado = dentro
? "✅ El punto est谩 <b>dentro</b> del buffer."
: "❌ El punto est谩 <b>fuera</b> del buffer.";
// Imprime el estado en la consola (sin etiquetas HTML)
console.log(mensajeEstado.replace(/<[^>]*>/g, ""));
} else {
// Si el buffer no es v谩lido, establece un mensaje de error
mensajeEstado = "⚠️ Buffer no v谩lido.";
console.error(mensajeEstado);
}
// Define el contenido del popup que se mostrar谩 al hacer clic en el marcador
const popupContent = `
<div>
<p><b>Ubicaci贸n:</b> ${coordenadas.lat.toFixed(5)}, ${coordenadas.lng.toFixed(5)}</p>
<p>${mensajeEstado}</p>
<button id="eliminar-marcador" style="padding: 5px 10px; background-color: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer;">Eliminar</button>
</div>
`;
// Asocia el popup al marcador y lo abre inicialmente
marker.bindPopup(popupContent).openPopup();
// Agrega un evento 'dragend' al marcador para actualizar las coordenadas cuando se arrastra
marker.on("dragend", (e) => {
const position = e.target.getLatLng();
setCoordenadas({ lat: position.lat, lng: position.lng });
});
// Agrega un evento 'popupopen' al marcador para manejar el bot贸n de eliminar dentro del popup
marker.on("popupopen", () => {
// Obtiene la referencia al bot贸n de eliminar por su ID
const btn = document.getElementById("eliminar-marcador");
// Si el bot贸n existe
if (btn) {
// Define la funci贸n que se ejecutar谩 al hacer clic en el bot贸n de eliminar
btn.onclick = () => {
// Verifica si la referencia al marcador actual es v谩lida
if (markerRef.current) {
// Elimina el marcador del grupo de capas
layerGroupRef.current?.removeLayer(markerRef.current);
// Limpia la referencia al marcador
markerRef.current = null;
// Limpia el estado de las coordenadas
setCoordenadas(null);
}
};
}
});
}
// El useEffect se ejecutar谩 cada vez que cambien las coordenadas, la referencia al grupo de capas o el buffer
}, [coordenadas, layerGroupRef, buffer]);
// El componente no renderiza nada visualmente por s铆 mismo, ya que manipula directamente las capas del mapa
return null;
};
// Exporta el componente PuntoMonitoreadoPoligono para su uso en otras partes de la aplicaci贸n
export default PuntoMonitoreadoPoligono;
siguiente componente
import React, { useState, useCallback } from "react"; // Importa React y los hooks useState y useCallback
import { useMapEvents, Marker, Popup } from "react-leaflet"; // Importa hooks y componentes espec铆ficos de react-leaflet
import icon from "./icon"; // Importa el icono personalizado para los marcadores (asumiendo que existe un archivo llamado "icon" en la misma carpeta)
// Componente funcional MapEvents que utiliza el hook useMapEvents para manejar eventos del mapa
const MapEvents = ({ onMapClick }) => {
// Utiliza el hook useMapEvents para adjuntar listeners a los eventos del mapa
useMapEvents({
click: (e) => { // Define una funci贸n que se ejecuta cuando se hace clic en el mapa
onMapClick(e.latlng); // Llama a la funci贸n onMapClick proporcionada por las props, pasando las coordenadas del clic
}
});
return null; // Este componente no renderiza nada visualmente, solo maneja eventos del mapa
};
// Componente funcional ManejoMarcadores que gestiona la creaci贸n, visualizaci贸n, edici贸n y eliminaci贸n de marcadores en el mapa
const ManejoMarcadores = () => {
// Estado para almacenar la lista de datos geogr谩ficos (cada elemento representa un marcador)
const [datosGeograficos, setDatosGeograficos] = useState([]);
// Funci贸n useCallback para agregar un nuevo marcador a la lista de datos geogr谩ficos
const agregarMarcador = useCallback((latlng) => {
setDatosGeograficos((prev) => [ // Actualiza el estado de datosGeograficos
...prev, // Mantiene los marcadores existentes
{ // Crea un nuevo objeto para el marcador
id: Date.now(), // Genera un ID 煤nico basado en la marca de tiempo actual
lat: parseFloat(latlng.lat.toFixed(6)), // Extrae y formatea la latitud a 6 decimales
lng: parseFloat(latlng.lng.toFixed(6)) // Extrae y formatea la longitud a 6 decimales
}
]);
}, []); // La funci贸n agregarMarcador solo depende de s铆 misma, por lo que el array de dependencias est谩 vac铆o
// Funci贸n useCallback para eliminar un marcador de la lista de datos geogr谩ficos por su ID
const eliminarMarcador = useCallback((id) => {
setDatosGeograficos(prev => prev.filter(marker => marker.id !== id)); // Filtra la lista, manteniendo solo los marcadores cuyo ID no coincide con el ID proporcionado
}, []); // La funci贸n eliminarMarcador solo depende de s铆 misma
// Funci贸n useCallback para actualizar la posici贸n de un marcador existente en la lista de datos geogr谩ficos
const actualizarPosicionMarcador = useCallback((id, latlng) => {
setDatosGeograficos(prev => // Actualiza el estado de datosGeograficos
prev.map(marker => // Mapea la lista de marcadores
marker.id === id // Verifica si el ID del marcador actual coincide con el ID proporcionado
? { // Si coincide, crea un nuevo objeto de marcador con la posici贸n actualizada
...marker,
lat: parseFloat(latlng.lat.toFixed(6)), // Actualiza la latitud
lng: parseFloat(latlng.lng.toFixed(6)) // Actualiza la longitud
}
: marker // Si no coincide, mantiene el marcador existente sin cambios
)
);
}, []); // La funci贸n actualizarPosicionMarcador solo depende de s铆 misma
// Funci贸n useCallback que se llama cuando se hace clic en el mapa (a trav茅s del componente MapEvents)
const handleMapClick = useCallback((latlng) => {
agregarMarcador(latlng); // Llama a la funci贸n para agregar un nuevo marcador en las coordenadas del clic
console.log("Nuevo marcador agregado:", latlng); // Imprime un mensaje en la consola con las coordenadas del nuevo marcador
}, [agregarMarcador]); // La funci贸n handleMapClick depende de la funci贸n agregarMarcador
// Funci贸n useCallback que se llama cuando se finaliza el arrastre de un marcador
const handleDragEnd = useCallback((markerId) => (e) => {
actualizarPosicionMarcador(markerId, e.target.getLatLng()); // Llama a la funci贸n para actualizar la posici贸n del marcador con el ID proporcionado a las nuevas coordenadas
}, [actualizarPosicionMarcador]); // La funci贸n handleDragEnd depende de la funci贸n actualizarPosicionMarcador
// Funci贸n useCallback que se llama cuando se hace clic en el bot贸n de eliminar dentro del Popup de un marcador
const handleDeleteMarker = useCallback((markerId, e) => {
e.stopPropagation(); // Evita que el evento de clic se propague al marcador subyacente
eliminarMarcador(markerId); // Llama a la funci贸n para eliminar el marcador con el ID proporcionado
}, [eliminarMarcador]); // La funci贸n handleDeleteMarker depende de la funci贸n eliminarMarcador
return (
<>
{/* Mapea la lista de datosGeograficos para renderizar un componente Marker por cada elemento */}
{datosGeograficos.map((marker) => (
<Marker
key={marker.id} // Propiedad key requerida por React para la renderizaci贸n eficiente de listas
position={[marker.lat, marker.lng]} // Define la posici贸n del marcador utilizando la latitud y longitud del dato geogr谩fico
icon={icon} // Asigna el icono personalizado al marcador
draggable // Permite que el marcador sea arrastrable por el usuario
eventHandlers={{
dragend: handleDragEnd(marker.id) // Asocia la funci贸n handleDragEnd al evento 'dragend' (cuando se suelta el marcador despu茅s de arrastrar), pasando el ID del marcador
}}
>
{/* Renderiza un componente Popup asociado al marcador (se muestra al hacer clic en el marcador) */}
<Popup>
<div>
<p>Latitud: {marker.lat.toFixed(13)}</p> {/* Muestra la latitud con 13 decimales */}
<p>Longitud: {marker.lng.toFixed(13)}</p> {/* Muestra la longitud con 13 decimales */}
{/* Bot贸n para eliminar el marcador */}
<button onClick={(e) => handleDeleteMarker(marker.id, e)}>
Eliminar marcador
</button>
</div>
</Popup>
</Marker>
))}
{/* Renderiza el componente MapEvents para habilitar la captura de clics en el mapa */}
<MapEvents onMapClick={handleMapClick} />
</>
);
};
export default ManejoMarcadores;
siguiente componente
import React, { useRef, useState, createContext, useContext, useCallback } from "react";
import { TileLayer, useMapEvents, Marker, Popup, Circle, LayerGroup } from "react-leaflet";
import L from "leaflet";
import icon from "./icon";
import iconAgarradera from "./iconagarradera";
import "leaflet-geometryutil";
import PuntoMonitoreadoCirculo from "./PuntoMonitoreadoCirculo";
// 馃幆 Contexto para manejar los datos geogr谩ficos relacionados con los c铆rculos
const DatosGeograficosContext = createContext();
// Hook personalizado para acceder f谩cilmente al contexto de datos geogr谩ficos
export const useDatosGeograficos = () => {
const context = useContext(DatosGeograficosContext);
if (!context) {
throw new Error("useDatosGeograficos debe ser usado dentro de un DatosGeograficosProvider");
}
return context;
};
// Funci贸n utilitaria para redondear n煤meros a una precisi贸n espec铆fica
const roundToPrecision = (value, decimals = 3) => {
return parseFloat(value.toFixed(decimals));
};
// 馃摝 Proveedor del contexto de datos geogr谩ficos
const DatosGeograficosProvider = ({ children }) => {
// Estado para almacenar la lista de datos geogr谩ficos (cada elemento representa un c铆rculo)
const [datosGeograficos, setDatosGeograficos] = useState([]);
// Funci贸n useCallback para agregar un nuevo marcador (que representa el centro de un c铆rculo)
const agregarMarcador = useCallback((latlng) => {
setDatosGeograficos((prev) => [
...prev,
{
id: Date.now(), // Genera un ID 煤nico para el marcador
lat: roundToPrecision(latlng.lat, 6), // Latitud del centro del c铆rculo, redondeada a 6 decimales
lng: roundToPrecision(latlng.lng, 6), // Longitud del centro del c铆rculo, redondeada a 6 decimales
radio: 500 // Radio inicial del c铆rculo en metros
}
]);
}, []); // La funci贸n agregarMarcador solo depende de s铆 misma
// Funci贸n useCallback para eliminar un marcador (y su c铆rculo asociado) por su ID
const eliminarMarcador = useCallback((id) => {
setDatosGeograficos(prev => prev.filter(marker => marker.id !== id)); // Filtra la lista, manteniendo solo los marcadores cuyo ID no coincide con el ID proporcionado
}, []); // La funci贸n eliminarMarcador solo depende de s铆 misma
// Funci贸n useCallback para actualizar la posici贸n del centro de un c铆rculo
const actualizarPosicionMarcador = useCallback((id, latlng) => {
setDatosGeograficos(prev =>
prev.map(marker =>
marker.id === id ? { // Si el ID del marcador coincide
...marker,
lat: roundToPrecision(latlng.lat, 6), // Actualiza la latitud del centro
lng: roundToPrecision(latlng.lng, 6) // Actualiza la longitud del centro
} : marker // Si no coincide, mantiene el marcador sin cambios
)
);
}, []); // La funci贸n actualizarPosicionMarcador solo depende de s铆 misma
// Funci贸n useCallback para actualizar el radio de un c铆rculo
const actualizarRadioMarcador = useCallback((id, radio) => {
setDatosGeograficos(prev =>
prev.map(marker =>
marker.id === id ? { // Si el ID del marcador coincide
...marker,
radio: roundToPrecision(radio) // Actualiza el radio del c铆rculo
} : marker // Si no coincide, mantiene el marcador sin cambios
)
);
}, []); // La funci贸n actualizarRadioMarcador solo depende de s铆 misma
// Objeto que contiene los valores y las funciones que se proporcionar谩n al contexto
const value = {
datosGeograficos,
agregarMarcador,
eliminarMarcador,
actualizarPosicionMarcador,
actualizarRadioMarcador
};
// Proporciona el contexto a los componentes hijos
return (
<DatosGeograficosContext.Provider value={value}>
{children}
</DatosGeograficosContext.Provider>
);
};
// Componente funcional MapEvents para capturar clics en el mapa y llamar a la funci贸n onMapClick
const MapEvents = ({ onMapClick }) => {
useMapEvents({
click: (e) => {
onMapClick(e.latlng); // Llama a la funci贸n onMapClick proporcionada por las props con las coordenadas del clic
}
});
return null; // Este componente no renderiza nada visualmente
};
// 馃幆 Funci贸n utilitaria para calcular un punto en el borde de un c铆rculo dado su centro y radio
const calcularPuntoBorde = (lat, lng, radio) => {
if (!lat || !lng || !radio) return L.latLng(0, 0); // Devuelve un LatLng(0, 0) si faltan datos
const centro = L.latLng(lat, lng); // Crea un objeto LatLng para el centro del c铆rculo
// Utiliza la funci贸n 'destination' de leaflet-geometryutil para calcular un punto a una distancia y direcci贸n dadas
return L.GeometryUtil.destination(centro, 90, radio); // 90 grados corresponden al Este
};
// Componente funcional ManejoCircunferencia que gestiona la visualizaci贸n y la interacci贸n con los c铆rculos
const ManejoCircunferencia = () => {
// Accede a las funciones y al estado del contexto de datos geogr谩ficos
const {
datosGeograficos,
agregarMarcador,
eliminarMarcador,
actualizarPosicionMarcador,
actualizarRadioMarcador
} = useDatosGeograficos();
// Estado para controlar la visibilidad del componente PuntoMonitoreadoCirculo para un c铆rculo espec铆fico
const [mostrarPunto, setMostrarPunto] = useState<number | null>(null);
// Referencia mutable para el grupo de capas donde se renderizar谩n los c铆rculos y sus elementos
const layerGroupRef = useRef<L.LayerGroup | null>(null);
// Funci贸n useCallback para agregar un nuevo marcador (centro del c铆rculo) al hacer clic en el mapa
const handleMapClick = useCallback((latlng) => {
agregarMarcador(latlng);
console.log("Nuevo marcador agregado:", latlng);
}, [agregarMarcador]);
// Funci贸n useCallback para actualizar la posici贸n del centro del c铆rculo al finalizar el arrastre del marcador central
const handleDragEnd = useCallback((markerId) => (e) => {
actualizarPosicionMarcador(markerId, e.target.getLatLng());
}, [actualizarPosicionMarcador]);
// Funci贸n useCallback para eliminar un c铆rculo al hacer clic en el bot贸n de eliminar en su popup
const handleDeleteMarker = useCallback((markerId, e) => {
e.stopPropagation(); // Evita que el evento de clic se propague al marcador subyacente
eliminarMarcador(markerId);
}, [eliminarMarcador]);
// Funci贸n useCallback para actualizar el radio del c铆rculo al cambiar el valor del input en el popup
const handleRadioChange = useCallback((markerId) => (e) => {
const nuevoRadio = parseFloat(e.target.value);
if (!isNaN(nuevoRadio) && nuevoRadio > 0) {
actualizarRadioMarcador(markerId, nuevoRadio);
}
}, [actualizarRadioMarcador]);
// Funci贸n useCallback para actualizar el radio del c铆rculo al finalizar el arrastre del marcador de agarre
const handleDragRadioEnd = useCallback((marker) => (e) => {
const centro = L.latLng(marker.lat, marker.lng); // Obtiene las coordenadas del centro del c铆rculo
const borde = e.target.getLatLng(); // Obtiene las coordenadas del punto de agarre arrastrado
const nuevoRadio = centro.distanceTo(borde); // Calcula la distancia entre el centro y el punto de agarre (nuevo radio)
console.log(`Radio calculado: ${nuevoRadio}`);
actualizarRadioMarcador(marker.id, nuevoRadio);
// Verificaci贸n de consistencia (opcional, para depuraci贸n)
const puntoBordeCalculado = calcularPuntoBorde(marker.lat, marker.lng, nuevoRadio);
const distanciaVerificacion = centro.distanceTo(puntoBordeCalculado);
console.log(`Verificaci贸n - Radio: ${nuevoRadio}, Distancia: ${distanciaVerificacion}`);
}, [actualizarRadioMarcador]);
return (
<>
<LayerGroup ref={layerGroupRef}>
{datosGeograficos.map((marker, index) => {
// Calcula un punto en el borde del c铆rculo para colocar el marcador de agarre
const puntoBorde = calcularPuntoBorde(marker.lat, marker.lng, marker.radio);
const centro = L.latLng(marker.lat, marker.lng);
const distanciaVerificacion = centro.distanceTo(puntoBorde);
console.log(`Marcador ${marker.id} - Radio: ${marker.radio}, Distancia verificada: ${distanciaVerificacion}`);
return (
<React.Fragment key={marker.id}>
{/* Marcador central (para mover todo el c铆rculo) */}
<Marker
position={[marker.lat, marker.lng]}
icon={icon}
draggable
eventHandlers={{
dragend: handleDragEnd(marker.id)
}}
>
<Popup>
<div>
<p>Latitud: {marker.lat.toFixed(6)}</p>
<p>Longitud: {marker.lng.toFixed(6)}</p>
<div>
<label>
Radio (m):
<input
type="number"
min="1"
value={marker.radio}
onChange={handleRadioChange(marker.id)}
style={{ width: '70px', marginLeft: '5px' }}
/>
</label>
</div>
<p>Distancia verificada: {distanciaVerificacion.toFixed(3)} m</p>
<button onClick={(e) => handleDeleteMarker(marker.id, e)}>
Eliminar marcador
</button>
<button
onClick={() => setMostrarPunto(index)}
style={{
padding: '8px 16px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginTop: '10px'
}}
>
Agregar Punto Monitoreo
</button>
</div>
</Popup>
</Marker>
{/* C铆rculo principal */}
<Circle
center={[marker.lat, marker.lng]}
radius={marker.radio}
color="blue"
fillColor="blue"
fillOpacity={0.2}
/>
{/* C铆rculo ligeramente m谩s grande (efecto visual) */}
<Circle
center={[marker.lat, marker.lng]}
radius={marker.radio + 3}
color="#85c1e9"
fillColor="#85c1e9"
fillOpacity={0.2}
/>
{/* Marcador de agarre para cambiar el radio */}
<Marker
position={[puntoBorde.lat, puntoBorde.lng]}
draggable
icon={iconAgarradera}
eventHandlers={{
dragend: handleDragRadioEnd(marker)
}}
/>
{/* Componente para manejar el punto de monitoreo dentro del c铆rculo */}
{mostrarPunto === index && (
<PuntoMonitoreadoCirculo
layerGroupRef={layerGroupRef}
buffer={{
center: [marker.lat, marker.lng],
radius: marker.radio,
borde : [puntoBorde.lat, puntoBorde.lng]
}}
/>
)}
</React.Fragment>
);
})}
</LayerGroup>
<MapEvents onMapClick={handleMapClick} />
</>
);
};
// 馃摝 Componente proveedor que envuelve la funcionalidad de manejo de c铆rculos con el proveedor de datos
export const CirculoProvider = ({ children }) => (
<DatosGeograficosProvider>
{children}
<ManejoCircunferencia />
</DatosGeograficosProvider>
);
export default CirculoProvider;
siguiente codigo
import React, { useState, useEffect, useRef } from "react"; // Importa React y los hooks useState, useEffect y useRef
import { useMapEvents } from "react-leaflet"; // Hook para acceder a los eventos del mapa de Leaflet
import L from "leaflet"; // Importa la biblioteca principal de Leaflet
import icon from "./icon"; // Importa el icono personalizado para los marcadores
import * as turf from "@turf/turf"; // Importa la biblioteca turf.js para an谩lisis espacial
// Define la interfaz para las coordenadas de latitud y longitud
interface Coordenadas {
lat: number;
lng: number;
}
// Define la interfaz para las propiedades del componente PuntoMonitoreadoProps
interface PuntoMonitoreadoProps {
layerGroupRef: React.RefObject<L.LayerGroup>; // Referencia mutable a un grupo de capas de Leaflet
buffer: {
center: [number, number]; // [lng, lat] - Coordenadas del centro del c铆rculo
borde: [number, number]; // [lng, lat] - Coordenadas de un punto en el borde del c铆rculo
radius: number; // Radio del c铆rculo (redundante ya que se puede calcular con centro y borde)
};
onRadiusChange?: (newRadius: number) => void; // Propiedad opcional para pasar una funci贸n que se llama cuando cambia el radio
}
// Componente funcional PuntoMonitoreadoCirculo para gestionar un punto de monitoreo dentro de un c铆rculo
const PuntoMonitoreadoCirculo: React.FC<PuntoMonitoreadoProps> = ({
layerGroupRef,
buffer,
onRadiusChange
}) => {
// Estado para almacenar las coordenadas del punto de monitoreo
const [coordenadas, setCoordenadas] = useState<Coordenadas | null>(null);
// Estado para controlar la visibilidad del input para modificar el radio
const [showRadiusInput, setShowRadiusInput] = useState(false);
// Estado para almacenar el nuevo valor del radio antes de aplicarlo
const [newRadius, setNewRadius] = useState(buffer.radius);
// Referencia mutable para el marcador de Leaflet del punto de monitoreo
const markerRef = useRef<L.Marker | null>(null);
// Hook para manejar eventos del mapa (doble clic y clic derecho) para establecer la ubicaci贸n del punto de monitoreo
useMapEvents({
dblclick: (e) => {
setCoordenadas({ lat: e.latlng.lat, lng: e.latlng.lng });
},
contextmenu: (e) => {
e.originalEvent.preventDefault(); // Evita el men煤 contextual del navegador
setCoordenadas({ lat: e.latlng.lat, lng: e.latlng.lng });
}
});
// useEffect para actualizar el estado del radio cuando cambia el radio proporcionado en las props del buffer
useEffect(() => {
setNewRadius(buffer.radius); // Sincroniza el estado local del radio con el valor del buffer
}, [buffer.radius]);
// useEffect principal para gestionar la creaci贸n, actualizaci贸n y eliminaci贸n del marcador del punto de monitoreo
useEffect(() => {
// Si no hay coordenadas o la referencia al grupo de capas no es v谩lida, no hacer nada
if (!coordenadas || !layerGroupRef.current) return;
// Limpieza: eliminar el marcador anterior y sus listeners si existen
if (markerRef.current) {
markerRef.current.off(); // Elimina todos los listeners del marcador anterior
layerGroupRef.current.removeLayer(markerRef.current); // Elimina el marcador del mapa
markerRef.current = null; // Resetea la referencia al marcador
}
// Crear un nuevo marcador en las coordenadas seleccionadas
const marker = L.marker([coordenadas.lat, coordenadas.lng], {
icon: icon, // Asigna el icono personalizado
draggable: true // Permite que el marcador sea arrastrable
});
markerRef.current = marker; // Guarda la referencia al nuevo marcador
layerGroupRef.current.addLayer(marker); // Agrega el marcador al grupo de capas del mapa
// Turf.js para c谩lculos geom茅tricos
const punto = turf.point([coordenadas.lat, coordenadas.lng]); // Crea un punto turf desde las coordenadas del marcador
const centro = turf.point(buffer.center); // Crea un punto turf desde el centro del c铆rculo
const borde = turf.point(buffer.borde); // Crea un punto turf desde un punto en el borde del c铆rculo
// Calcular distancias usando turf.distance
const distanciaPuntoCentro = turf.distance(centro, punto, { units: 'meters' }); // Distancia entre el punto de monitoreo y el centro del c铆rculo
const distanciaBordeCentro = turf.distance(centro, borde, { units: 'meters' }); // Distancia entre el borde del c铆rculo y su centro (radio real)
console.log("Distancia del punto al centro:", distanciaPuntoCentro.toFixed(2), "m");
console.log("Distancia del borde al centro (radio real):", distanciaBordeCentro.toFixed(2), "m");
console.log("Radio proporcionado:", buffer.radius.toFixed(2), "m");
// Determinar si el punto de monitoreo est谩 dentro del c铆rculo (con una posible tolerancia)
const estaDentro = distanciaPuntoCentro <= distanciaBordeCentro;
// Crear el mensaje de estado para el popup
const mensajeEstado = estaDentro
? "✅ El punto est谩 <b>dentro</b> del c铆rculo."
: "❌ El punto est谩 <b>fuera</b> del c铆rculo.";
// Contenido del popup del marcador
const popupContent = `
<div>
<p><b>Ubicaci贸n:</b> ${coordenadas.lat.toFixed(5)}, ${coordenadas.lng.toFixed(5)}</p>
<p><b>Radio actual:</b> ${buffer.radius.toFixed(2)} m </p>
<p>Este radio tiene 30 m de tolerancia</p>
<p>${mensajeEstado}</p>
<div style="margin-top: 10px;">
<button id="eliminar-marcador" style="padding: 5px 10px; background-color: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer;">
Eliminar
</button>
</div>
</div>
`;
marker.bindPopup(popupContent).openPopup(); // Asocia el popup al marcador y lo abre
// Evento de arrastre del marcador para actualizar las coordenadas
marker.on("dragend", (e) => {
const nuevaPos = e.target.getLatLng();
setCoordenadas({ lat: nuevaPos.lat, lng: nuevaPos.lng });
});
// Eventos del popup (abrir) para manejar los botones dentro
marker.on("popupopen", () => {
const btnModificar = document.getElementById("modificar-radio"); // (Este bot贸n no est谩 en el popup definido)
const btnEliminar = document.getElementById("eliminar-marcador");
// (La l贸gica para el bot贸n "modificar-radio" est谩 comentada impl铆citamente ya que el bot贸n no existe en el popup)
// if (btnModificar) {
// btnModificar.onclick = () => {
// setShowRadiusInput(true);
// marker.closePopup();
// };
// }
// L贸gica para el bot贸n "eliminar-marcador"
if (btnEliminar) {
btnEliminar.onclick = () => {
if (markerRef.current) {
markerRef.current.off(); // Elimina los listeners del marcador
layerGroupRef.current?.removeLayer(markerRef.current); // Elimina el marcador del mapa
markerRef.current = null; // Resetea la referencia
setCoordenadas(null); // Limpia las coordenadas
}
};
}
});
// Funci贸n de limpieza que se ejecuta al desmontar el componente o antes de que el efecto se vuelva a ejecutar
return () => {
if (markerRef.current) {
markerRef.current.off();
layerGroupRef.current?.removeLayer(markerRef.current);
markerRef.current = null;
}
};
}, [coordenadas, buffer, layerGroupRef, showRadiusInput]); // Dependencias del useEffect
// Handler para el cambio en el input del radio
const handleRadiusChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewRadius(parseFloat(e.target.value));
};
// Handler para el env铆o del formulario de modificaci贸n del radio
const handleRadiusSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (onRadiusChange && newRadius > 0) {
onRadiusChange(newRadius); // Llama a la funci贸n proporcionada para notificar el cambio de radio
}
setShowRadiusInput(false); // Cierra el input del radio
};
// Renderiza un input para modificar el radio si showRadiusInput es true
if (showRadiusInput) {
return (
<div className="radius-input-container" style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 1000,
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.2)'
}}>
<h3 style={{ marginTop: 0 }}>Modificar Radio</h3>
<form onSubmit={handleRadiusSubmit}>
<div style={{ marginBottom: '10px' }}>
<label>
Nuevo radio (metros):
<input
type="text"
value={newRadius}
onChange={handleRadiusChange}
style={{ marginLeft: '10px', padding: '5px' }}
/>
</label>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<button
type="submit"
style={{
padding: '5px 15px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Aplicar
</button>
<button
type="button"
onClick={() => setShowRadiusInput(false)}
style={{
padding: '5px 15px',
backgroundColor: '#f44336',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Cancelar
</button>
</div>
</form>
</div>
);
}
// Si showRadiusInput es false, no renderizar nada
return null;
};
export default PuntoMonitoreadoCirculo;
siguiente componente
import L from "leaflet"; // Importa la biblioteca principal de Leaflet
// Crea un icono personalizado de Leaflet
const Icon = L.icon({
iconSize: [30, 41], // [ancho, alto] en p铆xeles del icono
iconAnchor: [15, 41], // [x, y] en p铆xeles que corresponden a la ubicaci贸n del marcador (parte inferior central)
popupAnchor: [2, -40], // [x, y] en p铆xeles donde se abrir谩 la ventana emergente relativa al anchor del icono
iconUrl: "/icons/map_marker.png", // Ruta a la imagen del icono principal
shadowSize: [45, 45], // [ancho, alto] en p铆xeles de la imagen de la sombra
shadowAnchor: [10, 40], // [x, y] en p铆xeles que corresponden a la ubicaci贸n de la sombra relativa a su esquina superior izquierda
shadowUrl: "/icons/marker-shadow.png" // Ruta a la imagen de la sombra del marcador
});
// Exporta el icono personalizado para que pueda ser utilizado en otros componentes
export default Icon;
siguiente componente
import { useState, useEffect } from 'react'; // Importa los hooks useState y useEffect de React
import { useMapEvent, useMap } from 'react-leaflet'; // Importa los hooks useMapEvent y useMap de react-leaflet
const ZoomDisplay = () => {
// Estado para almacenar el nivel de zoom actual del mapa
const [zoom, setZoom] = useState<number | null>(null);
// Estado para almacenar la escala actual del mapa como una cadena de texto
const [scale, setScale] = useState<string | null>(null);
// Hook para acceder a la instancia del mapa de Leaflet
const map = useMap();
// Estilos en l铆nea para el contenedor que mostrar谩 el zoom y la escala
const containerStyle = {
position: 'absolute', // Posicionamiento absoluto para colocarlo sobre el mapa
bottom: '20px', // Margen desde la parte inferior del mapa
left: '50%', // Centrar horizontalmente
transform: 'translateX(-50%)', // Ajustar el centrado basado en el ancho del contenedor
backgroundColor: 'rgba(255, 255, 255, 0.7)', // Fondo blanco semitransparente
padding: '5px 10px', // Espaciado interno
borderRadius: '5px', // Bordes redondeados
border: '2px solid rgba(0, 0, 0, 0.2)', // Borde sutil
zIndex: 1000, // Asegurar que est茅 por encima de otros elementos del mapa
boxShadow: '0 0 10px rgba(0, 0, 0, 0.2)', // Sombra suave
fontFamily: 'Arial, sans-serif', // Fuente
fontSize: '14px', // Tama帽o de la fuente
fontWeight: 'bold', // Texto en negrita
color: '#333', // Color del texto
display: 'flex', // Usar flexbox para la disposici贸n interna
gap: '15px' // Espacio entre los elementos (zoom y escala)
};
// Funci贸n para calcular la escala del mapa basada en el nivel de zoom actual
const calculateScale = (currentZoom: number) => {
const latitude = map.getCenter().lat; // Obtiene la latitud del centro del mapa
const earthCircumference = 40075017; // Circunferencia de la Tierra en metros
const screenResolution = 256; // P铆xeles por tile (t铆picamente 256 para Leaflet)
// F贸rmula para calcular el denominador de la escala
const scaleDenominator = (earthCircumference * Math.cos((latitude * Math.PI) / 180)) /
(screenResolution * Math.pow(2, currentZoom));
return formatScale(scaleDenominator); // Formatea la escala para una mejor visualizaci贸n
};
// Funci贸n para formatear el denominador de la escala a una cadena legible
const formatScale = (rawScale: number): string => {
if (rawScale >= 1000000) { // Si la escala es de un mill贸n o m谩s
return `${(rawScale / 1000000).toFixed(1)}M`; // Muestra en millones (ej: 1.5M)
} else if (rawScale >= 1000) { // Si la escala es de mil o m谩s
return `${(rawScale / 1000).toFixed(0)}K`; // Muestra en miles (ej: 50K)
}
return rawScale.toFixed(0); // Si es menor que mil, muestra el n煤mero entero
};
// useEffect que se ejecuta una vez al montar el componente
useEffect(() => {
const initialZoom = map.getZoom(); // Obtiene el nivel de zoom inicial del mapa
setZoom(initialZoom); // Establece el estado del zoom inicial
setScale(calculateScale(initialZoom)); // Calcula y establece la escala inicial
}, [map]); // Dependencia: solo se ejecuta cuando la instancia del mapa cambia (poco probable)
// Hook para escuchar el evento 'zoomend' del mapa (cuando el zoom ha terminado de cambiar)
useMapEvent('zoomend', (event) => {
const newZoom = event.target.getZoom(); // Obtiene el nuevo nivel de zoom
setZoom(newZoom); // Actualiza el estado del zoom
setScale(calculateScale(newZoom)); // Recalcula y actualiza la escala
});
// Hook para escuchar el evento 'moveend' del mapa (cuando el mapa ha terminado de moverse)
useMapEvent('moveend', () => {
if (zoom !== null) { // Asegurarse de que el zoom tenga un valor
setScale(calculateScale(zoom)); // Recalcula la escala ya que la latitud del centro puede haber cambiado
}
});
return (
<div style={containerStyle}>
{zoom !== null && ( // Mostrar la informaci贸n del zoom solo si el estado zoom no es nulo
<div>
<span style={{ marginRight: '5px' }}>馃攳</span> {/* Icono de lupa */}
Zoom: {zoom.toFixed(1)} {/* Muestra el nivel de zoom con un decimal */}
</div>
)}
{scale !== null && ( // Mostrar la informaci贸n de la escala solo si el estado scale no es nulo
<div>
<span style={{ marginRight: '5px' }}>馃搹</span> {/* Icono de regla */}
Escala: 1:{scale} {/* Muestra la escala en formato 1:X */}
</div>
)}
</div>
);
};
export default ZoomDisplay;