diff --git a/docs/40_任务/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex任务_v1.md b/docs/40_任务/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex任务_v1.md new file mode 100644 index 000000000..eff6dd7a4 --- /dev/null +++ b/docs/40_任务/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex任务_v1.md @@ -0,0 +1,157 @@ +# STEP-070 MapLibreParticleLab V3-06 四类场 Fixture 与 RN 通用回归 — Codex 任务 v1 + +## 1. 目标 +基于 `particle/main` 当前 HEAD `3b012759838f1e1ecc44559faffe061d396b6cce`,完成 V3 通用 Scalar/Vector Field 的离线业务语义映射与回归资产:Wind、Rain、Wave、Tide/Current 四类场,外加固定 synthetic Wave independence fixture。 + +本 Step 的重点不是增加新的 Runtime 架构,而是证明 STEP-065~069 冻结的通用契约能够由真实/可追溯数据映射并通过 `.3` RN 公共 API 驱动。Android 编译/安装/ADB/设备视觉验证继续留到后续统一周期,不在本任务执行。 + +开始前完整阅读 `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目规范/`、STEP-065~069 Step/报告,以及当前实际源码。不得按记忆重建。 + +## 2. 必须保持的架构边界 +- Core/Renderer 不得出现 Wind/Wave/Rain/Tide/provider/network/vendor 业务分支。 +- Host/RN 负责业务字段到 canonical scalar/vector/validity 的映射。 +- canonical schema 继续使用 `maplibre-particle-field/v2`。 +- 一个 Snapshot:0/1 scalar + 0/1 vector + shared validity;neither 非法。 +- Wind / Tide(Current):vector `VELOCITY`,Field=`VECTOR_MAGNITUDE`,Particle 使用 VELOCITY。 +- Rain:scalar rain rate,Field=`SCALAR`,不启用 Particle。 +- Wave:scalar=wave height;vector=direction-to unit vector;semantics=`DIRECTION`;Field=`SCALAR`;Particle=`DIRECTION`,推进视觉速度只来自 `advectionScale`。 +- Wave period 不进入第二 scalar,不参与推进,不从 height/period 推导速度。 +- Rain precipitation_type 暂不进入 canonical V3.0。 +- T0/T1/MIX、Timeline、Playback、OLD_T1→NEW_T0 reuse 等 Core 逻辑不得修改。 + +## 3. 当前基线事实 +- RN package 已是 `@navsea/maplibre-particle-rn@13.4.1-particle.3`。 +- 正式 v2 API:`FieldRuntime.submitField(reactTag, metadataJson, payloadRefs)`。 +- `payloadRefs` 为必填 validity + optional scalar/vector local file URI,scalar/vector 至少一个。 +- `.3` 已提供 `setFieldValueSource()` 与 `setParticleAdvectionScale()`。 +- Android/JNI/Core multi-plane ingress 已实现,但 Android 编译留待 V3-08。 +- 现有 RN Test Host 的 REAL WEATHER 仍直接调用旧 `WeatherRuntimeBridge.submitWindField()`;本 Step 必须迁移验证入口到 package `.3` 的 `FieldRuntime`,不能继续用旧 bridge 证明新 API。 +- 仓库已有真实 WeatherV2 Wind canonical 资产:`platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-weatherv2/ecmwf-hres-wind-20260807/`。 +- WeatherV2 源树只读位于 `/root/sourceserver/weatherV2`;其 NSFT v1 语义以 `src/dev/core/nsft.py` 和当前生产 pipeline 为准。不要修改 WeatherV2。 + +## 4. 数据来源与可追溯性 +先调查 `/root/sourceserver/weatherV2` 当前实际输出,优先从已存在的 NSFT v1 `field_pmtiles`/`/fields` 等本地数值产物生成 fixture,不联网、不重新跑完整生产 pipeline。 + +至少为以下四类场确认一个可用 source/refTime/validTime/区域: +- Wind +- Rain +- Wave +- Tide/Current + +优先选日本及周边、相邻两个实际 validTime,便于 T0/T1/MIX。若某层当前本地没有可用正式 NSFT 数值产物,不得伪称“真实 WeatherV2”;应在报告明确说明并使用最小可追溯 synthetic/decoder-derived fallback,同时保留该层的业务语义。Wind 已有真实 canonical 基线可直接复用/升级。 + +不得把旧 QBIN 当作正式未来 transport。若为数据核验读取旧 BIN,只能作为来源调查/对照,最终 fixture 必须输出 v2 canonical planes。 + +## 5. 新建通用 fixture/converter 工具 +在 `tools/` 下新增 provider-neutral 或 WeatherV2-to-canonical 的离线工具,具体目录名由实际实现确定,但不得只为 Wind 特化。 + +工具至少应支持把已解码/NSFT 数值场规范化为: +- `metadata.json` +- `validity.u8` +- optional `scalar.f32` +- optional `vector.f32`(interleaved U/V) +- `conversion_summary.json` + +约束: +- float32-le;validity uint8;NW-origin row-major;rows north→south;cols west→east。 +- U eastward,V northward。 +- 每个输出文件记录 byteLength、SHA-256、grid、bounds、validTime、generation、source provenance。 +- vector DIRECTION 必须在 valid cell 上接近 unit length;允许原始量化误差,转换后可规范化,但近零/非 finite 必须 invalid/fail-closed,不得任造方向。 +- shared validity 必须由源有效性与所有存在 channel 的有限性共同约束;invalid cell 的具体 payload 值可为 0,但 validity=0 才是权威。 +- metadata 中不允许 Wind/Wave/Rain/Tide capability enum;`fieldId` 可包含可读 fixture 名称但 Core 不依赖它。 + +## 6. 四类 fixture 目录 +新增仓库内固定测试资产,建议: +`integration/particle-v3-fields/` + +至少: +- `wind-real/`:两个相邻 validTime,vector-only VELOCITY。 +- `rain-real/`:两个相邻 validTime,scalar-only。 +- `wave-real/`:两个相邻 validTime,scalar+vector DIRECTION。 +- `tide-real/`:两个相邻 validTime,vector-only VELOCITY。 +- `wave-synthetic-independence/`:两个兼容时次或至少足以验证 T0/T1 的 fixture。 + +若某 `*-real` 因本机数据缺失只能 fallback,目录名不得误用 real;改用 `*-decoder-fixture` 或等价明确命名,并在 dataset manifest/报告写明来源。 + +每组 fixture 提供 `dataset.json`,列出 frame 顺序、metadata/plane 路径、expected FieldValueSource、particle expected on/off、vector semantics、建议 advectionScale(Wave synthetic 可指定非 1 值用于验证 scale 独立性)。 + +## 7. Synthetic Wave Independence 固定语义 +必须实现并静态验证: +- scalar(wave height)西侧低、东侧高,形成明显 E→W 或 W→E 的横向数值梯度; +- vector 在所有 valid cell 固定向北 `(u=0, v=1)`; +- `vectorSemantics=DIRECTION`; +- Field source=`SCALAR`; +- Particle 方向应只由 vector 决定为北向;scalar 改变不得改变粒子方向/推进 magnitude; +- 改 `advectionScale` 只改变 DIRECTION visual movement magnitude,不改变 field scalar。 + +增加无 Android 的数据 oracle/脚本验证该 fixture:scalar 的东西梯度与 vector 的北向独立,不允许只靠人工阅读 JSON。 + +## 8. RN Test Host 升级到 `.3` 正式 API +只修改 Test Host 的 JS/TS 与离线 assets/config,不执行 Gradle。 + +要求: +- REAL WEATHER/V3 验证入口从内部 `WeatherRuntimeBridge.submitWindField` 迁移到 `@navsea/maplibre-particle-rn` 的 `FieldRuntime`。 +- 不从 package 内部路径 import bridge。 +- 通过 app cache/local file URI 提交 metadata + plane refs;不得 JS number[] / base64 / giant JSON。 +- 提供四类场选择/加载能力,至少能在 UI/fixture runner 中选择 Wind/Rain/Wave/Tide 和 synthetic Wave。 +- 每次切换 dataset 前 `clearFields()`,设置正确的 `setFieldValueSource()`、Particle visibility、`setParticleAdvectionScale()`,再顺序提交 T0/T1。 +- 对每个 dataset 明确断言:T0=`ACCEPTED_PENDING_INITIAL_WINDOW`,T1=`INITIAL_WINDOW_READY`,runtime state=`READY`。 +- Rain 必须 Particle OFF;Wind/Tide Particle ON;Wave Particle ON 且 DIRECTION 数据不被改成 VELOCITY。 +- Field/Particle visibility 仍独立。 +- 不在 JS 保存 T0/T1/MIX 第二份 Runtime 状态;JS 只保存 fixture/选择 UI 与调用结果。 +- 现有 GSI/base map 与已验证 UI 可保留,不做无关重构。 + +## 9. Core/Linux 回归 +新增最小 C++/数据 contract 测试,重点不是复制完整 Android bridge,而是验证: +- 四类 canonical shape 能被 `LocalParticleFieldFeed` / `RasterArraySource` 接受。 +- Wind/Tide vector-only VELOCITY compatible T0/T1。 +- Rain scalar-only compatible T0/T1。 +- Wave scalar+vector DIRECTION compatible T0/T1。 +- channel presence/semantics mismatch 继续 fail-closed。 +- synthetic Wave 的 scalar/vector independence oracle。 +- normal T0→T1 / next snapshot 不修改 Runtime 算法。 + +如果现有 test 已覆盖底层形状,不重复堆测试;新增 fixture-specific contract test 即可。 + +## 10. 本 Step 不做 +- 不执行 Android Gradle/AAR/APK/ADB/设备测试。 +- 不执行 Jest。 +- 不修改 NavSea。 +- 不修改 WeatherV2 源项目。 +- 不联网下载天气数据。 +- 不引入 provider/network/cache/manifest runtime 到 C++ Core。 +- 不改 v2 schema、Timeline/Playback、shader 数学、DIRECTION 算法。 +- 不发布 npm/tgz/tag;`.3` 仅保持源码版本。 +- 不修改 STEP-062 两处现有用户 dirty 文档,不清理历史 untracked/build/cache。 + +## 11. 必须验证 +至少执行并记录: +1. fixture converter/validator 自检 PASS,含 plane byteLength/SHA/grid/bounds/time/finite/validity。 +2. synthetic Wave independence oracle PASS。 +3. package `.3` TypeScript `tsc --noEmit` PASS。 +4. RN Test Host TypeScript/typecheck PASS(使用项目现有命令,不跑 Jest)。 +5. RN TurboModule codegen schema 检查 PASS,确保 STEP-069 ABI 未被破坏。 +6. 受影响 Core 精确对象编译 PASS。 +7. 相关 C++ fixture/Source 过滤测试 PASS。 +8. `git diff --check` PASS。 + +所有 noisy build 使用 `$build-output-throttle`。若完整 `mbgl-test/runner` 已经最新可直接使用;若因改 C++ test 触发重链,允许 Linux 重链。 + +## 12. 文档输出 +必须生成/更新: +- 本任务书。 +- `docs/50_Step记录/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_v1.md` +- `docs/报告/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex完成报告_v1.md` +- 追加 `粒子开发记录.md` +- 新工具 README / fixture dataset manifest 必须中文说明为主。 + +状态上限:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。不得标记 Android CLOSED/PASS。 + +## 13. Git 收尾 +所有本任务实际修改必须 scoped commit 并 push `origin/particle/main`。 +- 开始/结束都核对 branch、HEAD、origin。 +- 严禁 `git add .` / `git add -A`。 +- 不提交 STEP-062 两处用户 dirty、历史任务 MD、build/cache/tgz。 +- 可使用 1 个实现 commit + 1 个文档 handoff commit;必要时再用一个纯 Git 元数据收尾 commit。 +- 完成报告记录 implementation/handoff commit、push result、remote branch、Final HEAD、`HEAD == origin/particle/main`。 +- push 失败不得声称完成。 diff --git a/docs/50_Step记录/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_v1.md b/docs/50_Step记录/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_v1.md new file mode 100644 index 000000000..bc1170854 --- /dev/null +++ b/docs/50_Step记录/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_v1.md @@ -0,0 +1,33 @@ +# STEP-070 MapLibreParticleLab V3-06 四类场 Fixture 与 RN 通用回归 + +状态:IMPLEMENTED / WAITING_ANDROID_VALIDATION + +## 目标与边界 + +在 `particle/main` `3b012759` 上增加离线 V3 fixture、转换/校验工具、RN Test Host `.3` 公共 API 接线和最小 Core contract test。不修改 Timeline/Playback/T0/T1/MIX/rollover、schema、shader、WeatherV2、NavSea 或 STEP-062 两份用户 dirty 文档;不运行 Android Gradle/AAR/APK/ADB/Jest。 + +## 完成内容 + +- [x] `tools/particle_v3_fixture` 生成并校验 v2 metadata、shared validity、可选 scalar/vector、长度与 SHA-256。validator 对每一个 validity=1 的 cell 检查所有存在 plane:finite 且不等于 noData。 +- [x] `wind-real` 从本地 canonical v1 `field.f32` 重新封装;shared validity 严格派生为 `finite(U) && finite(V) && U != -9999 && V != -9999`。 +- [x] `wave-real` 由 WeatherV2 `ecmwf-wam/waves/2026090912/wave_bin` 的正式 `2026090918/2026090919` BIN 和 header sidecar 解码:scalar=`height_m`,vector=规范化 `unit_to_u/unit_to_v`,语义 `DIRECTION`;`period_s` 和原始 degree 不进入 canonical。 +- [x] `tide-real` 由 WeatherV2 `cmems/tide/2026090900/current_bin` 的正式 `2026090901/2026090902` BIN 和 header sidecar 解码为 vector-only `VELOCITY`。 +- [x] Wave/Tide 每帧 summary 记录正式源 BIN 与 sidecar SHA-256、target_time、referenceTime、bounds、grid、projection、included/excluded channels 和 validity 派生规则;两者 generation 为小且严格递增的 `1/2`。 +- [x] Rain 保持 `rain-decoder-fixture`,manifest/provenance 明确为非真实 field values,因为本 Step 未实现 PMTiles reader;fallback generation 为严格递增的 `1/2`。 +- [x] Test Host 的 `fieldDatasets` 已指向 `wave-real` 和 `tide-real`,继续仅经 package `FieldRuntime` 公共 API 提交 local file refs。 +- [x] 保留并验证 Wave synthetic scalar/vector independence;新增 C++ provider-neutral 四类 shape contract test。 + +## 验证 + +- [x] `python3 tools/particle_v3_fixture/generate_fixtures.py --write-reference-fixtures --validate`:PASS。 +- [x] package `.3` `tsc --noEmit`:PASS。 +- [x] RN Test Host `npm run typecheck`:PASS(不含 Jest)。 +- [x] RN TurboModule `combine-js-to-schema-cli.js`:PASS。 +- [x] 使用 `CCACHE_DIR=/tmp/step070-ccache`,重新编译 `source.test.cpp.o`:PASS。 +- [ ] `mbgl-test` / `mbgl-test-runner` 重链与 STEP-070/相关 V3 Source 过滤测试:重链前 target clean 扩大为完整依赖闭包;之后受限终端会话没有返回退出码。未标 PASS,当前仍待恢复构建并运行过滤测试。 +- [x] `git diff --check`:PASS(提交前复核)。 +- [ ] Android Gradle/AAR/APK/ADB/Jest:按任务范围未运行。 + +## 当前结果、阻塞与下一步 + +fixture 数据契约、TypeScript 与 codegen 已完成;Linux 完整重链/过滤测试仍待恢复。Android/JNI 真实编译和设备视觉验收仍由后续统一周期完成。Rain 要升级为真实数值场,需要另行实现只读 PMTiles reader;不得把现有 fallback 改称 real。 diff --git a/docs/报告/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex完成报告_v1.md b/docs/报告/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex完成报告_v1.md new file mode 100644 index 000000000..33285d832 --- /dev/null +++ b/docs/报告/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex完成报告_v1.md @@ -0,0 +1,11 @@ +# STEP-070 MapLibreParticleLab V3-06 四类场 Fixture 与 RN 通用回归 Codex 完成报告 + +结论:IMPLEMENTED / WAITING_ANDROID_VALIDATION。 + +Wind、Wave、Tide/Current 已形成真实本地离线 canonical fixture。Wind 从既有 v1 field 转换时,shared validity 现在严格要求 U/V 都 finite 且都不等于 `-9999`。Wave 直接读取 WeatherV2 `2026090912` run 的正式 `waves_particles.bin` 与 sidecar:height 进入 scalar,规范化后的 `unit_to_u/v` 进入 `DIRECTION` vector,period 被明确排除。Tide 从 CMEMS `2026090900` run 的正式 `current_vectors.bin` 读取 U/V 形成 `VELOCITY` vector。每帧摘要记录源 BIN 与 sidecar SHA-256、target_time、bounds、grid、projection 与 provenance。 + +Rain 仍是明确的 `rain-decoder-fixture`:本任务未实现 PMTiles reader,其数值不是真实场值。所有 fallback/synthetic generation 均为小且严格递增的 `1/2`,不再使用 epoch 毫秒。 + +RN Test Host 的 Wave/Tide 已改用新 real 目录;所有提交继续只使用 `@navsea/maplibre-particle-rn` 的 `FieldRuntime` 公共 API 和本地 file URI。fixture validator、package TypeScript、Host typecheck、TurboModule schema 与 `/tmp/step070-ccache` 下的 `source.test.cpp.o` 精确重编译均 PASS。重链前 target clean 意外扩大为完整依赖闭包,随后受限终端会话没有返回 `mbgl-test`/`mbgl-test-runner` 退出码;相关 Source 过滤测试也未运行,故两项均未标 PASS。完整构建日志按节流规范保存于 `.codex-build-logs/`。 + +未运行 Android Gradle/AAR/APK/ADB/Jest,故没有 Android PASS 声明。Git 收尾受当前沙箱 `.git/index.lock` 的只读文件系统阻断,未提交、未推送;没有尝试绕过限制。未修改 WeatherV2、NavSea、Core Runtime 算法或 STEP-062 两份用户 dirty 文档;Rain 真实 reader 与 Android/设备验收是后续工作。 diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090912/conversion_summary.json b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/conversion_summary.json new file mode 100644 index 000000000..a7015f208 --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/conversion_summary.json @@ -0,0 +1,35 @@ +{ + "grid": { + "width": 6, + "height": 4 + }, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "validTime": 1788955200000, + "generation": 1, + "source": { + "kind": "decoder-contract fallback", + "notReal": true, + "referenceTime": "2026090912", + "targetTime": "2026090912", + "说明": "本机未实现 PMTiles reader;此数值不是正式 WeatherV2 栅格。" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "09484872f71b85d73159e9ce51ef4c9d0abc8620f9e2c4f1155fd8b33acf2a61" + } + }, + "validCount": 24, + "invalidCount": 0 +} diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090912/metadata.json b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/metadata.json new file mode 100644 index 000000000..c14094471 --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/metadata.json @@ -0,0 +1,50 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "rain-decoder-fixture", + "generation": 1, + "dataVersion": 1, + "validTime": 1788955200000, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "grid": { + "width": 6, + "height": 4 + }, + "validity": { + "encoding": "uint8", + "byteLength": 24 + }, + "source": { + "kind": "decoder-contract fallback", + "notReal": true, + "referenceTime": "2026090912", + "targetTime": "2026090912", + "说明": "本机未实现 PMTiles reader;此数值不是正式 WeatherV2 栅格。" + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "scalar": { + "encoding": "float32-le", + "byteLength": 96, + "noData": -9999.0 + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "09484872f71b85d73159e9ce51ef4c9d0abc8620f9e2c4f1155fd8b33acf2a61" + } + } +} diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090912/scalar.f32 b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/scalar.f32 new file mode 100644 index 000000000..7d5c9df7c Binary files /dev/null and b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/scalar.f32 differ diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090912/validity.u8 b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/validity.u8 new file mode 100644 index 000000000..e03959b11 --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/validity.u8 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090918/conversion_summary.json b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/conversion_summary.json new file mode 100644 index 000000000..9e103084c --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/conversion_summary.json @@ -0,0 +1,35 @@ +{ + "grid": { + "width": 6, + "height": 4 + }, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "validTime": 1788976800000, + "generation": 2, + "source": { + "kind": "decoder-contract fallback", + "notReal": true, + "referenceTime": "2026090912", + "targetTime": "2026090918", + "说明": "本机未实现 PMTiles reader;此数值不是正式 WeatherV2 栅格。" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "74a93cc48d3875f0abd73b1ee14cc0f1a7293cd7dc7ef716a15725a162588855" + } + }, + "validCount": 24, + "invalidCount": 0 +} diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090918/metadata.json b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/metadata.json new file mode 100644 index 000000000..7fc6736ba --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/metadata.json @@ -0,0 +1,50 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "rain-decoder-fixture", + "generation": 2, + "dataVersion": 2, + "validTime": 1788976800000, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "grid": { + "width": 6, + "height": 4 + }, + "validity": { + "encoding": "uint8", + "byteLength": 24 + }, + "source": { + "kind": "decoder-contract fallback", + "notReal": true, + "referenceTime": "2026090912", + "targetTime": "2026090918", + "说明": "本机未实现 PMTiles reader;此数值不是正式 WeatherV2 栅格。" + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "scalar": { + "encoding": "float32-le", + "byteLength": 96, + "noData": -9999.0 + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "74a93cc48d3875f0abd73b1ee14cc0f1a7293cd7dc7ef716a15725a162588855" + } + } +} diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090918/scalar.f32 b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/scalar.f32 new file mode 100644 index 000000000..99097bd2d Binary files /dev/null and b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/scalar.f32 differ diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090918/validity.u8 b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/validity.u8 new file mode 100644 index 000000000..e03959b11 --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/validity.u8 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/particle-v3-fields/rain-decoder-fixture/dataset.json b/integration/particle-v3-fields/rain-decoder-fixture/dataset.json new file mode 100644 index 000000000..ea2c9fce2 --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/dataset.json @@ -0,0 +1,26 @@ +{ + "schema": "maplibre-particle-test-dataset/v2", + "datasetId": "rain-decoder-fixture", + "sourceProvenance": "非真实场值:仅 decoder-contract fallback,不能当作 WeatherV2 实测栅格", + "frames": [ + { + "label": "T0", + "path": "2026090912", + "metadata": "metadata.json", + "validity": "validity.u8", + "scalar": "scalar.f32" + }, + { + "label": "T1", + "path": "2026090918", + "metadata": "metadata.json", + "validity": "validity.u8", + "scalar": "scalar.f32" + } + ], + "expectedFieldValueSource": "SCALAR", + "particleExpected": false, + "vectorSemantics": null, + "suggestedAdvectionScale": 1.0, + "说明": "离线 canonical fixture;Runtime 只接收 metadata 与本地 plane URI。" +} diff --git a/integration/particle-v3-fields/tide-real/2026090901/conversion_summary.json b/integration/particle-v3-fields/tide-real/2026090901/conversion_summary.json new file mode 100644 index 000000000..3eaa450ba --- /dev/null +++ b/integration/particle-v3-fields/tide-real/2026090901/conversion_summary.json @@ -0,0 +1,66 @@ +{ + "grid": { + "width": 513, + "height": 513 + }, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "validTime": 1788915600000, + "generation": 1, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090901/current_vectors.bin", + "sourceSha256": "4de8b9843fcba13b452428cde7f1cf2b0a85ec0eb775c454fc6350dcdd527b13", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090901/current_vectors.bin.header.json", + "headerSha256": "5bde915b08ae4e5e41d0a45b50b0a4a1d4fbed5c7e513b00fd3d988d42697070", + "referenceTime": "2026090900", + "targetTime": "2026090901", + "grid": { + "width": 513, + "height": 513 + }, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "binMagic": "WVECBIN1", + "binFields": [ + { + "name": "u", + "dtype": "float32" + }, + { + "name": "v", + "dtype": "float32" + }, + { + "name": "speed", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 263169, + "sha256": "14e745cea79d2cb85b66ef8fc5e19c28df8f912fb975f9281becc2ce6562e597" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 2105352, + "sha256": "a0c767bde25d16a0e6beb30017f430d48ce95ba1a896bda477ba3f078af386cd" + } + }, + "validCount": 146779, + "invalidCount": 116390 +} diff --git a/integration/particle-v3-fields/tide-real/2026090901/metadata.json b/integration/particle-v3-fields/tide-real/2026090901/metadata.json new file mode 100644 index 000000000..42675cc9c --- /dev/null +++ b/integration/particle-v3-fields/tide-real/2026090901/metadata.json @@ -0,0 +1,85 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "tide-real", + "generation": 1, + "dataVersion": 1, + "validTime": 1788915600000, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "grid": { + "width": 513, + "height": 513 + }, + "validity": { + "encoding": "uint8", + "byteLength": 263169 + }, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090901/current_vectors.bin", + "sourceSha256": "4de8b9843fcba13b452428cde7f1cf2b0a85ec0eb775c454fc6350dcdd527b13", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090901/current_vectors.bin.header.json", + "headerSha256": "5bde915b08ae4e5e41d0a45b50b0a4a1d4fbed5c7e513b00fd3d988d42697070", + "referenceTime": "2026090900", + "targetTime": "2026090901", + "grid": { + "width": 513, + "height": 513 + }, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "binMagic": "WVECBIN1", + "binFields": [ + { + "name": "u", + "dtype": "float32" + }, + { + "name": "v", + "dtype": "float32" + }, + { + "name": "speed", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 2105352, + "noData": -9999.0, + "semantics": "VELOCITY", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 263169, + "sha256": "14e745cea79d2cb85b66ef8fc5e19c28df8f912fb975f9281becc2ce6562e597" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 2105352, + "sha256": "a0c767bde25d16a0e6beb30017f430d48ce95ba1a896bda477ba3f078af386cd" + } + } +} diff --git a/integration/particle-v3-fields/tide-real/2026090901/validity.u8 b/integration/particle-v3-fields/tide-real/2026090901/validity.u8 new file mode 100644 index 000000000..1d199ebc5 Binary files /dev/null and b/integration/particle-v3-fields/tide-real/2026090901/validity.u8 differ diff --git a/integration/particle-v3-fields/tide-real/2026090901/vector.f32 b/integration/particle-v3-fields/tide-real/2026090901/vector.f32 new file mode 100644 index 000000000..0fd8bfa59 Binary files /dev/null and b/integration/particle-v3-fields/tide-real/2026090901/vector.f32 differ diff --git a/integration/particle-v3-fields/tide-real/2026090902/conversion_summary.json b/integration/particle-v3-fields/tide-real/2026090902/conversion_summary.json new file mode 100644 index 000000000..624eb9bc5 --- /dev/null +++ b/integration/particle-v3-fields/tide-real/2026090902/conversion_summary.json @@ -0,0 +1,66 @@ +{ + "grid": { + "width": 513, + "height": 513 + }, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "validTime": 1788919200000, + "generation": 2, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090902/current_vectors.bin", + "sourceSha256": "2410eb208e11fdb73db9b8395b70cc5377dd078a4494ae3a67dbfa2f07280d34", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090902/current_vectors.bin.header.json", + "headerSha256": "b0001a7976dd3a6c9e626faea96d68519bf3e2d5e56cc5ce1aedfa41accf1b99", + "referenceTime": "2026090900", + "targetTime": "2026090902", + "grid": { + "width": 513, + "height": 513 + }, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "binMagic": "WVECBIN1", + "binFields": [ + { + "name": "u", + "dtype": "float32" + }, + { + "name": "v", + "dtype": "float32" + }, + { + "name": "speed", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 263169, + "sha256": "14e745cea79d2cb85b66ef8fc5e19c28df8f912fb975f9281becc2ce6562e597" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 2105352, + "sha256": "a144307fdfd1172b6f2da8af0dc50ff27a4444da3989149c9bec2d1d73c55f21" + } + }, + "validCount": 146779, + "invalidCount": 116390 +} diff --git a/integration/particle-v3-fields/tide-real/2026090902/metadata.json b/integration/particle-v3-fields/tide-real/2026090902/metadata.json new file mode 100644 index 000000000..7bb877170 --- /dev/null +++ b/integration/particle-v3-fields/tide-real/2026090902/metadata.json @@ -0,0 +1,85 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "tide-real", + "generation": 2, + "dataVersion": 2, + "validTime": 1788919200000, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "grid": { + "width": 513, + "height": 513 + }, + "validity": { + "encoding": "uint8", + "byteLength": 263169 + }, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090902/current_vectors.bin", + "sourceSha256": "2410eb208e11fdb73db9b8395b70cc5377dd078a4494ae3a67dbfa2f07280d34", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090902/current_vectors.bin.header.json", + "headerSha256": "b0001a7976dd3a6c9e626faea96d68519bf3e2d5e56cc5ce1aedfa41accf1b99", + "referenceTime": "2026090900", + "targetTime": "2026090902", + "grid": { + "width": 513, + "height": 513 + }, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "binMagic": "WVECBIN1", + "binFields": [ + { + "name": "u", + "dtype": "float32" + }, + { + "name": "v", + "dtype": "float32" + }, + { + "name": "speed", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 2105352, + "noData": -9999.0, + "semantics": "VELOCITY", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 263169, + "sha256": "14e745cea79d2cb85b66ef8fc5e19c28df8f912fb975f9281becc2ce6562e597" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 2105352, + "sha256": "a144307fdfd1172b6f2da8af0dc50ff27a4444da3989149c9bec2d1d73c55f21" + } + } +} diff --git a/integration/particle-v3-fields/tide-real/2026090902/validity.u8 b/integration/particle-v3-fields/tide-real/2026090902/validity.u8 new file mode 100644 index 000000000..1d199ebc5 Binary files /dev/null and b/integration/particle-v3-fields/tide-real/2026090902/validity.u8 differ diff --git a/integration/particle-v3-fields/tide-real/2026090902/vector.f32 b/integration/particle-v3-fields/tide-real/2026090902/vector.f32 new file mode 100644 index 000000000..5a14440a2 Binary files /dev/null and b/integration/particle-v3-fields/tide-real/2026090902/vector.f32 differ diff --git a/integration/particle-v3-fields/tide-real/dataset.json b/integration/particle-v3-fields/tide-real/dataset.json new file mode 100644 index 000000000..47696b519 --- /dev/null +++ b/integration/particle-v3-fields/tide-real/dataset.json @@ -0,0 +1,26 @@ +{ + "schema": "maplibre-particle-test-dataset/v2", + "datasetId": "tide-real", + "sourceProvenance": "真实本地 WeatherV2 CMEMS WVECBIN1;仅 U/V VELOCITY", + "frames": [ + { + "label": "T0", + "path": "2026090901", + "metadata": "metadata.json", + "validity": "validity.u8", + "vector": "vector.f32" + }, + { + "label": "T1", + "path": "2026090902", + "metadata": "metadata.json", + "validity": "validity.u8", + "vector": "vector.f32" + } + ], + "expectedFieldValueSource": "VECTOR_MAGNITUDE", + "particleExpected": true, + "vectorSemantics": "VELOCITY", + "suggestedAdvectionScale": 1.0, + "说明": "离线 canonical fixture;Runtime 只接收 metadata 与本地 plane URI。" +} diff --git a/integration/particle-v3-fields/wave-real/2026090918/conversion_summary.json b/integration/particle-v3-fields/wave-real/2026090918/conversion_summary.json new file mode 100644 index 000000000..3d1c0e381 --- /dev/null +++ b/integration/particle-v3-fields/wave-real/2026090918/conversion_summary.json @@ -0,0 +1,82 @@ +{ + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "validTime": 1788976800000, + "generation": 1, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090918/waves_particles.bin", + "sourceSha256": "5fadc8c4b67c0e111903df93603436a2aca94f736e2cefde3ec3df1011263c14", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090918/waves_particles.bin.header.json", + "headerSha256": "cd7b3b2a8c508ae5b5a021496311613c7e82cf857587c20f8d74c0a12738f307", + "referenceTime": "2026090912", + "targetTime": "2026090918", + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "binMagic": "WAVEBIN1", + "binFields": [ + { + "name": "height_m", + "dtype": "float32", + "unit": "m" + }, + { + "name": "period_s", + "dtype": "float32", + "unit": "s" + }, + { + "name": "direction_from_degree", + "dtype": "float32", + "unit": "degree" + }, + { + "name": "unit_to_u", + "dtype": "float32" + }, + { + "name": "unit_to_v", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "e94dba4163df1468ede4cd6cdb720162328b0bdf3053a868855cb16a424bd3a6" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 1577988, + "sha256": "0824b72e3b737d22fcaf4ff3447b6737fe12cbaa2a44d233f5b1710f81586d57" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "640dfb7126db6117507fb864aebc2ecfb3dade2726ff0592e7c87d7a3e912276" + } + }, + "validCount": 295196, + "invalidCount": 99301 +} diff --git a/integration/particle-v3-fields/wave-real/2026090918/metadata.json b/integration/particle-v3-fields/wave-real/2026090918/metadata.json new file mode 100644 index 000000000..fbc63b804 --- /dev/null +++ b/integration/particle-v3-fields/wave-real/2026090918/metadata.json @@ -0,0 +1,106 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "wave-real", + "generation": 1, + "dataVersion": 1, + "validTime": 1788976800000, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "grid": { + "width": 513, + "height": 769 + }, + "validity": { + "encoding": "uint8", + "byteLength": 394497 + }, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090918/waves_particles.bin", + "sourceSha256": "5fadc8c4b67c0e111903df93603436a2aca94f736e2cefde3ec3df1011263c14", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090918/waves_particles.bin.header.json", + "headerSha256": "cd7b3b2a8c508ae5b5a021496311613c7e82cf857587c20f8d74c0a12738f307", + "referenceTime": "2026090912", + "targetTime": "2026090918", + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "binMagic": "WAVEBIN1", + "binFields": [ + { + "name": "height_m", + "dtype": "float32", + "unit": "m" + }, + { + "name": "period_s", + "dtype": "float32", + "unit": "s" + }, + { + "name": "direction_from_degree", + "dtype": "float32", + "unit": "degree" + }, + { + "name": "unit_to_u", + "dtype": "float32" + }, + { + "name": "unit_to_v", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "scalar": { + "encoding": "float32-le", + "byteLength": 1577988, + "noData": -9999.0 + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 3155976, + "noData": -9999.0, + "semantics": "DIRECTION", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "e94dba4163df1468ede4cd6cdb720162328b0bdf3053a868855cb16a424bd3a6" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 1577988, + "sha256": "0824b72e3b737d22fcaf4ff3447b6737fe12cbaa2a44d233f5b1710f81586d57" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "640dfb7126db6117507fb864aebc2ecfb3dade2726ff0592e7c87d7a3e912276" + } + } +} diff --git a/integration/particle-v3-fields/wave-real/2026090918/scalar.f32 b/integration/particle-v3-fields/wave-real/2026090918/scalar.f32 new file mode 100644 index 000000000..0fb376c65 Binary files /dev/null and b/integration/particle-v3-fields/wave-real/2026090918/scalar.f32 differ diff --git a/integration/particle-v3-fields/wave-real/2026090918/validity.u8 b/integration/particle-v3-fields/wave-real/2026090918/validity.u8 new file mode 100644 index 000000000..f6b2a2287 Binary files /dev/null and b/integration/particle-v3-fields/wave-real/2026090918/validity.u8 differ diff --git a/integration/particle-v3-fields/wave-real/2026090918/vector.f32 b/integration/particle-v3-fields/wave-real/2026090918/vector.f32 new file mode 100644 index 000000000..f9e0ab92c Binary files /dev/null and b/integration/particle-v3-fields/wave-real/2026090918/vector.f32 differ diff --git a/integration/particle-v3-fields/wave-real/2026090919/conversion_summary.json b/integration/particle-v3-fields/wave-real/2026090919/conversion_summary.json new file mode 100644 index 000000000..953772a7b --- /dev/null +++ b/integration/particle-v3-fields/wave-real/2026090919/conversion_summary.json @@ -0,0 +1,82 @@ +{ + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "validTime": 1788980400000, + "generation": 2, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090919/waves_particles.bin", + "sourceSha256": "c084c8ae4b9e601007b3a852c25ac3607c01a4de69dcd87fc875116eccf9d633", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090919/waves_particles.bin.header.json", + "headerSha256": "72b3d79880b2ccd7055b5d1011401cba7938f7e7eb12e81ea6f259319626f421", + "referenceTime": "2026090912", + "targetTime": "2026090919", + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "binMagic": "WAVEBIN1", + "binFields": [ + { + "name": "height_m", + "dtype": "float32", + "unit": "m" + }, + { + "name": "period_s", + "dtype": "float32", + "unit": "s" + }, + { + "name": "direction_from_degree", + "dtype": "float32", + "unit": "degree" + }, + { + "name": "unit_to_u", + "dtype": "float32" + }, + { + "name": "unit_to_v", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "e94dba4163df1468ede4cd6cdb720162328b0bdf3053a868855cb16a424bd3a6" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 1577988, + "sha256": "99a70bc6e07311536844c9b6b481281e05af5f3b1cd1961e2aeb4696a614f789" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "7871320366889daedd4cc4b057f97d321947a29813dd9b55d5df4be1416022ea" + } + }, + "validCount": 295196, + "invalidCount": 99301 +} diff --git a/integration/particle-v3-fields/wave-real/2026090919/metadata.json b/integration/particle-v3-fields/wave-real/2026090919/metadata.json new file mode 100644 index 000000000..07a2d1ccc --- /dev/null +++ b/integration/particle-v3-fields/wave-real/2026090919/metadata.json @@ -0,0 +1,106 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "wave-real", + "generation": 2, + "dataVersion": 2, + "validTime": 1788980400000, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "grid": { + "width": 513, + "height": 769 + }, + "validity": { + "encoding": "uint8", + "byteLength": 394497 + }, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090919/waves_particles.bin", + "sourceSha256": "c084c8ae4b9e601007b3a852c25ac3607c01a4de69dcd87fc875116eccf9d633", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090919/waves_particles.bin.header.json", + "headerSha256": "72b3d79880b2ccd7055b5d1011401cba7938f7e7eb12e81ea6f259319626f421", + "referenceTime": "2026090912", + "targetTime": "2026090919", + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "binMagic": "WAVEBIN1", + "binFields": [ + { + "name": "height_m", + "dtype": "float32", + "unit": "m" + }, + { + "name": "period_s", + "dtype": "float32", + "unit": "s" + }, + { + "name": "direction_from_degree", + "dtype": "float32", + "unit": "degree" + }, + { + "name": "unit_to_u", + "dtype": "float32" + }, + { + "name": "unit_to_v", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "scalar": { + "encoding": "float32-le", + "byteLength": 1577988, + "noData": -9999.0 + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 3155976, + "noData": -9999.0, + "semantics": "DIRECTION", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "e94dba4163df1468ede4cd6cdb720162328b0bdf3053a868855cb16a424bd3a6" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 1577988, + "sha256": "99a70bc6e07311536844c9b6b481281e05af5f3b1cd1961e2aeb4696a614f789" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "7871320366889daedd4cc4b057f97d321947a29813dd9b55d5df4be1416022ea" + } + } +} diff --git a/integration/particle-v3-fields/wave-real/2026090919/scalar.f32 b/integration/particle-v3-fields/wave-real/2026090919/scalar.f32 new file mode 100644 index 000000000..e25d74ae0 Binary files /dev/null and b/integration/particle-v3-fields/wave-real/2026090919/scalar.f32 differ diff --git a/integration/particle-v3-fields/wave-real/2026090919/validity.u8 b/integration/particle-v3-fields/wave-real/2026090919/validity.u8 new file mode 100644 index 000000000..f6b2a2287 Binary files /dev/null and b/integration/particle-v3-fields/wave-real/2026090919/validity.u8 differ diff --git a/integration/particle-v3-fields/wave-real/2026090919/vector.f32 b/integration/particle-v3-fields/wave-real/2026090919/vector.f32 new file mode 100644 index 000000000..e2219c860 Binary files /dev/null and b/integration/particle-v3-fields/wave-real/2026090919/vector.f32 differ diff --git a/integration/particle-v3-fields/wave-real/dataset.json b/integration/particle-v3-fields/wave-real/dataset.json new file mode 100644 index 000000000..12a98c98e --- /dev/null +++ b/integration/particle-v3-fields/wave-real/dataset.json @@ -0,0 +1,28 @@ +{ + "schema": "maplibre-particle-test-dataset/v2", + "datasetId": "wave-real", + "sourceProvenance": "真实本地 WeatherV2 WAVEBIN1;height_m + 正规化 unit_to_u/v,period 不进入 canonical", + "frames": [ + { + "label": "T0", + "path": "2026090918", + "metadata": "metadata.json", + "validity": "validity.u8", + "scalar": "scalar.f32", + "vector": "vector.f32" + }, + { + "label": "T1", + "path": "2026090919", + "metadata": "metadata.json", + "validity": "validity.u8", + "scalar": "scalar.f32", + "vector": "vector.f32" + } + ], + "expectedFieldValueSource": "SCALAR", + "particleExpected": true, + "vectorSemantics": "DIRECTION", + "suggestedAdvectionScale": 1.75, + "说明": "离线 canonical fixture;Runtime 只接收 metadata 与本地 plane URI。" +} diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010100/conversion_summary.json b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/conversion_summary.json new file mode 100644 index 000000000..6be5e7c89 --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/conversion_summary.json @@ -0,0 +1,38 @@ +{ + "grid": { + "width": 6, + "height": 4 + }, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "validTime": 1767225600000, + "generation": 1, + "source": { + "kind": "synthetic independence oracle", + "notReal": true, + "targetTime": "2026010100" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "09484872f71b85d73159e9ce51ef4c9d0abc8620f9e2c4f1155fd8b33acf2a61" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 192, + "sha256": "e34af2311cfe54f9a33aa51b33021eee2df99622903a913aeca1bc4ce1d941a7" + } + }, + "validCount": 24, + "invalidCount": 0 +} diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010100/metadata.json b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/metadata.json new file mode 100644 index 000000000..b96dd99db --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/metadata.json @@ -0,0 +1,62 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "wave-synthetic-independence", + "generation": 1, + "dataVersion": 1, + "validTime": 1767225600000, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "grid": { + "width": 6, + "height": 4 + }, + "validity": { + "encoding": "uint8", + "byteLength": 24 + }, + "source": { + "kind": "synthetic independence oracle", + "notReal": true, + "targetTime": "2026010100" + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "scalar": { + "encoding": "float32-le", + "byteLength": 96, + "noData": -9999.0 + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 192, + "noData": -9999.0, + "semantics": "DIRECTION", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "09484872f71b85d73159e9ce51ef4c9d0abc8620f9e2c4f1155fd8b33acf2a61" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 192, + "sha256": "e34af2311cfe54f9a33aa51b33021eee2df99622903a913aeca1bc4ce1d941a7" + } + } +} diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010100/scalar.f32 b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/scalar.f32 new file mode 100644 index 000000000..7d5c9df7c Binary files /dev/null and b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/scalar.f32 differ diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010100/validity.u8 b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/validity.u8 new file mode 100644 index 000000000..e03959b11 --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/validity.u8 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010100/vector.f32 b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/vector.f32 new file mode 100644 index 000000000..7a52e7c9d Binary files /dev/null and b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/vector.f32 differ diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010106/conversion_summary.json b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/conversion_summary.json new file mode 100644 index 000000000..9a5f9180f --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/conversion_summary.json @@ -0,0 +1,38 @@ +{ + "grid": { + "width": 6, + "height": 4 + }, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "validTime": 1767247200000, + "generation": 2, + "source": { + "kind": "synthetic independence oracle", + "notReal": true, + "targetTime": "2026010106" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "74a93cc48d3875f0abd73b1ee14cc0f1a7293cd7dc7ef716a15725a162588855" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 192, + "sha256": "e34af2311cfe54f9a33aa51b33021eee2df99622903a913aeca1bc4ce1d941a7" + } + }, + "validCount": 24, + "invalidCount": 0 +} diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010106/metadata.json b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/metadata.json new file mode 100644 index 000000000..676476efe --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/metadata.json @@ -0,0 +1,62 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "wave-synthetic-independence", + "generation": 2, + "dataVersion": 2, + "validTime": 1767247200000, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "grid": { + "width": 6, + "height": 4 + }, + "validity": { + "encoding": "uint8", + "byteLength": 24 + }, + "source": { + "kind": "synthetic independence oracle", + "notReal": true, + "targetTime": "2026010106" + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "scalar": { + "encoding": "float32-le", + "byteLength": 96, + "noData": -9999.0 + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 192, + "noData": -9999.0, + "semantics": "DIRECTION", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "74a93cc48d3875f0abd73b1ee14cc0f1a7293cd7dc7ef716a15725a162588855" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 192, + "sha256": "e34af2311cfe54f9a33aa51b33021eee2df99622903a913aeca1bc4ce1d941a7" + } + } +} diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010106/scalar.f32 b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/scalar.f32 new file mode 100644 index 000000000..99097bd2d Binary files /dev/null and b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/scalar.f32 differ diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010106/validity.u8 b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/validity.u8 new file mode 100644 index 000000000..e03959b11 --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/validity.u8 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010106/vector.f32 b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/vector.f32 new file mode 100644 index 000000000..7a52e7c9d Binary files /dev/null and b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/vector.f32 differ diff --git a/integration/particle-v3-fields/wave-synthetic-independence/dataset.json b/integration/particle-v3-fields/wave-synthetic-independence/dataset.json new file mode 100644 index 000000000..b427c93ba --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/dataset.json @@ -0,0 +1,28 @@ +{ + "schema": "maplibre-particle-test-dataset/v2", + "datasetId": "wave-synthetic-independence", + "sourceProvenance": "固定 synthetic;height 与 direction 独立", + "frames": [ + { + "label": "T0", + "path": "2026010100", + "metadata": "metadata.json", + "validity": "validity.u8", + "scalar": "scalar.f32", + "vector": "vector.f32" + }, + { + "label": "T1", + "path": "2026010106", + "metadata": "metadata.json", + "validity": "validity.u8", + "scalar": "scalar.f32", + "vector": "vector.f32" + } + ], + "expectedFieldValueSource": "SCALAR", + "particleExpected": true, + "vectorSemantics": "DIRECTION", + "suggestedAdvectionScale": 2.5, + "说明": "离线 canonical fixture;Runtime 只接收 metadata 与本地 plane URI。" +} diff --git a/integration/particle-v3-fields/wind-real/2026080700/conversion_summary.json b/integration/particle-v3-fields/wind-real/2026080700/conversion_summary.json new file mode 100644 index 000000000..d4cb05602 --- /dev/null +++ b/integration/particle-v3-fields/wind-real/2026080700/conversion_summary.json @@ -0,0 +1,35 @@ +{ + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "validTime": 1786060800000, + "generation": 1786060800000, + "source": { + "kind": "WeatherV2 ECMWF HRES WVECBIN1 canonical v1 upgraded offline", + "referenceTime": "2026080600", + "targetTime": "2026080700", + "originalPayloadSha256": "363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283", + "validityDerivation": "finite U/V and both components != -9999.0" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "3100f7cc4f6b7357e857ec10f159c43a092cfaad8eed6327043406579f5808ee" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283" + } + }, + "validCount": 394497, + "invalidCount": 0 +} diff --git a/integration/particle-v3-fields/wind-real/2026080700/metadata.json b/integration/particle-v3-fields/wind-real/2026080700/metadata.json new file mode 100644 index 000000000..cfbd9c707 --- /dev/null +++ b/integration/particle-v3-fields/wind-real/2026080700/metadata.json @@ -0,0 +1,54 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "wind-real", + "generation": 1786060800000, + "dataVersion": 1786060800000, + "validTime": 1786060800000, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "grid": { + "width": 513, + "height": 769 + }, + "validity": { + "encoding": "uint8", + "byteLength": 394497 + }, + "source": { + "kind": "WeatherV2 ECMWF HRES WVECBIN1 canonical v1 upgraded offline", + "referenceTime": "2026080600", + "targetTime": "2026080700", + "originalPayloadSha256": "363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283", + "validityDerivation": "finite U/V and both components != -9999.0" + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 3155976, + "noData": -9999.0, + "semantics": "VELOCITY", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "3100f7cc4f6b7357e857ec10f159c43a092cfaad8eed6327043406579f5808ee" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283" + } + } +} diff --git a/integration/particle-v3-fields/wind-real/2026080700/validity.u8 b/integration/particle-v3-fields/wind-real/2026080700/validity.u8 new file mode 100644 index 000000000..b038f6342 --- /dev/null +++ b/integration/particle-v3-fields/wind-real/2026080700/validity.u8 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/particle-v3-fields/wind-real/2026080700/vector.f32 b/integration/particle-v3-fields/wind-real/2026080700/vector.f32 new file mode 100644 index 000000000..21fb3645d Binary files /dev/null and b/integration/particle-v3-fields/wind-real/2026080700/vector.f32 differ diff --git a/integration/particle-v3-fields/wind-real/2026080706/conversion_summary.json b/integration/particle-v3-fields/wind-real/2026080706/conversion_summary.json new file mode 100644 index 000000000..19bbde179 --- /dev/null +++ b/integration/particle-v3-fields/wind-real/2026080706/conversion_summary.json @@ -0,0 +1,35 @@ +{ + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "validTime": 1786082400000, + "generation": 1786082400000, + "source": { + "kind": "WeatherV2 ECMWF HRES WVECBIN1 canonical v1 upgraded offline", + "referenceTime": "2026080600", + "targetTime": "2026080706", + "originalPayloadSha256": "ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451", + "validityDerivation": "finite U/V and both components != -9999.0" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "3100f7cc4f6b7357e857ec10f159c43a092cfaad8eed6327043406579f5808ee" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451" + } + }, + "validCount": 394497, + "invalidCount": 0 +} diff --git a/integration/particle-v3-fields/wind-real/2026080706/metadata.json b/integration/particle-v3-fields/wind-real/2026080706/metadata.json new file mode 100644 index 000000000..82acd38ec --- /dev/null +++ b/integration/particle-v3-fields/wind-real/2026080706/metadata.json @@ -0,0 +1,54 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "wind-real", + "generation": 1786082400000, + "dataVersion": 1786082400000, + "validTime": 1786082400000, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "grid": { + "width": 513, + "height": 769 + }, + "validity": { + "encoding": "uint8", + "byteLength": 394497 + }, + "source": { + "kind": "WeatherV2 ECMWF HRES WVECBIN1 canonical v1 upgraded offline", + "referenceTime": "2026080600", + "targetTime": "2026080706", + "originalPayloadSha256": "ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451", + "validityDerivation": "finite U/V and both components != -9999.0" + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 3155976, + "noData": -9999.0, + "semantics": "VELOCITY", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "3100f7cc4f6b7357e857ec10f159c43a092cfaad8eed6327043406579f5808ee" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451" + } + } +} diff --git a/integration/particle-v3-fields/wind-real/2026080706/validity.u8 b/integration/particle-v3-fields/wind-real/2026080706/validity.u8 new file mode 100644 index 000000000..b038f6342 --- /dev/null +++ b/integration/particle-v3-fields/wind-real/2026080706/validity.u8 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/particle-v3-fields/wind-real/2026080706/vector.f32 b/integration/particle-v3-fields/wind-real/2026080706/vector.f32 new file mode 100644 index 000000000..5f143c03f Binary files /dev/null and b/integration/particle-v3-fields/wind-real/2026080706/vector.f32 differ diff --git a/integration/particle-v3-fields/wind-real/dataset.json b/integration/particle-v3-fields/wind-real/dataset.json new file mode 100644 index 000000000..1f9dee61d --- /dev/null +++ b/integration/particle-v3-fields/wind-real/dataset.json @@ -0,0 +1,26 @@ +{ + "schema": "maplibre-particle-test-dataset/v2", + "datasetId": "wind-real", + "sourceProvenance": "真实本地 WeatherV2 Wind canonical v1 输出;validity 由 U/V noData 与有限性派生", + "frames": [ + { + "label": "T0", + "path": "2026080700", + "metadata": "metadata.json", + "validity": "validity.u8", + "vector": "vector.f32" + }, + { + "label": "T1", + "path": "2026080706", + "metadata": "metadata.json", + "validity": "validity.u8", + "vector": "vector.f32" + } + ], + "expectedFieldValueSource": "VECTOR_MAGNITUDE", + "particleExpected": true, + "vectorSemantics": "VELOCITY", + "suggestedAdvectionScale": 1.0, + "说明": "离线 canonical fixture;Runtime 只接收 metadata 与本地 plane URI。" +} diff --git a/integration/react-native/MapLibreParticleRNTestHost/App.tsx b/integration/react-native/MapLibreParticleRNTestHost/App.tsx index 827e76f11..6e4dd3596 100644 --- a/integration/react-native/MapLibreParticleRNTestHost/App.tsx +++ b/integration/react-native/MapLibreParticleRNTestHost/App.tsx @@ -1,15 +1,15 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native"; import { Camera, Map } from "@maplibre/maplibre-react-native"; -import { WeatherRuntimeBridge, type ColorStop, type RuntimeSnapshot, type TargetFps } from "@maplibre-particle/weather-runtime-bridge"; -import { materializeRealWeatherFrames, realWeatherFrames } from "./src/realWeatherDataset"; +import { FieldRuntime, type FieldColorStop, type RuntimeSnapshot, type TargetFps } from "@navsea/maplibre-particle-rn"; +import { fieldDatasets, materializeV3Frame, type V3Dataset } from "./src/fieldDatasets"; import { weatherTestStyle } from "./src/weatherStyle"; const initialSnapshot: RuntimeSnapshot = { state: "WAITING_FOR_FIELDS", playbackState: "PAUSE", targetFps: "AUTO", disposed: false }; const initialVisual = { count: "4096", width: "1.5", length: "0.012", opacity: "0.9", trailOpacity: "0.8", trailFade: "0.94", weatherOpacity: "0.62" }; -const frozenStops: ColorStop[] = [ - { speedMps: 0, red: 0.08, green: 0.2, blue: 0.8, alpha: 0.72 }, { speedMps: 8, red: 0, green: 0.85, blue: 0.9, alpha: 0.72 }, - { speedMps: 16, red: 1, green: 0.85, blue: 0.05, alpha: 0.72 }, { speedMps: 30, red: 0.9, green: 0.08, blue: 0.08, alpha: 0.72 }, +const frozenStops: FieldColorStop[] = [ + { value: 0, red: 0.08, green: 0.2, blue: 0.8, alpha: 0.72 }, { value: 8, red: 0, green: 0.85, blue: 0.9, alpha: 0.72 }, + { value: 16, red: 1, green: 0.85, blue: 0.05, alpha: 0.72 }, { value: 30, red: 0.9, green: 0.08, blue: 0.08, alpha: 0.72 }, ]; export default function App() { @@ -17,22 +17,23 @@ export default function App() { const [snapshot, setSnapshot] = useState(initialSnapshot); const [result, setResult] = useState("Style is loading"); const [basemap, setBasemap] = useState("LOADING"); - const [frames, setFrames] = useState>({ T0: "PENDING", T1: "PENDING", T2: "PENDING", T3: "PENDING" }); - const [transport, setTransport] = useState("PENDING: FILE_REF / FLOAT32_LE / INTERLEAVED_UV"); + const [frames, setFrames] = useState>({ T0: "PENDING", T1: "PENDING" }); + const [dataset, setDataset] = useState(fieldDatasets[0]); + const [transport, setTransport] = useState("PENDING: V2 FILE_REF / FLOAT32_LE"); const [weatherVisible, setWeatherVisible] = useState(true); const [particleVisible, setParticleVisible] = useState(true); const [visual, setVisual] = useState(initialVisual); - const refresh = useCallback(async (tag: number) => setSnapshot(await WeatherRuntimeBridge.getState(tag)), []); + const refresh = useCallback(async (tag: number) => setSnapshot(await FieldRuntime.getState(tag)), []); const attach = useCallback(async (tag: number) => { - try { await WeatherRuntimeBridge.attach(tag); await refresh(tag); setReactTag(tag); setResult("WeatherRuntime attached; style ready"); } + try { await FieldRuntime.attach(tag); await refresh(tag); setReactTag(tag); setResult("FieldRuntime attached; style ready"); } catch (error) { setResult(`style/attach error: ${String(error)}`); } }, [refresh]); const onStyleReady = useCallback((event: unknown) => { const tag = findNodeHandle((event as { target?: unknown }).target as never); if (tag != null) void attach(tag); }, [attach]); - useEffect(() => () => { if (reactTag != null) void WeatherRuntimeBridge.detach(reactTag).catch(() => undefined); }, [reactTag]); + useEffect(() => () => { if (reactTag != null) void FieldRuntime.detach(reactTag).catch(() => undefined); }, [reactTag]); const command = useCallback(async (name: string, operation: (tag: number) => Promise) => { if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready"); @@ -40,46 +41,47 @@ export default function App() { catch (error) { setResult(`${name}: ${String(error)}`); } }, [reactTag, refresh]); - const loadRealWeather = useCallback(async () => { + const loadDataset = useCallback(async () => { if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready"); - const expected = ["ACCEPTED_PENDING_INITIAL_WINDOW", "INITIAL_WINDOW_READY", "ACCEPTED_NEXT_SNAPSHOT", "ACCEPTED_NEXT_SNAPSHOT"]; + const expected = ["ACCEPTED_PENDING_INITIAL_WINDOW", "INITIAL_WINDOW_READY"]; try { - setTransport("LOADING: materializing Expo Asset local file:// URIs"); setFrames({ T0: "PENDING", T1: "PENDING", T2: "PENDING", T3: "PENDING" }); - if (await WeatherRuntimeBridge.clearWindFields(reactTag) !== "ACCEPTED") throw new Error("clearWindFields was rejected"); - const localFrames = await materializeRealWeatherFrames(); + setTransport("LOADING: V2 metadata + validity/scalar/vector local file:// URIs"); setFrames({ T0: "PENDING", T1: "PENDING" }); + if (await FieldRuntime.clearFields(reactTag) !== "ACCEPTED") throw new Error("clearFields was rejected"); + await FieldRuntime.setFieldValueSource(reactTag, dataset.fieldValueSource); await FieldRuntime.setParticleVisible(reactTag, dataset.particle); await FieldRuntime.setParticleAdvectionScale(reactTag, dataset.advectionScale); + const localFrames = await Promise.all(dataset.frames.map(materializeV3Frame)); for (let index = 0; index < localFrames.length; index += 1) { - const frame = localFrames[index]; const submit = await WeatherRuntimeBridge.submitWindField(reactTag, frame.metadataJson, frame.payloadFileUri); + const frame = localFrames[index]; const submit = await FieldRuntime.submitField(reactTag, frame.metadataJson, frame.payloadRefs); setFrames((current) => ({ ...current, [frame.label]: submit })); if (submit !== expected[index]) throw new Error(`${frame.label} expected ${expected[index]}, got ${submit}`); } - const native = await WeatherRuntimeBridge.getState(reactTag); setSnapshot(native); + const native = await FieldRuntime.getState(reactTag); setSnapshot(native); if (native.state !== "READY") throw new Error(`Native WeatherRuntime expected READY, got ${native.state}`); - setTransport("READY: FILE_REF / FLOAT32_LE / INTERLEAVED_UV"); setResult("T0-T3 accepted in strict producer timestamp order; Native runtime READY"); - } catch (error) { setTransport(`ERROR: ${String(error)}`); setResult(`LOAD REAL WEATHER T0-T3: ${String(error)}`); } - }, [reactTag]); + setTransport("READY: V2 FILE_REF / FLOAT32_LE / shared validity"); setResult(`${dataset.title}: T0/T1 accepted; FieldRuntime READY`); + } catch (error) { setTransport(`ERROR: ${String(error)}`); setResult(`LOAD ${dataset.id}: ${String(error)}`); } + }, [reactTag, dataset]); const updateVisual = useCallback((key: keyof typeof initialVisual, value: string) => setVisual((current) => ({ ...current, [key]: value })), []); const applyVisual = useCallback(() => void command("particle visual", async (tag) => { const results = await Promise.all([ - WeatherRuntimeBridge.setParticleCount(tag, Number(visual.count)), WeatherRuntimeBridge.setParticleLineWidth(tag, Number(visual.width)), WeatherRuntimeBridge.setParticleLength(tag, Number(visual.length)), WeatherRuntimeBridge.setParticleOpacity(tag, Number(visual.opacity)), WeatherRuntimeBridge.setTrailOpacity(tag, Number(visual.trailOpacity)), WeatherRuntimeBridge.setTrailFade(tag, Number(visual.trailFade)), WeatherRuntimeBridge.setWeatherColorOpacity(tag, Number(visual.weatherOpacity)), + FieldRuntime.setParticleCount(tag, Number(visual.count)), FieldRuntime.setParticleLineWidth(tag, Number(visual.width)), FieldRuntime.setParticleLength(tag, Number(visual.length)), FieldRuntime.setParticleOpacity(tag, Number(visual.opacity)), FieldRuntime.setTrailOpacity(tag, Number(visual.trailOpacity)), FieldRuntime.setTrailFade(tag, Number(visual.trailFade)), FieldRuntime.setFieldOpacity(tag, Number(visual.weatherOpacity)), ]); if (results.some((value) => value !== "ACCEPTED")) throw new Error(results.join(", ")); return "ACCEPTED"; }), [command, visual]); const controls = useMemo(() => [ - ["PLAY", (tag: number) => WeatherRuntimeBridge.setPlaybackState(tag, "PLAY")], ["PAUSE", (tag: number) => WeatherRuntimeBridge.setPlaybackState(tag, "PAUSE")], ["HALF", (tag: number) => WeatherRuntimeBridge.setPlaybackSpeed(tag, "HALF")], ["NORMAL", (tag: number) => WeatherRuntimeBridge.setPlaybackSpeed(tag, "NORMAL")], ["DOUBLE", (tag: number) => WeatherRuntimeBridge.setPlaybackSpeed(tag, "DOUBLE")], - ["FPS AUTO", (tag: number) => WeatherRuntimeBridge.setTargetFps(tag, "AUTO" as TargetFps)], ["FPS 30", (tag: number) => WeatherRuntimeBridge.setTargetFps(tag, "FPS_30" as TargetFps)], ["FPS 60", (tag: number) => WeatherRuntimeBridge.setTargetFps(tag, "FPS_60" as TargetFps)], ["Weather stops", (tag: number) => WeatherRuntimeBridge.setWeatherColorStops(tag, frozenStops)], ["Particle color", (tag: number) => WeatherRuntimeBridge.setParticleColor(tag, 1, 1, 1, 0.95)], ["Trail color", (tag: number) => WeatherRuntimeBridge.setTrailColor(tag, 0.35, 0.85, 1, 0.8)], + ["PLAY", (tag: number) => FieldRuntime.setPlaybackState(tag, "PLAY")], ["PAUSE", (tag: number) => FieldRuntime.setPlaybackState(tag, "PAUSE")], ["HALF", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "HALF")], ["NORMAL", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "NORMAL")], ["DOUBLE", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "DOUBLE")], + ["FPS AUTO", (tag: number) => FieldRuntime.setTargetFps(tag, "AUTO" as TargetFps)], ["FPS 30", (tag: number) => FieldRuntime.setTargetFps(tag, "FPS_30" as TargetFps)], ["FPS 60", (tag: number) => FieldRuntime.setTargetFps(tag, "FPS_60" as TargetFps)], ["Field stops", (tag: number) => FieldRuntime.setFieldColorStops(tag, frozenStops)], ["Particle color", (tag: number) => FieldRuntime.setParticleColor(tag, 1, 1, 1, 0.95)], ["Trail color", (tag: number) => FieldRuntime.setTrailColor(tag, 0.35, 0.85, 1, 0.8)], ] as const, []); return setBasemap("READY")} onDidFailLoadingMap={() => { setBasemap("ERROR"); setResult("map loading error"); }}> - Basemap: GSI {basemap}Dataset: ECMWF HRES 20260807 · 513×769 · 3.01 MiB/frameRuntime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps}REAL DATA TRANSPORT = {transport} - {realWeatherFrames.map((frame) => {frame.label} {frame.cycle}: {frames[frame.label]})} - {result}