Mapas interactivos con Leaflet no significa solo mostrar un mapa base y colocar un marcador. En esta guía avanzada vas a convertir un visor sencillo en una pequeña aplicación geoespacial con pantalla completa, búsqueda de direcciones, exportación a PNG y PDF, eventos, carga dinámica de puntos de interés y agrupación de marcadores.
El artículo continúa el tutorial anterior sobre crear mapas interactivos con Leaflet, pero está preparado para poder seguirse de forma independiente si ya tienes una base mínima con HTML, CSS, JavaScript y Leaflet. La idea es mejorar el visor sin convertirlo todavía en una aplicación compleja con framework frontend.
TL;DR
Aprenderás a mejorar mapas interactivos con Leaflet añadiendo Leaflet.Fullscreen, Leaflet.Control.Geocoder con Nominatim, html2canvas, jsPDF, eventos del mapa, consultas a Overpass API y MarkerCluster para agrupar muchos puntos.
Pregunta incómoda
Un mapa puede parecer suficiente con un marcador y un popup, pero ¿qué ocurre cuando el usuario busca direcciones, mueve el mapa, carga decenas de POIs, cambia capas y necesita exportar la vista? Ahí empieza realmente el diseño de un visor geoespacial usable.
Qué vas a construir en este mapa interactivo con Leaflet
El objetivo es pasar de una demo cartográfica a un visor más próximo a un caso real. No vamos a construir todavía una arquitectura completa con backend, persistencia y autenticación, pero sí añadiremos las piezas habituales que aparecen en muchos proyectos GIS web, dashboards territoriales, visores de datos abiertos o aplicaciones educativas basadas en mapas.
| Funcionalidad | Plugin o técnica | Qué aporta al visor |
|---|---|---|
| Pantalla completa | Leaflet.Fullscreen | Más espacio útil para explorar el mapa. |
| Búsqueda de direcciones | Leaflet.Control.Geocoder + Nominatim | Permite localizar calles, plazas y ubicaciones sin navegar manualmente. |
| Exportación PNG/PDF | html2canvas + jsPDF | Facilita guardar o compartir la vista actual del mapa. |
| Eventos de Leaflet | API de eventos | Permite reaccionar a movimientos, zoom, popups y cambios de capas. |
| POIs dinámicos | Overpass API | Carga puntos de interés según el área visible del mapa. |
| Clustering de marcadores | Leaflet.markercluster | Evita saturación visual cuando hay muchos puntos cercanos. |
Nota técnica
En esta guía hablaremos de geocodificación, búsqueda de direcciones y carga dinámica de POIs. No estamos usando todavía la API navigator.geolocation del navegador para detectar automáticamente la posición GPS del usuario.
Tabla de contenidos
Base técnica del visor Leaflet
Antes de añadir funcionalidades avanzadas conviene limpiar la base. Uno de los problemas habituales en tutoriales largos es repetir el mismo bloque HTML una y otra vez. Para mejorar el SEO, la legibilidad y la experiencia del lector, dejaremos un único index.html con todas las dependencias necesarias y después iremos incorporando el JavaScript por secciones.
index.html con las dependencias necesarias
Este fichero carga Leaflet, los plugins del tutorial, las librerías de exportación y el archivo script.js, donde quedará la lógica del mapa.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mapas interactivos con Leaflet</title>
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<!-- Leaflet.Fullscreen -->
<link rel="stylesheet" href="https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/leaflet.fullscreen.css">
<!-- Leaflet.Control.Geocoder -->
<link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css">
<!-- Leaflet.markercluster -->
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.Default.css">
<!-- Iconos para los controles personalizados -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
min-height: 100vh;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
#map {
width: 90vw;
height: 95vh;
}
</style>
</head>
<body>
<h1>Mapa interactivo con Leaflet</h1>
<div id="map"></div>
<!-- Leaflet -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Plugins -->
<script src="https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/Leaflet.fullscreen.min.js"></script>
<script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script>
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script>
<!-- Exportación -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<!-- Código del visor -->
<script src="script.js"></script>
</body>
</html>
script.js base del mapa interactivo
A partir de aquí trabajaremos sobre un mapa centrado en València, con una capa base de OpenStreetMap, una ortofoto PNOA, varias capas WMS y un marcador inicial. Esta base nos permite centrarnos en las funcionalidades avanzadas sin repetir configuración en cada apartado.
// 1. Capas base
const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors'
});
const pnoaLayer = L.tileLayer.wms('https://www.ign.es/wms-inspire/pnoa-ma?', {
layers: 'OI.OrthoimageCoverage',
format: 'image/jpeg',
transparent: false,
attribution: 'PNOA - © Instituto Geográfico Nacional de España'
});
// 2. Capas WMS de apoyo
const curvasNivelLayer = L.tileLayer.wms('https://servicios.idee.es/wms-inspire/mdt?', {
layers: 'EL.ContourLine',
format: 'image/png',
transparent: true,
attribution: '© Instituto Geográfico Nacional de España'
});
const ignBaseLayer = L.tileLayer.wms('https://www.ign.es/wms-inspire/ign-base?', {
layers: 'IGNBaseOrto',
format: 'image/png',
transparent: true,
opacity: 0.75,
attribution: '© Instituto Geográfico Nacional de España'
});
const catastroLayer = L.tileLayer.wms('https://ovc.catastro.meh.es/cartografia/INSPIRE/spadgcwms.aspx?', {
layers: 'BU.Building',
format: 'image/png',
transparent: true,
attribution: 'Catastro © Ministerio de Hacienda'
});
// 3. Crear el mapa
const map = L.map('map', {
center: [39.470239, -0.376805],
zoom: 13,
layers: [osmLayer]
});
// 4. Control de capas
const baseMaps = {
'OpenStreetMap': osmLayer,
'Ortofoto PNOA': pnoaLayer
};
const overlayMaps = {
'Callejero BASE IGN': ignBaseLayer,
'Curvas de nivel': curvasNivelLayer,
'Edificios del Catastro': catastroLayer
};
L.control.layers(baseMaps, overlayMaps).addTo(map);
// 5. Marcador inicial
const marker = L.marker([39.470239, -0.376805], {
title: "Plaça de l'Ajuntament",
alt: 'Marcador de la Plaça de l’Ajuntament',
draggable: false
}).addTo(map);
marker.bindPopup(`
<h3>Bienvenido a València</h3>
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/6/60/Plaza_del_Ayuntamiento_de_Valencia.JPG/640px-Plaza_del_Ayuntamiento_de_Valencia.JPG"
alt="Plaça de l'Ajuntament"
style="width:230px;"
>
<p>Esta es la Plaça de l'Ajuntament, el corazón de la ciudad.</p>
`);
Mejora aplicada al Gutenberg original
He reducido la repetición de bloques largos de código HTML. Esto facilita la lectura, mejora la escaneabilidad y evita que el usuario tenga que revisar varias veces el mismo index.html con pequeñas diferencias.
1. Añadir pantalla completa a un mapa interactivo con Leaflet
La primera mejora es sencilla, pero muy visible: permitir que el usuario active el modo pantalla completa. En mapas interactivos con Leaflet, esta funcionalidad mejora mucho la exploración visual, especialmente cuando hay capas WMS, marcadores o controles superpuestos.
Para ello usamos Leaflet.Fullscreen. Como el CSS y el JavaScript ya están incluidos en el index.html, en script.js solo necesitamos registrar el control sobre el mapa.
// Añadir el control de pantalla completa al mapa
map.addControl(new L.Control.Fullscreen());

Buenas prácticas
Prueba el modo pantalla completa en escritorio, tablet y móvil. En layouts complejos puede revelar problemas de altura, anchura o z-index que no aparecen cuando el mapa está embebido en vista normal.
2. Búsqueda de direcciones en Leaflet con Nominatim
Una de las funcionalidades más útiles en mapas interactivos con Leaflet es la búsqueda de direcciones. Sin un buscador, el usuario depende del zoom, el desplazamiento manual y su conocimiento previo del territorio. Eso puede valer para una demo, pero no para una herramienta de consulta real.
Un geocoder convierte una dirección, como “Plaça de l’Ajuntament 1, València”, en coordenadas geográficas. También puede hacer el proceso inverso: recibir una latitud y una longitud y devolver una dirección legible.
Por qué usar Nominatim en un prototipo Leaflet
En este ejemplo usaremos Nominatim, un servicio basado en datos de OpenStreetMap. Es una buena opción para aprender, prototipar y construir visores internos, aunque en producción conviene revisar cuotas, caché, límites de uso y alternativas gestionadas.
Importante
No conviene abusar del servicio público de Nominatim en aplicaciones con mucho tráfico. Para un producto real, plantea una instancia propia, caché de resultados o un proveedor preparado para carga de producción.
Implementar Leaflet.Control.Geocoder
Como el plugin Leaflet.Control.Geocoder ya está cargado en el HTML, podemos añadir el control directamente al mapa. En el ejemplo limitamos resultados a España, personalizamos el placeholder y ajustamos el mapa al bounding box de la ubicación seleccionada.
const geocoder = L.Control.geocoder({
defaultMarkGeocode: true,
placeholder: 'Buscar dirección...',
position: 'topleft',
geocoder: L.Control.Geocoder.nominatim({
serviceUrl: 'https://nominatim.openstreetmap.org',
geocodingQueryParams: {
countrycodes: 'es'
}
})
})
.on('markgeocode', function (event) {
const bbox = event.geocode.bbox;
const boundsPolygon = L.polygon([
bbox.getSouthEast(),
bbox.getNorthEast(),
bbox.getNorthWest(),
bbox.getSouthWest()
]);
map.fitBounds(boundsPolygon.getBounds());
})
.addTo(map);



