Reimagining projections for the interactive maps era

A deep dive into Adaptive Projections in Mapbox GL JS

No items found.

Dec 9, 2021

Reimagining projections for the interactive maps era

A deep dive into Adaptive Projections in Mapbox GL JS

Guest

No items found.

Guest

Dec 9, 2021

Last month, we released Mapbox GL JS v2.6 with Adaptive Projections — a novel way to make interactive maps more accurate on a global scale, without any compromises to user experience, rendering quality or street-level precision.

We have put a lot of thought into making this feature feel seamless and natural, so that our customers could adopt it on all kinds of map apps by adding one line of code. Let’s take a deep dive into why we did it, and how it works under the hood.

Introduction to map projections

Any maps we see on either screen or paper use a certain map projection — a way to represent the globe on a flat surface. This has been a topic of research for at least several thousand years, ever since the ancient Greeks established that the Earth is a sphere. However, one thing is certain — there’s no perfect projection, and any way to map a sphere onto a plane inevitably leads to distortion (with a notable mathematical proof by Euler from 1777).

Andy Woodruff’s projected heads, inspired by Charles Deetz’ 1921 book about map projections

All projections distort shapes, sizes or distances, so choosing one is a matter of trade-offs:

  • Some maps need to preserve relative sizes, so that nothing looks bigger than it actually is (e.g. using the Albers projection for country maps or Equal Earth for world maps), but achieving this also requires distorting shapes.
  • Some maps need to preserve shapes, which is especially important for navigation (Mercator) and aeronautical maps (Lambert Conformal Conic), but this distorts sizes.
  • Compromise projections try to balance both kinds of distortion — not too much shape distortion and not too much size distortion. They aim for something that “looks right”, and are often used for visualizations on world maps (Winkel tripel, Natural Earth).

The most famous such trade-off, one we all experience in our daily lives, happened at the dawn of web cartography, which adopted Web Mercator as the projection of choice. At the time, interactive maps were invented for street-level navigation, and Mercator is perfect for this — it preserves angles between lines, avoids stretching of shapes, and makes it so that north is up at any point. 

Web Mercator is the best projection for street-level navigation

At the same time, Mercator heavily distorts sizes on a global scale, inflating shapes the farther away they are from the equator. This is why so many people living in the Internet era think that Greenland is as big as Africa, even though it's 14 times smaller:

The world in Web Mercator projection (Greenland is 14 times smaller than it appears)

Projections on interactive maps

Even though map projections are one of the fundamental concepts of cartography, adopting non-Mercator projections in interactive mapping has proven incredibly difficult. Despite many passionate campaigns by cartographers and mapping enthusiasts, Web Mercator remains dominant.

One reason for this is that along with the projection, the maps industry adopted a corresponding tiling scheme, which starts with a Mercator-projected square tile on zoom 0, and splits each tile into four on every subsequent level. This simple and efficient format for distributing map data became ubiquitous.

Consequently, to use another projection,all the data would have to be reprocessed into differently projected tiles, but this is enormously expensive — for example, Mapbox generates tiles for its base map styles up to zoom 16, which is 5.7+ billion tiles. Generating separate tile sets for each possible projection for every dataset would be impractical.

Web Mercator tiling scheme, with 4^zoom tiles on each level

Reprojection of raster map tiles is often not an option because text labels would get distorted, and sharp lines and icons would become blurry.

Another fundamental problem when applying map projections to an interactive map is that projections were designed for paper, and don’t work well when users can zoom between a world map and a street-level map. Projections that work well on a global scale are heavily distorted when zoomed in (as seen on this example of the Bay Area in Mollweide), and vice versa (Mercator).

OpenStreetMap raster tiles in Mollweide projection on zoom 11

There was a remarkable attempt at adapting map projections for interactive maps by Bernhard Jenny of Oregon State University in 2012. However, it unfortunately remained a proof of concept. The approach required reprojecting geometry on every rendering frame, which would be difficult to scale to modern vector rendering standards that often require rendering millions of points on the screen at once, while keeping compatibility with existing Mercator tiles. Additionally, interacting with a map that has abrupt projection changes may feel disorienting.

