EU Forest

Visualizing more than 500,000 trees using Google Maps and Deck.gl

Sometimes we have some projects that challenge us to prove the limits of Google Maps, one of the biggest difficulties that we have as developers when we’re using this tool, is the need and capacity to display a thousand, or a hundred of thousand points inside a custom map. Is this possible?

Google Maps, and their API have been evolved across the last couple of years, and with this, they’ve been bring us concepts like Custom Overlays that allow us to extend the natural capacity of what we generated, also created a flexible environment to let projects like deck.gl do a smooth integration.

In this article I would like to tell you my experience to achieve representing 588,983 trees on a regular Google Maps, so I’m encourage you to bring your favorite drink to be your partner to read this post.(☕|🥤|🍺|🍷)

Table of Contents

The Inspiration

The past year I had the opportunity to see in action to Alex Muramoto giving a talk named “Awesome web data viz with Google Maps Platform and deck.gl”, if you didn’t have the chance to attend one of the multiple DevFest where Alex was, you can watch the talk on Youtube following the next link: https://bit.ly/alexsgmapdeckgl.

During the talk, Alex drives us through different examples that allow us to extend Google Maps capacity using Deck.gl, he shows us the basis to create something beyond add a simple marker, now, we can add multiple points, arcs and make real time visualizations in determinante map regions, so after a while, I encouraged myself to do an experiment with this two technologies.

Selecting the Problem

Doing a small research on Internet, I could find a variety of complete datasets through different websites, some of them are the Paris - Open Data Site or the European Portal Data, also I retrieved differente papers related with geospatial visualization, one of the catched my eye: “EU-Forest, a high-resolution tree occurrence dataset for Europe”1, the paper addresses the way and methodology about how they do a information curation related with the European tree species to obtain an unique dataset that can give a good distribution through different countries, one of the best points is that the dataset is open and give some information valuable to be used, like coordinates, species and visualization examples that can be followed as clues to determinate if we’re on the right way.

Spatial distribution of all occurrences in EU forest. Spatial distribution of all occurrences in EU forest.

With all that in place, it will be really exciting reproduce the results of the experiment with Google Maps and Deck.gl.

Coordinate Systems, more than latitude and longitude

In the webpage of the publication you will see different documents, the one that we’re going to use as referent2 and make sense for our goal of visualzation is curated respect the tree species, this file is in CSV format and as we can read in the metadata file, the coordinates X and Y are regarding to the ETRS89-LAEA reference … 🤨, ok, that the fun begins!

Typically the libraries or map services that we integrate in our code, teach us to reference the position as latitude and longitude, so the first challenge that I had processing the dataset is understand and make the transformation from the coordinate values present in the file to something that can make sense on Deck.gl or Google Maps.

Is time to get the Google Search help!, I discover that the ETRS89-LAEA has an equivalent: EPSG:3035… so, with this in mind , now we can try to solve this first part.

EPSG

EPSG is the abbreviation that correspond to “European Petroleum Survey Group”, this group made different datasets with the purpose to have good spatial references that could be applied in a global, regional, national and local way, so in theory, one of this datasets correspond to the use of latitude or longitude values.

One of the most useful resources that I found and that allows you use all this references simplily, is the webpage epsg.io, inside it, you can use a service where you can transform the value between different systems.


Doing a quick inspection on the code, I can realize that they are using a library: pro4JS, Bingo! That is the part I need to start the data conversion 😁, and thanks to they a full list of all EPSG codes, I can get the equivalent of the traditional way that we use coordinates: EPSG:4326.

proj4JS

pro4JS, is an Open Source project that take inspiration in other: proJ, this library allow the conversion between coordinate systems, being a very specific niche in the Software World, sometimes the documentation could be not enough or could be hard to understand from where and what calculation is done.

You can install proj4 via npm or yarn to start using it, and according to the documentation the usage of the function is the next:

  proj4(fromProjection[, toProjection, coordinates])

The values that are required make reference to projections3, a projection is the operation required to get the equivalent between systems, under the proJ project, you can learn how to use this project, for now we’ll be focused in the value, understand how they form the formula could another great post. So to define the projection for EPSG:3035 you need to write the next Array:

[
  '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 ',
];

