Bosques de Europa
Representando +500,000 árboles con Google Maps y Deck.gl
Ya sea por el requerimiento de un proyecto o explotar los límites de Google Maps, una de las dificultades más grandesque tenemos como desarrolladores al utilizar esta herramienta es la necesidad y capacidad de poder desplegar miles ocientos de miles de puntos dentro de un mapa personalizado. ¿Es realmente posible?.
Google Maps y su API ha ido evolucionado a lo largo de los últimos años y con ello ha traído conceptos nuevos tales como `Custom Overlays`,los cuales permiten extender la capacidad de los mapas que generamos y brindan la oportunidad a proyectos como deck.gl de integrar suspropios desarrollos.
En este artículo me gustaría contar mi experiencia para lograr representar 588,983 árboles dentro de Google Maps, así que te invito a tomar tu bebida favoritamientras lees este post (☕|🥤|🍺|🍷)
Contenido
- La inspiración
- Eligiendo el problema
- Sistemas de coordenadas, más que latitud y longitud
- Obteniendo el color de los árboles
- Y ahora ¿Qué es eso deck.gl?
- Integrando deck.gl y Google Maps
- Conclusiones
- Referencias
La inspiración
El año pasado tuve la oportunidad de presenciar en dos ocasiones una plática de Alex Muramoto
titulada "Impresionantes visualizaciones de datos web con Google Maps Platform y deck.gl" si no tuviste la oportunidad de
asistir a algunos de los DevFest en los que estuvo presente puedes encontrar una grabación de su plática en este link: https://bit.ly/alexsgmapdeckgl.
Durante la charla Alex nos muestra diferentes ejemplos de cómo extender la capacidad de Google Maps con Deck.gl, nos brinda las bases para crear
mapas personalizados más allá del marcador que estamos
acostumbrados a utilizar, podemos agregar puntos, arcos, y visualizaciones animadas en tiempo real sobre segmentos del mapa, así que después
de un tiempo ocupado por cuestiones laborales, me anime a realizar un experimento con estas tecnologías.
Eligiendo el problema
Investigando encontré diferentes sets de datos bastantes completos en páginas como Paris Data o el Portal Europeo de Datos y también diferentes artículos científicos
que abordaban el tema de visualización geoespacial, uno de estos ellos llamó en especial mi atención, "EU-Forest, a high-resolution tree occurrence dataset for Europe" 1,
el artículo trata, sobre la curación digital de información para lograr generar un dataset que permita hacer pública la distribución de
europea de especies arbóreas, nos brindan el dataset completo utilizado en su publicación, el sistema de coordenadas utilizado y nos muestra diferentes
resultados de visualización.

Así que tomando estos puntos en cuenta, resulta bastante interesante replicar el experimento dentro de Google Maps y Deck.gl.
Sistemas de coordenadas, más que latitud y longitud

Existen diferentes archivos dentro de la publicación, pero el que tomaremos2 como base para la visualización de datos, es aquel que se encuentra
clasificado y curado respecto a las especies de los árboles, dicho archivo
se encuentra en formato CSV y como podemos leer en los metadatos, la representación
de los valores X y Y se encuentran dados en coordenadas de la referencia ETRS89-LAEA
🤨, ooook!.
La mayoría de las librerías o mapas que integramos dentro de nuestro código regularmente, nos han enseñado a utilizar y hacer referencia a las posiciones
con los valores de latitud y longitud, así que el primer reto que se presentó al procesar el dataset fue entender y transformar
estos valores a uno que haga sentido al sistema utilizado en Deck.gl o Google Maps.
Haciendo una pequeña búsqueda en Google, descubrí que el valor ETRS89-LAEA
también tiene un equivalente: EPSG:3035
.
EPSG
El EPSG es la abreviatura correspondiente a "European Petroleum Survey Group", este grupo se encargó de generar diferentes sets de datos,
para poder tener referencias espaciales que pudieran ser de aplicación global, regional, nacional o local, así que si quisiéramos
hacer referencia o uso a un dataset que utilice latitud y longitud como valores tendríamos que buscar el código EPSG correspondiente.
De los primeros recursos que encontré y que permitían buscar estas referencias de una manera sencilla, es la página epsg.io,
incluso dentro de la misma existe un servicio que nos permite hacer la transformación entre sistemas utilizando un formulario.

