Skip to content

CartoCSS notes

Mark Tehver edited this page Sep 23, 2021 · 14 revisions

About

These notes are intended to clarify how Carto Mobile SDK implements the CartoCSS styling language. The notes assume good understanding of the CartoCSS specification and cover various aspects specific to the SDK.

Supported CartoCSS parameters

SDK supports the following parameters from the CartoCSS specification:

Symbolizer Parameter
line line-color
line-opacity
line-width
line-dasharray
line-join
line-cap
line-offset
line-geometry-transform
line-comp-op
line-pattern line-pattern-file
line-pattern-fill
line-pattern-opacity
line-pattern-offset
line-pattern-geometry-transform
line-pattern-comp-op
polygon polygon-fill
polygon-opacity
polygon-geometry-transform
polygon-comp-op
polygon-pattern polygon-pattern-file
polygon-pattern-fill
polygon-pattern-opacity
polygon-pattern-geometry-transform
polygon-pattern-comp-op
point point-file
point-opacity
point-allow-overlap
point-ignore-placement
point-transform
point-comp-op
text text-name
text-face-name
text-placement
text-size
text-spacing
text-fill
text-opacity
text-halo-fill
text-halo-opacity
text-halo-radius
text-halo-rasterizer
text-allow-overlap
text-min-distance
text-transform
text-orientation
text-dx
text-dy
text-avoid-edges
text-wrap-width
text-wrap-before
text-wrap-character
text-character-spacing
text-line-spacing
text-horizontal-alignment
text-vertical-alignment
text-comp-op
text-clip
text-placement-priority
shield shield-name
shield-face-name
shield-file
shield-dx
shield-dy
shield-unlock-image
shield-placement
shield-size
shield-spacing
shield-fill
shield-text-opacity
shield-halo-fill
shield-halo-opacity
shield-halo-radius
shield-halo-rasterizer
shield-allow-overlap
shield-min-distance
shield-text-transform
shield-orientation
shield-text-dx
shield-text-dy
shield-avoid-edges
shield-wrap-width
shield-wrap-before
shield-wrap-character
shield-character-spacing
shield-line-spacing
shield-horizontal-alignment
shield-vertical-alignment
shield-comp-op
shield-clip
shield-placement-priority
marker marker-file
marker-placement
marker-type
marker-opacity
marker-color
marker-fill
marker-fill-opacity
marker-width
marker-height
marker-line-color
marker-line-opacity
marker-line-width
marker-spacing
marker-allow-overlap
marker-ignore-placement
marker-transform
marker-comp-op
marker-clip
marker-placement-priority
building building-fill
building-fill-opacity
building-height
building-min-height
building-geometry-transform

In addition, SDK support layer-level 'opacity' and limited set of 'comp-op' options. The following table lists the supported 'comp-op' values:

Comp-op
src
src-over
src-in
src-atop
dst
dst-over
dst-in
dst-atop
clear,zero
plus
minus
multiply
screen
darken
lighten

Extensions

View-level parameters

Most of the CartoCSS parameters are evaluated during tile loading ('loading domain'), but some parameters are evaluated during each frame ('rendering domain'). A typical example of view-level expression includes [view::zoom] parameter, for example:

  line-width: [view::zoom] * 0.2;

The list of all parameters that are evaluated in rendering domain:

Symbolizer Parameter
line line-color
line-opacity
line-width
line-pattern line-pattern-fill
line-pattern-opacity
polygon polygon-fill
polygon-opacity
polygon-pattern polygon-pattern-fill
polygon-pattern-opacity
point point-opacity
marker marker-width
marker-height
marker-color
marker-opacity
text text-size
text-fill
text-opacity
text-halo-fill
text-halo-opacity
text-halo-radius
shield shield-size
shield-fill
shield-text-opacity
shield-halo-fill
shield-halo-opacity
shield-halo-radius
building building-fill
building-fill-opacity

When a view-level expression is used for a parameter not in this table, its value is replaced with a reasonable default and the expression is evaluated during tile loading time. For example, [view::zoom] is replaced with [zoom] + 0.5. This gives forward compatibility, if more parameters will be converted to view-level parameters.

Nutiparameters

Nutiparameters are typed parameters that can be controlled from the application. This allows certain aspects of the styles to be controlled by the application. Typical examples include showing certain POIs, selecting map language and so on. Nutiparameters can be accessed from the style by using 'nuti::' prefix. For example:

  line-color: [nuti::color];

Here parameter 'color' is defined by the application.

Metavariables

SDK supports dynamic field names. Dynamic field names are specified using an expression enclosed in square or curly brackets. For example:

  text-name: [name_[nuti::lang]];

Here the field used for text names depends on the nutiparameter called 'lang'. If its value is 'en', then this expression is equivalent to [name_en].

Additional expressions and operators

SDK supports following operators in parameter values (in the order of precedence):

Operator
unary -, unary !
*, /
+, -
=~, =, !=, <=, <, >=, >
&&, ||
?:

For example, the following expression can be used for language-selection based on nutiparameter named 'lang':

  text-name: [name_[nuti::lang]] ? [name_[nuti::lang]] : [name];

