Visores de mapas (parte 3): OpenLayers

Siguiendo la estela de la semana pasada, que os hablamos de la librería de CesiumJS como visor de mapas OpenSource, hoy vamos a hablar de la librería de OpenLayers.

Esta otra librería, distribuida con licencia BSD-2, se centra en mostrar la información geoespacial exclusivamente en 2D, aunque como veremos más adelante se le puede incluir un plugin para generar una visualización 3D.

El principal potencial de esta librería es el de poder representar las capas en infinidad de sistemas de proyección diferentes, algo muy práctico si trabajáis en una zona muy local.

Además, cuenta con una extensa galería de ejemplos para el visor, como elementos de interacción, carga de diferentes tipos de capas o hacer uso de otras librerías externas como turf.js, a la que hizo referencia un usuario del blog en los comentarios hace poco.

Usando la librería

Como en la librería anterior, tenemos distintas opciones para usar OpenLayers:

  • Descargarla y/o instalarla.
  • Utilizar el entorno de CodeSandbox.
  • Consumirla vía URL.

Descargarla y/o instalarla

La versión disponible a la hora de publicar esta entrada es la 6.5.0, la cual podéis descargar en dos versiones:

Su uso es tan sencillo como descomprimir la librería y estilos en una carpeta, generar un archivo HTML en donde enlazáis la librería y la hoja de estilos, y a correr.

Otra opción sería instalar la librería usando NPM:

npm install ol

Con esto se instalará la librería en la carpeta del proyecto que hayamos definido, pudiendo importar únicamente las clases que vayamos a utilizar en vez de la librería al completo, con lo que optimizamos nuestro código.

Entorno CodeSandbox

La página web de OpenLayers no ofrece un entorno de programación interactivo como tenía Cesium con su Sandcastle, por lo que los ejemplos que presenta son estáticos.

Sin embargo, existe la posibilidad de usar CodeSandBox, la herramienta de creación de prototipos e IDE instantánea, para modificar los ejemplos y ver los cambios realizados (como en nuestro editor de Visores GIS de la Plataforma).

Para ello, cuando estemos en un ejemplo que nos interese modificar, únicamente tendremos que pulsar en el botón de «Edit», situado en la parte superior derecha de la pantalla, para lanzar CodeSandBox (tenéis que tener habilitadas la apertura de pestañas/ventanas).

Ya luego, desde esta herramienta, podéis compartir o incrustar vuestros mapas donde os interese, o incluso descargaros un archivo ZIP con todo el código, para que hagáis que él lo que preciséis.

Consumir la librería vía URL

La última opción es hacer uso directamente de las URLs de la librería; en vez de enlazar a nuestra carpeta con la librería descargada, enlazaremos directamente con el repositorio de OpenLayers.

Como en el caso de los otros visores, esta será la opción que usaremos para este tutorial.

Entorno de trabajo

Para llevar a cabo este tutorial, vamos a hacer nuevamente uso del entorno web de CloudLab de la Onesait Platform, que ya sabéis que es gratuito y en el que podéis probar todas las capacidades de la Plataforma sin limitación.

Como la semana pasada, vamos a generar un Dashboard con un Gadget, el cual contendrá el mapa y en donde podremos ir experimentando de una manera rápida y sencilla.

Si todo sale bien, obtendremos un Dashboard tal que así:

Carga de las librerías

Como ya hemos dicho, vamos a utilizar la librería disponible desde el repositorio de OpenLayers, que corresponde con los estilos y las funcionalidades:

https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.5.0/css/ol.css
https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.5.0/build/ol.js

Generación del Dashboard y Preparación del Gadget

Los pasos para generar el Dashboard y el Gadget son los mismos que los que vimos en la entrada sobre CesiumJS, por lo que si os surge alguna duda de cómo había que hacerlo, consultad dicha entrada.

Aquí lo único que hay que tener en consideración es incluir las URLs correctas en la configuración del Dashboard:

<link href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.5.0/css/ol.css" rel="stylesheet">

<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.5.0/build/ol.js"></script>

Recordad también que haremos uso de un Gadget de tipo «Template», para programar tanto el Front del mapa con HTML y CSS, como la lógica del mismo con JavaScript.

