Visores de mapas (parte 4): Leaflet

Pues llegamos a la última entrada de nuestra serie de visores de mapas OpenSource. Ya hemos visto CesiumJS y OpenLayers, y hoy veremos Leaflet.

Como comentamos al principio, Leaflet es una librería distribuida con licencia BSD-2 y orientada a representar mapas en dos dimensiones, lo que la asemeja bastante a la librería de OpenLayers.

Sus principales ventajas son su ligereza, su facilidad de uso y el buen resultado visual que genera, ya que de las tres librerías vistas, esta es la que mejor representa la simbología.

Aunque es una librería 2D, como en el caso de OpenLayers es posible hacer uso de ciertas extensiones que permite visualizar el mapa con una vista isométrica.

Una de estas extensiones es la de WRLD3D (antigua eegeo), la cual si bien no es Open Source, incluye un nivel de licencia de uso gratuito. Para proyectos sencillos puede venir bien.

Otra posibilidad es OSMBuildings, si bien mantiene una visualización 2D, incluye geometrías tridimensionales que generan el efecto de profundidad:

Usando la librería

Como en las librerías anteriores, tenemos distintas opciones para usar Leaflet:

  • Descargarla y/o instalarla.
  • Utilizar un entorno externo de programación en vivo.
  • Consumirla vía URL.

Descargarla y/o instalarla

A fecha de esta entrada, la versión de la librería es la 1.7.1, la cual podemos descargar desde la página web de Leaflet junto a sus estilos.

Una vez que la tengamos descargada en local, únicamente tenemos que enlazarla en nuestro HTML y podremos trabajar directamente con ella, sin necesidad de levantar entornos web como ocurría con Cesium.

Aquí, como veremos, hay varios archivos similares entre ellos. Los importantes son:

  • leaflet.js: el código JavaScript de la librería minificado.
  • leaflet-src.js: el código sin minificar, con ayudas para debubbing.
  • leaflet.css: la hoja de estilos de la librería.
  • images: la carpeta con las imágenes por defecto que utiliza Leaflet. Se debe localizar al mismo nivel que el archivo CSS.

Nota: aunque lo comentaremos otra vez más adelante, es muy importante incluir las hojas de estilo antes que la librería de JavaScript.

La otra posibilidad es la tirar de NPM, instalando las dependencias usando:

npm i leaflet

Entorno externo de programación en vivo

Si recordamos, en Cesium contábamos con un entorno propio de desarrollo en donde probar los códigos y ver los ejemplos, el denominado como Sandcastle.

Luego en OpenLayers, si bien no contábamos con un entorno propio, hacían uso de CodeSandbox para mostrar sus ejemplos y poder modificar el código.

En el caso de Leaflet no tenemos nada de esto; ni entorno propio, ni entorno recomendado ni nada. Por tanto, bien podemos utilizar tanto CodeSandbox como JSFiddle o cualquier otro IDE en línea que os guste para preparar visores de mapas con Leaflet, aunque sin los ejemplos o guías de las otras dos librerías.

Eso si, hay que decir que existe algún grupo en donde ver algunos ejemplos y llevar a cabo tus forks para enredar con los ejemplos, pero no es nada oficial y no se acerca a lo visto hasta ahora.

Consumir la librería vía URL

Como en casos anteriores, esta será la vía con la que vamos a trabajar. Para ello haremos uso de alguno de los CDN disponibles para Leaflet: unpkg, cdnjs o jsDelivr.

Como da un poco igual cual escojamos, para este tutorial usaremos el de cdnjs porque sus URLs son las más fáciles de copiar:

https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.js
https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.css

Entorno de trabajo

Aquí repetimos lo de las dos entradas anteriores; vamos a utilizar el CloudLab, el entorno gratuito de pruebas de la Onesait Platform, en donde podéis probar todas las capacidades de la Plataforma sin limitación.

El modus operandi será el de siempre; vamos a montar un Dashboard con un Gadget que incluya el mapa, para obtener algo similar a esto:

Si, aquí también parpadean las cosas.

Carga de las librerías

En el configurador del Dashboard, introduciremos las URLs de la hoja de estilos y librería de JavaScript (si no recordáis cómo lo hacíamos, en la parte 2 de esta serie lo explicábamos).

<link href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.css" rel="stylesheet">

<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.js"></script>

Igual que en los casos anteriores, vamos a utilizar un Gadget de tipo «Template», pudiendo preparar tanto el HTML y CSS de la parte Frontend del mapa, como la parte de JavaScript para preparar digamos el Backend del visor.

Generar el mapa de Leaflet

Vamos a empezar con lo bonito; a crear nuestro visor de mapa.