Oregon State University’s Adaptive Composite Map Projections demo

Due to all these limitations, non-Mercator projections are a rare sight on regular web maps, instead being mostly confined to purpose-built D3-based visualizations. Advancing vector rendering technology seemed like the only viable path towards wider adoption.

Projections in Mapbox GL JS

Ever since Mapbox changed the maps industry with the introduction of GL JS and its vector rendering technology, we were uniquely positioned to tackle this problem — perhaps the most requested feature in the project’s 8-year history.

When we finally got to work on this, we wanted to do it right. This was our chance to make alternative projections so compelling that there would be no barriers to widespread adoption. And once you commit to a particular design of such a big and complex feature, it’s difficult to change afterwards, so this one shot had to count. We settled on the following guiding principles:

  • Compatibility with existing sources and styles, so that anyone can start using alternative projections right away on their Mapbox maps. This required loading Web Mercator tiles and reprojecting the data on the client.
  • Vector rather than raster reprojection to keep vector features rendering sharp and precise.
  • Dynamically adjusting the projection while zooming in to eliminate distortion.
  • Redefining what zoom and bearing mean to make switching to a different projection feel intuitive and seamless.
  • Zero overhead for rendering performance, so that maps with alternative projections feel as fast as our usual maps.

Projections and zoom level

One of the first questions we asked when designing projections seems trivial: what does zoom level mean? Historically, for Mercator maps, zoom was defined as 0 for a map with a single tile containing the whole world, with each increment by 1 doubling the size.

With a non-Mercator projection, both the tiles and the world are no longer axis-aligned squares, so we can’t rely on the same definition. But intuitively, we want the zoom value to reflect the scale of features on the map — so that regardless of the projection chosen, a certain map zoom and center configuration would make the map cover approximately the same area, with sizes of shapes relatively the same. We can do that we these steps:

  1. Take an arbitrary area on the map as a reference point (e.g. a red square below).
  2. Calculate its size in reprojected coordinates.
  3. Calculate its size in Mercator coordinates.
  4. Adjust the projected map scale so that these sizes match.
Adjusting map scale so that the red square size is approximately the same for the same zoom

There’s just one catch — which point do we take as a reference? If we use the center of the map view, then the map will visibly zoom as you pan, which feels weird. And if we use a fixed point on the map (e.g. the center of the world), the size discrepancy will grow as the user moves away from that point.

The solution we landed on combines these approaches:

  • On low zoom levels, use a fixed point (projection center). This way, the shape of the world remains the same & doesn’t scale during panning.
  • On high zoom levels, use the center of the screen. Since panning is limited to a relatively small distance, any scale change will be unnoticeable.
  • On middle zoom levels, use both, smoothly transitioning between the two as the user moves from low to high zooms or back.

This makes designing maps for different projections and picking the right one a joy:

Seamlessly switching between Mercator and US Albers in Mapbox Studio

Projections and rotation

Similarly to zoom, when using interactive maps, we have certain preconceived notions about how a map should be oriented. Conditioned by Mercator, we intuitively expect it to align to the north unless we rotate it. And we expect any given map view configuration (center, zoom and bearing) to have the same orientation regardless of projection.

So we rotate the map automatically to match this intuition in a similar way to zoom:

  • On low zoom levels, orient the map so the north is straight up from the projection center. This way the map won’t rotate when panned.
  • On high zoom levels, orient the map so the north is straight up from the screen center. This way north is up like in Mercator regardless of which point on Earth is shown, and rotation during panning is unnoticeable.
  • On middle zoom levels, smoothly transition between the two approaches.
An extreme case of rotation correction: zooming to Alaska on an Albers polar projection

Correcting distortion when zooming in

After we apply the zoom and bearing adjustments above, one problem remains: when we zoom in, the map is noticeably skewed:

Zooming to San Francisco on Winkel tripel projection without skew correction