Reverse geocoding al hacer clic en el mapa
Además de buscar una dirección, también podemos hacer reverse geocoding: el usuario hace clic en el mapa, enviamos las coordenadas a Nominatim y mostramos la dirección aproximada en un popup.
function reverseGeocode(lat, lng) {
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`;
fetch(url)
.then(response => response.json())
.then(data => {
const address = data.display_name || 'Dirección no disponible';
L.marker([lat, lng])
.addTo(map)
.bindPopup(`<strong>Ubicación:</strong><br>${address}`)
.openPopup();
})
.catch(error => {
console.error('Error en reverse geocoding:', error);
});
}
map.on('click', function (event) {
reverseGeocode(event.latlng.lat, event.latlng.lng);
});
3. Exportar mapas Leaflet a PNG y PDF
Exportar la vista actual del mapa puede ser útil en informes, presentaciones, revisión de incidencias, documentación interna o materiales docentes. En esta sección añadiremos dos controles personalizados: uno para generar una imagen PNG y otro para crear un PDF.
Límite técnico
La exportación depende de la captura del DOM. Algunas teselas, imágenes o capas externas pueden requerir configuración CORS adecuada. Si una capa bloquea la captura, el resultado puede salir incompleto o con zonas en blanco.
Exportar el mapa como imagen PNG
Para exportar a imagen usaremos html2canvas. El control personalizado se añade a la barra de Leaflet y captura el contenedor #map cuando el usuario hace clic.
const ExportPNGControl = L.Control.extend({
onAdd: function () {
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
const button = L.DomUtil.create('a', 'leaflet-bar-part', container);
button.href = '#';
button.title = 'Exportar mapa como PNG';
button.innerHTML = '<i class="fa-solid fa-file-export"></i>';
L.DomEvent.on(button, 'click', function (event) {
L.DomEvent.stopPropagation(event);
L.DomEvent.preventDefault(event);
exportMapAsPNG();
});
return container;
}
});
new ExportPNGControl({ position: 'topright' }).addTo(map);
function exportMapAsPNG() {
const mapElement = document.getElementById('map');
html2canvas(mapElement, {
useCORS: true,
logging: false
}).then(canvas => {
const link = document.createElement('a');
link.download = 'mapa-leaflet.png';
link.href = canvas.toDataURL('image/png');
link.click();
});
}


Exportar el mapa como documento PDF
Para generar PDF combinamos html2canvas con jsPDF. Primero capturamos el mapa como imagen y después insertamos esa imagen dentro de un documento PDF en orientación horizontal.
const PDFExportControl = L.Control.extend({
onAdd: function () {
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
const button = L.DomUtil.create('a', 'leaflet-bar-part', container);
button.href = '#';
button.title = 'Exportar mapa como PDF';
button.innerHTML = '<i class="fa-solid fa-file-pdf"></i>';
L.DomEvent.on(button, 'click', function (event) {
L.DomEvent.stopPropagation(event);
L.DomEvent.preventDefault(event);
exportMapAsPDF();
});
return container;
}
});
new PDFExportControl({ position: 'topright' }).addTo(map);
function exportMapAsPDF() {
const mapElement = document.getElementById('map');
html2canvas(mapElement, {
useCORS: true,
logging: false
}).then(canvas => {
const imageData = canvas.toDataURL('image/png');
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'px',
format: [canvas.width, canvas.height]
});
pdf.addImage(imageData, 'PNG', 0, 0, canvas.width, canvas.height);
pdf.save('mapa-leaflet.pdf');
});
}


4. Eventos de Leaflet para crear visores interactivos
Los eventos son la diferencia entre un mapa estático y un visor realmente interactivo. Leaflet permite reaccionar cuando el usuario mueve el mapa, cambia el zoom, activa capas, abre popups o hace clic sobre marcadores. En proyectos reales, esta parte suele conectar el mapa con filtros, paneles laterales, consultas a APIs o analítica de uso.
Eventos habituales en un mapa Leaflet
| Evento | Cuándo se dispara | Uso habitual |
|---|---|---|
moveend | Cuando termina el desplazamiento del mapa. | Recargar datos según el área visible. |
zoomend | Cuando termina un cambio de zoom. | Adaptar detalle, simbología o densidad de datos. |
click | Cuando el usuario hace clic en el mapa o en una capa. | Consultar coordenadas o abrir información contextual. |
popupopen | Cuando se abre un popup. | Cargar información adicional bajo demanda. |
layeradd / layerremove | Cuando se activa o desactiva una capa. | Actualizar leyendas, KPIs o estado de la interfaz. |
Ejemplos básicos de eventos en Leaflet
// Evento de carga
map.on('load', function () {
console.log('El mapa se ha cargado.');
});
// Evento al terminar de mover el mapa
map.on('moveend', function () {
console.log('Nuevo centro del mapa:', map.getCenter());
});
// Evento al terminar de cambiar el zoom
map.on('zoomend', function () {
console.log('Nivel de zoom actual:', map.getZoom());
});
// Evento al añadir o quitar capas
map.on('layeradd', function (event) {
console.log('Capa añadida:', event.layer);
});
map.on('layerremove', function (event) {
console.log('Capa eliminada:', event.layer);
});
Cargar información dinámica al abrir un popup
Un patrón muy útil consiste en abrir el popup con un estado inicial y cargar después información adicional. Esto evita pedir demasiados datos al cargar el mapa y mejora el rendimiento cuando hay muchos marcadores.
const demoMarker = L.marker([39.470239, -0.376805]).addTo(map);
demoMarker.bindPopup('Cargando información...');
demoMarker.on('popupopen', function (event) {
setTimeout(function () {
event.popup.setContent('Información cargada dinámicamente.<br>València, España');
}, 1000);
});
5. Carga dinámica de POIs desde OpenStreetMap con Overpass API
Una mejora clave en mapas interactivos con Leaflet es cargar puntos de interés según la zona visible. En lugar de descargar todos los datos al iniciar el visor, detectamos el área actual del mapa, calculamos su bounding box y consultamos una API externa.
Obtener el bounding box visible del mapa
El bounding box define el rectángulo geográfico visible: latitud y longitud mínima, latitud y longitud máxima. Lo usaremos para pedir a Overpass API únicamente los POIs que estén dentro de la vista actual.
function getBoundingBox() {
const bounds = map.getBounds();
return {
minLat: bounds.getSouthWest().lat,
minLng: bounds.getSouthWest().lng,
maxLat: bounds.getNorthEast().lat,
maxLng: bounds.getNorthEast().lng
};
}
map.on('moveend', function () {
loadPOIs();
});
Consultar restaurantes desde Overpass API
En este ejemplo recuperamos restaurantes de OpenStreetMap usando Overpass API. Puedes cambiar la etiqueta amenity=restaurant por otros tipos de POI según el caso de uso: centros educativos, farmacias, bibliotecas, aparcamientos o equipamientos públicos.
function loadPOIs() {
const bbox = getBoundingBox();
const query = `
[out:json][timeout:25];
(
node["amenity"="restaurant"](${bbox.minLat},${bbox.minLng},${bbox.maxLat},${bbox.maxLng});
);
out body;
>;
out skel qt;
`;
const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(query)}`;
fetch(url)
.then(response => response.json())
.then(data => processPOIs(data.elements))
.catch(error => {
console.error('Error al cargar POIs desde Overpass API:', error);
});
}

