X-GIS

Guides

Migrating from Mapbox.

X-GIS reads the Mapbox Style Spec — paste a style.json into /convert and get an equivalent .xgis source. This page explains what carries across, what's heuristically lossy, and the conceptual differences between the two engines.

Mental model differences

Mapbox GL JS is an imperative library — you instantiate a Map, call addLayer(), wire styling through paint property setters, ship glue code in JS. xgis is a language: the source is the program, and the compiler emits the WGSL.

Mapbox

  • Imperative API — map.addLayer()
  • JSON style spec parsed at runtime
  • Expressions evaluated by a tree walker per frame
  • GLSL shaders hand-written for each paint property
  • Symbol layers (text, icons) first-class

xgis

  • Declarative — layer x { … }
  • Compile-time IR + WGSL codegen
  • Zoom-driven values lower to GPU uniforms
  • Per-feature expressions baked into vertex attributes
  • Symbol text layers compile to label-* + render via the SDF label engine

What this means in practice

Most cartographic styles port cleanly. Text labels render end-to-end via the SDF text pipeline — Mapbox text-field, text-color, text-size (zoom-interpolated), text-halo-*, text-anchor, text-transform, text-offset, text-rotate, text-letter-spacing, text-max-width + text-line-height + text-justify (multiline wrap), text-font (font-stack fallback), text-allow-overlap, text-ignore-placement and text-padding (greedy bbox collision) all map to label-* utilities. symbol-placement: line renders road / waterway / highway names along their geometry, rotated to the local tangent (Batch 1d v1 — single-segment placement per feature; per-glyph along-curve and repeat-along-line ship in v2). Icon symbols render via the sprite-atlas pipeline (icon-image / -size / -color / -opacity / -rotate).

Using the converter

Open /convert, click a preset chip (Liberty / Bright / Positron — three OpenFreeMap styles) or paste a URL pointing at any style.json. The page emits an .xgis source and a count badge showing how many features got dropped or rewritten.

Or skip the build step entirely — the splice-form import "url" directive fetches and runs the converter at runtime, so a one-liner inside your xgis source drops a full base map onto the canvas. Auto-detect only fires when the URL returns JSON shaped like a Mapbox v8 style (version + layers); plain xgis modules go through unchanged.

import "https://tiles.openfreemap.org/styles/bright"
source places { type: geojson, url: "places.geojson" }
layer my_pois {
source: places
| fill-rose-500 fill-opacity-80
}

The converter is also exported from the compiler package — call it from your build pipeline if you want to bake the converted xgis into your bundle (skips the runtime fetch).

import { convertMapboxStyle } from '@xgis/compiler'
const style = await fetch('https://tiles.openfreemap.org/styles/bright')
.then(r => r.json())
const xgis = convertMapboxStyle(style)
// xgis is a string — feed to <XGISMap>.run() directly.

Compatibility matrix

Supported
  • Sources: vector (PMTiles + TileJSON), raster, GeoJSON
  • Layers: background, fill, line, fill-extrusion, symbol (text + icons), circle, heatmap
  • Paint: fill-color/-opacity, line-color/-width/-dasharray/-opacity, fill-extrusion-color/-opacity/-height/-base
  • Symbol text: text-field, text-color, text-size (zoom-interpolated), text-halo-color/-width, text-anchor (5-way), text-transform, text-offset, text-rotate, text-letter-spacing, text-max-width + text-line-height + text-justify (multiline), text-font stack, text-allow-overlap, text-ignore-placement, text-padding (collision), symbol-placement: line + line-center (along-path)
  • Filters: legacy + expression form (==/!=/</>/all/any/in/!in/has/!has)
  • Expressions: get / coalesce / case / match / arithmetic (+ - * / % ^) / min / max / abs / ceil / floor / round / sqrt / sin / cos / tan / asin / acos / atan / ln / log10 / log2 / pi / e / ln2 / concat / length / downcase / upcase / step (N-stop) / let+var / to-string / to-number / to-boolean / to-color / rgb / rgba
  • Zoom: interpolate(zoom, …) with linear curve
Lossy / heuristic
  • interpolate curve type (exponential / cubic-bezier) folded to linear
  • fill-translate, line-translate dropped
  • fill-outline-color emitted as a separate stroke layer when paired
  • $type / $id filters dropped (geometry type implied by utility)
  • text-anchor diagonals (top-left / etc.) collapse to dominant axis
  • text-font resolved by browser glyph-fallback chain (no opentype-sdf bake step)
  • fill-pattern / line-pattern supported in constant-string form (data-driven pattern selection is a follow-up)
Unsupported (engine)
  • text-keep-upright per-glyph flip on curves
  • symbol-spacing repeat-along-line (single-label per feature in v1)
  • text-writing-mode: vertical (CJK vertical text)
  • Hillshade, sky layers
  • number-format, format (rich text)
  • distance, within (geometry predicates)
  • Top-level fog, terrain

Expression mapping

Common Mapbox expressions and their xgis equivalents. The converter applies these automatically — this is for when you're reading converter output and want to know what changed.

Zoom-driven width

Mapbox
"line-width": [
"interpolate", ["linear"],
["zoom"],
11, 1,
19, 2.5
]
xgis
| stroke-[interpolate(zoom, 11, 1, 19, 2.5)]

Property-driven color

Mapbox
"fill-color": [
"match",
["get", "class"],
"park", "#cfe7c1",
"water", "#a4c8d5",
"#dadada"
]
xgis
| fill match(.class) {
"park" -> #cfe7c1
"water" -> #a4c8d5
_ -> #dadada
}

Coalesce fallback

Mapbox
"fill-extrusion-height": [
"coalesce",
["get", "render_height"],
["get", "height"],
5
]
xgis
| fill-extrusion-height-[.render_height ?? .height ?? 5]

Filter — legacy form

Mapbox
"filter": [
"all",
["==", "$type", "LineString"],
["==", "class", "highway"]
]
xgis
filter: .class == "highway"
// $type dropped — geometry type
// is implied by the utility used

Caveats & gotchas

What the converter skips

Text symbol layers convert to label-* utilities and render through the SDF / CJK label engine — the common case, and it works. The converter only emits a comment when a symbol has neither a text-field nor an icon-image, or its text expression isn't convertible; those skips are listed in the conversion-notes block at the bottom of the output.

TileJSON URLs need the right type

Mapbox sources without a .pmtiles extension are emitted as type: tilejson — the runtime fetches the manifest, then the per-tile .pbf. If the host doesn't send Access-Control-Allow-Origin, all tile fetches fail silently (the catalog stays empty).

Curve type loses subtlety

Mapbox ["exponential", base] curves get flattened to linear. For a smooth-feel close to the original add more stops in between — `interpolate(zoom, 11, 1, 14, 1.5, 18, 4)` approximates an exponential rise better than two endpoints alone.

Round-trip is not exact

The converter's goal is "produces a sensible visual approximation", not "byte-equivalent round-trip". For pixel-precise reproduction of an existing Mapbox map, plan some manual touch-up after running the converter.

Specifications

See also