MapLibre Android Version
11.9.0
Android SDK Version
15
Device
Google Pixel 8a
What happened?
I have a map with some points. The points have a clustered GeoJsonSource that gets updated frequently when the user moves the map.
- On a map click, I query the rendered features with
mapLibreMap.queryRenderedFeatures.
- I look for a cluster feature (
feature.getBooleanProperty("cluster") == true)
- I want to zoom to the appropriate zoom level and call
source.getClusterExpansionZoom(feature) to retrieve that zoom level
The problem I run into is the following: When the source is updated after I queried the rendered features, but before I call source.getClusterExpansionZoom or source.getClusterLeaves, I am calling these functions with a feature that no longer exists in the source.
For getClusterExpansionZoom I get the error: The associated promise has been destructed prior to the associated state becoming ready.
For getClusterLeaves I get the error: No cluster with the specified id.
The problem I have is this: even with a try/catch around these functions, the app still crashes. I believe there's some sort of uncaught exception in the native code?
I tried synchronizing calls to these functions with source.setGeoJson(), but since that function happens asynchronously in the native code, there's no way to tell when the new features will be available to query.
Here's a minimal Android App that reproduces the problem:
class MainActivity : Activity() {
private lateinit var mapView: MapView
private lateinit var mapLibreMap: MapLibreMap
private lateinit var geoJsonSource: GeoJsonSource
private val SOURCE_ID = "clustered-points"
private val CLUSTER_LAYER_ID = "cluster-layer"
private val SYMBOL_LAYER_ID = "symbol-layer"
private val random = Random()
private val updateInterval = 1.seconds
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MapLibre.getInstance(this)
setContentView(R.layout.activity_main)
mapView = findViewById(R.id.mapView)
mapView.onCreate(savedInstanceState)
mapView.getMapAsync { map ->
mapLibreMap = map
setupMap()
setupClickListener()
simulateFrequentUpdates()
}
}
private fun setupMap() {
geoJsonSource = GeoJsonSource(
SOURCE_ID,
FeatureCollection.fromFeatures(generateRandomPoints(100)),
GeoJsonOptions()
.withCluster(true)
.withClusterMaxZoom(14)
.withClusterRadius(50)
)
mapLibreMap.setStyle("https://demotiles.maplibre.org/style.json") { style ->
style.addSource(geoJsonSource)
style.addLayer(
CircleLayer(CLUSTER_LAYER_ID, SOURCE_ID)
.withProperties(
circleColor(Color.MAGENTA),
circleRadius(20f)
)
.withFilter(has("point_count"))
)
style.addLayer(
SymbolLayer(SYMBOL_LAYER_ID, SOURCE_ID)
.withProperties(
textField("{point_count_abbreviated}"),
textSize(15f),
textColor(Color.WHITE),
textIgnorePlacement(true),
textAllowOverlap(true)
)
.withFilter(has("point_count"))
)
}
}
private fun setupClickListener() {
mapLibreMap.addOnMapClickListener(
MapLibreMap.OnMapClickListener { point ->
val renderedFeatures = mapLibreMap.queryRenderedFeatures(
mapLibreMap.projection.toScreenLocation(point),
CLUSTER_LAYER_ID,
)
val clusterFeature = renderedFeatures
.firstOrNull { it.getBooleanProperty("cluster") }
?: return@OnMapClickListener false
GlobalScope.launch(Dispatchers.Main) {
delay(updateInterval) // add delay for reproducability
try {
val leaves = geoJsonSource.getClusterLeaves(clusterFeature, 10, 0).features() ?: emptyList()
println("Leaves.size: ${leaves.size}")
val zoom = geoJsonSource.getClusterExpansionZoom(clusterFeature)
println("Cluster expansion zoom: $zoom")
} catch (e: Exception) {
println("Exception")
e.printStackTrace()
}
}
true
}
)
}
private fun simulateFrequentUpdates() {
fixedRateTimer(period = updateInterval.inWholeMilliseconds) {
runOnUiThread {
geoJsonSource.setGeoJson(FeatureCollection.fromFeatures(generateRandomPoints(500)))
Log.d("MapLibreBug", "GeoJsonSource updated")
}
}
}
private fun generateRandomPoints(count: Int): List<Feature> {
val features = mutableListOf<Feature>()
for (i in 0 until count) {
// Roughly Europe: latitude 36 to 71, longitude -10 to 40
val lat = 36.0 + random.nextDouble() * (71.0 - 36.0)
val lng = -10.0 + random.nextDouble() * (40.0 - (-10.0))
features.add(Feature.fromGeometry(Point.fromLngLat(lng, lat)))
}
return features
}
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 onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}
override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}
}
MapLibre Android Version
11.9.0
Android SDK Version
15
Device
Google Pixel 8a
What happened?
I have a map with some points. The points have a clustered GeoJsonSource that gets updated frequently when the user moves the map.
mapLibreMap.queryRenderedFeatures.feature.getBooleanProperty("cluster") == true)source.getClusterExpansionZoom(feature)to retrieve that zoom levelThe problem I run into is the following: When the source is updated after I queried the rendered features, but before I call
source.getClusterExpansionZoomorsource.getClusterLeaves, I am calling these functions with a feature that no longer exists in the source.For
getClusterExpansionZoomI get the error:The associated promise has been destructed prior to the associated state becoming ready.For
getClusterLeavesI get the error:No cluster with the specified id.The problem I have is this: even with a try/catch around these functions, the app still crashes. I believe there's some sort of uncaught exception in the native code?
I tried synchronizing calls to these functions with
source.setGeoJson(), but since that function happens asynchronously in the native code, there's no way to tell when the new features will be available to query.Here's a minimal Android App that reproduces the problem: