docs: add React Native particle API guide v2
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
965
docs/30_设计/MapLibre粒子ReactNative使用说明_v2.md
Normal file
965
docs/30_设计/MapLibre粒子ReactNative使用说明_v2.md
Normal file
@@ -0,0 +1,965 @@
|
||||
# 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 包名:
|
||||
|
||||
```text
|
||||
@navsea/maplibre-particle-rn
|
||||
```
|
||||
|
||||
当前版本:
|
||||
|
||||
```text
|
||||
13.4.1-particle.1
|
||||
```
|
||||
|
||||
当前采用静态 HTTPS tarball 发布,不使用 npm Registry,也不需要 `.npmrc`:
|
||||
|
||||
```text
|
||||
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.1.tgz
|
||||
```
|
||||
|
||||
Android Native AAR 独立位于 Maven 仓库:
|
||||
```text
|
||||
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:
|
||||
|
||||
```bash
|
||||
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,这是当前预期行为。代码中仍然使用正式包名导入:
|
||||
|
||||
```ts
|
||||
import { Map, Camera, WeatherRuntime } from "@navsea/maplibre-particle-rn";
|
||||
```
|
||||
|
||||
当前 peer dependency 契约:
|
||||
|
||||
```text
|
||||
@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 中加入:
|
||||
|
||||
```json
|
||||
{
|
||||
"expo": {
|
||||
"plugins": ["@navsea/maplibre-particle-rn/app.plugin"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个 plugin 会完成两件事:
|
||||
|
||||
1. 向 Android Gradle 加入 `https://pbf.jpcn.ccwu.cc/maven`;
|
||||
2. 把官方 `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. 公共导入入口
|
||||
|
||||
包根入口当前导出:
|
||||
|
||||
```ts
|
||||
export * from "@maplibre/maplibre-react-native";
|
||||
export { default as WeatherRuntime } from "./NativeWeatherRuntimeBridge";
|
||||
export * from "./weatherV2";
|
||||
```
|
||||
|
||||
因此调用方统一从一个入口导入即可:
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
particleWeatherIds = {
|
||||
source: "rn-weather-fields",
|
||||
weatherColorLayer: "rn-weather-color",
|
||||
rasterParticleLayer: "rn-weather-particles",
|
||||
};
|
||||
```
|
||||
|
||||
`WeatherRuntime.attach()` 会按这三个固定 ID 查找 source/layer。正式宿主 style 如果不用 `createParticleWeatherStyle()`,也必须提供同名对象。
|
||||
## 6. `createParticleWeatherStyle()`
|
||||
|
||||
签名:
|
||||
|
||||
```ts
|
||||
createParticleWeatherStyle(
|
||||
overrides?: Record<string, unknown>,
|
||||
): string
|
||||
```
|
||||
|
||||
返回 MapLibre Style JSON 字符串。默认包含:
|
||||
|
||||
```text
|
||||
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
|
||||
```
|
||||
|
||||
最小调用:
|
||||
|
||||
```tsx
|
||||
<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。
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
<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 公共类型
|
||||
|
||||
```ts
|
||||
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 提交结果:
|
||||
|
||||
```ts
|
||||
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`
|
||||
|
||||
```ts
|
||||
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()`
|
||||
|
||||
```ts
|
||||
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 对象。
|
||||
严格时间序列典型返回:
|
||||
|
||||
```text
|
||||
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 显示开关
|
||||
|
||||
```ts
|
||||
await WeatherRuntime.setWeatherEnabled(reactTag, true);
|
||||
await WeatherRuntime.setWeatherColorVisible(reactTag, true);
|
||||
await WeatherRuntime.setParticleVisible(reactTag, true);
|
||||
```
|
||||
|
||||
语义区别:
|
||||
|
||||
- `setWeatherEnabled`:整个 Weather Runtime 的总开关;关闭时同时清空 Field;
|
||||
- `setWeatherColorVisible`:只控制背景 WeatherColor 场图;
|
||||
- `setParticleVisible`:只控制运动粒子层。
|
||||
|
||||
### 9.4 时间与播放
|
||||
|
||||
```ts
|
||||
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 色阶
|
||||
|
||||
类型:
|
||||
|
||||
```ts
|
||||
type ColorStop = Readonly<{
|
||||
speedMps: number;
|
||||
red: number;
|
||||
green: number;
|
||||
blue: number;
|
||||
alpha: number;
|
||||
}>;
|
||||
```
|
||||
|
||||
调用:
|
||||
|
||||
```ts
|
||||
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 粒子与尾迹视觉参数
|
||||
|
||||
```ts
|
||||
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 到 Android `Int.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 色阶、粒子颜色、尾迹颜色是三套独立视觉设置:
|
||||
|
||||
```text
|
||||
setWeatherColorStops -> 背景风速场颜色
|
||||
setParticleColor -> 粒子本体颜色
|
||||
setTrailColor -> 粒子尾迹颜色
|
||||
```
|
||||
|
||||
## 10. canonical Wind Field 数据结构
|
||||
|
||||
MapLibre Particle 接收规则二维 U/V 向量场,而不是图片或粒子坐标数组。每个时间片由:
|
||||
|
||||
```text
|
||||
metadataJson + local binary payload file
|
||||
```
|
||||
|
||||
组成。
|
||||
当前 canonical schema:
|
||||
|
||||
```text
|
||||
maplibre-particle-field/v1
|
||||
```
|
||||
|
||||
固定向量语义:
|
||||
|
||||
```text
|
||||
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 示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"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 }
|
||||
}
|
||||
```
|
||||
二进制排列:
|
||||
|
||||
```text
|
||||
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 构造函数
|
||||
|
||||
```ts
|
||||
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()`
|
||||
|
||||
签名:
|
||||
|
||||
```ts
|
||||
client.getWindContext(options?: {
|
||||
datasetId?: string;
|
||||
referenceTime?: string;
|
||||
}): Promise<WeatherV2WindContext>
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```ts
|
||||
type WeatherV2WindContext = Readonly<{
|
||||
datasetId: string;
|
||||
model: string;
|
||||
referenceTime: string;
|
||||
targetTimes: readonly string[];
|
||||
particleTemplate: string;
|
||||
bounds?: {
|
||||
west: number;
|
||||
south: number;
|
||||
east: number;
|
||||
north: number;
|
||||
};
|
||||
}>;
|
||||
```
|
||||
|
||||
调用示例:
|
||||
|
||||
```ts
|
||||
const context = await client.getWindContext({
|
||||
datasetId: "ecmwf-hres",
|
||||
referenceTime: "2026080600",
|
||||
});
|
||||
```
|
||||
|
||||
两个参数都省略时,从 manifest 的 wind dataset 列表选择第一个匹配项。`referenceTime`/`targetTimes` 当前服务协议使用 `YYYYMMDDHH` 字符串。
|
||||
### 11.3 `selectWindFrames()`
|
||||
|
||||
签名:
|
||||
|
||||
```ts
|
||||
client.selectWindFrames(
|
||||
context,
|
||||
options?: {
|
||||
targetTimes?: readonly string[];
|
||||
count?: number;
|
||||
},
|
||||
): readonly WeatherV2WindFrameRef[]
|
||||
```
|
||||
|
||||
返回元素:
|
||||
|
||||
```ts
|
||||
type WeatherV2WindFrameRef = Readonly<{
|
||||
datasetId: string;
|
||||
model: string;
|
||||
referenceTime: string;
|
||||
targetTime: string;
|
||||
url: string;
|
||||
}>;
|
||||
```
|
||||
|
||||
若不传 `targetTimes`,默认从 `context.targetTimes` 开头取 `count ?? 4` 帧;`count` 必须是 `>=2` 的整数。若显式传 `targetTimes`,这些时间必须都存在于 context,并严格递增,且至少两帧。
|
||||
|
||||
```ts
|
||||
const refs = client.selectWindFrames(context, {
|
||||
targetTimes: ["2026080700", "2026080706", "2026080712", "2026080718"],
|
||||
});
|
||||
```
|
||||
|
||||
正式 NavSea 应根据当前时间、Route/ETA 或业务策略选择 targetTimes;固定四帧只是当前回归样例。
|
||||
### 11.4 `fetchCanonicalWindFrame()`
|
||||
|
||||
```ts
|
||||
const field = await client.fetchCanonicalWindFrame(ref);
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```ts
|
||||
type CanonicalWindField = Readonly<{
|
||||
metadataJson: string;
|
||||
payload: Uint8Array;
|
||||
fieldId: string;
|
||||
validTime: number;
|
||||
generation: number;
|
||||
bounds: WeatherV2Bounds;
|
||||
width: number;
|
||||
height: number;
|
||||
sourceUrl: string;
|
||||
}>;
|
||||
```
|
||||
|
||||
该方法会:
|
||||
|
||||
```text
|
||||
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` 的调用方:
|
||||
|
||||
```ts
|
||||
const canonical = decodeWeatherV2WindFrame(arrayBuffer, frameRef);
|
||||
```
|
||||
|
||||
签名等价于:
|
||||
|
||||
```ts
|
||||
decodeWeatherV2WindFrame(
|
||||
source: ArrayBuffer,
|
||||
frame: WeatherV2WindFrameRef,
|
||||
): CanonicalWindField
|
||||
```
|
||||
|
||||
一般业务代码优先使用 `WeatherV2HttpClient.fetchCanonicalWindFrame()`,不需要直接调用 decoder。
|
||||
## 13. `WeatherV2BinaryFileStore`
|
||||
|
||||
WeatherV2 helper 不绑定 Expo FileSystem。宿主只需提供:
|
||||
|
||||
```ts
|
||||
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 示例:
|
||||
|
||||
```ts
|
||||
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 串成一条流程。
|
||||
|
||||
参数类型:
|
||||
|
||||
```ts
|
||||
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";
|
||||
}>;
|
||||
```
|
||||
|
||||
调用:
|
||||
|
||||
```ts
|
||||
const loaded = await loadWeatherV2WindIntoRuntime({
|
||||
reactTag,
|
||||
client,
|
||||
fileStore,
|
||||
datasetId: "ecmwf-hres",
|
||||
referenceTime: "2026080600",
|
||||
targetTimes: ["2026080700", "2026080706", "2026080712", "2026080718"],
|
||||
weatherColorVisible: true,
|
||||
particleVisible: true,
|
||||
playbackState: "PAUSE",
|
||||
});
|
||||
```
|
||||
内部顺序固定为:
|
||||
|
||||
```text
|
||||
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)`,应先:
|
||||
|
||||
```ts
|
||||
await WeatherRuntime.setWeatherEnabled(reactTag, true);
|
||||
```
|
||||
|
||||
再调用完整 WeatherV2 loader。
|
||||
|
||||
返回类型:
|
||||
```ts
|
||||
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`,也可以注入自定义实现,只要满足:
|
||||
|
||||
```ts
|
||||
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。
|
||||
|
||||
```tsx
|
||||
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. 错误处理
|
||||
|
||||
需要区分两类失败:
|
||||
|
||||
1. Promise reject:参数格式错误、MapView 找不到、style 未 ready、Runtime 已 dispose、本地文件不可读等 Bridge/生命周期错误;
|
||||
2. Promise resolve 但返回 `REJECTED` / `REJECTED_*`:请求到达 Native/Core,但被 Runtime、Adapter、Timeline 或 capacity 契约拒绝。
|
||||
建议所有 command 都检查结果:
|
||||
|
||||
```ts
|
||||
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 的责任边界
|
||||
|
||||
```text
|
||||
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 逻辑为:
|
||||
|
||||
```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:
|
||||
|
||||
```text
|
||||
.../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` 时才需要这些类型:
|
||||
|
||||
```ts
|
||||
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 调用层级
|
||||
|
||||
普通业务优先级:
|
||||
|
||||
```text
|
||||
第一选择: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 公共控制面。
|
||||
Reference in New Issue
Block a user