Saltar a contenido

Mas allá de la capa base, añadiendo mis propios datos

Autores

Datos vectoriales

Creamos el esqueleto del visor con una capa base del PNOA

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <title>GeoJSON y Leaflet</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css">
    <style>
        html,
        body,
        .map {
            height: 100%;
            margin: 0px;
        }

    </style>
</head>
<body>
    <div id="map" class="map"></div>
    <script src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
    <script>
        const map = L.map('map').setView([42.2, -8.8], 12);
        const PNOA = L.tileLayer.wms('http://www.ign.es/wms-inspire/pnoa-ma?', {
            layers: 'OI.OrthoimageCoverage',
            attribution: 'PNOA cedido por © <a href="http://www.ign.es/ign/main/index.do" target="_blank">Instituto Geográfico Nacional de España</a>'
        }).addTo(map);

    </script>
</body>
</html>

Cargamos un GeoJSON externo mediante script

Al contrario que OpenLayers Leaflet no tiene métodos "nativos" para acceder a una url externa que represente un GeoJSON. La persona que desarrolle el mapa es la responsable de pasar el contenido del GeoJSON a Leaflet.

Una de las formas más sencillas de hacer esto, es "convertir" el fichero GeoJSON en un fichero "javascript"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
vigo_geojson = 
    {
        "type": "FeatureCollection",
        "name": "vigo",
        "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
        "features": [
        { "type": "Feature", 
            "properties": { "id": 1, "Nombre": "Vigo", "Poblacion": 500000 }, 
            "geometry": { "type": "Point", "coordinates": [ -8.733678516914864, 42.219577180236797 ] } 
        }
    }

y cargarlo como un script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <title>GeoJSON y Leaflet</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css">
    <style>
        html,
        body,
        .map {
            height: 100%;
            margin: 0px;
        }

    </style>
</head>
<body>
    <div id="map" class="map"></div>
    <script src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
    <script src="vigo_js.geojson"></script>
    <script>
        const PNOA = L.tileLayer.wms('http://www.ign.es/wms-inspire/pnoa-ma?', {
            layers: 'OI.OrthoimageCoverage',
            attribution: 'PNOA cedido por © <a href="http://www.ign.es/ign/main/index.do" target="_blank">Instituto Geográfico Nacional de España</a>'
        });
        const vigo = L.geoJSON(vigo_geojson);
        const map = L.map('map', {
            layers: [
                PNOA,
                vigo
            ]
        }).setView([42.2, -8.8], 12);

    </script>
</body>
</html>

Leaflet, GeoJSON con geometrías "Punto", y los Marker

Otro punto que suele sorprender a la gente que se introduce en Leaflet, es como trata esta librería las features GeoJSON con una geometría de tipo Point.

Leaflet las interpreta por defecto como un objeto de tipo L.Marker. Que en la práctica podemos considerar como iconos. Y por tanto su opción principal de estilo es cambiar el icono que empleamos para mostrarla.

Prueba a eliminar la llamada L.geoJSON del código y añadir un marcador simple al mapa en las mismas coordenadas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <title>GeoJSON y Leaflet</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css">
    <style>
        html,
        body,
        .map {
            height: 100%;
            margin: 0px;
        }

    </style>
</head>
<body>
    <div id="map" class="map"></div>
    <script src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
    <script src="vigo_js.geojson"></script>
    <script>
        const map = L.map('map').setView([42.2, -8.8], 12);
        const PNOA = L.tileLayer.wms('http://www.ign.es/wms-inspire/pnoa-ma?', {
            layers: 'OI.OrthoimageCoverage',
            attribution: 'PNOA cedido por © <a href="http://www.ign.es/ign/main/index.do" target="_blank">Instituto Geográfico Nacional de España</a>'
        }).addTo(map);
        // L.geoJSON(vigo_geojson).addTo(map);
        L.marker([-8.733678516914864, 42.219577180236797]).addTo(map);

    </script>
</body>
</html>

Atención

¿No aparece el marcador? ¿Por qué será?

Tratemos de cambiar un poco el marcador por defecto. Si buscas iconos para tus mapas un buen punto para empezar es la librería Maki de Mapbox.

Fijate en como hemos usado el className para poder usar css para modificar el estilo del elemento img con el Leaflet incrusta el icono en el mapa

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <title>GeoJSON y Leaflet</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css">
    <style>
        html,
        body,
        .map {
            height: 100%;
            margin: 0px;
        }
        .town-icon {
            background-color: white;
        }

    </style>
</head>
<body>
    <div id="map" class="map"></div>
    <script src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
    <script src="vigo_js.geojson"></script>
    <script>
        const map = L.map('map').setView([42.2, -8.8], 12);
        const PNOA = L.tileLayer.wms('http://www.ign.es/wms-inspire/pnoa-ma?', {
            layers: 'OI.OrthoimageCoverage',
            attribution: 'PNOA cedido por © <a href="http://www.ign.es/ign/main/index.do" target="_blank">Instituto Geográfico Nacional de España</a>'
        }).addTo(map);
        L.marker([42.21, -8.73], {
            icon: L.icon({
                iconUrl: 'town-15.svg', // cuidado con las rutas
                iconSize: [45, 45],
                className: 'town-icon'
            })
        }).addTo(map);

    </script>
</body>
</html>

Representar los Point como CircleMarker

Si queremos cambiar el ícono de nuestros puntos geojson, o usar otro tipo de representación "vectorial" para ellos como L.CircleMarker es un pelín más complicado.

Leaflet proporciona un hook pointToLayer que se puede pasar en la creación de la capa geoJSON, esa función será llamada para cada feature de tipo punto de la capa.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <title>GeoJSON y Leaflet</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css">
    <style>
        html,
        body,
        .map {
            height: 100%;
            margin: 0px;
        }

    </style>
</head>
<body>
    <div id="map" class="map"></div>
    <script src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
    <script src="vigo_js.geojson"></script>
    <script>
        const map = L.map('map').setView([42.2, -8.8], 12);
        let PNOA = L.tileLayer.wms('http://www.ign.es/wms-inspire/pnoa-ma?', {
            layers: 'OI.OrthoimageCoverage',
            attribution: 'PNOA cedido por © <a href="http://www.ign.es/ign/main/index.do" target="_blank">Instituto Geográfico Nacional de España</a>'
        }).addTo(map);
        const geojsonMarkerOptions = {
            // Stroke
            color: 'black',
            weight: 2,
            opacity: 1,
            // Fill
            fillColor: 'red',
            fillOpacity: 1,
            // Radius
            radius: 30,
        };
        L.geoJSON(vigo_geojson, {
            pointToLayer: function(feature, latlng) {
                return L.circleMarker(latlng, geojsonMarkerOptions);
            }
        }).addTo(map);

    </script>
</body>
</html>

¿Y si quiero aplicar una simbología distinta en función de una variable alfnumérica? El tutorial de Leaflet lo explica, las funciones style, pointToLayer y onEachFeature son tus amigas. ¡Pruebalo!

Obtener un GeoJSON por "url"

Hasta ahora estábamos cargando el GeoJSON como un script, pero es habitual que estos datos provengan de un servicio externo o queramos cargarlos mediante llamadas asíncronas.

Simplemente debemos recuperarlos de la forma que estamos acostumbrados por ejemplo $.getJSON o con fetch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <title>GeoJSON y Leaflet</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css">
    <style>
        html,
        body,
        .map {
            height: 100%;
            margin: 0px;
        }

    </style>
</head>
<body>
    <div id="map" class="map"></div>
    <script src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
    <script>
        const map = L.map('map').setView([42.2, -8.8], 12);
        const PNOA = L.tileLayer.wms('http://www.ign.es/wms-inspire/pnoa-ma?', {
            layers: 'OI.OrthoimageCoverage',
            attribution: 'PNOA cedido por © <a href="http://www.ign.es/ign/main/index.do" target="_blank">Instituto Geográfico Nacional de España</a>'
        }).addTo(map);
        const geojsonMarkerOptions = {
            // Stroke
            color: 'black',
            weight: 2,
            opacity: 1,
            // Fill
            fillColor: 'red',
            fillOpacity: 1,
            // Radius
            radius: 30,
        };
        fetch('vigo.geojson').then(response => response.json()).then(vigo_geojson => {
            L.geoJSON(vigo_geojson, {
                pointToLayer: function(feature, latlng) {
                    return L.circleMarker(latlng, geojsonMarkerOptions);
                }
            }).addTo(map);
        });

    </script>
</body>
</html>

El formato GeoJSON

El Servicio WFS, Web Feature Service

Como explicamos al hablar de GeoJSON Leaflet no proporciona un método nativo para hacer peticiones ajax a este tipo de recursos.

Así que debemos obtener todas las features en la carga inicial, descargarlas dinámicamente mediante código a medida o usar algún tipo de plugin.

En este tutorial puedes ver como usar un plugin de WFS.

Este otro plugin permite la carga dinámica de GeoJSON en función de los movimientos del usuario por el mapa cacheando la respuesta. No es específico de WFS pero encaja bien en lo que hace OpenLayers.

En este tutorial vamos a hacer una implementación con código a medida muy simple a efectos de entender que es lo que está sucediendo. Hemos complicado un poco algunas partes del código, simplemente por entender la complejidad que puede llegar a tener trabajar con sistemas de coordenadas y la construcción de peticiones a servicios WFS.

En primer lugar construimos nuestro mapa como es habitual, pero en este caso añadiremos una capa GeoJSON vacía que rellenaremos en cada movimiento del usuario con nuevos datos.

Primero construiremos una URL base con la que hacer las peticiones al servicio WFS. Podríamos hacerlo simplemente concatenando y substituyendo strings o usando alguna utilidad de Leaflet como hacen en este ejemplo. Usaremos "es6".

Para construír nuestra url necesitamos obtener los límites (bounds) de nuestro mapa. Aquí la cosa se complica un poco. Leaflet usa por defecto el EPSG:3857 (coordenadas proyectadas) pero las coordenadas que introducimos o nos devuelven los métodos son geográficas. Si hacemos una petición a un servicio WFS usando como parámetro srsname: 'EPSG:3857', las coordenadas del bbox deben ir en proyectadas y no en geográficas. Por lo que debemos "transformar" las coordenadas

El orden concreto en que se deben especificar las coordenadas en un parámetro BBOX puede depender del protocolo, la versión, el EPSG y la implementación concreta del software. Probablemente lo mejor sea probar. Para el caso que nos ocupa, al parámetro BBox del WFS dice:

As for which corners of the bounding box to specify, the only requirement is for a bottom corner (left or rightto be provided first. For example, bottom left and top right, or bottom right and top left.)

es decir en Geoserver se le pasa primero una cualquiera de las esquinas inferior y luego una superior.

El orden de las coordenadas en cada punto para EPSG:3857 es este, norte. Y además en el parámetro de bbox especificamos en que CRS estamos pasando las coordenadas para evitar errores.

Después pondremos un listener moveend, que escuche la interacción del usuario con el mapa, pan y zoom, y pida los nuevos datos tras cada movimiento.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <title>WFS y Leaflet</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css">
    <style>
        html,
        body,
        .map {
            height: 100%;
            margin: 0px;
        }

    </style>
</head>
<body>
    <div id="map" class="map"></div>
    <script src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
    <script>
        let map = L.map('map').setView([42.2, -8.8], 12);
        let pnoaOrthoWMS = L.tileLayer.wms('http://www.ign.es/wms-inspire/pnoa-ma?', {
            layers: 'OI.OrthoimageCoverage',
            attribution: 'PNOA cedido por © <a href="http://www.ign.es/ign/main/index.do" target="_blank">Instituto Geográfico Nacional de España</a>'
        }).addTo(map);
        const geojsonLayer = L.geoJSON();
        map.addLayer(geojsonLayer);
        var baseURL = 'https://cors-anywhere.herokuapp.com/http://ideadif.adif.es/gservices/Tramificacion/wfs'
        let defaultParameters = {
            service: 'WFS',
            version: '1.1.0',
            request: 'GetFeature',
            typename: 'Tramificacion:TramosFueraServicio',
            outputFormat: 'application/json',
            srsname: 'EPSG:3857',
        }
        const buildURL = function(bbox) {
            const params = Object.assign({}, {
                bbox: bbox
            }, defaultParameters);
            const urlParams = new URLSearchParams(params);
            const url = new URL(baseURL);
            for (let pair of urlParams.entries()) {
                url.searchParams.set(pair[0], pair[1])
            }
            return url;
        }
        const getBoundsIn3857 = function() {
            const sm = L.Projection.SphericalMercator;
            const bounds = map.getBounds();
            const southWest = sm.project(bounds.getSouthWest());
            const northEast = sm.project(bounds.getNorthEast());
            const northWest = sm.project(bounds.getNorthWest());
            const southEast = sm.project(bounds.getSouthEast());
            return {
                southWest,
                northEast,
                northWest,
                southEast,
            }
        }
        const getBBoxStringForGeoserver3857WithMapIn3857 = function() {
            const bounds = getBoundsIn3857();
            const southWest_East = bounds.southWest.x;
            const southWest_North = bounds.southWest.y;
            const northEast_East = bounds.northEast.x;
            const northEast_North = bounds.northEast.y;
            const bbox = `${southWest_East},${southWest_North},${northEast_East},${northEast_North},EPSG:3857`;
            return bbox;
        }
        const onMoveEnd = function() {
            const bbox = getBBoxStringForGeoserver3857WithMapIn3857();
            const url = buildURL(bbox);
            fetch(url).then(response => response.json()).then(json => {
                geojsonLayer.addData(json);
            });
        }
        map.on('moveend', onMoveEnd);
        onMoveEnd();

    </script>
</body>
</html>

Atención

Vale, y después de todo esto, porqué no veo nada. ¡DuckDuckGo¡ es tu amigo.

Leaflet, GeoJSON y el CRS

A pesar de todo lo dicho, el estándar GeoJSON dice que las capas deben ir en el CRS EPSG:4326, y por defecto Leaflet espera que las coordenadas vengan en este sistema.

Cuando hacemos una petición al WFS indicándole otro sistema de coordenadas (EPSG:3857), Leaflet las interpreta de forma incorrecta al ser coordenadas proyectadas.

Buf. Vale. Pero y entonces que hago:

  • Si tu capa base admite el sistema EPSG:4326, la mayoría de los WMS lo harán, puedes usar ese CRS por defecto en Leaflet y todo es muy sencillo
  • Si como capa base quieres usar un servicios de teselas típico como OSM o Google, vendrán siempre en 3857, por tanto tendrás que pedir las capas WFS en 4326 o reproyectar al vuelo mediante alguna librería.

Hagamos lo primero en este ejemplo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <title>WFS y Leaflet</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css">
    <style>
        html,
        body,
        .map {
            height: 100%;
            margin: 0px;
        }

    </style>
</head>
<body>
    <div id="map" class="map"></div>
    <script src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
    <script>
        let map = L.map('map', {
            center: [42.2, -8.8],
            zoom: 12,
            crs: L.CRS.EPSG4326,
        });
        let pnoaOrthoWMS = L.tileLayer.wms('http://www.ign.es/wms-inspire/pnoa-ma?', {
            layers: 'OI.OrthoimageCoverage',
            attribution: 'PNOA cedido por © <a href="http://www.ign.es/ign/main/index.do" target="_blank">Instituto Geográfico Nacional de España</a>'
        }).addTo(map);
        const geojsonLayer = L.geoJSON(null, {
            style: {
                weigth: 2,
                color: 'red',
            }
        });
        map.addLayer(geojsonLayer);
        var baseURL = 'https://cors-anywhere.herokuapp.com/http://ideadif.adif.es/gservices/Tramificacion/wfs'
        let defaultParameters = {
            service: 'WFS',
            version: '1.1.0',
            request: 'GetFeature',
            typename: 'Tramificacion:TramosFueraServicio',
            outputFormat: 'application/json',
            srsname: 'EPSG:4326',
        }
        const buildURL = function(bbox) {
            const params = Object.assign({}, {
                bbox: bbox
            }, defaultParameters);
            const urlParams = new URLSearchParams(params);
            const url = new URL(baseURL);
            for (let pair of urlParams.entries()) {
                url.searchParams.set(pair[0], pair[1])
            }
            return url;
        }
        const getBBoxStringForGeoserver4326WithMapIn4326 = function() {
            return map.getBounds().toBBoxString() + ',EPSG:4326';
        }
        const onMoveEnd = function() {
            const bbox = getBBoxStringForGeoserver4326WithMapIn4326();
            console.log(bbox);
            const url = buildURL(bbox);
            fetch(url).then(response => response.json()).then(json => {
                geojsonLayer.addData(json);
            });
        }
        map.on('moveend', onMoveEnd);
        onMoveEnd();

    </script>
</body>
</html>

Si quieres continuar practicando:

  • Prueba a hacer las peticiones a un servicio WFS sobre un mapa base de OSM
  • Cachea y gestiona adecuadamente las llamadas
  • En lugar de hacer peticiones inmediatamente tras cada movimiento, espera a que el usuario acabe