diff --git a/integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/build.gradle b/integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/build.gradle index a89180fe9..f8b5b0630 100644 --- a/integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/build.gradle +++ b/integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/build.gradle @@ -12,7 +12,7 @@ android { dependencies { implementation "com.facebook.react:react-android" implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.20" - implementation "org.maplibre.gl:android-sdk-opengl:13.4.1" + implementation "org.maplibre.gl:android-sdk-opengl:13.4.1-step060d" // RN autolinking names the scoped MLRN Android project deterministically. The app owns the // runtime dependency; this module only needs its public MLRNMapView type at compile time. compileOnly project(":maplibre_maplibre-react-native") diff --git a/integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/src/main/java/org/maplibre/particle/rntesthost/weather/WeatherRuntimeBridgeModule.kt b/integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/src/main/java/org/maplibre/particle/rntesthost/weather/WeatherRuntimeBridgeModule.kt index f0b420b6f..8c502a816 100644 --- a/integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/src/main/java/org/maplibre/particle/rntesthost/weather/WeatherRuntimeBridgeModule.kt +++ b/integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/src/main/java/org/maplibre/particle/rntesthost/weather/WeatherRuntimeBridgeModule.kt @@ -12,6 +12,7 @@ import com.facebook.react.uimanager.IllegalViewOperationException import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.common.UIManagerType import org.maplibre.android.maps.MapLibreMap +import org.maplibre.android.maps.Style import org.maplibre.android.maps.WeatherRuntime import org.maplibre.reactnative.components.mapview.MLRNMapView @@ -24,8 +25,21 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) : @ReactMethod override fun attach(reactTag: Double, promise: Promise) = withMapView(reactTag, promise) { view, map -> val style = map.style ?: return@withMapView reject(promise, "STYLE_NOT_READY", "The MapLibre style is not loaded") - if (style.getSource(WeatherIds.SOURCE) == null || style.getLayer(WeatherIds.COLOR_LAYER) == null || style.getLayer(WeatherIds.PARTICLE_LAYER) == null) { - return@withMapView reject(promise, "WEATHER_OBJECTS_NOT_READY", "The Test Host weather source/layers are absent from the current style") + val sourceExists = style.getSource(WeatherIds.SOURCE) != null + val weatherColorLayerExists = style.getLayer(WeatherIds.COLOR_LAYER) != null + val particleLayerExists = style.getLayer(WeatherIds.PARTICLE_LAYER) != null + if (!sourceExists || !weatherColorLayerExists || !particleLayerExists) { + return@withMapView reject( + promise, + "WEATHER_OBJECTS_NOT_READY", + weatherObjectsNotReadyDiagnostic( + reactTag.toInt(), + style, + sourceExists, + weatherColorLayerExists, + particleLayerExists, + ), + ) } val runtime = map.replaceWeatherRuntime(WeatherRuntime.Config(WeatherIds.SOURCE, WeatherIds.COLOR_LAYER, WeatherIds.PARTICLE_LAYER)) // MLRN drops its MapView by detaching it. This listener captures only that concrete map, @@ -126,7 +140,60 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) : private fun snapshot(runtime: WeatherRuntime) = Arguments.createMap().apply { putString("state", runtime.state.name); putString("playbackState", runtime.playbackState.name); putString("targetFps", runtime.targetFps.name); putBoolean("disposed", runtime.isDisposed) } + + /** + * Keep the attach rejection searchable and bounded: MLRN lifecycle investigation needs the + * exact absent weather object plus enough final-style context, but never an unbounded dump. + * This executes on the UI queue together with the Style lookup above. + */ + private fun weatherObjectsNotReadyDiagnostic( + reactTag: Int, + style: Style, + sourceExists: Boolean, + weatherColorLayerExists: Boolean, + particleLayerExists: Boolean, + ): String { + val sourceIds = style.getSources().map { it.id } + val layerIds = style.getLayers().map { it.id } + return buildString { + append("WEATHER_OBJECTS_NOT_READY reactTag=").append(reactTag) + append(" mapView=present mapLibreMap=present style=present") + append(" source[").append(WeatherIds.SOURCE).append("]=") + .append(presence(sourceExists)) + append(" layer[").append(WeatherIds.COLOR_LAYER).append("]=") + .append(presence(weatherColorLayerExists)) + append(" layer[").append(WeatherIds.PARTICLE_LAYER).append("]=") + .append(presence(particleLayerExists)) + append(" styleSourceCount=").append(sourceIds.size) + append(" styleLayerCount=").append(layerIds.size) + append(" styleSourceIds=").append(boundedIds(sourceIds)) + append(" styleLayerIds=").append(boundedIds(layerIds)) + } + } + + private fun presence(exists: Boolean) = if (exists) "present" else "missing" + + private fun boundedIds(ids: List): String { + val weatherIds = ids.filter { it in WeatherIds.ALL }.distinct() + val otherIds = ids.filterNot { it in WeatherIds.ALL }.take(MAX_STYLE_IDS - weatherIds.size) + val shown = weatherIds + otherIds + val omitted = ids.size - shown.size + return buildString { + append('[').append(shown.joinToString(",")) + if (omitted > 0) append(",…+").append(omitted) + append(']') + } + } + private fun reject(promise: Promise, code: String, message: String) = promise.reject(code, message) - private object WeatherIds { const val SOURCE = "rn-weather-fields"; const val COLOR_LAYER = "rn-weather-color"; const val PARTICLE_LAYER = "rn-weather-particles" } - companion object { const val NAME = "WeatherRuntimeBridge" } + private object WeatherIds { + const val SOURCE = "rn-weather-fields" + const val COLOR_LAYER = "rn-weather-color" + const val PARTICLE_LAYER = "rn-weather-particles" + val ALL = setOf(SOURCE, COLOR_LAYER, PARTICLE_LAYER) + } + companion object { + const val NAME = "WeatherRuntimeBridge" + const val MAX_STYLE_IDS = 24 + } } diff --git a/integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js b/integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js index d96871a41..ea841f88f 100644 --- a/integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js +++ b/integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js @@ -10,7 +10,7 @@ const marker = "// MapLibreParticleRNTestHost custom OpenGL SDK"; module.exports = function withParticleNativeAndroid(config) { return withProjectBuildGradle(config, (project) => { if (project.modResults.contents.includes(marker)) return project; - project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.clear()\n p.repositories.mavenLocal()\n p.repositories.google()\n p.repositories.mavenCentral { content { excludeModule(\"org.maplibre.gl\", \"android-sdk-opengl\") } }\n }\n}\nrootProject.ext.set(\"org.maplibre.reactnative.nativeVersion\", \"13.4.1\")\nrootProject.ext.set(\"org.maplibre.reactnative.nativeVariant\", \"opengl\")\n`; + project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.clear()\n p.repositories.mavenLocal()\n p.repositories.google()\n p.repositories.mavenCentral { content { excludeModule(\"org.maplibre.gl\", \"android-sdk-opengl\") } }\n }\n}\nrootProject.ext.set(\"org.maplibre.reactnative.nativeVersion\", \"13.4.1-step060d\")\nrootProject.ext.set(\"org.maplibre.reactnative.nativeVariant\", \"opengl\")\n`; return project; }); }; diff --git a/platform/android/VERSION b/platform/android/VERSION index c27905ac3..8af61e2f9 100644 --- a/platform/android/VERSION +++ b/platform/android/VERSION @@ -1 +1 @@ -13.4.1 +13.4.1-step060d