As I mentioned, the value for the projection could be complicated at the beginning, and many mistakes happen for a misspelling in the string, so that’s why I created a repository with a list of values that can be used to obtain the value of a EPSG code through the use of an index. The list contains 6137 values that we can use in the conversions, you can read the full list and get the names in the README file of the project, to use it on proj4, you need to write a code as follows:

 const proj4 = require("proj4")
 const proj4list = require("./list.min");
 proj4.defs([
   proj4list["EPSG:3035"],
   proj4list["EPSG:4326"]
 );
 proj4("EPSG:3035", "EPSG:4326", [ 4305500, 2674500 ]);

So now we have the way to convert the X and Y original values present in the dataset, now we can have them on latitude and longitude!, right now our script to convert the CSV file to a JSON format will follow the next logic:

  • 📃 Read each row in the CSV document.
  • 🗺 Convert X and Y to latitude y longitude.
  • 🌲 Replace the tree specie with an number, that ca be mapped to manage the colors.

The content of our dataset will be something like this:

[
  {
    "specie": 70,
    "lat": 35.01966518543305,
    "lng": 32.6269824790667
  }, ...
]

If you wouli like access to the final dataset, you can find the information classified by country in the next repository: Datasets:EU-Forest

Getting trees colors

The visualization that we want to do could fine really well just having with the position of the trees, and the result will be something similar as the presented in the paper, but as users that are constantly consuming information, is important represent the data in the right way and make it a great experience and especially understable.

So with this premise, let’s start working on this part 🤓.

The main idea is get a couple of images from Google Search Images, merge them and obtain the main color of it.

To help in this purpose, I found the node.js library that use Puppeteer, images-scraper, that allow us get an Array of image paths from a query.

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);
  });
}

For each element in the Array, we’re going to download the file, so now, we need to write a function that allow us save the files into a directory, and at the same time, let us save the path directory inside other Array to be used later, so the function to make the download is the next:

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);
  }
}

Now that we have the downloadImage function, we can proceed to get the images and merge them using merge-img, since we don’t know the image dimensions we’re going to save them in PNG, to make a better color analysis.

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 () => {
      // HERE WLL BE THE IMAGE ANALYSIS
    });
  });
}

The resulting image will be something like this:


Now that we have a image to process, we’re going to use Color Thief,a popular library that get the main color, the only parameter that we need to set is the merged image.

img.write(`${output}`, async () => {
  resolve(await colorThief.getColor(output));
});

Again you can find the full code in one repository that I created, you can access using via this link.

If you follow the install instructions you’ll be able to run the next command in your terminal;

node example.js palette "Batman" 10

The values corresponding to the main colors that this script produces, can be found in the next file, they are the colors related with the 242 tree species processed.

And, What is deck.gl?

Deck.gl is one Open Source tool that is changing the way we can do map data visualization as developers, and at the end impact to our product customers, created by the Uber Visualization team, through WebGL2 can do a better use of our hardware, and let explore and visualize BIG AMOUNT of data using superposed layers in the browser, also give to the developers a way to personalize each layer, update or remove them if necessary.

First steps using deck.gl

Ok! now that we understand that deck.gl is using layers, is time to use them and visualice something.

One of the characteristics that I like from deck.gl, is the independence of a map system, so you can use it with techs like Google Maps, Mapbox, Arcgis and either without a map framework.

To have a first approach to deck.gl, we will use two datasets created by geojson.xyz.

We can make use of deck.gl in different ways, one of them and is helpful if you’re new in code, is directly loading the script into our HTML.

<script
  type="text/javascript"
  src="https://unpkg.com/deck.gl@latest/dist.min.js"
></script>

When we add the Javascript without webpack or a module handler, we need to initialize the methods that we’re going to use in some variables or constants:

const { DeckGL, GeoJsonLayer, ArcLayer } = deck;

We will also initialize three constants, two of them for the dataset management, and the other will be the central point corresponding to the Mexico City Airport.

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,
};

To start deck.gl,now we need to create a new instance of the main Class, adding as parameters an initial state and an Array corresponding to the layers where we will show the information.

new DeckGL({
  initialViewState: INITIAL_VIEW_STATE,
  controller: true,
  layers: [
    // ... HERE WILL BE THE DATA LAYERS
  ],
});

Given that our datasets are already in JSON format, we can use GeoJsonLayer to create them quickly, in this case we’re going to start adding the Country layer.

...
layers : [
  new GeoJsonLayer({
    id: 'country-layer',    //Layer Id
    data: COUNTRIES,        //Dataset
    stroked: true,          //Stroke or Border
    filled: true,           //Fill
    lineWidthMinPixels: 1,  //Pixel Width
    opacity: 0.4,           //Opacity
    getLineColor: [154, 154, 154],  //RGB color for stroke
    getFillColor: [242, 242, 242]   //RGB color for fill
  }),
]
...

On screen, we’re going to have a result as shown in the following image:

p Map Deck.gl's ability to manage layers allows us to create our own maps.

With the map created, we can now add a layer corresponding to the existing airports:

