feat: add v2 multi-plane field ingress

This commit is contained in:
OpenAI Codex
2026-09-11 09:00:29 +08:00
parent 9e9ea07bc7
commit 148bf80fe4
17 changed files with 368 additions and 20 deletions

View File

@@ -1124,10 +1124,21 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env,
auto* layer = map->getStyle().getLayer(jni::Make<std::string>(env, layerId));
if (!layer) return false;
const auto property = jni::Make<std::string>(env, propertyName);
if (property == "weather-color-field-value-source") {
auto* weatherColorLayer = dynamic_cast<mbgl::style::WeatherColorLayer*>(layer);
if (!weatherColorLayer || (value != 0.0f && value != 1.0f)) {
mbgl::Log::Error(mbgl::Event::JNI, "ParticleField: event=FIELD_VALUE_SOURCE_SET result=REJECTED");
return false;
}
weatherColorLayer->setFieldValueSource(value == 0.0f ? mbgl::style::FieldValueSource::Scalar
: mbgl::style::FieldValueSource::VectorMagnitude);
return true;
}
if (property != "raster-particle-time-mix" && property != "raster-particle-particle-count" &&
property != "raster-particle-line-width" && property != "raster-particle-particle-length" &&
property != "raster-particle-particle-opacity" && property != "raster-particle-trail-opacity" &&
property != "raster-particle-trail-fade" && property != "raster-particle-speed-color-enabled") {
property != "raster-particle-trail-fade" && property != "raster-particle-speed-color-enabled" &&
property != "raster-particle-advection-scale") {
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_PROPERTY");
return false;
}
@@ -1163,6 +1174,9 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env,
particleLayer->setTrailOpacity(value);
} else if (property == "raster-particle-trail-fade") {
particleLayer->setTrailFade(value);
} else if (property == "raster-particle-advection-scale") {
if (value <= 0.0f) return false;
particleLayer->setAdvectionScale(value);
} else {
if (value != 0.0f && value != 1.0f) {
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_COLOR_MODE");
@@ -1327,6 +1341,51 @@ jni::jint NativeMapView::submitRasterArraySourceHostField(JNIEnv& env,
return static_cast<jni::jint>(result);
}
jni::jint NativeMapView::submitRasterArraySourceHostFieldV2(JNIEnv& env,
const jni::String& sourceId,
const jni::String& metadata,
const jni::Array<jbyte>& packedPlanes,
const jni::jboolean scalarPresent,
const jni::jint scalarLength,
const jni::jboolean vectorPresent,
const jni::jint vectorLength,
const jni::jint validityLength) {
const auto id = jni::Make<std::string>(env, sourceId);
auto* source = dynamic_cast<mbgl::style::RasterArraySource*>(map->getStyle().getSource(id));
if (!source) return static_cast<jni::jint>(mbgl::style::RasterArrayHostFieldSubmitResult::RejectedSourceMode);
NullCheck(env, &packedPlanes);
if (scalarLength < 0 || vectorLength < 0 || validityLength <= 0 || (!scalarPresent && scalarLength != 0) ||
(!vectorPresent && vectorLength != 0) || (scalarPresent && scalarLength == 0) ||
(vectorPresent && vectorLength == 0) || (!scalarPresent && !vectorPresent)) {
return static_cast<jni::jint>(mbgl::style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
}
const uint64_t total = static_cast<uint64_t>(scalarLength) + static_cast<uint64_t>(vectorLength) +
static_cast<uint64_t>(validityLength);
if (total != packedPlanes.Length(env)) {
return static_cast<jni::jint>(mbgl::style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
}
std::vector<char> packed(total);
if (!packed.empty()) jni::GetArrayRegion(env, *packedPlanes, 0, packed.size(), reinterpret_cast<jbyte*>(packed.data()));
size_t offset = 0;
const auto take = [&packed, &offset](const size_t length) {
std::vector<char> result(packed.begin() + static_cast<std::ptrdiff_t>(offset),
packed.begin() + static_cast<std::ptrdiff_t>(offset + length));
offset += length;
return result;
};
mbgl::particle::ParticleFieldPayloadBundle payloads{
.scalar = scalarPresent ? std::optional<std::vector<char>>(take(static_cast<size_t>(scalarLength))) : std::nullopt,
.vector = vectorPresent ? std::optional<std::vector<char>>(take(static_cast<size_t>(vectorLength))) : std::nullopt,
.validity = take(static_cast<size_t>(validityLength)),
};
const auto result = source->submitHostField(jni::Make<std::string>(env, metadata), payloads);
mbgl::Log::Info(mbgl::Event::JNI,
"ParticleField: event=HOST_FIELD_V2_SUBMIT_BRIDGE sourceId=" + id + " scalar=" +
(scalarPresent ? "present" : "absent") + " vector=" + (vectorPresent ? "present" : "absent") +
" result=" + mbgl::style::rasterArrayHostFieldSubmitResultName(result));
return static_cast<jni::jint>(result);
}
jni::jboolean NativeMapView::clearRasterArraySourceHostFields(JNIEnv& env, const jni::String& sourceId) {
const auto id = jni::Make<std::string>(env, sourceId);
auto* source = dynamic_cast<mbgl::style::RasterArraySource*>(map->getStyle().getSource(id));
@@ -1761,6 +1820,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
METHOD(&NativeMapView::advanceRasterArraySourceFieldWindow, "nativeAdvanceRasterArraySourceFieldWindow"),
METHOD(&NativeMapView::repeatRasterArraySourceFieldWindow, "nativeRepeatRasterArraySourceFieldWindow"),
METHOD(&NativeMapView::submitRasterArraySourceHostField, "nativeSubmitRasterArraySourceHostField"),
METHOD(&NativeMapView::submitRasterArraySourceHostFieldV2, "nativeSubmitRasterArraySourceHostFieldV2"),
METHOD(&NativeMapView::clearRasterArraySourceHostFields, "nativeClearRasterArraySourceHostFields"),
METHOD(&NativeMapView::seekRasterArraySourceTimelineToTimestamp, "nativeSeekRasterArraySourceTimelineToTimestamp"),
METHOD(&NativeMapView::controlRasterArraySourceTimeline, "nativeControlRasterArraySourceTimeline"),

View File

@@ -298,6 +298,15 @@ public:
const jni::String&,
const jni::String&,
const jni::Array<jbyte>&);
jni::jint submitRasterArraySourceHostFieldV2(JNIEnv&,
const jni::String&,
const jni::String&,
const jni::Array<jbyte>&,
jni::jboolean,
jni::jint,
jni::jboolean,
jni::jint,
jni::jint);
jni::jboolean clearRasterArraySourceHostFields(JNIEnv&, const jni::String&);
jni::jboolean seekRasterArraySourceTimelineToTimestamp(JNIEnv&, const jni::String&, jni::jlong);
jni::jboolean controlRasterArraySourceTimeline(JNIEnv&, const jni::String&, jni::jint, jni::jfloat);

View File

@@ -402,6 +402,14 @@ public final class MapLibreMap {
return nativeMapView.submitRasterArraySourceHostField(sourceId, metadata, payload);
}
/** Explicit v2 ingress: packed bytes are split by presence flags and lengths in JNI, never in JS. */
public int submitRasterArraySourceHostFieldV2(String sourceId, String metadata, byte[] packedPlanes,
boolean scalarPresent, int scalarLength,
boolean vectorPresent, int vectorLength, int validityLength) {
return nativeMapView.submitRasterArraySourceHostFieldV2(sourceId, metadata, packedPlanes, scalarPresent,
scalarLength, vectorPresent, vectorLength, validityLength);
}
/** Clears Host-owned canonical fields while retaining the raster-array Source and Style layers. */
public boolean clearRasterArraySourceHostFields(String sourceId) {
return nativeMapView.clearRasterArraySourceHostFields(sourceId);

View File

@@ -167,6 +167,10 @@ interface NativeMap {
int submitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload);
int submitRasterArraySourceHostFieldV2(String sourceId, String metadata, byte[] packedPlanes,
boolean scalarPresent, int scalarLength,
boolean vectorPresent, int vectorLength, int validityLength);
boolean clearRasterArraySourceHostFields(String sourceId);
boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp);

View File

@@ -1019,6 +1019,16 @@ final class NativeMapView implements NativeMap {
: nativeSubmitRasterArraySourceHostField(sourceId, metadata, payload);
}
@Override
public int submitRasterArraySourceHostFieldV2(String sourceId, String metadata, byte[] packedPlanes,
boolean scalarPresent, int scalarLength,
boolean vectorPresent, int vectorLength, int validityLength) {
return checkState("submitRasterArraySourceHostFieldV2")
? MapLibreMap.HOST_FIELD_REJECTED_SOURCE_MODE
: nativeSubmitRasterArraySourceHostFieldV2(sourceId, metadata, packedPlanes, scalarPresent, scalarLength,
vectorPresent, vectorLength, validityLength);
}
@Override
public boolean clearRasterArraySourceHostFields(String sourceId) {
return !checkState("clearRasterArraySourceHostFields")
@@ -1799,6 +1809,11 @@ final class NativeMapView implements NativeMap {
@Keep
private native int nativeSubmitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload);
@Keep
private native int nativeSubmitRasterArraySourceHostFieldV2(String sourceId, String metadata, byte[] packedPlanes,
boolean scalarPresent, int scalarLength,
boolean vectorPresent, int vectorLength, int validityLength);
@Keep
private native boolean nativeClearRasterArraySourceHostFields(String sourceId);

View File

@@ -1,6 +1,7 @@
package org.maplibre.android.maps;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.os.Handler;
import android.os.Looper;
@@ -12,6 +13,7 @@ import java.util.List;
* It owns no provider, network, route, ETA, Snapshot, T0/T1, or GPU state.
*/
public final class WeatherRuntime {
private static final long MAX_FIELD_PAYLOAD_BYTES = 64L * 1024L * 1024L;
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; } }
@@ -76,7 +78,27 @@ public final class WeatherRuntime {
if (state == State.READY && result.accepted()) scheduleCadenceWake();
return result;
}
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); }
/** v2 provider-neutral ingress; missing scalar/vector is expressed only by its explicit presence flag. */
public FieldSubmitResult submitField(@NonNull String metadataJson, @Nullable byte[] scalarBytes, @Nullable byte[] vectorBytes, @NonNull byte[] validityBytes) {
requireAttached(); requireEnabled();
if (metadataJson.isEmpty() || validityBytes.length == 0 || (scalarBytes == null && vectorBytes == null)) return FieldSubmitResult.REJECTED_ADAPTER;
final int scalarLength = scalarBytes == null ? 0 : scalarBytes.length;
final int vectorLength = vectorBytes == null ? 0 : vectorBytes.length;
if ((scalarBytes != null && scalarLength == 0) || (vectorBytes != null && vectorLength == 0)) return FieldSubmitResult.REJECTED_ADAPTER;
final long total = (long) scalarLength + vectorLength + validityBytes.length;
// Do not rely on Kotlin's file-reference guard: Java callers can bypass it.
if (total > MAX_FIELD_PAYLOAD_BYTES || total > Integer.MAX_VALUE) return FieldSubmitResult.REJECTED_ADAPTER;
final byte[] packed = new byte[(int) total]; int offset = 0;
if (scalarBytes != null) { System.arraycopy(scalarBytes, 0, packed, offset, scalarLength); offset += scalarLength; }
if (vectorBytes != null) { System.arraycopy(vectorBytes, 0, packed, offset, vectorLength); offset += vectorLength; }
System.arraycopy(validityBytes, 0, packed, offset, validityBytes.length);
FieldSubmitResult result = FieldSubmitResult.fromCode(map.submitRasterArraySourceHostFieldV2(config.sourceId, metadataJson,
packed, scalarBytes != null, scalarLength, vectorBytes != null, vectorLength, validityBytes.length));
state = stateAfterSubmit(state, result); if (state == State.READY && result.accepted()) scheduleCadenceWake(); return result;
}
public CommandResult clearFields() { if (disposed || state == State.DISABLED) return CommandResult.REJECTED; boolean ok = map.clearRasterArraySourceHostFields(config.sourceId); state = stateAfterClear(state, ok); if (ok) cancelCadenceWake(); return result(ok); }
/** @deprecated v1 compatibility alias. */
public CommandResult clearWindFields() { return clearFields(); }
public CommandResult setWeatherEnabled(boolean enabled) {
if (disposed) return CommandResult.REJECTED;
if (!enabled) {
@@ -108,6 +130,11 @@ public final class WeatherRuntime {
public CommandResult setParticleOpacity(float opacity) { return setParticleFloat("raster-particle-particle-opacity", opacity); }
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 setParticleAdvectionScale(float scale) { return disposed || !Float.isFinite(scale) || scale <= 0f ? CommandResult.REJECTED : setParticleFloat("raster-particle-advection-scale", scale); }
public CommandResult setFieldValueSource(@NonNull FieldValueSource source) {
if (disposed) return CommandResult.REJECTED;
return result(map.setStyleLayerFloatProperty(config.weatherColorLayerId, "weather-color-field-value-source", source.code));
}
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) {
@@ -143,6 +170,7 @@ public final class WeatherRuntime {
return result(map.setWeatherColorStops(config.weatherColorLayerId, speeds, colors));
}
private static CommandResult result(boolean accepted) { return accepted ? CommandResult.ACCEPTED : CommandResult.REJECTED; }
public enum FieldValueSource { SCALAR(0f), VECTOR_MAGNITUDE(1f); final float code; FieldValueSource(float code) { this.code = code; } }
private boolean shouldWakeWeather() {
return !disposed && targetFps != TargetFps.AUTO && state == State.READY && (particleVisible || (weatherColorVisible && playbackState == PlaybackState.PLAY));
}