Siguiendo el esquema de los casos anteriores, en la parte de HTML vamos a crear un contenedor en la ventana de la izquierda con el siguiente nombre:

<div id="leafletContainer" class='leafletContainer'></div>

Para que nos quede lo más parecido a los otros mapas, vamos a generar también unos estilos:

<style>
  .leafletContainer {
    height: 100%;
    width: 100%;
  }
</style>

<div id="leafletContainer" class='leafletContainer'></div>

Hecho esto, pasaremos a generar el código JavaScript que genere el visor del mapa en la ventana de la derecha. Crearemos una variable llamada «viewer», que usaremos con el constructor del mapa de Leaflet de la siguiente forma:

//This function will be call once to init components
vm.initLiveComponent = function () {
    
    let viewer = L.map('leafletContainer')

}

Una vez que sincronicemos y compilemos el Gadget, se generará el visor del mapa. Sin embargo, aunque aparezca arriba a la izquierda los controladores de zoom, y abajo a la derecha la atribución de Leaflet, en general veréis todo de color gris y sin nada.

Esto se debe a que, como ocurría en el caso de OpenLayers, es preciso incluir la información de inicio de la cámara, así como alguna capa para visualizar, concretamente el mapa base para saber donde nos encontramos de una forma visual.

Para añadir el mapa base, haremos uso de la clase de «tileLayer()», añadiendo como parámetro de entrada la URL del servicio de mapa base de OpenStreetMap:

//This function will be call once to init components
vm.initLiveComponent = function () {

    let viewer = L.map('leafletContainer')

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(viewer)
};

Sincronizando y compilando nuevamente, vemos que todo sigue gris, y es porque como ya hemos comentado, es indispensable incluir las coordenadas y nivel de zoom de la vista inicial.

Esto lo conseguimos usando el método de «setView()». Como en casos anteriores, introduciremos las coordenadas para visualizar España.


//This function will be call once to init components
vm.initLiveComponent = function () {

    let viewer = L.map('leafletContainer')

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(viewer)

    viewer.setView([-3, 40], 5)
};

Pues ya está listo. Compilamos el código y ya tenemos nuestro visor enfocado a… ¿Kenia?

Pues si, las coordenadas [-3 , 40] hacen referencia a una localidad cercana a Mombasa porque en Leaflet, a diferencia de Cesium u OpenLayers, las coordenadas se introducen como latitud y longitud, y no longitud y latitud. Esto puede parecer una tontería, pero genera muchos problemas después si no lo tenemos en cuenta.

¿Soluciones? Pues tenemos algunas:

  • Modificar los datos originales, que en caso de que vengan de un servicio que se use en otros navegadores, generará un problema similar al actual en aquellos.
  • Parsear el objeto de coordenadas, para invertir el orden desde Front. No tocamos los datos originales, pero sigue siendo un poco guarro.
  • Hacer uso del método de clase GeoJSON.coordsToLatLng()

Este método, pensado para trabajar con un GeoJSON, parsea inteligentemente las coordenadas que le indiquemos, arreglando el problemilla que teníamos entre manos.

//This function will be call once to init components
vm.initLiveComponent = function () {
    let viewer = L.map('leafletContainer')

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: false,
    }).addTo(viewer)

    viewer.setView(L.GeoJSON.coordsToLatLng([-3, 40]), 5)

    setTimeout(() => {
        viewer.invalidateSize()
    }, 100)
}

Ahora si, nuestro visor enfoca al punto que nos interesaba desde el principio.

Como en el caso de OpenLayers, introducimos un timeout con una función que nos sirve para mantener el mapa ajustado al tamaño del Gadget. Esto si vamos a tener el mapa en una ventana estática no sería necesario, pero para el caso de los Gadgets del Dashboard, en los que solemos cambiar tamaños, es una buena práctica.

//This function will be call once to init components
vm.initLiveComponent = function () {

    let viewer = L.map('leafletContainer')

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: false,
    }).addTo(viewer)

    viewer.setView(L.GeoJSON.coordsToLatLng([-3, 40]), 5)

    setTimeout(() => {
        viewer.invalidateSize()
    }, 100)
}

Posibles configuraciones del visor del mapa

Por como está diseñado Leaflet, encontramos tropecientas opciones de configuración del objeto de mapa.

A cada parte que avanzamos, esto se ve peor. Pulsa en la imagen para ver las propiedades.

Como ocurría en OpenLayers, diferenciamos entre propiedades en general, controles e interacciones, así como animaciones, efectos de inercia de desplazamiento del mapa, navegación, opciones de rueda del ratón e incluso opciones táctiles.

