feat: add v2 multi-plane field ingress
This commit is contained in:
@@ -175,6 +175,9 @@ public:
|
||||
// update API. This must return Noop without changing the Source window.
|
||||
RasterArrayRuntimeFieldUpdateResult repeatRuntimeFieldWindow();
|
||||
RasterArrayHostFieldSubmitResult submitHostField(const std::string& metadataText, const std::vector<char>& payload);
|
||||
// v2 ingress preserves explicit scalar/vector/validity plane presence.
|
||||
RasterArrayHostFieldSubmitResult submitHostField(const std::string& metadataText,
|
||||
const particle::ParticleFieldPayloadBundle& payloads);
|
||||
// Clears producer-owned Host ingress state while retaining the Source and
|
||||
// its Style layers for a subsequent T0/T1 submission.
|
||||
bool clearHostFields();
|
||||
@@ -191,6 +194,7 @@ protected:
|
||||
Mutable<Source::Impl> createMutable() const noexcept override;
|
||||
|
||||
private:
|
||||
RasterArrayHostFieldSubmitResult submitDecodedHostField(particle::ParticleFieldSnapshot snapshot);
|
||||
struct RuntimeFieldCandidate {
|
||||
RasterArrayRuntimeFieldDescriptor descriptor;
|
||||
std::unique_ptr<AsyncRequest> metadataRequest;
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 变更记录
|
||||
|
||||
## 13.4.1-particle.3
|
||||
|
||||
- `FieldRuntime.submitField` 升级为 `maplibre-particle-field/v2` 的明确 `FieldPayloadRefs`:shared validity 加可选 scalar/vector 本地文件,不再接受模糊单 payload。
|
||||
- 新增 `setFieldValueSource` 与 `setParticleAdvectionScale`,仅桥接既有 renderer 配置,不重传 field 或改变 Timeline。
|
||||
- 保留 `submitWindField` native/internal v1 ABI;公共兼容入口改名为 `submitLegacyVectorField`,避免与 v2 混淆。
|
||||
- Android bridge 在读取前校验全部 local file、非空 plane 与合计 64 MiB;Java/JNI 用有 presence flags 和长度的 v2 packed container 恢复 Core `ParticleFieldPayloadBundle`。
|
||||
- Android artifact 未构建、未发布;Android Gradle/AAR/APK/ADB 与 Jest 均未执行。
|
||||
|
||||
## 13.4.1-particle.2
|
||||
|
||||
- 公共 RN/TS API 重整为纯 `FieldRuntime` Field Renderer 接口。
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# @navsea/maplibre-particle-rn
|
||||
|
||||
`13.4.1-particle.2` 提供 React Native 的纯 Field Renderer 接口。它只接收宿主已经准备好的 canonical field 元数据与本地二进制文件,并控制既有 Native/Core 渲染器;不提供地图组件、底图、数据集、provider、HTTP、缓存或业务策略。
|
||||
`13.4.1-particle.3` 提供 React Native 的纯 Field Renderer 接口。它只接收宿主已经准备好的 canonical field 元数据与本地二进制文件,并控制既有 Native/Core 渲染器;不提供地图组件、底图、数据集、provider、HTTP、缓存或业务策略。
|
||||
|
||||
Map 与 Camera 必须由 Consumer 直接从 `@maplibre/maplibre-react-native@11.3.6` 导入。本包将该库声明为 peer dependency,不再转导出 MapLibre React Native。
|
||||
|
||||
## 版本关系
|
||||
|
||||
- RN/TS 包:`13.4.1-particle.2`
|
||||
- RN/TS 包:`13.4.1-particle.3`
|
||||
- Android Native artifact:`cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`
|
||||
|
||||
本次仅整理 RN/TS 公共接口。Native artifact 未构建、未修改、未发布;Expo plugin 继续解析上述冻结 artifact。
|
||||
@@ -33,7 +33,11 @@ const mapStyle = JSON.stringify({
|
||||
|
||||
```ts
|
||||
await FieldRuntime.clearFields(reactTag);
|
||||
await FieldRuntime.submitField(reactTag, metadataJson, localFileUri);
|
||||
await FieldRuntime.submitField(reactTag, metadataJson, {
|
||||
validityFileUri: "file:///absolute/path/validity.u8",
|
||||
scalarFileUri: "file:///absolute/path/scalar.f32", // 可选
|
||||
vectorFileUri: "file:///absolute/path/vector-uv.f32", // 可选;至少 scalar/vector 之一
|
||||
});
|
||||
await FieldRuntime.setFieldColorStops(reactTag, [
|
||||
{ value: 0, red: 0.08, green: 0.2, blue: 0.8, alpha: 0.72 },
|
||||
{ value: 30, red: 0.9, green: 0.08, blue: 0.08, alpha: 0.72 },
|
||||
@@ -47,19 +51,24 @@ await FieldRuntime.setFieldColorStops(reactTag, [
|
||||
全部方法均以 `reactTag` 为首参并返回既有桥接命令结果:
|
||||
|
||||
- 生命周期:`attach`、`detach`、`getState`
|
||||
- 数据:`clearFields`、`submitField`
|
||||
- 数据:`clearFields`、`submitField`(v2 `FieldPayloadRefs`)
|
||||
- renderer 配置:`setFieldValueSource("SCALAR" | "VECTOR_MAGNITUDE")`、`setParticleAdvectionScale(scale)`
|
||||
- 显示与播放:`setEnabled`、`setFieldVisible`、`setParticleVisible`、`setTargetFps`、`setTargetTime`、`setPlaybackState`、`setPlaybackSpeed`
|
||||
- 色彩:`setFieldColorStops`、`setParticleColor`、`setTrailColor`
|
||||
- 粒子视觉参数:`setParticleCount`、`setParticleLineWidth`、`setParticleLength`、`setParticleOpacity`、`setTrailOpacity`、`setTrailFade`、`setFieldOpacity`
|
||||
|
||||
`FieldColorStop` 使用无业务含义的 `value` 字段;色阶继续遵循既有 Native 契约,至少提供 2 个且 `value` 严格递增。包在非原生环境可安全导入;实际调用 `FieldRuntime` 而 Native bridge 不存在时会给出稳定、明确的错误。
|
||||
|
||||
`submitField` 的 v2 `payloadRefs` 必须有 `validityFileUri`,并且必须有 `scalarFileUri` 或 `vectorFileUri`。每个引用只能是 `file://` URI 或绝对本地路径;桥接层会在读入前检查全部文件、非空性和合计 64 MiB 上限。它不接受 JS number array、Base64 或大 JSON payload。Core 继续按 metadata 精确校验各 plane 的长度、encoding、layout 和 validity。
|
||||
|
||||
`.2` 的 `submitField(reactTag, metadataJson, payloadFileUri)` 是单一 v1 vector payload。`.3` 不再以该模糊形状作为主入口;确有旧 native seam 的 Consumer 可显式调用 `submitLegacyVectorField`,它仅转发冻结的 v1 `submitWindField` ABI。新代码应使用 v2 `submitField`。
|
||||
|
||||
## 安装与发布
|
||||
|
||||
正式包仍以 HTTPS tarball 交付:
|
||||
|
||||
```text
|
||||
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.2.tgz
|
||||
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.3.tgz
|
||||
```
|
||||
|
||||
Consumer 只依赖发布 tarball,不依赖本仓库源码路径。发布前通过 `npm pack` 检查 tarball 内容;本包不包含 AAR、APK、fixture 或凭证。
|
||||
|
||||
@@ -8,7 +8,6 @@ import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.turbomodule.core.interfaces.TurboModule
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.uimanager.IllegalViewOperationException
|
||||
import com.facebook.react.uimanager.UIManagerHelper
|
||||
import com.facebook.react.uimanager.common.UIManagerType
|
||||
@@ -64,6 +63,7 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
|
||||
|
||||
@ReactMethod override fun getState(reactTag: Double, promise: Promise) = withRuntime(reactTag, promise) { runtime -> promise.resolve(snapshot(runtime)) }
|
||||
@ReactMethod override fun clearWindFields(reactTag: Double, promise: Promise) = command(reactTag, promise) { it.clearWindFields() }
|
||||
@ReactMethod override fun clearFields(reactTag: Double, promise: Promise) = command(reactTag, promise) { it.clearFields() }
|
||||
|
||||
/** Binary ingress is a local file reference, never a JS number array or encoded string. */
|
||||
@ReactMethod
|
||||
@@ -79,6 +79,30 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
|
||||
catch (error: IllegalStateException) { reject(promise, "NATIVE_REJECTED", error.message ?: "WeatherRuntime rejected the field") }
|
||||
}
|
||||
}
|
||||
|
||||
/** v2 reads each declared local plane only after every reference and aggregate size is checked. */
|
||||
@ReactMethod
|
||||
override fun submitField(
|
||||
reactTag: Double,
|
||||
metadataJson: String,
|
||||
validityFileUri: String,
|
||||
scalarFileUri: String,
|
||||
vectorFileUri: String,
|
||||
promise: Promise,
|
||||
) {
|
||||
if (metadataJson.isEmpty() || metadataJson.toByteArray(Charsets.UTF_8).size > MAX_METADATA_UTF8_BYTES) {
|
||||
return reject(promise, "INVALID_ARGUMENT", "metadataJson UTF-8 payload must be non-empty and at most $MAX_METADATA_UTF8_BYTES bytes")
|
||||
}
|
||||
val refs = try { resolvePayloadRefs(validityFileUri, scalarFileUri, vectorFileUri) }
|
||||
catch (error: IllegalArgumentException) { return reject(promise, "PAYLOAD_FILE_UNREADABLE", error.message ?: "payload references are invalid") }
|
||||
val planes = try { readPayloadRefs(refs) }
|
||||
catch (error: IOException) { return reject(promise, "PAYLOAD_FILE_UNREADABLE", "payload file could not be read: ${error.message ?: "I/O error"}") }
|
||||
catch (error: RuntimeException) { return reject(promise, "PAYLOAD_FILE_UNREADABLE", "payload file could not be read") }
|
||||
withRuntime(reactTag, promise) { runtime ->
|
||||
try { promise.resolve(runtime.submitField(metadataJson, planes.scalar, planes.vector, planes.validity).name) }
|
||||
catch (error: IllegalStateException) { reject(promise, "NATIVE_REJECTED", error.message ?: "WeatherRuntime rejected the field") }
|
||||
}
|
||||
}
|
||||
@ReactMethod override fun setWeatherEnabled(reactTag: Double, enabled: Boolean, promise: Promise) = command(reactTag, promise) { it.setWeatherEnabled(enabled) }
|
||||
@ReactMethod override fun setWeatherColorVisible(reactTag: Double, visible: Boolean, promise: Promise) = command(reactTag, promise) { it.setWeatherColorVisible(visible) }
|
||||
@ReactMethod override fun setParticleVisible(reactTag: Double, visible: Boolean, promise: Promise) = command(reactTag, promise) { it.setParticleVisible(visible) }
|
||||
@@ -103,6 +127,14 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
|
||||
@ReactMethod override fun setParticleOpacity(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setParticleOpacity(valid) }
|
||||
@ReactMethod override fun setTrailOpacity(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setTrailOpacity(valid) }
|
||||
@ReactMethod override fun setTrailFade(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setTrailFade(valid) }
|
||||
@ReactMethod
|
||||
override fun setParticleAdvectionScale(reactTag: Double, scale: Double, promise: Promise) {
|
||||
if (!scale.isFinite() || scale <= 0.0) return reject(promise, "INVALID_ARGUMENT", "advectionScale must be finite and > 0")
|
||||
command(reactTag, promise) { it.setParticleAdvectionScale(scale.toFloat()) }
|
||||
}
|
||||
@ReactMethod
|
||||
override fun setFieldValueSource(reactTag: Double, source: String, promise: Promise) =
|
||||
enumCommand(reactTag, promise, source, WeatherRuntime.FieldValueSource.values()) { runtime, value -> runtime.setFieldValueSource(value) }
|
||||
@ReactMethod override fun setWeatherColorOpacity(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setWeatherColorOpacity(valid) }
|
||||
@ReactMethod override fun setParticleColor(reactTag: Double, red: Double, green: Double, blue: Double, alpha: Double, promise: Promise) = colorCommand(reactTag, red, green, blue, alpha, promise) { runtime, r, g, b, a -> runtime.setParticleColor(r, g, b, a) }
|
||||
@ReactMethod override fun setTrailColor(reactTag: Double, red: Double, green: Double, blue: Double, alpha: Double, promise: Promise) = colorCommand(reactTag, red, green, blue, alpha, promise) { runtime, r, g, b, a -> runtime.setTrailColor(r, g, b, a) }
|
||||
@@ -145,6 +177,10 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
|
||||
command(reactTag, promise) { fn(it, value) }
|
||||
}
|
||||
private fun readLocalPayload(reference: String): ByteArray {
|
||||
val file = resolveLocalFile(reference)
|
||||
return file.inputStream().use { readBounded(it, file.length()) }
|
||||
}
|
||||
private fun resolveLocalFile(reference: String): File {
|
||||
if (reference.isBlank()) throw IllegalArgumentException("payload file reference is empty")
|
||||
val uri = Uri.parse(reference)
|
||||
if (uri.scheme != null && uri.scheme != "file") throw IllegalArgumentException("payload file reference must use file:// or an absolute local path")
|
||||
@@ -153,17 +189,41 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
|
||||
}
|
||||
if (!file.isFile || !file.canRead()) throw IllegalArgumentException("payload file is not a readable local file")
|
||||
if (file.length() <= 0L || file.length() > MAX_PAYLOAD_BYTES) throw IllegalArgumentException("payload file size must be in 1..$MAX_PAYLOAD_BYTES bytes")
|
||||
return file.inputStream().use(::readBounded)
|
||||
return file
|
||||
}
|
||||
private fun readBounded(input: InputStream): ByteArray {
|
||||
private data class PayloadRefs(val scalar: File?, val vector: File?, val validity: File)
|
||||
private data class PayloadPlanes(val scalar: ByteArray?, val vector: ByteArray?, val validity: ByteArray)
|
||||
private fun resolvePayloadRefs(validityFileUri: String, scalarFileUri: String, vectorFileUri: String): PayloadRefs {
|
||||
val validity = resolveLocalFile(validityFileUri)
|
||||
// Empty strings are the frozen TurboModule ABI markers for absent optional planes.
|
||||
val scalar = scalarFileUri.takeIf { it.isNotEmpty() }?.let(::resolveLocalFile)
|
||||
val vector = vectorFileUri.takeIf { it.isNotEmpty() }?.let(::resolveLocalFile)
|
||||
if (scalar == null && vector == null) throw IllegalArgumentException("payloadRefs requires scalarFileUri or vectorFileUri")
|
||||
var total = 0L
|
||||
for (file in listOfNotNull(scalar, vector, validity)) {
|
||||
if (file.length() > MAX_PAYLOAD_BYTES - total) throw IllegalArgumentException("aggregate payload size must not exceed $MAX_PAYLOAD_BYTES bytes")
|
||||
total += file.length()
|
||||
}
|
||||
return PayloadRefs(scalar, vector, validity)
|
||||
}
|
||||
private fun readPayloadRefs(refs: PayloadRefs): PayloadPlanes {
|
||||
val scalar = refs.scalar
|
||||
val vector = refs.vector
|
||||
return PayloadPlanes(
|
||||
scalar?.inputStream()?.use { readBounded(it, scalar.length()) },
|
||||
vector?.inputStream()?.use { readBounded(it, vector.length()) },
|
||||
refs.validity.inputStream().use { readBounded(it, refs.validity.length()) },
|
||||
)
|
||||
}
|
||||
private fun readBounded(input: InputStream, expectedLength: Long): ByteArray {
|
||||
val output = ByteArrayOutputStream(); val buffer = ByteArray(DEFAULT_READ_BUFFER_BYTES)
|
||||
while (true) {
|
||||
val count = input.read(buffer)
|
||||
if (count < 0) break
|
||||
if (output.size().toLong() + count > MAX_PAYLOAD_BYTES) throw IllegalArgumentException("payload file exceeds $MAX_PAYLOAD_BYTES bytes")
|
||||
if (output.size().toLong() + count > expectedLength || output.size().toLong() + count > MAX_PAYLOAD_BYTES) throw IllegalArgumentException("payload file changed while being read")
|
||||
output.write(buffer, 0, count)
|
||||
}
|
||||
return output.toByteArray()
|
||||
return output.toByteArray().also { if (it.size.toLong() != expectedLength) throw IllegalArgumentException("payload file changed while being read") }
|
||||
}
|
||||
private fun withRuntime(reactTag: Double, promise: Promise, fn: (WeatherRuntime) -> Unit) = withMap(reactTag, promise) { map ->
|
||||
val runtime = map.weatherRuntime
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navsea/maplibre-particle-rn",
|
||||
"version": "13.4.1-particle.2",
|
||||
"version": "13.4.1-particle.3",
|
||||
"description": "NavSea MapLibre Particle React Native pure Field Renderer API.",
|
||||
"license": "BSD-2-Clause",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getNativeFieldRuntimeBridge } from "./NativeWeatherRuntimeBridge";
|
||||
import type { ColorChannel, CommandResult, FieldColorStop, FieldSubmitResult, PlaybackState, RuntimeSnapshot, TargetFps } from "./types";
|
||||
import type { ColorChannel, CommandResult, FieldColorStop, FieldPayloadRefs, FieldSubmitResult, FieldValueSource, PlaybackState, RuntimeSnapshot, TargetFps } from "./types";
|
||||
|
||||
function nativeBridge() {
|
||||
const bridge = getNativeFieldRuntimeBridge();
|
||||
@@ -12,8 +12,22 @@ export const FieldRuntime = {
|
||||
attach: (reactTag: number): Promise<RuntimeSnapshot> => nativeBridge().attach(reactTag),
|
||||
detach: (reactTag: number): Promise<void> => nativeBridge().detach(reactTag),
|
||||
getState: (reactTag: number): Promise<RuntimeSnapshot> => nativeBridge().getState(reactTag),
|
||||
clearFields: (reactTag: number): Promise<CommandResult> => nativeBridge().clearWindFields(reactTag),
|
||||
submitField: (reactTag: number, metadataJson: string, payloadFileUri: string): Promise<FieldSubmitResult> => nativeBridge().submitWindField(reactTag, metadataJson, payloadFileUri),
|
||||
clearFields: (reactTag: number): Promise<CommandResult> => nativeBridge().clearFields(reactTag),
|
||||
submitField: (reactTag: number, metadataJson: string, payloadRefs: FieldPayloadRefs): Promise<FieldSubmitResult> =>
|
||||
nativeBridge().submitField(
|
||||
reactTag,
|
||||
metadataJson,
|
||||
payloadRefs.validityFileUri,
|
||||
payloadRefs.scalarFileUri ?? "",
|
||||
payloadRefs.vectorFileUri ?? "",
|
||||
),
|
||||
/** Explicit v1 compatibility only; v2 submitField always receives FieldPayloadRefs. */
|
||||
submitLegacyVectorField: (reactTag: number, metadataJson: string, payloadFileUri: string): Promise<FieldSubmitResult> => nativeBridge().submitWindField(reactTag, metadataJson, payloadFileUri),
|
||||
setFieldValueSource: (reactTag: number, source: FieldValueSource): Promise<CommandResult> => nativeBridge().setFieldValueSource(reactTag, source),
|
||||
setParticleAdvectionScale: (reactTag: number, scale: number): Promise<CommandResult> => {
|
||||
if (!Number.isFinite(scale) || scale <= 0) return Promise.reject(new Error("advectionScale must be finite and > 0"));
|
||||
return nativeBridge().setParticleAdvectionScale(reactTag, scale);
|
||||
},
|
||||
setEnabled: (reactTag: number, enabled: boolean): Promise<CommandResult> => nativeBridge().setWeatherEnabled(reactTag, enabled),
|
||||
setFieldVisible: (reactTag: number, visible: boolean): Promise<CommandResult> => nativeBridge().setWeatherColorVisible(reactTag, visible),
|
||||
setParticleVisible: (reactTag: number, visible: boolean): Promise<CommandResult> => nativeBridge().setParticleVisible(reactTag, visible),
|
||||
|
||||
@@ -8,13 +8,18 @@ type CommandResult = "ACCEPTED" | "REJECTED";
|
||||
type FieldSubmitResult = "REJECTED_SOURCE_MODE" | "REJECTED_ADAPTER" | "REJECTED_INCOMPATIBLE" | "REJECTED_NON_MONOTONIC" | "REJECTED_CAPACITY" | "ACCEPTED_PENDING_INITIAL_WINDOW" | "INITIAL_WINDOW_READY" | "ACCEPTED_NEXT_SNAPSHOT" | "DUPLICATE" | "UNKNOWN";
|
||||
type ColorStop = { speedMps: number; red: number; green: number; blue: number; alpha: number };
|
||||
type RuntimeSnapshot = { state: RuntimeState; playbackState: PlaybackState; targetFps: TargetFps; disposed: boolean };
|
||||
|
||||
export interface Spec extends TurboModule {
|
||||
attach(reactTag: number): Promise<RuntimeSnapshot>;
|
||||
detach(reactTag: number): Promise<void>;
|
||||
getState(reactTag: number): Promise<RuntimeSnapshot>;
|
||||
clearWindFields(reactTag: number): Promise<CommandResult>;
|
||||
submitWindField(reactTag: number, metadataJson: string, payloadFileUri: string): Promise<FieldSubmitResult>;
|
||||
clearFields(reactTag: number): Promise<CommandResult>;
|
||||
// Keep the codegen ABI to primitive parameters. Empty scalar/vector strings are explicit
|
||||
// optional-plane markers at this boundary only; the public API keeps FieldPayloadRefs.
|
||||
submitField(reactTag: number, metadataJson: string, validityFileUri: string, scalarFileUri: string, vectorFileUri: string): Promise<FieldSubmitResult>;
|
||||
setFieldValueSource(reactTag: number, source: "SCALAR" | "VECTOR_MAGNITUDE"): Promise<CommandResult>;
|
||||
setParticleAdvectionScale(reactTag: number, scale: number): Promise<CommandResult>;
|
||||
setWeatherEnabled(reactTag: number, enabled: boolean): Promise<CommandResult>;
|
||||
setWeatherColorVisible(reactTag: number, visible: boolean): Promise<CommandResult>;
|
||||
setParticleVisible(reactTag: number, visible: boolean): Promise<CommandResult>;
|
||||
|
||||
@@ -3,7 +3,9 @@ export type {
|
||||
ColorChannel,
|
||||
CommandResult,
|
||||
FieldColorStop,
|
||||
FieldPayloadRefs,
|
||||
FieldSubmitResult,
|
||||
FieldValueSource,
|
||||
PlaybackState,
|
||||
RuntimeSnapshot,
|
||||
RuntimeState,
|
||||
|
||||
@@ -3,6 +3,13 @@ export type PlaybackState = "PLAY" | "PAUSE";
|
||||
export type TargetFps = "AUTO" | "FPS_30" | "FPS_60";
|
||||
export type CommandResult = "ACCEPTED" | "REJECTED";
|
||||
export type FieldSubmitResult = "REJECTED_SOURCE_MODE" | "REJECTED_ADAPTER" | "REJECTED_INCOMPATIBLE" | "REJECTED_NON_MONOTONIC" | "REJECTED_CAPACITY" | "ACCEPTED_PENDING_INITIAL_WINDOW" | "INITIAL_WINDOW_READY" | "ACCEPTED_NEXT_SNAPSHOT" | "DUPLICATE" | "UNKNOWN";
|
||||
export type FieldPayloadRefs = Readonly<
|
||||
{ validityFileUri: string } & (
|
||||
| { scalarFileUri: string; vectorFileUri?: string }
|
||||
| { scalarFileUri?: string; vectorFileUri: string }
|
||||
)
|
||||
>;
|
||||
export type FieldValueSource = "SCALAR" | "VECTOR_MAGNITUDE";
|
||||
export type ColorChannel = number;
|
||||
export type FieldColorStop = Readonly<{ value: number; red: ColorChannel; green: ColorChannel; blue: ColorChannel; alpha: ColorChannel }>;
|
||||
export type RuntimeSnapshot = Readonly<{ state: RuntimeState; playbackState: PlaybackState; targetFps: TargetFps; disposed: boolean }>;
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -739,13 +739,44 @@ RasterArrayHostFieldSubmitResult RasterArraySource::submitHostField(const std::s
|
||||
return RasterArrayHostFieldSubmitResult::RejectedAdapter;
|
||||
}
|
||||
|
||||
auto snapshot = std::move(*decoded.snapshot);
|
||||
Log::Info(Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_LEGACY_VECTOR_ONLY sourceId=" + getID() +
|
||||
" payloadBytes=" + std::to_string(payload.size()));
|
||||
return submitDecodedHostField(std::move(*decoded.snapshot));
|
||||
}
|
||||
|
||||
RasterArrayHostFieldSubmitResult RasterArraySource::submitHostField(
|
||||
const std::string& metadataText,
|
||||
const particle::ParticleFieldPayloadBundle& payloads) {
|
||||
const auto bytes = [](const std::optional<std::vector<char>>& plane) {
|
||||
return plane ? std::to_string(plane->size()) : std::string("absent");
|
||||
};
|
||||
Log::Info(Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_V2_SUBMIT_REQUEST sourceId=" + getID() + " metadataBytes=" +
|
||||
std::to_string(metadataText.size()) + " scalarBytes=" + bytes(payloads.scalar) +
|
||||
" vectorBytes=" + bytes(payloads.vector) + " validityBytes=" + bytes(payloads.validity));
|
||||
if (!impl().hostFieldSubmission) {
|
||||
return RasterArrayHostFieldSubmitResult::RejectedSourceMode;
|
||||
}
|
||||
auto decoded = particle::LocalParticleFieldFeed::load(metadataText, payloads);
|
||||
if (!decoded) {
|
||||
Log::Warning(Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_V2_SUBMIT_REJECTED sourceId=" + getID() +
|
||||
" result=REJECTED_ADAPTER errorCode=" + particle::particleFieldAdapterErrorName(decoded.error) +
|
||||
" error=" + decoded.message);
|
||||
return RasterArrayHostFieldSubmitResult::RejectedAdapter;
|
||||
}
|
||||
return submitDecodedHostField(std::move(*decoded.snapshot));
|
||||
}
|
||||
|
||||
RasterArrayHostFieldSubmitResult RasterArraySource::submitDecodedHostField(particle::ParticleFieldSnapshot snapshot) {
|
||||
Log::Info(Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_SNAPSHOT_READY sourceId=" + getID() + " fieldId=" + snapshot.fieldID +
|
||||
" generation=" + std::to_string(snapshot.generation) +
|
||||
" validTime=" + std::to_string(snapshot.validTime) + " width=" + std::to_string(snapshot.width) +
|
||||
" height=" + std::to_string(snapshot.height) +
|
||||
" layout=interleaved-uv encoding=float32-le payloadBytes=" + std::to_string(payload.size()));
|
||||
" height=" + std::to_string(snapshot.height) + " scalar=" +
|
||||
(snapshot.hasScalar() ? "present" : "absent") + " vector=" +
|
||||
(snapshot.hasVector() ? "present" : "absent") + " validity=present");
|
||||
|
||||
std::shared_ptr<const particle::ParticleFieldSnapshot> previous;
|
||||
if (timeline) {
|
||||
|
||||
@@ -94,6 +94,23 @@ particle::ParticleFieldPayloadBundle v2Payload(std::optional<std::vector<char>>
|
||||
return {.scalar = std::move(scalar), .vector = std::move(vector), .validity = std::move(validity)};
|
||||
}
|
||||
|
||||
std::string v2HostMetadata(const int index, const bool scalar, const bool vector, const char* semantics = "VELOCITY") {
|
||||
std::string result = "{\"schema\":\"maplibre-particle-field/v2\",\"fieldId\":\"v2-host-" +
|
||||
std::to_string(index) + "\",\"generation\":" + std::to_string(index + 1) +
|
||||
",\"validTime\":" + std::to_string(100 + index) +
|
||||
",\"grid\":{\"width\":2,\"height\":1},\"bounds\":{\"west\":0,\"south\":0,\"east\":1,\"north\":1},\"validity\":{\"encoding\":\"uint8\",\"byteLength\":2}";
|
||||
if (scalar) result += ",\"scalar\":{\"encoding\":\"float32-le\",\"byteLength\":8,\"noData\":-9999}";
|
||||
if (vector) result += std::string(",\"vector\":{\"layout\":\"interleaved-uv\",\"encoding\":\"float32-le\",\"byteLength\":16,\"noData\":-9999,\"semantics\":\"") + semantics + "\"}";
|
||||
return result + "}";
|
||||
}
|
||||
|
||||
std::string v2LegacyCompatibleHostMetadata(const int index) {
|
||||
return "{\"schema\":\"maplibre-particle-field/v2\",\"fieldId\":\"v2-compatible-t" +
|
||||
std::to_string(index) + "\",\"generation\":" + std::to_string(index + 1) +
|
||||
",\"validTime\":" + std::to_string(1704067200000ULL + index * 3600000ULL) +
|
||||
",\"grid\":{\"width\":16,\"height\":16},\"bounds\":{\"west\":-180,\"south\":-85,\"east\":180,\"north\":85},\"validity\":{\"encoding\":\"uint8\",\"byteLength\":256},\"vector\":{\"layout\":\"interleaved-uv\",\"encoding\":\"float32-le\",\"byteLength\":2048,\"noData\":-9999,\"semantics\":\"VELOCITY\"}}";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
using namespace mbgl;
|
||||
@@ -545,6 +562,73 @@ TEST(Source, HostFieldSubmissionBuildsOneCoreOwnedTimeline) {
|
||||
EXPECT_EQ(source.impl().timeline->frame().t0Index, 2u);
|
||||
}
|
||||
|
||||
TEST(Source, HostFieldV2BundleUsesTheSameTimelineIngress) {
|
||||
SourceTest test;
|
||||
style::RasterArraySource source{"host-submission"};
|
||||
source.setObserver(&test.styleObserver);
|
||||
source.loadDescription(*test.fileSource);
|
||||
const auto valid = std::vector<char>{1, 1};
|
||||
const auto scalarT0 = floatBytes({2.0f, 3.0f});
|
||||
const auto scalarT1 = floatBytes({2.5f, 3.5f});
|
||||
const auto vectorT0 = floatBytes({1.0f, 0.0f, 0.0f, 1.0f});
|
||||
const auto vectorT1 = floatBytes({0.5f, 0.0f, 0.0f, 1.0f});
|
||||
|
||||
EXPECT_EQ(source.submitHostField(v2HostMetadata(0, true, false), v2Payload(scalarT0, std::nullopt, valid)),
|
||||
style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow);
|
||||
EXPECT_EQ(source.submitHostField(v2HostMetadata(1, true, false), v2Payload(scalarT1, std::nullopt, valid)),
|
||||
style::RasterArrayHostFieldSubmitResult::InitialWindowReady);
|
||||
EXPECT_TRUE(source.impl().field->hasScalar());
|
||||
EXPECT_FALSE(source.impl().field->hasVector());
|
||||
|
||||
EXPECT_TRUE(source.clearHostFields());
|
||||
EXPECT_EQ(source.submitHostField(v2HostMetadata(2, false, true), v2Payload(std::nullopt, vectorT0, valid)),
|
||||
style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow);
|
||||
EXPECT_EQ(source.submitHostField(v2HostMetadata(3, false, true), v2Payload(std::nullopt, vectorT1, valid)),
|
||||
style::RasterArrayHostFieldSubmitResult::InitialWindowReady);
|
||||
EXPECT_TRUE(source.impl().field->hasVector());
|
||||
EXPECT_EQ(source.impl().field->vectorSemantics, particle::VectorSemantics::Velocity);
|
||||
|
||||
EXPECT_TRUE(source.clearHostFields());
|
||||
EXPECT_EQ(source.submitHostField(v2HostMetadata(4, true, true, "DIRECTION"), v2Payload(scalarT0, vectorT0, valid)),
|
||||
style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow);
|
||||
EXPECT_EQ(source.submitHostField(v2HostMetadata(5, true, true, "DIRECTION"), v2Payload(scalarT1, vectorT1, valid)),
|
||||
style::RasterArrayHostFieldSubmitResult::InitialWindowReady);
|
||||
EXPECT_EQ(source.impl().field->vectorSemantics, particle::VectorSemantics::Direction);
|
||||
EXPECT_EQ(source.submitHostField(v2HostMetadata(6, true, true), v2Payload(scalarT1, vectorT1, valid)),
|
||||
style::RasterArrayHostFieldSubmitResult::RejectedIncompatibleSnapshot);
|
||||
|
||||
EXPECT_TRUE(source.clearHostFields());
|
||||
EXPECT_EQ(source.submitHostField(v2HostMetadata(7, false, true),
|
||||
particle::ParticleFieldPayloadBundle{.scalar = std::nullopt,
|
||||
.vector = vectorT0,
|
||||
.validity = std::nullopt}),
|
||||
style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
|
||||
EXPECT_EQ(source.submitHostField(v2HostMetadata(8, false, false), v2Payload(std::nullopt, std::nullopt, valid)),
|
||||
style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
|
||||
}
|
||||
|
||||
TEST(Source, HostFieldLegacyAndCompatibleV2VectorOnlyShareOneTimeline) {
|
||||
SourceTest test;
|
||||
style::RasterArraySource source{"host-submission"};
|
||||
source.setObserver(&test.styleObserver);
|
||||
source.loadDescription(*test.fileSource);
|
||||
const auto legacyBytes = fixtureBytes("uniform-east", "field.f32");
|
||||
const std::vector<char> legacyPayload(legacyBytes.begin(), legacyBytes.end());
|
||||
const std::vector<char> validity(16 * 16, 1);
|
||||
const auto northBytes = fixtureBytes("uniform-north", "field.f32");
|
||||
const std::vector<char> nextPayload(northBytes.begin(), northBytes.end());
|
||||
|
||||
EXPECT_EQ(source.submitHostField(localMetadata(0), legacyPayload),
|
||||
style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow);
|
||||
EXPECT_EQ(source.submitHostField(v2LegacyCompatibleHostMetadata(1),
|
||||
v2Payload(std::nullopt, nextPayload, validity)),
|
||||
style::RasterArrayHostFieldSubmitResult::InitialWindowReady);
|
||||
ASSERT_TRUE(source.impl().timeline);
|
||||
EXPECT_EQ(source.impl().timeline->size(), 2u);
|
||||
EXPECT_EQ(source.impl().field->vectorSemantics, particle::VectorSemantics::Velocity);
|
||||
EXPECT_EQ(source.impl().nextField->vectorSemantics, particle::VectorSemantics::Velocity);
|
||||
}
|
||||
|
||||
TEST(Source, HostFieldClearAndTimestampSeekAreFailClosed) {
|
||||
SourceTest test;
|
||||
style::RasterArraySource source{"host-submission"};
|
||||
|
||||
Reference in New Issue
Block a user