@datosgeo-atdt/geo-herramientas
v1.0.0
Published
Herramientas de análisis geoespacial para mapas MapLibre GL JS
Readme
@datosgeo-atdt/geo-herramientas
Herramientas de análisis geoespacial para React + MapLibre GL JS. Incluye hooks y componentes listos para usar: rutas óptimas, isócronas de tiempo, edificios 3D, terreno 3D y selector de estilos de mapa.
Instalación
npm install @datosgeo-atdt/geo-herramientasDependencias requeridas (peer dependencies)
Este paquete requiere que tu proyecto ya tenga instalado:
npm install maplibre-gl react react-domImportar estilos CSS
Importante: debes importar el CSS del paquete una sola vez en tu proyecto (por ejemplo en tu archivo main.tsx o App.tsx):
import '@datosgeo-atdt/geo-herramientas/dist/index.css';Sin esta importación los botones aparecerán sin estilo (negros y sin posicionamiento).
Uso rápido
Todos los hooks y componentes reciben un mapRef — la referencia al objeto Map de MapLibre que obtienes de tu instancia del mapa.
import { useRef } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import '@datosgeo-atdt/geo-herramientas/dist/index.css';
const mapRef = useRef<maplibregl.Map>(null);Componentes
<MapToolbar />
Barra de herramientas completa que integra todos los controles en un solo componente.
import { MapToolbar, useOptimalRoute, useIsochrone, useBuildings3D } from '@datosgeo-atdt/geo-herramientas';
function MiMapa() {
const mapRef = useRef<maplibregl.Map>(null);
const [currentStyle, setCurrentStyle] = useState('https://www.mapabase.atdt.gob.mx/style.json');
const { routeMode, setRouteMode, clearRoute } = useOptimalRoute({ mapRef });
const { isochroneMode, setIsochroneMode, isLoading, isComplete, clearIsochrone } = useIsochrone({
mapRef,
orsApiKey: 'TU_API_KEY_ORS',
});
const { buildings3DEnabled, toggleBuildings3D } = useBuildings3D({ mapRef });
return (
<div style={{ position: 'relative', width: '100%', height: '100vh' }}>
{/* Tu mapa */}
<div ref={/* tu container ref */} style={{ width: '100%', height: '100%' }} />
<MapToolbar
mapRef={mapRef}
routeMode={routeMode}
onToggleRouteMode={() => setRouteMode(!routeMode)}
isochroneMode={isochroneMode}
onToggleIsochroneMode={() => setIsochroneMode(!isochroneMode)}
isochroneLoading={isLoading}
isochroneComplete={isComplete}
buildings3DEnabled={buildings3DEnabled}
onToggleBuildings3D={toggleBuildings3D}
currentStyle={currentStyle}
onStyleChange={setCurrentStyle}
onClear={() => { clearRoute(); clearIsochrone(); }}
activeColor="#611232"
/>
</div>
);
}Props de MapToolbar
| Prop | Tipo | Requerido | Descripción |
|------|------|-----------|-------------|
| mapRef | RefObject<maplibregl.Map> | ✅ | Referencia al mapa |
| routeMode | boolean | ✅ | Estado del modo ruta |
| onToggleRouteMode | () => void | ✅ | Activa/desactiva modo ruta |
| isochroneMode | boolean | ✅ | Estado del modo isocrona |
| onToggleIsochroneMode | () => void | ✅ | Activa/desactiva modo isocrona |
| isochroneLoading | boolean | — | Muestra spinner de carga |
| isochroneComplete | boolean | — | Muestra indicador de éxito |
| buildings3DEnabled | boolean | ✅ | Estado de edificios 3D |
| onToggleBuildings3D | () => void | ✅ | Activa/desactiva edificios 3D |
| terrainEnabled | boolean | — | Estado del terreno 3D (controlado externo) |
| setTerrainEnabled | Dispatch<SetStateAction<boolean>> | — | Setter para terreno controlado externo |
| currentStyle | string | ✅ | URL del estilo de mapa activo |
| onStyleChange | (url: string) => void | ✅ | Callback al cambiar estilo |
| mapStyles | MapStyle[] | — | Lista personalizada de estilos |
| onClear | () => void | ✅ | Limpia todas las capas |
| activeColor | string | — | Color de acento (default #611232) |
| className | string | — | Clase CSS extra para la barra |
<MapStyleSelector />
Selector flotante de estilos de mapa base (blanco, color, oscuro, satélite).
import { MapStyleSelector } from '@datosgeo-atdt/geo-herramientas';
const [currentStyle, setCurrentStyle] = useState('https://www.mapabase.atdt.gob.mx/style.json');
<MapStyleSelector
currentStyle={currentStyle}
onStyleChange={setCurrentStyle}
/>Para usar estilos personalizados:
const misEstilos = [
{ id: 'claro', label: 'Claro', url: 'https://mi-servidor/style-light.json', icon: 'map' },
{ id: 'oscuro', label: 'Oscuro', url: 'https://mi-servidor/style-dark.json', icon: 'map-dark' },
{ id: 'satelite', label: 'Satélite', url: 'https://mi-servidor/style-sat.json', icon: 'satellite' },
];
<MapStyleSelector
currentStyle={currentStyle}
onStyleChange={setCurrentStyle}
styles={misEstilos}
activeColor="#1a73e8"
/>Props de MapStyleSelector
| Prop | Tipo | Requerido | Descripción |
|------|------|-----------|-------------|
| currentStyle | string | ✅ | URL del estilo activo |
| onStyleChange | (url: string) => void | ✅ | Callback al seleccionar un estilo |
| styles | MapStyle[] | — | Estilos personalizados (usa los de ATDT por defecto) |
| activeColor | string | — | Color del botón activo |
<TerrainToggle />
Botón para activar/desactivar el terreno 3D con hillshading.
import { TerrainToggle } from '@datosgeo-atdt/geo-herramientas';
// Modo no controlado (maneja su propio estado)
<TerrainToggle mapRef={mapRef} />
// Modo controlado (el estado lo manejas tú)
const [terrainEnabled, setTerrainEnabled] = useState(false);
<TerrainToggle
mapRef={mapRef}
terrainEnabled={terrainEnabled}
setTerrainEnabled={setTerrainEnabled}
exaggeration={2}
activePitch={60}
activeBearing={-45}
/>Props de TerrainToggle
| Prop | Tipo | Requerido | Descripción |
|------|------|-----------|-------------|
| mapRef | RefObject<maplibregl.Map> | ✅ | Referencia al mapa |
| terrainEnabled | boolean | — | Estado controlado externamente |
| setTerrainEnabled | Dispatch<SetStateAction<boolean>> | — | Setter controlado externamente |
| exaggeration | number | — | Exageración del terreno (default 2) |
| activePitch | number | — | Inclinación al activar (default 60) |
| activeBearing | number | — | Rotación al activar (default -45) |
Hooks
useOptimalRoute
Calcula la ruta óptima entre múltiples puntos usando OSRM. El usuario hace clic en el mapa para agregar nodos.
import { useOptimalRoute } from '@datosgeo-atdt/geo-herramientas';
const {
routeMode,
setRouteMode,
selectedNodes,
setSelectedNodes,
routeData,
distanceText,
clearRoute,
isCalculating,
} = useOptimalRoute({
mapRef,
lineColor: '#007cbf', // color de la línea de ruta
lineWidth: 4, // grosor en px
osrmUrl: 'https://router.project-osrm.org', // servidor OSRM
profile: 'driving', // 'driving' | 'walking' | 'cycling'
});
// Activar modo: el usuario hace clic en el mapa para agregar puntos
<button onClick={() => setRouteMode(!routeMode)}>
{routeMode ? 'Cancelar ruta' : 'Trazar ruta'}
</button>
{distanceText && <p>Distancia total: {distanceText}</p>}
<button onClick={clearRoute}>Limpiar</button>Opciones de useOptimalRoute
| Parámetro | Tipo | Default | Descripción |
|-----------|------|---------|-------------|
| mapRef | RefObject<maplibregl.Map> | — | Referencia al mapa |
| lineColor | string | '#007cbf' | Color de la línea de ruta |
| lineWidth | number | 4 | Grosor de la línea en px |
| osrmUrl | string | 'https://router.project-osrm.org' | URL del servidor OSRM |
| profile | string | 'driving' | Perfil de transporte |
Valores retornados
| Valor | Tipo | Descripción |
|-------|------|-------------|
| routeMode | boolean | Si el modo ruta está activo |
| setRouteMode | (active: boolean) => void | Activa/desactiva el modo |
| selectedNodes | SelectedNode[] | Nodos seleccionados por el usuario |
| setSelectedNodes | Dispatch | Setter de nodos (para eliminar puntos manualmente) |
| routeData | number[][] | Coordenadas de la ruta calculada |
| distanceText | string \| null | Distancia formateada (ej. "2.34 km") |
| clearRoute | () => void | Elimina ruta y puntos del mapa |
| isCalculating | boolean | Indica si hay un cálculo en curso |
useIsochrone
Genera isócronas de tiempo (áreas alcanzables) usando OpenRouteService. El usuario hace clic en el mapa para generar el análisis.
import { useIsochrone } from '@datosgeo-atdt/geo-herramientas';
const {
isochroneMode,
setIsochroneMode,
isLoading,
isComplete,
clearIsochrone,
timeRange,
setTimeRange,
} = useIsochrone({
mapRef,
orsApiKey: 'TU_API_KEY', // API key de openrouteservice.org
ranges: [900, 1800, 3600], // rangos en segundos: 15 min, 30 min, 1 hora
profile: 'driving-car', // perfil de movilidad
colors: ['#D79BB3', '#A24D7B', '#4A0D4D'], // un color por rango
});
// Activa el modo: el siguiente clic en el mapa genera las isócronas
<button onClick={() => setIsochroneMode(!isochroneMode)}>
{isochroneMode ? 'Cancelar' : 'Generar isócronas'}
</button>
// Control de visibilidad por tiempo (en minutos)
<input
type="range"
min={15}
max={60}
value={timeRange}
onChange={e => setTimeRange(Number(e.target.value))}
/>
<span>Mostrar hasta {timeRange} min</span>Obtén tu API key gratuita en openrouteservice.org
Opciones de useIsochrone
| Parámetro | Tipo | Default | Descripción |
|-----------|------|---------|-------------|
| mapRef | RefObject<maplibregl.Map> | — | Referencia al mapa |
| orsApiKey | string | — | API key de OpenRouteService |
| ranges | number[] | [900, 1800, 3600] | Rangos en segundos |
| profile | string | 'driving-car' | Perfil ORS ('driving-car', 'foot-walking', 'cycling-regular') |
| colors | string[] | colores morados | Un color por rango, del más pequeño al más grande |
Valores retornados
| Valor | Tipo | Descripción |
|-------|------|-------------|
| isochroneMode | boolean | Si el modo está activo |
| setIsochroneMode | (active: boolean) => void | Activa/desactiva el modo |
| isLoading | boolean | Calculando isócronas |
| isComplete | boolean | Cálculo completado (se resetea en 2.5 s) |
| clearIsochrone | () => void | Elimina las capas del mapa |
| timeRange | number | Minutos visibles actualmente |
| setTimeRange | Dispatch | Controla qué rangos se muestran |
useBuildings3D
Renderiza edificios en 3D con extrusión. Soporta tres modos de fuente: GeoJSON, vector tiles propios, o reutilizar una fuente que ya viene en el estilo del mapa.
Modo 1 — GeoJSON (más simple)
import { useBuildings3D } from '@datosgeo-atdt/geo-herramientas';
const { buildings3DEnabled, toggleBuildings3D, isLoading } = useBuildings3D({
mapRef,
footprintsUrl: 'https://mi-servidor/edificios.geojson',
heightProperty: 'altura',
defaultHeight: 10,
buildingColor: '#c0b8b0',
baseColor: '#a09890',
});
<button onClick={toggleBuildings3D} disabled={isLoading}>
{buildings3DEnabled ? 'Ocultar edificios 3D' : 'Ver edificios 3D'}
</button>Modo 2 — Vector tiles propios
Cuando tienes un servidor de vector tiles propio que sí tiene CORS configurado para tu dominio:
const { buildings3DEnabled, toggleBuildings3D, isLoading } = useBuildings3D({
mapRef,
sourceType: 'vector',
footprintsUrl: 'https://mi-servidor/api/edificios', // endpoint TileJSON
// sourceLayer no es necesario si el layer se llama 'footprints' (default automático)
sourceLayer: 'edificios', // especificar solo si el layer tiene otro nombre
heightProperty: 'height_mean',
defaultHeight: 10,
});Modo 3 — Reutilizar fuente del estilo (recomendado para CORS)
Si tu estilo de mapa ya incluye la fuente de edificios, úsala directamente en vez de cargarla de nuevo. Esto evita peticiones adicionales que pueden fallar por CORS cuando el servidor de tiles no permite tu dominio de desarrollo:
// El estilo base ya tiene definida la fuente "lotes" con los tiles de edificios.
// Pasamos existingSourceId para reutilizarla sin hacer ninguna petición extra.
// sourceLayer se omite: cuando hay existingSourceId se usa 'footprints' automáticamente.
const { buildings3DEnabled, toggleBuildings3D, isLoading } = useBuildings3D({
mapRef,
existingSourceId: 'lotes', // nombre exacto de la fuente en el estilo
heightProperty: 'height_mean',
buildingColor: '#DDDDDD', // opcional: color personalizado
buildingOpacity: 0.7, // opcional: opacidad entre 0 y 1
});¿Cómo sé el nombre de la fuente? Está definido en el JSON de tu estilo de mapa, dentro de la clave
"sources". Por ejemplo:"sources": { "lotes": { "type": "vector", ... } }. ¿Y elsourceLayer? Por defecto es'footprints'cuando usasexistingSourceIdosourceType: 'vector'. Solo pásalo explícitamente si tu tileset usa un nombre diferente.
Opciones de useBuildings3D
| Parámetro | Tipo | Default | Descripción |
|-----------|------|---------|-------------|
| mapRef | RefObject<maplibregl.Map> | — | Referencia al mapa |
| existingSourceId | string | — | ID de una fuente ya cargada por el estilo. Si se pasa, footprintsUrl se ignora y no se hace ninguna petición extra |
| footprintsUrl | string | URL ATDT | URL del GeoJSON o TileJSON. Se usa solo si existingSourceId no se especifica |
| sourceType | 'geojson' \| 'vector' | 'geojson' | Tipo de fuente cuando se crea una nueva (ignorado con existingSourceId) |
| sourceLayer | string | 'footprints'* | Nombre del layer dentro del tileset. *Se auto-asigna 'footprints' cuando se usa existingSourceId o sourceType: 'vector'; solo pásalo si tu tileset usa otro nombre |
| heightProperty | string | 'altura' | Propiedad del feature que contiene la altura en metros |
| defaultHeight | number | 10 | Altura en metros cuando el feature no tiene la propiedad |
| buildingColor | string | '#c0b8b0' | Color de la extrusión |
| buildingOpacity | number | 0.85 | Opacidad de la extrusión (0–1) |
| baseColor | string | '#a09890' | Color del piso del edificio |
Ejemplo completo
El patrón recomendado separa la lógica de los hooks en un componente hijo que solo se monta después de que el mapa está listo (mapLoaded). Esto evita que los hooks intenten acceder al mapa antes de que exista.
import { useRef, useState, useEffect } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import {
MapToolbar,
useOptimalRoute,
useIsochrone,
useBuildings3D,
} from '@datosgeo-atdt/geo-herramientas';
const STYLE_INICIAL = 'https://www.mapabase.atdt.gob.mx/style.json';
const ORS_API_KEY = 'TU_API_KEY_ORS';
// ── Componente hijo: solo se monta cuando el mapa ya está listo ──────────────
function MapControls({
mapRef,
currentStyle,
setCurrentStyle,
}: {
mapRef: React.RefObject<maplibregl.Map>;
currentStyle: string;
setCurrentStyle: (s: string) => void;
}) {
const route = useOptimalRoute({ mapRef });
const isochrone = useIsochrone({ mapRef, orsApiKey: ORS_API_KEY });
// Si el estilo del mapa ya incluye la fuente "lotes", usa existingSourceId
// para evitar una segunda petición que puede fallar por CORS en desarrollo.
// sourceLayer se omite: con existingSourceId se usa 'footprints' automáticamente.
const buildings = useBuildings3D({
mapRef,
existingSourceId: 'lotes', // fuente ya cargada por el estilo base
heightProperty: 'height_mean',
});
return (
<MapToolbar
mapRef={mapRef}
routeMode={route.routeMode}
onToggleRouteMode={() => route.setRouteMode(!route.routeMode)}
isochroneMode={isochrone.isochroneMode}
onToggleIsochroneMode={() => isochrone.setIsochroneMode(!isochrone.isochroneMode)}
isochroneLoading={isochrone.isLoading}
isochroneComplete={isochrone.isComplete}
buildings3DEnabled={buildings.buildings3DEnabled}
onToggleBuildings3D={buildings.toggleBuildings3D}
currentStyle={currentStyle}
onStyleChange={setCurrentStyle}
onClear={() => { route.clearRoute(); isochrone.clearIsochrone(); }}
/>
);
}
// ── Componente principal del mapa ────────────────────────────────────────────
export default function MapView() {
const containerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<maplibregl.Map>(null);
const [mapLoaded, setMapLoaded] = useState(false);
const [currentStyle, setCurrentStyle] = useState(STYLE_INICIAL);
const isFirstStyle = useRef(true);
// Inicializar el mapa
useEffect(() => {
if (!containerRef.current) return;
const map = new maplibregl.Map({
container: containerRef.current,
style: STYLE_INICIAL,
center: [-99.133, 19.432],
zoom: 11,
});
(mapRef as React.MutableRefObject<maplibregl.Map>).current = map;
map.addControl(new maplibregl.NavigationControl());
map.on('style.load', () => setMapLoaded(true));
return () => { map.remove(); setMapLoaded(false); };
}, []);
// Cambiar estilo de mapa base
useEffect(() => {
if (isFirstStyle.current) { isFirstStyle.current = false; return; }
setMapLoaded(false);
mapRef.current?.setStyle(currentStyle);
}, [currentStyle]);
return (
// ⚠️ position: relative es obligatorio para que los botones
// se posicionen correctamente sobre el mapa
<div style={{ position: 'relative', width: '100%', height: '100vh' }}>
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />
{mapLoaded && (
<MapControls
mapRef={mapRef as React.RefObject<maplibregl.Map>}
currentStyle={currentStyle}
setCurrentStyle={setCurrentStyle}
/>
)}
</div>
);
}Tipos exportados
import type {
MapRef,
SelectedNode,
MapStyle,
UseOptimalRouteOptions,
UseOptimalRouteReturn,
UseIsochroneOptions,
UseIsochroneReturn,
UseBuildings3DOptions,
UseBuildings3DReturn,
TerrainToggleProps,
MapStyleSelectorProps,
MapToolbarProps,
} from '@datosgeo-atdt/geo-herramientas';Licencia
MIT © DatosGeo ATDT