Procesar y mostrar POIs en el mapa
Una vez recibidos los datos, limpiamos la capa anterior y añadimos los nuevos marcadores. Este patrón evita que el mapa acumule puntos antiguos cuando el usuario se desplaza por otras zonas.
let poiMarkers = L.layerGroup().addTo(map);
function processPOIs(elements) {
poiMarkers.clearLayers();
elements.forEach(element => {
const lat = element.lat || element.center?.lat;
const lng = element.lon || element.center?.lng;
if (!lat || !lng) {
return;
}
const name = element.tags?.name || 'Restaurante sin nombre';
const street = element.tags?.['addr:street'] || '';
const number = element.tags?.['addr:housenumber'] || '';
const city = element.tags?.['addr:city'] || '';
const postcode = element.tags?.['addr:postcode'] || '';
const address = [street, number, postcode, city]
.filter(Boolean)
.join(' ');
const poiMarker = L.marker([lat, lng])
.bindPopup(`
<strong>${name}</strong><br>
${address || 'Dirección no disponible'}
`);
poiMarkers.addLayer(poiMarker);
});
}

Cuidado en producción
No lances consultas a Overpass API en cada pequeño movimiento del mapa sin control. Añade debounce, caché por bounding box, límites de zoom y mensajes de carga para evitar saturar la API y empeorar la experiencia del usuario.
6. Agrupar marcadores en Leaflet con MarkerCluster
Cuando un visor empieza a cargar muchos puntos, el mapa puede volverse ilegible. Ver decenas o cientos de marcadores superpuestos no ayuda al usuario a entender el territorio. Para resolverlo usamos Leaflet.markercluster, un plugin que agrupa marcadores cercanos y los despliega progresivamente al hacer zoom.
Implementar clustering sobre los POIs dinámicos
Como MarkerCluster ya está incluido en el HTML, solo tenemos que sustituir el L.layerGroup() por L.markerClusterGroup(). Así mantenemos la misma función processPOIs(), pero cambiamos el comportamiento visual del grupo.
let poiMarkers = L.markerClusterGroup();
map.addLayer(poiMarkers);
function processPOIs(elements) {
poiMarkers.clearLayers();
elements.forEach(element => {
const lat = element.lat || element.center?.lat;
const lng = element.lon || element.center?.lng;
if (!lat || !lng) {
return;
}
const name = element.tags?.name || 'Restaurante sin nombre';
const poiMarker = L.marker([lat, lng])
.bindPopup(`<strong>${name}</strong>`);
poiMarkers.addLayer(poiMarker);
});
}


