feat: add weather-local render cadence

This commit is contained in:
OpenAI Codex
2026-09-07 14:55:32 +08:00
parent 6d514e3aae
commit b9b3dd2253
13 changed files with 224 additions and 8 deletions

View File

@@ -1218,6 +1218,18 @@ jni::jboolean NativeMapView::setRasterParticleTrailColor(JNIEnv& env,
return true;
}
jni::jboolean NativeMapView::setRasterArraySourceTargetFps(JNIEnv& env,
const jni::String& sourceId,
const jni::jint targetFps) {
auto* source = dynamic_cast<mbgl::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env, sourceId)));
return source && source->setRuntimeTargetFps(static_cast<uint8_t>(targetFps));
}
jni::jint NativeMapView::getRasterArraySourceTargetFps(JNIEnv& env, const jni::String& sourceId) {
auto* source = dynamic_cast<mbgl::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env, sourceId)));
return source ? source->runtimeTargetFps() : 0;
}
jni::jboolean NativeMapView::setWeatherColorStops(JNIEnv& env,
const jni::String& layerId,
const jni::Array<jni::jfloat>& speeds,
@@ -1721,6 +1733,8 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
METHOD(&NativeMapView::setRasterParticleColors, "nativeSetRasterParticleColors"),
METHOD(&NativeMapView::setRasterParticleColor, "nativeSetRasterParticleColor"),
METHOD(&NativeMapView::setRasterParticleTrailColor, "nativeSetRasterParticleTrailColor"),
METHOD(&NativeMapView::setRasterArraySourceTargetFps, "nativeSetRasterArraySourceTargetFps"),
METHOD(&NativeMapView::getRasterArraySourceTargetFps, "nativeGetRasterArraySourceTargetFps"),
METHOD(&NativeMapView::setWeatherColorStops, "nativeSetWeatherColorStops"),
METHOD(&NativeMapView::getWeatherColorStopCapacity, "nativeGetWeatherColorStopCapacity"),
METHOD(&NativeMapView::advanceRasterArraySourceFieldWindow, "nativeAdvanceRasterArraySourceFieldWindow"),

View File

@@ -286,6 +286,8 @@ public:
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 setRasterArraySourceTargetFps(JNIEnv&, const jni::String&, jni::jint);
jni::jint getRasterArraySourceTargetFps(JNIEnv&, const jni::String&);
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

@@ -309,6 +309,15 @@ public final class MapLibreMap {
return nativeMapView.setRasterParticleTrailColor(layerId, trailColor);
}
/** Sets the source-local weather cadence (0=auto, 30 or 60 FPS). */
public boolean setRasterArraySourceTargetFps(String sourceId, int targetFps) {
return nativeMapView.setRasterArraySourceTargetFps(sourceId, targetFps);
}
public int getRasterArraySourceTargetFps(String sourceId) {
return nativeMapView.getRasterArraySourceTargetFps(sourceId);
}
/** 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

@@ -149,6 +149,10 @@ interface NativeMap {
boolean setRasterParticleTrailColor(String layerId, float[] trailColor);
boolean setRasterArraySourceTargetFps(String sourceId, int targetFps);
int getRasterArraySourceTargetFps(String sourceId);
boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops);
int getWeatherColorStopCapacity();

View File

@@ -972,6 +972,16 @@ final class NativeMapView implements NativeMap {
return !checkState("setRasterParticleTrailColor") && nativeSetRasterParticleTrailColor(layerId, trailColor);
}
@Override
public boolean setRasterArraySourceTargetFps(String sourceId, int targetFps) {
return !checkState("setRasterArraySourceTargetFps") && nativeSetRasterArraySourceTargetFps(sourceId, targetFps);
}
@Override
public int getRasterArraySourceTargetFps(String sourceId) {
return checkState("getRasterArraySourceTargetFps") ? 0 : nativeGetRasterArraySourceTargetFps(sourceId);
}
public boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops) {
return !checkState("setWeatherColorStops") && nativeSetWeatherColorStops(layerId, speedsMps, rgbaStops);
}
@@ -1755,6 +1765,8 @@ final class NativeMapView implements NativeMap {
@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 nativeSetRasterArraySourceTargetFps(String sourceId, int targetFps);
@Keep private native int nativeGetRasterArraySourceTargetFps(String sourceId);
@Keep private native boolean nativeSetWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops);
@Keep private native int nativeGetWeatherColorStopCapacity();

View File

@@ -2,6 +2,9 @@ package org.maplibre.android.maps;
import androidx.annotation.NonNull;
import android.os.Handler;
import android.os.Looper;
import java.util.List;
/**
@@ -12,6 +15,7 @@ public final class WeatherRuntime {
public enum State { DISABLED, WAITING_FOR_FIELDS, READY, ERROR }
public enum PlaybackState { PLAY, PAUSE }
public enum PlaybackSpeed { HALF(0.5f), NORMAL(1.0f), DOUBLE(2.0f); final float value; PlaybackSpeed(float value) { this.value = value; } }
public enum TargetFps { AUTO(0), FPS_30(30), FPS_60(60); final int value; TargetFps(int value) { this.value = value; } }
public enum FieldSubmitResult {
REJECTED_SOURCE_MODE(-1), REJECTED_ADAPTER(-2), REJECTED_INCOMPATIBLE(-3), REJECTED_NON_MONOTONIC(-4), REJECTED_CAPACITY(-5),
ACCEPTED_PENDING_INITIAL_WINDOW(1), INITIAL_WINDOW_READY(2), ACCEPTED_NEXT_SNAPSHOT(3), DUPLICATE(4), UNKNOWN(Integer.MIN_VALUE);
@@ -44,6 +48,16 @@ public final class WeatherRuntime {
private State state = State.WAITING_FOR_FIELDS;
private boolean weatherColorVisible = true;
private boolean particleVisible = true;
private PlaybackState playbackState = PlaybackState.PAUSE;
private TargetFps targetFps = TargetFps.AUTO;
private Handler cadenceHandler;
private final Runnable cadenceWake = new Runnable() {
@Override public void run() {
if (!shouldWakeWeather()) return;
map.triggerRepaint();
scheduleCadenceWake();
}
};
public WeatherRuntime(@NonNull MapLibreMap map, @NonNull Config config) {
this.map = map; this.config = config; this.weatherColorStopCapacity = map.getWeatherColorStopCapacity();
@@ -58,9 +72,10 @@ public final class WeatherRuntime {
if (metadataJson.isEmpty() || payloadBytes.length == 0) return FieldSubmitResult.REJECTED_ADAPTER;
FieldSubmitResult result = FieldSubmitResult.fromCode(map.submitRasterArraySourceHostField(config.sourceId, metadataJson, payloadBytes));
state = stateAfterSubmit(state, result);
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); return result(ok); }
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 setWeatherEnabled(boolean enabled) {
if (!enabled) {
if (state == State.DISABLED) return CommandResult.ACCEPTED;
@@ -69,6 +84,7 @@ public final class WeatherRuntime {
boolean particle = map.setStyleLayerVisibility(config.particleLayerId, false);
boolean clear = map.clearRasterArraySourceHostFields(config.sourceId);
state = stateAfterDisable(color && particle && clear);
if (state != State.READY) cancelCadenceWake();
return result(color && particle && clear);
}
if (state == State.ERROR) return CommandResult.REJECTED;
@@ -78,10 +94,10 @@ public final class WeatherRuntime {
state = stateAfterEnable(state, color && particle);
return result(color && particle);
}
public CommandResult setWeatherColorVisible(boolean visible) { weatherColorVisible = visible; return result(map.setStyleLayerVisibility(config.weatherColorLayerId, visible && state != State.DISABLED)); }
public CommandResult setParticleVisible(boolean visible) { particleVisible = visible; return result(map.setStyleLayerVisibility(config.particleLayerId, visible && state != State.DISABLED)); }
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 setForecastTime(long timestampMs) { requireEnabled(); return result(timestampMs >= 0 && map.seekRasterArraySourceTimelineToTimestamp(config.sourceId, timestampMs)); }
public CommandResult setPlaybackState(@NonNull PlaybackState playback) { requireEnabled(); return result(map.controlRasterArraySourceTimeline(config.sourceId, playback == PlaybackState.PLAY ? 0 : 1, 0f)); }
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 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 setParticleCount(int count) { return setParticleFloat("raster-particle-particle-count", count); }
@@ -103,6 +119,14 @@ public final class WeatherRuntime {
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}));
}
public CommandResult setTargetFps(@NonNull TargetFps target) {
if (!map.setRasterArraySourceTargetFps(config.sourceId, target.value)) return CommandResult.REJECTED;
targetFps = target;
cancelCadenceWake();
scheduleCadenceWake();
return CommandResult.ACCEPTED;
}
public TargetFps getTargetFps() { return targetFps; }
public CommandResult setWeatherColorStops(@NonNull List<ColorStop> stops) {
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];
@@ -110,6 +134,20 @@ public final class WeatherRuntime {
return result(map.setWeatherColorStops(config.weatherColorLayerId, speeds, colors));
}
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));
}
private void scheduleCadenceWake() {
if (!shouldWakeWeather()) return;
try {
if (cadenceHandler == null) cadenceHandler = new Handler(Looper.getMainLooper());
cadenceHandler.removeCallbacks(cadenceWake);
cadenceHandler.postDelayed(cadenceWake, targetFps == TargetFps.FPS_30 ? 34L : 17L);
} catch (RuntimeException ignored) {
// Local JVM tests may not provide an Android Looper; Core cadence remains authoritative.
}
}
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)); }
static State stateAfterSubmit(State current, FieldSubmitResult submit) {
if (submit == FieldSubmitResult.ACCEPTED_PENDING_INITIAL_WINDOW) return State.WAITING_FOR_FIELDS;

View File

@@ -92,4 +92,14 @@ public class WeatherRuntimeStateTest {
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setParticleColor(Float.NaN, 0f, 0f, 1f));
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setTrailColor(0f, 0f, 0f, 2f));
}
@Test public void targetFpsIsTypedAndDoesNotSubmitFields() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
Mockito.when(map.setRasterArraySourceTargetFps("source", 30)).thenReturn(true);
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setTargetFps(WeatherRuntime.TargetFps.FPS_30));
assertEquals(WeatherRuntime.TargetFps.FPS_30, runtime.getTargetFps());
Mockito.verify(map, Mockito.never()).submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class));
}
}