24 de agosto de 2025
En el post anterior vimos cómo crear un mapa web básico. Ahora vamos a dar un salto cualitativo añadiendo funcionalidades avanzadas: marcadores personalizados desde GeoJSON, capas WMS y un control para que el usuario pueda elegir qué mapa base visualizar.
Para este ejemplo, definiremos los datos directamente en una variable de JavaScript. En una aplicación real, estos datos vendrían normalmente de una API o un fichero .geojson
.
Además de las capas de teselas habituales (llamadas capas TMS o XYZ), Leaflet puede consumir servicios WMS (Web Map Service). Estos servicios son un estándar muy común en el mundo de los SIG. Para este ejemplo, usaremos el servicio de ortofotos del Plan Nacional de Ortofotografía Aérea (PNOA) de España. Se hace a través de L.tileLayer.wms
:
var pnoa = L.tileLayer.wms('http://www.ign.es/wms-inspire/pnoa-ma', {
layers: 'OI.OrthoimageCoverage',
format: 'image/jpeg',
transparent: true,
attribution: 'PNOA cedido por © <a href="http://www.ign.es/ign/main/index.do">Instituto Geográfico Nacional</a>'
});
Una de las formas más potentes de personalizar los marcadores es usando L.divIcon
. A diferencia de L.icon
que usa una imagen, divIcon
nos permite usar código HTML y CSS, lo que abre la puerta a usar librerías de iconos como Font Awesome.
El proceso combina la lógica de JavaScript con los estilos de CSS.
En la parte de JavaScript, dentro de la función pointToLayer
, decidimos qué icono y qué estilo aplicar a cada punto. Usamos un switch
para elegir una clase de Font Awesome (ej: fa-tree
) y una clase CSS propia (ej: parque
) basándonos en las propiedades de los datos. Luego, pasamos estas clases al divIcon
:
var customIcon = L.divIcon({
className: 'custom-marker ' + markerColorClass, // Clases para el CSS
html: `<i class="fas ${iconClass}"></i>`, // HTML con el icono de Font Awesome
iconSize: [30, 30]
});
A continuación, en el CSS, definimos los estilos. Tenemos una clase base, .custom-marker
, que crea el círculo contenedor del icono. Luego, las clases específicas (.playa
, .parque
, .faro
) establecen el color tanto del borde del círculo como del propio icono de Font Awesome que contiene.
.custom-marker {
background-color: rgba(255, 255, 255, 0.9);
border: 2px solid #333;
border-radius: 50%;
/* ... más estilos ... */
}
.custom-marker.playa { border-color: #00aaff; color: #00aaff; }
.custom-marker.parque { border-color: #28a745; color: #28a745; }
De esta forma, el HTML y el CSS trabajan juntos para crear un marcador totalmente personalizado.
Para permitir al usuario cambiar entre diferentes mapas base y activar o desactivar capas de datos, usamos L.control.layers
.
var baseMaps = {
"Mapa": carto,
"Ortofoto": pnoa
};
var overlayMaps = {
"Puntos de interés": markers
};
L.control.layers(baseMaps, overlayMaps).addTo(map);
El siguiente código HTML es un ejemplo completo y autocontenido.
<!DOCTYPE html>
<html>
<head>
<title>Leaflet Avanzado: WMS y Control de Capas</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<style>
html, body { height: 100%; margin: 0; padding: 0; display: flex; flex-direction: column; }
h1 { text-align: center; padding: 10px 0; margin: 0; }
#map { flex-grow: 1; }
.custom-marker {
background-color: rgba(255, 255, 255, 0.9);
border: 2px solid #333;
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
font-size: 16px;
}
.custom-marker.playa { border-color: #00aaff; color: #00aaff; }
.custom-marker.parque { border-color: #28a745; color: #28a745; }
.custom-marker.faro { border-color: #ffc107; color: #ffc107; }
</style>
</head>
<body>
<h1>Leaflet Avanzado: WMS y Control de Capas</h1>
<div id="map"></div>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script>
// 1. Definir capas base
var carto = L.tileLayer(
'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
{
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>'
}
);
var pnoa = L.tileLayer.wms('http://www.ign.es/wms-inspire/pnoa-ma', {
layers: 'OI.OrthoimageCoverage',
format: 'image/jpeg',
transparent: true,
attribution: 'PNOA cedido por © <a href="http://www.ign.es/ign/main/index.do">IGN</a>'
});
// 2. Inicializar el mapa
var map = L.map('map', {
center: [43.35, -8.36],
zoom: 13,
layers: [carto]
});
// 3. Datos GeoJSON
var geojsonData = {
"type": "FeatureCollection",
"name": "oleiros_puntos",
"features": [
{
"type": "Feature",
"properties": { "name": "Playa de Santa Cristina", "tipo": "Playa" },
"geometry": { "type": "Point", "coordinates": [-8.378631, 43.339596] }
},
{
"type": "Feature",
"properties": { "name": "Parque de José Martí", "tipo": "Parque" },
"geometry": { "type": "Point", "coordinates": [-8.378567, 43.336728] }
},
{
"type": "Feature",
"properties": { "name": "Faro de Mera", "tipo": "Faro" },
"geometry": { "type": "Point", "coordinates": [-8.354416, 43.383347] }
},
{
"type": "Feature",
"properties": { "name": "Playa de Bastiagueiro", "tipo": "Playa" },
"geometry": { "type": "Point", "coordinates": [-8.362489, 43.340981] }
}
]
};
// 4. Capa de marcadores personalizados
var markers = L.geoJSON(geojsonData, {
pointToLayer: function (feature, latlng) {
var iconClass, markerColorClass = '';
switch (feature.properties.tipo) {
case 'Playa':
iconClass = 'fa-umbrella-beach';
markerColorClass = 'playa';
break;
case 'Parque':
iconClass = 'fa-tree';
markerColorClass = 'parque';
break;
case 'Faro':
iconClass = 'fa-lightbulb';
markerColorClass = 'faro';
break;
default:
iconClass = 'fa-map-marker-alt';
break;
}
var customIcon = L.divIcon({
className: 'custom-marker ' + markerColorClass,
html: `<i class="fas ${iconClass}"></i>`,
iconSize: [30, 30]
});
var popupText = '<strong>' + feature.properties.name + '</strong>' +
'<br>Tipo: ' + feature.properties.tipo;
return L.marker(latlng, { icon: customIcon }).bindPopup(popupText);
}
}).addTo(map);
// 5. Configurar el control de capas
var baseMaps = {
"Mapa": carto,
"Ortofoto": pnoa
};
var overlayMaps = {
"Puntos de interés": markers
};
L.control.layers(baseMaps, overlayMaps).addTo(map);
// 6. Ajustar el zoom a los marcadores
map.fitBounds(markers.getBounds());
</script>
</body>
</html>
Un detalle importante a tener en cuenta es el orden de las coordenadas. Si te has dado cuenta el orden de las coordenadas es diferente cuando definimos el centro del mapa en leaflet, respecto de como se definen en el geojson. No existe un estándar único en el mundo de la información geográfica, lo que a menudo causa errores inesperados.
L.mapsetView()
o L.marker()
, generalmente espera el formato [latitud, longitud]
.[longitud, latitud]
.Esta inconsistencia es habitual entre diferentes herramientas, formatos y estándares del mundo SIG. Afortunadamente, en este caso, la función L.geoJSON()
de Leaflet es lo suficientemente inteligente como para leer correctamente las coordenadas en el formato estándar de GeoJSON sin que tengamos que hacer ninguna conversión manual. Para quien quiera profundizar más en este curioso problema, el artículo Lon, lat is the right way de Tom MacWright lo explica de maravilla.