Checklist técnico para mapas interactivos con Leaflet
Antes de dar por terminado el visor, conviene revisar algunos puntos que suelen marcar la diferencia entre una demo funcional y una herramienta mantenible.
- Comprueba que el mapa se adapta correctamente a escritorio, tablet y móvil.
- Valida que todos los plugins se cargan en el orden correcto.
- Revisa CORS si exportas teselas o imágenes externas a PNG/PDF.
- Añade límites de zoom antes de consultar APIs externas.
- Evita recargar POIs en cada movimiento sin debounce o caché.
- Centraliza las capas base, overlays y grupos de marcadores.
- Separa el código en módulos si el visor empieza a crecer.
- Incluye textos alternativos y capturas claras si el tutorial se publica en WordPress.
Conclusión: de demo a visor geoespacial avanzado
Con estas mejoras, el visor deja de ser un simple ejemplo de Leaflet y empieza a comportarse como una aplicación geoespacial más completa. Los mapas interactivos con Leaflet pueden incorporar búsqueda, capas externas, exportación, eventos, POIs dinámicos y clustering sin abandonar una arquitectura ligera basada en HTML, CSS y JavaScript.
El siguiente paso natural sería separar la lógica en módulos, añadir un panel lateral con filtros, cachear resultados de Overpass, persistir favoritos y migrar la interfaz a Vue, React o un framework similar si el proyecto necesita crecer.
Lectura recomendada
Si todavía no has leído la base del proyecto, empieza por la primera parte: crear mapas interactivos con Leaflet desde cero. Esta guía avanzada funciona mejor cuando ya tienes claro cómo inicializar el mapa, crear capas y añadir marcadores.
Preguntas frecuentes sobre mapas interactivos con Leaflet
¿Se pueden crear mapas interactivos con Leaflet sin usar Google Maps?
Sí. Leaflet permite crear mapas interactivos usando OpenStreetMap, capas WMS, GeoJSON, servicios de geocodificación como Nominatim y plugins de la comunidad.
¿Cómo se añade un buscador de direcciones en Leaflet?
Una forma habitual es utilizar Leaflet.Control.Geocoder con Nominatim. Este plugin añade un control de búsqueda al mapa y permite centrar la vista sobre la ubicación encontrada.
¿Cómo se agrupan muchos marcadores en Leaflet?
Para agrupar muchos marcadores puedes usar Leaflet.markercluster. El plugin agrupa puntos cercanos según el nivel de zoom y mejora la legibilidad del visor.
¿Puedo exportar un mapa Leaflet como imagen o PDF?
Sí. Puedes capturar el contenedor del mapa con html2canvas y generar una imagen PNG o un documento PDF con jsPDF, teniendo en cuenta las restricciones CORS de teselas y recursos externos.
Datos SEO recomendados para Yoast
| Campo | Valor recomendado |
|---|---|
| Frase clave objetivo | mapas interactivos con Leaflet |
| Título SEO | Mapas interactivos con Leaflet: guía avanzada paso a paso |
| Slug | Mantener el slug actual y no cambiarlo sin redirección 301. |
| Metadescripción | Mapas interactivos con Leaflet paso a paso: añade pantalla completa, geocoder con Nominatim, exportación PNG/PDF, eventos, POIs dinámicos y MarkerCluster. |


Deja una respuesta