Skip to content

Bounding boxes and pyramids

ezu tile renders one tile. Three other commands render many, and they share every flag with tile — --style, --assets-dir, --param, source overrides.

ezu bbox renders every tile covering a lon/lat box at one zoom and stitches them into a single image:

Terminal window
ezu bbox --style watercolor.json \
--bbox 139.74,35.65,139.78,35.69 \
--zoom 13 --out tokyo.png

The box is min-lng,min-lat,max-lng,max-lat. The result is tile-aligned: it covers at least the box, snapped outward to tile edges. This is the command for a figure in a slide deck.

ezu frame renders a map at a scale and paper size, cropped exactly to the page:

Terminal window
ezu frame --style watercolor.json \
--center 139.70,35.66 --scale 1000 --dpi 200 \
--size-mm 245x174 --out page.png

That is 1:1000 on a 245 × 174 mm page at 200 dpi: a 1929 × 1370 px image centred on the given point. --size-px 1929x1370 gives the size in pixels instead. --dpi is required either way, because the scale is a ratio of paper to ground and the dpi is what turns paper into pixels.

A tile renderer draws whole tiles at integer zooms, so ezu reaches the scale through the tile size instead. It picks the zoom whose tile size comes nearest the style’s own tile-size, renders at that size for this run, then copies each tile’s share straight into the page — the full tile mosaic is never held in memory. The log says what it settled on:

frame: 1929×1370 px at z=19, tile-size 489 (style: 512), scale 1:1000.0 (+0.002% vs 1:1000), …

The tile size is a whole number of pixels, so the scale can be off by up to half a pixel in a tile’s width: at most about 0.1 % for a tile size near 512, 0.2 % near 256. --zoom fixes the zoom and derives the tile size from it, for a style whose zoom-dependent choices (which layers draw, which labels appear) you want pinned. The scale is matched at the centre’s latitude, and Web Mercator drifts from it towards the top and bottom of a tall page.

--dpi only sets the scale. A style’s sizes are in pixels — a 2 px road edge, 14 px text — and stay that many pixels at any dpi, so at 200 dpi they print smaller than at 96. Make lines and labels heavier in the style, or with a param, when a page needs them bigger.

--features NODE=PATH writes what a features node produced across the page as a GeoJSON FeatureCollection, next to the image. The node can be any node in the style whose output is features, wired to the output or not. The usual one is label-boxes, which lists every label with what placement did to it: placed, moved off its first position (variant-index), left overlapping (overlapped) or dropped (placed: false with drop-reason, given include: all). A print pipeline can read the dropped ones from it and put their names in a list beside the map.

Terminal window
ezu frame --style labels.json \
--center 139.70,35.66 --scale 1000 --dpi 200 --size-mm 245x174 \
--out page.png \
--features boxes=labels.geojson --features-key boxes=label-id,part

The node is evaluated tile by tile, and a label near a tile edge is seen by every tile it reaches, so without a key the same label appears once per tile. A shape cut at tile edges, such as a polygon read from a vector tile, appears as its pieces. --features-key NODE=PROP[,PROP…] makes features whose listed properties are all equal one feature: the first in tile order (rows top to bottom, then left to right, and each tile’s features in their own order) is kept, and the file is sorted by those properties, numbers numerically, text alphabetically, a missing property first. A label’s boxes share its label-id on every tile, so label-id,part lists each box once.

A line label has one glyph box per glyph, and a label with an icon has more than one collision box, all with the same label-id and part; the key keeps only the first of them. For a list of labels, ask label-boxes for the parts that come one to a label, "parts": ["text", "icon", "background"].

Features whose bounding box lies wholly outside the page are left out. The rest are written whole, not clipped to the page. --features-coords picks the coordinates:

  • lonlat (the default): WGS84 longitude and latitude, as RFC 7946 asks, to 7 decimal places.
  • px: pixels of the output image, from its top-left corner with y down, to 2 decimal places, for drawing over the page.