In addition to the operators listed above, the following functions are supported:

Function
exp
log
pow
step
linear
cubic
length
uppercase
lowercase
capitalize
concat
replace
match

Interpolation expressions

SDK contains three special functions for key/value interpolation called step, linear and cubic. The first one provides 'constant' interpolation, or simply a table lookup, the second one provides linear interpolation between two nearest key values and the last one provides cubic interpolation with 2nd order continuity. The most useful is linear interpolation. For example:

  line-width: linear([x], (2, 1), (3, 2), (5, 8));

The following table demonstrates the values this expression produces depending on the value of [x]:

x Value
<=2 1
2.5 1.5
3 2
4 5
>=5 8

Complex selectors

Starting from SDK 4.4.2, SDK supports when selectors. This allows using any expression when filtering features, for example:

when (([class]='minor' || [class]=[subclass]) && [name]='Lane') {
  line-color: #fff;
}

It is recommended to use this feature sparingly, as when selectors may cause combinatorial explosion in the number of rules produced.

Global options

SDK supports global options for defining map pole colors when using globe view:

Map {
  south-pole-color: #fff;
  north-pole-color: #00f;
}

Label ids

SDK has special logic for labels (texts, shields, markers) and tries to keep them stationary while tiles change. In order to do so, labels need to be uniquely identified. By default SDK uses a combination of tile-level feature id and text/file name of the label. Starting from SDK 4.2, SDK has option to specify custom expression/key for the id via parameters text-feature-id, shield-feature-id and marker-feature-id. For example:

text-feature-id: [class] + '\n' + [name];

Starting from SDK 4.3, if 'feature id' expression evaluates to null, 0 or empty string, a unique id will be autogenerated.

Differences and incompatibilities

Carto Mobile SDK uses 'from scratch' implementation of the CartoCSS styling language and interprets few aspects of the styling differently compared to the MapBox Studio Classic (shorthand MBSC).

Interpretation of the '%' sign

The following line is interpreted differently in the SDK vs MBSC:

  line-width: mix(#fff, #000, 50);

While the following line works in both implementations:

  line-width: mix(#fff, #000, 50%);

MBSC interprets %-sign as an optional 'unit' while SDK treats this as a multiplicator with value of 1/100. The interpretation of SDK is consistent across all values and functions while the MBSC interpretation depends on the context and does not work everywhere. It is recommended to always add explicit % suffix to the functions that mix or modify colors. This makes the expressions compatible with both SDK and MBSC.

Interpretation of trailing comma in style selectors

The trailing comma after '#layer0' in the following example causes different interpretation of the style in the SDK vs MBSC:

  #layer0, {
    line-width: 1;
  }

MBSC ignores the trailing command while SDK interprets the command as a separator and assumes that the rules applies to elements of layer named 'layer0' and to all other elements. A simple fix is to remove all trailing commas from selectors to make the behavior consistent.

Interpretation of fields and pseudo-fields in expressions

The following expression results in different output in SDK vs MBSC:

  marker-width: ([scalerank]-1)*5;

SDK handles this as expected while MBSC provides very strange result. This seems to be a limitation in MBSC CartoCSS parser or CartoCSS -> MapnikXML translator. If compatibility between SDK and MBSC is required, complex expressions involving fields and pseudo-fields like [zoom] should be avoided.

Label rendering mode vs Simple rendering mode

SDK supports two separate rendering modes for points/markers/texts: 'label' mode and 'simple' mode. By default the mode is controlled by two parameters: 'allow-overlap' and 'clip'. If 'clip' is not defined, then 'allow-overlap' is used implicitly as its value. If 'clip' is set to false (explicitly or implicitly via 'allow-overlap'), then 'label' rendering mode is used. Otherwise 'simple' mode is used.

Label rendering mode does no clipping at tile boundaries and supports overlap detection and line geometry but is quite costly both memory wise and performance wise. It does not scale over 10,000 features. Label mode also requires unique feature ids to work properly. Labels are never clipped at tile borders.

Simple mode does not support overlap detection nor other label features but is very low cost and scales to 100,000 features and above. Simple mode features are always clipped at tile borders. Also simple mode features ignore 'placement' parameter and the features always rotate with the map.

Performance

SDK uses OpenGL as a rendering backend and few features can be unexpectedly expensive due to technical reasons.

Multiple symbolizers per layer

The following example uses 'polygon-pattern' and 'polygon' symbolizers:

#layer {
  polygon-pattern-file: url('pattern.png');
  polygon-fill: #fff;
}

If the layer contains many polygons (over a thousand) this results in lots of OpenGL draw calls and will most likely drop the rendering rate to a 1 frame per second. There are few special cases that are optimized and are not affected by this: SDK supports batching 'line' and 'line-pattern' symbolizers and 'line' and 'polygon' symbolizers.

Layer-level effects

Both layer-level 'comp-op' and 'opacity' require drawing to a separate frame buffer and full viewport blending, thus making these operations very expensive especially on high-resolution devices. If possible, element-level 'comp-op' and 'opacity' should be used ('polygon-opacity', for example).