The Interactive Elevation Profile Tool
The application we are going to build is a web application that allows us to draw a path and retrieve elevation profiles along that path
Before we dive into the code, it is beneficial to understand the process of retrieving elevation from Terrain-RGB tiles. Let’s go through it now.
Draw a path on the map
The first thing we need to do is to create a simple web app with a map and add the ability to draw paths. We will use
maplibre-gl-js library along with
maplibre-gl-draw plugin which allows us to do it. By the path, we mean a linestring with several definition points.
In the next step, we need to generate a set of points along the line. Why is this necessary? Imagine that the user draws a line with only two definition points (red dots). Without sampling, the elevation profile would be just a line connecting two elevations.
Later, we will retrieve the elevation for each point. Distance between points should be regular so that we get a nice and smooth elevation profile but it should not be smaller than Terrain-RGB pixel size.
In order to access the elevation data, we need to fetch Terrain-RGB tiles from the server. Which tiles? Map tiles are organized in a pyramid indexed by
(x, y, zoom) indices where the top contains a single tile that covers the entire world,
the next level contains
2 x 2 tiles, n-th level contains
2^zoom x 2^zoom tiles etc. We also need a projection that will convert geographic coordinates
(latitude, longitude) to meters. We will use Web Mercator projection.
The math which is needed here might be difficult to digest, but a good place to start is MapTiler page Tiles à la Google Maps - Coordinates, Tile Bounds, and Projection which explains how map tiles work.
For the given geographic coordinate
(latitude, longitude), we need to find 3 values:
(x, y, zoom).
zoom: we will use MapTiler Terrain-RGB tileset whose most detailed zoom level is 12. We expect users will draw short profiles so we will the most detailed data. (For long profiles spanning many kilometers, it would be better to use lower resolution data though).
x,y: we will calculate x and y coordinates using the globalmaptiles library. This library allows us to translate geographic coordinates to the Web Mercator coordinates and find the pixel coordinates for a given zoom level. The conversion process is as follows
Once we have the tile index, we can download the tile from MapTiler cloud.
Finding relevant pixels
We have downloaded the tile successfully. The tile is a png image that we can decode to a byte array where pixels are organized into rows and columns, each represented as an RGBA value. The byte array has origin in top-left corner and there are 512 rows and 512 columns.
The next task is to find the correct pixel in the byte array which covers the requested geographic coordinates. We are working on zoom level 12 and our tile size is 512 pixels. With these parameters, we calculate:
The pixel coordinates
(px, py)for the requested geographic coordinate
The pixel coordinate
(tx, ty)for the tile lower-left corner
(x-offset, y-offset)to retrieve the pixel and decode the elevation
Decoding the elevation
Once we have offsets within the RGBA byte array, we can retrieve the 3 values (we don't need A) and decode the elevation:
Draw the chart
When we determined elevation for each sample along the path, the last step is to draw a chart so that the user can see the profile. We will use an open-source Chartist library.
Let’s dive into the code. We are going to explain the most important parts. The full source code is available on GitHub.
The app skeleton
The application user interface is very simple - just a container with two placeholders - one for the map and one for the elevation profile chart. The challenge is to make sure that the map and the chart fill browser window height, do not overflow, and resizes correctly when the user resizes the window. We use CSS Flex Box Layout to build this layout.
Here is the HTML fragment which implements the layout:
And the corresponding CSS stylesheet
Drawing the path
After we prepared the layout, we can create a new instance of the MapLibre map control and tell it to use
In order to draw paths, we are going to use maplibre-gl-draw plugin. The plugin can do many things but we will use it just for drawing paths (line strings). We create the plugin instance and add it to the collection of MapLibre map controls.
There are a few events the plugin eventually triggers. For us the most important one is
draw.create event which is triggered when the user finished the path. The callback which handles the event receives one argument and it contains the GeoJSON representation of the path the user created. We only need coordinates which we can access as follows.
Generating sampling points
As mentioned in the intro section, we need to generate a set of points along the path. This is easy thanks to the Turf.js library. But before we calculate these points we have to determine the step size. To display the elevation profile in the browser window 200 samples should be sufficient. So we will simply calculate the length of the path and divide it by the number of samples. We will also calculate the pixel size for the given latitude and will make sure the step size is not smaller than the pixel size, otherwise, our profile would have couple of samples with equal elevation.
To generate points, we use
turf.along method in a simple loop.
In the GitHub sample, the code for downloading tiles is written in the
ElevationProvider class. The class uses
GlobalMercator class for coordinate system and projection calculations. Terrain-RGB tiles have 512x512 pixels so we pass this size into the
GlobalMercator constructor. We will use a lazy loading pattern to download the tile from the server once we encounter a first point that intersects that tile and we cache its data to
tiles dictionary so that we don't have to download again for the following points intersecting that tile.
Tile index is calculated simply by converting
latitude, longitude to meters using Web Mercator projection, then converting meters to pixels at the given zoom level.
GlobalMercator class has a handy
LatLonToTile method for it.
Note that the library was designed for the TMS tiling scheme, but we need a Google Web Mercator tiling scheme, therefore there is that additional step to convert tile index using
With the tile index, we can download the tile from MapTiler cloud and read the tile data.
We will load the tile using
Image class. The reason is that the tile is a PNG image and we want to use the browser API to decode it and access raw bytes. So we will package the code for downloading into the
Promise so that we can call it from our asynchronous function.
Image has been loaded, we can use the browser’s
CanvasRenderingContext2D to turn the image into an array of raw bytes. The most important method is
context.getImageData which will return the 2D array of RGBA values.
Now we need to calculate offsets as described in the intro section. We will again use
GlobalMercator class to calculate tile extent in meters (
TileBoundsmethod) and then covert it to pixels (
Similarly, we calculate the pixel coordinates of the sample point. With pixel coordinates of both the sample point and the tile extent, we can evaluate indices that we will use to locate the correct but chunk in the tile image data. The “noise“ in the code (
max methods) just ensures that we always get the correct index in interval <0,511) even for corner cases caused by rounding.
Decoding the elevation
The next step is super-easy. Just use calculated indices and retrieve RGB values. Then decode it into the elevation.
Calculating the profile
With all the code written above, we can just loop over samples and calculate elevation for each sample.
Rendering the chart
And finally, show the chart. In order to get a nice chart, we determine the minimum elevation in the profile to configure the chart component and along with some padding we set it as a
low limit in the chart component.
With a little effort, it is quite easy to generate terrain profiles using MapTiler Terrain-RGB tileset. Access to the elevation data opens a wide range of applications such as microwave link planning, line-of-sight analysis, pipelines, power lines, etc.