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 layers not yet implemented (text rendering pending)

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). Icons depend on the upcoming sprite atlas.

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)
  • • 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 dropped (bitmap atlases unimplemented)
Unsupported (engine)
  • • Icon symbols + sprite atlas (icon-image / icon-size / icon-color)
  • 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)
  • • Circle, heatmap, hillshade, sky layers
  • number-format, format (rich text)
  • distance, within (geometry predicates)
  • • Top-level light, 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

Symbol layers are silently skipped

The converter emits a comment for every symbol layer it finds, and the conversion notes block at the bottom of the output lists them. You'll see a city with no labels until the runtime ships text rendering.

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

Was this page helpful?

Tell us what's missing