MapLibre provider¶
An OGC API - Maps provider that draws PNG maps of a PMTiles archive, of vector or raster tiles, with MapLibre Native, the engine behind MapLibre GL. The renderer runs in its own process and reads the archive in place, from a local path or a bucket, the way the PMTiles provider does. The map route awaits the provider, so a map being drawn keeps no worker thread busy.
Install¶
The renderer ships as Linux wheels of the mlnative fork, for x86_64 and aarch64. They need glibc 2.39 or later, as on Ubuntu 24.04. From a checkout, install the maps dependency group with the pmtiles extra, which the provider needs to read the archive:
uv sync removes the packages the command does not name, so add every other extra the server uses, such as --extra geoparquet.
Dependency groups are not published to PyPI. An installation from PyPI needs the pmtiles extra, Pillow and the mlnative wheel for the machine, from the fork's release (x86_64 or aarch64 in the file name):
pip install "fastgeoapi[pmtiles]" "pillow>=11" \
https://github.com/francbartoli/mlnative/releases/download/v0.4.0.dev1/mlnative-0.4.0.dev1-py3-none-manylinux_2_39_x86_64.whl
The system needs the libraries the MapLibre Native binary links, and Vulkan with lavapipe to draw without a GPU:
apt-get install libuv1 libvulkan1 mesa-vulkan-drivers libicu74 libcurl4t64 \
libpng16-16t64 libjpeg-turbo8 libwebp7
Without the group, a map answers 500 with "map rendering is not available" and says what to install; without the pmtiles extra, it answers 500 with "map source not available" and names the extra. The rest of the server needs neither.
Configuration¶
Add a provider of type map to a collection, next to its tiles if it has them:
providers:
- type: map
name: app.provider.maplibre.MapLibreMapProvider
data: s3://my-bucket/tiles/roads.pmtiles
storage_crs: http://www.opengis.net/def/crs/EPSG/0/3857
store_options:
endpoint: fly.storage.tigris.dev
region: auto
options:
styles:
night: s3://my-bucket/styles/night.json
default_style: night
format:
name: png
mimetype: image/png
storage_crs must be EPSG:3857: maps are drawn in Web Mercator only.
| Option | Default | Meaning |
|---|---|---|
max_size | 2048 | The largest width or height; larger answers 413, and below 1 answers 400. |
queue | 8 | Maps that may wait for the renderer; one more answers 503. |
timeout | 30 | Seconds a map may take, the wait for the renderer included; longer answers 504. |
render_limit | four times timeout | Seconds a render goes on after its map answered 504, so the tiles it reads stay cached; past it the renderer is replaced. |
max_rss_mb | 600 | Memory of the renderer process after a map; above it the process is replaced. |
styles | none | Style names mapped to MapLibre style files, local or in a bucket. |
default_style | none | The style drawn when a request names none, one of styles; without it, a plain style of the archive's vector layers or raster tiles. |
style_source | archive | The name the styles give the collection's source in their sources. |
data_url | none | The https address of the data in a public bucket; needed only when the range cache is off and the bucket is read with skip_signature. |
sign_ttl | 3600 | Seconds a presigned data URL lasts; it is renewed when a fifth is left. |
range_cache | true | Read a remote archive through the range cache; false gives the renderer the public or signed URL instead. |
renderer | app.maps.mlnative.create_renderer | The function that builds the renderer from these options; another engine plugs in here. |
tile_size | the first tile's width | The width in pixels of the tiles of a raster archive. |
dem | none | terrarium or mapbox for a raster archive of elevations: without a configured style, the relief is drawn as a hillshade. |
style_factory | app.maps.styles.create_maplibre_styles | The function that builds the styles from the source, the style documents and these options; it must give styles in the language the renderer reads. |
Each style is read at its first map. A style file that cannot be read answers 500 "style ... could not be read" for that style only; the other styles keep drawing.
The renderer reads a remote archive through this process: a server on 127.0.0.1, on a port the system picks and with a random token in every path, hands it the byte ranges from the range cache described in the PMTiles guide. The renderer never sees a signed URL, and a renderer process started again finds the ranges an earlier one read. Past FASTGEOAPI_RANGE_CACHE_REVALIDATE_SECONDS, a map is drawn at the version of the archive it knows while the HEAD that checks the archive runs behind it, so no map waits for that request. An archive replaced in place appears in the maps drawn after the HEAD returns. A map still being drawn when a configuration reload replaces the provider keeps reading its ranges to the end. With the range cache off, a private bucket still works without data_url: the server signs a URL with the store's credentials and gives only that URL to the renderer.
The first map reads the archive header to tell vector tiles from raster ones. A raster archive of PNG, JPEG or WebP tiles is drawn as a raster layer; an archive of AVIF tiles answers 500 with "map source not drawable", because MapLibre Native cannot decode AVIF. A style that declares the archive's source with its type keeps that source and its settings, and the header is not read for it, so a raster-dem source declared without encoding is read as mapbox by MapLibre. With dem, the default style shades the relief.
What a request can ask¶
A request can pass bbox, bbox-crs, crs, width, height, transparent and f, as in OGC API - Maps. crs, when given, must be the EPSG:3857 URI: any other CRS answers 400. f takes png, and f=html returns the same PNG, as pygeoapi's map route does, since there is no HTML map page.
The bbox is read in CRS84 unless bbox-crs names another CRS. Two values answer 500, because of how pygeoapi reads the parameter: the CRS84 URI itself, so leave bbox-crs out for CRS84, and a bare EPSG code such as 4326, so give the EPSG URI instead.
A bbox with another aspect than the image is stretched to it, as a WMS GetMap does. datetime, subset and properties are ignored.
HTTP caching¶
A drawn map carries an ETag and a Cache-Control header, as tiles do: the PMTiles guide has the setting and the rule for protected instances. The ETag covers the view, the size, the style the renderer would get, the archive's version and the renderer's version. A map asked again with its ETag answers 304 without waiting for the renderer. Errors, a full queue included, carry no cache headers. Editing a style file in place changes nothing until a configuration reload: the maps are still drawn with the style read before, and keep their ETag.
Limits to know¶
One renderer process serves each map provider and draws one map at a time. Under load, maps wait in a queue of queue places; past it they get a 503 with "the map renderer is busy, retry later" and Retry-After: 5. A map whose client disconnects leaves the queue at once, so a browser map that pans or zooms before its image arrives does not fill it. A map that answers 504 is still drawn in the background, up to render_limit, so asking for it again a little later is often quick; a render that has started goes on even after its client has left, for the same reason. The first map of a new area waits for the tiles it needs to be read from the bucket. A small view took from 2 to 9 seconds in our measurements, depending on how far the bucket is. A wide view over a large archive needs many more tiles: the whole world from the Overture divisions archive, read from Europe, took more than 90 seconds cold and answered 504 while the render went on in the background.
When the archive changes, a map already being drawn may fail with a 500: it asks for ranges of the old version, the ones not cached any more answer 404, and the renderer gives the map up. The next map reads the new version.
The collection page asks for an image as wide as its map, so max_size should stay above the widest map a browser will show: a wider map gets a 413 and keeps its previous image.
A style with labels needs a glyphs URL the renderer can reach. The MCP server gives each map collection a tool that returns the image to the model.