feat: add WeatherV2 HTTP particle provider
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-09 20:09:28 +08:00
parent bcd020fc55
commit e15f583c65
11 changed files with 615 additions and 7 deletions

View File

@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native";
import { Camera, Map, WeatherRuntime, type ColorStop, type RuntimeSnapshot, type TargetFps } from "@navsea/maplibre-particle-rn";
import { materializeRealWeatherFrames, realWeatherFrames } from "./src/realWeatherDataset";
import { loadHttpWeatherV2 } from "./src/httpWeatherV2Demo";
import { weatherTestStyle } from "./src/weatherStyle";
const initialSnapshot: RuntimeSnapshot = { state: "WAITING_FOR_FIELDS", playbackState: "PAUSE", targetFps: "AUTO", disposed: false };
@@ -18,6 +19,7 @@ export default function App() {
const [basemap, setBasemap] = useState("LOADING");
const [frames, setFrames] = useState<Record<string, string>>({ T0: "PENDING", T1: "PENDING", T2: "PENDING", T3: "PENDING" });
const [transport, setTransport] = useState("PENDING: FILE_REF / FLOAT32_LE / INTERLEAVED_UV");
const [httpTransport, setHttpTransport] = useState("PENDING: WeatherV2 HTTP → canonical U/V → WeatherRuntime");
const [weatherVisible, setWeatherVisible] = useState(true);
const [particleVisible, setParticleVisible] = useState(true);
const [visual, setVisual] = useState(initialVisual);
@@ -57,6 +59,21 @@ export default function App() {
} catch (error) { setTransport(`ERROR: ${String(error)}`); setResult(`LOAD REAL WEATHER T0-T3: ${String(error)}`); }
}, [reactTag]);
const loadHttpWeather = useCallback(async () => {
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
try {
setHttpTransport("LOADING: manifest → time index → WVECBIN1 → canonical U/V");
const loaded = await loadHttpWeatherV2(reactTag);
setSnapshot(loaded.runtime);
setFrames(Object.fromEntries(loaded.frames.map((frame, index) => [`T${index}`, frame.submitResult])) as Record<string, string>);
setHttpTransport(`READY: ${loaded.frames.length} HTTP frames / ${loaded.context.referenceTime}`);
setResult(`HTTP WeatherV2 READY: ${loaded.frames.map((frame) => frame.fieldId).join(", ")}`);
} catch (error) {
setHttpTransport(`ERROR: ${String(error)}`);
setResult(`LOAD HTTP WEATHER: ${String(error)}`);
}
}, [reactTag]);
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([
@@ -76,6 +93,7 @@ export default function App() {
<Text>Basemap: GSI {basemap}</Text><Text>Dataset: ECMWF HRES 20260807 · 513×769 · 3.01 MiB/frame</Text><Text>Runtime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps}</Text><Text selectable>REAL DATA TRANSPORT = {transport}</Text>
{realWeatherFrames.map((frame) => <Text selectable key={frame.label}>{frame.label} {frame.cycle}: {frames[frame.label]}</Text>)}
<Text selectable>{result}</Text><Button title="LOAD REAL WEATHER T0-T3" onPress={() => void loadRealWeather()} />
<Text selectable>HTTP DATA TRANSPORT = {httpTransport}</Text><Button title="LOAD HTTP WEATHER T0-T3" onPress={() => void loadHttpWeather()} />
<View style={styles.row}>{controls.map(([label, operation]) => <Button key={label} title={label} onPress={() => void command(label, operation)} />)}</View>
<View style={styles.row}><Button title={`Weather ${weatherVisible ? "visible" : "hidden"}`} onPress={() => { const next = !weatherVisible; setWeatherVisible(next); void command("weather visible", (tag) => WeatherRuntime.setWeatherColorVisible(tag, next)); }} /><Button title={`Particle ${particleVisible ? "visible" : "hidden"}`} onPress={() => { const next = !particleVisible; setParticleVisible(next); void command("particle visible", (tag) => WeatherRuntime.setParticleVisible(tag, next)); }} /></View>
<View style={styles.row}>{realWeatherFrames.map((frame) => <Button key={`seek-${frame.label}`} title={`seek ${frame.label}`} onPress={() => void command(`seek ${frame.label}`, (tag) => WeatherRuntime.setForecastTime(tag, frame.validTime))} />)}</View>

View File

@@ -2,8 +2,10 @@
这是正式发布包 Consumer 验收 Host。它用于模拟 NavSea 的升级方式:Consumer 只直接安装 `@navsea/maplibre-particle-rn@13.4.1-particle.1`,由该 npm 包接入 React Native,并由 Gradle 从 `https://pbf.jpcn.ccwu.cc/maven` 解析正式 Particle AAR。
禁止直接依赖 `@maplibre/maplibre-react-native`、旧 `weather-runtime-bridge`、MapLibre Native source tree、Maven Local、本地 AAR 或 workspace/source-path bridge。Map、Camera、WeatherRuntime 统一从 `@navsea/maplibre-particle-rn` 导入。
禁止直接依赖 `@maplibre/maplibre-react-native`、旧 `weather-runtime-bridge`、MapLibre Native source tree、Maven Local、本地 AAR 或 workspace/source-path bridge。Map、Camera、WeatherRuntime、WeatherV2 HTTP client 统一从 `@navsea/maplibre-particle-rn` 导入。
当前验收数据复用 STEP-062 已通过的真实 WeatherV2 fixture:ECMWF HRES 20260807,513×769,T0/T1/T2/T3 四个 canonical `float32-le / interleaved-uv` 风场。`.f32` 通过 Expo Asset materialize 为本地 `file://` URI 后提交给 WeatherRuntime,payload 不进入 JS 数组。
Host 保留两条可对照数据链:`LOAD REAL WEATHER T0-T3` 使用 STEP-062 已通过的本地 canonical fixture;`LOAD HTTP WEATHER T0-T3` 从 `https://weather.jpcn.ccwu.cc` 获取 manifest/time-index/WVECBIN1,由包内 JS/TS converter 生成同一 canonical `float32-le / interleaved-uv` U/V 文件后提交 WeatherRuntime。
验收目标是同时看到 GSI 底图、WeatherColor 与 Particle,并确认 T0~T3 顺序接受、Runtime 进入 READY。这样验证的是“正式 npm 包 → 正式 Maven 仓库 → 正式 AAR → RN/Native/Core”的完整升级链,而不是源码工程或开发期 AAR。
HTTP 回归固定使用 ECMWF HRES `referenceTime=2026080600` 与 2026080700/06/12/18 四帧,以便和既有 fixture 做同源对照。正式 NavSea 不应固定这些时次,而应通过 `getWindContext()` 获取服务器可用时次后按业务策略选择。
验收目标是同时看到 GSI 底图、WeatherColor 与 Particle,并确认 T0~T3 顺序接受、Runtime 进入 READY。这样验证的是“正式 npm 包 → WeatherV2 HTTP/转换 → 正式 Maven AAR → RN/Native/Core”的完整升级链。

View File

@@ -0,0 +1,40 @@
import { File, Paths } from "expo-file-system";
import {
WeatherV2HttpClient,
loadWeatherV2WindIntoRuntime,
type LoadWeatherV2WindResult,
type WeatherV2BinaryFileStore,
} from "@navsea/maplibre-particle-rn";
const weatherClient = new WeatherV2HttpClient({
baseUrl: "https://weather.jpcn.ccwu.cc",
});
const fileStore: WeatherV2BinaryFileStore = {
async writeTempFile(fileName, bytes) {
const file = new File(Paths.cache, fileName);
file.write(bytes);
return file.uri;
},
};
const frozenTargetTimes = [
"2026080700",
"2026080706",
"2026080712",
"2026080718",
] as const;
export async function loadHttpWeatherV2(reactTag: number): Promise<LoadWeatherV2WindResult> {
return loadWeatherV2WindIntoRuntime({
reactTag,
client: weatherClient,
fileStore,
datasetId: "ecmwf-hres",
referenceTime: "2026080600",
targetTimes: frozenTargetTimes,
weatherColorVisible: true,
particleVisible: true,
playbackState: "PAUSE",
});
}