Como crear un mapa animado con Leaflet

27 de Marzo de 2017

El objetivo de este post es de describir en detalle todas las etapas de un pequeño proyecto de mapa animado, desde la idea inicial hasta el resultado final pasando por la recuperación y la edición de los datos.

tl;dr: el resultado final es accesible aqui (hacer clic en el botón "Play" en el rincón arriba a la derecha para iniciar la animación). La página esta en inglés porque hice una traducción de este post en francés aquí y tenía flojera de hacer una versión para cada idioma.

Y para los más motivados, empezamos con las explicaciones detalladas!

NB: Como mencionado, esta inspirado de una dataviz hecha, entre otros, por Mike Bostrock (el creador de D3.js)  cuando trabajaba en el New York Times. 

La idea

Simplemente, quería realizar una aplicación con Leaflet representando datos temporales. Es decir, cuando hacemos clic en un botón "Play", pasa algo y el mapa se "anima".

La primera etapa fue encontrar los datos que me permitirían realizar eso. Inmediatamente he pensando en los eventos deportivos porque pensé que los datos serían más limpios (los participantes salen al mismo tiempo y siguen más o menos el mismo recorrido). Además, para que el resultado sea más interesante, quería tener más de un individuo en el mapa. Primero he buscado sobre las páginas de carreras tipo maratón, pensando que algunos eventos publicaban los datos de los mejores participantes pero no he hallado nada.

Y finalmente encontré este sitio, que publica los datos de la America\'s Cup del 2013. La página que había encontrado en la época ya no existe, pero los datos todavía están en Google Drive, aquí.

Los datos

El archivo AC.zip (101 Mo) contiene a priori todos los datos de la competición del 2013. He trabajado únicamente con los datos de la final entre un barco americano y un barco neozelandés (archivo 130925.zip). Una vez dezipado, los datos se encuentran en la carpeta "csv". Usaremos los 2 archivos siguientes:

 Los archivos tienen la misma estructura y contienien 24 columnas. Guardamos solamente 5:

Llamo mis 2 archivos "us.csv" y "nz.csv". A continuación, las 10 primeras líneas del archivo "us.csv". Si nos firamos en la primera columna, vemos que tenemos datos cada 0.2 segundos.

Tabla PostgreSQL

Los archivos iniciales no tenían exactamente el mismo número de líneas, así que manualmente he borrado algunas líneas (es decir segundos) que no estaban comunes para tener el mismo número de datos en ambos archivos.

El código 1/2 : estructuración de los datos

Todas las consultas SQL a continuación han sido ejecutadas con PostgreSQL/Postgis, versiones respectivas 9.3 y 2.1.1.

Vamos a ver aquí únicamente las consultas que corresponden a los datos del barco americano, pero obviamente hay que hacer lo mismo con los datos del barco neozelandés.

Primero, importamos los datos CSV en una tabla llamada "data_us":

Ahora vamos a trabajar los datos para tener para cada instante el porcentaje de la carrera recorrido en una tabla PostGIS. Luego, exportaremos esta tabla al formato GeoJSON para poder manipularla con Leaflet.

Los datos no incluyen únicamente la carrera en si mismo, pero también un periodo anterior (calentamiento) y posterior (después de la carrera). Como solo queremos calcular la distancia recorrida durante la carrera, debemos añadir una columna para diferenciar estos 3 periodos (estas informaciones se encuentran en el archivo "130925\csv\20130925130025_events.csv"):

La consulta para obtener los datos que queremos es bastante complicada, así que lo haremos por etapas (cada vez, la etapa anterior esta incrustada en la etapa pendiente).

Primero, debemos convertir las coordenadas en formato texto al formato geography. Para eso, usamos la función PostGIS ST_GeogFromText. Además, para cada punto, queremos también el punto anterior para poder calcular la distancia entre ellos (lo haremos en la etapa siguiente). Entonces utilizamos la función lag (una de las funciones window de PostgreSQL, quienes son muy potentes y práticas). Y como hay demasiados datos (cada 0.2 segundos), haremos una selección para guardar solamente datos cada 3 segundos:

Con eso, podemos ahora calcular la distancia en metros entre 2 puntos vecinos usando la función ST_distancia. COALESCE sirve para poner la distancia nula (0) al primer punto que no tiene punto anterior:

Ahora, calculamos para cada punto la distancia combinada (es decir desde el inicio), que llamamos \'cumul\' pero solamente durante la carrera (section = \'race\'):

Ya tenemos la distancia recorrida para cada instante. Para exprimir eso en porcentaje, debemos conocer la distancia total recorrida durante la carrera. Para obtenerla, calculamos el máximo de la columna \'cumul\' a partir de la consulta anterior (SELECT max(cumul) FROM (...).  Este valor es de 22106.229818404 metros para el barco US y de 22766.524924026 metros para el barco NZ.

Calculamos ahora el porcentaje recorrido que llamamos \'run\'. Esa vez creamos una nueva tabla (boat_us) que va a contener el tiempo en segundos, llamado \'time\' (empezando a 0, entonces hay que sustraer 46833 a \'secs\'), el porcentaje recorrido \'run\', la dirección del viento, la velocidad del viento y la geometría del punto:

A este nivel, el porcentaje recorrido \'run\' se queda a cero durante el calentamiento, luego aumenta progresivamente durante la carrera hasta culminar a 100% en la llegada. Una vez la carrera terminada, baja en 0 (porque hicimos el cálculo únicamente sobre section=\'race\'). Vamos a cambiar eso  para que el valor de \'run\' se quede a 100 una vez la carrera terminada. 2274 corresponde al tiempo de llegada en segundos del barco US (para el barco NZ, es 2316) :