...
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}`)
    })
]
...
Airports Now we have two overlays, one corresponding to the map and the other to airports.

Now we will add a new layer to display some connectivity, for that we’re going to use ArcLayer, the idea is to have a single point as origin, from it the ars will be displayed as airport connections.

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,
});

The result of the 3 layers would be the following:

In this iframe you can interact with the result.

With the property dataTransform, we can also modify the behavior of the data,

También podemos modificar el comportamiento de los datos relacionados con la propiedad de dataTransform, let’s imagine that we need to display only the connections between Mexico and the existing airports in the strip shown in the following image.

Globe map Limit between longitude: 90 and 93.

To do this, we will modify the attribute as follows:

const MAX = -93, MIN = -90;
...
new ArcLayer({
    ...
    dataTransform: d => d.features.filter(f => f.geometry.coordinates[0] < MIN && f.geometry.coordinates[0] > MAX),
  ...
})
...
In this iframe you can interact with the result.

Being one of the most popular map systems to represent information, Google Maps can be used together with deck.gl for layer management, reading the source code we can see that it uses Custom Overlays to achieve integration.

...
export default class GoogleMapsOverlay {
  constructor(props) {
    this.props = {};
    this._map = null;

    const overlay = new google.maps.OverlayView();
    overlay.onAdd = this._onAdd.bind(this);
...

Understanding Custom Overlays

We cand define Custom Overlays4as objects that will be on the map and that are linked to coordinates given by a latitude and longitude. One of the main features is that they will move when you zoom or drag the map to another position.

Using Custom Overlays mainly requires 5 steps:

1.-Set your custom overlay object’s prototype to a new instance of google.maps.OverlayView()

USGSOverlay.prototype = new google.maps.OverlayView();

2.- Create a constructor for your custom overlay, and set any initialization parameters.

function USGSOverlay(bounds, image, map) {
  this.bounds_ = bounds;
  this.image_ = image;
  this.map_ = map;
  this.div_ = null;
  this.setMap(map);
}

3.- Implement an onAdd() method within your prototype, and attach the overlay to the map.

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.- Implement a draw() method within your prototype, and handle the visual display of your object.

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.- And finally you should also implement an onRemove().

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);
}

In the following example, we can see the result of superimposing an image corresponding to the area of Texcoco Lake5 in Mexico City.

In this iframe you can interact with the result.


Google Maps Overlay and our trees dataset

All right! The time has come to apply everything we have learned, we already have our cured dataset of tree species, and the main color for each one, also we already have a better understanding about how deck.gl and Custom Overlay work.

As we mentioned, deck.gl has its own method to make use of the Custom Overlays calledGoogleMapsOverlay, and with it, we can save lot of the work and time as we saw in the previous section.

import { colors } from './colors'; //Tree colors

async function init() {
  await loadScript();
  const GMAP = new google.maps.Map(MAP, MAP_PROPS);

  const deckGL_overlay = new GoogleMapsOverlay();
  //Here we indicate the map that we want to use
  deckGL_overlay.setMap(GMAP);
  //Adding the layers as Array,
  deckGL_overlay.setProps({ layers: [await getLayer()] });
}

As we can see in the implementation, we need to send the layers into an Array as property of the GoogleMapsOverlay variable.

Now, we can do the request to get the tree positions that processed previously, we’re going to implement a function named getLayer, an in this, we’ll get the data directly from the JSON file that contains them, we will also indicate the color to display respect to the specie index.

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],
  });
}

The result can be seen in the following iframe or by visiting the URL: https://eu-forest.mapsviz.com .

Europe Forest Map using deck.gl and Google Maps.

If you want, you can visit the repository with the full code, and take a look about how the web was built.

Conclusion

There is no doubt, that the combination of deck.gl and Google Maps allow us generate experiences to visualize data in a very simple way and with relatively little computing power. Nowadays, we have the tools that make possible create Geospatial analysis newly way, different what we used to, also on a relative low cost. Although exist tasks that are already solved by specialized software, we can give them a different perspective and “democratize” it thanks to the web technologies. In these times when the information flow from different points and is constant, allow that other people understand the magnitude of it, is one of the duties that we have as Software Engineers.

For me make this experiment was really fun, I learned about different ways that we have developed to measure and represent our location, including things that surround us closely or globally. I hope this article is a first step to inspire you, feed your curiosity or be a reference to create an interesting new visualization.

References and links

1.- Mauri, A. et al. EU-Forest, a high-resolution tree occurrence dataset for Europe. Sci. Data 4:160123 doi: 10.1038/sdata.2016.123 (2017).

2.- Mauri, A., Strona, G., & San-Miguel-Ayanz, J. Figshare https://dx.doi.org/10.6084/m9.figshare.c.3288407 (2016).

3.- Projections - PROJ

4.- Custom Overlays Google Maps Documentation

5.- All Things living, all things dead - Cartografías del Lago de Texcoco

6.- Sitio geojson.xyz