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

Add images and icons

A SymbolLayer draws an icon at each feature in its source. Its iconImage parameter takes an image expression, which chooses the icon.

Pass any Compose Painter to image(). You can load one with painterResource or draw your own. This painter draws a badge using your Material theme’s colors and typography:

App.kt
@Composable
private fun rememberBadgePainter(label: String): Painter {
val colors = MaterialTheme.colorScheme
val style = MaterialTheme.typography.labelLarge.copy(color = colors.onPrimaryContainer)
val text = rememberTextMeasurer().measure(label, style = style)
val padding = with(LocalDensity.current) { 12.dp.toPx() }
return remember(colors.primaryContainer, text, padding) {
object : Painter() {
override val intrinsicSize = Size(text.size.width + padding * 2, text.size.height + padding)
override fun DrawScope.onDraw() {
drawRoundRect(colors.primaryContainer, cornerRadius = CornerRadius(size.height / 2))
drawText(
text,
topLeft =
Offset((size.width - text.size.width) / 2, (size.height - text.size.height) / 2),
)
}
}
}
}

Use the painter as an icon on a SymbolLayer:

App.kt
val position = Position(longitude = -74.0120, latitude = 40.7067)
val state =
rememberMapState(initialCameraPosition = CameraPosition(center = position, zoom = 15.5)) {
val source =
rememberGeoJsonSource(GeoJsonData.Features(Feature(Point(position), properties = null)))
SymbolLayer(
id = "badge",
source = source,
iconImage = image(rememberBadgePainter("12 min")),
iconAllowOverlap = const(true),
)
}
MaplibreMap(state = state)

image() uses the painter’s intrinsic size; pass size to set a different icon size. To color a single-color icon from the layer, pass drawAsSdf = true to image() and set the layer’s iconColor.

The map hides an icon that would overlap another icon or label. Set iconAllowOverlap = const(true) on the layer to draw every icon.

rememberImageSource pins one image to the map at four corner coordinates, and a RasterLayer draws it. The image moves and scales with the map. This example lays a 1660 plan of New Amsterdam over lower Manhattan:

App.kt
val state = rememberMapState {
val corners =
PositionQuad(
topLeft = Position(longitude = -74.0178, latitude = 40.7040),
topRight = Position(longitude = -74.0118, latitude = 40.7100),
bottomRight = Position(longitude = -74.0060, latitude = 40.7067),
bottomLeft = Position(longitude = -74.0120, latitude = 40.7006),
)
val plan = rememberImageSource(position = corners, uri = Res.getUri("files/castello-plan.jpg"))
RasterLayer(id = "castello-plan", source = plan, opacity = const(0.8f))
}
MaplibreMap(state = state)

The corners do not need to form a rectangle that faces north, so you can rotate the image to fit the map. To draw pixels that your app creates, pass a PreparedImage instead of a URI.

A layer can ask for an icon that the style does not contain, for example when your data names an icon for each feature. The map then draws no icon for that feature. Set MapState.missingImageResolver to supply the image when the map asks for it. This example draws a fallback painter for every missing icon:

App.kt
val mapState = rememberMapState()
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
DisposableEffect(mapState, fallback, density, layoutDirection) {
mapState.missingImageResolver = {
ResolvedStyleImage.fromPainter(fallback, density, layoutDirection)
}
onDispose { mapState.missingImageResolver = null }
}
MaplibreMap(state = mapState)

The resolver receives a MissingImageRequest with the icon’s ID. Return a ResolvedStyleImage, or null to leave the icon empty.

To add or replace an image under a fixed ID from code, call StyleImages.set on mapState.style.images.