The same style, data and flags give a byte-identical file on every run, so a diff of two runs shows whether anything on the page changed.

A print job is usually many pages from one style. Run once per page, ezu reads the style, builds its graph and fetches the background tiles again for every page, though neighbouring pages share most of them. --pages renders a whole job in one run from a JSON file:

Terminal window
ezu frame --style labels.json --pages job.json
{
"defaults": { "dpi": 200, "size-mm": [245, 174], "features-coords": "px" },
"pages": [
{ "center": [139.70, 35.66], "scale": 1000, "out": "p1.png",
"features": { "boxes": { "out": "p1-labels.geojson", "key": ["label-id", "part"] } } },
{ "center": [139.71, 35.66], "scale": 2500, "zoom": 17, "out": "p2.png" }
]
}

A page has a field for each flag of a single page: center ([lng, lat]), scale, dpi, size-mm or size-px ([width, height]), zoom, out, format, features-coords, and features, which maps a node to its file out and, if it has one, its key. Every field but out can be given once in defaults instead, and a page’s own value wins. A page’s paper size and its features replace the defaults’ whole, so a page with size-px does not also take a default size-mm. Relative paths are read from the pages file’s directory. Each page writes exactly what a single-page ezu frame with the same values writes.

The whole file is checked before the first page renders: an unknown field, a page without out, or two pages writing the same file is an error that names the page. --pages takes the place of the page flags, so it cannot be given with --center, --out and the rest; --concurrency and the style flags still apply, to every page.

Pages render one after another, each page’s tiles in parallel as before. The style is read and its sources opened once. Vector tiles one page fetched are kept in memory for the next, up to 256 MB (EZU_TILE_CACHE_MB sets another limit), so pages over the same area at the same zoom fetch them once; raster and DEM tiles are kept too. The intermediate cache of the graph belongs to one tile size, and a page at a new tile size starts a fresh one, so a job ordered by scale reuses the most. Each page logs a line with its time and the tiles it fetched and reused, and the run ends with the totals.

ezu tiles bulk-renders <out>/<z>/<x>/<y>.png across a zoom range:

Terminal window
ezu tiles --style watercolor.json \
--bbox 139.74,35.65,139.78,35.69 \
--min-zoom 10 --max-zoom 14 \
--out pyramid

--concurrency sets how many tiles render at once (default: your core count). Because the intermediate cache is shared across tiles within one style, and world-anchored nodes drop the tile id from their cache key, a pyramid run reuses far more work than the same tiles rendered one process at a time. See caching and parallelism.

The output tree is a plain XYZ directory — serve it with any static file server and point MapLibre, Leaflet, or OpenLayers at https://example.com/pyramid/{z}/{x}/{y}.png.

Style params let one style produce variants without editing JSON:

Terminal window
for paper in '#fbf6e6' '#ffe0f0' '#e8f2ff'; do
ezu bbox --style watercolor.json --bbox 139.74,35.65,139.78,35.69 \
--zoom 13 --param "paper=$paper" --out "tokyo-$paper.png"
done

Values are validated against the declarations — a number outside its min/max or a malformed colour fails before rendering starts.

  • WebP — --out tile.webp (or --format webp) writes lossless WebP, usually 20–40 % smaller than PNG on painterly content, through a pure-Rust encoder.
  • Overzoom — when a requested tile is missing from the source, ezu walks up to --overzoom-levels parent zooms (default 4) and reprojects the ancestor’s geometry into the requested frame. --overzoom-levels 0 disables it.
  • Attribution — Document::attributions() (and the tile server’s /style/attribution) return the merged attribution for everything a style pulled in, including TileJSON and PMTiles metadata. Render it wherever the tiles end up.

Map renders on this site are made fromOpenStreetMap data viaProtomaps (© OpenStreetMap contributors), elevation from Re:Earth Terrain,Mapterhorn andEGM2008 (NGA), and aerial imagery from GSI Japan(© 国土地理院). The painterly styles use CC0 brushes byDavid Revoy.