Generar el mapa de OpenLayers

Una vez que tenemos todo preparado, es hora de ponernos manos a la obra a preparar nuestro primer mapa.

Empezaremos generando la parte visual del visor, creando un contenedor en la parte HTML (la de la izquierda, vamos). Éste lo denominaros de una manera parecida a como lo hicimos en la entrada anterior, para seguir unas formas:

<div id="openLayersContainer"></div>

También codificaremos un poco los estilos del contenedor, para que quede bonito y brillante. Concretamente definiremos una anchura y altura total del contenedor, para que nos ocupe por completo el área del Gadget:

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

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

Ahora, en la parte de JavaScript (la de la derecha), vamos a programar la parte lógica. Buscaremos la función de «vm.initLiveComponent», y en su interior crearemos una variable denominada «viewer», tal como os muestro a continuación:

//This function will be call once to init components
vm.initLiveComponent = function () {
    
    let viewer = new ol.Map({
        target: 'openLayersContainer',
        layers: [
            new ol.layer.Tile({
                source: new ol.source.OSM()
            })
        ],
        view: new ol.View({
            center: ol.proj.fromLonLat([-3, 40]),
            zoom: 5
        })
    })

}

Explicando un poco el código, lo que hacemos es definir una variable, denominada «viewer» que generará el mapa en el «div» que definamos como «target», que en este caso será «openLayersContainer».

A este mapa le cargaremos una capa inicial, correspondiente al mapa base de OpenStreetMaps. Cada capa creada irá en el array de «layers», pudiendo incluirse una capa como tal o el constructor de la capa (como es en este caso).

Por último, definimos una vista inicial mediante la propiedad de «view», indicando donde queremos que enfoque el mapa («center»), indicando una longitud y una latitud, y a qué altitud («zoom») queremos que esté la cámara.

A diferencia de Cesium, que presenta un preset de propiedades, en OpenLayers hay que introducir estas propiedades en el constructor del visor, ya que sino no veremos nada en pantalla. Estas tres propiedades son las mínimas, pero como veremos hay muchas otras que se pueden incluir.

Introducidas estas propiedades, si sincronizáis y compiláis el Gadget, se os debería de ver así (dependiendo del tamaño de la pantalla, claro):

En el caso particular de esta representación en los Dashboads, es necesario introducir cierto código para asegurarnos que en todo momento el contendor del mapa se adapta al tamaño de la pantalla.

Así, tras definir el «viewer» introduciremos este código:



    setTimeout(function () { viewer.updateSize() }, 100)

Con esto nos aseguramos que el visor carga perfectamente, independientemente del tamaño de la pantalla del navegador ni los cambios que realicemos.

Posibles configuraciones del visor del mapa

Como ya he comentado, a la hora de crear el visor hay que introducir unas propiedades mínimas, pero existen más, si bien no tantas como encontrábamos en Cesium:

Pincha sobre la imagen para ver el listado, que aquí se sigue sin ver nada.

Las propiedades más importantes son las que ya hemos definido, por lo que sólo haré mención a un par que suelen ser útiles por temas de optimización:

  • pixelRatio: dicho rápidamente, la resolución que queremos que tenga el mapa (los dips, vamos).
  • maxTilesLoading: el número de baldosas/losetas que se cargarán a la vez (las imágenes cuadradas del mapa base, por ejemplo).

Aparte, hay otras opciones que si bien no son propiedades como tal, son útiles de conocer. Me refiero concretamente a las de «controls», «interactions» y «overlays».

Los controles se refieren a aquellos elementos del UI del mapa con los que interactuamos. Esta opción agrupa el array de los controles cargados en el mapa, diferenciando entre:

  • Attribution: con la información de atribución de las capas y contenidos del mapa.
  • FullScreen: la opción que nos permite visualizar el mapa a pantalla completa.
  • MousePosition: que muestra las coordenadas correspondientes a la posición del ratón sobre el mapa. Esto se suele usar mucho.
  • OverviewMap: este controlador crea un minimapa en donde se muestra la posición del visor principal.
  • Rotate: esta opción permite girar la orientación del mapa (por defecto apunta al norte).
  • ScaleLine: la escala gráfica del mapa. Tiene una opción que permite mostrar la escala numérica.
  • ZoomSlider: este control activa una barra de selección para modificar el zoom del mapa.
  • ZoomToExtent: activa un botón que, una vez pulsado, mueve la cámara del mapa a la posición y zoom definida en los parámetros de entrada.
  • Zoom: gestiona los botones de zoom (+) y zoom (-) del mapa.
