Skip to content
This site is a preview of the main branch. The published documentation is at maplibre.org.

Expressions in Kotlin

A layer property, such as the color of a circle, can have one value for the whole layer or a different value for each feature. It can also change with the zoom level. MapLibre describes these properties with expressions: small formulas inside the style that the map evaluates while it draws. The MapLibre Style Specification writes expressions as JSON arrays. MapLibre Compose builds them with typed Kotlin functions from org.maplibre.compose.expressions.dsl, so a layer parameter has a type such as Expression<ColorValue>.

For a fixed value, wrap the value in const. Sizes take a Compose Dp, colors take a Compose Color, and offsets take a DpOffset.

App.kt
val state = rememberMapState {
val earthquakes =
rememberGeoJsonSource(
GeoJsonData.Uri("https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson")
)
CircleLayer(
id = "quakes-constant",
source = earthquakes,
radius = const(4.dp),
color = const(Color.Red),
)
}
MaplibreMap(state = state)

The examples below draw the same earthquakes source.

Each feature in a source has properties, such as the magnitude of an earthquake. The feature object reads the data of the feature that the map is drawing: feature["mag"] is its mag property. The map learns the type of a property only when it evaluates the expression, so give the property a type with a type assertion such as asNumber before you compare or calculate with it. An equality test such as eq works without a type.

step picks an output by ranges of its input. switch picks the output of the first condition that is true.

App.kt
val state = rememberMapState {
CircleLayer(
id = "quakes-by-magnitude",
source = earthquakes,
// Below magnitude 4: 4 dp. From 4 up to 6: 8 dp. 6 and above: 16 dp.
radius =
step(
input = feature["mag"].asNumber(),
fallback = const(4.dp),
4 to const(8.dp),
6 to const(16.dp),
),
// Blue where the "tsunami" property is 1, red everywhere else.
color =
switch(
condition(test = feature["tsunami"] eq const(1), output = const(Color.Blue)),
fallback = const(Color.Red),
),
)
}
MaplibreMap(state = state)

The map evaluates these expressions separately for each earthquake, so each circle gets its size and color from its own data.

zoom is the current zoom level of the map. Pass it to interpolate to compute a value between stops, so that circles grow as the user zooms in.

App.kt
val state = rememberMapState {
CircleLayer(
id = "quakes-by-zoom",
source = earthquakes,
// 2 dp at zoom 5 and below, 8 dp at zoom 10 and above, in between on a straight line.
radius = interpolate(linear(), zoom(), 5 to const(2.dp), 10 to const(8.dp)),
)
}
MaplibreMap(state = state)

MapLibre accepts zoom() only as the input of a step or interpolate at the top of a property’s expression.

The filter parameter of a layer takes a true-or-false expression. The layer draws only the features for which it is true. The source keeps every feature, so another layer on the same source can draw the rest.

App.kt
val state = rememberMapState {
CircleLayer(
id = "strong-quakes",
source = earthquakes,
filter = feature["mag"].asNumber() gte const(5f),
)
}
MaplibreMap(state = state)

A feature does not always have every property. In this data, most earthquakes have no felt value, so feature["felt"] is null for them. A type assertion such as asNumber() fails on null. When an expression fails for a feature, MapLibre uses the default value of the property for that feature, and a filter that fails hides the feature.

To choose the value yourself, give the type assertion a fallback. To skip features that lack the value, filter them out.

App.kt
val state = rememberMapState {
// Every earthquake, sized by its "felt" value. A missing value counts as 0 reports.
CircleLayer(
id = "quakes-by-felt",
source = earthquakes,
radius =
interpolate(
linear(),
feature["felt"].asNumber(const(0f)),
0 to const(4.dp),
100 to const(16.dp),
),
)
// Only the earthquakes that have a "felt" value.
CircleLayer(id = "felt-quakes", source = earthquakes, filter = feature["felt"] neq nil())
}
MaplibreMap(state = state)

To use the first of several properties that has a value, use coalesce. The Expression reference lists which functions accept a missing value.

Kotlin code in the content block runs when the content composes. It builds an expression and passes it to the layer. The map evaluates that expression later, while it draws, for each feature.

App.kt
val state = rememberMapState {
// Kotlin chooses one stroke color for the whole layer while the content composes.
val strokeColor = if (isSystemInDarkTheme()) Color.White else Color.Black
CircleLayer(
id = "quakes-themed",
source = earthquakes,
strokeColor = const(strokeColor),
strokeWidth = const(1.dp),
// The map chooses a fill color for each earthquake while it draws.
color =
switch(
condition(test = feature["mag"].asNumber() gte const(5f), output = const(Color.Red)),
fallback = const(Color.Yellow),
),
)
}
MaplibreMap(state = state)

The Kotlin if reads app state and picks one stroke color for the whole layer. When the system theme changes, the content recomposes and the layer gets the new color. The switch expression reads the data of each earthquake, which Kotlin code cannot see, and picks a fill color for each one.

Use Kotlin for choices that depend on app state, and expressions for choices that depend on feature data or zoom. To change how one feature looks when the user selects it, use feature state; see Query and highlight features.