fix: finalize WeatherRuntime host API boundary

This commit is contained in:
OpenAI Codex
2026-09-07 14:10:06 +08:00
parent 01295df4b8
commit 544e4aee3a
8 changed files with 94 additions and 6 deletions

View File

@@ -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<jni::jfloat>& particle) {
auto* layer = dynamic_cast<mbgl::style::RasterParticleLayer*>(map->getStyle().getLayer(jni::Make<std::string>(env, layerId)));
if (!layer || particle.Length(env) != 4) return false;
std::array<float, 4> 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<jni::jfloat>& trail) {
auto* layer = dynamic_cast<mbgl::style::RasterParticleLayer*>(map->getStyle().getLayer(jni::Make<std::string>(env, layerId)));
if (!layer || trail.Length(env) != 4) return false;
std::array<float, 4> 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<jni::jfloat>& 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"),

View File

@@ -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<jni::jfloat>&, const jni::Array<jni::jfloat>&);
jni::jboolean setRasterParticleColor(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&);
jni::jboolean setRasterParticleTrailColor(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&);
jni::jboolean setWeatherColorStops(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&, const jni::Array<jni::jfloat>&);
jni::jint getWeatherColorStopCapacity(JNIEnv&);
jni::jboolean advanceRasterArraySourceFieldWindow(JNIEnv&, const jni::String&);

View File

@@ -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);

View File

@@ -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();

View File

@@ -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();

View File

@@ -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}));
}

View File

@@ -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<float[]> particleColor = ArgumentCaptor.forClass(float[].class);
ArgumentCaptor<float[]> 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));
}
}

View File

@@ -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")
}