Ejemplos de barras de zoom.

Por otro lado, las interacciones hacen referencia a aquellas funcionalidades que generan modificaciones en el mapa. Aquí nos encontramos con:

  • DoubleClickZoom: al hacer doble clic con el ratón en un punto, se habilita la opción de hacer zoom (+).
  • DragAndDrop: esta opción permite detectar cuando algo se ha arrastrado sobre el mapa y se ha soltado dentro. Un ejemplo corriente de esto es arrastrar un GeoJSON sobre el mapa, y recuperarlo con esta interacción.
  • KeyboardPan: esto permitiría desplazar la vista el mapa usando las flechas del teclado.
  • KeyboardZoom: similar a lo anterior, pero para modificar el zoom de la cámara de vista.
  • MouseWheelZoom: por defecto, moviendo la rueda del ratón ampliamos o disminuimos el zoom; con esta interacción podemos cambiar el comportamiento de la rueda.
  • PointerInteraction: esta interacción recibe la respuesta de los eventos de «pinchar» con el ratón, moverlo o «soltarlo».
  • Select: para seleccionar elementos del mapa y recuperar su información.
Dibujar geometrías es un tipo de interacción.

Por último, nos queda comentar las opciones de superposición, que como por su propio nombre se entiende, es lo que se superpone en el mapa, como puede ser una ventana emergente. Aquí no encontramos diferentes tipos, sino que cualquier elemento HTML que configuremos será una superposición.

Un popup genérico con información sobre la posición donde he clicado (una interacción).

Cargar una capa

Ahora que conocemos de qué va esto de OpenLayers y cómo podemos personalizarlo, vamos a lo interesante, a cargar una capa con datos.

Como en el caso anterior, vamos a hacer uso del formato GeoJSON, utilizando el mismo ejemplo:

