indica las diferentes formas de manejar una coordenada :
creamos el primer componente :
import { memo, useRef, useMemo, useCallback, useEffect, useState } from 'react';
import { MapContainer, TileLayer, LayersControl, LayerGroup } from 'react-leaflet';
import 'leaflet/dist/leaflet.css'; // Importa los estilos CSS de Leaflet.
import ZoomDisplay from './Zoommap'; // Componente para mostrar el nivel de zoom del mapa.
import UbicarCoordenadaDD from './UbucarDD'; // Componente para ubicar el mapa mediante coordenadas decimales (DD).
import UbicarCoordenadadms from './UbicarDMS'; // Componente para ubicar el mapa mediante coordenadas en grados, minutos y segundos (DMS).
import Ubicardm from './UbicarDM'; // Componente para ubicar el mapa mediante coordenadas en grados y minutos decimales (DM).
import UbicarMGRS from './UbicarGRMS'; // Componente para ubicar el mapa mediante el sistema de referencia de cuadrícula militar (MGRS).
import UbicarMetrica from './UbicarMetrica'; // Componente para ubicar el mapa mediante coordenadas métricas.
import CapturaClick from './CapturaClick'; // Componente para capturar clics en el mapa y posiblemente mostrar información.
// 🔹 Estilos y propiedades constantes
const mapStyles = {
position: 'absolute', // Permite que el mapa ocupe todo el contenedor padre.
top: 0,
left: 0,
right: 0,
bottom: 0,
overflow: 'hidden', // Oculta cualquier parte del mapa que se salga de los límites.
touchAction: 'none', // Deshabilita las acciones táctiles predeterminadas del navegador en el mapa.
zIndex: 0, // Asegura que el mapa esté en la capa base de apilamiento.
willChange: 'transform', // Indica al navegador que la propiedad 'transform' va a cambiar (optimización).
backfaceVisibility: 'hidden', // Oculta la parte posterior del elemento durante las transformaciones 3D (optimización).
backgroundColor: '#f0f0f0', // Color de fondo del contenedor del mapa.
};
const tileLayerProps = {
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', // URL de los tiles de OpenStreetMap.
attribution: '© OpenStreetMap contributors', // Atribución requerida por OpenStreetMap.
updateWhenIdle: true, // Actualiza los tiles solo cuando el mapa está quieto.
updateWhenZooming: false, // No actualiza los tiles durante el zoom (mejora la fluidez).
keepBuffer: 4, // Número de tiles a mantener en el buffer alrededor de la vista actual.
noWrap: true, // Evita la repetición horizontal de los tiles.
detectRetina: false, // Deshabilita la detección de pantallas Retina.
bounds: [ // Límites geográficos para la visualización de los tiles.
[-85, -175], // Suroeste.
[85, 175], // Noreste.
],
};
const mapOptions = {
center: [19.285653, -99.147809] as [number, number], // Centro inicial del mapa (Ciudad de México).
zoom: 5, // Nivel de zoom inicial.
preferCanvas: true, // Utiliza Canvas para renderizar los tiles (puede mejorar el rendimiento).
fadeAnimation: false, // Deshabilita la animación de fundido al cargar nuevos tiles.
attributionControl: false, // Oculta el control de atribución de Leaflet.
zoomControl: false, // Oculta los controles de zoom por defecto de Leaflet.
maxBoundsViscosity: 1.0, // Fuerza el mapa a mantenerse dentro de los límites máximos.
maxBounds: [ // Límites máximos del mapa.
[-85, -175], // Suroeste.
[85, 175], // Noreste.
],
worldCopyJump: false, // Deshabilita el salto de copia del mundo al hacer zoom out.
};
// 🔹 Botón Toggle reusable
// Componente funcional memoizado para crear botones que activan/desactivan funcionalidades.
const ToggleButton = memo(({ children, label, top, left, isActive, onClick }) => (
<>
<button
onClick={onClick} // Llama a la función onClick proporcionada al hacer clic.
style={{
position: 'absolute', // Permite posicionar el botón exactamente en las coordenadas.
top, // Posición vertical del botón.
left, // Posición horizontal del botón.
zIndex: 1000, // Asegura que el botón esté por encima del mapa.
backgroundColor: isActive ? '#4CAF50' : '#f44336', // Cambia el color según si el botón está activo.
color: 'white', // Color del texto del botón.
padding: '5px 10px', // Espaciado interno del botón.
border: 'none', // Elimina el borde del botón.
borderRadius: '4px', // Bordes ligeramente redondeados.
cursor: 'pointer', // Cambia el cursor a una mano al pasar por encima.
}}
>
{label} {/* Texto del botón. */}
</button>
{isActive && children} {/* Renderiza los componentes hijos solo si el botón está activo. */}
</>
));
// Componente funcional principal para mostrar el mapa y las funcionalidades de ubicación.
const MapViewCapLoc = () => {
const mapRef = useRef(null); // Referencia para acceder a la instancia del componente MapContainer de Leaflet.
const tileLayerRef = useRef(null); // Referencia para acceder a la instancia del componente TileLayer de Leaflet.
const layerGroupRef = useRef(null); // Referencia para agrupar capas vectoriales (como marcadores de ubicación).
const [activeButton, setActiveButton] = useState<string | null>(null); // Estado para controlar qué botón de funcionalidad está activo.
// Función useCallback para manejar los cambios en la vista del mapa (zoom, pan).
const handleViewportChanged = useCallback(() => {
const map = mapRef.current;
if (map) {
const center = map.getCenter(); // Obtiene las coordenadas del centro del mapa.
const zoom = map.getZoom(); // Obtiene el nivel de zoom actual del mapa.
console.log('Viewport changed:', { center, zoom }); // Registra en la consola los cambios de la vista.
}
}, []); // El array de dependencias vacío significa que esta función no se recreará en cada renderizado.
// Efecto para adjuntar/desadjuntar un listener al evento 'moveend' del mapa.
useEffect(() => {
const map = mapRef.current;
if (map) {
map.on('moveend', handleViewportChanged); // Adjunta el listener al evento 'moveend' (cuando el movimiento del mapa termina).
return () => map.off('moveend', handleViewportChanged); // Función de limpieza para remover el listener al desmontar el componente.
}
}, [handleViewportChanged]); // El efecto se ejecuta cuando la función 'handleViewportChanged' cambia.
// Función useCallback para manejar el cambio de estado de los botones de funcionalidad.
const handleToggleButton = useCallback(
(buttonId: string) => {
setActiveButton(prev => (prev === buttonId ? null : buttonId)); // Si el botón ya está activo, lo desactiva; si no, lo activa.
},
[] // El array de dependencias vacío significa que esta función no se recreará en cada renderizado.
);
return (
<div className="map-container">
{/* Componente principal de react-leaflet que contiene el mapa. */}
<MapContainer {...mapOptions} ref={mapRef} style={mapStyles}>
{/* Componente para mostrar la información del zoom actual. */}
<ZoomDisplay />
{/* Componente para controlar las capas base del mapa. */}
<LayersControl position="topright">
{/* Capa base de OpenStreetMap. */}
<LayersControl.BaseLayer name="OpenStreetMap" checked>
{/* Componente para renderizar los tiles del mapa. */}
<TileLayer ref={tileLayerRef} {...tileLayerProps} />
</LayersControl.BaseLayer>
</LayersControl>
{/* Grupo de capas para elementos vectoriales que se añaden al mapa (ej. marcadores). */}
<LayerGroup ref={layerGroupRef}>
{/* Componente para capturar clics en el mapa y realizar alguna acción. */}
<CapturaClick layerGroupRef={layerGroupRef} />
</LayerGroup>
</MapContainer>
{/* Botones para las diferentes funcionalidades de ubicación. */}
<ToggleButton label="Ubicar DD" top={10} left={490} isActive={activeButton === 'dd'} onClick={() => handleToggleButton('dd')}>
<UbicarCoordenadaDD layerGroupRef={layerGroupRef} mapRef={mapRef} />
</ToggleButton>
<ToggleButton label="Ubicar DMS" top={10} left={670} isActive={activeButton === 'dms'} onClick={() => handleToggleButton('dms')}>
<UbicarCoordenadadms layerGroupRef={layerGroupRef} mapRef={mapRef} />
</ToggleButton>
<ToggleButton label="Ubicar DM" top={50} left={390} isActive={activeButton === 'dm'} onClick={() => handleToggleButton('dm')}>
<Ubicardm layerGroupRef={layerGroupRef} mapRef={mapRef} />
</ToggleButton>
<ToggleButton label="Ubicar MGRS" top={50} left={490} isActive={activeButton === 'mgrs'} onClick={() => handleToggleButton('mgrs')}>
<UbicarMGRS layerGroupRef={layerGroupRef} mapRef={mapRef} />
</ToggleButton>
<ToggleButton label="Ubicar Métricas" top={50} left={670} isActive={activeButton === 'metricas'} onClick={() => handleToggleButton('metricas')}>
<UbicarMetrica layerGroupRef={layerGroupRef} mapRef={mapRef} />
</ToggleButton>
</div>
);
};
// 🔹 Estilos globales aplicados dinámicamente al head del documento.
if (typeof document !== 'undefined') {
const style = document.createElement('style');
style.textContent = `
.map-container {
width: 100vw;
height: 100vh;
position: relative;
background: #f0f0f0;
overflow: hidden;
isolation: isolate; /* Crea un nuevo contexto de apilamiento para evitar problemas con elementos externos. */
}
.error-fallback {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.9);
z-index: 1000;
}
`;
document.head.appendChild(style); // Añade el elemento <style> al <head> del documento.
}
export default memo(MapViewCapLoc); // Envuelve el componente con 'memo' para optimizarlo, evitando re-renderizados innecesarios si las props no cambian.
siguiente componente :
import { useState, useEffect } from 'react';
import { useMapEvent, useMap } from 'react-leaflet';
// Componente funcional de React para mostrar el nivel de zoom y la escala del mapa.
const ZoomDisplay = () => {
// Estado para almacenar el nivel de zoom actual del mapa. Inicializado a null.
const [zoom, setZoom] = useState<number | null>(null);
// Estado para almacenar la escala actual del mapa como una cadena de texto. Inicializado a null.
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', // Para posicionarlo sobre el mapa.
bottom: '20px', // Ubicado en la parte inferior.
left: '50%', // Centrado horizontalmente.
transform: 'translateX(-50%)', // Ajuste para centrado preciso.
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, // Asegura que esté sobre 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', // Para alinear los elementos internos.
gap: '15px' // Espacio entre el icono y el texto.
};
// Función para calcular la escala del mapa basada en el nivel de zoom y la latitud central.
const calculateScale = (currentZoom: number) => {
// Obtiene la latitud del centro actual del mapa.
const latitude = map.getCenter().lat;
// Circunferencia de la Tierra en metros.
const earthCircumference = 40075017;
// Resolución de la pantalla en píxeles por tile (para tiles de 256x256).
const screenResolution = 256;
// Cálculo del denominador de la escala. La escala varía con la latitud debido a la forma de la Tierra.
const scaleDenominator = (earthCircumference * Math.cos((latitude * Math.PI) / 180)) /
(screenResolution * Math.pow(2, currentZoom));
// Formatea el denominador de la escala para una mejor visualización.
return formatScale(scaleDenominator);
};
// Función para formatear el valor de la escala a un formato legible (ej: 1M, 10K).
const formatScale = (rawScale: number): string => {
if (rawScale >= 1000000) {
return `${(rawScale / 1000000).toFixed(1)}M`; // Millones.
} else if (rawScale >= 1000) {
return `${(rawScale / 1000).toFixed(0)}K`; // Miles.
}
return rawScale.toFixed(0); // Valor numérico sin sufijo para escalas más pequeñas.
};
// Hook useEffect para realizar acciones una vez que el componente se ha montado.
useEffect(() => {
// Obtiene el nivel de zoom inicial del mapa.
const initialZoom = map.getZoom();
// Establece el estado del zoom inicial.
setZoom(initialZoom);
// Calcula y establece la escala inicial basada en el zoom inicial.
setScale(calculateScale(initialZoom));
// La dependencia [map] asegura que este efecto se ejecute si la instancia del mapa cambia (raro en este contexto).
}, [map]);
// Hook useMapEvent para escuchar el evento 'zoomend' del mapa (cuando el zoom ha terminado).
useMapEvent('zoomend', (event) => {
// Obtiene el nuevo nivel de zoom del evento.
const newZoom = event.target.getZoom();
// Actualiza el estado del zoom.
setZoom(newZoom);
// Recalcula y actualiza el estado de la escala basada en el nuevo zoom.
setScale(calculateScale(newZoom));
});
// Hook useMapEvent para escuchar el evento 'moveend' del mapa (cuando el mapa ha terminado de moverse).
useMapEvent('moveend', () => {
// Verifica que el valor del zoom no sea null antes de calcular la escala.
if (zoom !== null) {
// Recalcula y actualiza la escala, ya que la escala depende de la latitud central.
setScale(calculateScale(zoom));
}
});
// Renderiza el componente.
return (
<div style={containerStyle}>
{/* Muestra la información del zoom si el estado 'zoom' no es null. */}
{zoom !== null && (
<div>
{/* Icono de lupa para indicar el zoom. */}
<span style={{ marginRight: '5px' }}>🔍</span>
{/* Muestra el nivel de zoom con un decimal. */}
Zoom: {zoom.toFixed(1)}
</div>
)}
{/* Muestra la información de la escala si el estado 'scale' no es null. */}
{scale !== null && (
<div>
{/* Icono de regla para indicar la escala. */}
<span style={{ marginRight: '5px' }}>📏</span>
{/* Muestra la escala en formato 1:valor. */}
Escala: 1:{scale}
</div>
)}
</div>
);
};
// Exporta el componente para que pueda ser utilizado en otras partes de la aplicación.
export default ZoomDisplay;
siguiente componente :
import React, { useState } from "react";
// Importa el hook personalizado useDibujarMarker desde el archivo './DibujarMarker'.
// Este hook probablemente se encarga de dibujar un marcador en el mapa cuando las coordenadas cambian.
import useDibujarMarker from './DibujarMarker';
// Componente funcional UbicarCoordenadaDD que permite al usuario ingresar coordenadas decimales (DD)
// y ubicar un marcador en el mapa.
const UbicarCoordenadaDD = ({ layerGroupRef, mapRef }) => {
// Estado para almacenar el valor de la longitud ingresada por el usuario. Inicializado como una cadena vacía.
const [lng, setLng] = useState("");
// Estado para almacenar el valor de la latitud ingresada por el usuario. Inicializado como una cadena vacía.
const [lat, setLat] = useState("");
// Estado para almacenar un array con las coordenadas [latitud, longitud] para dibujar el marcador.
// Inicializado como null hasta que se ingresan y validan las coordenadas.
const [coordenadas, setCoordenadas] = useState(null);
// Estado para almacenar mensajes para el usuario, como errores de validación o confirmaciones.
const [mensaje, setMensaje] = useState("");
// Llama al hook personalizado useDibujarMarker.
// Este hook recibe las coordenadas actuales, la referencia al layerGroup (donde se dibujará el marker)
// y la referencia al objeto del mapa de Leaflet.
// El hook se volverá a ejecutar cada vez que el valor de 'coordenadas' cambie.
useDibujarMarker(coordenadas, layerGroupRef, mapRef);
// Función que se ejecuta cuando el usuario hace clic en el botón "Ubicar DD".
const handleUbicar = () => {
// Limpia cualquier mensaje de error o éxito previo.
setMensaje("");
// Verifica si los campos de longitud o latitud están vacíos.
if (lng === "" || lat === "") {
// Si alguno está vacío, establece un mensaje de error y detiene la ejecución.
setMensaje("Por favor, ingrese valores para latitud y longitud.");
return;
}
// Intenta convertir las cadenas de longitud y latitud a números de punto flotante.
let parsedLat = parseFloat(lat);
let parsedLng = parseFloat(lng);
// Verifica si la conversión a número falló (si el valor no es un número válido).
if (isNaN(parsedLat) || isNaN(parsedLng)) {
// Si la conversión falla, establece un mensaje de error y detiene la ejecución.
setMensaje("Por favor, ingrese valores numéricos válidos.");
return;
}
// Sugerencia al usuario si la latitud es negativa (podría indicar inversión de coordenadas).
if (parsedLat < 0) {
setMensaje("Si la latitud es negativa, probablemente estén invertidas las coordenadas.");
return;
}
// Sugerencia al usuario si la longitud es positiva (podría indicar inversión de coordenadas).
if (parsedLng > 0) {
setMensaje("Si la longitud es positiva, probablemente estén invertidas las coordenadas.");
return;
}
// Valida que la latitud esté dentro del rango válido (-90 a 90 grados).
if (parsedLat < -90 || parsedLat > 90) {
setMensaje("La latitud debe estar entre -90 y 90 grados.");
return;
}
// Valida que la longitud esté dentro del rango válido (-180 a 180 grados).
if (parsedLng < -180 || parsedLng > 180) {
setMensaje("La longitud debe estar entre -180 y 180 grados.");
return;
}
// Si todas las validaciones pasan, actualiza el estado 'coordenadas' con los valores parseados.
// Esto activará la ejecución del hook useDibujarMarker y dibujará el marcador en el mapa.
setCoordenadas([parsedLat, parsedLng]);
// Limpia cualquier mensaje anterior de éxito (si hubiera).
setMensaje("");
};
// Función de orden superior que devuelve un manejador de eventos para los cambios en los inputs.
// Recibe una función 'setter' (como setLng o setLat) para actualizar el estado correspondiente.
const handleInputChange = (setter) => (e) => {
// Obtiene el valor actual del input.
const value = e.target.value;
// Utiliza una expresión regular para validar que solo se ingresen números (positivos o negativos)
// y un único punto decimal.
if (/^-?\d*\.?\d*$/.test(value)) {
// Si el valor cumple con el patrón, actualiza el estado utilizando la función 'setter' proporcionada.
setter(value);
}
// Si el valor no cumple con el patrón (contiene caracteres no válidos), el estado no se actualiza,
// impidiendo la entrada de caracteres no numéricos.
};
return (
// Contenedor principal del componente con estilos en línea para su posicionamiento y apariencia.
<div
style={{
position: "absolute", // Para poder posicionarlo sobre el mapa.
top: "160px", // Posición vertical desde la parte superior.
left: "60px", // Posición horizontal desde la izquierda.
zIndex: 1000, // Asegura que esté por encima de otros elementos del mapa.
background: "white", // Fondo blanco.
padding: "10px", // Espaciado interno.
borderRadius: "8px", // Bordes redondeados.
boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)", // Sombra suave.
}}
>
{/* Etiqueta para el campo de entrada de la longitud. */}
<label>Longitud (x):</label>
{/* Campo de entrada para la longitud. */}
<input
type="number" // Aunque el tipo sea 'number', la validación se hace con la regex en handleInputChange.
value={lng} // Valor controlado por el estado 'lng'.
onChange={handleInputChange(setLng)} // Asigna el manejador de cambios para actualizar el estado 'lng'.
placeholder="Ej: -00.00000000" // Texto de ejemplo para el usuario.
/>
{/* Etiqueta para el campo de entrada de la latitud. */}
<label>Latitud (y):</label>
{/* Campo de entrada para la latitud. */}
<input
type="number" // Aunque el tipo sea 'number', la validación se hace con la regex en handleInputChange.
value={lat} // Valor controlado por el estado 'lat'.
onChange={handleInputChange(setLat)} // Asigna el manejador de cambios para actualizar el estado 'lat'.
placeholder="Ej: 00.00000000" // Texto de ejemplo para el usuario.
/>
{/* Botón para activar la función de ubicación de coordenadas. */}
<button onClick={handleUbicar}>Ubicar DD</button>
{/* Sección para mostrar mensajes al usuario. */}
{mensaje && (
<div
style={{
color: mensaje.includes("fuera") || mensaje.includes("Por favor") ? 'red' : 'green',
marginTop: '10px',
}}
>
{mensaje}
</div>
)}
</div>
);
};
// Exporta el componente para que pueda ser utilizado en otras partes de la aplicación.
export default UbicarCoordenadaDD;
suguiente componente :
import React, { useState, ChangeEvent } from 'react';
// Importa el hook personalizado useDibujarMarker desde el archivo './DibujarMarker'.
// Este hook probablemente se encarga de dibujar un marcador en el mapa cuando las coordenadas cambian.
import useDibujarMarker from './DibujarMarker';
// Importa la librería Leaflet para trabajar con mapas. Aunque aquí solo se usa en la definición de tipos.
import L from 'leaflet';
// Define la interfaz Props para este componente funcional.
interface Props {
// layerGroupRef es una referencia mutable a un LayerGroup de Leaflet.
// Se utiliza para agregar o eliminar capas (como marcadores) del mapa.
layerGroupRef: React.RefObject<L.LayerGroup>;
// mapRef es una referencia mutable a la instancia del mapa de Leaflet.
// Permite interactuar directamente con el objeto del mapa.
mapRef: React.RefObject<L.Map>;
}
// Define la interfaz DMS (Grados, Minutos, Segundos) para representar las coordenadas.
interface DMS {
degrees: string;
minutes: string;
seconds: string;
}
// Componente funcional React UbicarCoordenadadms que permite al usuario ingresar coordenadas en formato
// Grados, Minutos y Segundos (DMS) y ubicar un marcador en el mapa.
const UbicarCoordenadadms: React.FC<Props> = ({ layerGroupRef, mapRef }) => {
// Estado para almacenar los valores de latitud en formato DMS. Inicializado con cadenas vacías.
const [latDms, setLatDms] = useState<DMS>({ degrees: '', minutes: '', seconds: '' });
// Estado para almacenar los valores de longitud en formato DMS. Inicializado con cadenas vacías.
const [lngDms, setLngDms] = useState<DMS>({ degrees: '', minutes: '', seconds: '' });
// Estado para almacenar las coordenadas convertidas a formato decimal [latitud, longitud].
// Inicializado como null hasta que se convierten las coordenadas DMS.
const [coordenadas, setCoordenadas] = useState<[number, number] | null>(null);
// Estado para almacenar mensajes de error para el usuario, por ejemplo, validaciones fallidas.
const [error, setError] = useState<string>('');
// Llama al hook personalizado useDibujarMarker.
// Este hook recibe las coordenadas decimales actuales, la referencia al LayerGroup y la referencia al mapa.
// Se ejecutará cada vez que el valor de 'coordenadas' cambie, actualizando el marcador en el mapa.
useDibujarMarker(coordenadas, layerGroupRef, mapRef);
// Función para convertir grados, minutos y segundos a formato decimal.
// Recibe los valores numéricos de grados, minutos y segundos.
const dmsToDecimal = (degrees: number, minutes: number, seconds: number): number => {
// Calcula el valor decimal: grados + (minutos / 60) + (segundos / 3600).
// Se utiliza Math.abs para asegurar que el cálculo sea siempre positivo,
// el signo se aplicará posteriormente basado en el signo de los grados.
return Math.abs(degrees) + (minutes / 60) + (seconds / 3600);
};
// Manejador para los cambios en los campos de entrada (grados, minutos, segundos).
// Recibe el evento de cambio del input y el tipo de coordenada ('lat' o 'lng').
const handleInputChange = (e: ChangeEvent<HTMLInputElement>, type: 'lat' | 'lng') => {
// Extrae el nombre del input (degrees, minutes, seconds) y su valor.
const { name, value } = e.target;
// Actualiza el estado correspondiente (latDms o lngDms) basado en el tipo de coordenada.
if (type === 'lat') {
// Utiliza la función de actualización de estado para mantener los valores anteriores
// y solo actualizar la propiedad del input que cambió.
setLatDms(prev => ({ ...prev, [name]: value }));
} else {
// Similar a la latitud, actualiza el estado de la longitud.
setLngDms(prev => ({ ...prev, [name]: value }));
}
// Limpia cualquier mensaje de error que pudiera estar visible al editar los campos.
setError('');
};
// Función para convertir los valores DMS ingresados a coordenadas decimales.
const convertDmsToDecimal = () => {
// Extrae los valores de grados, minutos y segundos de los estados de latitud y longitud.
const { degrees: degLat, minutes: minLat, seconds: secLat } = latDms;
const { degrees: degLng, minutes: minLng, seconds: secLng } = lngDms;
// Verifica si alguno de los campos está vacío.
if (
degLat === '' || minLat === '' || secLat === '' ||
degLng === '' || minLng === '' || secLng === ''
) {
// Si algún campo está vacío, establece un mensaje de error y detiene la conversión.
setError('Por favor complete todos los campos');
return;
}
try {
// Intenta convertir los valores de cadena a números.
const latDeg = Number(degLat);
const latMin = Number(minLat);
const latSec = Number(secLat);
const lngDeg = Number(degLng);
const lngMin = Number(minLng);
const lngSec = Number(secLng);
// Validar rangos de latitud (grados: -90 a 90, minutos/segundos: 0 a 59).
if (latDeg < -90 || latDeg > 90 || latMin < 0 || latMin >= 60 || latSec < 0 || latSec >= 60) {
setError('Valores de latitud fuera de rango');
return;
}
// Validar rangos de longitud (grados: -180 a 180, minutos/segundos: 0 a 59).
if (lngDeg < -180 || lngDeg > 180 || lngMin < 0 || lngMin >= 60 || lngSec < 0 || lngSec >= 60) {
setError('Valores de longitud fuera de rango');
return;
}
// Sugerencia al usuario si la latitud es negativa (podría indicar inversión de coordenadas).
if (latDeg < 0) {
setError("Si la latitud es negativa, probablemente estén invertidas las coordenadas.");
return;
}
// Sugerencia al usuario si la longitud es positiva (podría indicar inversión de coordenadas).
if (lngDeg > 0) {
setError("Si la longitud es positiva, probablemente estén invertidas las coordenadas.");
return;
}
// Convierte los valores DMS a decimales, manteniendo el signo de los grados.
const lat = dmsToDecimal(latDeg, latMin, latSec) * (latDeg >= 0 ? 1 : -1);
const lng = dmsToDecimal(lngDeg, lngMin, lngSec) * (lngDeg >= 0 ? 1 : -1);
// Actualiza el estado 'coordenadas' con los valores decimales convertidos.
// Esto activará el hook useDibujarMarker.
setCoordenadas([lat, lng]);
// Limpia cualquier mensaje de error después de una conversión exitosa.
setError('');
} catch (err) {
// Captura cualquier error que ocurra durante la conversión (por ejemplo, si Number() falla).
setError('Error al convertir las coordenadas');
}
};
return (
<>
{/* Contenedor para los campos de entrada de la longitud en formato DMS. */}
<div style={styles.panel(220)}>
<label>(longitud)</label>
{/* Input para los grados de longitud. */}
<input
type="number"
name="degrees"
value={lngDms.degrees}
onChange={(e) => handleInputChange(e, 'lng')}
placeholder="Grados °"
min="-180"
max="180"
step="0.000001"
/>
{/* Input para los minutos de longitud. */}
<input
type="number"
name="minutes"
value={lngDms.minutes}
onChange={(e) => handleInputChange(e, 'lng')}
placeholder="Minutos '"
min="0"
max="59"
step="0.000001"
/>
{/* Input para los segundos de longitud. */}
<input
type="number"
name="seconds"
value={lngDms.seconds}
onChange={(e) => handleInputChange(e, 'lng')}
placeholder='Segundos "'
min="0"
max="59"
step="0.000001"
/>
{/* Etiqueta indicando la dirección (Oeste). */}
<label> W </label>
{/* Botón para iniciar la conversión de DMS a decimal y ubicar el marcador. */}
<button onClick={convertDmsToDecimal}>Ubicar DMS</button>
</div>
{/* Contenedor para los campos de entrada de la latitud en formato DMS. */}
<div style={styles.panel(300)}>
<label>(latitud)</label>
{/* Input para los grados de latitud. */}
<input
type="number"
name="degrees"
value={latDms.degrees}
onChange={(e) => handleInputChange(e, 'lat')}
placeholder="Grados °"
min="-90"
max="90"
step="0.000001"
/>
{/* Input para los minutos de latitud. */}
<input
type="number"
name="minutes"
value={latDms.minutes}
onChange={(e) => handleInputChange(e, 'lat')}
placeholder="Minutos '"
min="0"
max="59"
step="0.000001"
/>
{/* Input para los segundos de latitud. */}
<input
type="number"
name="seconds"
value={latDms.seconds}
onChange={(e) => handleInputChange(e, 'lat')}
placeholder='Segundos "'
min="0"
max="59"
step="0.000001"
/>
{/* Etiqueta indicando la dirección (Norte). */}
<label> N </label>
</div>
{/* Muestra el mensaje de error si existe. */}
{error && <div style={{ color: 'red', position: 'absolute', top: '350px', left: '60px' }}>{error}</div>}
</>
);
};
// Define un objeto de estilos para los contenedores de los inputs.
const styles = {
panel: (top: number) => ({
position: 'absolute' as const, // Posicionamiento absoluto para ubicarlo sobre el mapa.
top: `${top}px`, // Posición vertical desde la parte superior.
left: '60px', // Posición horizontal desde la izquierda.
zIndex: 1000, // Asegura que esté por encima de otros elementos del mapa.
background: "white", // Fondo blanco para mejor legibilidad.
padding: "10px", // Espaciado interno.
borderRadius: "8px", // Bordes redondeados para una apariencia más suave.
boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)", // Sombra suave para destacar el panel.
display: 'flex', // Utiliza flexbox para alinear los elementos internos.
alignItems: 'center', // Alinea los elementos verticalmente al centro.
gap: '5px' // Espacio entre los elementos internos.
}),
};
export default UbicarCoordenadadms;
suguiente componente :
import React, { useState } from 'react';
// Importa el hook personalizado useDibujarMarker desde el archivo './DibujarMarker'.
// Este hook probablemente se encarga de dibujar un marcador en el mapa cuando las coordenadas cambian.
import useDibujarMarker from './DibujarMarker';
// Componente funcional Ubicardm que permite al usuario ingresar coordenadas en formato
// Grados y Minutos decimales (DM) y ubicar un marcador en el mapa.
const Ubicardm = ({ layerGroupRef, mapRef }) => {
// Estado para almacenar las coordenadas decimales [latitud, longitud] para dibujar el marcador.
// Inicializado como null hasta que se convierten las coordenadas DM.
const [coordenadas, setCoordenadas] = useState<[number, number] | null>(null);
// Estado para almacenar los valores de grados y minutos para latitud ('degrees') y longitud ('degreesw').
// Inicializado con cadenas vacías.
const [dms, setDms] = useState({ degrees: '', degreesw: '' });
// Estado para almacenar mensajes de error para el usuario, por ejemplo, validaciones fallidas.
const [error, setError] = useState<string>('');
// Llama al hook personalizado useDibujarMarker.
// Este hook recibe las coordenadas decimales actuales, la referencia al LayerGroup y la referencia al mapa.
// Se ejecutará cada vez que el valor de 'coordenadas' cambie, actualizando el marcador en el mapa.
useDibujarMarker(coordenadas, layerGroupRef, mapRef);
// Manejador para los cambios en los campos de entrada (latitud y longitud).
// Recibe el evento de cambio del input.
const handleChange = (e) => {
// Extrae el nombre del input (degrees o degreesw) y su valor.
const { name, value } = e.target;
// Actualiza el estado 'dms' manteniendo los valores anteriores y actualizando solo la propiedad del input que cambió.
setDms((prev) => ({
...prev,
[name]: value
}));
};
// Función para convertir grados y minutos decimales a formato decimal.
// Recibe el valor de grados y minutos como un número o cadena.
const dmToDecimal = (degrees) => {
// Verifica si el valor de grados es nulo, indefinido o no es un número.
if (!degrees || isNaN(degrees)) {
return null; // Retorna null si el valor no es válido.
}
// Asegura que el valor de grados sea positivo para el cálculo. El signo se manejará por separado.
degrees = Math.abs(parseFloat(degrees));
let grados, minutos;
// Determina si el formato es GGMM (grados enteros seguidos de minutos enteros) o GG.MM (grados enteros seguidos de minutos decimales).
if (degrees >= 100) { // Formato GGMM
grados = Math.floor(degrees / 100); // Obtiene la parte entera de los grados.
minutos = degrees % 100; // Obtiene la parte de los minutos.
} else { // Formato GG.MM
grados = Math.floor(degrees); // Obtiene la parte entera de los grados.
minutos = (degrees - grados) * 60; // Obtiene la parte decimal de los minutos y la convierte a minutos.
}
// Valida que los minutos no sean mayores o iguales a 60.
if (minutos >= 60) {
return null; // Retorna null si los minutos no son válidos.
}
// Retorna el valor decimal de la coordenada: grados + (minutos / 60).
return grados + (minutos / 60);
};
// Función para convertir los valores DM ingresados a coordenadas decimales.
const convertDmToDecimal = () => {
// Extrae los valores de latitud y longitud del estado 'dms'.
const { degrees, degreesw } = dms;
// Verifica si alguno de los campos está vacío.
if (degrees === '' || degreesw === '') {
setError('Por favor ingrese valores para latitud y longitud');
return;
}
// Convierte la latitud y la longitud a formato decimal utilizando la función dmToDecimal.
const lat = dmToDecimal(degrees);
const lng = dmToDecimal(degreesw);
// Verifica si la conversión de latitud o longitud resultó en null (formato incorrecto).
if (lat === null || lng === null) {
setError('Formato de grados incorrecto');
return;
}
// Valida que la latitud esté dentro del rango válido (0 a 90 grados Norte).
if (lat < 0 || lat > 90) {
setError('La latitud debe estar entre 0° y 90°');
return;
}
// Valida que la longitud esté dentro del rango válido (0 a 180 grados Oeste).
if (lng < 0 || lng > 180) {
setError('La longitud debe estar entre 0° y 180°');
return;
}
// Convierte la longitud a negativo porque se asume que es Oeste (W).
setCoordenadas([lat, -lng]);
// Limpia cualquier mensaje de error después de una conversión exitosa.
setError('');
};
return (
<>
{/* Contenedor para el input de la longitud (Oeste). */}
<div style={{
position: 'absolute',
top: '315px',
left: '60px',
zIndex: 1000,
background: "white",
padding: "10px",
borderRadius: "8px",
boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
}}>
{/* Input para ingresar la longitud en formato GGMM o GG.MM. */}
<input
type="number"
name="degreesw"
value={dms.degreesw}
placeholder="Longitud (GGMM o GG.MM)"
step="0.000001"
onChange={handleChange}
/>
{/* Etiqueta para indicar que este campo es para la Longitud Oeste. */}
<label> W (Longitud) </label>
{/* Botón para iniciar la conversión de DM a decimal y ubicar el marcador. */}
<button onClick={convertDmToDecimal}>Ubicar DM</button>
</div>
{/* Contenedor para el input de la latitud (Norte). */}
<div style={{
position: 'absolute',
top: '385px',
left: '60px',
zIndex: 1000,
background: "white",
padding: "10px",
borderRadius: "8px",
boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
}}>
{/* Input para ingresar la latitud en formato GGMM o GG.MM. */}
<input
type="number"
name="degrees"
value={dms.degrees}
placeholder="Latitud (GGMM o GG.MM)"
step="0.000001"
onChange={handleChange}
/>
{/* Etiqueta para indicar que este campo es para la Latitud Norte. */}
<label> N (Latitud)</label>
</div>
{/* Sección para mostrar mensajes de error al usuario. */}
{error && (
<div style={{
position: 'absolute',
top: '455px',
left: '60px',
zIndex: 1000,
background: "white",
padding: "8px",
borderRadius: "8px",
boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
color: 'red'
}}>
{error}
</div>
)}
</>
);
};
export default Ubicardm;
siguiente componente :
import React, { useState } from "react";
// Importa la función 'inverse' con el alias 'toLatLon' desde la librería 'mgrs'.
// Esta función se utiliza para convertir coordenadas MGRS (Military Grid Reference System) a Latitud y Longitud.
import { inverse as toLatLon } from "mgrs";
// Importa el hook personalizado useDibujarMarker desde el archivo './DibujarMarker'.
// Este hook probablemente se encarga de dibujar un marcador en el mapa cuando las coordenadas cambian.
import useDibujarMarker from './DibujarMarker';
// Componente funcional UbicarMGRS que permite al usuario ingresar una coordenada MGRS
// y ubicar un marcador en el mapa correspondiente a esa ubicación.
export const UbicarMGRS = ({ layerGroupRef, mapRef }) => {
// Estado para almacenar la cadena de texto ingresada por el usuario como coordenada MGRS a buscar.
const [coordenadasBuscar, setCoordenadasBuscar] = useState("");
// Estado para almacenar las coordenadas [longitud, latitud] resultantes de la conversión de MGRS.
// Inicializado como null hasta que se realiza una conversión exitosa.
const [coordenadas, setCoordenadas] = useState<[number, number] | null>(null);
// Estado para almacenar mensajes de error para el usuario, por ejemplo, si el formato MGRS es inválido
// o si ocurre un error durante la conversión.
const [error, setError] = useState("");
// Llama al hook personalizado useDibujarMarker.
// Este hook recibe las coordenadas [longitud, latitud] actuales, la referencia al LayerGroup (donde se dibujará el marker)
// y la referencia al objeto del mapa de Leaflet.
// El hook se volverá a ejecutar cada vez que el valor de 'coordenadas' cambie.
useDibujarMarker(coordenadas, layerGroupRef, mapRef);
// Función para validar si una cadena de texto dada tiene un formato MGRS válido.
const isValidMGRS = (mgrs) => {
// Si la cadena es nula, indefinida o no es un string, no es un MGRS válido.
if (!mgrs || typeof mgrs !== 'string') return false;
// Elimina espacios en blanco al inicio y al final y convierte la cadena a mayúsculas para la validación.
const trimmed = mgrs.trim().toUpperCase();
// Un MGRS mínimo debe tener 7 caracteres: zona (1-2) + banda (1) + cuadrícula (2) + al menos 2 dígitos de precisión.
if (trimmed.length < 7) return false;
// Verifica si la cadena contiene solo letras mayúsculas y números.
if (!/^[A-Z0-9]+$/.test(trimmed)) return false;
// Expresión regular para el formato típico de MGRS:
// - (\d{1,2}): Una o dos cifras para la zona.
// - ([C-HJ-NP-X]): Una letra para la banda (omite I y O).
// - ([A-HJ-NP-Z]{2}): Dos letras para la designación del cuadrado de 100 km.
// - (\d{2,10})?: Opcionalmente, un par de números (o más pares) para la precisión de las coordenadas.
const mgrsPattern = /^(\d{1,2})([C-HJ-NP-X])([A-HJ-NP-Z]{2})(\d{2,10})?$/;
// Retorna el resultado de la prueba de la expresión regular contra la cadena MGRS.
return mgrsPattern.test(trimmed);
};
// Función que se ejecuta cuando el usuario hace clic en el botón "Ubicar MGRS".
const handleUbicar = () => {
// Obtiene la coordenada MGRS del estado, elimina espacios y la convierte a mayúsculas.
const mgrs = coordenadasBuscar.trim().toUpperCase();
// Si el campo de entrada está vacío, establece un mensaje de error y detiene la ejecución.
if (mgrs === "") {
setError("Por favor ingrese una coordenada MGRS.");
return;
}
// Valida el formato de la coordenada MGRS utilizando la función isValidMGRS.
if (!isValidMGRS(mgrs)) {
setError("Formato MGRS inválido. Ejemplo válido: 33UXP04.");
return;
}
try {
// Intenta convertir la coordenada MGRS a latitud y longitud utilizando la función toLatLon.
const [lat, lon] = toLatLon(mgrs);
// Verifica si la conversión resultó en números válidos para latitud y longitud.
if (typeof lat !== 'number' || typeof lon !== 'number') {
throw new Error("Conversión inválida");
}
// Si la conversión es exitosa, actualiza el estado 'coordenadas' con [longitud, latitud]
// (el orden es importante para Leaflet) y limpia cualquier mensaje de error previo.
setCoordenadas([lon, lat]);
setError("");
} catch (error) {
// Si ocurre un error durante la conversión (por ejemplo, si la coordenada MGRS es válida en formato
// pero no representa una ubicación válida), registra el error en la consola y establece un mensaje de error para el usuario.
console.error("Error al convertir MGRS:", error);
setError("Error al convertir la coordenada MGRS. Verifica que sea válida.");
}
};
// Manejador para los cambios en el campo de entrada de la coordenada MGRS.
const handleChange = (e) => {
// Obtiene el valor del input y lo convierte a mayúsculas.
const value = e.target.value.toUpperCase();
// Permite solo la entrada de letras mayúsculas y números, o una cadena vacía.
if (/^[A-Z0-9]*$/.test(value) || value === "") {
// Actualiza el estado 'coordenadasBuscar' con el nuevo valor.
setCoordenadasBuscar(value);
// Limpia cualquier mensaje de error que pudiera estar visible al editar el campo.
setError("");
}
};
return (
<div>
{/* Contenedor para el input de la coordenada MGRS y el botón de ubicación. */}
<div
style={{
position: "absolute",
top: "400px",
left: "60px",
zIndex: 1000,
background: "white",
padding: "10px",
borderRadius: "8px",
boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
}}
>
{/* Campo de entrada de texto para que el usuario ingrese la coordenada MGRS. */}
<input
type="text"
value={coordenadasBuscar}
onChange={handleChange}
placeholder="Coordenadas MGRS (ej. 33UXP04)"
className="border p-2 rounded w-64" // Clases de Tailwind CSS para estilos básicos.
/>
{/* Botón para activar la función de ubicación de la coordenada MGRS. */}
<button
onClick={handleUbicar}
className="ml-2 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition" // Clases de Tailwind CSS para estilos básicos.
>
Ubicar MGRS
</button>
</div>
{/* Sección para mostrar mensajes de error al usuario si el estado 'error' tiene un valor. */}
{error && (
<div style={{
position: "absolute",
top: "480px",
left: "60px",
zIndex: 1000,
color: 'red',
background: "white",
padding: "8px",
borderRadius: "8px",
boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
}}>
{error}
</div>
)}
</div>
);
};
export default UbicarMGRS;
suguiente componente :
import React, { useState, useEffect } from "react";
// Importa la librería proj4, utilizada para realizar transformaciones de coordenadas entre diferentes sistemas de referencia espacial.
import proj4 from "proj4";
// Importa el hook personalizado useDibujarMarker desde el archivo './DibujarMarker'.
// Este hook probablemente se encarga de dibujar un marcador en el mapa cuando las coordenadas cambian.
import useDibujarMarker from "./DibujarMarker";
// Definir un objeto que mapea las zonas UTM (Universal Transverse Mercator) específicas para México
// con sus correspondientes códigos EPSG (European Petroleum Survey Group).
const utmZones = {
11: "EPSG:32611", // Baja California (Zona 11 Norte)
12: "EPSG:32612", // Sonora, Chihuahua (Zona 12 Norte)
13: "EPSG:32613", // Durango, Zacatecas, Nayarit (Zona 13 Norte)
14: "EPSG:32614", // Ciudad de México, Estado de México, Puebla (Zona 14 Norte)
15: "EPSG:32615", // Veracruz, Tabasco, Campeche (Zona 15 Norte)
16: "EPSG:32616", // Quintana Roo, Yucatán (Zona 16 Norte)
};
// Registrar las definiciones de las proyecciones UTM en la librería proj4.
// Esto permite a proj4 entender y realizar transformaciones con estos sistemas de coordenadas.
Object.entries(utmZones).forEach(([zoneNum, epsg]) => {
// Define cada proyección UTM utilizando el número de zona, el datum WGS84 (World Geodetic System 1984),
// las unidades en metros, la ausencia de definiciones predeterminadas adicionales y la indicación de que es una zona norte.
proj4.defs(epsg, `+proj=utm +zone=${zoneNum} +datum=WGS84 +units=m +no_defs +north`);
});
// Función para determinar la zona UTM aproximada en la que se encuentra una coordenada Este (X) en México.
// Se basa en rangos típicos de valores Este para las diferentes zonas UTM del país.
const determinarZonaUTM = (xEste) => {
// Si el valor Este está fuera de un rango general esperado para México, retorna null.
if (xEste < 200000 || xEste > 900000) return null;
// Asigna la zona UTM basándose en rangos del valor Este. Estos rangos son aproximados y pueden variar.
if (xEste < 300000) return utmZones[11]; // Zona 11
if (xEste < 400000) return utmZones[12]; // Zona 12
if (xEste < 500000) return utmZones[13]; // Zona 13
if (xEste < 600000) return utmZones[14]; // Zona 14
if (xEste < 700000) return utmZones[15]; // Zona 15
return utmZones[16]; // Zona 16 (para valores Este más altos)
};
// Componente funcional UbicarMetrica que permite al usuario ingresar coordenadas UTM (Este y Norte)
// y ubicar un marcador en el mapa correspondiente a esa ubicación geográfica en México.
const UbicarMetrica = ({ layerGroupRef, mapRef }) => {
// Estado para almacenar el valor de la coordenada Este (X) ingresada por el usuario. Inicializado como una cadena vacía.
const [xEste, setXEste] = useState("");
// Estado para almacenar el valor de la coordenada Norte (Y) ingresada por el usuario. Inicializado como una cadena vacía.
const [yNorte, setYNorte] = useState("");
// Estado para almacenar un array de coordenadas [latitud, longitud] convertidas desde UTM.
// Se utiliza un array para potencialmente almacenar múltiples ubicaciones, aunque en la lógica actual solo se usa la última.
const [coordenadas, setCoordenadas] = useState<[number, number][]>([]);
// Estado para almacenar mensajes de error para el usuario, por ejemplo, si los valores ingresados no son válidos
// o si ocurre un error durante la conversión de coordenadas.
const [error, setError] = useState("");
// Hook useEffect que se ejecuta cuando la lista de 'coordenadas' cambia.
// Su propósito es dibujar un nuevo marcador en el mapa cada vez que se agrega una nueva coordenada convertida.
useEffect(() => {
// Verifica si hay al menos una coordenada en el array.
if (coordenadas.length > 0) {
// Obtiene la última coordenada agregada al array.
const ultima = coordenadas[coordenadas.length - 1];
// Llama al hook useDibujarMarker para dibujar un marcador en el mapa en la ubicación de la última coordenada.
useDibujarMarker(ultima, layerGroupRef, mapRef);
}
// Las dependencias del useEffect son 'coordenadas', 'layerGroupRef' y 'mapRef'.
// El efecto se re-ejecutará si alguno de estos valores cambia.
}, [coordenadas, layerGroupRef, mapRef]);
// Función para validar si un valor ingresado es un número positivo válido (puede ser entero o decimal).
const esNumeroValido = (valor) => /^(\d+(\.\d+)?)$/.test(valor);
// Función que se ejecuta cuando el usuario hace clic en el botón "Ubicar".
const handleUbicar = () => {
// Limpia cualquier mensaje de error previo.
setError("");
// Verifica si alguno de los campos de Este o Norte está vacío después de eliminar espacios en blanco.
if (!xEste.trim() || !yNorte.trim()) {
setError("Ambos campos son obligatorios.");
return;
}
// Valida si los valores ingresados para Este y Norte son números positivos válidos.
if (!esNumeroValido(xEste) || !esNumeroValido(yNorte)) {
setError("Solo se permiten números positivos válidos.");
return;
}
// Convierte los valores de Este y Norte a números de punto flotante.
const parsedX = parseFloat(xEste);
const parsedY = parseFloat(yNorte);
// Valida si las coordenadas UTM ingresadas se encuentran dentro de un rango aproximado válido para México.
// Estos rangos son generales y pueden no cubrir todas las posibles coordenadas UTM en el país.
if (parsedX < 200000 || parsedX > 900000 || parsedY < 1000000 || parsedY > 4000000) {
setError("Coordenadas fuera del rango válido para México.");
return;
}
// Determina la zona UTM aproximada basándose en el valor de la coordenada Este.
const zonaEPSG = determinarZonaUTM(parsedX);
// Si no se pudo determinar una zona UTM válida, establece un mensaje de error.
if (!zonaEPSG) {
setError("No se pudo determinar la zona UTM.");
return;
}
try {
// Realiza la transformación de coordenadas desde el sistema UTM determinado (zonaEPSG)
// al sistema de coordenadas geográficas WGS84 (EPSG:4326), que utiliza latitud y longitud.
// proj4 espera la coordenada Este primero y luego la Norte. Retorna un array [longitud, latitud].
const [lng, lat] = proj4(zonaEPSG, "EPSG:4326", [parsedX, parsedY]);
// Actualiza el estado 'coordenadas' agregando la nueva coordenada [latitud, longitud] al array.
setCoordenadas((prev) => [...prev, [lat, lng]]);
// Limpia los campos de entrada después de una conversión exitosa.
setXEste("");
setYNorte("");
} catch (err) {
// Si ocurre un error durante la conversión de coordenadas, registra el error en la consola
// y establece un mensaje de error para el usuario.
console.error("Error en conversión:", err);
setError("Error al convertir las coordenadas.");
}
};
return (
<div
style={{
position: "absolute",
top: "450px",
left: "60px",
zIndex: 1000,
background: "white",
padding: "12px",
borderRadius: "8px",
boxShadow: "0 4px 8px rgba(0, 0, 0, 0.15)",
width: "260px"
}}
>
{/* Contenedor para el campo de entrada de la coordenada Este (X). */}
<div style={{ marginBottom: "8px" }}>
<label>X (Este): </label>
<input
type="text"
value={xEste}
onChange={(e) => setXEste(e.target.value)}
placeholder="Ej. 400000"
className="border p-1 rounded w-full" // Clases de Tailwind CSS para estilos básicos.
/>
</div>
{/* Contenedor para el campo de entrada de la coordenada Norte (Y). */}
<div style={{ marginBottom: "8px" }}>
<label>Y (Norte): </label>
<input
type="text"
value={yNorte}
onChange={(e) => setYNorte(e.target.value)}
placeholder="Ej. 2144730"
className="border p-1 rounded w-full" // Clases de Tailwind CSS para estilos básicos.
/>
</div>
{/* Botón para activar la función de ubicación de coordenadas UTM. */}
<button
onClick={handleUbicar}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 w-full" // Clases de Tailwind CSS para estilos básicos.
>
Ubicar
</button>
{/* Sección para mostrar mensajes de error al usuario si el estado 'error' tiene un valor. */}
{error && (
<div
style={{
marginTop: "10px",
color: "red",
backgroundColor: "#ffe6e6",
padding: "8px",
borderRadius: "6px",
}}
>
{error}
</div>
)}
</div>
);
};
export default UbicarMetrica;
suguiente componente :
import React, { useState, useEffect, useMemo } from "react";
// Importa el hook useMapEvent de react-leaflet para acceder a los eventos del mapa.
import { useMapEvent } from "react-leaflet";
// Importa la librería Leaflet y la interfaz LatLng para trabajar con coordenadas.
import L, { LatLng } from "leaflet";
// Importa la librería proj4 para realizar transformaciones de coordenadas entre diferentes sistemas de referencia espacial.
import proj4 from "proj4";
// Importa la librería mgrs para convertir entre coordenadas geográficas y el sistema MGRS.
import * as mgrs from "mgrs";
// Importa el icono personalizado desde el archivo './icon'.
import icon from "./icon";
// Define la interfaz Coordenadas para almacenar varios formatos de una misma ubicación.
interface Coordenadas {
lat: number; // Latitud en grados decimales.
lng: number; // Longitud en grados decimales.
xEste: number | null; // Coordenada Este en UTM (metros).
yNorte: number | null; // Coordenada Norte en UTM (metros).
zonaUTM: string | null; // Código EPSG de la zona UTM.
mgrsCoord: string; // Coordenada en formato MGRS.
latDMS: string; // Latitud en formato Grados, Minutos y Segundos.
lngDMS: string; // Longitud en formato Grados, Minutos y Segundos.
latDM: string; // Latitud en formato Grados y Minutos decimales.
lngDM: string; // Longitud en formato Grados y Minutos decimales.
}
// Define la interfaz CapturaClickProps para las propiedades del componente.
interface CapturaClickProps {
layerGroupRef: React.RefObject<L.LayerGroup>; // Referencia mutable a un LayerGroup de Leaflet para agregar capas.
}
// 🔹 Definición de las proyecciones UTM (Universal Transverse Mercator) para algunas zonas.
// Record<number, string> asegura que las claves son números y los valores son strings.
const utmZones: Record<number, string> = {
11: "EPSG:32611", 12: "EPSG:32612", 13: "EPSG:32613",
14: "EPSG:32614", 15: "EPSG:32615", 16: "EPSG:32616",
};
// Registra las definiciones de las proyecciones UTM en la librería proj4.
Object.entries(utmZones).forEach(([zone, epsg]) => {
// Define cada proyección UTM utilizando el número de zona, el datum WGS84, las unidades en metros,
// la ausencia de definiciones predeterminadas adicionales y la indicación de que es una zona norte.
proj4.defs(epsg, `+proj=utm +zone=${zone} +datum=WGS84 +units=m +no_defs +north`);
});
// Función para determinar la zona UTM aproximada basada en la longitud.
const determinarZonaUTM = (lng: number): string | null => {
// Calcula la zona UTM dividiendo la longitud + 180 por 6 y tomando la parte entera + 1.
const zona = Math.floor((lng + 180) / 6) + 1;
// Retorna el código EPSG de la zona UTM si existe en el objeto utmZones, de lo contrario retorna null.
return utmZones[zona] || null;
};
// 🔹 Funciones de conversión de coordenadas.
// Función para convertir grados decimales a Grados y Minutos decimales (DM).
const decimalToDM = (decimal: number, isLat: boolean): string => {
const absolute = Math.abs(decimal); // Obtiene el valor absoluto.
const degrees = Math.floor(absolute); // Obtiene la parte entera (grados).
const minutes = ((absolute - degrees) * 60).toFixed(4); // Calcula los minutos decimales.
const direction = isLat ? (decimal >= 0 ? "N" : "S") : (decimal >= 0 ? "E" : "W"); // Determina la dirección (N/S para latitud, E/W para longitud).
return `${degrees}° ${minutes}' ${direction}`; // Formatea la salida.
};
// Función para convertir grados decimales a Grados, Minutos y Segundos (DMS).
const decimalToDMS = (decimal: number, isLat: boolean): string => {
const absolute = Math.abs(decimal); // Obtiene el valor absoluto.
const degrees = Math.floor(absolute); // Obtiene la parte entera (grados).
const minutesFloat = (absolute - degrees) * 60; // Calcula los minutos con decimales.
const minutes = Math.floor(minutesFloat); // Obtiene la parte entera de los minutos.
const seconds = ((minutesFloat - minutes) * 60).toFixed(2); // Calcula los segundos decimales.
const direction = isLat ? (decimal >= 0 ? "N" : "S") : (decimal >= 0 ? "E" : "W"); // Determina la dirección.
return `${degrees}° ${minutes}' ${seconds}" ${direction}`; // Formatea la salida.
};
// Función para validar si una latitud y longitud están dentro de los rangos válidos.
const validarCoordenadas = (lat: number, lng: number): boolean =>
lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
// Componente funcional CapturaClick que permite capturar las coordenadas del mouse al moverse sobre el mapa
// y mostrar un marcador con información detallada al hacer clic.
const CapturaClick: React.FC<CapturaClickProps> = ({ layerGroupRef }) => {
// Estado para almacenar las coordenadas en varios formatos al mover el mouse.
const [coordenadas, setCoordenadas] = useState<Coordenadas | null>(null);
// Estado para almacenar las coordenadas del último clic del usuario.
const [click, setClick] = useState<Coordenadas | null>(null);
// Función para abrir las coordenadas en Google Maps en una nueva ventana.
const openInGoogleMaps = (lat: number, lng: number) =>
window.open(`https://www.google.com/maps?q=&layer=c&cbll=${lat},${lng}`, "_blank");
// Función para abrir las coordenadas en Google Earth en una nueva ventana.
const openInGoogleEarth = (lat: number, lng: number) =>
window.open(`https://earth.google.com/web/@${lat},${lng},5a,5000d,10y,0h,0t,0r`, "_blank");
// 🔸 Evento de movimiento del mouse sobre el mapa.
useMapEvent("mousemove", ({ latlng }) => {
const { lat, lng } = latlng; // Obtiene la latitud y longitud del evento.
// Si las coordenadas no son válidas, no hace nada.
if (!validarCoordenadas(lat, lng)) return;
// Determina la zona UTM basada en la longitud.
const zonaUTM = determinarZonaUTM(lng);
// Convierte las coordenadas a MGRS si la latitud está dentro del rango válido (-80 a 84 grados).
const mgrsCoord = lat <= 84 && lat >= -80 ? mgrs.forward([lng, lat], 8) : "Fuera de rango";
// Convierte las coordenadas a UTM si se pudo determinar la zona UTM.
const [xEste, yNorte] = zonaUTM
? proj4("EPSG:4326", zonaUTM, [lng, lat])
: [null, null];
// Actualiza el estado 'coordenadas' con todos los formatos calculados.
setCoordenadas({
lat, lng, xEste, yNorte, zonaUTM,
mgrsCoord,
latDMS: decimalToDMS(lat, true),
lngDMS: decimalToDMS(lng, false),
latDM: decimalToDM(lat, true),
lngDM: decimalToDM(lng, false),
});
});
// 🔸 Evento de clic del usuario en el mapa.
useMapEvent("click", () => {
// Al hacer clic, guarda las coordenadas actuales del mouse en el estado 'click'.
if (coordenadas) setClick(coordenadas);
// Limpia cualquier marcador anterior del LayerGroup.
layerGroupRef.current?.clearLayers();
});
// Hook useEffect que se ejecuta cuando el estado 'click' cambia.
useEffect(() => {
// Si no hay un clic o la referencia al LayerGroup no está definida, no hace nada.
if (!click || !layerGroupRef.current) return;
// Extrae la información de coordenadas del estado 'click'.
const {
lat, lng, xEste, yNorte, zonaUTM,
mgrsCoord, latDMS, lngDMS, latDM, lngDM
} = click;
// Define el contenido HTML para el popup del marcador.
const popupContent = `
📌 Coordenadas Decimales :<br>
Longitud: <b>${lng?.toFixed(13) || "N/A"}</b><br>
Latitud : <b>${lat?.toFixed(13) || "N/A"}</b><br><br>
📌 Coordenadas Métricas (UTM - ${zonaUTM || "N/A"}):<br>
X: <b>${xEste?.toFixed(4) || "N/A"}</b><br>
Y: <b>${yNorte?.toFixed(4) || "N/A"}</b><br><br>
📌 MGRS: <b>${mgrsCoord}</b><br><br>
📌 DMS:<br>
Longitud: <b>${lngDMS}</b><br>
Latitud : <b>${latDMS}</b><br><br>
📌 DM:<br>
Longitus: <b>${lngDM}</b><br>
Latitud: <b>${latDM}</b><br><br>
<button id="googleMapsBtn">Ir a Google</button>
<button id="googleEarthBtn">Ir a Earth</button>
`;
// Crea un marcador en la ubicación del clic con el icono personalizado y el popup.
const marker = L.marker([lat, lng], { icon }).bindPopup(popupContent);
// Agrega un listener para el evento 'popupopen' del marcador.
marker.on("popupopen", () => {
// Agrega listeners a los botones dentro del popup para abrir Google Maps y Google Earth.
document.getElementById("googleMapsBtn")?.addEventListener("click", () =>
openInGoogleMaps(lat, lng)
);
document.getElementById("googleEarthBtn")?.addEventListener("click", () =>
openInGoogleEarth(lat, lng)
);
});
// Agrega el marcador al LayerGroup, haciéndolo visible en el mapa.
layerGroupRef.current.addLayer(marker);
}, [click, layerGroupRef]); // El efecto se ejecuta cuando 'click' o 'layerGroupRef' cambian.
// El componente no renderiza nada visualmente por sí mismo, solo interactúa con el mapa a través de eventos.
return null;
};
export default CapturaClick;
siguiente componente :
import L from 'leaflet';
// Importa la librería Leaflet, esencial para trabajar con mapas interactivos.
import icon from "./icon";
// Importa el icono personalizado desde el archivo './icon'. Este icono se utilizará para los marcadores.
// Función para abrir la ubicación en Google Maps Street View en una nueva ventana del navegador.
// Recibe la latitud (lat) y la longitud (lng) como argumentos.
const openInGoogleMapsStreetView = (lat, lng) => {
// Construye la URL de Google Maps con las coordenadas proporcionadas.
// El formato '0{lat},{lng}' es un formato común para pasar coordenadas a Google Maps.
const url = `https://www.google.com/maps?q=&layer=c&cbll=${lat},${lng}`;
// Abre una nueva ventana o pestaña en el navegador con la URL de Google Maps.
window.open(url, '_blank');
};
// Función para abrir la ubicación en Google Earth Web en una nueva ventana del navegador.
// Recibe la latitud (lat) y la longitud (lng) como argumentos.
const openInGoogleEarthStreetView = (lat, lng) => {
// Construye la URL de Google Earth Web con las coordenadas proporcionadas.
// El formato incluye la latitud, longitud y algunos parámetros de vista (altitud, inclinación, etc.).
const url = `https://earth.google.com/web/@${lat},${lng},5a,5000d,10y,0h,0t,0r`;
// Abre una nueva ventana o pestaña en el navegador con la URL de Google Earth Web.
window.open(url, '_blank');
};
// Hook personalizado de React para dibujar un marcador en un mapa de Leaflet.
// Recibe tres argumentos:
// - punto: Un array que contiene la latitud y la longitud de la ubicación a marcar ([lat, lng]).
// - layerGroupRef: Una referencia mutable (Ref) a un LayerGroup de Leaflet donde se agregará el marcador.
// - mapRef: Una referencia mutable (Ref) a la instancia del objeto del mapa de Leaflet.
const useDibujarMarker = (punto, layerGroupRef, mapRef) => {
// Si 'punto' es null o undefined (no se proporciona una ubicación), la función se detiene sin hacer nada.
if (!punto) return;
// Limpia todas las capas existentes dentro del LayerGroup referenciado.
// Esto asegura que solo se muestre el marcador más reciente.
layerGroupRef.current?.clearLayers();
// Genera un ID único para el popup del marcador basado en la marca de tiempo actual.
const popupId = `popup-${Date.now()}`;
// Define el contenido HTML que se mostrará dentro del popup del marcador.
// Incluye las coordenadas de la ubicación y dos botones para abrir en Google Maps y Google Earth.
const popupContent = `
<div id="${popupId}">
<p><b>Ubicación:</b> ${punto[0]}, ${punto[1]}</p><br>
<button class="googleMapsBtn">Ir a Google Maps</button>
<button class="googleEarthBtn">Ir a Google Earth</button>
</div>
`;
// Crea un nuevo marcador de Leaflet en las coordenadas proporcionadas ('punto').
// Utiliza el 'icon' importado para la apariencia del marcador.
// Asocia el 'popupContent' al marcador para que se muestre al hacer clic en él.
const marker = L.marker([punto[0], punto[1]], { icon }).bindPopup(popupContent);
// Agrega un listener para el evento 'popupopen' del marcador.
// Este evento se dispara cuando el popup del marcador se abre.
marker.on("popupopen", () => {
// Obtiene el contenedor del popup utilizando el ID único generado.
const container = document.getElementById(popupId);
// Busca el botón con la clase 'googleMapsBtn' dentro del contenedor del popup
// y agrega un event listener para abrir Google Maps al hacer clic.
container?.querySelector(".googleMapsBtn")?.addEventListener("click", () =>
openInGoogleMapsStreetView(punto[0], punto[1])
);
// Busca el botón con la clase 'googleEarthBtn' dentro del contenedor del popup
// y agrega un event listener para abrir Google Earth al hacer clic.
container?.querySelector(".googleEarthBtn")?.addEventListener("click", () =>
openInGoogleEarthStreetView(punto[0], punto[1])
);
});
// Agrega el marcador recién creado al LayerGroup referenciado, haciéndolo visible en el mapa.
layerGroupRef.current?.addLayer(marker);
// Centra el mapa en las coordenadas proporcionadas ('punto') y ajusta el nivel de zoom actual del mapa.
// 'flyTo' proporciona una animación suave al moverse al nuevo centro y zoom.
mapRef.current?.flyTo(punto, mapRef.current.getZoom());
};
// Exporta el hook personalizado 'useDibujarMarker' para que pueda ser utilizado en otros componentes.
export default useDibujarMarker;
siguiente componente :
import L from "leaflet";
// Importa la librería Leaflet, esencial para trabajar con mapas interactivos.
// Crea una instancia de un icono personalizado de Leaflet.
// Este objeto define la apariencia y el comportamiento del marcador que se mostrará en el mapa.
const Icon = L.icon({
// Define el tamaño del icono del marcador en píxeles [ancho, alto].
iconSize: [30, 41],
// Define el punto del icono que corresponderá a la ubicación real de la coordenada [x, y].
// En este caso, el punto inferior central del icono se alineará con la latitud y longitud.
iconAnchor: [15, 41],
// Define el punto del icono donde se anclará la parte superior del popup cuando se abra [x, y].
// Un valor positivo en x desplaza el popup a la derecha, y un valor negativo en y lo desplaza hacia arriba.
popupAnchor: [2, -40],
// Especifica la URL de la imagen que se utilizará como icono del marcador.
iconUrl: "/icons/map_marker.png",
// Define el tamaño de la imagen de la sombra del marcador en píxeles [ancho, alto].
shadowSize: [45, 45],
// Define el punto de la imagen de la sombra que se alineará con el 'iconAnchor' [x, y].
shadowAnchor: [10, 40],
// Especifica la URL de la imagen que se utilizará como sombra del marcador.
shadowUrl: "/icons/marker-shadow.png"
});
// Exporta la instancia del icono personalizado para que pueda ser utilizada en otros componentes
// al crear marcadores en el mapa de Leaflet.
export default Icon;