Skip to content

Table Source Layers

Not released or fully implemented

The bulk mode of martin cp is not in any release yet, and parts of what these pages describe are not implemented yet. It is only built with the unstable-generate feature:

cargo build --package martin --bin martin --features unstable-generate

Not implemented yet:

  • Rendering points, lines and polygons, with fill ranges for covered tiles
  • Tile grouping, MLT and MVT encoding, compression and deduplication
  • Planning tables into layers and running a whole generation end to end
  • The PostgreSQL source with partitioned whole-table scans
  • Bulk mode in martin cp, and PMTiles output
  • Layers with CEL expressions, rules and sort_by
  • OSM PBF, shapefile, GeoPackage and CSV files as sources
  • geometry other than point, line and polygon
  • tile operations (merge_lines, merge_polygons, merge_multi, label_grid, dedup_by, limit)
  • prefetch

layers is a key of a PostgreSQL table source. It turns one table into one or more output layers, all fed by a single read of the table, each choosing its features, zooms, attributes and draw order. The bulk mode of martin cp writes them into a tileset, and the tile server renders them on request, see Serving.

A table source without layers is one layer, named by its layer_id or by its source id, holding all of its properties and its feature ids. A table source cannot set both layer_id and layers.

postgres:
  tables:
    roads:
      schema: public
      table: roads
      srid: 4326
      geometry_column: geom
      id_column: osm_id
      properties:
        class: text
        name: text
        "name:de": text
      layers:
        roads:
          geometry: line
          attributes: [class]
        road_labels:
          minzoom: 10
          simplify: 2
          attributes: [name, "name:*"]
          id: false

Every feature is offered to each layer, which may take it or not. Inside each tile, the layers of one table come in the order they are listed. Tables come in the order of --source, or ordered by source id without it. Layer names must be unique across all sources, and a run holds at most 256 layers.

Layer keys

Key Meaning Details
minzoom, maxzoom A number, or an expression for each feature's own zoom Zooms
extent, buffer, clip_geom Override the table source's
simplify, simplify_at_maxzoom Pixels of tolerance when simplifying lines and polygons Simplification
min_size, min_size_at_maxzoom Pixels, below which lines and polygons are dropped Simplification
geometry point, line or polygon: only features of that type Features
where An expression: only features for which it is true Features
attributes Which properties to write, and under which names Attributes
id true (default), false, or an expression Feature ids
rules Different zooms, simplification and attributes for some features Rules
sort_by The draw order of the layer's features in each tile Draw order

extent, buffer and clip_geom default to the table source's, then to 4096, 64 and true. buffer must be less than half the extent. With clip_geom: false, every tile a line or polygon reaches holds all of it, not just its part there.

Properties

A layer can only use the table source's properties, both as attributes and in expressions. An auto-published table has all of its columns as properties, but a table source in the configuration file only has the properties it lists. The id_column is not a property. To use it in an expression, list it under properties as well.

When the table has a jsonb property, a name that is not a column is read as a key of it. Otherwise, an unknown name fails the run. A prefix such as name:* only matches columns, not jsonb keys.

Only the columns that some layer uses are read.

Zooms

Zooms narrow from the run down to each feature: every level can raise the lowest zoom and lower the highest, never widen what the level above allows.

Zoom ranges narrow from the command line to the table, the layer, each feature and each attribute012345678910111213141516--min-zoom 0 --max-zoom 14the run0-14table source: minzoom: 2narrows the run2-14layer: maxzoom: 12narrows the table2-12rule or expression: 8per feature, clamped to the layer8-12attribute: { minzoom: 10 }within the feature's zooms10-12zoom

  1. --min-zoom and --max-zoom set the run's zooms. A table source's minzoom and maxzoom narrow them. A table source left with no zoom fails the run.
  2. A layer's minzoom and maxzoom numbers narrow the table source's. A layer left with no zoom is skipped with a warning.
  3. A minzoom or maxzoom expression gives each feature its own zooms within the layer's. Its result is clamped to the layer's zooms, and null keeps the layer's bound. A feature whose minzoom ends up above its maxzoom is left out of the layer.
  4. An attribute with minzoom or maxzoom is only written at those zooms of the feature.

