Skip to content

Server-side raster tile rendering

Martin can render a style into raster images server-side: as XYZ tiles, or as a single static image at a chosen camera.

Warning

This feature is included in the default build on Linux. Its behaviour may change in patch releases.

Limitations of our current implementation:

  • Rendering support is currently only available on Linux. To add support for macOS/Windows, please see https://github.com/maplibre/maplibre-native-rs.
  • Currently, martin does not cache style rendered requests and
  • does not support concurrency for this feature.

We welcome contributions to help stabilise this feature!

To enable rendering, turn it on in the configuration file:

styles:
    rendering: true

Rendered XYZ tiles

We support generating a rasterised image for an XYZ tile of a given style.

After enabling rendering, you can use the /style/<style_id>/{z}/{x}/{y}.{filetype} API to get a <style_id>'s rendered png/jpeg content.

Static images

Info

We currently do not have the same capabilities as Tileserver-GL to layout images. We are working on adding this feature and are very open to contributions if you want to help!

Warning

Static rendering shares the limitations listed above (Linux only, no caching, no concurrency). The HTTP shape may still change in patch releases.

Martin can render a single PNG/JPEG/WebP of a style at a chosen camera. The same URL is served by two methods:

GET  /style/{style_id}/static/{camera}/{size}.{ext}
POST /style/{style_id}/static/{camera}/{size}.{ext}

GET returns the base map alone. POST additionally accepts a GeoJSON FeatureCollection in the body, with styling on each feature's properties, and overlays those features on top of the base style for that single render. An empty or missing body is equivalent to GET.

Camera

The {camera} segment chooses what the image looks at:

Form Meaning
lon,lat,zoom Center at (lon, lat) and zoom (north up, flat).
lon,lat,zoom@bearing Center + bearing in degrees (clockwise from north).
lon,lat,zoom@bearing,pitch Center + bearing + pitch in degrees.
minLon,minLat,maxLon,maxLat Fit the given bounding box to the requested size.

Info

The image is always rendered INSIDE of the requested size. So if the bbox is [-10°,-1°,10°,1°] and size 500x500 is requested, the image will be centered on 0,0 with a 500x500 box that is fully inside the bbox, so the left-top most pixel is approximately at 1°,-1°. We will not expand the image outside the bbox.

Size and format

{size}.{ext} follows the WIDTHxHEIGHT[@{scale}x].{ext} pattern, e.g. 800x600.png, 400x300@2x.jpg. Allowed extensions are png, jpg, and webp. Width and height are capped at 2048 px each; scale is capped at 4x.

Overlay body (POST)

The overlay body is a GeoJSON FeatureCollection. Each feature carries its style on properties using MapLibre Style Spec paint/layout property names (circle-*, line-*, fill-*). Any property left unset falls back to MapLibre's own default.

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": { "type": "LineString",
        "coordinates": [[-10, 5], [10, 5]] },
      "properties": { "line-color": "#f00", "line-width": 3, "line-cap": "round" }
    },
    {
      "type": "Feature",
      "geometry": { "type": "Point", "coordinates": [0, 0] },
      "properties": { "circle-color": "#00f", "circle-radius": 8 }
    },
    {
      "type": "Feature",
      "geometry": { "type": "Polygon",
        "coordinates": [[[-5,-5],[5,-5],[5,5],[-5,5],[-5,-5]]] },
      "properties": { "fill-color": "#0a0", "fill-opacity": 0.5 }
    }
  ]
}

How features become layers:

  • Point / MultiPoint -> one circle layer.
  • LineString / MultiLineString -> one line layer.
  • Polygon / MultiPolygon -> a fill layer (unless only line-* properties are set), plus a line layer when any line-* property is present (so a polygon with line-color gets an outline of line-width).
  • GeometryCollection and features with geometry: null are silently skipped.

Anything the server can't make sense of is rejected with 400 Bad Request: a malformed body, a top-level type that isn't FeatureCollection, a member type that isn't Feature, an invalid CSS color, a non-numeric width, or an unknown line-cap/line-join enum value. Unknown property keys on feature.properties are silently ignored, so GeoJSON files that already carry application metadata (id, name, title, description, …) work without modification.

Supported style properties

Every property is optional; an unset property uses the MapLibre Style Spec default.

MapLibre property Applies to MapLibre default
circle-color Point #000000
circle-opacity Point 1
circle-radius Point 5
circle-stroke-color Point #000000
circle-stroke-opacity Point 1
circle-stroke-width Point 0
line-color LineString / Polygon #000000
line-opacity LineString / Polygon 1
line-width LineString / Polygon 1
line-cap LineString / Polygon butt
line-join LineString / Polygon miter
fill-color Polygon #000000
fill-opacity Polygon 1
fill-outline-color Polygon matches fill-color

line-cap is one of butt, round, square; line-join is one of miter, bevel, round. Colors accept any CSS color string.

Range checks are not enforced - *-opacity values outside 0..=1 and negative widths are passed through to MapLibre verbatim.

Out of scope

  • Data-driven expressions on individual feature properties (each feature becomes its own GeoJSON source and layer with literal paint values).
  • Layer types beyond fill / line / circle (no symbol, heatmap, raster, fill-extrusion).
  • Externally-referenced GeoJSON URLs (every feature is inline).
  • Symbol/icon markers (no symbol layer) - use a circle instead.
  • Z-ordering relative to base style layers - all overlay layers are drawn on top of the base style, in feature-array order.

All examples below render the same camera (/static/0,0,2/200x200.png) against the maplibre_demo style, so the visual differences come only from the overlay body.

Line

{
  "type": "FeatureCollection",
  "features": [{
    "type": "Feature",
    "geometry": {
      "type": "LineString",
      "coordinates": [[-10.0, -10.0], [10.0, 10.0]]
    },
    "properties": {
      "line-color": "#95BEFA",
      "line-width": 5,
      "line-cap": "round",
      "line-join": "round"
    }
  }]
}

Pastel-blue diagonal stroke over the demo basemap

Fill

{
  "type": "FeatureCollection",
  "features": [{
    "type": "Feature",
    "geometry": {
      "type": "Polygon",
      "coordinates": [[
        [-10.0, -10.0], [10.0, -10.0],
        [10.0, 10.0], [-10.0, 10.0],
        [-10.0, -10.0]
      ]]
    },
    "properties": {
      "fill-color": "red",
      "fill-opacity": 1.0
    }
  }]
}

Filled red square polygon

Fill opacity (alpha blending)

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": { "type": "Polygon",
        "coordinates": [[[-40, -20], [10, -20], [10, 20], [-40, 20], [-40, -20]]] },
      "properties": {
        "fill-color": "#285DAA",
        "fill-opacity": 0.5
      }
    },
    {
      "type": "Feature",
      "geometry": { "type": "Polygon",
        "coordinates": [[[-10, -20], [40, -20], [40, 20], [-10, 20], [-10, -20]]] },
      "properties": {
        "fill-color": "#95BEFA",
        "fill-opacity": 0.5
      }
    }
  ]
}

Two semi-transparent brand-color rectangles overlapping

Circle (marker)

{
  "type": "FeatureCollection",
  "features": [{
    "type": "Feature",
    "geometry": {
      "type": "Point",
      "coordinates": [0.0, 0.0]
    },
    "properties": {
      "circle-color": "#285DAA",
      "circle-radius": 8
    }
  }]
}

Primary-colored circle marker at the equator/prime meridian