This is a natural property of non-conformal projections like Winkel tripel — it preserves relative sizes of shapes, but meridians and parallels are not perpendicular to each other, so shapes look distorted when zoomed in. How do we correct this?

When you're zoomed in enough, the view represents a nearly flat portion of the Earth. Consequently, alternative projections are no longer necessary, since you don't have to compromise between distorting shape or size. Notice how as the map zooms in, the curves representing meridians and parallels straighten out, approximating straight lines on higher zoom levels. We want to skew the map back so that these lines are perpendicular, and stretch it so that vertical and horizontal distances are equal. This will make the map conformal, matching it nearly perfectly with the Mercator projection.

This skewing/stretching is called an affine transformation, and it can be applied in real time on the GPU by correcting the WebGL view matrix. This way, we can apply the coordinate reprojection once when a tile loads, and then adjust the view of those coordinates as the user zooms without any performance overhead. It also feels much more natural when interacting with a map than more advanced transformations that twist and distort the view non-uniformly.

Inverse shear (from green to blue), a special case of affine transformation (source)

We do this the same way as with previous corrections — no adjustment on low zoom levels, then smoothly transitioning to full correction as the user zooms in, giving the best of both worlds:

Zooming to San Francisco on Winkel tripel projection with skew correction

Tile loading changes

Due to rotation, pitch, and more recently 3D terrain features, the code responsible for deciding which tiles to load is quite convoluted. With projections, it gets even more complex: tiles are no longer perfect squares, instead taking various shapes depending on the projection used.

Moreover, since some tiles of the same zoom level can become large, and others small when reprojected, we adapted the tile loading logic, fetching tiles of different zoom levels on the same view depending on their size.

Tiles loaded on a map in the Albers USA projection

Resampling vector geometry

When we reproject vector tile data from Mercator to another projection, it’s not enough to calculate new coordinate values — line and polygon segments that were previously straight might become curved, so we need to add more points in between to follow the curve. Add too many, and rendering performance would be impacted. Add too few, and the lines wouldn't match the projection.

We find a balance with an adaptive algorithm — adding a point in the middle of a line segment, then in the middle of the newly created two segments, and continuing recursively until each segment approximates the curve with sufficient precision. It is very similar to the popular Douglas-Peucker polyline simplification algorithm, but in reverse. This approach allows us to render reprojected geometry accurately with only 2–3% additional points on a typical map.

A straight line that became curved after reprojection, resampled with additional points

Reprojecting raster tiles

To render raster tiles such as the satellite layer in alternative projections, we need to load Mercator images and then map them to the distorted tile shapes. To do that in WebGL, we need to split the tiles into many little triangles, with each triangle responsible for representing a tiny part of the image. Generally, the less triangles there are, the faster the rendering.

2 years ago, we introduced MARTINI — an open source library for real-time generation of 3D terrain meshes from raster heightmaps, with an accompanying interactive article describing how it works in detail. As it turned out, we can use exactly the same approach to generate triangular meshes for reprojecting raster tiles. Only instead of measuring how well individual triangle edges approximate terrain (to decide whether to split them further), we measure how curved an edge becomes with reprojection.

This results in a triangular mesh in which less distorted regions have less triangles, and more distorted regions have more:

A MARTINI reprojection mesh for the 0/0/0 tile of the Equal Earth projection

Get started with Adaptive Projections

As a result, your fast, beautiful and detailed interactive Mapbox maps can now be effortlessly used in alternative projections. Thank you for reading!

Play with the new feature on this demo, check out the minimal example and documentation, try it out on your maps (either with code or Mapbox Studio), and let us know if you run into any issues. And look out for another awesome upcoming feature the Mapbox community has been longing for years:

Bonus: bloopers

Development of such complex features never goes smoothly, but on the upside, there’s a lot of accidental art to enjoy (our team has a separate #lol-gl Slack channel for sharing these):

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.

Block quote

Ordered list

  1. Item 1
  2. Item 2
  3. Item 3

Unordered list

  • Item A
  • Item B
  • Item C

Text link

Bold text

Emphasis

Superscript

Subscript

No items found.
No items found.