ezu-translate
Translate map-engine styles into ezu recipes — the node-DAG
Document JSON that ezu renders on the CPU (no GPU, no
headless browser).
Map engines describe their maps in their own style languages.
ezu-translate lowers those styles into ezu recipes so ezu can render them.
Each engine is a frontend exposed as a module; MapLibre GL is the
first, under ezu_translate::maplibre, and more engines can be added as
sibling modules over time.
MapLibre frontend (ezu_translate::maplibre)
MapLibre is an ordered list of layers whose paint/layout properties are computed per feature and per (fractional) zoom via expressions. ezu is a typed node DAG whose ops are styled uniformly. The two models differ deeply, so this frontend targets the tractable subset first and reports everything it can't yet reproduce.
let style: Value = from_str?;
let opts = default;
let = convert?;
// `recipe` is ezu Document JSON — feed to ezu_style::Document::from_json,
// or write to a .json and render with the `ezu` CLI. The recipe is
// zoom-independent: one recipe renders correctly at every zoom.
for w in &report.warnings
Or from the command line:
What it converts
| MapLibre | ezu |
|---|---|
| ordered layer list | blend chain (painter's algorithm) |
background |
solid |
fill (solid colour, fill-outline-color) |
features + fill-solid (outline → edge) |
line (+ line-dasharray, line-cap/join, line-gap-width) |
features + crisp stroke (dash in px); a gap renders MapLibre's casing annulus — one stroke's footprint with the corridor knocked out, so joins, caps, and dash phase behave like the GL shader |
raster |
raster |
circle (+ circle-stroke-*) |
a circle sprite stamped at each point (stroke = a larger ring stamped underneath) |
symbol icons (icon-image incl. data-driven, icon-size/-rotate/-opacity/-anchor/-offset/-padding, icon-allow-overlap/icon-overlap/-ignore-placement, icon-optional/text-optional, icon-text-fit + -padding with nine-slice sprite stretchX/stretchY/content metadata) |
the icon rides the layer's label: a point symbol's icon and text boxes join the shared collision index and place or drop as one unit (*-optional lets one half survive alone); icon-only symbol layers place too. Line-placed icons still lower to a collision-free stamp |
symbol text (symbol-placement: point/line/line-center: text-field, -size/-color/-halo-*/-opacity, anchor/offset/justify/wrapping/transform/spacing, text-variable-anchor (+ text-radial-offset, per-anchor offset mirroring), symbol-spacing/text-max-angle/text-keep-upright on lines; collision: text-allow-overlap/-ignore-placement/-padding, text-overlap, symbol-sort-key) |
text node — point placement labels each point; line placement walks each polyline with tangent-rotated glyphs (per-glyph collision), anchored on tile-clipped geometry with MapLibre's spacing phases. Zero-config: an unmapped text-font stack is served from the style's own glyphs endpoint as an SDF glyphs source (the same pre-rendered glyphs MapLibre draws); ConvertOptions::fonts / CLI --font "NAME=SOURCE" overrides with a real font per entry for higher-fidelity outline rendering — SOURCE is a font-file URL (http(s)://…, file:…, data:…) or an installed-font reference (system:Noto Sans, optionally ?weight=700&style=italic); a system: source is portable to write but resolves to whatever face the rendering machine has installed ({token} fields rewrite to expressions). Collision is deterministic across tiles (candidates gathered from the 8 neighbour tiles, deduped, ordered by symbol-sort-key with ties broken by tile feature order as in MapLibre, placed greedily) so borders stay seamless — the layer's source/layer/filter are threaded onto the node for neighbour gathering. All of a style's symbol layers collide in one shared index: each layer lowers to a text-labels (candidates) + text-draw (its placed labels) pair around a single label-placement node, placed top layer first — a POI can knock out a road name, as MapLibre does |
fill-pattern (constant) |
icon → tiling, clipped to the fill shape via blend { clip: true } |
line-pattern (constant) |
icon → line-stamp (repeat along the line, fit to line-width) |
fill-extrusion |
flat footprint fill-solid with fill-extrusion-color (no 3-D — height/base dropped) |
top-level sprite (single URL or [{id, url}] sheets; sheet:icon names) |
one sprite source per sheet (atlas <url>.png + index <url>.json, or inline index) |
hillshade (over raster-dem) |
dem + hillshade (tone calibration still approximate) |
heatmap (-radius/-weight/-intensity/-opacity incl. expressions; heatmap-color over heatmap-density) |
features → density (GL-JS kernel) → color-ramp with the colour expression baked to a 256-entry ramp per tile (ramp-expr); an opacity zoom curve becomes an expr scalar node feeding the ramp's opacity |
layer filter — expression-form (e.g. ["all", ["==", ["get", k], v], ["has", n]]) and legacy-form (bare field names, !in/!has/none) |
expression-form passes through verbatim as the features node's filter-expr; legacy-form is converted to the equivalent expression by maplibre_expr::convert_legacy_filter (MapLibre's own pre-compile conversion, strict-type semantics included) — both evaluated by ezu-paint via maplibre-expr (full fidelity) |
zoom / data functions (stops, interpolate, step, any expression) |
emitted raw onto the target node's *-expr field (e.g. fill-expr, color-expr, width-expr, opacity-expr, radius-expr), evaluated per tile by ezu-paint via maplibre-expr |
CSS named colours (steelblue, white, transparent, …) |
resolved to hex |
layout.visibility: "none" |
layer dropped (default), or — with ConvertOptions::keep_hidden — kept but gated off behind a switch (flip its select to b to enable) |
multiple vector sources |
all emitted; each features node targets its (source, layer) |
inline / remote geojson source (WGS84 lon/lat) |
geojson source; the host projects it into each tile and binds it as one feature layer (features targets (source, source)) |
layer minzoom / maxzoom |
the features node's min-zoom / max-zoom render-time gate (the layer draws only for min-zoom <= z <= max-zoom) |
Recipes are zoom-independent: zoom and data functions are emitted as
raw expressions and evaluated per tile (with the tile's zoom in the
maplibre-expr context), so a single recipe renders correctly at every
zoom — nothing is baked to a fixed zoom.
What it does not (yet) — reported in Report::warnings
text-rotation-alignment: viewporton line placement — line-placed glyphs always rotate with the line (map alignment). Likewiseicon-rotation-alignment/icon-pitch-alignmentare ignored.- Line-placed icons don't collide — a
symbol-placement: linelayer's icon lowers to a collision-freestamp, drawn independently of the line's text (point-placed icons collide and pair, see the table). - Label collision is handled (default on) but diverges from MapLibre by
design: it is world-space deterministic rather than viewport-driven,
so there is no "tiles nearest the viewport centre first" priority and
no per-frame fade in/out.
text-overlap: cooperativehas no equivalent and is treated asnever(collide) with a warning. A symbol carrying both an icon and text takes one overlap decision — the conjunction of the icon's and the text's overlap flags. - SDF (recolourable) icons — an
sdf: truesprite entry is drawn as its raw RGBA;icon-colortinting isn't applied. - Data-driven
fill-pattern/line-pattern(only a constant name converts; data-drivenicon-imageis supported). Data-driven values — fill/line/circle colour/opacity/width/radius, theicon-*and text paint properties — are supported, emitted as*-expr. An expression-valuedicon-text-fit-paddingalso warns (constant only). line-gap-widthonline-patternlayers (the gap applies to plainlinelayers only), andline-offset/line-blur.- True 3-D
fill-extrusion(the footprint is drawn flat). - Layers whose
text-fonthas no--font NAME=SOURCEmapping and whose style declares noglyphsendpoint skip their text.
See ezu-compare to measure how close a converted recipe
lands against a MapLibre reference render.
License
MIT or Apache-2.0, at your option.