Protomaps (PMTiles) is a relatively new storage format for storing and accessing vector tile data. The format is similar to MBTiles in that a single file can contain an entire tileset (of the entire planet if need be), though what differs is the retrieval method of individual tiles.
PMTiles are indexed in such a way that the data for a specific z/x/y tile can be accessed directly from the file using a specific byte range.
See how a PMTiles setup compares to MBTiles. Notice no additional servers are needed.
There are some scenarios when building an application you might want more control over: logging, authentication, bandwidth restrictions, or just more control over which data is served.
The creators of PMTiles made it very easy to get started. The pmtiles CLI can convert any existing MBtiles file to PMTiles with a single command:
The Planetiler project which builds map tiles using the OpenMapTiles schema can also output directly to the PMTiles format. For this example, we’ll create a tileset for New York City.
From there, we should have a local nyc.pmtiles file that we can use in the next section.
Let’s start with the most basic case. We have a PMTiles file, and we want to create an endpoint for serving this data to a client. We’ll assume the file is co-located on the same filesystem as the server. This means we can use Node’s built-in fs library for accessing the file. We'll use an architecture similar to the following.
The PMTiles library already has support for loading from with this FileAPISource class, but you’ll notice it’s based on the browser’s File API. We’re basically going to recreate this file, but swap out the browser API with the Node version.
To do that, we’ll create a new class FileSource that implements the pmtiles Source interface. In order for this to work, we’ll need to re-implement the getBytesfunction.
For any given tile, the PMTiles library will figure out the correct byte range to locate the data for that file, and it will pass in the offset and length for the range. It’s then up to us to implement the logic for retrieving those bytes from the file.
The class can now be instantiated with our file to create a new PMTiles instance.
And in our server, we can add a route to fetch a specific tile from the pmtiles file.
Putting it all together, I've added a simple HTML index page along with a MapLibre style object to be able to render the data in a map.
While serving the tiles from a local file lets us use vector tiles without an additional dependency (SQLite) we still need to have the file co-located on the server that is running. One of the nice things about using byte-ranges, is we can serve the same data from a remote file, assuming the file protocol we’re using supports byte-range requests. To do this, we'll be setting up something like the following.
For this example, we’ll host the file on S3. To do this, we’ll create a new class (called S3Source) that will take the S3 file location (bucket & path) instead of the local path. We’ll also modify the getBytes function to fetch the byte range from S3, and convert the data to a binary buffer.