A rule can set minzoom and maxzoom too. For the features it takes, they replace the layer's expressions.

The highest zoom is 27.

Simplification and minimum size

Lines and polygons are simplified at each zoom, and dropped once they are smaller than min_size. Both are in pixels of a 256-pixel tile, whatever the extent, and default to the values Planetiler uses.

Key Below the layer's max zoom At the layer's max zoom
simplify simplify, default 0.1 simplify_at_maxzoom, default simplify or 1/16
min_size min_size, default 1 min_size_at_maxzoom, default min_size or 1/16

The layer's max zoom is its highest zoom after narrowing, not the highest zoom of a single feature. A rule's simplify and min_size apply at every zoom, including the max zoom.

Features

A feature belongs to a layer when all of these hold:

  1. the table source's filter, which the database applies while reading,
  2. the layer's geometry type, if set,
  3. the layer's where expression, if set. null and failed evaluations count as false.

A rule's where does not filter features: it only picks the rule.

Attributes

Without attributes, a layer writes all of the table source's properties. [] writes none.

A list copies those properties as they are. A map names each output attribute:

  • a string is an expression, so kind: class writes the column class as kind,
  • { value: ... }, a number or a boolean is a literal,
  • { expr: ..., minzoom: ..., maxzoom: ... } is only written at those zooms.

In both, name:* copies the columns starting with name:, written "name:*": "name:*" in a map. An expression that is null leaves the attribute out.

Feature ids

id: true writes the id_column as the feature id, id: false writes none, and an expression computes it. A computed id is only written when it is an integer of zero or more.

Rules

rules is a list. The first rule whose where holds for a feature applies to it, and a last rule without where takes every other feature. A feature that matches no rule keeps the layer's settings.

postgres:
  tables:
    roads:
      schema: public
      table: roads
      geometry_column: geom
      properties:
        class: text
      layers:
        roads:
          attributes:
            kind: class
          rules:
            - where: "class == 'motorway'"
              minzoom: 4
              attributes:
                kind: { value: major }
            - where: "class == 'path'"
              minzoom: 12
              simplify: 0
            - minzoom: 8

A rule can set:

  • minzoom and maxzoom, numbers or expressions. They replace the layer's zoom expressions and are clamped to the layer's zooms. A rule's expression that is null falls back to the layer's.
  • simplify and min_size, at every zoom.
  • attributes, as a map without * prefixes. They replace the layer's attributes of the same name, at all zooms, and add the others.

Draw order

Without sort_by, the features of a layer keep the order the source reads them in.

  • A table is read in its physical order.
  • A view or foreign table with an integer id_column is read by that id, which overrides the view's ORDER BY.
  • Any other view is read in its own order.

The tile server reads each tile's features in the order PostgreSQL finds them, which can differ from request to request, so only sort_by gives served tiles a fixed draw order.

sort_by is a list of expressions, each a string or { expr: ..., desc: true }, compared in turn. Features that compare equal keep the order they are read in.

postgres:
  tables:
    buildings:
      schema: public
      table: buildings
      geometry_column: geom
      properties:
        z_order: int4
        height: float8
      layers:
        buildings:
          sort_by: ["z_order", { expr: "height", desc: true }]

sort_by compares z_order ascending, then height descending; null sorts first and features that compare equal keep the order they were read inread orderfeaturez_orderheightf1110f2030f3125f4030f5null5sortedfeaturez_orderheightf5null5f2030f4030f3125f1110sort_by: [z_order, {expr: height, desc: true}]null firsttie: read order

Values order by type first: null (and failed evaluations), booleans, integers, floating-point numbers, then strings. Any integer sorts before any floating-point number, so convert mixed numbers with double(...) to compare them by value. desc reverses the whole order, so null comes last.

Expressions

Expressions are written in CEL, the Common Expression Language. CEL looks like the conditions of C, Java or JavaScript, but it has no statements, loops or side effects. Every expression returns one value, and it always finishes.

A property is read by its name, such as rank > 5, or with feature['name:en'] when the name is not an identifier. A missing property is null. Strings take single or double quotes, so quote the whole expression in YAML, for example where: "kind == 'park'".

