Mapbox Single Tile

I've been poking around in Mapbox GL JS lately (e.g. 3DEP Elevation Source for Mapbox), and am having fun trying to do weird things by inserting a FastAPI call where normal tile loads would go.

For this post, the goal is to show only a single imagery and terrain map tile at a given pair of coordinates and zoom level. In normal use, Mapbox shows all necessary imagery and terrain tiles (example). Too easy! It's much harder to show only a single tile, but as they say, less is more (more effort, I assume).

The first step is to understand how the map decides which tiles to load when the time is right. If you open the network tab in your browser dev tools, you'll see a stream of calls back to Mapbox HQ for tiles. Each url contains the name of the requested data source plus a few numbers (e.g. .../mapbox.satellite/15/5982/13231...). The numbers follow the typical Slippy Map convention of {z}/{x}/{y}, where z, x, and y are Slippy Map Tile Names (go ahead and bookmark that site). Just by knowing the request, the tile elves working at Mapbox are able to assemble the necessary tile and send it back to us, where our client is smart enough to display where they should.

Okay, but how do we bust into this process to do something around the time a tile would normally load? Sadly, there's no such event as tile_is_about_to_be_requested! But do not despair, there is a super-handy transformRequest option on the Map object, which allows us to do things when requests happen. Want to add some non-default headers to the requests? Sure! Want to print the requests to console for some reason? Okay!

In our case, we're going to listen for a couple things:

The pseudocode looks like this:

map = new mapboxgl.Map({
    ... map options
    transformRequest: (url, resourceType) => {
        if this is a request for an imagery tile {
            if the tile coordinates do not match our desired tile coordinates {
                return a null URL
            } else {
                return the original url
            }
        }

        if this is a request for a terrain tile {
            return a URL to a custom API that will process the terrain further
        }
    }
})

The real code looks like this.

Okay, so that more or less takes care of the imagery requests, but the terrain is a little trickier. Behind the scenes, Mapbox GL JS makes a lot of choices for which terrain tiles to show depending on the view angle of the map (e.g. far away tiles are requested at lower resolution and zoom level), so it's hard to know which tiles are going to be requested up front. My solution is to stand up a custom API that accomodates all these requests, uses rasterio to fetch the necessary tiles from AWS Terrain Tiles, clips out only the necessary pixels that overlap the imagery, encodes to Terrarium encoding, and returns the processed tile to be displayed by the map client. It's a little slow and brute-force-y, but it seems to do the job, so just set your expectations accordingly.

The API code is here. Deploy it1, point the terrain transformRequest url to it, and if all goes well, the whole thing should look like this:

Mapbox single tile

The end! Have a look at the code and see if you can make sense of it. Let me know on Twitter or Mastodon how it goes!

1You can start the API by:

  1. pip install the various dependencies (e.g. pip install "fastapi[all]" rasterio shapely pillow etc.)
  2. Start the API with uvicorn main:app --reload