Entre las propiedades en general más comunes encontramos:

  • crs: el sistema de coordenadas de referencia; o lo que es lo mismo, el sistema en el que estarán proyectados los datos. Este visor soporta los más comunes (ver más):
    • EPSG:4326: el utilizado por los GPS, considerando la totalidad de la superficie planetaria.
    • EPSG:3857: el denominado como Pseudo-Mercator, similar al anterior, pero con los polos recortados entre 85,06ºN y 85,06ºS.
    • EPSG:3395: un invento raro que parte también del 4326, pero que omite por fuera de 84ºN y 80ºS. Según la documentación, se utiliza para algunos proveedores de mapas base.
  • center: las coordenadas, en formato latitud y longitud, de la posición inicial del mapa. Esta es otra opción a la que hemos usado antes.
  • zoom: el nivel inicial de zoom. Pasa igual que en la opción previa.
  • minZoom: esta opción permite limitar el zoom mínimo del mapa.
  • maxZoom: esta opción, similar a la anterior, permite limitar el zoom máximo del mapa en este caso.
  • layers: este array permite precargar capas (el constructor se introduciría en esta opción).
  • maxBounds: con esto limitamos el desplazamiento lateral de la cámara al interior de ciertas coordenadas. Es una buena opción para limitar la vista a una zona en concreto (un país, una ciudad, etc.).
  • renderer: el método de renderizado que se usará, diferenciando entre el Canvas o en SVG.

En el caso de los controles, únicamente encontramos dos opciones:

  • attributionControl: para incluir la información de propiedad de los contenidos del mapa.
  • zoomControl: para mostrar u ocultar la botonera de zoom.

Sobre las interacciones, aquí hay varias, entre las que resalto las de:

  • closePopupOnClick: para cerrar o mantener abiertos los popups al hacer clic en el mapa.
  • doubleClickZoom: que activa o desactiva el hacer zoom al hacer doble clic sobre el mapa (o incluso con el dedo en pantallas).
  • dragging: esta opción controla el desplazamiento del mapa cuando hemos hecho clic y lo mantenemos sobre el mapa.

Como podemos ver, por defecto no tenemos herramientas de dibujado, pero existir existen. Así, encontramos un API de dibujado documentado en GitHub, con sus botoneras y eso.

Existen más opciones de configuración, pero digamos que las que hemos visto son las principales para ir tirando y hacer un visor de mapa práctico.

Cargar una capa

Parece que siempre tardamos en llegar a esta parte, pero aquí estamos. Una vez que tenemos el mapa funcionando y configurado a nuestro gusto, pasamos a meterle una capa vectorial de verdad.

Vamos a seguir reutilizando nuestro ejemplo de siempre; los puntos de interés:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "id": "Work"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [
          -3.641436696052551,
          40.529066557739924
        ]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "id": "Lunch"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [
          -3.6394304037094116,
          40.53058739857294
        ]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "id": "Beer"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [
          -3.639392852783203,
          40.530179669832364
        ]
      }
    }
  ]
}

Este GeoJSON lo añadiremos a nuestro código de JavaScript del Gadget como una variable, a la que llamaremos más adelante.

Como hemos comentado previamente, Leaflet es una librería bastante sencilla, por lo que añadir una capa es tan sencillo como utilizar el método de «GeoJson()»:

//This function will be call once to init components
vm.initLiveComponent = function () {

    let viewer = L.map('leafletContainer')

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: false,
    }).addTo(viewer)

    viewer.setView(L.GeoJSON.coordsToLatLng([-3, 40]), 5)

    const myJSON = {... aquí vendría el GeoJSON ...}

    let layer = L.geoJson(myJSON).addTo(viewer)

    setTimeout(() => {
        viewer.invalidateSize()
    }, 100)
}

Sencillo, ¿verdad? Además, que usando «GeoJson()» directamente nos aseguramos de que las coordenadas se incluyen correctamente.

Hecho esto, compilando el Gadget tendremos los puntos representados en el mapa.

Como es costumbre, los puntos se ven por donde caen, pero poco más. Así que vamos a hacer un poco de zoom para verlos mejor.

El haber definido la capa como una variable, nos permite interactuar con dicha capa directamente. Así, mediante el método de «fitBounds()» del visor del mapa, y el «getBounds()» de la capa, podemos hacer zoom directamente a la capa:

//This function will be call once to init components
vm.initLiveComponent = function () {

    let viewer = L.map('leafletContainer')

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: false,
    }).addTo(viewer)

    viewer.setView(L.GeoJSON.coordsToLatLng([-3, 40]), 5)

    const myJSON = {... aquí vendría el GeoJSON ...}

    let layer = L.geoJson(myJSON).addTo(viewer)    

    setTimeout(() => {
        viewer.invalidateSize()

        viewer.fitBounds(layer.getBounds())
    }, 100)
}

