Fast contour vector-tile generator from contour lines or elevation rasters.
Turns contour lines — FlatGeobuf, GeoPackage, shapefiles, anything GDAL reads — or a DEM into clustered PMTiles vector tiles, ready for MapLibre GL, Martin or any PMTiles server. Scales from a single raster to a worldwide contour layer in one file.
Built as a fast Rust replacement for the tilemaker contour pipeline (tilemaker → mbtiles → pmtiles convert) — it reads the same config.json, uses all CPU cores via rayon, and keeps working where tilemaker runs out of memory and disk:
- Builds per country, merges into one file.
--shardstiles each group of sources (a country folder) on its own, reuses every group whose inputs did not change, and merges them. Replacing one country re-tiles only that country. - Compact inputs.
corrie packstores contour lines in.cpkpacks, about 3.6× smaller than shapefiles and read in place — no ingest step, no temp cache. - Checkable.
corrie comparetells whether two builds hold the same lines in every tile, exactly or within N pixels. - Safe for long runs. The output is written beside the live file and swapped in when complete; Ctrl+C or a closed terminal cleans up after itself.
- Inspectable.
corrie unpackwrites packs back out as GeoPackage, FlatGeobuf, GeoJSON or Shapefile for QGIS — neighbouring countries in one file, to check their lines meet at the border.
Three ways to get corrie, easiest first.
Only Docker is required. Run the published image (amd64 and arm64):
cd /path/to/your/data
docker run --rm -v "$PWD:/data" ghcr.io/mapriot/corrie tile config.json contours.pmtilesThe current directory is bind-mounted at /data, where corrie runs: relative paths, and the sources a config lists, are resolved inside it. To build the image yourself instead: git clone https://github.com/mapriot/corrie && docker build -t corrie corrie.
Needs GDAL installed — see prerequisites below.
cargo install corriegit clone https://github.com/mapriot/corrie
cd corrie
cargo build --release # binary at target/release/corrieOn macOS with Homebrew GDAL you may need PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig" cargo build --release.
Native builds (crates.io / source) need GDAL on your system (its development packages also bring SQLite, which corrie links for MBTiles):
| Platform | Command |
|---|---|
| macOS | brew install gdal |
| Ubuntu / Debian | sudo apt install libgdal-dev gdal-bin |
| Fedora / RHEL | sudo dnf install gdal-devel |
| Windows | OSGeo4W or Conda — ensure gdal-config is on your PATH (untested) |
Supported GDAL versions: 3.5–3.12 — the versions the gdal Rust crate ships bindings for. GDAL 3.13 and newer are not yet supported and will fail to build. If your package manager installs 3.13+ (current Homebrew gdal does), install a supported build via conda-forge — conda install -c conda-forge 'gdal>=3.5,<3.13' — use Docker, or build against the gdal crate's main branch:
git clone https://github.com/georust/gdal ../georust-gdal
cargo install --path . \
--config 'patch.crates-io.gdal.path="../georust-gdal"' \
--config 'patch.crates-io.gdal-sys.path="../georust-gdal/gdal-sys"'| Command | What it does |
|---|---|
tile |
Contour lines listed in a config.json → clustered PMTiles; --shards for per-country builds |
pack |
Vector files → compact .cpk packs that tile reads in place |
unpack |
Packs → GeoPackage, FlatGeobuf, GeoJSON or Shapefile, e.g. for QGIS |
merge |
Join archives written by tile (e.g. one per country) into one |
compare |
Do two archives hold the same lines, tile by tile? |
build |
A DEM → contour tiles directly (PMTiles or MBTiles) |
corrie <command> --help lists every option.
Every command below is run, in order, by examples/readme.sh on two neighbouring tiles of the Copernicus GLO-30 DEM in the Alps (47–48°N; west.tif 12–13°E, east.tif 13–14°E), which it downloads; the outputs shown are from that run. CORRIE_IMAGE=ghcr.io/mapriot/corrie examples/readme.sh runs them with the Docker image.
# A DEM straight to contour tiles — 50 m lines from z11 down to 5 m at z14
corrie build dem.tif contours.pmtiles
# Contour lines (made with gdal_contour) listed in a tilemaker-style config.json
corrie tile config.json contours.pmtiles
# Many regions: one archive per region folder, reused while unchanged, merged into one file
corrie tile world.json world.pmtiles --shards groups/
# Pack a region's contour lines into one compact .cpk
corrie pack contours/west/*.fgb -o contours/west/west.cpk
# Do two builds hold the same lines in every tile?
corrie compare old.pmtiles new.pmtiles
# Neighbouring regions' lines in one file for QGIS
corrie unpack border.gpkg contours/west/west.cpk contours/east/east.cpkTo look at a result, drop the .pmtiles file on pmtiles.io, or style it with MapLibre (see Using with MapLibre GL).
build contours one raster (or a VRT mosaic) tile by tile, any format and CRS GDAL reads, with configurable intervals (see corrie build). For large areas, built and updated country by country, contour with GDAL and use tile.
Make the lines with GDAL (and clip or filter them as you like), then tile them. The lines must be in WGS84 longitude/latitude, with the elevation in a column (elevation by default):
gdalwarp -t_srs EPSG:4326 dem.tif dem-4326.tif # if the DEM is in another CRS
gdal_contour -a elevation -i 10 dem-4326.tif contours.fgbconfig.json (tilemaker's format, the fields corrie reads):
{
"settings": {
"minzoom": 11,
"maxzoom": 14,
"bounding_box": [-180, -85, 180, 85]
},
"layers": {
"contour": {
"minzoom": 11, "maxzoom": 14,
"source": "contours.fgb",
"simplify_below": 14, "simplify_level": 0.00005, "simplify_ratio": 2
}
}
}corrie tile config.json contours.pmtilesPut each region's (or country's) lines in its own folder, pack each into one file, and list every pack in a config, all writing to one layer:
mkdir -p contours/west contours/east
gdal_contour -a elevation -i 10 west.tif contours/west/lines.fgb
gdal_contour -a elevation -i 10 east.tif contours/east/lines.fgb
corrie pack contours/west/*.fgb -o contours/west/west.cpk
corrie pack contours/east/*.fgb -o contours/east/east.cpk
corrie tile world.json world.pmtiles --shards groups/world.json:
{
"settings": { "minzoom": 11, "maxzoom": 14, "bounding_box": [-180, -85, 180, 85] },
"layers": {
"east": { "minzoom": 11, "maxzoom": 14, "source": "contours/east/east.cpk", "write_to": "contour",
"simplify_below": 14, "simplify_level": 0.00005, "simplify_ratio": 2 },
"west": { "minzoom": 11, "maxzoom": 14, "source": "contours/west/west.cpk", "write_to": "contour",
"simplify_below": 14, "simplify_level": 0.00005, "simplify_ratio": 2 }
}
}The tile run (progress lines left out):
2 groups in "groups/"
[1/2] east building from 1 source
[1/2] east built: 68076 features, 4336 tiles in 0.3s
[2/2] west building from 1 source
[2/2] west built: 75384 features, 4199 tiles in 0.3s
Merging 2 group archives into "world.pmtiles" ...
Merged: 8404 tiles, 131 joined from several groups, in 0.1s
Done in 0.7s: 2 groups (2 built, 0 reused), 8404 tiles
When a region's lines change, pack it again and run the same tile command: only that region is tiled again, the others are reused, and everything is merged.
corrie pack contours/east/*.fgb -o contours/east/east.cpk --force
corrie tile world.json world.pmtiles --shards groups/2 groups in "groups/"
[1/2] east building from 1 source
[1/2] east built: 68076 features, 4336 tiles in 0.3s
[2/2] west reused
Merging 2 group archives into "world.pmtiles" ...
Merged: 8404 tiles, 131 joined from several groups, in 0.1s
Done in 0.4s: 2 groups (1 built, 1 reused), 8404 tiles
- Fixed attributes.
tilewrites one MVT layer whose lines carryelevation,mainandshow, with fixed zoom rules for 100, 50, 20, 10 and 5 m lines (see Attribute derivation). Other rules mean changingsrc/attribute_rules.rs.buildhas options for its intervals andmain/show. - WGS84 input. Lines for
tileandpackmust be in longitude/latitude (EPSG:4326); they are not reprojected. - Contours only. corrie reads the contour subset of tilemaker's config (no OSM data, no Lua) and writes gzipped MVT.
Given a tilemaker config whose sources are contour lines (for contour data,
typically many layers that all write_to one output layer), corrie tile
produces a single clustered PMTiles file with the same layer schema. A
source is a pack (.cpk, see corrie pack),
a shapefile, or any other vector file GDAL reads, in WGS84
longitude/latitude; one config can mix them.
corrie tile <CONFIG.json> <OUTPUT.pmtiles> [OPTIONS]| Option | Description |
|---|---|
--shards <DIR> |
Tile each group of sources into DIR/<group>.pmtiles, reuse group archives whose inputs are unchanged, then merge them into OUTPUT. See Per-group builds. |
--cache-dir <DIR> |
Keep the packs made from sources that are not packs in DIR, and reuse each while its source is unchanged. Without it, they go to a temp dir that is removed when the run ends. Not with --shards. |
--rebuild-cache |
Pack the sources again even if their packs in --cache-dir are current. |
-j, --workers <N> |
Worker threads. Default: all cores. |
- Packs. Sources that are packs are opened as they are. Every other source is first packed (in parallel) into the cache (see The cache). A pack stores each line in pieces of 128 segments with a spatial index over the pieces, so a tile only reads the parts of a long contour line that reach it.
- Tiles. Starting from the lowest-zoom tiles that intersect the
config's
bounding_box, every tile that holds data is visited, then built in parallel: query each pack's index, decode the pieces, clip each line to the tile, simplify (Douglas-Peucker, tilemaker's per-zoom rules), quantize to the 4096 tile grid, group lines by attributes, encode MVT, gzip. - Write. Tiles are appended in Hilbert order into a clustered PMTiles v3 archive with leaf directories (see Outputs).
Every line is clipped, simplified and quantized on its own; lines are only
grouped by attributes at the end. This is what makes per-group builds and
corrie merge exact.
The cache holds the packs of sources that are not packs. A config whose sources are all packs needs none.
- Default (no
--cache-dir): the packs go to a temp dir ($TMPDIR/corrie-cache-<pid>-*) that is removed when the run ends. --cache-dir <DIR>: the packs are kept inDIR(<name>-<hash of the source path>.cpk), and each is reused while its source has the same path, size, modification time and elevation column.--rebuild-cachepacks every source again.- Stopping a run with Ctrl+C, SIGTERM or SIGHUP (a closed terminal or
tmux pane) removes the temp cache and the run's
.partialoutput, and exits with status 130, also when the terminal or the pipe behind stderr is already gone. A file already atOUTPUTis left as it was. A run started withnohupkeeps ignoring SIGHUP. In a kept--cache-dir, a pack is written as.partialand renamed when complete, so a stopped run leaves no half-written pack behind. A run that fails with an error removes its.partialoutput too. - SIGKILL or an out-of-memory kill leaves the temp cache (and the
.partialfile) behind; the nextcorrie tilerun that uses a temp cache removes temp caches of processes that no longer run.
Disk: a pack takes about 4.6 bytes per vertex (a billion vertices:
about 4.6 GB). Use --shards to keep the temp cache at
the size of the largest group, or pack the sources once with corrie pack.
corrie tile world.json world.pmtiles --shards groups/The config's sources are grouped by folder: the first directory below the
deepest directory that contains all the source files. With sources in
contours/cz/data/merged_contours_1.shp and contours/at/final_contours_0_1.cpk
the groups are cz and at. Files that lie directly in that common folder
form one group each, named after the file.
For each group, in name order:
- If
DIR/<group>.pmtilesexists and was built from the same inputs, it is reused. The archive's metadata holds a key over the group's layer settings, the zoom range and bounding box, the path, size and modification time of each source file (and of an SHP's.dbf), and corrie's tiling version (bumped when a release changes the tiles it writes). Switching a source from its SHP to its pack changes the key, so that group is tiled once more; deleting the SHP afterwards does not. - Otherwise the group is tiled with its own temp cache into
DIR/<group>.pmtiles.partial, which is renamed to<group>.pmtileswhen complete.
Then all group archives are merged into OUTPUT (see corrie merge): tiles
inside one group are copied as stored, tiles on group borders are joined.
See Many regions, one file for a run.
The output holds the same tiles and lines as a single run over all sources
(corrie compare reports them identical; line order inside a tile can
differ). The temp cache only ever holds one group's packs, the group
archives stay in DIR (together about the size of the output), and a rerun
only tiles new or changed groups. Archives in DIR that belong to no group
of the config, and .partial files left by a killed run, are reported and
left alone.
tile and merge write OUTPUT.partial next to OUTPUT and rename it to
OUTPUT when it is complete:
- A file already at
OUTPUTstays valid and readable until then, so a tile server can keep serving it, andmergecan write over one of its inputs. The disk needs room for both the old and the new file. OUTPUTis checked before any work starts: its folder must exist, and an existingOUTPUTmust be a regular file (not a folder or a device).- If
OUTPUTis a symlink, the new file is written next to the file it points to and replaces that file; the link stays. Other hard links to the old file keep the old content. - The
.partialfile is locked while a run writes it. A second run writing the sameOUTPUT(or two--shardsruns on oneDIR) stops with "another corrie is writing …". - If the final rename fails, the finished file is kept at
OUTPUT.partialand the error says so.
corrie tile parses the subset of the tilemaker config relevant for
contour lines. Unknown fields (OSM keys, Lua hooks, etc.) are silently
ignored.
Top-level settings:
| Field | Notes |
|---|---|
minzoom, maxzoom |
Required. Inclusive range of zoom levels to write. |
basezoom |
Optional. Not used for tile output (informational). |
include_ids |
Optional. Read but not used: features always carry an id. |
bounding_box |
Required. [minLon, minLat, maxLon, maxLat]. Tiles are written within the lowest-zoom (minzoom) tiles that intersect this box; data outside those tiles is ignored. |
compress |
Optional. Ignored: tiles are always gzipped (PMTiles default). |
name, version, description |
Read but not written to the output. |
Per layer in layers:
| Field | Notes |
|---|---|
minzoom, maxzoom |
Required. Layer-level zoom gate (in addition to per-feature minzoom). |
source |
Required. Path to a pack (.cpk), a .shp file or another vector file GDAL reads. Relative paths resolve against the config file's directory. |
source_columns |
Optional. The first entry is the column that holds the elevation (default "elevation"); not used for packs. |
write_to |
Optional. Names this layer's output layer. corrie writes a single MVT layer: it is named after the output layer of the alphabetically first layer key, and every layer's lines go into it. |
simplify_below |
Optional. Default 0. Douglas-Peucker simplification runs when z < simplify_below. |
simplify_level |
Optional. Default 0.01. Base DP epsilon in degrees of longitude/latp. |
simplify_length |
Optional. Default 0. Alternative base epsilon in metres (matches tilemaker's code). Takes precedence over simplify_level when set. |
simplify_ratio |
Optional. Default 2.0. Per-zoom scaling factor: epsilon at zoom z is base * simplify_ratio^(simplify_below - 1 - z). |
simplify_algorithm |
Optional. Only Douglas-Peucker is implemented; other values are ignored. |
At every zoom, including simplify_below and above, the tolerance is at
least half a tile pixel, so vertices closer together than that are dropped.
Distances are measured to the segment between two kept vertices, so a line
that runs out and back keeps its tip when the tip lies farther than the
tolerance.
Each contour line is tagged with elevation, plus main and show
booleans, based on its elevation in metres. The rules are hard-coded:
| Elevation | minzoom |
main |
show |
|---|---|---|---|
% 100 == 0 |
11 | 1 | 1 |
% 50 == 0 (not 100) |
11 | 1 | — |
% 20 == 0 (not 50) |
12 | — | — |
% 10 == 0 (not 20) |
13 | — | — |
| otherwise (e.g. 5 m steps) | 14 | — | — |
minzoom here is the per-feature zoom gate: the feature is emitted only at
tiles whose zoom is ≥ its computed minzoom and also within the layer's
own minzoom–maxzoom range.
One layer, named after the output layer (write_to, or the layer key) of
the alphabetically first layer key in the config; for contour configs where
every layer writes to contour, that is contour. Each feature has:
elevation: integer (metres)main: integer1when present (omitted otherwise)show: integer1when present (omitted otherwise)
Geometry is LineString or MultiLineString — sub-linestrings sharing the
same (elevation, main, show) attributes within a tile are packed into one
MultiLineString feature to keep tile size down.
A minimal config is in the Quick start.
corrie pack <SOURCE>... [-o PACK] [--field elevation] [--drop-seams DEG] [--force] [-j N]
corrie pack --config <CONFIG.json> [--per-group] [--only GROUPS] [--out-config FILE] [--drop-seams DEG] [--force] [-j N]A pack (.cpk) holds one source's lines and their elevations, and nothing
else: about 4.6 bytes per vertex, against about 16.6 for a shapefile set
(.shp, .shx, .dbf). corrie tile reads packs in place (memory-mapped),
with no ingest step and no temp cache, and gives the same tiles as from the
source the pack was made from.
- Coordinates are stored on a 2^32 grid over Web Mercator: a vertex moves by less than 1 cm. Vertices that fall into the same grid cell as the one before are merged.
- Elevation is the source column (
--field, or with--configeach layer'ssource_columns), rounded down to whole metres. Other columns are not kept. - Lines are stored in pieces of 128 segments with a spatial index over the pieces, so a tile reads only the parts of a line that reach it.
--drop-seams DEGleaves out every feature whose vertices, over all its parts, all lie withinDEGdegrees of a whole-degree meridian or parallel. Contouring a mosaic of 1° DEM tiles (JAXA AW3D30, for example) traces such lines where one tile's data meets nodata. A feature with one vertex elsewhere is kept whole. The run reports how many were left out.
Each source becomes <name>.cpk next to it, or with -o PACK all of them
become one pack, in the order given: a region tiled in pieces (a mosaic of
contour files) is best packed into one file. Sources can be packs too: their
lines are copied as stored, so packing packs again loses nothing
(--drop-seams applies to vector files only).
A pack that is up to date (made from the same sources, in the same order,
with the same sizes, modification times, elevation column and --drop-seams)
is kept unless --force is given; packs are written as .partial and renamed
when complete. The run ends with the sizes:
1 pack: 0.30 GB of sources -> 0.08 GB of packs (3.7x smaller), 0.3s
With --config, pack packs the sources of a tilemaker config and writes a
copy of the config, <config>-packs.json (or --out-config, which may be
the config itself), whose sources are the packs. Only the source strings
change, so the copy keeps the config's layout and a diff shows just them.
--only east packs and switches only that group (the folder names
--shards uses), to move a few regions at a time. With lines.json being
world.json with each region's lines.fgb as its source:
corrie pack --config lines.json --only east
diff lines.json lines-packs.json
corrie tile lines-packs.json lines-packs.pmtiles --shards groups-packs/
corrie compare world.pmtiles lines-packs.pmtiles # IDENTICAL4c4
< "east": { "minzoom": 11, "maxzoom": 14, "source": "contours/east/lines.fgb", "write_to": "contour",
---
> "east": { "minzoom": 11, "maxzoom": 14, "source": "contours/east/lines.cpk", "write_to": "contour",
Once a config reads a pack, its source files are no longer needed for
tiling; with --shards, deleting them does not rebuild the group.
--per-group packs each group's sources, files or packs, into one pack,
<folder>/<group>.cpk, and writes a config copy with one layer per group,
named after it. It turns a config of many files per country into one pack
per country, for example after shapefile size limits forced a country into
tiles:
corrie pack --config lines.json --per-group --out-config regions.json # contours/east/*.fgb -> contours/east/east.cpk, …
corrie tile regions.json regions.pmtiles --shards groups-regions/
corrie compare world.pmtiles regions.pmtiles # IDENTICALA group's layers must agree on everything but their source; the rewritten
layers name their output layer (write_to) explicitly.
corrie unpack <OUTPUT> <PACK>... [--bbox min_lon,min_lat,max_lon,max_lat]Writes the lines of packs to one file, in WGS84, the format chosen by the
extension: .fgb (FlatGeobuf), .gpkg, .geojson or .shp. Each line has
elevation, main, show (as in the tiles), source (the pack's file
name) and folder (the pack's folder, for example its country), so a country
and its neighbours can be checked in QGIS side by side, styled by folder:
corrie unpack border.fgb contours/west/west.cpk contours/east/east.cpk --bbox 12.95,47,13.05,48With --bbox, only lines touching the box are written, each one whole: here the lines along the regions' shared edge at 13°E.
corrie merge <OUTPUT.pmtiles> <INPUT.pmtiles>... [-j N]Merges archives written by corrie tile, for example one per country, into
one clustered archive. A tile present in one input is copied byte for byte.
A tile present in several inputs (on their borders) is decoded, its lines
are combined per (elevation, main, show), and it is encoded again.
Because tiling is per line, merging archives tiled from separate sources gives the same tiles and lines as tiling all the sources in one run. Build the inputs with the same zoom range and layer settings. All inputs must use the same layer name, which the output keeps; an input listed twice is refused. The output's zoom range spans all inputs and its bounds are the union of theirs.
corrie merge both.pmtiles groups/west.pmtiles groups/east.pmtiles # the same tiles as world.pmtilesThe output is written as OUTPUT.partial and renamed when complete (see
Outputs), so it can be one of the inputs, for example to add a
region to a file:
cp groups/west.pmtiles atlas.pmtiles
corrie merge atlas.pmtiles atlas.pmtiles groups/east.pmtilesmerge reads only corrie's contour schema; it is not a general PMTiles
merger. It holds the directory entries of all inputs in memory, about 60
bytes per tile (25 million tiles: about 1.5 GB).
corrie compare <A.pmtiles> <B.pmtiles> [--show 10] [--tolerance PX] [--json FILE]Matches tiles by z/x/y. Tiles with different bytes are decoded and their
lines compared per (elevation, main, show) group, ignoring line order.
Exits 0 when the archives are identical (or, with --tolerance, when every
difference is within it), 1 otherwise.
Comparing the two regions with the west one alone:
corrie compare world.pmtiles groups/west.pmtiles --show 2tiles: a=8404 b=4199 common=4199
only in a: 4205 11/1103/719 11/1102/719
only in b: 0
common tiles with different lines: 131
a has every line of b, plus more: 131
11/1097/719 points a=13764 b=13101
11/1097/718 points a=14859 b=14117
Error: archives differ
A differing tile is reported as one of:
- a has every line of b, plus more — e.g. the 131 tiles above along the regions' shared edge, which also hold the east's lines.
- b has every line of a, plus more — the same, the other way round.
- lines changed — neither contains the other: lines moved or differ.
--tolerance PX compares archives whose lines are expected to move a
little, for example builds by two corrie versions
(corrie compare world.pmtiles regions.pmtiles --tolerance 8). Lines then count as the
same when every vertex inside the tile lies within PX tile pixels of the
other side's lines, and the other way round. A tile is 4096 tile pixels and
is drawn 512 screen pixels wide, so --tolerance 8 is one screen pixel.
Deviations are printed in both units, and two more kinds appear:
- within the tolerance — the lines differ, but by at most
PX. - only on one side, a sliver at the tile edge — a tile only one archive
has, whose lines all lie within
PXof the tile's edge: a line moved just across it. Such tiles count as matching and are not listed under "only in a" or "only in b".
--json FILE writes every differing tile with its kind, point counts and
largest deviation.
For a single elevation raster, skip the gdal_contour → shapefile step and generate contour tiles directly. Uses marching-squares per tile.
corrie build <INPUT> <OUTPUT> [OPTIONS]| Option | Default | Description |
|---|---|---|
<INPUT> |
— | Elevation raster (GeoTIFF, VRT, any GDAL format/CRS) |
<OUTPUT> |
— | .pmtiles or .mbtiles (chosen by extension) |
--min-z <N> |
11 |
Minimum zoom |
--max-z <N> |
14 |
Maximum zoom |
--levels <PAIRS> |
11:50,12:20,13:10,14:5 |
Zoom:interval pairs |
--main-every <N> |
50 |
Tag contours divisible by N as main=1 |
--show-every <N> |
100 |
Tag contours divisible by N as show=1 |
--layer <NAME> |
contour |
MVT layer name |
--elevation-attr <NAME> |
elevation |
MVT attribute name for elevation |
--nodata <VALUE> |
(from raster) | Override nodata value |
--simplify <EPS> |
2.0 |
DP tolerance in MVT pixels (0 = off) |
-j, --workers <N> |
(all CPUs) | Worker thread count |
The --levels flag takes comma-separated ZOOM:INTERVAL pairs. Each pair
says "draw a contour every INTERVAL metres starting at ZOOM and at all
higher zooms". With the default:
| Zoom | Active intervals | Lines drawn every |
|---|---|---|
| 11 | 50 | 50 m |
| 12 | 50, 20 | 20 m |
| 13 | 50, 20, 10 | 10 m |
| 14 | 50, 20, 10, 5 | 5 m |
# Default: z11–z14 from a GeoTIFF
corrie build dem.tif out.pmtiles
# Custom zoom range, custom intervals
corrie build dem.tif out.pmtiles --min-z 10 --max-z 15 --levels "10:100,12:50,14:10"
# MBTiles output, more aggressive line simplification
corrie build dem.tif out.mbtiles --simplify 4.0
# Multi-raster mosaic via VRT
gdalbuildvrt merged.vrt west.tif east.tif
corrie build merged.vrt out.pmtilescorrie uses all cores, reads only the parts of each line a tile needs (lines are indexed in 128-segment pieces), and with packs has no ingest step at all. Highlights, on the contour lines of Madagascar, Kosovo, Montenegro, Réunion and the Comoros, made with gdal_contour every 10 m from the JAXA ALOS AW3D30 DEM (30 m) and clipped to each country (17 shapefiles, 506 M vertices, 163,365 tiles at z11–14), on an Apple M-series laptop with 10 workers:
| Run | Time | Peak RSS |
|---|---|---|
tile from the shapefiles (packing them takes 2.7 s of it) |
10.9 s | 2.7 GB |
tile from packs |
7.9 s | 2.6 GB |
pack of the 17 shapefiles (8.4 GB → 2.3 GB) |
2.2 s | 0.6 GB |
Both tile runs write byte-identical archives. Whole continents are tiled the same way with --shards, one country at a time.
- RAM: packs are memory-mapped, so the peak RSS above is mostly packs
in the page cache, which the kernel can drop under pressure. Besides
them, a run holds the list of tiles to build and the output's tile
directory (tens of bytes per tile) and one tile's lines per worker. With
--shards, one group's packs are open at a time. - Disk: packs take about 4.6 bytes per vertex. A config of SHPs needs a
temp cache of that size (see The cache); with
--shards, only for the largest group at a time, plus the group archives.
Output is a clustered PMTiles file (Hilbert-ordered tile directory), suitable for direct serving without re-clustering.
PMTiles output includes vector_layers metadata, so MapLibre can introspect
the source. Serve the file with Martin or another PMTiles server, or read it
straight from static hosting with the
PMTiles protocol. A minimal
style, with the source named terrain:
{
"id": "contours",
"type": "line",
"source": "terrain",
"source-layer": "contour",
"paint": {
"line-color": "#8a7048",
"line-width": ["case", ["==", ["get", "main"], 1], 1.5, 0.75],
"line-opacity": 0.6
}
}Label the index (main) contours:
{
"id": "contour-labels",
"type": "symbol",
"source": "terrain",
"source-layer": "contour",
"filter": ["==", ["get", "main"], 1],
"layout": {
"symbol-placement": "line",
"text-field": ["concat", ["to-string", ["get", "elevation"]], " m"]
}
}MIT — see LICENSE