fix: share weather cadence across renderers

This commit is contained in:
OpenAI Codex
2026-09-07 15:16:11 +08:00
parent 8d12a4b361
commit ab86eb8f67
9 changed files with 124 additions and 57 deletions

View File

@@ -50,13 +50,10 @@ public final class WeatherRuntime {
private boolean particleVisible = true;
private PlaybackState playbackState = PlaybackState.PAUSE;
private TargetFps targetFps = TargetFps.AUTO;
private boolean disposed;
private Handler cadenceHandler;
private final Runnable cadenceWake = new Runnable() {
@Override public void run() {
if (!shouldWakeWeather()) return;
map.triggerRepaint();
scheduleCadenceWake();
}
@Override public void run() { runCadenceWake(); }
};
public WeatherRuntime(@NonNull MapLibreMap map, @NonNull Config config) {
@@ -64,10 +61,14 @@ public final class WeatherRuntime {
}
public State getState() { return state; }
public Config getConfig() { return config; }
public boolean isDisposed() { return disposed; }
/** Ends this map/style-scoped facade. It is safe to call repeatedly. */
public void dispose() { if (disposed) return; disposed = true; cancelCadenceWake(); }
/** Core-owned capacity; Host never sees the shader's UBO layout. */
public int getWeatherColorStopCapacity() { return weatherColorStopCapacity; }
public FieldSubmitResult submitWindField(@NonNull String metadataJson, @NonNull byte[] payloadBytes) {
requireAttached();
requireEnabled();
if (metadataJson.isEmpty() || payloadBytes.length == 0) return FieldSubmitResult.REJECTED_ADAPTER;
FieldSubmitResult result = FieldSubmitResult.fromCode(map.submitRasterArraySourceHostField(config.sourceId, metadataJson, payloadBytes));
@@ -75,8 +76,9 @@ public final class WeatherRuntime {
if (state == State.READY && result.accepted()) scheduleCadenceWake();
return result;
}
public CommandResult clearWindFields() { if (state == State.DISABLED) return CommandResult.REJECTED; boolean ok = map.clearRasterArraySourceHostFields(config.sourceId); state = stateAfterClear(state, ok); if (ok) cancelCadenceWake(); return result(ok); }
public CommandResult clearWindFields() { if (disposed || state == State.DISABLED) return CommandResult.REJECTED; boolean ok = map.clearRasterArraySourceHostFields(config.sourceId); state = stateAfterClear(state, ok); if (ok) cancelCadenceWake(); return result(ok); }
public CommandResult setWeatherEnabled(boolean enabled) {
if (disposed) return CommandResult.REJECTED;
if (!enabled) {
if (state == State.DISABLED) return CommandResult.ACCEPTED;
map.controlRasterArraySourceTimeline(config.sourceId, 1, 0f);
@@ -94,12 +96,12 @@ public final class WeatherRuntime {
state = stateAfterEnable(state, color && particle);
return result(color && particle);
}
public CommandResult setWeatherColorVisible(boolean visible) { weatherColorVisible = visible; CommandResult result = result(map.setStyleLayerVisibility(config.weatherColorLayerId, visible && state != State.DISABLED)); scheduleCadenceWake(); return result; }
public CommandResult setParticleVisible(boolean visible) { particleVisible = visible; CommandResult result = result(map.setStyleLayerVisibility(config.particleLayerId, visible && state != State.DISABLED)); scheduleCadenceWake(); return result; }
public CommandResult setWeatherColorVisible(boolean visible) { if (disposed) return CommandResult.REJECTED; weatherColorVisible = visible; CommandResult result = result(map.setStyleLayerVisibility(config.weatherColorLayerId, visible && state != State.DISABLED)); cancelCadenceWake(); scheduleCadenceWake(); return result; }
public CommandResult setParticleVisible(boolean visible) { if (disposed) return CommandResult.REJECTED; particleVisible = visible; CommandResult result = result(map.setStyleLayerVisibility(config.particleLayerId, visible && state != State.DISABLED)); cancelCadenceWake(); scheduleCadenceWake(); return result; }
public CommandResult setForecastTime(long timestampMs) { requireEnabled(); return result(timestampMs >= 0 && map.seekRasterArraySourceTimelineToTimestamp(config.sourceId, timestampMs)); }
public CommandResult setPlaybackState(@NonNull PlaybackState playback) { requireEnabled(); CommandResult result = result(map.controlRasterArraySourceTimeline(config.sourceId, playback == PlaybackState.PLAY ? 0 : 1, 0f)); if (result == CommandResult.ACCEPTED) { playbackState = playback; scheduleCadenceWake(); } return result; }
public CommandResult setPlaybackState(@NonNull PlaybackState playback) { requireEnabled(); CommandResult result = result(map.controlRasterArraySourceTimeline(config.sourceId, playback == PlaybackState.PLAY ? 0 : 1, 0f)); if (result == CommandResult.ACCEPTED) { playbackState = playback; cancelCadenceWake(); scheduleCadenceWake(); } return result; }
public CommandResult setPlaybackSpeed(@NonNull PlaybackSpeed speed) { requireEnabled(); return result(map.controlRasterArraySourceTimeline(config.sourceId, 2, speed.value)); }
public CommandResult setWeatherColorOpacity(float opacity) { return result(Float.isFinite(opacity) && map.setStyleLayerFloatProperty(config.weatherColorLayerId, "weather-color-opacity", opacity)); }
public CommandResult setWeatherColorOpacity(float opacity) { return disposed ? CommandResult.REJECTED : result(Float.isFinite(opacity) && map.setStyleLayerFloatProperty(config.weatherColorLayerId, "weather-color-opacity", opacity)); }
public CommandResult setParticleCount(int count) { return setParticleFloat("raster-particle-particle-count", count); }
public CommandResult setParticleLineWidth(float width) { return setParticleFloat("raster-particle-line-width", width); }
public CommandResult setParticleLength(float length) { return setParticleFloat("raster-particle-particle-length", length); }
@@ -109,17 +111,21 @@ public final class WeatherRuntime {
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) {
if (disposed) return CommandResult.REJECTED;
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) {
if (disposed) return CommandResult.REJECTED;
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) {
if (disposed) return CommandResult.REJECTED;
return result(map.setRasterParticleColors(config.particleLayerId, new float[]{red, green, blue, alpha}, new float[]{trailRed, trailGreen, trailBlue, trailAlpha}));
}
public CommandResult setTargetFps(@NonNull TargetFps target) {
if (disposed) return CommandResult.REJECTED;
if (!map.setRasterArraySourceTargetFps(config.sourceId, target.value)) return CommandResult.REJECTED;
targetFps = target;
cancelCadenceWake();
@@ -128,6 +134,7 @@ public final class WeatherRuntime {
}
public TargetFps getTargetFps() { return targetFps; }
public CommandResult setWeatherColorStops(@NonNull List<ColorStop> stops) {
if (disposed) return CommandResult.REJECTED;
if (weatherColorStopCapacity < 2 || stops.size() < 2 || stops.size() > weatherColorStopCapacity) return CommandResult.REJECTED;
float[] speeds = new float[stops.size()]; float[] colors = new float[stops.size() * 4];
for (int i = 0; i < stops.size(); i++) { ColorStop stop = stops.get(i); speeds[i] = stop.speedMps; colors[i * 4] = stop.red; colors[i * 4 + 1] = stop.green; colors[i * 4 + 2] = stop.blue; colors[i * 4 + 3] = stop.alpha; }
@@ -135,7 +142,7 @@ public final class WeatherRuntime {
}
private static CommandResult result(boolean accepted) { return accepted ? CommandResult.ACCEPTED : CommandResult.REJECTED; }
private boolean shouldWakeWeather() {
return targetFps != TargetFps.AUTO && state == State.READY && (particleVisible || (weatherColorVisible && playbackState == PlaybackState.PLAY));
return !disposed && targetFps != TargetFps.AUTO && state == State.READY && (particleVisible || (weatherColorVisible && playbackState == PlaybackState.PLAY));
}
private void scheduleCadenceWake() {
if (!shouldWakeWeather()) return;
@@ -148,7 +155,8 @@ public final class WeatherRuntime {
}
}
private void cancelCadenceWake() { if (cadenceHandler != null) cadenceHandler.removeCallbacks(cadenceWake); }
private CommandResult setParticleFloat(String property, float value) { return result(Float.isFinite(value) && map.setStyleLayerFloatProperty(config.particleLayerId, property, value)); }
void runCadenceWake() { if (!shouldWakeWeather()) return; map.triggerRepaint(); scheduleCadenceWake(); }
private CommandResult setParticleFloat(String property, float value) { return disposed ? CommandResult.REJECTED : result(Float.isFinite(value) && map.setStyleLayerFloatProperty(config.particleLayerId, property, value)); }
static State stateAfterSubmit(State current, FieldSubmitResult submit) {
if (submit == FieldSubmitResult.ACCEPTED_PENDING_INITIAL_WINDOW) return State.WAITING_FOR_FIELDS;
if (submit == FieldSubmitResult.INITIAL_WINDOW_READY || submit == FieldSubmitResult.ACCEPTED_NEXT_SNAPSHOT) return State.READY;
@@ -162,5 +170,6 @@ public final class WeatherRuntime {
if (current == State.DISABLED) return State.WAITING_FOR_FIELDS;
return current;
}
private void requireEnabled() { if (state == State.DISABLED) throw new IllegalStateException("WeatherRuntime is disabled; enable and submit fields first"); }
private void requireAttached() { if (disposed) throw new IllegalStateException("WeatherRuntime is disposed"); }
private void requireEnabled() { requireAttached(); if (state == State.DISABLED) throw new IllegalStateException("WeatherRuntime is disabled; enable and submit fields first"); }
}

View File

@@ -102,4 +102,30 @@ public class WeatherRuntimeStateTest {
assertEquals(WeatherRuntime.TargetFps.FPS_30, runtime.getTargetFps());
Mockito.verify(map, Mockito.never()).submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class));
}
@Test public void disposeIsIdempotentAndRejectsFurtherControls() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
runtime.dispose();
runtime.dispose();
assertEquals(true, runtime.isDisposed());
Mockito.clearInvocations(map);
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setTargetFps(WeatherRuntime.TargetFps.FPS_30));
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setWeatherColorVisible(false));
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setParticleVisible(false));
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setWeatherEnabled(false));
Mockito.verifyNoInteractions(map);
}
@Test public void disposedWakeCannotRepaintOrRescheduleOldMap() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
WeatherRuntime oldRuntime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
oldRuntime.dispose();
oldRuntime.runCadenceWake();
Mockito.verify(map, Mockito.never()).triggerRepaint();
WeatherRuntime replacement = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
assertEquals(false, replacement.isDisposed());
}
}

View File

@@ -256,6 +256,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private fun leaveRealWeatherMode() {
dismissRealWeatherTuning()
weatherRuntime?.dispose()
weatherRuntime = null
if (realWeatherMode) {
preRealWeatherVisuals?.let { visuals ->
particleCount = visuals.count; lineWidth = visuals.width; particleLength = visuals.length
@@ -438,6 +440,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") {
weatherRuntime?.dispose()
weatherRuntime = null
this.active = active
this.enabled = enabled
val state = statusText()
@@ -806,7 +810,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
override fun onResume() { super.onResume(); mapView.onResume() }
override fun onPause() { mapView.onPause(); super.onPause() }
override fun onStop() { mapView.onStop(); super.onStop() }
override fun onDestroy() { mapView.onDestroy(); super.onDestroy() }
override fun onDestroy() { weatherRuntime?.dispose(); weatherRuntime = null; mapView.onDestroy(); super.onDestroy() }
override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() }
override fun onSaveInstanceState(outState: Bundle) {