Leaflet.js avanzado: GeoJSON, WMS y selector de capas

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.

Definiendo nuestros datos GeoJSON

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.

Añadiendo capas WMS

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>'
});

Marcadores personalizados con HTML, CSS y Font Awesome

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.

Creando un selector de capas

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);

Ejemplo completo

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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <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>

Bonus final: Latitud y Longitud, el eterno debate

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.

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.

← Volver al blog