Large data: PMTiles, FlatGeobuf, GeoParquet & COG
“Cloud-native” files are made to be read in parts, straight from a web server: the browser asks for the bytes it needs (HTTP range requests), so a map can draw a city from a country-sized file without downloading all of it. Since 0.9, maplibre-yaml reads four of them with nothing but YAML — in plain HTML from the CDN, in Astro, and under any bundler:
| Format | File | Holds | Source type: |
|---|---|---|---|
| PMTiles | .pmtiles | Vector tiles, raster tiles or elevation, in one file | vector, raster, raster-dem |
| FlatGeobuf | .fgb | Features (points, lines, polygons) with their properties | geojson |
| GeoParquet | .parquet, .geoparquet | Features in a columnar table: only the columns a map uses are read | geojson |
| Cloud Optimized GeoTIFF (COG) | .tif, .tiff | One image: imagery, elevation, or a band of values (in Web Mercator) | raster, raster-dem |
A Protomaps .pmtiles file can also be a whole basemap, from one line of
YAML (Host your own basemap).
The rule: type: is what MapLibre draws, the URL is what the file is
Section titled “The rule: type: is what MapLibre draws, the URL is what the file is”You keep writing the source type MapLibre knows — vector, raster,
raster-dem, geojson — and point url: at the file. The file name tells
maplibre-yaml how to read it.
type: mapid: cloud-native-minimalconfig: center: [10.2, 42.15] zoom: 10 mapStyle: "https://tiles.openfreemap.org/styles/positron"sources: outlines: type: vector url: /gallery-assets/cloud-native/districts.pmtiles districts: type: geojson url: /gallery-assets/cloud-native/districts.fgblayers: - id: districts type: fill source: districts paint: { fill-color: "#e9c46a", fill-opacity: 0.6 } - id: outlines type: line source: outlines source-layer: districts paint: { line-color: "#1d3557" }That is the whole setup: no <script> to register anything, no extra import
map entry. Layers, popups, the legend, fitTo and clustering work exactly as
they do for any other source.
pmtiles://in front of the URL works too (url: pmtiles://data/x.pmtilesorpmtiles://https://example.org/x.pmtiles) — it’s the form MapLibre’s own examples use.format:names the format when the URL can’t — a signed link or an API endpoint with no file extension. It wins over the URL.
sources: parcels: type: geojson url: "https://data.example.org/api/parcels?f=fgb" format: flatgeobufPMTiles
Section titled “PMTiles”One .pmtiles file holds a whole tile pyramid. Vector, raster and elevation
archives all work:
sources: buildings: type: vector url: data/buildings.pmtiles # layers say which source-layer to draw imagery: type: raster url: data/imagery.pmtiles tileSize: 256 # most raster archives use 256-pixel tiles terrain: type: raster-dem url: data/terrain.pmtiles encoding: terrarium- The archive’s own metadata (zoom range, bounds, attribution, vector layer
names) reaches MapLibre, so you rarely need
minzoom/maxzoomhere. - Framing: a tile archive has no client-side extent to fit, so
fitTodoesn’t apply to PMTiles sources in 0.9 — setcenter/zoom(orbounds) for where the data is. - Making one:
tippecanoe -o buildings.pmtiles buildings.geojson(vector),ogr2ogr -f PMTiles out.pmtiles in.gpkg, orpmtiles convert in.mbtiles out.pmtilesfor an existing MBTiles file (a browser can’t read.mbtilesdirectly). - A page that registers its own
pmtilesprotocol keeps it. If your page already doesaddProtocol("pmtiles", …)through@maplibre-yaml/core/maplibre(the 0.7 way, still supported — for custom headers or a customSource), maplibre-yaml leaves that registration alone.
PMTiles inside a basemap style
Section titled “PMTiles inside a basemap style”A basemap style loaded from a URL can use PMTiles too, with no page
JavaScript: mapStyle: (or basemap: in format v2) can point at a style
whose sources are pmtiles:// URLs, such as a Protomaps basemap or one you
host yourself (below). maplibre-yaml registers the pmtiles:// protocol
before the map is created, so the style’s first requests find it. A page that
registered its own pmtiles protocol keeps it.
Relative URLs inside the style are relative to the style.json, the way a
stylesheet’s url() is relative to the stylesheet. That applies to a source’s
url (including the file inside pmtiles://), tiles, sprite and
glyphs. MapLibre by itself would resolve them against the page, which
breaks as soon as the page and the style live in different folders. base
applies to the style URL itself, so a style under base="/my-repo/" finds its
own files under /my-repo/ too. If the style names a file that isn’t there,
the map’s notice says which one, instead of showing a blank basemap.
Host your own basemap
Section titled “Host your own basemap”A basemap can be one PMTiles file next to your site, served by the same static
host, with no tile server and no API key. Point mapStyle: at the file
(mapStyle: data/nyc.pmtiles) and maplibre-yaml builds the whole
Protomaps basemap style around it, in the page:
-
Cut a regional extract from the Protomaps daily planet build with the
pmtilescommand-line tool:Terminal window pmtiles extract https://build.protomaps.com/<date>.pmtiles nyc.pmtiles \--bbox=-74.26,40.48,-73.70,40.92 --maxzoom=13Pick
<date>from build.protomaps.com. The tool reads only the part of the planet it needs. This extract is about 12 MB; without--maxzoom=13(all zoom levels, to 15) it is about 95 MB. Zoom 13 is enough for a city map: MapLibre draws closer zooms from the zoom-13 tiles, with fewer minor street names (see Size below). -
Put the file in your site:
public/data/nyc.pmtilesin an Astro or Vite project, or adata/folder next to your HTML page. -
Use it from your document:
config:center: [-73.98, 40.71]zoom: 12mapStyle: data/nyc.pmtilesThat is the light flavor, with labels in the page’s language. To choose, write
mapStyle:as a block:config:center: [-73.98, 40.71]zoom: 12mapStyle:pmtiles: data/nyc.pmtilesflavor: light # dark, white, grayscale or blacklang: en # optional: label language
That’s the whole basemap. Details:
-
Paths. A relative
pmtiles:path resolves against the page, andbaseapplies to it like to every other path in the document, so the same YAML works on a GitHub Pages project site (<ml-map base="/my-repo/">; the Astro components set it from your site’sbase). A full URL (https://…/nyc.pmtiles) or apmtiles://URL works too, from any server with range requests. -
Labels and icons come from Protomaps’ hosted fonts and sprites (
protomaps.github.io/basemaps-assets), so labels need that host to be reachable; when it isn’t, the map’s notice says so. To host everything yourself, copy the fonts and the sprite for your flavor from basemaps-assets and name them:mapStyle:pmtiles: data/nyc.pmtilesglyphs: fonts/{fontstack}/{range}.pbfsprite: sprites/v4/light -
Label language.
lang:picks the language labels are drawn in where OpenStreetMap has the name (en,es,fr,de,ja,zh-Hans, and about 40 more). Without it, labels follow the page’slangwhen the basemap has that language, and English otherwise. Arabic, Hebrew, Persian and Urdu labels (ar,he,fa,ur) are written right to left: maplibre-gl 6.9 and later draw them as they are; with an older maplibre-gl, maplibre-yaml sets MapLibre’s right-to-left text plugin, which the browser downloads from unpkg.com the first time such a label is drawn. -
Your layers draw on top of the basemap, as with any basemap.
before:can name a basemap layer to slide one underneath, for examplebefore: places_subplaceto keep neighborhood names above your data, and a story chapter can restyle basemap layers by id (water,earth,landuse_park,roads_minor,buildings, …;map.getStyle().layerslists them all). -
Attribution to OpenStreetMap and Protomaps is added to the map.
-
When something is wrong, the map shows a notice instead of a blank basemap: a file that isn’t there, a server without range requests, or a PMTiles file that isn’t a Protomaps basemap (your own tiles go in
sources:with layers). -
The style generator (
@protomaps/basemaps) is part of maplibre-yaml and loaded only by maps that use this form.
Size. Each extra zoom level makes an extract roughly three times bigger:
the New York extract above is about 12 MB at --maxzoom=13 and about 95 MB
with every zoom level (to 15). GitHub’s web upload (“Add file →
Upload files”) takes files up to 25 MB, git push warns above 50 MB and
refuses files over 100 MB, and a GitHub Pages site is limited to 1 GB. To go
smaller, cut a tighter --bbox around the area you show, or lower
--maxzoom (12 still reads well for a whole city). For larger areas, use a
host meant for big files, such as an object store.
Hosting. The file is read with HTTP range requests, like any PMTiles file.
To preview locally, serve the folder with npx serve (or astro dev);
published, GitHub Pages, Netlify, Cloudflare, S3 and R2 support them.
python -m http.server does not (see
Hosting).
A full style.json of your own
Section titled “A full style.json of your own”To change the basemap’s design beyond a flavor, use a style file instead.
Generate one for your file (Protomaps’
style generator, or
mlym emit on a document using the form above), save it as style.json
beside the file, edit it, and point its source at the file:
"url": "pmtiles://nyc.pmtiles". Its glyphs and sprite can stay on
Protomaps’ asset host, or be copied next to the style and named relatively.
Then:
config: mapStyle: basemap/style.json # public/basemap/ in an Astro or Vite siteRelative URLs inside the style resolve against the style (see PMTiles inside a basemap style).
FlatGeobuf
Section titled “FlatGeobuf”A .fgb file is read into an ordinary GeoJSON source, so everything that
works with a .geojson file works with it. Two ways to read it, with load::
load: | What it reads | Needs |
|---|---|---|
all (default) | The whole file, once | Any web server |
viewport | Only the features in view, re-read as the map moves; nothing below minzoom (default 10) | A file with a spatial index, and a server with range requests |
sources: districts: type: geojson url: data/districts.fgb # small file: read whole parcels: type: geojson url: https://data.example.org/parcels.fgb load: { mode: viewport, minzoom: 14 } # big file: only what's in viewSizes. Reading a file whole costs memory and time in the browser, so:
- up to 10 MB: read whole, no remarks;
- 10–50 MB: read whole, with a console note suggesting
load: viewportor PMTiles; - over 50 MB: not read whole. A file with a spatial index is read by view instead (with a console note); one without is refused, and the map says so.
As a rule of thumb, whole-file data is comfortable up to about 100,000 simple
features (~20 MB). Beyond that, use load: viewport, or PMTiles for anything
large and static.
- Coordinates: the file must be longitude/latitude (EPSG:4326). A file in
another projection is refused with the
ogr2ogrcommand that fixes it. - Compression: write the file uncompressed. A Zstandard-compressed
FlatGeobuf is recognized and refused with a notice saying so (
zstd -ddecompresses it). - Making one:
ogr2ogr -f FlatGeobuf -lco SPATIAL_INDEX=YES out.fgb in.geojson(the index is GDAL’s default; QGIS’s “Export → FlatGeobuf” works too). - Live data (
refresh:,stream:) isn’t available for FlatGeobuf sources in 0.9 — validation says so. Serve GeoJSON for data that changes. fitToframes the decoded features; withload: viewportit frames the extent recorded in the file’s header.classify:with computed classes (method:) needs the whole file. On a source read by view —load: viewport, or a file over 50 MB that switched to it — validation (forload: viewport) or the map (for the size switch) asks for explicitbreaks:instead, which work either way. See Thematic maps.
GeoParquet
Section titled “GeoParquet”A .parquet (or .geoparquet) file is read into an ordinary GeoJSON source,
like a FlatGeobuf file: layers, popups, the legend, fitTo, clustering and
classify: work as they do for a .geojson file.
type: mapid: geoparquet-wellsconfig: center: [10.2, 42.15] zoom: 10 mapStyle: "https://tiles.openfreemap.org/styles/positron" fitTo: { source: wells, padding: 20 }sources: wells: type: geojson url: /gallery-assets/cloud-native/points.parquetlayers: - id: wells type: circle source: wells paint: circle-radius: 4 circle-color: ["match", ["get", "kind"], "well", "#e63946", "station", "#457b9d", "#2a9d8f"] interactive: click: popup: - h3: [{ property: kind }] - p: [{ str: "Value: " }, { property: value }]Only the columns the map uses are read. GeoParquet stores each column
separately, so maplibre-yaml reads the geometry plus the properties the
document references for that source — here kind and value, not the rest
of the file. It looks at ["get", …] and ["has", …] in paint, layout
and filter, {name} tokens in text, popup and legend property: items,
classify: field and normalize, the source’s promoteId, filter
and clusterProperties, and in a story the chapters’ setFilter,
setPaintProperty and setLayoutProperty actions. On a wide file (dozens of columns) that is most of
the saving. To choose yourself, set columns::
sources: wells: type: geojson url: data/wells.parquet columns: [kind, value, owner] # or: columns: allTwo ways to read it, with load:, as for FlatGeobuf:
load: | What it reads | Needs |
|---|---|---|
all (default) | The columns it uses, from every row | Any web server |
viewport | Only the rows in view, re-read as the map moves; nothing below minzoom (default 10) | A bbox covering column, row groups sorted by location, and a server with range requests |
Reading by view. A GeoParquet 1.1 file can carry a bbox column (each
row’s extent), and the file’s footer records each row group’s range of those
values, so row groups outside the view are never downloaded. That only pays
off when nearby rows sit in the same row groups — a file sorted by location.
A file whose row groups each span most of the area is read whole instead,
with a console note; one without a bbox column is refused with a notice
that says how to add it. fitTo frames the extent recorded in the file’s
metadata.
Sizes. The same limits as FlatGeobuf, counted on the columns the map
reads (the footer gives their sizes before any data is downloaded): up to
10 MB read whole, 10–50 MB read whole with a console note, over
50 MB read by view if the file allows it, refused otherwise. A small file
(under 512 KB) usually arrives in one request; a host that answers 416 for
a small file’s end (GitHub Pages, npx serve) costs a second.
Without range requests (python -m http.server) the first request
returns the whole file. That is fine for a small file; a file over 50 MB is
refused before it downloads, and the notice explains why.
- Geometry encoding: WKB, the default of GeoPandas, GDAL and DuckDB.
GeoArrow-encoded geometry isn’t read yet; the notice gives the command that
rewrites the file as WKB (
ogr2ogr -f Parquet -lco GEOMETRY_ENCODING=WKB out.parquet in.parquet, or GeoPandasto_parquet(..., geometry_encoding="WKB")). - Coordinates: longitude/latitude (
OGC:CRS84, the GeoParquet default, or EPSG:4326). A file in another projection is refused with theogr2ogrcommand that fixes it. - Compression: snappy (the usual default), zstd, gzip, lz4 and brotli all work. Brotli’s decoder is large (about 68 KB), so it is downloaded only for a file that uses it.
- A plain Parquet file — one without GeoParquet’s geometry metadata, such as a table with a WKB column written by hand — is named as such in the notice.
- Live data (
refresh:,stream:) isn’t available for GeoParquet sources in 0.9 — validation says so. classify:with computed classes (method:) works on a file read whole. On a source read by view, validation (forload: viewport) or the map (for a file that switched to it) asks for explicitbreaks:instead.
Making a GeoParquet file
Section titled “Making a GeoParquet file”GDAL (ogr2ogr, also inside QGIS):
ogr2ogr -f Parquet out.parquet in.geojson -lco COMPRESSION=SNAPPY# for load: viewport — a bbox column, rows sorted by location, smaller row groupsogr2ogr -f Parquet out.parquet in.geojson -lco WRITE_COVERING_BBOX=YES \ -lco SORT_BY_BBOX=YES -lco ROW_GROUP_SIZE=10000Add -t_srs EPSG:4326 when the source data is in a projected CRS.
GeoPandas (Python):
import geopandas as gpdgdf = gpd.read_file("in.geojson").to_crs(4326)gdf.to_parquet("out.parquet") # WKB geometry, snappy# for load: viewport — sort by location, then write the bbox columngdf = gdf.iloc[gdf.geometry.hilbert_distance().argsort()]gdf.to_parquet("out.parquet", write_covering_bbox=True, row_group_size=10_000)DuckDB (with its spatial extension):
INSTALL spatial; LOAD spatial;COPY (SELECT * FROM ST_Read('in.geojson')) TO 'out.parquet' (FORMAT parquet);Whatever writes it, keep the file to the area and columns you show: a smaller file loads faster for every visitor.
Cloud Optimized GeoTIFF (COG)
Section titled “Cloud Optimized GeoTIFF (COG)”A COG is an ordinary GeoTIFF laid out for the web: its pixels are stored in tiles, with smaller copies (overviews) for zoomed-out views, so the browser reads only the parts it draws. Satellite and aerial imagery, elevation models and gridded measurements (vegetation, temperature) are often published this way.
Imagery
Section titled “Imagery”A .tif URL on a raster source:
sources: imagery: type: raster url: data/imagery.tif # or cog://data/imagery.tiflayers: - { id: imagery, type: raster, source: imagery }RGB, greyscale and paletted images draw in their own colours. maplibre-yaml
sets tileSize: 256 (the size the COG reader draws), unless you set one
yourself. A URL without a telling extension takes format: cog.
Elevation: hillshade and terrain
Section titled “Elevation: hillshade and terrain”The same kind of file on a raster-dem source is read as elevation (in
metres, from the first band). It drives hillshade layers and 3D terrain:
exactly like any other elevation source:
type: mapid: cog-elevationconfig: center: [10.2, 42.15] zoom: 10 pitch: 50 mapStyle: "https://tiles.openfreemap.org/styles/positron"sources: elevation: type: raster-dem url: /gallery-assets/cloud-native/elevation.cog.tifterrain: source: elevation exaggeration: 1.5layers: - id: hills type: hillshade source: elevation paint: { hillshade-exaggeration: 0.5 }You don’t write encoding:: the COG reader converts the heights to MapLibre’s
mapbox (Terrain-RGB) encoding, and maplibre-yaml sets that for you.
One band of values: colorRamp
Section titled “One band of values: colorRamp”A COG with a single band of measurements (a vegetation index, a temperature, a
probability) has no colours of its own. colorRamp: maps the values from
min to max onto a palette:
type: mapid: cog-vegetationconfig: center: [10.2, 42.15] zoom: 9.4 mapStyle: "https://tiles.openfreemap.org/styles/positron"sources: vegetation: type: raster url: /gallery-assets/cloud-native/ndvi.cog.tif colorRamp: { palette: YlGn, min: 0, max: 0.85 }layers: - { id: vegetation, type: raster, source: vegetation }| Key | Meaning |
|---|---|
palette | A palette name, the same ones classify: uses: viridis (the default), cividis, and the ColorBrewer names (YlGn, Blues, RdBu, Spectral …). A misspelt name gets a did-you-mean. |
colors | Your own colours instead of a palette, low to high: colors: ["#2c7bb6", "#ffffbf", "#d7191c"] |
min, max | The values drawn in the first and last colour (required). Values outside the range take the end colours. |
reverse | true runs the colours from high to low. |
classes | A number of flat classes (classes: 5) instead of a smooth ramp: the palette’s own set for that many classes, or that many colours spread along your colors:. Qualitative palettes (okabe-ito, Set1 …) need it: as a smooth ramp they blend unrelated colours. |
Pixels the file marks as having no value (its NoData value, or NaN) are left
transparent. colorRamp: applies to raster sources only; in a format v2
document it goes under the source’s runtime:.
Making a COG
Section titled “Making a COG”GDAL (3.1 or later; it also ships with QGIS) writes COGs. One command reprojects any georeferenced raster to Web Mercator and writes it as a COG:
gdalwarp -t_srs EPSG:3857 -of COG -co COMPRESS=DEFLATE input.tif output.tifA GeoTIFF that is already in Web Mercator but isn’t cloud-optimized (stored in strips, or without overviews) is converted with:
gdal_translate -of COG -co COMPRESS=DEFLATE input.tif output.tif-co COMPRESS=DEFLATEis lossless;-co COMPRESS=JPEGmakes imagery much smaller (lossy, for photos only, not for elevation or measurements).-co TILING_SCHEME=GoogleMapsCompatible(withgdalwarporgdal_translate) also aligns the file with web map tiles, which makes reads a little more efficient.- Check a file with
gdalinfo output.tif: it should reportCoordinate System is: … "WGS 84 / Pseudo-Mercator",LAYOUT=COGand overviews.
An ordinary GeoTIFF still draws, but the browser may have to download most of
the file to show it, so while you develop the map’s notice says so, with the
gdal_translate command.
Size and other details
Section titled “Size and other details”A COG can be any size: the browser reads only the tiles in view, at the
resolution the view needs, through the overviews. The limits are the host’s:
GitHub refuses files over 100 MB in a repository and a GitHub Pages site is
limited to 1 GB in total, so for a large mosaic use an object store (S3, R2,
Google Cloud Storage) and point url: at it. Clip the file to the area you
show (gdalwarp -te …), and pick the compression that suits the data
(above), to keep it small.
- Framing:
fitTo: { source: imagery }frames the COG’s own extent, read from the file. - A page that registers its own
cogprotocol keeps it. If your page already callsaddProtocol("cog", …)through@maplibre-yaml/core/maplibre(for example to send request headers), maplibre-yaml leaves it alone. - maplibre-gl 4.5 or later is needed for COGs (the COG reader’s own minimum); on an older one the map’s notice says so.
- Content Security Policy: the COG reader decodes images in background
workers it starts from
blob:URLs. A site with a strict CSP needsworker-src 'self' blob:; without it, the map’s notice says the COG couldn’t be read and why.
Hosting: range requests and CORS
Section titled “Hosting: range requests and CORS”PMTiles, COG and GeoParquet always, and FlatGeobuf with load: viewport, read
files in parts with HTTP range requests. GitHub Pages, Netlify, Cloudflare
Pages, Vercel, S3, R2, raw.githubusercontent.com and fastly.jsdelivr.net
support them, and so do npx serve, astro dev and astro preview on your
computer; python -m http.server doesn’t, and cdn.jsdelivr.net doesn’t
reliably. A file on another site also needs CORS that allows the Range
header and exposes Content-Range. On a GitHub Pages project site, set
base and write paths as you would locally (data/x.pmtiles,
pmtiles://data/x.pmtiles).
The full table of hosts, the CORS rule for S3 and R2, and base are in
Publishing & subpaths.
When something goes wrong
Section titled “When something goes wrong”Problems with one file never take down the map: the basemap and every other layer still draw, and the map’s notice (“Part of this map couldn’t load”) says what happened in plain words. The common cases:
| What happened | What the notice says |
|---|---|
| The server doesn’t support range requests | ”…/x.pmtiles was served without range-request support (the server sent the whole file instead of the part asked for). PMTiles, FlatGeobuf, GeoParquet and COG files need a web server that supports HTTP range requests: GitHub Pages, Netlify, Cloudflare, S3 and R2 do; python -m http.server does not — preview locally with npx serve instead.” |
| A host that compresses the parts of files it sends (cdn.jsdelivr.net) | “…/x.pmtiles comes from a server that compresses the parts of files it sends, so the PMTiles reader can’t read the file a part at a time. Put the file on your own site (GitHub Pages, Netlify and npx serve work); a file mirrored from GitHub works from fastly.jsdelivr.net, not cdn.jsdelivr.net.” |
| Another site refused (CORS), or it’s unreachable | ”The browser couldn’t read …: the server didn’t allow this page to load it (CORS), or it couldn’t be reached. A PMTiles file on another site needs that site to send Access-Control-Allow-Origin and to allow the Range header.” |
| A wrong path that returned a web page | ”…/x.pmtiles isn’t a PMTiles file. A wrong path often returns a web page instead — check the file name and folder.” |
| The file isn’t there | ”…/x.fgb didn’t load (404 Not Found)”, with where the browser looked and the base hint for project sites |
| Too large to read whole, no index | ”…/x.fgb is 180 MB, too large to load whole in a browser. Add load: viewport to the source (the file needs a spatial index), or convert it to PMTiles.” |
| The format’s reader couldn’t be downloaded | ”The PMTiles reader (…/chunks/pmtiles.js) couldn’t be downloaded. maplibre-yaml loads its file-format readers on demand from the folder its script came from: if you copied maplibre-yaml to your own site, copy the whole dist/ folder (not just register.js), or load it from the CDN. …” |
| A FlatGeobuf in the wrong projection | ”…/x.fgb is in EPSG:3857; MapLibre needs longitude/latitude (EPSG:4326). Reproject it: ogr2ogr -t_srs EPSG:4326 out.fgb x.fgb.” |
A mapStyle: { pmtiles: … } file that isn’t a Protomaps basemap | ”…/x.pmtiles doesn’t look like a Protomaps basemap: its layers are …, while a Protomaps basemap has earth, water, roads and places.” A basemap file that fails to load starts with “The basemap didn’t load:” and the reason, as above. |
| A Protomaps file cut before the current tile schema (late 2024) | “…/x.pmtiles is a Protomaps basemap built with an older tile schema (version 3.7.1), so only land and water draw … Cut a new extract with pmtiles extract from https://build.protomaps.com.” |
| A GeoParquet file with GeoArrow geometry | ”…/x.parquet stores its geometry as GeoArrow (point), which maplibre-yaml doesn’t read yet: it reads GeoParquet with WKB geometry. Rewrite it with WKB: ogr2ogr -f Parquet -lco GEOMETRY_ENCODING=WKB out.parquet x.parquet, or GeoPandas to_parquet(..., geometry_encoding="WKB").” |
| A Parquet file without geometry metadata | ”…/x.parquet is a Parquet file but not GeoParquet (it has no geometry metadata). Write it with GeoPandas to_parquet, ogr2ogr -f Parquet, or DuckDB’s spatial extension.” |
| A COG that isn’t in Web Mercator | ”…/x.tif is in EPSG:4326; a COG must be in Web Mercator (EPSG:3857) to be drawn. Reproject it: gdalwarp -t_srs EPSG:3857 -of COG -co COMPRESS=DEFLATE x.tif out.tif (GDAL 3.1 or later).” |
| A GeoTIFF that isn’t cloud-optimized (a hint while developing; it still draws) | “…/x.tif is a GeoTIFF but not a Cloud Optimized one (it has no internal tiles or no overviews), so the browser may download most of the file to draw it. Convert it: gdal_translate -of COG -co COMPRESS=DEFLATE x.tif out.tif.” |
| A strict Content Security Policy | ”The Cloud Optimized GeoTIFF (COG) reader couldn’t be set up (this page’s Content Security Policy doesn’t allow the background workers COG images are decoded in; allow them with worker-src 'self' blob:) …” |
The readers for these formats are not part of maplibre-yaml’s main script.
A page downloads a reader from dist/chunks/, next to the script, only when
a map uses that format: PMTiles about 8 KB gzip, FlatGeobuf about 12 KB,
GeoParquet about 23 KB (plus about 68 KB for a brotli-compressed file), COG
about 132 KB, and the Protomaps basemap style generator about 7 KB. From a
CDN that is automatic; if you host maplibre-yaml’s files yourself, copy the
whole dist/ folder
(Installation).
Exporting with mlym emit
Section titled “Exporting with mlym emit”A style.json can carry a URL but not a file reader, so:
-
PMTiles export as
pmtiles://URLs (a.pmtilesURL gains the scheme). The style then needs a renderer that readspmtiles://, such as maplibre-gl JS with the protocol registered. With a bundler, afternpm install pmtiles:import maplibregl from "maplibre-gl";import { Protocol } from "pmtiles";maplibregl.addProtocol("pmtiles", new Protocol().tile);In a plain HTML page, import
addProtocolfrom themaplibre-glyour import map names andProtocolfromhttps://esm.sh/pmtiles@4(esm.sh resolves its dependencies; unpkg does not), before the map is created. -
A Protomaps basemap (
mapStyle: { pmtiles: … }) exports as the full style it generates: every layer, the file aspmtiles://…, and the glyph and sprite URLs. It needs the samepmtilesregistration. A relative file path is written as it is, so it resolves against whichever page loads the style; give a full URL for a style used anywhere else. Labels are inlang:(English when unset: an exported style has no page language to follow). -
FlatGeobuf and GeoParquet are converted:
mlym emitreads the file (next to the YAML, in the current folder or itspublic/, or over http(s)) and writes its features into the style as GeoJSON — for GeoParquet, every column, or thecolumns:you listed. If it can’t read the file, the source is emitted empty and reported, and--strictrefuses. -
COG sources export as
cog://URLs, in the form the map itself uses:#demon an elevation source, acolorRamp:written out as the COG protocol’s#color:fragment, andtileSize: 256. As with PMTiles, the style then needs the protocol registered — here the one from@geomatico/maplibre-cog-protocol, which maplibre-yaml uses itself. With a bundler, afternpm install @geomatico/maplibre-cog-protocol:import maplibregl from "maplibre-gl";import { cogProtocol } from "@geomatico/maplibre-cog-protocol";maplibregl.addProtocol("cog", cogProtocol);In a plain HTML page, load the plugin’s own browser build (
<script src="https://unpkg.com/@geomatico/maplibre-cog-protocol@0.10/dist/index.js"></script>, which definesMaplibreCOGProtocol) and calladdProtocol("cog", MaplibreCOGProtocol.cogProtocol)on themaplibre-glyour import map names, before the map is created.
See Export classes for the full table.
Format v2
Section titled “Format v2”In a format v2 document the URL stays in the source and
format: / load: / columns: / colorRamp: move under the source’s runtime:.
A Protomaps basemap is written under style: as
basemap: data/nyc.pmtiles or basemap: { pmtiles: data/nyc.pmtiles, flavor: dark },
the same forms as mapStyle:.
version: 2type: mapid: cloud-native-v2style: basemap: "https://tiles.openfreemap.org/styles/positron" center: [10.2, 42.15] zoom: 10 sources: outlines: { type: vector, url: /gallery-assets/cloud-native/districts.pmtiles } points: type: geojson url: /gallery-assets/cloud-native/points.fgb runtime: load: { mode: viewport, minzoom: 11 } districts: type: geojson url: /gallery-assets/cloud-native/districts.parquet runtime: columns: [name, category] vegetation: type: raster url: /gallery-assets/cloud-native/ndvi.cog.tif runtime: colorRamp: { palette: YlGn, min: 0, max: 0.85 } layers: - { id: vegetation, type: raster, source: vegetation } - { id: districts, type: fill, source: districts, paint: { fill-color: "#e9c46a", fill-opacity: 0.5 } } - { id: outlines, type: line, source: outlines, source-layer: districts } - { id: points, type: circle, source: points }