From 544e4aee3ad639ce5d64c054c3668e2ea2de3d72 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Mon, 7 Sep 2026 14:10:06 +0800 Subject: [PATCH] fix: finalize WeatherRuntime host API boundary --- .../src/cpp/native_map_view.cpp | 26 +++++++++++++++++ .../src/cpp/native_map_view.hpp | 2 ++ .../maplibre/android/maps/MapLibreMap.java | 8 +++++ .../org/maplibre/android/maps/NativeMap.java | 4 +++ .../maplibre/android/maps/NativeMapView.java | 10 +++++++ .../maplibre/android/maps/WeatherRuntime.java | 9 ++++++ .../android/maps/WeatherRuntimeStateTest.java | 29 +++++++++++++++++++ .../ParticleCoreLayerValidationActivity.kt | 12 ++++---- 8 files changed, 94 insertions(+), 6 deletions(-) diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp index f5412af78..ab826b5fa 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp @@ -1194,6 +1194,30 @@ jni::jboolean NativeMapView::setRasterParticleColors(JNIEnv& env, return true; } +jni::jboolean NativeMapView::setRasterParticleColor(JNIEnv& env, + const jni::String& layerId, + const jni::Array& particle) { + auto* layer = dynamic_cast(map->getStyle().getLayer(jni::Make(env, layerId))); + if (!layer || particle.Length(env) != 4) return false; + std::array color; + particle.GetRegion(env, 0, color); + if (!mbgl::style::validWeatherRGBA(color)) return false; + layer->setParticleColor(color); + return true; +} + +jni::jboolean NativeMapView::setRasterParticleTrailColor(JNIEnv& env, + const jni::String& layerId, + const jni::Array& trail) { + auto* layer = dynamic_cast(map->getStyle().getLayer(jni::Make(env, layerId))); + if (!layer || trail.Length(env) != 4) return false; + std::array color; + trail.GetRegion(env, 0, color); + if (!mbgl::style::validWeatherRGBA(color)) return false; + layer->setTrailColor(color); + return true; +} + jni::jboolean NativeMapView::setWeatherColorStops(JNIEnv& env, const jni::String& layerId, const jni::Array& speeds, @@ -1695,6 +1719,8 @@ void NativeMapView::registerNative(jni::JNIEnv& env) { METHOD(&NativeMapView::setStyleLayerFloatProperty, "nativeSetStyleLayerFloatProperty"), METHOD(&NativeMapView::setStyleLayerVisibility, "nativeSetStyleLayerVisibility"), METHOD(&NativeMapView::setRasterParticleColors, "nativeSetRasterParticleColors"), + METHOD(&NativeMapView::setRasterParticleColor, "nativeSetRasterParticleColor"), + METHOD(&NativeMapView::setRasterParticleTrailColor, "nativeSetRasterParticleTrailColor"), METHOD(&NativeMapView::setWeatherColorStops, "nativeSetWeatherColorStops"), METHOD(&NativeMapView::getWeatherColorStopCapacity, "nativeGetWeatherColorStopCapacity"), METHOD(&NativeMapView::advanceRasterArraySourceFieldWindow, "nativeAdvanceRasterArraySourceFieldWindow"), diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp index 7095ee621..9f64906f5 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp @@ -284,6 +284,8 @@ public: jni::jboolean setStyleLayerFloatProperty(JNIEnv&, const jni::String&, const jni::String&, jni::jfloat); jni::jboolean setStyleLayerVisibility(JNIEnv&, const jni::String&, jni::jboolean); jni::jboolean setRasterParticleColors(JNIEnv&, const jni::String&, const jni::Array&, const jni::Array&); + jni::jboolean setRasterParticleColor(JNIEnv&, const jni::String&, const jni::Array&); + jni::jboolean setRasterParticleTrailColor(JNIEnv&, const jni::String&, const jni::Array&); jni::jboolean setWeatherColorStops(JNIEnv&, const jni::String&, const jni::Array&, const jni::Array&); jni::jint getWeatherColorStopCapacity(JNIEnv&); jni::jboolean advanceRasterArraySourceFieldWindow(JNIEnv&, const jni::String&); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java index b8bde2abe..a4f359810 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java @@ -301,6 +301,14 @@ public final class MapLibreMap { return nativeMapView.setRasterParticleColors(layerId, particleColor, trailColor); } + public boolean setRasterParticleColor(String layerId, float[] particleColor) { + return nativeMapView.setRasterParticleColor(layerId, particleColor); + } + + public boolean setRasterParticleTrailColor(String layerId, float[] trailColor) { + return nativeMapView.setRasterParticleTrailColor(layerId, trailColor); + } + /** Installs a validated WeatherColor speed ramp without changing particle style. */ public boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops) { return nativeMapView.setWeatherColorStops(layerId, speedsMps, rgbaStops); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java index b455a29f5..b7fe3852e 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java @@ -145,6 +145,10 @@ interface NativeMap { boolean setRasterParticleColors(String layerId, float[] particleColor, float[] trailColor); + boolean setRasterParticleColor(String layerId, float[] particleColor); + + boolean setRasterParticleTrailColor(String layerId, float[] trailColor); + boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops); int getWeatherColorStopCapacity(); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java index 2729654d1..f8cf435f8 100755 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java @@ -964,6 +964,14 @@ final class NativeMapView implements NativeMap { return !checkState("setRasterParticleColors") && nativeSetRasterParticleColors(layerId, particleColor, trailColor); } + public boolean setRasterParticleColor(String layerId, float[] particleColor) { + return !checkState("setRasterParticleColor") && nativeSetRasterParticleColor(layerId, particleColor); + } + + public boolean setRasterParticleTrailColor(String layerId, float[] trailColor) { + return !checkState("setRasterParticleTrailColor") && nativeSetRasterParticleTrailColor(layerId, trailColor); + } + public boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops) { return !checkState("setWeatherColorStops") && nativeSetWeatherColorStops(layerId, speedsMps, rgbaStops); } @@ -1745,6 +1753,8 @@ final class NativeMapView implements NativeMap { @Keep private native boolean nativeSetStyleLayerVisibility(String layerId, boolean visible); @Keep private native boolean nativeSetRasterParticleColors(String layerId, float[] particleColor, float[] trailColor); + @Keep private native boolean nativeSetRasterParticleColor(String layerId, float[] particleColor); + @Keep private native boolean nativeSetRasterParticleTrailColor(String layerId, float[] trailColor); @Keep private native boolean nativeSetWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops); @Keep private native int nativeGetWeatherColorStopCapacity(); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java index 0f46d222a..a25a9389d 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java @@ -91,6 +91,15 @@ public final class WeatherRuntime { public CommandResult setTrailOpacity(float opacity) { return setParticleFloat("raster-particle-trail-opacity", opacity); } public CommandResult setTrailFade(float fade) { return setParticleFloat("raster-particle-trail-fade", fade); } public CommandResult setParticleSpeedColorEnabled(boolean enabled) { return setParticleFloat("raster-particle-speed-color-enabled", enabled ? 1f : 0f); } + /** Sets only the particle RGBA; trail color and WeatherColor ramp are unchanged. */ + public CommandResult setParticleColor(float red, float green, float blue, float alpha) { + return result(map.setRasterParticleColor(config.particleLayerId, new float[]{red, green, blue, alpha})); + } + /** Sets only the trail RGBA; particle color and WeatherColor ramp are unchanged. */ + public CommandResult setTrailColor(float red, float green, float blue, float alpha) { + return result(map.setRasterParticleTrailColor(config.particleLayerId, new float[]{red, green, blue, alpha})); + } + /** Compatibility helper for existing callers; new Host code should use the independent setters. */ public CommandResult setParticleColors(float red, float green, float blue, float alpha, float trailRed, float trailGreen, float trailBlue, float trailAlpha) { return result(map.setRasterParticleColors(config.particleLayerId, new float[]{red, green, blue, alpha}, new float[]{trailRed, trailGreen, trailBlue, trailAlpha})); } diff --git a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java index c379ce80e..73c5e062c 100644 --- a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java +++ b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java @@ -2,7 +2,9 @@ package org.maplibre.android.maps; import org.junit.Test; import org.mockito.Mockito; +import org.mockito.ArgumentCaptor; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; /** Unit coverage for facade-only state transitions; no renderer frame callback is involved. */ @@ -63,4 +65,31 @@ public class WeatherRuntimeStateTest { assertEquals(WeatherRuntime.State.ERROR, runtime.getState()); Mockito.verify(map, Mockito.never()).setStyleLayerVisibility(Mockito.anyString(), Mockito.anyBoolean()); } + + @Test public void particleAndTrailColorsAreIndependentTypedCommands() { + MapLibreMap map = Mockito.mock(MapLibreMap.class); + Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8); + Mockito.when(map.setRasterParticleColor(Mockito.anyString(), Mockito.any(float[].class))).thenReturn(true); + Mockito.when(map.setRasterParticleTrailColor(Mockito.anyString(), Mockito.any(float[].class))).thenReturn(true); + WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle")); + assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setParticleColor(0.1f, 0.2f, 0.3f, 1.0f)); + assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setTrailColor(0.9f, 0.8f, 0.7f, 1.0f)); + ArgumentCaptor particleColor = ArgumentCaptor.forClass(float[].class); + ArgumentCaptor trailColor = ArgumentCaptor.forClass(float[].class); + Mockito.verify(map).setRasterParticleColor(Mockito.eq("particle"), particleColor.capture()); + Mockito.verify(map).setRasterParticleTrailColor(Mockito.eq("particle"), trailColor.capture()); + assertArrayEquals(new float[] {0.1f, 0.2f, 0.3f, 1.0f}, particleColor.getValue(), 0.0f); + assertArrayEquals(new float[] {0.9f, 0.8f, 0.7f, 1.0f}, trailColor.getValue(), 0.0f); + Mockito.verify(map, Mockito.never()).submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class)); + } + + @Test public void invalidIndependentColorIsRejectedByNativeContract() { + MapLibreMap map = Mockito.mock(MapLibreMap.class); + Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8); + Mockito.when(map.setRasterParticleColor(Mockito.anyString(), Mockito.any(float[].class))).thenReturn(false); + Mockito.when(map.setRasterParticleTrailColor(Mockito.anyString(), Mockito.any(float[].class))).thenReturn(false); + WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle")); + assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setParticleColor(Float.NaN, 0f, 0f, 1f)); + assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setTrailColor(0f, 0f, 0f, 2f)); + } } diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index 83423b1be..5590c27b1 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -364,7 +364,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { 2 -> when (value) { 0.5f -> runtime?.setPlaybackSpeed(WeatherRuntime.PlaybackSpeed.HALF); 1.0f -> runtime?.setPlaybackSpeed(WeatherRuntime.PlaybackSpeed.NORMAL); 2.0f -> runtime?.setPlaybackSpeed(WeatherRuntime.PlaybackSpeed.DOUBLE); else -> null } else -> null // Existing fixture-only diagnostic commands remain outside the REAL WEATHER Host facade. } - if (accepted != WeatherRuntime.CommandResult.ACCEPTED && (runtime == null || !maplibreMap.controlRasterArraySourceTimeline(PARTICLE_SOURCE_ID, command, value))) { + if (accepted != WeatherRuntime.CommandResult.ACCEPTED && !realWeatherMode && (runtime == null || !maplibreMap.controlRasterArraySourceTimeline(PARTICLE_SOURCE_ID, command, value))) { Log.i(TAG, "ParticleTimeline: event=REJECTED command=$command") } else { timelineState = when (command) { 0 -> "PLAY"; 1 -> "PAUSE"; 6 -> "WAIT"; 7 -> "INVALID"; 8 -> "PAUSE"; else -> timelineState } @@ -547,7 +547,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } } return runtimeResult == WeatherRuntime.CommandResult.ACCEPTED || - (runtimeResult == null && maplibreMap.setStyleLayerFloatProperty(PARTICLE_LAYER_ID, property, value)) + (!realWeatherMode && runtimeResult == null && maplibreMap.setStyleLayerFloatProperty(PARTICLE_LAYER_ID, property, value)) } private fun resetVisualDefaults() { @@ -615,16 +615,16 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { val metadata = assets.open(metadataPath).bufferedReader().use { it.readText() } val payload = assets.open(payloadPath).use { it.readBytes() } Log.i(TAG, "ParticleField: event=REAL_WEATHER_DATASET_LOAD index=$index metadata=$metadataPath payloadBytes=${payload.size}") - weatherRuntime?.submitWindField(metadata, payload)?.getCode() ?: MapLibreMap.HOST_FIELD_REJECTED_SOURCE_MODE + weatherRuntime?.submitWindField(metadata, payload) ?: WeatherRuntime.FieldSubmitResult.REJECTED_SOURCE_MODE } catch (error: Exception) { Log.e(TAG, "ParticleField: event=REAL_WEATHER_SUBMIT_REJECTED index=$index reason=ASSET_READ_FAILED", error) - MapLibreMap.HOST_FIELD_REJECTED_ADAPTER + WeatherRuntime.FieldSubmitResult.REJECTED_ADAPTER } } - timelineState = if (results.size == 4 && results.all { it > 0 }) "READY" else "NOT READY" + timelineState = if (results.size == 4 && results.all { it.accepted() }) "READY" else "NOT READY" fitRealWeatherBounds() status.text = statusText() - Log.i(TAG, "ParticleField: event=REAL_WEATHER_INITIAL_WINDOW_READY results=${results.joinToString(",")} timeline=$timelineState") + Log.i(TAG, "ParticleField: event=REAL_WEATHER_INITIAL_WINDOW_READY results=${results.joinToString(",") { it.name }} timeline=$timelineState") Log.i(TAG, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY source=$PARTICLE_SOURCE_ID scale=0..30mps") Log.i(TAG, "ParticleField: event=REAL_WEATHER_COMPOSITE_READY source=$PARTICLE_SOURCE_ID basemap=GSI failSoft=true color=weather-color particle=raster-particle") }