Skip to content

Add images and icons

A SymbolLayer draws an icon at each feature in its source. The image expression supplies the icon, from a Compose painter, an ImageBitmap, or the base style’s sprite.

Pass a painter to image(). Use painterResource to load an app resource, or supply your own painter:

App.kt
val state = rememberMapState {
val stations =
rememberGeoJsonSource(GeoJsonData.Uri(Res.getUri("files/data/amtrak_stations.geojson")))
SymbolLayer(
id = "station-icons",
source = stations,
iconImage = image(painterResource(Res.drawable.map_24px), size = DpSize(24.dp, 24.dp)),
)
}
MaplibreMap(state = state)

Base styles usually bundle a sprite: a named set of icons. Pass the icon name to image():

App.kt
val state = rememberMapState {
SymbolLayer(id = "station-markers", source = stations, iconImage = image("marker"))
}
MaplibreMap(state = state)

An ImageSource positions one image on the map by its four corner coordinates. A RasterLayer draws it:

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)

A style can reference an icon that its sprite does not contain. Set MapState.missingImageResolver to supply one. The map calls the resolver with a MissingImageRequest that holds the icon’s ID, and adds the ResolvedStyleImage that it returns:

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)

For imperative image updates, prepare an image with ResolvedStyleImage.fromBitmap(bitmap) or ResolvedStyleImage.fromPainter, then submit it with state.style.images.set(id, image). Preparation owns a pixel copy, so the result can be reused across image IDs and loaded styles.

set, setAll, and remove enqueue commands without waiting for the map. A replacement keeps the previous image until it succeeds; an engine rejection is logged. Looking up an image with state.style.images[id] suspends until the commands enqueued before it have run, so code that needs an image in place awaits its lookup. A finished command does not mean the map has rendered a frame.