Serve tiles
There are three ways to get ezu tiles in front of a client, in increasing order of effort.
1. Pre-render a pyramid
Section titled “1. Pre-render a pyramid”The simplest thing that works. ezu tiles writes a plain XYZ tree; any static
file server or object store serves it.
ezu tiles --style style.json --bbox 139.6,35.5,139.9,35.8 \ --min-zoom 8 --max-zoom 15 --out pyramidCheapest to operate, immune to traffic spikes, and every tile is identical for every request. The cost is storage and the fact that a style change means re-rendering. Best when the area is bounded and the style is stable.
2. ezu serve behind a cache
Section titled “2. ezu serve behind a cache”ezu serve is a working tile server: /tiles/{z}/{x}/{y}.png (and .webp),
with param overrides as query values. Put
a CDN or reverse-proxy cache in front of it and you have a render-on-demand
basemap.
It is built for development, so treat it accordingly: no auth, no rate limiting, and one process. But it renders the same bytes a library host would.
Tile size and hi-DPI tiles
Section titled “Tile size and hi-DPI tiles”A hi-DPI screen wants the same map at a higher density: twice the pixels on
each side, with every line twice as wide and every label twice as large, so
it looks like the plain tile only sharper. That is the pixel ratio.
--pixel-ratio 2 on ezu tile, ezu bbox, ezu tiles and ezu serve
renders it, from 1 to 4 with fractions allowed. Everything the style gives in
pixels — line widths, text and icon sizes, halos, blurs, dashes, offsets,
spacings, noise scales and pad — is multiplied by the ratio, and labels are
placed exactly where the plain tile places them. Shrunk back by the ratio, a
retina tile comes out very close to the plain one.
ezu serve also takes the MapLibre and XYZ spelling for a hi-DPI tile in the
URL: /tiles/{z}/{x}/{y}@2x.png is the tile at pixel ratio 2, and @1x to
@4x are accepted, on .webp as well. A URL without one renders at the
server’s --pixel-ratio, 1 unless given. Point a MapLibre raster source at
the @2x URL with the style’s tileSize and a retina screen gets one tile
pixel per device pixel.
map.addSource('ezu', { type: 'raster', tiles: ['http://127.0.0.1:8080/tiles/{z}/{x}/{y}@2x.png'], tileSize: 512, // the style's tile-size: what each tile covers on screen});--tile-size is the other knob, and does something else: it renders every
tile at that size instead of the style’s tile-size, as the style with its
tile-size edited would — the same ground on a bigger canvas, with the
style’s pixel sizes unchanged, so lines and text draw finer against the map.
The two combine: the canvas is the tile size times the pixel ratio.
Sprite icons are drawn from the sprite sheet the style names; at a ratio
above 1 they are scaled up from it, so a host that wants them sharp binds the
sheet’s @2x version, whose pixel ratio the index records. Print frames take
no pixel ratio: ezu frame’s dpi already fixes how many pixels a page has.
--pad replaces the margin a tile renders with outside its edge. By default it
is the style’s pad raised to what its filters reach, and there is rarely a
reason to change it; a margin narrower than that reach computes the tile’s edge
from clamped data, which seams against the neighbour.
3. Embed the library
Section titled “3. Embed the library”For production, host the renderer yourself — ezu in a Rust service
(use from Rust), @reearth/ezu in a Worker
(use in the browser), or
github.com/reearth/ezu/go in a Go service
(use from Go). You own fetching, caching, and
concurrency, which is the point: ezu does no I/O, so nothing surprises your
network layer.
What makes tiles cacheable
Section titled “What makes tiles cacheable”ezu is deliberate about determinism, and it is worth knowing why:
- World-anchored painting.
fill-dabs,line, andnoiseare functions of world coordinates, so the same geometry paints the same way regardless of which tile it is being drawn into. - Shared label placement. Every label layer hands candidates to one placement stage, which decides them against one collision index, gathering candidates from the eight neighbouring tiles. A label near an edge resolves the same way from either side.
- No viewport input. Nothing in the pipeline depends on where a camera is, so
a tile is a pure function of
(style, params, tile id, source data)— which is exactly the cache key you want.
Consequence: the same style at the same zoom always yields the same bytes. Cache aggressively, and key your cache on the style version and any param overrides.
Overzoom
Section titled “Overzoom”Clients ask for tiles deeper than the data goes. ezu handles this rather than
returning nothing: it walks up to --overzoom-levels parent zooms (default 4),
takes the ancestor tile, and reprojects its geometry into the requested frame.
0 disables it and the request fails instead.
Raster and DEM pyramids have their own policy per source, on-missing:
empty(default) — transparent pixels / zero elevationupsample— walk up parent zooms and upsample the covered sub-regionerror— fail the render, so a tile server can answer 404
Requests past a source’s declared max-zoom always upsample from the max-zoom
ancestor, regardless of policy.
In a wasm host you do the walking: bind the ancestor’s bytes and tell the
renderer which zoom they came from with bindSource(name, bytes, { sourceZoom }).
Vector geometry is reprojected into the tile being rendered; a dem or raster
payload has its covering sub-rectangle resampled. Every tile-scoped source kind
takes it, and each neighbour resolves against its own ancestor.
Attribution
Section titled “Attribution”A style declares attribution at the document level and per source, and inherits
what upstream TileJSON or PMTiles metadata says. Serve the merged result —
Document::attributions() in Rust, renderer.attribution in JS,
GET /style/attribution from ezu serve — and render it wherever the tiles are
displayed. Basemap data usually requires it.
Sizing notes
Section titled “Sizing notes”On an Apple M1, a Protomaps basemap converted with ezu translate and rendered
at 512 px evaluates in ~13–30 ms per tile single-threaded across z12–z15 (graph
evaluation only). End to end, a 251-tile z13–z14 pyramid renders in ~9 s wall
across 8 cores — ~37 ms/tile amortized including HTTP fetch, MVT decode, and
PNG encode, which dominate at that render cost. Painterly styles cost more. See
benchmarks for methodology before planning
capacity on those numbers.
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.