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

Query and highlight features

A feature is one point, line, or polygon in a source, with its properties. You can ask the map which features it draws at a place on the screen, or which features a source has loaded. You can also attach a value such as selected to one feature and draw that feature differently.

To respond when the user taps a feature in one of your layers, give the layer an onClick handler instead, as shown in Handle gestures and clicks. The handler receives the features at the tap.

MapState.queryRenderedFeatures returns the features that the map draws inside a rectangle on the screen. This map lists the names of the places near a long press:

App.kt
@Composable
fun NearbyPlaces(onPlacesFound: (List<String>) -> Unit) {
val state =
rememberMapState(baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"))
val scope = rememberCoroutineScope()
MaplibreMap(
state = state,
interactions =
MapInteractions {
callbacks {
longClick {
onEvent { event ->
val area =
DpRect(
origin = event.screenOffset - DpOffset(24.dp, 24.dp),
size = DpSize(48.dp, 48.dp),
)
scope.launch {
val places =
state.queryRenderedFeatures(
rect = area,
layerIds = setOf("poi_r1", "poi_r7", "poi_r20"),
)
onPlacesFound(
places.mapNotNull { it.properties?.get("name")?.jsonPrimitive?.contentOrNull }
)
}
ClickResult.Consume
}
}
}
},
)
}

layerIds limits the result to features drawn by those layers. Here they are the point-of-interest layers of the OpenFreeMap Liberty style. Without layerIds, the result includes every layer, such as roads, water, and labels. The topmost feature comes first. To query a single point, pass a DpOffset instead of a DpRect. To keep only features with certain properties, pass an expression as predicate.

queryRenderedFeatures is a suspend function, so call it from a coroutine.

VectorTileSourceHandle.querySourceFeatures returns the features of a vector tile source, whether or not a layer draws them. Use it to search the data behind the map, such as every café in the loaded tiles:

Places.kt
suspend fun loadedCafes(state: MapState): List<Feature<Geometry, JsonObject?>> {
val tiles = state.style.sources["openmaptiles"] as? VectorTileSourceHandle ?: return emptyList()
return tiles.querySourceFeatures(
sourceLayerIds = setOf("poi"),
predicate = feature["class"] eq const("cafe"),
)
}

state.style.sources returns a source’s handle by ID, or by the source object that you declared. It returns null until the style has loaded. The result contains only features from the tiles that the map has loaded for the current view, and it can contain the same feature more than once when the feature crosses a tile edge.

Feature state is data that your app attaches to one feature while the map runs. A layer can read it with feature.state and draw that feature differently. This map draws three stops in blue and turns the tapped stop red:

App.kt
@Composable
fun SelectableStops() {
val stops = remember {
buildFeatureCollection<Point, JsonObject?> {
// Feature state needs an integer id on each feature.
addFeature(geometry = Point(Position(longitude = -122.4194, latitude = 37.7793))) {
setId(1)
}
addFeature(geometry = Point(Position(longitude = -122.4089, latitude = 37.7841))) {
setId(2)
}
addFeature(geometry = Point(Position(longitude = -122.3937, latitude = 37.7955))) {
setId(3)
}
}
}
val state =
rememberMapState(
baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"),
initialCameraPosition =
CameraPosition(center = Position(longitude = -122.41, latitude = 37.785), zoom = 13.0),
) {
val mapState = LocalMapState.current
val source = rememberGeoJsonSource(GeoJsonData.Features(stops))
var selectedId by remember { mutableStateOf<String?>(null) }
CircleLayer(
id = "stops",
source = source,
radius = const(10.dp),
color =
switch(
condition(feature.state("selected").asBoolean(const(false)), const(Color.Red)),
fallback = const(Color.Blue),
),
onClick = { features ->
val handle = mapState?.style?.sources?.get(source)
val id = features.first().id?.content
if (handle != null && id != null) {
selectedId?.let { handle.removeFeatureState(it, "selected") }
handle.setFeatureState(id, buildJsonObject { put("selected", true) })
selectedId = id
}
ClickResult.Consume
},
)
}
MaplibreMap(state = state)
}

setFeatureState adds values to a feature’s state. The map clears feature state when it loads another base style, so the example remembers selectedId inside the content block, which resets at the same time. removeFeatureState removes them, and resetFeatureStates clears the state of every feature in the source. Inside the content block of rememberMapState, LocalMapState gives the map state, and style.sources gives the source’s handle.

Feature state finds a feature by its id. Give each GeoJSON feature an integer id, and pass the id to setFeatureState as text, as features.first().id?.content does in the example. For a vector tile source, also pass the source layer, and the tile data must give each feature an id.

Only some layer properties can read feature state, such as the color and radius of a circle layer. The API reference for each layer property says whether it can. Filters cannot read feature state.