Incluimos el zoom a la capa dentro del timeout debido al mínimo pero existente tiempo necesario para generar la capa. Al igual que OpenLayers y a diferencia de Cesium, Leaflet no trabaja a base de promesas, por lo que nos tocaría generarlas por nuestra parte. Pero por ir a lo sencillo, usaremos esta forma.

Ahora si, ya visualizamos nuestra capa a un buen nivel de zoom, y encima vemos que los puntos se diferencian bien, a diferencia de lo que nos ocurría con OpenLayers y su simbología por defecto.

Pues ya estaría, sólo nos queda hacer parpadear a la capa de puntos, como en casos anteriores.

Sin embargo, en Leaflet el concepto es algo distinto. No se pueden ocultar las capas para luego volver a mostrarlas, sino que hay que modificar su opacidad o, como se suele hacer, eliminar la capa y volver a montarla.

En código, esto sería:

//This function will be call once to init components
vm.initLiveComponent = function () {

    let viewer = L.map('leafletContainer')

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: false,
    }).addTo(viewer)

    viewer.setView(L.GeoJSON.coordsToLatLng([-3, 40]), 5)

    const myJSON = {... aquí vendría el GeoJSON ...}

    let layer = L.geoJson(myJSON).addTo(viewer) 

    setTimeout(() => {
        viewer.invalidateSize()
        viewer.fitBounds(layer.getBounds())
    }, 100)

    setTimeout(() => {
        setInterval(() => {
            if (layer) {
                viewer.removeLayer(layer)
                layer = null
            } else {
                layer = L.geoJson(myJSON).addTo(viewer)
            }
        }, 1000);
    }, 100)
}

Nada mal, ¿eh? Como vemos, todo se puede hacer con todos los visores de mapas; únicamente hay que encontrar el modo correcto de hacerlo de cada uno.

No vamos a explicar cómo eliminar una capa, porque es precisamente así como conseguimos llevar a cabo este efecto visual de aparecer y desaparecer.

Para ir terminando, dejamos el código completo tanto del recuadro de HTML como el de JavaScript, por si queréis copiarlo y pegarlo en vuestro Gadget directamente y comprobar que, efectivamente, funciona.

HTML/CSS
<!-- Write your HTML <div></div> and CSS <style></style> here -->
<!--Focus here and F11 to full screen editor-->
<style>
  .leafletContainer {
    height: 100%;
    width: 100%;
  }
</style>

<div id="leafletContainer" class='leafletContainer'></div>
JavaScript
//Write your controller (JS code) code here
//Focus here and F11 to full screen editor

//This function will be call once to init components
vm.initLiveComponent = function () {

    window.viewer = L.map('leafletContainer')

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: false,
    }).addTo(viewer)

    viewer.setView(L.GeoJSON.coordsToLatLng([-3, 40]), 5)

    const myJSON = {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "properties": {
                    "id": "Work"
                },
                "geometry": {
                    "type": "Point",
                    "coordinates": [
                        -3.641436696052551,
                        40.529066557739924
                    ]
                }
            },
            {
                "type": "Feature",
                "properties": {
                    "id": "Lunch"
                },
                "geometry": {
                    "type": "Point",
                    "coordinates": [
                        -3.6394304037094116,
                        40.53058739857294
                    ]
                }
            },
            {
                "type": "Feature",
                "properties": {
                    "id": "Beer"
                },
                "geometry": {
                    "type": "Point",
                    "coordinates": [
                        -3.639392852783203,
                        40.530179669832364
                    ]
                }
            }
        ]
    }

    let layer = L.geoJson(myJSON).addTo(viewer)

    setTimeout(() => {
        viewer.invalidateSize()
        viewer.fitBounds(layer.getBounds())
    }, 100)

    setTimeout(() => {
        setInterval(() => {
            if (layer) {
                viewer.removeLayer(layer)
                layer = null
            } else {
                layer = L.geoJson(myJSON).addTo(viewer)
            }
        }, 1000);
    }, 100)
};

//This function will be call when data change. On first execution oldData will be null
vm.drawLiveComponent = function (newData, oldData) {

};

//This function will be call on element resize
vm.resizeEvent = function () {

}

//This function will be call when element is destroyed
vm.destroyLiveComponent = function () {

};

//This function will be call when receiving a value from vm.sendValue(idGadgetTarget,data)
vm.receiveValue = function (data) {

};

Pues con esto estaría todo; ya sabemos también cómo empezar a montar un mapa con Leaflet.


Con esto también damos por terminada esta serie sobre visores de mapas con librerías Open Source. Esperamos que os hayan parecido interesantes y que podáis sacarle partido.

Cualquier duda o problema que os surja, dejadnos un comentario y os responderemos tan pronto como nos sea posible.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *