Reference
Mapbox Style Spec coverage.
Single source of truth for what the convertMapboxStyle pipeline handles. Each row is checked against the converter source at build time — entries marked supported must have a matching reference in the converter; new converter cases that miss a table entry fail CI. See the migration guide at Mapbox migration for narrative context and examples.
Summary
Reading this table
Supported — converter emits an xgis form AND the runtime honours it. Partial — converter emits SOMETHING but loses information (e.g. exponential interpolation folded to linear) or the runtime side has a gap. Unsupported — silently dropped or warned. N/A — Mapbox-only concept with no xgis equivalent and no plan to add.
Impact tier reflects user-visible severity in common basemap styles (OFM Bright, MapLibre demo), not effort to fix.
Top-level style properties
Fields on the root Mapbox style object.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
version | n/a | — | Spec versioning; ignored. | — |
name | supported | — | Emitted as a leading /* comment */ in the converted xgis. | mapbox-to-xgis.ts |
metadata | unsupported | low | Silent drop — informational only in Mapbox. | — |
center | supported | — | Applied by the demo-runner Mapbox importer after `runSource()` via `Camera.centerX/Y` + `markCameraPositioned()`. URL-hash camera still wins (hash parsing runs first). Compiler does NOT encode camera state into xgis source — top-level camera lives in the runtime, not the DSL. | — |
zoom | supported | — | Same path as `center` — runtime-side via demo-runner. | — |
bearing | supported | — | Same path as `center` — runtime-side via demo-runner. | — |
pitch | supported | — | Same path as `center` — runtime-side via demo-runner. | — |
sources | supported | — | — | sources.ts |
layers | supported | — | — | layers.ts |
sprite | supported | — | Importer extracts the URL from raw JSON and forwards to XGISMap.setSpriteUrl(). Runtime IconStage fetches `${url}.json` + `${url}.png` (DPR>=1.5 tries `@2x` first) and renders bitmap icons; SDF icons + icon-text-fit are Phase 2. Unknown icon names dropped silently at prepare-time; iter 526 added IconStage.getMissingIconNames() diagnostic for post-load misses. | — |
glyphs | supported | — | Importer extracts the URL from raw JSON and forwards to XGISMap.setGlyphsUrl(). Runtime TextStage fetches MapLibre SDF PBFs and upgrades visually when available; Canvas2D fallback stays on for offline / missing-glyph cases. Not encoded in xgis source. | — |
transition | unsupported | low | Per-property fade-in dropped. | — |
light | unsupported | low | No fill-extrusion ambient lighting model. | — |
fog | unsupported | low | Mapbox v3 distance-fog gradient. Would need a post-process pass with depth-based mixing. | — |
terrain | unsupported | medium | Roadmap Batch 4 (raster-dem + hillshade). | — |
projection | partial | low | mercator only; URL `?proj=` provides limited overrides at runtime. | — |
imports | unsupported | — | Mapbox v3 style-import not parsed. | — |
Source types
`sources[id].type` values.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
vector (.pmtiles) | supported | — | Routed to PMTilesBackend. | sources.ts:38 |
vector (TileJSON) | supported | — | Runtime fetches manifest then attaches PMTiles backend. | sources.ts:41 |
pmtiles | supported | — | Community-extension type ("type":"pmtiles") accepted as a sibling of the .pmtiles-URL detection path. | sources.ts:94 |
tilejson (explicit) | supported | — | Third-party convention: `"type":"tilejson"` directly. Routed alongside the `vector` + URL-sniffing path. | sources.ts:105 |
raster | supported | — | — | sources.ts:48 |
geojson (URL) | supported | — | — | sources.ts:73 |
geojson (inline) | supported | — | Captured via inlineGeoJSON collector → auto-pushed after run(). | sources.ts:77 |
raster-dem | partial | medium | Source registered, no hillshade renderer yet (Batch 4). | sources.ts:57 |
image | unsupported | low | Single-image source (e.g. user-supplied PNG draped onto a quad). Not in current loader; raster is the closest substitute. | — |
video | unsupported | low | Streaming video source. Not in current loader. | — |
Layer types
`layer.type` values.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
background | supported | — | Lifts to top-level `background { fill: # }` directive. | mapbox-to-xgis.ts:82 |
fill | supported | — | — | — |
line | supported | — | — | — |
symbol (text) | supported | — | TextStage renders SDF glyphs from Canvas2D fonts. | layers.ts:154 |
symbol (icon-only) | unsupported | high | No text-field → skipped. Awaits Batch 2 (sprite atlas). | layers.ts:159 |
fill-extrusion | supported | — | Extruded polygon with per-vertex z. | — |
raster | supported | — | — | — |
circle | supported | — | Routes to the runtime PointRenderer (SDF disks). circle-radius/-color/-stroke-color/-stroke-width/-opacity all map onto the existing point utility surface, including interpolate-by-zoom + data-driven forms. | layers.ts:514 |
heatmap | unsupported | medium | Batch 3 (accumulation MRT + Gaussian blur). | layers.ts:18 |
hillshade | unsupported | medium | Batch 4 (raster-dem + lighting shader). | layers.ts:19 |
sky | unsupported | low | Atmospheric sky dome (sky-color / sky-atmosphere-* / sky-type). Layer-level skip added to SKIP_REASONS so the converter emits an explicit // SKIPPED comment with diagnostic note rather than falling through to the generic handler. | layers.ts:SKIP_REASONS |
Layer common fields
Shared across all `layer` shapes regardless of type.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
id | supported | — | Sanitised into a valid xgis identifier. | layers.ts:520 |
type | supported | — | Discriminator — see Layer types table above. | — |
source | supported | — | — | layers.ts:521 |
source-layer | supported | — | Lowered to `sourceLayer: "..."` block prop. | layers.ts:522 |
minzoom | supported | — | PR #81: enforced at every label submission via `inZoomRange`. | layers.ts:523 |
maxzoom | supported | — | — | layers.ts:524 |
filter | supported | — | Legacy + expression form; routes through filter-eval. | layers.ts:525 |
metadata | unsupported | low | Informational — silently dropped. | — |
ref | n/a | — | Deprecated layer-ref shorthand (Mapbox style spec v7). | — |
Layout — fill / line
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
visibility | supported | — | `none` → `visible: false`. | layers.ts:538 |
line-cap | supported | — | butt / round / square literals only. | layers.ts:548 |
line-join | supported | — | miter / round / bevel literals only. | layers.ts:552 |
line-miter-limit | supported | — | Constant only. | layers.ts:556 |
line-round-limit | unsupported | low | Limit beyond which round joins switch to bevel. X-GIS line-join logic uses a fixed threshold; per-layer override not threaded. | — |
fill-sort-key | unsupported | low | Per-feature fill draw-order. X-GIS uses layer-order; per-feature would need an additional sort pass. | — |
line-sort-key | unsupported | low | Per-feature line draw-order. Same gap as fill-sort-key. | — |
circle-sort-key | unsupported | low | Per-feature draw-order key for circle layers; current renderer ignores it. | — |
Layout — symbol
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
symbol-placement | supported | — | point / line / line-center literals; `["step", ["zoom"], …]` form expands to multiple layers with intersected minzoom/maxzoom + segment-resolved placement (OFM Bright highway-shield-* coverage). Non-zoom step inputs fall back to default placement. | layers.ts:447 |
symbol-spacing | supported | — | Defaults to 250 px when missing on line placement. | layers.ts:471 |
symbol-avoid-edges | unsupported | low | Skip labels whose bbox crosses tile boundaries. Useful for de-duping labels at tile seams; X-GIS today uses cross-tile collision instead. | — |
symbol-sort-key | partial | medium | Constant numeric value plumbed end-to-end (iter 399-405). Runtime collision pass sorts CollisionItems by sortKey ascending — lower wins. Expression form (`["get", "rank"]`) flattens to 0 with a warning. | layers.ts:702 |
symbol-z-order | unsupported | low | Per-feature draw-order override. X-GIS uses symbol-sort-key for ordering today; symbol-z-order would need a separate sort pass after collision. | — |
text-field | supported | — | String / {token} / expression / number / boolean / null. Colon-bearing locale keys route via `get("name:xx")`. | layers.ts:164 |
text-font | supported | — | Family extracted, weight + italic stripped into `label-font-weight-N` / `label-italic`. | layers.ts:417 |
text-size | supported | — | Constant + interpolate-by-zoom + per-feature expression (sizeExpr). | layers.ts:231 |
text-max-width | supported | — | Default 10 ems for non-line placement (Mapbox parity). | layers.ts:385 |
text-line-height | supported | — | — | — |
text-letter-spacing | supported | — | Constant + interpolate-by-zoom. | — |
text-justify | supported | — | auto / left / center / right literals. | — |
text-anchor | supported | — | Full 9-way (center / top / bottom / left / right + 4 diagonals). | layers.ts:295 |
text-variable-anchor | supported | — | Real layout property (and legacy array-in-text-anchor) lower to anchorCandidates; runtime collision picks first non-overlapping. | layers.ts:370 |
text-variable-anchor-offset | supported | — | Per-anchor em offsets; runtime applies MapLibre baseline shift. | layers.ts:435 |
text-radial-offset | supported | — | Constant em; runtime fromRadialOffset per candidate anchor (MapLibre-parity). | layers.ts:435 |
text-offset | supported | — | Constant 2-tuple only. | layers.ts:329 |
text-rotate | supported | — | Constant only. | — |
text-padding | supported | — | Constant + interpolate-by-zoom. | layers.ts:351 |
text-transform | supported | — | uppercase / lowercase / none literals. | — |
text-allow-overlap | supported | — | — | — |
text-ignore-placement | supported | — | — | — |
text-overlap | partial | low | MapLibre overlap-policy enum (never / always / cooperative). always → label-allow-overlap; never → default; cooperative approximated as always (priority-aware collision pending) + warning. Wins over legacy text-allow-overlap when both declared. | layers.ts:418 |
text-optional | unsupported | low | Icons not implemented — moot. | — |
text-rotation-alignment | supported | — | Literal map / viewport / auto. Honoured at runtime. | map.ts:2369 |
text-pitch-alignment | partial | medium | Converter emits, runtime ignores — labels never project onto ground plane. Iter 10 surfaced an explicit warning when `map` is authored (the gap-revealing case) so authors of pitched-view styles see the diagnostic. `viewport` and `auto` match X-GIS' billboard-rendering default and stay silent. | map.ts:2461 |
text-keep-upright | supported | — | Per-glyph flip for line labels. | text-stage.ts:509 |
text-writing-mode | unsupported | medium | CJK vertical text would need a per-glyph rotation pipeline. | — |
text-max-angle | unsupported | low | Maximum angle between consecutive glyphs on a line-placed label. X-GIS uses a fixed threshold; per-layer override would thread through label-placement. | — |
icon-image | supported | high | Constant + data-driven match/case via label-icon-image-[<expr>] bracket binding. Per-feature evaluation in TextStage.applyFeatureExprs dispatches IconStage.addIcon. Iter 490 + 491 shipped 2026-05-18. Iter 535 verified end-to-end across the OFM Bright highway-shield path (road_N / us-interstate_N / us-state_N): the iter 531 null-comparison fix unblocks the shield-layer filter, the diagnostic quartet (iter 526/532/533/534) confirmed dispatch → vertex buffer → GPU draw all complete. The atlas ships shields as WHITE-on-transparent backgrounds (zero SDF sprites) so colored shield appearance comes from the text-field number overlay — not sprite tinting. | layers.ts:1007 + map.ts:applyFeatureExprs |
icon-size | supported | — | Constant + zoom-interp (iter 523). Bracket-binding `label-icon-size-[interpolate(zoom, …)]` lowers to LabelShapes.iconSize PropertyShape; runtime resolveNumberShape at dispatchIcon time. Data-driven (case/match/get) still drops with a warning — no per-feature path. OFM bright road_oneway / road_oneway_opposite (15→0.5, 19→1) honoured. | layers.ts:1075 |
icon-rotate | supported | — | Constant degrees. | layers.ts:641 |
icon-anchor | supported | — | Literal 9-way enum. | layers.ts:627 |
icon-offset | supported | — | [x, y] in CSS px; split into label-icon-offset-x / -y utilities. | layers.ts:631 |
icon-allow-overlap | partial | medium | No icon collision queue yet — every icon places (matches `true` semantics). OFM label_city/town/village/city_capital authoring `true` (4 layers per fixture) renders correctly. `false` would suppress overlapping icons; not implemented (would need icon-side collision bboxes). Iter 495 status review. | — |
icon-overlap | partial | medium | MapLibre overlap-policy enum. `always` matches X-GIS default (every icon places). `never`/`cooperative` need icon collision bboxes (deferred). Iter 495 status review. | — |
icon-ignore-placement | unsupported | medium | Same icon-collision gap as icon-allow-overlap. "true" would let other labels overlap this icon's footprint. | — |
icon-optional | partial | low | Default `false` (icon required for label placement) is X-GIS' current contract — labels with iconImage place when both fit. OFM label_city/town/etc. all author the default. `true` (label may place icon-less) needs icon-side collision arbitration; not implemented. | — |
icon-rotation-alignment | supported | medium | All three values (map / viewport / auto) honored. "viewport"/"auto" map to X-GIS axis-aligned icons; "map" adds the per-segment tangent to icon-rotate at dispatch time under symbol-placement=line (OFM road_oneway one-way arrows). Compiler iter 506 emits label-icon-rotation-alignment-map; runtime adds tangent in dispatchIcon. | layers.ts:1056 + map.ts:dispatchIcon |
icon-padding | unsupported | low | Per-icon collision-bbox padding. X-GIS uses a fixed 2px default per spec; per-layer override needs to thread through label-collision. | — |
icon-text-fit | unsupported | medium | Shield/badge backgrounds depend on this. | — |
icon-text-fit-padding | unsupported | low | Padding when icon-text-fit fits glyph bbox; dependent on icon-text-fit. | — |
icon-keep-upright | unsupported | low | Flip line-placed icons so they always face up. Currently icons follow the symbol-placement=line tangent without flipping. | — |
icon-pitch-alignment | unsupported | low | viewport (default) / map / auto. X-GIS uses viewport-aligned icons unconditionally; map mode would project the icon quad onto the ground plane. | — |
Paint — background
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
background-color | partial | low | Constant + CSS form only — interpolate-by-zoom of background falls through (rare). | — |
background-opacity | partial | low | Constant numeric form folds into background-color hex alpha (iter 47, mirror of circle-stroke-opacity iter 4). Zoom-interp / data-driven still warn — would need a per-frame uniform on the background-fill emit path. | — |
background-pattern | unsupported | low | Needs sprite atlas + tiled fragment. Batch 2 dependency. | — |
Paint — fill
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
fill-color | supported | — | Constant + interpolate-by-zoom + per-feature case/match expressions. | paint.ts:91 |
fill-opacity | supported | — | — | paint.ts:133 |
fill-antialias | partial | low | Default `true` is X-GIS' permanent contract — fragment shader smoothsteps every fill edge. OFM bright `building` / `road_area_pier` / `road_pier` author `true` explicitly = no-op match. OFM liberty `landcover_wood`/`grass`/`ice` set `false` for a pixel-art look; that opt-out (4 liberty layers) is not yet implemented and renders smooth instead of stepped. Iter 14 added a specific gap warning when `false` is authored explicitly so the gap surfaces rather than silently dropping. | — |
fill-outline-color | supported | — | Lowers to `stroke-<color> stroke-1` on the same fill layer — the xgis polygon renderer paints fill + outline in the same pass. Constant + interpolate-by-zoom. | paint.ts:153 |
fill-pattern | supported | high | Stage 2 (true UV-tiled bitmap) landed iter-181/182/183 2026-05-20. Sprite atlas bound at @group(0) @binding(5) on every polygon pipeline + dedicated `sprite_samp` at binding(6). `fs_fill_pattern` fragment shader samples the atlas at world-anchored UV computed from `abs_merc / pattern_repeat_m`; pattern repeat in Mercator metres derived per-frame from sprite design CSS-px width × WORLD_MERC / (256 * 2^cameraZoom) so the bitmap stays anchored to the ground. Pattern parameters pack into reused uniform slots (fill_color = UV bbox, fill_translate = repeat metres) so the 192-byte Uniforms struct is unchanged. VTR routes fillPattern shows to `fillPipelinePatternGround` (+ Fallback) variant; ground polygons on the baseBindGroupLayout path only — variant + featureBindGroupLayout pattern shows fall through to the Stage 1 sprite-centre-pixel colour. Constant string form supported end-to-end. Documented trade-offs: pattern shows cannot also use solid fill-color or fill-translate; extrude-pattern walls still flat (Stage 2 ground-only). | paint.ts iter-177/181/182/183 |
fill-translate | partial | low | Constant vec2 + zoom-interp last-stop approx end-to-end. Runtime WGSL u.fill_translate_x/y adds CSS-px offset converted to NDC at vs_main (`clip.xy += u.fill_translate * clip.w`). OFM building-top pseudo-3D roof offset honoured. Full per-frame zoom-interp deferred. Iter 501 + 508 shipped 2026-05-18. | paint.ts:addFillTranslate |
fill-translate-anchor | unsupported | low | viewport / map coordinate space for fill-translate; depends on fill-translate path. | — |
Paint — line
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
line-color | supported | — | — | paint.ts:102 |
line-width | supported | — | Constant + interpolate-by-zoom (linear AND exponential base) + per-feature width. PR #104 added per-frame zoom-stops; PR #108 conformance test pins differential parity with MapLibre createExpression() at z=4..20 (incl. fractional zooms). | paint.ts:113 |
line-opacity | supported | — | — | paint.ts:133 |
line-dasharray | partial | medium | Constant numeric array only — interpolate-by-zoom dasharray not lowered. Iter 27 sharpened the non-constant warning to name the specific shape (zoom-interp needs PropertyShape<array>; data-driven needs per-feature dash plumbing). | paint.ts:126 |
line-blur | supported | — | Edge feathering in CSS px. The line shader uses `aa_width_px` to widen both the geometry quad and the smoothstep range so the edge soft-fades over `1.5 + blur` px each side. Constant only — interpolate-by-zoom warns and drops. | paint.ts:190 |
line-gap-width | supported | medium | Constant + zoom-interp last-stop approx end-to-end via stroke-gap-N utility. Runtime double-draws each line at ±(gap+stroke)/2 via writeLayerSlot (iter 499). OFM road-casing layers honoured. Iter 498 + 499 + 513 shipped 2026-05-18. | paint.ts:addLineGapWidth |
line-offset | supported | — | Positive Mapbox values (right of travel) → `stroke-offset-right-N`; negative → `stroke-offset-left-N`. The xgis line renderer threads `strokeOffset` through to the vertex shader including offset-aware miter / join geometry. Constant only — interpolate-by-zoom warns and drops. | paint.ts:175 |
line-translate | unsupported | low | CSS-px viewport offset for lines. fill-translate is supported via u.fill_translate_x/y; line-translate would need a matching line-renderer uniform. Iter 32 added a specific gap warning naming the missing u.line_translate_x/y plumbing. | — |
line-translate-anchor | unsupported | low | viewport / map coordinate space for line-translate; dependent on line-translate. | — |
line-pattern | supported | low | Stage 2 landed iter-185 2026-05-20. line-renderer declares sprite_atlas at binding 5 + sprite_samp at binding 6 (shared TileBindGroupLayout with VTR so iter-181/182 atlas binding is already attached). New `fs_line_pattern` fragment + `pipelinePattern` alpha-blend pipeline. Pattern shows route via getDrawPipeline(translucent, patternActive=true). World-anchored UV (abs_merc / repeat_m) — Stage 2.1 along-line UV (arc length + transverse v) is a follow-up refinement. UV bbox packed into stroke_color uniform slot (20-23); repeat metres packed into layer.color.r / .a via writeLayerSlot override. Constant string form supported end-to-end. iter-165 probe: ZERO line-pattern uses in OFM bright/liberty target fixtures, so visual A/B unavailable against current set — Stage 2 is insurance for other styles (USA OSM / custom sprites). | line-renderer.ts iter-178/185 |
line-gradient | unsupported | low | Gradient along the line via ["line-progress"]. iter-166 probe: ZERO uses in OFM bright/liberty (also 0 lineMetrics declarations) — empirically confirms the low impact rating. Implementation cost (iter-158 scoping, the renderer change is NOT the hard part): (1) PREREQUISITE — geojson-vt currently IGNORES source.lineMetrics (geojsonvt/index.ts:14, sources.ts:406). line-progress is normalised over the ORIGINAL feature but geojson-vt clips lines per tile, so the clip stage must track each clipped segment's [progressStart,progressEnd] fraction of the original arc-length. This compiler-tiler change is the bulk of the work. (2) line-segment-build.ts interpolates per-vertex progress 0..1. (3) new per-vertex progress attribute + WGSL line fragment samples a gradient LUT the converter emits from the line-gradient interpolate stops. ~5 files; multi-day; not a surgical fix. PMTiles vector sources can't support it anyway (don't preserve original-line arc-length across tile boundaries) — feature is GeoJSON-source-with-lineMetrics-true only, niche. | paint.ts:218 specific warning |
Paint — symbol
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
text-color | supported | — | Constant + interpolate-by-zoom + per-feature colorExpr. | layers.ts:199 |
text-opacity | supported | — | Constant folded into label-color alpha (applyAlphaMultiplier). Zoom-interp + data-driven emit `label-opacity-[…]` → LabelShapes.opacity PropertyShape; runtime resolveNumberShape multiplies into resolvedColor.a + resolvedHalo.color.a per frame. Iter 113. | layers.ts:480 |
text-halo-color | supported | — | Constant + interpolate-by-zoom. | layers.ts:269 |
text-halo-width | supported | — | Constant + interpolate-by-zoom; PR #76 fixed scaling into SDF units. | layers.ts:259 |
text-halo-blur | supported | — | Constant only at conversion; IR exposes a PropertyShape so future zoom-interp / data-driven emit lands without IR changes. | layers.ts:283 |
text-translate | supported | — | Pixel-space offset added on top of em-unit text-offset. | layers.ts:340 |
text-translate-anchor | unsupported | low | viewport (default) vs map coordinate space for text-translate. X-GIS applies text-translate in viewport space only; the `map` mode would need MVP-aware offset. | — |
icon-color | supported | — | SDF sprite tint. iter 138 (Plan §4): IconRenderer carries a per-vertex tint + fwidth SDF fragment path; one batch mixes raster + SDF quads (per-vertex sdf flag, no pipeline split). Constant + zoom-interp + data-driven all route through LabelShapes.iconColor PropertyShape<RGBA> (same contract as text-color); runtime resolveColorShape at dispatchIcon → IconStage tint. Raster sprites ignore the tint per Mapbox spec. | layers.ts icon-color emit / icon-renderer.ts fs sdf branch |
icon-opacity | supported | — | Constant + zoom-interp + data-driven all route through LabelShapes.iconOpacity PropertyShape. Runtime resolveNumberShape at dispatchIcon → IconStage.addIcon per-vertex alpha. Iter 113. | layers.ts:1260 |
icon-halo-color | unsupported | low | SDF icon halo colour. iter-162 probe (playground/scripts/sprite-sdf-buffer-probe.ts) fetched the live OFM bright sprite: 264 entries, ZERO SDF. icon-halo applies ONLY to SDF sprites (Mapbox spec), so for the dominant OFM target styles this property is a NO-OP — implementing the composite produces zero visual change there. impact reclassified medium → low. iter-138 SDF icon foundation (fragment branch + per-vertex tint) STAYS and correctly serves any future style with SDF icons (USA OSM highway-shield-heavy styles, custom sprites). Composite shader work (second smoothstep at edge-haloWidth, mirror fs_text) is straightforward; the spritezero buffer constant remains UNRESOLVED (pin via the probe when a style with SDF icons becomes the target). | — |
icon-halo-width | unsupported | low | SDF icon halo width. Same iter-162 disposition as icon-halo-color: OFM bright has 0 SDF icons → no-op on the target style. impact reclassified medium → low. | — |
icon-halo-blur | unsupported | low | SDF icon halo feather. Same iter-162 disposition: OFM bright has 0 SDF icons → no-op on the target style. | — |
icon-translate | unsupported | low | CSS-px viewport offset for icons. Symmetric with line-translate / fill-translate; not threaded through IconStage. Iter 35 added a specific gap warning noting X-GIS shares text-translate offset for both icon and text today. | — |
icon-translate-anchor | unsupported | low | viewport / map coordinate space for icon-translate; dependent on icon pipeline (Batch 2). | — |
Paint — circle
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
circle-radius | supported | — | Constant + interpolate-by-zoom + per-feature expression. CSS px (Mapbox radius = xgis size). | layers.ts:537 |
circle-color | supported | — | Constant + interpolate-by-zoom + per-feature case/match. | — |
circle-opacity | supported | — | Mapbox 0..1 → xgis 0..100 scaled. Constant + interpolate-by-zoom. | — |
circle-stroke-color | supported | — | — | — |
circle-stroke-width | supported | — | CSS px; constant + interpolate-by-zoom. | — |
circle-blur | unsupported | low | Soft edge for circles. Point-renderer fragment uses smoothstep AA already; a per-feature blur attr + wider quad would extend the soft band. | — |
circle-stroke-opacity | partial | low | Constant numeric form folds into stroke-color hex alpha (iter 4, Plan §4 partial landing — same pattern later applied to background-opacity in iter 47). Zoom-interp / data-driven forms still warn + drop — need a dedicated paint shape for per-frame uniform multiplication. | layers.ts:circle-stroke-color block |
circle-translate | unsupported | low | CSS-px viewport offset for circles. Symmetric with fill/line/icon translate. Iter 34 added a specific gap warning naming the missing point-renderer translate uniform. | — |
circle-translate-anchor | unsupported | low | viewport / map for circle-translate; dependent on circle-translate. | — |
circle-pitch-scale | unsupported | low | viewport (default — radius constant on screen) vs map (radius scales with zoom). X-GIS uses viewport-scale unconditionally. | — |
circle-pitch-alignment | unsupported | low | viewport (default) vs map. X-GIS uses viewport-aligned circles; map mode would project the disc onto the ground plane. | — |
Paint — fill-extrusion
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
fill-extrusion-color | supported | — | — | — |
fill-extrusion-opacity | supported | — | — | — |
fill-extrusion-height | supported | — | Constant + interpolate-by-zoom + per-feature expression. | paint.ts:154 |
fill-extrusion-base | supported | — | — | paint.ts:165 |
fill-extrusion-translate | partial | low | iter-180 routed through addFillTranslate alongside fill-translate. The fill-extrusion vertex shaders (vs_main_quantized + vs_main_quantized_extruded) already apply u.fill_translate_x/y; the converter just stopped dropping the value. Constant vec2 + zoom-interp last-stop approximation supported. Full per-frame zoom-interp deferred (mirror of fill-translate). | paint.ts:259 iter-180 |
fill-extrusion-translate-anchor | unsupported | low | viewport / map space for fill-extrusion-translate; dependent on translate. | — |
fill-extrusion-pattern | supported | low | Stage 2 landed iter-186 2026-05-20. New `fillPipelinePatternExtruded` + Fallback variants (vs_main_quantized_extruded vertex + extrudedZBufferLayout for per-feature z + fs_fill_pattern fragment). VTR routes extruded pattern shows via setPatternExtrudedPipelines + an extrudedPatternActive gate symmetric with the iter-183 ground path. Same world-anchored UV math as fill-pattern + line-pattern (abs_merc / repeat_m). Documented Stage 2 trade-off: pattern-extrude shows lose the per-fragment wall_shade lighting — sprite colour replaces the shaded fill rgb directly. Stage 2.1 (dedicated fs_fill_pattern_extruded that multiplies the sample by wall_shade) is a follow-up refinement. Constant string form supported end-to-end. iter-165 probe: ZERO uses in OFM bright/liberty target fixtures — Stage 2 is insurance for other styles. | paint.ts:270 iter-179/186 |
fill-extrusion-vertical-gradient | partial | low | Default `true` is honoured end-to-end — fragment shader applies a vertical gradient ramp (0.6 base → 1.0 roof) plus a roof bonus matching MapLibre. Setting `false` to disable the gradient is the remaining gap (would need a per-show flag + WGSL branch). Iter 12 added spec-default suppression so authoring true (the spec default) no longer surfaces a spurious "ignored property" warning; only `false` (the real gap) warns. Promoted unsupported → partial in the capability-table expansion (iter 59) since the default path is real Phase 9 lighting, not stub. | — |
fill-extrusion-ambient-occlusion-intensity | unsupported | low | AO would need per-vertex normal + screen-space AO pass. Not in current renderer. | — |
fill-extrusion-ambient-occlusion-radius | unsupported | low | See fill-extrusion-ambient-occlusion-intensity. | — |
Paint — raster
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
raster-opacity | supported | — | Constant + interpolate-by-zoom + data-driven (all PropertyShape kinds) routed through the global RasterRenderer opacity uniform. Single raster show per scene is supported; multi-raster styles fall back to the first declared show. | paint.ts:38 |
raster-hue-rotate | unsupported | low | Rotate raster hue in HSL. Would need a fragment HSL-rotate pass. | — |
raster-brightness-min | unsupported | low | Lower bound of raster brightness remap. Fragment-shader linear contrast adjust. | — |
raster-brightness-max | unsupported | low | Upper bound of raster brightness remap. | — |
raster-saturation | unsupported | low | HSL saturation multiplier on raster sample. | — |
raster-contrast | unsupported | low | Fragment-shader contrast scale. | — |
raster-fade-duration | unsupported | low | Crossfade between zoom levels. X-GIS swaps tiles atomically; no fade. | — |
raster-resampling | unsupported | low | linear (default) vs nearest. Sampler is fixed to linear; per-show override would need a separate sampler binding. Iter 17 added spec-default suppression + iter 18 generic SPEC_DEFAULT_NO_WARN helper so authoring `linear` (matches X-GIS) is silent; `nearest` warns explicitly. | — |
resampling | unsupported | low | MapLibre v3 alias for raster-resampling — same semantic. | — |
Paint — heatmap
Heatmap layer renderer is not implemented; every property here is unsupported pending a roadmap entry.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
heatmap-radius | unsupported | medium | Heatmap layer renderer not implemented — radius (px) defines per-feature Gaussian footprint. | — |
heatmap-weight | unsupported | medium | Per-feature contribution multiplier; no renderer. | — |
heatmap-intensity | unsupported | medium | Overall density scale (per-zoom interpolated); no renderer. | — |
heatmap-color | unsupported | medium | Density → colour ramp (interpolate over `heatmap-density`); no renderer. | — |
heatmap-opacity | unsupported | medium | Layer-level opacity; no renderer. | — |
Paint — hillshade
Hillshade layer renderer is not implemented; raster-dem source is recognised but produces no output.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
hillshade-illumination-direction | unsupported | medium | Hillshade renderer not implemented (raster-dem source registered but unused). Direction in degrees from N clockwise. | — |
hillshade-illumination-altitude | unsupported | medium | Light elevation angle (0–90°); no renderer. | — |
hillshade-illumination-anchor | unsupported | low | map / viewport — whether the sun follows bearing; no renderer. | — |
hillshade-exaggeration | unsupported | medium | Vertical-relief multiplier; no renderer. | — |
hillshade-shadow-color | unsupported | medium | Shadow side colour; no hillshade renderer. | — |
hillshade-highlight-color | unsupported | medium | Lit side colour; no hillshade renderer. | — |
hillshade-accent-color | unsupported | low | Per-feature accent tint; no hillshade renderer. | — |
hillshade-method | unsupported | low | basic / combined / igor / multidirectional — different DEM gradient algorithms. | — |
resampling | unsupported | low | bilinear / nearest sampling of the DEM raster; depends on hillshade renderer. | — |
Expression operators
Mapbox Style Spec v1 expression form (the bracketed `["op", …]` syntax).
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
literal | supported | — | Scalar + array forms. Null-valued wrappers (`["literal", null]`) treated as "property omitted" by the paint-helper gate (isOmitted in paint.ts). | expressions.ts:33 |
get | supported | — | Bare field for identifier-safe names; `get("name:xx")` for colon-bearing locale keys. | expressions.ts:25 |
has | supported | — | — | expressions.ts:43 |
!has | supported | — | — | expressions.ts:52 |
coalesce | supported | — | Lowers to xgis `??` chain. | expressions.ts:59 |
case | supported | — | — | expressions.ts:65 |
match | supported | — | Routes through `match() { … }` when input is FieldAccess; ternary fallback otherwise. | expressions.ts:83 |
step | supported | — | — | expressions.ts:185 |
let / var | supported | — | Pure substitution at convert time. | expressions.ts:199 |
all | supported | — | — | — |
any | supported | — | — | — |
! | supported | — | — | — |
== / != / < / <= / > / >= | supported | — | — | — |
in | supported | — | Both expression form and legacy form. Empty value list lowers to constant `false` per spec. | expressions.ts:560 |
!in | supported | — | — | — |
+ / - / * / / / % | supported | — | — | — |
min / max | supported | — | — | — |
^ / abs / ceil / floor / round / sqrt | supported | — | — | — |
sin / cos / tan / asin / acos / atan | supported | — | — | — |
ln / log10 / log2 | supported | — | — | — |
pi / e / ln2 | supported | — | Zero-arg constants. | — |
concat | supported | — | — | — |
length | supported | — | — | — |
upcase / downcase | supported | — | — | — |
at | supported | — | Array indexing. | — |
to-number / number | supported | — | Converter passes through to a coalesce chain; xgis evaluator coerces in arithmetic context. Iter 539 added spec-compliant `to_number(v, fallback…)` builtin in the evaluator for hand-authored xgis source / tooling chains that bypass the converter. | evaluator.ts:to_number |
to-string / to-boolean / to-color | supported | — | Converter passes through to coalesce chains; iter 539 added spec-compliant `to_string` / `to_boolean` builtins in the evaluator (null → "", number → str, etc.); iter 541 added `to_color` (hex regex validation, X-GIS hex-only — converter pre-resolves CSS names like "red" via tokens/colors.ts:resolveColor). | evaluator.ts:to_string + to_boolean + to_color |
rgb / rgba | partial | low | Constant channels only — hex-encoded at convert time. Per-channel v8 literal-wrap (`["literal", N]`) accepted. | expressions.ts:507 |
hsl / hsla | partial | low | Constant channels only — converted via CSS hsl()/hsla() and re-hexed at convert time. Per-channel v8 literal-wrap accepted. | colors.ts:69 |
interpolate (linear) | supported | — | — | — |
interpolate (exponential) | supported | — | Mapbox `["exponential", N]` lowers to `interpolate_exp(zoom, N, …)`; runtime applies the Mapbox curve formula. base=1 collapses to the linear fast path. | paint.ts:46 |
interpolate (cubic-bezier) | partial | low | Numeric-valued zoom AND data-driven interpolates densify at compile time into a piecewise-linear approximation (6 samples per segment, CSS bezier-eased via Newton-Raphson). Runtime sees a longer linear stop list and visually approximates the bezier curve. Non-numeric values (colour stops) still warn and fold to pure linear. Iter 60-62 landings. | paint.ts:cssBezierEase + expressions.ts:interpolate handler |
interpolate-hcl | supported | — | LCh (polar Lab, hue shortest-path) colour interpolation: hex stops densify at compile time (iter 61-62 linear, iter 137 exponential — 6 samples / segment); non-hex (data-driven) stops now route to the runtime evaluator case interpolate_hcl (iter 164) which parses each stop's y at eval time, interpolates in LCh, and returns a hex. Full coverage modulo exponential×non-hex (rare combination — still warns and downgrades). | paint.ts + expressions.ts + eval/evaluator.ts interpolate_hcl |
interpolate-lab | supported | — | Lab (D50) colour interpolation: hex stops densify at compile time (iter 61-62 linear, iter 137 exponential — 6 samples / segment); non-hex (data-driven) stops now route to the runtime evaluator case interpolate_lab (iter 164) which parses each stop's y at eval time, interpolates in Lab, and returns a hex. Full coverage modulo exponential×non-hex (rare combination — still warns and downgrades). | paint.ts + expressions.ts + eval/evaluator.ts interpolate_lab |
geometry-type | supported | — | Routes via synthetic `$geometryType` prop injected at filter-eval time. | expressions.ts:263 |
id | supported | — | Routes via synthetic `$featureId` prop injected from `feature.id` (GeoJSON RFC 7946 §3.2; MVT feature.id) at every filter-eval site. Same pattern as `geometry-type`. | expressions.ts:278 |
properties | unsupported | low | Returns whole feature properties object — X-GIS expressions access by field name (`.field` / `["get","field"]`); no object literal accessor. | — |
feature-state | n/a | — | Mapbox v8 dynamic property setter — no xgis equivalent. | — |
typeof | supported | — | Returns Mapbox-shaped strings ("string" / "number" / "boolean" / "object" / "null"). | expressions.ts:237 |
format | partial | low | Span texts concatenated via xgis concat(); per-span opts (font-scale / text-color / text-font / vertical-align) dropped — X-GIS labels render with one style per layer. Iter 25 added per-section partial-drop semantics: when one section fails to convert (e.g. uses an unsupported accessor), surviving sections still concat — only ALL-sections-fail returns null. Pre-fix any single failure bailed the whole format expression and dropped the label silently. | expressions.ts:208 |
image | unsupported | high | Sprite atlas (Batch 2). | — |
number-format | supported | — | Lowers to positional `number_format(input, minFrac, maxFrac, locale, currency)` (xgis has no object-literal syntax). Routes through Intl.NumberFormat at runtime; null slots use spec defaults. | expressions.ts:275 |
collator | unsupported | low | Locale-aware comparator for ==/!=/in. X-GIS uses byte-exact string compare. Surface as warning when authored. | — |
resolved-locale | unsupported | low | Returns locale string from a collator. Depends on collator support. | — |
is-supported-script | unsupported | low | Returns true if all chars in a string are renderable. X-GIS assumes Unicode-renderable. No-op gate. | — |
array | partial | low | Type-assertion drops to value pass-through (X-GIS arrays carry no per-element type tag, so the spec's "abort if not array" semantic is lost; in paint/filter use a non-array would null-cascade anyway). | expressions.ts:163 |
slice | supported | — | String or array; Mapbox `["slice", input, start[, end]]`. Routes to JS String/Array `.slice` semantics. | expressions.ts:248 |
index-of | supported | — | Lowers to xgis `index_of(needle, haystack[, from])`. Returns -1 when not found. | expressions.ts:257 |
zoom | supported | — | Lowers to bare `zoom` identifier. Works in `interpolate(zoom, …)` / `step(zoom, …)` AND anywhere else (filter compare, case condition, arithmetic). | expressions.ts:471 |
pitch | unsupported | low | Returns current camera pitch in expressions (e.g. for pitch-aware styling). X-GIS expression scope has zoom but no pitch identifier yet. | — |
distance-from-center | unsupported | low | Returns screen-space distance from viewport centre for the current feature. Would need per-feature distance evaluation in worker. | — |
distance | unsupported | low | Geometry-to-geometry geodesic distance. Surface as warning when authored; would need spatial index for performance. | — |
within | unsupported | low | Point-in-polygon test for filter context. Surface as warning when authored. | — |
accumulated | n/a | — | Heatmap-only. | — |
heatmap-density | n/a | — | Heatmap-only. | — |
line-progress | n/a | — | line-gradient only. | — |
sky-radial-progress | n/a | — | — | — |
Filters
Legacy + expression form. Most filter operators reuse the expression infrastructure.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
== / != / < / <= / > / >= (legacy form) | supported | — | Field-as-second-arg shape recognised. | expressions.ts:420 |
in / !in (legacy + expression form) | supported | — | — | — |
has / !has | supported | — | — | — |
all / any / ! | supported | — | — | — |
match (boolean form) | supported | — | Lowers to OR/AND chain when all values are boolean literals. | expressions.ts:335 |
$type | unsupported | low | Legacy filter — use the new `["geometry-type"]` accessor instead. | expressions.ts:414 |
$id | unsupported | low | Legacy filter — use the new `["id"]` expression-form accessor instead. Mirror of $type → geometry-type migration. | — |
Keeping this page accurate
The table lives in
compiler/src/convert/spec-coverage.ts.
A vitest regression
(spec-coverage-drift.test.ts)
scans the converter source files at build time and fails when:
- • A new
case 'X':,layout['X'], orpaint['X']reference appears in the converter without a corresponding table entry, or - • An entry marked
supportedhas no matching reference in the source (catches stale entries after the converter loses a feature).