{
  "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 JSON lo cargaremos añadiendo las siguientes líneas de código:

//This function will be call once to init components
vm.initLiveComponent = function () {
    
    let viewer = new ol.Map({
        target: 'openLayersContainer',
        layers: [
            new ol.layer.Tile({
                source: new ol.source.OSM()
            })
        ],
        view: new ol.View({
            center: ol.proj.fromLonLat([-3, 40]),
            zoom: 5
        })
    })

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

    let vectorSource = new ol.source.Vector({
        features: new ol.format.GeoJSON().readFeatures(myJSON, {
            featureProjection: 'EPSG:3857',
        }),
    })

    let vectorLayer = new ol.layer.Vector({
        source: vectorSource
    })

    viewer.addLayer(vectorLayer)

}

Aquí lo que hacemos es, en primer lugar, generar una fuente de datos con el constructor «ol.source.Vector()», pasando como entidades el GeoJSON y especificando su sistema de proyección.

Una vez generada la fuente de datos, creamos una capa vectorial con ella mediante «ol.layer.Vector()».

Ya por último, hacemos uso del método de carga de capas «addLayer()» del visor de mapas, usando la capa vectorial creada previamente.

Sincronizando y compilando el Gadget, debería de aparecernos en pantalla:

Estas tres líneas de código se podrían simplificar en una sola, pero creo que es mejor ir poco a poco para entenderlo mejor. También tenéis que recordar que si cargáis el GeoJSON desde un servicio, tendréis que considerar hacer algo asíncrono para que no os falle.

Como en el caso anterior, tenemos la capa cargada, pero apenas se ve, debido a las condiciones de zoom inicial definidas en el visor. ¿Podemos hacer zoom a la capa recién cargada? Por supuesto.

Para ello, en primer lugar obtendremos la extensión de la capa de puntos -esto es, el recuadro que contiene todas las entidades-, y luego haremos zoom a dicha extensión.

El código, que deberíamos incluir tras la función de añadir capa, sería el siguiente:

let layerExtent = vectorLayer.getSource().getExtent()
viewer.getView().fit(layerExtent)
La simbología por defecto no se ve muy bien, siendo sinceros.

Bueno, pues ya tendríamos ahí nuestros puntos de interés sobre el mapa; poco visibles, pero ahí están. La simbología es posible cambiarla, pero no es algo que vayamos a tratar en estos tutoriales.

Si recordáis de la entrada de Cesium, lo que hicimos ahora fue cambiar la propiedad de visibilidad de la capa, haciéndola parpadear cada segundo. Pues bien, aquí también podemos hacerlo.

El código sería el siguiente:

    setTimeout(function () {
        setInterval(() => {
            // Check if the show property is true
            if (vectorLayer.getVisible()) {
                vectorLayer.setVisible(false)
            } else {
                vectorLayer.setVisible(true)
            }
        }, 1000);
    }, 100)

Como notaréis, hay semejanzas y diferencias con respecto al caso del otro visor.

OpenLayers no crea sus capas usando promesas, tal como lo hace Cesium, por lo que sería necesario generar la promesa asíncrona por nuestra cuenta. Para evitarnos todo eso para el ejemplo, he generado un «timeout» que nos haga la función de promesa resuelta.

Por otro lado, el obtener la visibilidad de la capa es extremadamente sencillo, ya que OpenLayers incluye un montón de funcionalidades que hacen que no sea necesario irse hasta la propiedad en concreto para leer su valor, sino simplemente hacer la llamada correspondiente.

Además, para cambiar el valor de dicha propiedad hay que usar la funcionalidad de «set», ya que si lo hacemos directamente, no funcionará.

Bien, pues tenemos la capa cargada, ahora parpadeando, y el último punto será el quitarla el mapa. Igual que en todos los casos (como veis los visores, en general, funcionan todos igual), contamos con el método de «removeLayer()».

Usado con nuestra capa, y repitiendo lo de darle tres segundos antes de eliminarla, sería así:

    setTimeout(() => {
        viewer.removeLayer(vectorLayer)
    }, 3000);

Pues con esto llegamos al final de nuestro pequeño tutorial acerca de cómo empezar a usar OpenLayers. Como veis, no tiene mucho más misterio que conocer las URLs de las librerías y los métodos principales, ya que el resto es muy similar a lo ya visto.

Os dejo el código JavaScript completo que he ido generando, por si preferís hacer un Ctrl C/Ctrl V directamente:

//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 () {

    let viewer = new ol.Map({
        target: 'openLayersContainer',
        layers: [
            new ol.layer.Tile({
                source: new ol.source.OSM()
            })
        ],
        view: new ol.View({
            center: ol.proj.fromLonLat([-3, 40]),
            zoom: 5
        })
    });

    setTimeout(function () {
        viewer.updateSize()
    }, 100)

    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 vectorSource = new ol.source.Vector({
        features: new ol.format.GeoJSON().readFeatures(myJSON, {
            featureProjection: 'EPSG:3857',
        }),
    })

    let vectorLayer = new ol.layer.Vector({
        source: vectorSource
    })

    viewer.addLayer(vectorLayer)

    let layerExtent = vectorLayer.getSource().getExtent();
    viewer.getView().fit(layerExtent);

    setTimeout(function () {
        setInterval(() => {
            if (vectorLayer.getVisible()) {
                vectorLayer.setVisible(false)
            } else {
                vectorLayer.setVisible(true)
            }
        }, 1000);
    }, 100)

    // Uncomment this code and comment the above one to remove the layer after three seconds
    // setTimeout(() => {
    //     viewer.removeLayer(vectorLayer)
    // }, 3000);

}

//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) { }

Esperamos que os haya resultado interesante la entrada, y nos vemos la semana que viene para hablar de Leaflet, el tercero de nuestros visores de mapas Open Source.

Si algo no os ha quedado claro, o queréis añadir alguna cosa más, por favor dejadnos un comentario.

2 Comments

Deja una respuesta

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