30 KiB
MapLibre 粒子 React Native 使用说明 v2
本文面向使用 @navsea/maplibre-particle-rn 的 React Native / Expo 调用方,描述当前 13.4.1-particle.1 的真实公共接口、参数结构、安装方式与调用顺序。
本文只说明已经存在的公开 API。@navsea/maplibre-particle-rn 会完整 re-export @maplibre/maplibre-react-native@11.3.6;上游 MapLibre React Native 的全部通用 Map API 不在本文重复展开,本文重点说明 NavSea Particle 新增能力。
1. 当前发布物
React Native 包名:
@navsea/maplibre-particle-rn
当前版本:
13.4.1-particle.1
当前采用静态 HTTPS tarball 发布,不使用 npm Registry,也不需要 .npmrc:
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.1.tgz
Android Native AAR 独立位于 Maven 仓库:
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1
https://pbf.jpcn.ccwu.cc/maven/
2. 安装
新 React Native / Expo 项目建议一次安装正式 tarball 和固定的 MapLibre React Native peer dependency:
npm install https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.1.tgz @maplibre/maplibre-react-native@11.3.6
安装后,package.json 中 @navsea/maplibre-particle-rn 可能记录为完整 HTTPS URL,这是当前预期行为。代码中仍然使用正式包名导入:
import { Map, Camera, WeatherRuntime } from "@navsea/maplibre-particle-rn";
当前 peer dependency 契约:
@maplibre/maplibre-react-native = 11.3.6
react >= 19.1.0
react-native >= 0.80.0
不要同时再安装另一份自定义 MapLibre Native AAR、旧 STEP AAR、源码路径 bridge 或第二份 MLRN native module。
3. Expo 配置
Expo / Expo Prebuild 项目必须在 app.json 或 app config 中加入:
{
"expo": {
"plugins": ["@navsea/maplibre-particle-rn/app.plugin"]
}
}
这个 plugin 会完成两件事:
- 向 Android Gradle 加入
https://pbf.jpcn.ccwu.cc/maven; - 把官方
org.maplibre.gl:android-sdk-opengl替换为cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1,避免 APK 中同时进入官方 AAR 与 Particle AAR。
首次接入、plugin/AAR 版本变化或 Android Native 代码变化后,需要重新生成/编译 Android 原生工程。仅 JS/TS 逻辑变化时,已有包含正确 Native bridge/AAR 的 Debug APK 可以继续通过 Metro 验证新 JS。
Bare React Native 如果不使用 Expo config plugin,必须在宿主 Android Gradle 中等价完成 Maven repository 与 dependency substitution;不能只依赖 autolinking。
4. 公共导入入口
包根入口当前导出:
export * from "@maplibre/maplibre-react-native";
export { default as WeatherRuntime } from "./NativeWeatherRuntimeBridge";
export * from "./weatherV2";
因此调用方统一从一个入口导入即可:
import {
Map,
Camera,
WeatherRuntime,
WeatherV2HttpClient,
loadWeatherV2WindIntoRuntime,
decodeWeatherV2WindFrame,
createParticleWeatherStyle,
particleWeatherIds,
type ColorStop,
type RuntimeSnapshot,
type WeatherV2WindContext,
type LoadWeatherV2WindResult,
} from "@navsea/maplibre-particle-rn";
Map、Camera 以及其他通用 MapLibre React Native API 都来自上游 @maplibre/maplibre-react-native@11.3.6,只是由本包重新导出。本文不重新定义这些上游组件的全部 Props。
5. 固定 Weather 对象 ID
particleWeatherIds = {
source: "rn-weather-fields",
weatherColorLayer: "rn-weather-color",
rasterParticleLayer: "rn-weather-particles",
};
WeatherRuntime.attach() 会按这三个固定 ID 查找 source/layer。正式宿主 style 如果不用 createParticleWeatherStyle(),也必须提供同名对象。
6. createParticleWeatherStyle()
签名:
createParticleWeatherStyle(
overrides?: Record<string, unknown>,
): string
返回 MapLibre Style JSON 字符串。默认包含:
source: rn-weather-fields / raster-array / host-field-submission=true
layer : rn-weather-color / weather-color
layer : rn-weather-particles / raster-particle
底图 : GSI standard raster
最小调用:
<Map mapStyle={createParticleWeatherStyle()}>
<Camera initialViewState={{ center: [135, 32], zoom: 3.9 }} />
</Map>
overrides 当前是顶层浅覆盖,不是深度 merge。若传入 sources 或 layers,会覆盖 helper 原来的整个顶层字段,因此正式 NavSea style 更推荐显式把 Particle source/layers 合入宿主 style,而不是依赖复杂 overrides。
7. Map 生命周期与 reactTag
WeatherRuntime 是 per-MapView Native facade。所有方法第一个参数都是当前 MapView 的正整数 reactTag。必须在 Map style 完成加载以后取得 tag 并 attach。
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;
const state = await WeatherRuntime.attach(tag);
setReactTag(tag);
console.log(state);
}, []);
useEffect(() => {
return () => {
if (reactTag != null) {
void WeatherRuntime.detach(reactTag).catch(() => undefined);
}
};
}, [reactTag]);
Map:
<Map
mapStyle={createParticleWeatherStyle()}
onDidFinishLoadingStyle={onStyleReady}
>
<Camera initialViewState={{ center: [135, 32], zoom: 3.9 }} />
</Map>
attach() 前必须满足 style、rn-weather-fields、rn-weather-color、rn-weather-particles 都已存在,否则 Promise 会 reject。页面销毁时应调用 detach();MapView 自身从 Window detach 时 Native 也有兜底 dispose。
8. WeatherRuntime 公共类型
type RuntimeState =
| "DISABLED"
| "WAITING_FOR_FIELDS"
| "READY"
| "ERROR";
type PlaybackState = "PLAY" | "PAUSE";
type TargetFps = "AUTO" | "FPS_30" | "FPS_60";
type CommandResult = "ACCEPTED" | "REJECTED";
type RuntimeSnapshot = Readonly<{
state: RuntimeState;
playbackState: PlaybackState;
targetFps: TargetFps;
disposed: boolean;
}>;
Field 提交结果:
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";
注意:Native 日志内部可能打印更详细的 C++ reason 名称;React API 应以这里的 FieldSubmitResult 字符串为准。
9. WeatherRuntime 方法总表
除 detach() 外,方法返回值均通过 Promise 返回。Bridge 参数错误、Map/style/runtime 不存在等情况会 Promise reject;Core 正常处理但拒绝业务命令时通常返回 "REJECTED" 或对应 FieldSubmitResult。
| 方法 | 参数 | 返回 | 用途 |
|---|---|---|---|
attach(reactTag) |
reactTag:number |
Promise<RuntimeSnapshot> |
为当前 MapView 建立 WeatherRuntime |
detach(reactTag) |
reactTag:number |
Promise<void> |
dispose 当前 MapView Runtime |
getState(reactTag) |
reactTag:number |
Promise<RuntimeSnapshot> |
读取 Native 权威状态 |
clearWindFields(reactTag) |
reactTag:number |
Promise<CommandResult> |
清空 Host Field/Timeline |
submitWindField(reactTag, metadataJson, payloadFileUri) |
tag + JSON + 本地文件 | Promise<FieldSubmitResult> |
提交一张 canonical 风场 |
setWeatherEnabled(reactTag, enabled) |
boolean |
Promise<CommandResult> |
总开关 |
setWeatherColorVisible(reactTag, visible) |
boolean |
Promise<CommandResult> |
场图显示开关 |
setParticleVisible(reactTag, visible) |
boolean |
Promise<CommandResult> |
粒子显示开关 |
setTargetFps(reactTag, target) |
AUTO/FPS_30/FPS_60 |
Promise<CommandResult> |
Runtime 绘制 cadence |
setForecastTime(reactTag, timestampMs) |
epoch ms | Promise<CommandResult> |
seek 到目标预报时间 |
setPlaybackState(reactTag, playback) |
PLAY/PAUSE |
Promise<CommandResult> |
播放/暂停 Timeline |
setPlaybackSpeed(reactTag, speed) |
HALF/NORMAL/DOUBLE |
Promise<CommandResult> |
0.5x/1x/2x 播放速度 |
setWeatherColorStops(reactTag, stops) |
ColorStop[] |
Promise<CommandResult> |
风速→场图颜色 |
setParticleColor(...) |
RGBA | Promise<CommandResult> |
粒子颜色 |
setTrailColor(...) |
RGBA | Promise<CommandResult> |
尾迹颜色 |
setParticleCount(reactTag, count) |
非负整数 | Promise<CommandResult> |
粒子数量 |
setParticleLineWidth(reactTag, value) |
finite number | Promise<CommandResult> |
粒子线宽 |
setParticleLength(reactTag, value) |
finite number | Promise<CommandResult> |
粒子长度 |
setParticleOpacity(reactTag, value) |
finite number | Promise<CommandResult> |
粒子透明度 |
setTrailOpacity(reactTag, value) |
finite number | Promise<CommandResult> |
尾迹透明度 |
setTrailFade(reactTag, value) |
finite number | Promise<CommandResult> |
尾迹衰减 |
setWeatherColorOpacity(reactTag, value) |
finite number | Promise<CommandResult> |
场图整体透明度 |
9.1 attach / detach / getState / clearWindFields
await WeatherRuntime.attach(reactTag);
await WeatherRuntime.getState(reactTag);
await WeatherRuntime.clearWindFields(reactTag);
await WeatherRuntime.detach(reactTag);
reactTag 必须是正整数并解析到当前 Fabric MLRNMapView。clearWindFields() 成功后 Runtime 回到 WAITING_FOR_FIELDS;已有 Timeline 和当前 Field 会被清空。
setWeatherEnabled(false) 会隐藏 WeatherColor/Particle 并清空 Field,Runtime 进入 DISABLED。重新 setWeatherEnabled(true) 后进入 WAITING_FOR_FIELDS,需要重新提交至少 T0/T1。
9.2 submitWindField()
const result = await WeatherRuntime.submitWindField(
reactTag,
metadataJson,
payloadFileUri,
);
参数:
reactTag:正整数 MapView tag;metadataJson:非空 UTF-8 JSON,Bridge 上限 1 MiB;payloadFileUri:file://URI,或 Android 可读的绝对本地路径;不能使用 HTTP URL;- payload 文件必须可读、非空,Bridge 上限 64 MiB。
正式数据路径不使用 JS number[]、base64 或逐 cell 对象。
严格时间序列典型返回:
T0 -> ACCEPTED_PENDING_INITIAL_WINDOW
T1 -> INITIAL_WINDOW_READY
T2 -> ACCEPTED_NEXT_SNAPSHOT
T3 -> ACCEPTED_NEXT_SNAPSHOT
至少 T0/T1 两张兼容 Field 建立 initial window 后,Runtime 才进入 READY。
9.3 显示开关
await WeatherRuntime.setWeatherEnabled(reactTag, true);
await WeatherRuntime.setWeatherColorVisible(reactTag, true);
await WeatherRuntime.setParticleVisible(reactTag, true);
语义区别:
setWeatherEnabled:整个 Weather Runtime 的总开关;关闭时同时清空 Field;setWeatherColorVisible:只控制背景 WeatherColor 场图;setParticleVisible:只控制运动粒子层。
9.4 时间与播放
await WeatherRuntime.setForecastTime(reactTag, timestampMs);
await WeatherRuntime.setPlaybackState(reactTag, "PLAY");
await WeatherRuntime.setPlaybackSpeed(reactTag, "NORMAL");
await WeatherRuntime.setTargetFps(reactTag, "AUTO");
timestampMs 必须是非负 Unix epoch 毫秒整数。播放速度允许 HALF、NORMAL、DOUBLE;Target FPS 允许 AUTO、FPS_30、FPS_60。React 只设置目标时间/播放状态,T0/T1 选择、MIX 和 GPU 时间插值由 Native/Core 管理。
9.5 WeatherColor 色阶
类型:
type ColorStop = Readonly<{
speedMps: number;
red: number;
green: number;
blue: number;
alpha: number;
}>;
调用:
await WeatherRuntime.setWeatherColorStops(reactTag, [
{ speedMps: 0, red: 0.08, green: 0.20, blue: 0.80, alpha: 0.72 },
{ speedMps: 8, red: 0.00, green: 0.85, blue: 0.90, alpha: 0.72 },
{ speedMps: 16, red: 1.00, green: 0.85, blue: 0.05, alpha: 0.72 },
{ speedMps: 30, red: 0.90, green: 0.08, blue: 0.08, alpha: 0.72 },
]);
约束:
- 当前 Core capacity 为最多 8 个 stop;至少 2 个;
speedMps必须 finite 且严格递增,单位 m/s;- RGBA 必须 finite 且全部在
[0,1]; - 两个 stop 之间由 Shader 做线性颜色插值;
- 小于第一个 stop 使用第一个颜色,大于最后一个 stop 使用最后一个颜色。
例如若 0 m/s 与 2 m/s 两个 stop 使用相同 RGBA,则 0~2 m/s 会保持同色。
9.6 粒子与尾迹视觉参数
await WeatherRuntime.setParticleColor(reactTag, 1, 1, 1, 0.95);
await WeatherRuntime.setTrailColor(reactTag, 0.35, 0.85, 1, 0.8);
await WeatherRuntime.setParticleCount(reactTag, 4096);
await WeatherRuntime.setParticleLineWidth(reactTag, 1.5);
await WeatherRuntime.setParticleLength(reactTag, 0.012);
await WeatherRuntime.setParticleOpacity(reactTag, 0.9);
await WeatherRuntime.setTrailOpacity(reactTag, 0.8);
await WeatherRuntime.setTrailFade(reactTag, 0.94);
await WeatherRuntime.setWeatherColorOpacity(reactTag, 0.62);
硬性 Bridge 参数约束:
setParticleColor/setTrailColor:RGBA 各分量[0,1];setParticleCount:0 到 AndroidInt.MAX_VALUE的整数;- line width / length / opacity / fade / WeatherColor opacity:Bridge 要求 finite number,最终是否接受仍由 Native style property 决定。
上面的 4096 / 1.5 / 0.012 / 0.9 / 0.8 / 0.94 / 0.62 是当前 Consumer Test Host 的验证值,不是所有参数的固定默认契约。
WeatherColor 色阶、粒子颜色、尾迹颜色是三套独立视觉设置:
setWeatherColorStops -> 背景风速场颜色
setParticleColor -> 粒子本体颜色
setTrailColor -> 粒子尾迹颜色
10. canonical Wind Field 数据结构
MapLibre Particle 接收规则二维 U/V 向量场,而不是图片或粒子坐标数组。每个时间片由:
metadataJson + local binary payload file
组成。 当前 canonical schema:
maplibre-particle-field/v1
固定向量语义:
origin = north-west
rowOrder = north-to-south
columnOrder = west-to-east
storage = row-major
layout = interleaved-uv
encoding = float32-le
U = eastward-positive, m/s
V = northward-positive, m/s
noData = -9999(当前 WeatherV2 converter)
每 cell = U float32 + V float32 = 8 bytes
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, "east": 157.5, "north": 55.77657301866769 },
"grid": { "width": 513, "height": 769 },
"vector": {
"layout": "interleaved-uv",
"encoding": "float32-le",
"noData": -9999,
"unit": "m/s",
"origin": "north-west",
"rowOrder": "north-to-south",
"columnOrder": "west-to-east",
"uPositive": "east",
"vPositive": "north"
},
"payload": { "byteLength": 3155976 }
}
二进制排列:
cell = y * width + x
byteOffset = cell * 8
[offset + 0..3] = U float32 little-endian
[offset + 4..7] = V float32 little-endian
float sequence = U0,V0,U1,V1,U2,V2,...
payload.byteLength 必须严格等于 width × height × 8。有效 U/V 不能是 NaN/Infinity;当前 WeatherV2 converter 对 invalid cell 把 U/V 写为 -9999。
同一 Timeline 内,Field 必须保持兼容 grid/bounds/语义,并且 validTime、generation 按 Native monotonicity 契约严格向前。React 不直接构造 T0/T1/MIX window。
11. WeatherV2HttpClient
WeatherV2 HTTP 层负责读取服务器 manifest/time index、选择预报帧、下载 WVECBIN1 并转换成 canonical U/V。Native/Core 不理解 WeatherV2/provider/HTTP。
11.1 构造函数
type WeatherV2HttpClientOptions = Readonly<{
baseUrl: string;
headers?: Readonly<Record<string, string>>;
fetchImpl?: WeatherV2Fetch;
}>;
const client = new WeatherV2HttpClient({
baseUrl: "https://weather.jpcn.ccwu.cc",
});
baseUrl 必须是绝对 http:// 或 https:// URL。headers 可增加/覆盖请求 header;默认会带 Accept: application/json, application/octet-stream。fetchImpl 用于宿主注入兼容 fetch,实现测试、代理或特殊网络层。
11.2 getWindContext()
签名:
client.getWindContext(options?: {
datasetId?: string;
referenceTime?: string;
}): Promise<WeatherV2WindContext>
返回:
type WeatherV2WindContext = Readonly<{
datasetId: string;
model: string;
referenceTime: string;
targetTimes: readonly string[];
particleTemplate: string;
bounds?: {
west: number;
south: number;
east: number;
north: number;
};
}>;
调用示例:
const context = await client.getWindContext({
datasetId: "ecmwf-hres",
referenceTime: "2026080600",
});
两个参数都省略时,从 manifest 的 wind dataset 列表选择第一个匹配项。referenceTime/targetTimes 当前服务协议使用 YYYYMMDDHH 字符串。
11.3 selectWindFrames()
签名:
client.selectWindFrames(
context,
options?: {
targetTimes?: readonly string[];
count?: number;
},
): readonly WeatherV2WindFrameRef[]
返回元素:
type WeatherV2WindFrameRef = Readonly<{
datasetId: string;
model: string;
referenceTime: string;
targetTime: string;
url: string;
}>;
若不传 targetTimes,默认从 context.targetTimes 开头取 count ?? 4 帧;count 必须是 >=2 的整数。若显式传 targetTimes,这些时间必须都存在于 context,并严格递增,且至少两帧。
const refs = client.selectWindFrames(context, {
targetTimes: ["2026080700", "2026080706", "2026080712", "2026080718"],
});
正式 NavSea 应根据当前时间、Route/ETA 或业务策略选择 targetTimes;固定四帧只是当前回归样例。
11.4 fetchCanonicalWindFrame()
const field = await client.fetchCanonicalWindFrame(ref);
返回:
type CanonicalWindField = Readonly<{
metadataJson: string;
payload: Uint8Array;
fieldId: string;
validTime: number;
generation: number;
bounds: WeatherV2Bounds;
width: number;
height: number;
sourceUrl: string;
}>;
该方法会:
GET frame.url
-> 校验 WVECBIN1 magic/header/fields/length
-> 读取 U/V/speed/valid planes
-> 只保留 canonical U/V
-> invalid cell 写 noData=-9999
-> 生成 maplibre-particle-field/v1 metadata
payload 此时仍在 JS 内存中,是 Uint8Array;提交 Native 前应通过 WeatherV2BinaryFileStore 写成临时本地文件。
12. decodeWeatherV2WindFrame()
这是公开的底层 converter,适合已经自行取得 WVECBIN1 ArrayBuffer 的调用方:
const canonical = decodeWeatherV2WindFrame(arrayBuffer, frameRef);
签名等价于:
decodeWeatherV2WindFrame(
source: ArrayBuffer,
frame: WeatherV2WindFrameRef,
): CanonicalWindField
一般业务代码优先使用 WeatherV2HttpClient.fetchCanonicalWindFrame(),不需要直接调用 decoder。
13. WeatherV2BinaryFileStore
WeatherV2 helper 不绑定 Expo FileSystem。宿主只需提供:
type WeatherV2BinaryFileStore = Readonly<{
writeTempFile(
fileName: string,
bytes: Uint8Array,
): Promise<string>;
removeTempFile?(
fileUri: string,
): Promise<void>;
}>;
writeTempFile() 必须返回 Native 可读的 file:// URI或绝对本地路径。removeTempFile() 可选;helper 在每帧提交完成后 best-effort 清理。
Expo FileSystem 示例:
import { File, Paths } from "expo-file-system";
const fileStore = {
async writeTempFile(fileName: string, bytes: Uint8Array) {
const file = new File(Paths.cache, fileName);
file.write(bytes);
return file.uri;
},
};
14. loadWeatherV2WindIntoRuntime()
这是 WeatherV2 最常用的高层入口。它把 HTTP 取数、转换、临时文件和 Native submission 串成一条流程。
参数类型:
type LoadWeatherV2WindOptions = Readonly<{
reactTag: number;
client: WeatherV2WindClient;
fileStore: WeatherV2BinaryFileStore;
datasetId?: string;
referenceTime?: string;
targetTimes?: readonly string[];
count?: number;
weatherColorVisible?: boolean;
particleVisible?: boolean;
playbackState?: "PLAY" | "PAUSE";
}>;
调用:
const loaded = await loadWeatherV2WindIntoRuntime({
reactTag,
client,
fileStore,
datasetId: "ecmwf-hres",
referenceTime: "2026080600",
targetTimes: ["2026080700", "2026080706", "2026080712", "2026080718"],
weatherColorVisible: true,
particleVisible: true,
playbackState: "PAUSE",
});
内部顺序固定为:
clearWindFields
-> setWeatherEnabled(true)
-> 设置 WeatherColor / Particle visibility
-> getWindContext
-> selectWindFrames
-> 逐帧 fetch + WVECBIN1 decode
-> writeTempFile
-> submitWindField
-> best-effort removeTempFile
-> setForecastTime(第一帧 validTime)
-> 可选 setPlaybackState
-> getState,要求 READY
当前实现按帧串行下载/转换/提交,避免同时在 JS 内存保留多张大 Field。
同一个 reactTag 上,该函数带 single-flight:前一次完整加载尚未结束时再次调用,会直接返回同一个 in-flight Promise,不会再次 clearWindFields() 或交叉提交另一套 T0/T1/T2/T3。调用方仍建议在 UI 上把加载按钮置为 disabled/loading。
注意当前调用前提:Runtime 已 attach(),且不要处于 DISABLED。如果此前调用过 setWeatherEnabled(false),应先:
await WeatherRuntime.setWeatherEnabled(reactTag, true);
再调用完整 WeatherV2 loader。
返回类型:
type LoadedWeatherV2WindFrame = Readonly<{
fieldId: string;
validTime: number;
generation: number;
bounds: WeatherV2Bounds;
width: number;
height: number;
sourceUrl: string;
submitResult: FieldSubmitResult;
}>;
type LoadWeatherV2WindResult = Readonly<{
context: WeatherV2WindContext;
frames: readonly LoadedWeatherV2WindFrame[];
runtime: RuntimeSnapshot;
}>;
成功返回时 runtime.state 必须已经是 READY,否则 helper 自己 throw。
15. WeatherV2WindClient 接口
loadWeatherV2WindIntoRuntime() 不强制 client 必须是内置 WeatherV2HttpClient,也可以注入自定义实现,只要满足:
type WeatherV2WindClient = Readonly<{
getWindContext(options?: {
datasetId?: string;
referenceTime?: string;
}): Promise<WeatherV2WindContext>;
selectWindFrames(
context: WeatherV2WindContext,
options?: {
targetTimes?: readonly string[];
count?: number;
},
): readonly WeatherV2WindFrameRef[];
fetchCanonicalWindFrame(
frame: WeatherV2WindFrameRef,
): Promise<CanonicalWindField>;
}>;
因此以后 NavSea 可以保留统一的 Runtime loader,同时替换 HTTP/cache/provider 层。
16. 最小完整 WeatherV2 React Native 示例
以下示例演示:Map style ready → attach → 从 HTTP 获取 WeatherV2 → canonical U/V → Native Runtime → WeatherColor + Particle。
import { useCallback, useEffect, useState } from "react";
import { View, findNodeHandle } from "react-native";
import { File, Paths } from "expo-file-system";
import {
Map,
Camera,
WeatherRuntime,
WeatherV2HttpClient,
createParticleWeatherStyle,
loadWeatherV2WindIntoRuntime,
} from "@navsea/maplibre-particle-rn";
const client = new WeatherV2HttpClient({
baseUrl: "https://weather.jpcn.ccwu.cc",
});
const fileStore = {
async writeTempFile(fileName: string, bytes: Uint8Array) {
const file = new File(Paths.cache, fileName);
file.write(bytes);
return file.uri;
},
};
export default function WeatherMap() {
const [reactTag, setReactTag] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
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);
}, []);
const loadWeather = useCallback(async () => {
if (reactTag == null || loading) return;
setLoading(true);
try {
const context = await client.getWindContext({
datasetId: "ecmwf-hres",
});
const targetTimes = context.targetTimes.slice(0, 4);
const result = await loadWeatherV2WindIntoRuntime({
reactTag,
client,
fileStore,
datasetId: context.datasetId,
referenceTime: context.referenceTime,
targetTimes,
weatherColorVisible: true,
particleVisible: true,
playbackState: "PAUSE",
});
console.log(result.runtime);
console.log(result.frames);
} finally {
setLoading(false);
}
}, [reactTag, loading]);
useEffect(() => {
if (reactTag == null) return;
void loadWeather();
return () => {
void WeatherRuntime.detach(reactTag).catch(() => undefined);
};
}, [reactTag, loadWeather]);
return (
<View style={{ flex: 1 }}>
<Map
mapStyle={createParticleWeatherStyle()}
onDidFinishLoadingStyle={onStyleReady}
style={{ flex: 1 }}
>
<Camera
initialViewState={{
center: [135, 32],
zoom: 3.9,
}}
/>
</Map>
</View>
);
}
实际产品中不要简单使用 context.targetTimes.slice(0, 4) 作为长期业务策略。应由 React/NavSea 天气业务层根据当前时间、船位、Route/ETA、缓存和预测有效期决定要提交哪些 canonical Field。
17. 错误处理
需要区分两类失败:
- Promise reject:参数格式错误、MapView 找不到、style 未 ready、Runtime 已 dispose、本地文件不可读等 Bridge/生命周期错误;
- Promise resolve 但返回
REJECTED/REJECTED_*:请求到达 Native/Core,但被 Runtime、Adapter、Timeline 或 capacity 契约拒绝。 建议所有 command 都检查结果:
const commandResult = await WeatherRuntime.setParticleVisible(reactTag, true);
if (commandResult !== "ACCEPTED") {
throw new Error(`setParticleVisible failed: ${commandResult}`);
}
直接提交 Field 时同样不要忽略返回值。loadWeatherV2WindIntoRuntime() 已经内置严格的 T0/T1/T2... expected result 检查,因此高层 WeatherV2 路径优先使用该 helper。
常见 Field 拒绝原因包括:schema/metadata 不合法、payload 长度不匹配、NaN/Infinity、时间不递增、Field 不兼容、Timeline capacity 已满或 source 不是 Host submission 模式。
18. React 与 Native 的责任边界
React Native / NavSea JS/TS
provider / HTTP
cache / retry / prefetch
当前时间与 forecast time 选择
Route / ETA
WeatherV2 解析或其他 provider -> canonical U/V
临时文件管理
低频 Runtime 控制
↓
metadataJson + local file
↓
@navsea/maplibre-particle-rn
↓
MapLibre Native / C++
Field 校验
Snapshot / Timeline
T0/T1 Runtime Field Window
GPU 空间采样 + 时间插值
WeatherColor
Particle / Trail
GPU rendering
React 不向 Native 传 provider 业务对象,也不每帧驱动粒子位置;Native 不负责 HTTP、Route、ETA 或业务缓存。
19. Bare React Native Android 手工配置
如果宿主不使用 Expo config plugin,需要在 Android 根工程中完成与 app.plugin.js 等价的配置。核心要求不是“再加一份 AAR”,而是让 MLRN 的 OpenGL Native SDK 请求统一解析到 NavSea Particle AAR。
当前等价 Groovy 逻辑为:
allprojects { p ->
p.repositories.maven {
url = p.uri("https://pbf.jpcn.ccwu.cc/maven")
}
p.configurations.configureEach { configuration ->
configuration.resolutionStrategy.dependencySubstitution {
substitute module("org.maplibre.gl:android-sdk-opengl") using module(
"cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1"
)
}
}
}
rootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")
rootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")
包自身 react-native.config.js 已声明 Android sourceDir=./android,正常 React Native autolinking 应自动接入 WeatherRuntimeBridgePackage。
20. 版本与升级
当前静态包 URL 包含完整版本号,因此项目升级必须显式修改 URL:
.../navsea-maplibre-particle-rn-13.4.1-particle.1.tgz
.../navsea-maplibre-particle-rn-13.4.1-particle.2.tgz
已经发布的 tarball 不覆盖。版本有修改时创建新的 particle.N,重新 npm pack、上传、Consumer 验证后再给 NavSea 使用。
21. WeatherV2 低层网络类型
只有需要替换默认 globalThis.fetch 时才需要这些类型:
type WeatherV2FetchResponse = Readonly<{
ok: boolean;
status: number;
json(): Promise<unknown>;
arrayBuffer(): Promise<ArrayBuffer>;
}>;
type WeatherV2Fetch = (
url: string,
init?: Readonly<{
headers?: Readonly<Record<string, string>>;
}>,
) => Promise<WeatherV2FetchResponse>;
例如可以用自定义 fetch 包装日志、认证 header 或测试 mock,但返回对象必须至少实现上述四项契约。
22. 当前推荐的 React 调用层级
普通业务优先级:
第一选择:loadWeatherV2WindIntoRuntime()
-> 完整 WeatherV2 HTTP + canonical + submit 流程
需要自定义选帧:WeatherV2HttpClient.getWindContext()
+ selectWindFrames()
+ loadWeatherV2WindIntoRuntime()
需要自定义缓存/Provider:实现 WeatherV2WindClient
+ WeatherV2BinaryFileStore
+ loadWeatherV2WindIntoRuntime()
已有 canonical 本地文件:WeatherRuntime.submitWindField()
已有 WVECBIN1 ArrayBuffer:decodeWeatherV2WindFrame()
不要为了普通显示直接绕过高层 helper 操作 Core T0/T1/MIX;这些不是 React 公共控制面。