13 KiB
MapLibre 粒子 React Native 使用说明 v1
本文面向 NavSea / React Native 调用侧,说明如何通过正式包 @navsea/maplibre-particle-rn 使用 MapLibre Particle Android 能力。本文描述的是当前已验证的 13.4.1-particle.1 契约,不是 Weather Provider、下载服务或天气业务层设计。
1. 使用模型
React Native App 只直接依赖正式包:
@navsea/maplibre-particle-rn@13.4.1-particle.1
Android Native 由该包的 Expo/Gradle 接线从正式 Maven 仓库解析:
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1
正式 Maven 仓库:https://pbf.jpcn.ccwu.cc/maven。
包会复用唯一的 @maplibre/maplibre-react-native@11.3.6 peer dependency,并把其 OpenGL Native SDK 请求替换为 Particle AAR。App 不应再并行引入第二份 MapLibre Native AAR、Maven Local AAR、源码路径 bridge 或 MapLibre Native 源码树。
以后升级粒子版本的目标流程是:升级 @navsea/maplibre-particle-rn 版本 → npm 安装 → Gradle 取得对应正式 AAR → 重新编译 App。
2. Expo / React Native 配置
app.json / app config 中加入正式 plugin:
{
"expo": {
"plugins": ["@navsea/maplibre-particle-rn/app.plugin"]
}
}
React 侧统一从正式包导入,不直接从官方 MLRN 或旧 bridge 导入:
import {
Camera,
Map,
WeatherRuntime,
createParticleWeatherStyle,
particleWeatherIds,
} from "@navsea/maplibre-particle-rn";
WeatherColor 与 RasterParticle 当前是 MapLibre Core style layer,不是 React 组件。测试或独立页面可直接使用 createParticleWeatherStyle();NavSea 有自己的正式 style 时,应把同样的 raster-array source、weather-color layer 和 raster-particle layer 合入宿主 style,而不是在 MapView 外再画一层 Canvas/WebView。
3. Map 生命周期与 WeatherRuntime
必须在 Map style 已完成加载后 attach Native Runtime,并在页面销毁时 detach:
const [reactTag, setReactTag] = useState<number | null>(null);
const onStyleReady = useCallback(async (event: unknown) => {
const tag = findNodeHandle((event as { target?: unknown }).target as never);
if (tag == null) return;
await WeatherRuntime.attach(tag);
setReactTag(tag);
}, []);
useEffect(() => () => {
if (reactTag != null) void WeatherRuntime.detach(reactTag);
}, [reactTag]);
典型 Map:
<Map mapStyle={createParticleWeatherStyle()} onDidFinishLoadingStyle={onStyleReady}>
<Camera initialViewState={{ center: [135, 32], zoom: 3.9 }} />
</Map>
WeatherRuntime.getState(tag) 返回 Native/Core 权威状态。至少两帧兼容的合法 Field 建立 initial window 后,Runtime 才进入 READY;React 不自行维护第二套 T0/T1/MIX Runtime Window。
4. 显示数据的总体结构
这是本接口最重要的部分。每一个时间片不是图片,也不是粒子坐标列表,而是一张规则二维风矢量场。一个 Field 由两部分组成:
metadataJson
+
local file:// binary payload
其中 metadata 描述时间、范围、网格和编码;binary payload 只保存每个网格点的 U/V 风矢量。
当前正式 schema:
maplibre-particle-field/v1
固定语义:
二维规则 grid
origin = north-west
row order = north-to-south
column order = west-to-east
storage = row-major
layout = interleaved-uv
encoding = float32 little-endian
U = eastward-positive,单位 m/s
V = northward-positive,单位 m/s
每 cell = 2 × float32 = 8 bytes
MapLibre 接收的是这张向量场;WeatherColor 根据 U/V 得到色场,RasterParticle 从同一 U/V Field 采样并推进 GPU 粒子。React 不需要提前生成每个粒子的经纬度或轨迹。
5. metadataJson 结构
当前真实 WeatherV2 T0 metadata 的核心结构如下:
{
"schema": "maplibre-particle-field/v1",
"fieldId": "ecmwf-hres-wind-2026080700",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"generation": 1786060800000,
"validTime": 1786060800000,
"dataVersion": 1786060800000,
"bounds": { "west": 112.5, "south": 0.0, "east": 157.5, "north": 55.77657301866769 },
"grid": { "width": 513, "height": 769 },
"vector": {
"layout": "interleaved-uv",
"encoding": "float32-le",
"noData": -9999.0,
"unit": "m/s",
"origin": "north-west",
"rowOrder": "north-to-south",
"columnOrder": "west-to-east",
"uPositive": "east",
"vPositive": "north"
},
"payload": { "byteLength": 3155976 }
}
字段说明:
| 字段 | 含义 / 当前约束 |
|---|---|
schema |
必须为 maplibre-particle-field/v1 |
fieldId |
Producer 提供的非空 Field 标识 |
fieldType |
当前实现必须为 WIND |
displayDomain |
当前实现必须为 GLOBAL |
generation |
非 0 uint64;Producer 负责赋值,Core 不猜版本 |
validTime |
非 0 uint64;同一序列使用同一时间基准并严格递增;当前 WeatherV2 使用 Unix 毫秒 |
dataVersion |
可选 uint64,用于数据版本 / identity;当前真实 fixture 提供 |
bounds |
west < east、south < north,描述规则网格覆盖范围 |
grid.width/height |
大于 0;总 cell 数=width × height |
vector.layout |
只支持 interleaved-uv |
vector.encoding |
只支持 float32-le |
vector.noData |
必须为有限数值;当前 fixture 为 -9999.0 |
payload.byteLength |
必须严格等于 width × height × 8 |
stride |
可省略;若提供,当前 interleaved UV 必须为 8 bytes |
真实 fixture 还带 unit/origin/rowOrder/columnOrder/uPositive/vPositive、payload.sha256、source 等审计字段。当前 Native parser 强制读取的是 schema、identity、时间、bounds、grid、layout、encoding、noData 与 byteLength;额外审计字段不会替代上述 canonical 语义。
注意:当前 payload.sha256 可用于文件生产/发布审计,但 LocalParticleFieldFeed 目前只读取 payload.byteLength,不要把 sha256 写成 Core 已强制校验的安全门槛。
6. binary payload 的精确排列
二维索引:
cell = y * width + x
其中 x=0 是最西列,y=0 是最北行。payload 按 row-major 连续存储:先完整保存最北一行,再向南逐行保存。
每个 cell 占 8 bytes:
byteOffset = cell * 8
[byteOffset + 0 .. +3] = U,float32 little-endian
[byteOffset + 4 .. +7] = V,float32 little-endian
等价的 float 序列是:
U0, V0, U1, V1, U2, V2, ...
也就是:
velocity[2 * cell] = U
velocity[2 * cell + 1] = V
U>0 表示向东,U<0 表示向西;V>0 表示向北,V<0 表示向南。noData 只要出现在某 cell 的 U 或 V 任一分量,该 cell 就标记为无效。NaN/Infinity 不是 noData:当前 Adapter 会直接拒绝整张 Field。
真实 WeatherV2 fixture:
width = 513
height = 769
cells = 513 × 769 = 394,497
bytes = 394,497 × 8 = 3,155,976
所以 payload.byteLength 必须正好是 3155976;少 1 byte、多 1 byte 或 metadata 声明与实际文件不一致都会 fail-closed。
一个 2×2 示例:
北行: NW(U=5,V=1) NE(U=6,V=1)
南行: SW(U=4,V=0) SE(U=5,V=0)
payload 中的 float 顺序必须是:
5,1, 6,1, 4,0, 5,0
总长度为 2 × 2 × 8 = 32 bytes。
React/Node 侧若自己生成 canonical payload,可用 DataView.setFloat32(offset, value, true);最后一个参数必须为 true,明确写成 little-endian。业务层也可以从服务器取得已经生成好的 .f32 文件,不要求 JS 重新编码。
7. React 侧如何提交一帧
当前正式 bridge 不把大块 U/V 数据变成 JS number[]。React 侧准备好 metadata JSON 和本地文件 URI 后直接提交:
const result = await WeatherRuntime.submitWindField(
reactTag,
JSON.stringify(metadata),
payloadFileUri, // 必须是 local file:// URI
);
测试 Host 使用 expo-asset 把随 App 打包的 .f32 materialize 到本地 cache:
const asset = Asset.fromModule(require("./field.f32"));
await asset.downloadAsync();
const payloadFileUri = asset.localUri;
正式 NavSea 不要求数据必须来自 Expo Asset。可以由天气业务层下载、转换或合成 canonical U/V Field,然后保存到 App 可读的本地文件,再把 file:// URI 交给 WeatherRuntime。Native 读取 bytes 后进入唯一的 RasterArraySource::submitHostField() / Adapter / Snapshot / Timeline 路径。
不要使用以下正式 transport:
number[]
JSON float array
base64
hex string
每 cell 一个 {lat,lon,u,v} 对象
React 每帧传粒子位置
这些都会扩大 JS/Native 开销并破坏当前已冻结的 binary Field 边界。
8. 多时间片与 Timeline
至少提交两个彼此兼容、时间递增的 Field,Core 才能建立 initial window:
T0 → ACCEPTED_PENDING_INITIAL_WINDOW
T1 → INITIAL_WINDOW_READY
T2 → ACCEPTED_NEXT_SNAPSHOT
T3 → ACCEPTED_NEXT_SNAPSHOT
随后:
const state = await WeatherRuntime.getState(reactTag);
// 预期 state.state === "READY"
同一时间序列必须保持相同的 grid 尺寸、bounds、noData、fieldType 和 displayDomain,validTime 必须使用同一时间基准并严格递增。当前真实 WeatherV2 验收使用 2026-08-07 00/06/12/18 四帧,每帧相隔 6 小时。
React 可以用:
await WeatherRuntime.setForecastTime(reactTag, targetTimestampMs);
await WeatherRuntime.setPlaybackState(reactTag, "PLAY");
await WeatherRuntime.setPlaybackSpeed(reactTag, "NORMAL");
但 Timeline cursor、T0/T1 选择、时间插值、Runtime Field Window 和 GPU Field 切换仍由 Core 管理;React 只发低频控制命令,不在 JS 中复制一套播放状态机。
9. 显示与视觉控制
当前公开控制包括:
setWeatherEnabled
setWeatherColorVisible
setParticleVisible
setTargetFps
setWeatherColorStops
setParticleColor
setTrailColor
setParticleCount
setParticleLineWidth
setParticleLength
setParticleOpacity
setTrailOpacity
setTrailFade
setWeatherColorOpacity
例如:
await WeatherRuntime.setWeatherColorVisible(reactTag, true);
await WeatherRuntime.setParticleVisible(reactTag, true);
await WeatherRuntime.setParticleCount(reactTag, 4096);
await WeatherRuntime.setParticleLineWidth(reactTag, 1.5);
await WeatherRuntime.setParticleOpacity(reactTag, 0.9);
await WeatherRuntime.setTrailOpacity(reactTag, 0.8);
await WeatherRuntime.setTrailFade(reactTag, 0.94);
await WeatherRuntime.setWeatherColorOpacity(reactTag, 0.62);
这些参数只控制显示,不改变 U/V canonical 数据结构。天气业务数据和渲染视觉参数必须分开管理。
10. 当前 Native 会拒绝哪些数据
当前 Adapter 采用 fail-closed。以下情况会拒绝 Field,而不是尽量猜测:
- schema 不是
maplibre-particle-field/v1; fieldId/fieldType/displayDomain缺失或类型错误;generation或validTime为 0;- width/height 为 0;
west >= east或south >= north;noData不是有限数值;- layout 不是
interleaved-uv; - encoding 不是
float32-le; - stride 提供但不是 8;
- metadata
byteLength不等于width × height × 8; - 实际文件长度与 expected byte length 不一致;
- payload 中包含 NaN/Infinity;
- 当前 fieldType 不是
WIND; - 当前 displayDomain 不是
GLOBAL; - 时间序列不满足既有 compatibility / monotonicity 规则。
因此 React 侧不要通过容错转换掩盖 producer 错误。Producer/天气业务层应在提交前完成 canonical 化并保持 Field identity、时间和二进制长度稳定。
11. 当前已验证的真实数据基线
Package Consumer 已使用 ECMWF HRES WeatherV2 真实数据完成 Android 实机验证:4 帧 2026080700 / 06 / 12 / 18,每帧 513×769、3,155,976 bytes,bounds=112.5,0.0 → 157.5,55.77657301866769,transport=FILE_REF / FLOAT32_LE / INTERLEAVED_UV。用户确认正式 package Consumer APK 正常。
12. NavSea 侧责任边界
推荐保持以下分工:
React Native / NavSea 天气业务层
负责:取数、Route/ETA、多地点/多时间合成、canonical U/V 生成、文件缓存、提交时机
↓ metadataJson + local file:// field
@navsea/maplibre-particle-rn
↓
MapLibre Native / Core
负责:校验、Snapshot、Timeline、时间插值、空间采样、WeatherColor、Particle、GPU 资源与渲染
MapLibre Native 不理解 provider、HTTP、WeatherV2、Route、ETA 或业务缓存;React 不接管 GPU 粒子模拟和 Core Timeline。
13. 版本升级检查
每次升级 @navsea/maplibre-particle-rn 后,至少确认:
- npm 根层只有一份
@maplibre/maplibre-react-native,没有嵌套 native module; - Gradle
dependencyInsight最终解析到对应cc.navsea.maplibre:android-sdk-particle-opengl:<version>; - 不出现旧
step061、Maven Local 或源码路径; - Package Consumer 用真实 T0–T3 Field 达到 Runtime
READY; - WeatherColor 与 Particle 均正常显示,地图交互无异常;
- Consumer 通过后再升级 NavSea 的包版本。
正式参考实现:integration/react-native/MapLibreParticlePackageConsumerTestHost/。它的意义不是演示 UI,而是验证“正式 npm package → 正式 Maven AAR → 真实 canonical WeatherV2 → Core/Renderer”的完整消费链。