Skip to main content

The style CLI

To see a conversion rather than read it, the style preview runs this same converter in the browser and renders the result with the SDK.

npx @massif-maps/style-tools mapbox2css style.json out/ --validate

massif-style is one command carrying every style conversion the project ships. It is an npm package, so a style author needs no C++ toolchain, and the conversions that must agree with the SDK run the SDK's own code compiled to WebAssembly — there is no second implementation to drift.

Not released yet

The package is built and tested in CI but has not been published to npm. Until it is, take massif-style.mjs and massif-style.wasm from an SDK release, or build it from tools/style-cli/.

SubcommandWhat it does
css2xml <project.json> <out.xml>compiles a CartoCSS style project to the mapnik XML the decoder reads
mapbox2css <style.json> <out-dir>translates a MapBox / MapLibre style JSON into a CartoCSS project

Node 20 or newer. The wasm build uses NODERAWFS, so it runs under Node — not in a browser.

Converting a MapBox or MapLibre style​

massif-style mapbox2css style.json out/ --validate

Writes a style project — project.json plus the .mss files — which is exactly what a "project" style set loads:

{"type": "vector",
"source": "osm",
"style": {"type": "mbvt",
"project": {"type": "project",
"assets": {"type": "dir", "path": "/sdcard/my-style"},
"name": "style"}}}

Flags:

--validatecompiles the result with css2xml in the same process, so CartoCSS the compiler would reject fails here rather than on device
--strictexit non-zero if any MapBox property was dropped
--shield-anchors [sides]let a POI name take the first free side of its icon, and draw the icon alone when none is (shield-anchors + shield-text-optional). Sides in preference order, default right,left,top,bottom; a layer stating its own text-variable-anchor keeps it
--icon-font FACE
--icon-font-map FILE
draw every shield icon as a glyph of an icon font instead of a sprite, so the style ships one font rather than a sheet of PNGs. FILE maps icon-image names onto that face's characters ({"mountain": ""}, "U+E90A" and 59658 all read). A name it has no glyph for keeps its sprite when the sheet has one, else draws no icon; a marker — a oneway arrow, a crossing — keeps its sprite. A recolourable badge keeps its disc at the size measured off the sheet, so every glyph sits on the same circle whatever its outline
--icon-font-size PXthe height in sprite pixels a glyph fills at icon-size 1, so icon-size scales the glyph as it scaled the sprite; unset, the glyph is a multiple of text-size

massif-style mapbox2css --help lists the rest.

Every conversion prints a coverage report naming each property it could not carry and how often it appeared. That is the number to read: a translation is not "done" because it produced files.

A hillshade layer becomes a composite slot when its metadata["massif:sdk-layer"] states the HillshadeRasterTileLayer settings: opacity, exaggeration, contrast, heightScale, hillshadeMethod and the three colours become the matching hillshade-* properties of a #<id> rule. The paint itself is not read — MapLibre's hillshade-exaggeration is the layer's contrast (Matching MapLibre). Without the metadata the layer is dropped. metadata["massif:sdk-slot"] overrides those values for the slot only, so one can be an expression over a parameter while massif:sdk-layer stays literal for a standalone layer. The rule ends at visibleZoomRange's top, or at [zoom < 'param::NAME'] when metadata["massif:maxzoom-param"] names a parameter (512 px tiles only, like massif:minzoom-param).

What it cannot carry​

These are CartoCSS gaps, not converter bugs — the report names them one by one:

  • Layer types with no symbolizer — heatmap, and symbol layers that have only an icon.
  • Properties — line-blur, line-gap-width, line-gradient, fill-extrusion-pattern, every *-translate, most raster-* adjustments.
  • Expressions with no CartoCSS form — feature-state, within, let/var, number-format, image, %, abs/floor/ceil, and interpolate over anything other than zoom.
  • Draw order across source layers. One entry in the project's layers array pulls every attachment of that name, so two MapBox layers on different source layers cannot be interleaved. The converter reports how many end up out of order rather than reordering silently.
  • The sprite. icon-image is dropped, so a symbol layer with an icon and no text produces nothing.

Compiling a CartoCSS project​

massif-style css2xml project.json style.xml
massif-style css2xml --roundtrip project.json style.xml # parse the XML back and diff it

This is the SDK's own compiler — CartoCSSMapLoader and MapGenerator, the same code the map runs at runtime — so a project that compiles here renders there. Use it in CI to fail a style change before it reaches a device.

Writing the CartoCSS by hand​

Every property the decoder understands, with its live/baked flag, is in the CartoCSS property reference. Properties marked live can be changed through a style parameter with a repaint instead of a re-decode.

Also planned​

  • carto2css — mapnik XML back to CartoCSS. Not written; MapGenerator only goes one way.
  • A browser build, for a playground that compiles a style in the page. NODERAWFS rules the current artifact out.

How the package is built, why the split between wasm and TypeScript, and how it is released: style tools internals.