Filter Map Features by Properties
This tutorial shows how to dynamically filter what's visible on your MapMetrics Android map based on data properties — useful for category filtering, search, and interactive data exploration.
Prerequisites
- Completed the Getting Started Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Filter by Category
Show/hide features based on a category property:
kotlin
import android.graphics.Color
import android.os.Bundle
import android.widget.ToggleButton
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.JsonObject
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.MapMetricsMap
import org.maplibre.android.maps.Style
import org.maplibre.android.style.expressions.Expression
import org.maplibre.android.style.expressions.Expression.*
import org.maplibre.android.style.layers.CircleLayer
import org.maplibre.android.style.layers.PropertyFactory.*
import org.maplibre.android.style.sources.GeoJsonSource
import org.maplibre.geojson.Feature
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point
class FilterActivity : AppCompatActivity() {
private lateinit var mapView: MapView
private lateinit var map: MapMetricsMap
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_filter)
mapView = findViewById(R.id.mapView)
mapView.onCreate(savedInstanceState)
mapView.getMapAsync { mapMetricsMap ->
map = mapMetricsMap
map.setStyle(
Style.Builder().fromUri(
"https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY"
)
) { style ->
addData(style)
setupFilters(style)
}
}
}
private fun addData(style: Style) {
// Sample data with categories
val features = listOf(
createFeature(2.3522, 48.8566, "restaurant", "Le Petit Bistro"),
createFeature(2.3400, 48.8600, "restaurant", "Café de Flore"),
createFeature(2.3376, 48.8606, "museum", "Louvre Museum"),
createFeature(2.3266, 48.8600, "museum", "Musée d'Orsay"),
createFeature(2.3464, 48.8462, "park", "Luxembourg Gardens"),
createFeature(2.3131, 48.8601, "park", "Tuileries Garden"),
createFeature(2.3350, 48.8550, "hotel", "Hôtel Lutetia"),
createFeature(2.3280, 48.8680, "hotel", "Le Meurice"),
)
style.addSource(
GeoJsonSource("places", FeatureCollection.fromFeatures(features))
)
// Color circles by category
style.addLayer(
CircleLayer("places-layer", "places")
.withProperties(
circleRadius(8f),
circleColor(
match(
get("category"),
color(Color.GRAY), // default
stop("restaurant", color(Color.parseColor("#FF6B35"))),
stop("museum", color(Color.parseColor("#4285F4"))),
stop("park", color(Color.parseColor("#34A853"))),
stop("hotel", color(Color.parseColor("#9C27B0")))
)
),
circleStrokeColor(Color.WHITE),
circleStrokeWidth(2f)
)
)
map.cameraPosition = CameraPosition.Builder()
.target(LatLng(48.857, 2.335))
.zoom(13.0)
.build()
}
private fun createFeature(lng: Double, lat: Double, category: String, name: String): Feature {
val props = JsonObject().apply {
addProperty("category", category)
addProperty("name", name)
}
return Feature.fromGeometry(Point.fromLngLat(lng, lat), props)
}
private fun setupFilters(style: Style) {
val layer = style.getLayer("places-layer") as? CircleLayer
val categories = mapOf(
R.id.btnRestaurants to "restaurant",
R.id.btnMuseums to "museum",
R.id.btnParks to "park",
R.id.btnHotels to "hotel",
)
val activeCategories = mutableSetOf("restaurant", "museum", "park", "hotel")
for ((btnId, category) in categories) {
findViewById<ToggleButton>(btnId).apply {
isChecked = true
setOnCheckedChangeListener { _, checked ->
if (checked) activeCategories.add(category)
else activeCategories.remove(category)
applyFilter(layer, activeCategories)
}
}
}
}
private fun applyFilter(layer: CircleLayer?, categories: Set<String>) {
if (categories.isEmpty()) {
// Hide all
layer?.setFilter(literal(false))
} else if (categories.size == 4) {
// Show all — clear filter
layer?.setFilter(literal(true))
} else {
// Show only selected categories
val conditions = categories.map { cat ->
eq(get("category"), literal(cat))
}
layer?.setFilter(any(*conditions.toTypedArray()))
}
}
override fun onStart() { super.onStart(); mapView.onStart() }
override fun onResume() { super.onResume(); mapView.onResume() }
override fun onPause() { super.onPause(); mapView.onPause() }
override fun onStop() { super.onStop(); mapView.onStop() }
override fun onDestroy() { super.onDestroy(); mapView.onDestroy() }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}
}Filter by Numeric Range
Show features within a value range:
kotlin
// Show only features with "rating" >= 4.0
layer?.setFilter(
gte(get("rating"), literal(4.0))
)
// Show features with "price" between 10 and 50
layer?.setFilter(
all(
gte(get("price"), literal(10)),
lte(get("price"), literal(50))
)
)Filter by Text Search
Filter features whose name contains a search query:
kotlin
import android.text.Editable
import android.text.TextWatcher
import android.widget.EditText
private fun setupSearch(style: Style) {
val layer = style.getLayer("places-layer") as? CircleLayer
findViewById<EditText>(R.id.searchInput).addTextChangedListener(
object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
val query = s.toString().lowercase()
if (query.isEmpty()) {
layer?.setFilter(literal(true))
} else {
// Note: Expression-based text search is limited.
// For full text search, filter the GeoJSON source instead.
filterSourceByName(style, query)
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
}
)
}
private fun filterSourceByName(style: Style, query: String) {
val allFeatures = /* keep a reference to all features */ listOf<Feature>()
val filtered = allFeatures.filter { feature ->
val name = feature.getStringProperty("name") ?: ""
name.lowercase().contains(query)
}
val source = style.getSource("places") as? GeoJsonSource
source?.setGeoJson(FeatureCollection.fromFeatures(filtered))
}Common Filter Expressions
| Expression | Description | Example |
|---|---|---|
eq(a, b) | Equals | eq(get("type"), literal("park")) |
neq(a, b) | Not equals | neq(get("status"), literal("closed")) |
gt(a, b) | Greater than | gt(get("rating"), literal(3)) |
gte(a, b) | Greater or equal | gte(get("price"), literal(10)) |
lt(a, b) | Less than | lt(get("age"), literal(5)) |
lte(a, b) | Less or equal | lte(get("distance"), literal(100)) |
has("key") | Property exists | has("phone") |
all(...) | AND — all must match | all(gt(...), lt(...)) |
any(...) | OR — at least one must match | any(eq(...), eq(...)) |
not(expr) | Negate | not(has("archived")) |
Next Steps
- Data-Driven Styling — Style features by properties
- Multiple Markers — Marker category filtering
- Circle Layer — Styled point data
Tip: For better search performance with large datasets, filter at the source level (source.setGeoJson()) instead of the layer level (layer.setFilter()). Source-level filtering prevents features from being rendered at all, while layer filters still process them on the GPU.