Vemos las 10 primeras líneas de la tabla \'boat_us\':

Datos finales

Como lo he mencionado arriba, hay que hacer lo mismo con el barco NZ. Pero para tener un archivo plus ligero solamente necesitamos guardar las columnas \'run\' y \'geog\' (porque usaremos las columnas \'time\', \'wind_dir\' y \'wind_speed\' de la capa \'boat_us\').

Nuestras 2 nuevas tablas son tablas PostGIS, así que podemos abrirlas en QGIS:

Las capas en QGIS

En QGIS, guardamos las 2 capas al formato GeoJSON, manteniendo para las coordenadas solamente 5 cifras después de la coma (reduce el tamaño del archivo y la precisión es casi la misma). Llamamos los archivos "boat_us.geojson" y "boat_nz.geojson".

Última manipulación, abrimos los 2 archivos con nuestro editor de texto y añadimos al inicio \'var boat_us =\' y \'var boat_nz =\' respectivamente, y guardamos. Eso nos permitirá, dentro del código  JavaScript, tener acceso a los datos, llamando las variables \'boat_us\' y \'boat_nz\'.

Así es el inicio del archivo "boat_us.geojson" en un editor de texto:

Los datos en un editor de texto

Ya está, nuestros datos estan formateados como los queremos. Podemos pasar a la etapa de desarrollo del mapa animado.

El código 2/2 : realización del mapa animado

Además de Leaflet, usaremos JQuery y Bootstrap (que no son indispensables pero facilitan las cosas) con las versiones siguientes:

Empezamos con establecer la estructura de nuestra página HTML. Importamos primero el CSS. El archivo "style.css" en la línea 8 contiene nuestras reglas CSS personales. Luego las 3 capas de la línea 29 hasta 31 (contienen los datos del barco US, NZ y les boyas "boats.geojson").

Finalmente entre las etiquetas <script></script> (de la línea 32 hasta 42) vamos a escribir el código JS. Para empezar creamos el objeto map centrado en nuestra zona de interés y agregamos un fondo de mapa:

Pueden consultar el resultado aquí.

Nuestra estructura HTML ya está lista y no vamos a modificarla hasta el final. El trabajo va a consistir en añadir código JS entre las líneas 41 y 42 actuales.

Empezamos la animación con representar el recorrido progresivo del barco US. Para eso, uso window.requestAnimationFrame (unos artículos interesantes acerca de las animaciones en el navegador).

Creamos primero una polylínea (polyline_us) a partir de los 2 primeros puntos de la capa \'boat_us\'. Luego iteramos sobre los puntos siguientes para agregarlos uno por uno a polyline_us:

Pueden consultar el resultado aquí. Cool, no?

Se debe mencionar que si consultamos un otro tab del navegador durante la animación, ella se detiene y vuelve a empezar cuando volvemos a consular el tab. Es una de las ventajas de window.requestAnimationFrame.

Próxima etapa, añadir el barco NZ y a cada iteración guardar solamente los 10 puntos anteriores para dar una mejor ilusión de movimiento y de velocidad (creamos la función draw). Se reemplaza entonces el código anterior con:

Pueden consultar el resultado aquí.

Mucho mejor, pero no tenemos el control de la animación, empieza sola y no se puede hacer pausa o volver a empezar. Tenemos que resolver eso agregando un control Leaflet que va a contener un botón con 3 estados:

También modificamos el estilo de los barcos en movimiento agregando arriba de las líneas unos puntos representando la posición actual de los barcos:

Pueden consultar el resultado aquí. Hay que hacer clic en botón \'Play\' en el rincón arriba a la derecha del mapa.

Ya hicimos lo más difícil. Solo queda agregar la capa de las boyas y algunos controls.

Veamos primero como añadir el control que permite hacer un zoom sobre los 2 barcos y seguirlos. Y cuando hacemos clic de nuevo en el botón, el zoom vuelve al nivel de zoom inicial. Después del control "button_animation", creamos el control "button_tracker":

Ahora debemos modificar la función draw para que cambie el centro del mapa para cada iteración en el caso en el cual el usuario hizo clic sobre el botón traker. Para eso, agregamos al final de la función el código siguiente:

Pueden consultar el resultado aquí.

No voy a detallar aquí el código de los 2 otros controls, el que contiene la velocidad y la dirección del viento y el mostrando el tiempo y el porcentaje recorrido. Pueden consultar el código final. Se debe mencionar que actualizo estos controls cada 6 iteraciones (porque si no es demasiado rápido y no se lee muy bien). Para el primer control, hay que usar las propiedades "wind_speed" y "wind_dir" de la capa "boat_us". Para el segundo, "time" y "run".

A continuación, agregamos las boyas y las líneas que conectan algunas de ellas y los labels.

De la línea 1 hasta 27 declaramos las líneas, sus estilo y luego las agregamos al mapa.

De la línea 29 hasta 42 declaramos el estilo de las boyas y agregamos la capa (la variable floats que esta en el archivo "floats.geojson").

Finalmente, agregamos los labels asociados a las líneas:

Pueden consultar el resultado aquí.

Ya está, agregando los 2 controls que faltan así que los tooltips sobre los 2 controls/botones parar indicar al usuario para que sirven, obtenemos la versión final.

Pueden descargar los diferentes archivos aquí (código y datos, pero no contiene JQuery, Bootstrap o Leaflet).

Twittear