7 / 2              // 3, integer division rounds toward zero
-7 / 2             // -3
7 % 3              // 1
7.0 / 2.0          // 3.5
2 * 1.5            // fails, integers and doubles do not mix in arithmetic
double(2) * 1.5    // 3.0
3 >= 2 && 'a' != 'b'           // true
!(1 > 2) || false              // true
1 < 2.5                        // true, comparisons do mix integers and doubles
'park' in ['park', 'forest']   // true
population > 1000000 ? 2 : 8      // 2 for a city of 5 million, else 8
height > 0 ? height : 'unknown'   // the two branches can have different types
1 ? 2 : 3                         // fails, the condition must be a boolean

When an attribute gets different types in one tile, such as numbers and strings, all its values in that tile are written as strings.

int(2.7)        // 2
int('12')       // 12
double('1.5')   // 1.5
string(5)       // '5'
'Main' + ' St'                      // 'Main St'
size('Straße')                      // 6
'Main St'.startsWith('Main')        // true
'Main St'.endsWith('St')            // true
'Main St'.contains('n S')           // true
'Main St'.lowerAscii()              // 'main st'
' Main St '.trim()                  // 'Main St'
'Main St'.replace('St', 'Street')   // 'Main Street'
'Main St'.substring(0, 4)           // 'Main'
'Main St'.indexOf('St')             // 5
'shop;cafe'.split(';')                          // ['shop', 'cafe']
['shop', 'cafe'].join(', ')                     // 'shop, cafe'
'shop;cafe'.split(';').exists(k, k == 'shop')   // true
[1, 2, 3].all(x, x > 0)                         // true
[1, 2, 3].map(x, x * 2)                         // [2, 4, 6]
[1, 2, 3].filter(x, x > 1)                      // [2, 3]
[10, 20][1]                                     // 20
{'a': 1}['a']                                   // 1
coalesce(feature['name:en'], name)   // the English name, else the name
coalesce(null, null)                 // null

Martin adds these functions to CEL:

  • coalesce(a, b, ...) returns its first argument that is not null.

Regular expressions (matches) and timestamps are not available.

Expression Result
where A boolean, where null counts as false
minzoom, maxzoom An integer, clamped to the layer's zooms, or null for the layer's
id A non-negative integer, else the feature has no id
attribute Any value, where null leaves the attribute out
sort_by Any value, see draw order

An expression that fails on a feature, for example by dividing by zero or by returning a string where a zoom is needed, counts as null there. The first failure of each expression is logged, and the run ends with how often each one failed.

postgres:
  tables:
    places:
      schema: public
      table: places
      geometry_column: geom
      properties:
        osm_id: int8
        class: text
        name: text
        "name:en": text
        population: int8
      layers:
        places:
          where: "population > 1000"
          minzoom: "population > 1000000 ? 2 : 8"
          id: "osm_id * 10"
          attributes:
            kind: class
            label: { expr: "coalesce(feature['name:en'], name)", minzoom: 10 }
            source: { value: osm }
            rank: 3

Serving

The tile server renders a table source with layers itself instead of asking PostGIS for an ST_AsMVT tile. Each request reads the whole geometry of every feature that reaches the tile or its buffer, and renders the tile bulk mode writes for the same maxzoom. Serving tiles shows how.

  • The highest zoom is the table source's maxzoom. Without one, it is the highest zoom the extent fits: 18 for an extent of 4096. A higher maxzoom fails at startup.
  • Tiles are MVT, on the Web Mercator tile grid only, and the source takes no URL query parameters.
  • max_feature_count limits the features read for each tile. With it, order_by chooses which are kept. order_by alone changes nothing, as sort_by sets the draw order.

Validation

  • Reading the configuration fails on invalid values, with their line and column: a zoom above 30, a negative simplify, a minzoom above maxzoom, an expression that is not valid CEL, or layer_id together with layers. Unknown keys are warned about.
  • Before rendering, after connecting to the database, the run fails on unknown property names, duplicate layer names, more than 256 layers, a zoom above 27, and the settings that are not supported yet.