Inspeccionado el código, me doy cuenta que utiliza una librería llamada, pro4JS! Bingo! es lo que necesito para comenzar a convertir los datos 😁, además gracias a
su formulario también descubro que las coordenadas tradicionales entran dentro del código EPSG:4326
.
proj4JS
pro4JS, es un proyecto Open Source basado en proJ, el cual permite convertir coordenadas entre diferentes sistemas,
al ser de un nicho tecnológico muy específico, la documentación puede resultar en ocasiones escasa y comprender del todo los cálculos que se efectúan, puede resultar difícil
si no estás inmerso en el tema o lo usas de manera habitual. Aun así, el uso de pro4JS parece simple al inicio, se puede instalar vía npm
o yarn
, y la documentación en Github
nos indica que su uso es el siguiente:
proj4(fromProjection[, toProjection, coordinates])
Los valores que necesitaremos hacen referencia a las proyecciones 3, una proyección es la operación
de coordenadas que permitirán obtener la equivalencia de cada uno de los sistemas,
dentro del proyecto proJ
existe una lista y explicación más detallada de esta parte, en otra ocasión escribiré como se forman los valores de cada las proyecciones. Mientras tanto,
el ejemplo del valor necesario para definir el código EPSG:3035
es el siguiente:
[
'EPSG:3035',
'+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs ',
];
Resulta un poco complicado al inicio, comprender u obtener los valores necesarios para la proyección, Razón por la cual generé un repositorio con una
lista en Javascript que permite
obtener los valores usando como índice el valor EPSG. La lista contiene 6137 valores, que se pueden utilizar en las conversiones
y de la que además se puede leer la lista completa en el archivo README del proyecto,
para utilizarla la lista con proj4 el código se escribiría de la siguiente manera:
const proj4 = require("proj4")
const proj4list = require("./list.min");
proj4.defs([
proj4list["EPSG:3035"],
proj4list["EPSG:4326"]
);
proj4("EPSG:3035", "EPSG:4326", [ 4305500, 2674500 ]);
Con la capacidad de convertir cada uno de los valores de X a Y, ya podemos crear un dataset con las coordenadas con los valores
de latitud y longitud, nuestro script para convertir el CSV a un JSON seguiría entonces los siguientes pasos:
- 📃 Leer cada fila del archivo CSV original.
- 🗺 Convertir X y Y a latitud y longitud.
- 🌲 Sustituir la especie por un valor numérico para poder ser mapeado más adelante, esas especies las almacenaremos en un archivo por separado.
De esta manera, podremos generar un dataset con la siguiente estructura:
[
{
"specie": 70,
"lat": 35.01966518543305,
"lng": 32.6269824790667
}, ...
]
Si desean hacer uso de los dataset, pueden encontrar la información clasificada también por países, dentro del siguiente repositorio :
Datasets : EU-Forest
Obteniendo el color de los árboles
La visualización que necesitamos, podría funcionar solo con la posición de cada uno de los puntos de los árboles,
podríamos generar directamente la gráfica como la imagen presentada en el artículo. Como usuarios que consumen información,
el representar los datos de manera correcta, genera una mejor experiencia y entendimiento de lo que estamos viendo en nuestros monitores.
Así que con esa premisa, comencemos a trabajar en esta parte 🤓.
Para resolver esto, la idea es obtener un par de imágenes a partir de Google Search Images, unirlas y obtener el color dominante en las imágenes fusionadas.
Encontré la librería images-scraper para usarla con node.js, que nos permite obtener un Array con las rutas de imágenes a partir de una query utilizando Puppeteer.
const scraper = require('images-scraper');
const google = new scraper({ puppeteer: { headless: true } });
function searchAndAnalize(term, limit) {
/*
results [
{
url: 'https://...',
source: 'https://...',
description: '...'
},
...
]
*/
return new Promise(async (resolve, reject) => {
const results = await google.scrape(term, limit);
});
}
Por cada elemento en el Array, descargaremos cada uno de los archivos, construiremos una función que nos permite almacenar los archivos
en un directorio, y a su vez, permita alamacenar las rutas en otro Array para ser procesado más adelante. La función a través de request
se encarga de mandar una petición y procesar la respuesta con fs
.
const fs = require('fs');
const request = require('request');
async function downloadImage(url) {
try {
return new Promise((resolve, reject) => {
request.head(url, function (error, res, body) {
if (error) {
reject(error);
}
let extension =
res.headers['content-type'] === 'image/jpeg' ? 'jpg' : 'png';
let file_name = `${collectionPath}/${new Date().getTime()}.${extension}`;
request(url)
.pipe(fs.createWriteStream(file_name))
.on('close', () => {
resolve(file_name);
});
});
});
} catch (error) {
console.log('error in', url);
}
}
Con esta función elaborada, podemos descargar las imágenes y unirlas utilizando merge-img, ya que no controlamos las dimensiones de la imagen seleccionaremos PNG
como output para poder ser analizado.
function searchAndAnalize(term, limit) {
return new Promise(async (resolve, reject) => {
const results = await google.scrape(term, limit);
let images = await Promise.all(
results.map(async (result) => {
return await downloadImage(result.url);
})
);
let img = await mergeImg(images);
const output = `${collectionPath}/result_${new Date().getTime()}.png`;
img.write(`${output}`, async () => {
//AQUI VAMOS ANALIZAR LA IMAGEN
});
});
}
El resultado de cada imagen creada, será muy parecida a la siguiente:

Ahora utilizaremos Color Thief, una librería bastante popular para obtener los colores promedio con Javascript,
lo único que tenemos que asignar como parámetro es la imagen a analizar.
img.write(`${output}`, async () => {
resolve(await colorThief.getColor(output));
});
Nuevamente generé un repositorio completo con esta funcionalidad, pueden encontrarlo aquí.
Instalando todas las dependencias y siguiendo las instrucciones podemos ejecutar un comando en nuestra terminal que funciona de la siguiente
manera:
node example.js palette "Batman" 10
Los valores correspondientes a los colores significativos que produce este script, se puede encontrar en el siguiente
archivo,
en este se encuentran las 242 especies de árboles procesadas.
Y ahora ¿Qué es eso deck.gl?
Deck.gl es una de las herramientas Open source, que está cambiando la manera en la que podemos visualizar datos dentro
de mapas, creada por el equipo de visualización de Uber, este Framework a través del uso de
WebGL2 y que por consecuencia, hace un mejor uso de nuestro hardware, permite explorar la información de grandes conjuntos de datos mediante el renderizando y
superposición de capas dentro de nuestro navegador, permite al desarrollador la personalización de capas existentes o añadir nuevas en caso de ser necesario.
Primeros pasos con deck.gl
Ok, ya entendimos que deck.gl funciona a base de capas, ahora hagamos uso de las mismas para generar una visualización.
Una de las características que me gusta de deck.gl, paradójicamente es su independencia del sistema de mapas que prefieras, puede ser utilizado
con tecnologías como Google Maps, Mapbox, Arcgis, e inclusive puede sin nada como que sirva como marco.
Para hacer este primer acercamiento a deck.gl usaremos dos de los dataset creados por geojson.xyz.
Podemos utilizar deck.gl en diferentes formas, una de ellas es integrando directamente el script dentro de nuestro HTML.
<script
type="text/javascript"
src="https://unpkg.com/deck.gl@latest/dist.min.js"
></script>
Cuando agregamos el javascript sin webpack o algún manejador de módulos, tenemos que iniciar en alguna variable o constante los métodos que utilizaremos:
const { DeckGL, GeoJsonLayer, ArcLayer } = deck;
Iniciaremos también tres constantes, dos de ellas para el manejo de los dataset y otra indicando un punto central correspondiente
al Aeropuerto de la Ciudad de México.
const COUNTRIES =
'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_scale_rank.geojson';
const AIR_PORTS =
'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson';
const INITIAL_VIEW_STATE = {
latitude: 19.43,
longitude: -99.08,
zoom: 4,
bearing: 0,
pitch: 30,
};
Para iniciar deck.gl, ahora necesitamos crear una nueva instancia de la función principal, agregando como parámetros
un estado inicial, un controlador y en un array correspondiente a las capas en el que iremos mostrando la información.
new DeckGL({
initialViewState: INITIAL_VIEW_STATE,
controller: true,
layers: [
// ... AQUI IRAN LAS CAPAS DE DATOS
],
});
Dado que nuestros datasets ya se encuentra en formato JSON, podemos utilizar GeoJsonLayer
para crearlos de manera rápida,
en este caso comenzaremos agregando una capa que corresponda a los países.
...
layers : [
new GeoJsonLayer({
id: 'base-map', //ID de la capa
data: COUNTRIES, //Dataset
stroked: true, //Indicamos si tiene borde
filled: true, //Indicamos si esta iluminado
lineWidthMinPixels: 1, //Ancho del pixel
opacity: 0.4, //Opacidad
getLineColor: [154, 154, 154], //RGB para la linea
getFillColor: [242, 242, 242] //RGB para el iluminado
}),
]
...
En pantalla tendremos un resultado como se muestra en la siguiente imagen:

Con el mapa creado, ahora podemos agregar una capa correspondiente a los aeropuertos existentes:
...
layers : [
...,
new GeoJsonLayer({
id: 'airports',
data: AIR_PORTS,
filled: true,
pointRadiusMinPixels: 2,
pointRadiusScale: 2000,
getRadius: f => 11 - f.properties.scalerank,
getFillColor:[21, 192, 25],
pickable: true,
autoHighlight: true,
onClick: info =>
info.object && alert(`${info.object.properties.name}`)
})
]
...

Ahora agregaremos una capa para indicar la conexión entre diferentes aeropuertos, para eso utilizaremos
ArcLayer
, la idea es tener un punto del cual se originarán las conexiones y desde ahí lanzar arcos que conectan con el resto
de los aeropuertos.
new ArcLayer({
id: 'arcs',
data: AIR_PORTS,
dataTransform: (d) => d.features.filter((f) => true),
getSourcePosition: (f) => [-99.08, 19.43],
getTargetPosition: (f) => f.geometry.coordinates,
getSourceColor: [238, 157, 30],
getTargetColor: [21, 192, 25],
getWidth: 1,
});
El resultado de las 3 capas sería el siguiente:
También podemos modificar el comportamiento de los datos relacionados con la propiedad de dataTransform
, imaginemos que
necesitamos mostrar solo la conexión entre México y los aeropuertos existentes en la franja que se muestra en la siguiente imagen.

Para ello, modificaremos el atributo de la siguiente manera:
const MAX = -93, MIN = -90;
...
new ArcLayer({
...
dataTransform: d => d.features.filter(f => f.geometry.coordinates[0] < MIN && f.geometry.coordinates[0] > MAX),
...
})
...
Integrando deck.gl y Google Maps
Al ser uno de los sistemas de mapas más populares para representar información, Google Maps puede ser utilizado junto con deck.gl
para el manejo de capas, leyendo el código fuente
nos podemos dar cuenta que utiliza Custom Overlays
para lograr la integración.
...
export default class GoogleMapsOverlay {
constructor(props) {
this.props = {};
this._map = null;
const overlay = new google.maps.OverlayView();
overlay.onAdd = this._onAdd.bind(this);
...
Entendiendo Custom Overlay
Podemos definir a los Custom Overlays
4 como objetos que estarán en el mapa y que están vinculados a
coordenadas dadas por una latitud y longitud. Una de las principales características, es que se moverán al hacer zoom o arrastrar
el mapa a otra posición.
Utilizar los Custom Overlays
requiere principalmente de 5 pasos:
1.- Inicializar el prototype
de un objeto con google.maps.OverlayView()
USGSOverlay.prototype = new google.maps.OverlayView();
2.- Crear un constructor para nuestro objeto y así poder iniciar los parámetros requeridos.
function USGSOverlay(bounds, image, map) {
this.bounds_ = bounds;
this.image_ = image;
this.map_ = map;
this.div_ = null;
this.setMap(map);
}
3.- Implementar el método onAdd
en el prototype
creado. En esta parte, nos encargaremos de generar el contenedor de la imagen
que utilizaremos.
USGSOverlay.prototype.onAdd = function () {
var div = document.createElement('div');
div.style.borderStyle = 'none';
div.style.borderWidth = '0px';
div.style.position = 'absolute';
var img = document.createElement('img');
img.src = this.image_;
img.style.width = '100%';
img.style.height = '100%';
img.style.position = 'absolute';
div.appendChild(img);
this.div_ = div;
var panes = this.getPanes();
panes.overlayLayer.appendChild(div);
};
4.- Implementar el método draw
en el prototype
.
USGSOverlay.prototype.draw = function () {
var overlayProjection = this.getProjection();
var sw = overlayProjection.fromLatLngToDivPixel(this.bounds_.getSouthWest());
var ne = overlayProjection.fromLatLngToDivPixel(this.bounds_.getNorthEast());
var div = this.div_;
div.style.left = sw.x + 'px';
div.style.top = ne.y + 'px';
div.style.width = ne.x - sw.x + 'px';
div.style.height = sw.y - ne.y + 'px';
};
5.- Y finalmente implementar el método onRemove
en el prototype
.
USGSOverlay.prototype.onRemove = function () {
this.div_.parentNode.removeChild(this.div_);
this.div_ = null;
};
Una vez que tenemos implementado nuestro Custom Overlay
podemos invocarlo dentro la función que utilizamos para iniciar
nuestro mapa.
En nuestro método necesitamos definir el área que ocupará nuestra imagen,
para ello utilizaremos el método LatLngBounds
, el cual recibe 2 parámetros: LatLng southwest
y LatLng northeast
, esto quiere
decir que para indicar la posición correctamente de nuestra imagen, primero tenemos que indicar la coordenada correspondiente
al lado inferior izquierdo en que se posicionará, y como segundo la coordenada superior derecha.
function initMap() {
//Nuestro mapa principal
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 11,
center: { lat: 19.487711, lng: -99.008554 },
});
//Indicamos el área que ocupará la imagen
var bounds = new google.maps.LatLngBounds(
//Suroeste o inferior izquierdo
new google.maps.LatLng(19.389876, -99.1009),
//Noreste o superior derecho
new google.maps.LatLng(19.599925, -98.858176)
);
//Imagen que agregaremos al mapa
var srcImage = './map.png';
//Nuestro overlay recive el área, la imagen y el mapa
overlay = new USGSOverlay(bounds, srcImage, map);
}
En el siguiente ejemplo, podemos ver el resultado de sobreponer una imagen correspondiente
al área del lago de Texcoco5 en Ciudad de México.
GoogleMapsOverlay y nuestro dataset de árboles.
Bien! Ha llegado la hora de aplicar todo lo que hemos aprendido y trabajado hasta este momento, ya contamos con nuestro dataset curado de las especies de los árboles y con los colores que los representarán, además entendimos un poco más del funcionamiento de deck.gl y Custom Overlays
con un par de ejemplos muy sencillos.
Como mencionamos, deck.gl tiene un método propio para hacer uso de los Custom Overlays
, el método se llama GoogleMapsOverlay
y nos
ahorra mucho del trabajo que vimos en la sección anterior.
import { colors } from './colors'; //Colores recolectados
async function init() {
await loadScript();
GMAP = new google.maps.Map(MAP, MAP_PROPS);
deckGL_overlay = new GoogleMapsOverlay();
//Indicamos el mapa a utilizar
deckGL_overlay.setMap(GMAP);
//Agregamos la capa o capas a utilizar, siempre en un array
deckGL_overlay.setProps({ layers: [await getLayer()] });
}
Como podemos ver en la implementación, lo único que tenemos que indicar como propiedad del objeto son las capas que queremos visualizar. Deck.gl permite manejar multiples capas a través de un Array como parámetro.
Ahora, para poder llamar la capa correspondiente a los árboles que procesamos anteriormente, haremos
uso de la función getLayer
, en esta, obtendremos los datos directamente del archivo JSON que contiene,
los puntos de cada elemento, además iremos indicando el color a visualizar respecto a la especie que se encuentre
en la información recibida.
async function getLayer(layer = 'all') {
//Realizamos la petición de información
let request = await fetch(
`https://dataset-euforest.storage.googleapis.com/country_${layer}.json`
);
let data = await request.json();
return await new ScatterplotLayer({
id: 'scatterplot-layer',
data: data, //Esta es la inforamción de los árboles
opacity: 1,
stroked: false,
filled: true,
radiusScale: 20,
radiusMinPixels: 1,
radiusMaxPixels: 100,
lineWidthMinPixels: 1,
//Obtenemos la latitu y la longitud
getPosition: (d) => [d.lng, d.lat],
getRadius: (d) => 50,
//A través del índice de la especie, retornamos el color
getFillColor: (d) => colors[d.specie],
getLineColor: (d) => [30, 30, 30],
});
}
El resultado se puede apreciar en el siguiente iframe o visitando la URL: https://eu-forest.mapsviz.com.
Si deseas, puedes visitar el repositorio con el código completo de la implementación
y ver el resto del desarrollo.
Conclusiones
Sin lugar a duda, la dupla que hacen deck.gl y Google Maps, nos permiten generar experiencias y visualización de datos
de una manera muy simple y relativamente con poco poder computo. Hoy en día, contamos con las herramientas que nos permiten hacer un análisis Geoespacial
muy diferente al que estábamos acostumbrados. Si bien existen tareas que ya se encuentran resueltas
en artículos científicos o por software especializado, podemos darle un toque diferente y "democratizarlo" gracias a la web.
En estos tiempos en que la información se encuentra en diferentes puntos y se genera de manera constante,
el permitir que otras personas comprendan la magnitud de la misma, es uno de los deberes que tenemos
como Ingenieros de Software.
Para mí fue un camino divertido, en el que aprendí sobre diferentes maneras que hemos desarrollado para medir y representar
nuestra ubicación, incluyendo las cosas que nos rodean de manera cercana o global. Espero que este artículo, sea un primer paso
que te inspire, alimente tu curiosidad y sirva de referencia para crear una nueva visualización interesante.
Referencias y enlaces
4.- Custom Overlays Google Maps Documentation
5.- All Things living, all things dead - Cartografías del Lago de Texcoco