feat: add STEP-062 real weather test map
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:
@@ -1,107 +1,88 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native";
|
||||
import { Map } from "@maplibre/maplibre-react-native";
|
||||
import { WeatherRuntimeBridge, type RuntimeSnapshot, type TargetFps } from "@maplibre-particle/weather-runtime-bridge";
|
||||
import { writeCanonicalWindFixture } from "./src/canonicalWindFixture";
|
||||
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 { 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 },
|
||||
];
|
||||
|
||||
export default function App() {
|
||||
const [reactTag, setReactTag] = useState<number | null>(null);
|
||||
const [snapshot, setSnapshot] = useState<RuntimeSnapshot>(initialSnapshot);
|
||||
const [result, setResult] = useState("Style is loading");
|
||||
const [opacity, setOpacity] = useState("0.62");
|
||||
const [binaryTransport, setBinaryTransport] = useState("PENDING: submit fixture to validate FILE_REF / FLOAT32_LE / INTERLEAVED_UV");
|
||||
const [invalidProbe, setInvalidProbe] = useState("PENDING");
|
||||
const [t0Submit, setT0Submit] = useState("PENDING");
|
||||
const [t1Submit, setT1Submit] = useState("PENDING");
|
||||
const [fieldWindow, setFieldWindow] = useState("TIMELINE_NOT_READY");
|
||||
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 [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 attach = useCallback(async (tag: number) => {
|
||||
try { setSnapshot(await WeatherRuntimeBridge.attach(tag)); setReactTag(tag); setResult("WeatherRuntime attached"); }
|
||||
catch (error) { setResult(String(error)); }
|
||||
}, []);
|
||||
|
||||
// MLRN forwards the native MapView event. Its target is the concrete Fabric MLRNMapView tag;
|
||||
// this is the same tag consumed by MLRN's own NativeMapViewModule.
|
||||
try { await WeatherRuntimeBridge.attach(tag); await refresh(tag); setReactTag(tag); setResult("WeatherRuntime 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 WeatherRuntimeBridge.detach(reactTag).catch(() => undefined); }, [reactTag]);
|
||||
|
||||
const command = useCallback(async (name: string, operation: (tag: number) => Promise<unknown>) => {
|
||||
if (reactTag == null) return setResult("MAP_NOT_READY: wait for style ready");
|
||||
try { const value = await operation(reactTag); setResult(`${name}: ${String(value)}`); setSnapshot(await WeatherRuntimeBridge.getState(reactTag)); }
|
||||
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
|
||||
try { const value = await operation(reactTag); await refresh(reactTag); setResult(`${name}: ${String(value)}`); }
|
||||
catch (error) { setResult(`${name}: ${String(error)}`); }
|
||||
}, [reactTag]);
|
||||
}, [reactTag, refresh]);
|
||||
|
||||
const submitBinaryFixture = useCallback(async () => {
|
||||
if (reactTag == null) return setResult("MAP_NOT_READY: wait for style ready");
|
||||
const loadRealWeather = 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"];
|
||||
try {
|
||||
setBinaryTransport("PENDING: clearing fields and validating FILE_REF / FLOAT32_LE / INTERLEAVED_UV");
|
||||
setInvalidProbe("PENDING");
|
||||
setT0Submit("PENDING");
|
||||
setT1Submit("PENDING");
|
||||
setFieldWindow("PENDING: awaiting Core initial window");
|
||||
const clear = await WeatherRuntimeBridge.clearWindFields(reactTag);
|
||||
if (clear !== "ACCEPTED") throw new Error(`clearWindFields: ${clear}`);
|
||||
const [t0, t1, invalid] = writeCanonicalWindFixture();
|
||||
const rejected = await WeatherRuntimeBridge.submitWindField(reactTag, invalid.metadataJson, invalid.payloadFileUri);
|
||||
setInvalidProbe(rejected);
|
||||
if (rejected !== "REJECTED_ADAPTER") throw new Error(`invalid probe expected REJECTED_ADAPTER, got ${rejected}`);
|
||||
const t0Result = await WeatherRuntimeBridge.submitWindField(reactTag, t0.metadataJson, t0.payloadFileUri);
|
||||
setT0Submit(t0Result);
|
||||
if (t0Result !== "ACCEPTED_PENDING_INITIAL_WINDOW") throw new Error(`T0 expected ACCEPTED_PENDING_INITIAL_WINDOW, got ${t0Result}`);
|
||||
const t1Result = await WeatherRuntimeBridge.submitWindField(reactTag, t1.metadataJson, t1.payloadFileUri);
|
||||
setT1Submit(t1Result);
|
||||
if (t1Result !== "INITIAL_WINDOW_READY") throw new Error(`T1 expected INITIAL_WINDOW_READY, got ${t1Result}`);
|
||||
const native = await WeatherRuntimeBridge.getState(reactTag);
|
||||
setSnapshot(native);
|
||||
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();
|
||||
for (let index = 0; index < localFrames.length; index += 1) {
|
||||
const frame = localFrames[index]; const submit = await WeatherRuntimeBridge.submitWindField(reactTag, frame.metadataJson, frame.payloadFileUri);
|
||||
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);
|
||||
if (native.state !== "READY") throw new Error(`Native WeatherRuntime expected READY, got ${native.state}`);
|
||||
setFieldWindow(`Core evidence: INITIAL_WINDOW_READY + runtime READY; producer identity: T0=${t0.generation}@${t0.validTime}, T1=${t1.generation}@${t1.validTime}; MIX: Core-owned / not exposed`);
|
||||
setBinaryTransport("READY: FILE_REF / FLOAT32_LE / INTERLEAVED_UV");
|
||||
setResult(`Binary field submit: invalid=${rejected}, T0=${t0Result}, T1=${t1Result}`);
|
||||
} catch (error) {
|
||||
setBinaryTransport(`ERROR: ${String(error)}`);
|
||||
setResult(`Binary field submit: ${String(error)}`);
|
||||
setFieldWindow("TIMELINE_NOT_READY");
|
||||
}
|
||||
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]);
|
||||
|
||||
const buttons = useMemo(() => [
|
||||
["getState", (tag: number) => WeatherRuntimeBridge.getState(tag)],
|
||||
["enable", (tag: number) => WeatherRuntimeBridge.setWeatherEnabled(tag, true)],
|
||||
["weather visible", (tag: number) => WeatherRuntimeBridge.setWeatherColorVisible(tag, true)],
|
||||
["particle visible", (tag: number) => WeatherRuntimeBridge.setParticleVisible(tag, true)],
|
||||
["FPS 30", (tag: number) => WeatherRuntimeBridge.setTargetFps(tag, "FPS_30" as TargetFps)],
|
||||
["PLAY", (tag: number) => WeatherRuntimeBridge.setPlaybackState(tag, "PLAY")],
|
||||
["PAUSE", (tag: number) => WeatherRuntimeBridge.setPlaybackState(tag, "PAUSE")]
|
||||
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)),
|
||||
]);
|
||||
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)],
|
||||
] as const, []);
|
||||
|
||||
return <SafeAreaView style={styles.root}>
|
||||
<View style={styles.map}><Map mapStyle={weatherTestStyle} onDidFinishLoadingStyle={onStyleReady} /></View>
|
||||
<ScrollView style={styles.controls} contentContainerStyle={styles.controlContent}>
|
||||
<Text>reactTag: {reactTag ?? "not attached"}</Text>
|
||||
<Text>runtime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps}</Text>
|
||||
<Text selectable>Binary Transport: {binaryTransport}</Text>
|
||||
<Text selectable>Invalid Probe: {invalidProbe}</Text>
|
||||
<Text selectable>T0 Submit Result: {t0Submit}</Text>
|
||||
<Text selectable>T1 Submit Result: {t1Submit}</Text>
|
||||
<Text selectable>Runtime Field Window: {fieldWindow}</Text>
|
||||
<Text selectable>{result}</Text>
|
||||
<Button title="Submit binary WIND T0/T1" onPress={() => void submitBinaryFixture()} />
|
||||
{buttons.map(([label, operation]) => <Button key={label} title={label} onPress={() => void command(label, operation)} />)}
|
||||
<TextInput value={opacity} onChangeText={setOpacity} keyboardType="decimal-pad" style={styles.input} />
|
||||
<Button title="weather opacity" onPress={() => void command("weather opacity", (tag) => WeatherRuntimeBridge.setWeatherColorOpacity(tag, Number(opacity)))} />
|
||||
<View style={styles.map}><Map mapStyle={weatherTestStyle} onDidFinishLoadingStyle={onStyleReady} onDidFinishLoadingMap={() => setBasemap("READY")} onDidFailLoadingMap={() => { setBasemap("ERROR"); setResult("map loading error"); }}><Camera initialViewState={{ center: [135, 32], zoom: 3.9 }} /></Map></View>
|
||||
<ScrollView style={styles.controls} contentContainerStyle={styles.content}>
|
||||
<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()} />
|
||||
<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) => WeatherRuntimeBridge.setWeatherColorVisible(tag, next)); }} /><Button title={`Particle ${particleVisible ? "visible" : "hidden"}`} onPress={() => { const next = !particleVisible; setParticleVisible(next); void command("particle visible", (tag) => WeatherRuntimeBridge.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) => WeatherRuntimeBridge.setForecastTime(tag, frame.validTime))} />)}</View>
|
||||
<Text>Visual: count / width / length / particle opacity / trail opacity / trail fade / WeatherColor opacity</Text><View style={styles.row}>{(Object.keys(visual) as Array<keyof typeof initialVisual>).map((key) => <TextInput key={key} value={visual[key]} onChangeText={(value) => updateVisual(key, value)} keyboardType="decimal-pad" style={styles.input} />)}<Button title="Apply visual" onPress={applyVisual} /></View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: { flex: 1 }, map: { flex: 1 }, controls: { maxHeight: 280 }, controlContent: { gap: 8, padding: 12 }, input: { borderWidth: 1, padding: 6 }
|
||||
});
|
||||
const styles = StyleSheet.create({ root: { flex: 1 }, map: { flex: 1 }, controls: { maxHeight: 310, backgroundColor: "#fff" }, content: { gap: 7, padding: 10 }, row: { flexDirection: "row", flexWrap: "wrap", gap: 6, alignItems: "center" }, input: { borderWidth: 1, minWidth: 48, padding: 5 } });
|
||||
|
||||
@@ -47,6 +47,14 @@ Style JSON 明确创建 background、host-field `RasterArraySource`、`WeatherCo
|
||||
|
||||
启动入口是 `App.tsx`。`app.json` 已固定 `orientation: landscape`;下一次 Android prebuild/build 应生成 `MainActivity android:screenOrientation="landscape"`。Android build、安装及设备操作必须由用户手工执行,详见 `docs/60_测试/STEP-060_RNTestHost_Android人工编译与验收_v1.md`。
|
||||
|
||||
## STEP-062 真实 WeatherV2 综合地图
|
||||
|
||||
Test Host 固定消费已验收的 `org.maplibre.gl:android-sdk-opengl:13.4.1-step061`,本阶段不构建或发布 AAR。页面 style 顺序是 `background → GSI raster → WeatherColorLayer → RasterParticleLayer`;GSI 使用国土地理院标准地图 `https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png`(256、maxzoom 18、含 attribution),相机初始聚焦日本及周边 WeatherV2 bounds。
|
||||
|
||||
受控 assets `assets/weather/ecmwf-hres-wind-20260807/` 原样复制 Android Validation 的 `dataset.json` 与四个 `metadata.json`/`field.f32`。`expo-asset` 是明确直接依赖,`metro.config.js` 仅增加 `.f32` asset extension。点击 `LOAD REAL WEATHER T0-T3` 会先 clear,然后通过 `Asset.fromModule(...).downloadAsync()` 取得四个 Expo cache `file://` URI,按 producer validTime 提交;JS 只传小型 metadata JSON 和文件引用,不 materialize 3.01 MiB payload 为数组、JSON、base64 或 hex。
|
||||
|
||||
硬断言依次为 T0=`ACCEPTED_PENDING_INITIAL_WINDOW`、T1=`INITIAL_WINDOW_READY`、T2/T3=`ACCEPTED_NEXT_SNAPSHOT`,并在最后要求 Native state=`READY`;任何偏差均显示 `ERROR`。界面提供 PLAY/PAUSE、HALF/NORMAL/DOUBLE、四个 epoch-ms forecast seek、AUTO/30/60 FPS、两层可见性、冻结 WeatherColor stops、独立 particle/trail 色彩,以及粒子/轨迹视觉参数。Expo/系统管理 asset cache;本页不自建 `.f32` 临时文件。
|
||||
|
||||
## Windows 安装与 Metro reverse
|
||||
|
||||
Test Host 的 Metro 固定在 8091,但 Debug APK 的 RN dev-server 探测仍从设备 `localhost:8081` 发起。手工安装前以受控脚本 `scripts/05-install-and-run.ps1` 保留两条 reverse:`tcp:8091 -> tcp:8091` 与 `tcp:8081 -> tcp:8091`。脚本固定设备 `192.168.1.146`、package `org.maplibre.particle.rntesthost`、activity `.MainActivity`,并在安装前完成 reverse。
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"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,
|
||||
"sha256": "363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283"
|
||||
},
|
||||
"source": {
|
||||
"kind": "WeatherV2 ECMWF HRES WVECBIN1",
|
||||
"referenceTime": "2026080600",
|
||||
"targetTime": "2026080700",
|
||||
"sourceBytes": 5128989,
|
||||
"resampled": false
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"schema": "maplibre-particle-field/v1",
|
||||
"fieldId": "ecmwf-hres-wind-2026080706",
|
||||
"fieldType": "WIND",
|
||||
"displayDomain": "GLOBAL",
|
||||
"generation": 1786082400000,
|
||||
"validTime": 1786082400000,
|
||||
"dataVersion": 1786082400000,
|
||||
"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,
|
||||
"sha256": "ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451"
|
||||
},
|
||||
"source": {
|
||||
"kind": "WeatherV2 ECMWF HRES WVECBIN1",
|
||||
"referenceTime": "2026080600",
|
||||
"targetTime": "2026080706",
|
||||
"sourceBytes": 5128989,
|
||||
"resampled": false
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"schema": "maplibre-particle-field/v1",
|
||||
"fieldId": "ecmwf-hres-wind-2026080712",
|
||||
"fieldType": "WIND",
|
||||
"displayDomain": "GLOBAL",
|
||||
"generation": 1786104000000,
|
||||
"validTime": 1786104000000,
|
||||
"dataVersion": 1786104000000,
|
||||
"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,
|
||||
"sha256": "d86a6a51fd5216cf9368756962a5366fd32a627afd40cfde235ba7358c02c5a0"
|
||||
},
|
||||
"source": {
|
||||
"kind": "WeatherV2 ECMWF HRES WVECBIN1",
|
||||
"referenceTime": "2026080600",
|
||||
"targetTime": "2026080712",
|
||||
"sourceBytes": 5128989,
|
||||
"resampled": false
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"schema": "maplibre-particle-field/v1",
|
||||
"fieldId": "ecmwf-hres-wind-2026080718",
|
||||
"fieldType": "WIND",
|
||||
"displayDomain": "GLOBAL",
|
||||
"generation": 1786125600000,
|
||||
"validTime": 1786125600000,
|
||||
"dataVersion": 1786125600000,
|
||||
"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,
|
||||
"sha256": "970785b0334a9d1511b8897874a81f681814a32b1e0b90e37cf64b77ca9fa39b"
|
||||
},
|
||||
"source": {
|
||||
"kind": "WeatherV2 ECMWF HRES WVECBIN1",
|
||||
"referenceTime": "2026080600",
|
||||
"targetTime": "2026080718",
|
||||
"sourceBytes": 5128989,
|
||||
"resampled": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"schema": "maplibre-particle-test-dataset/v1",
|
||||
"datasetId": "ecmwf-hres-wind-20260807",
|
||||
"source": "WeatherV2 ECMWF HRES WVECBIN1",
|
||||
"referenceTime": "2026080600",
|
||||
"validTimes": [
|
||||
"2026080700",
|
||||
"2026080706",
|
||||
"2026080712",
|
||||
"2026080718"
|
||||
],
|
||||
"snapshotLayout": "<validTime>/metadata.json + <validTime>/field.f32",
|
||||
"notes": "开发期离线 canonical 测试索引;不是 Weather Manifest,也不进入 ParticleFieldSnapshot。"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
|
||||
const config = getDefaultConfig(__dirname);
|
||||
config.resolver.assetExts = [...config.resolver.assetExts, "f32"];
|
||||
|
||||
module.exports = config;
|
||||
@@ -11,6 +11,7 @@
|
||||
"@maplibre-particle/weather-runtime-bridge": "file:./modules/weather-runtime-bridge",
|
||||
"@maplibre/maplibre-react-native": "11.3.6",
|
||||
"expo": "56.0.4",
|
||||
"expo-asset": "~56.0.14",
|
||||
"expo-file-system": "56.0.11",
|
||||
"react": "19.2.3",
|
||||
"react-native": "0.85.2"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"@maplibre-particle/weather-runtime-bridge": "file:./modules/weather-runtime-bridge",
|
||||
"@maplibre/maplibre-react-native": "11.3.6",
|
||||
"expo": "56.0.4",
|
||||
"expo-asset": "~56.0.14",
|
||||
"expo-file-system": "56.0.11",
|
||||
"react": "19.2.3",
|
||||
"react-native": "0.85.2"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Asset } from "expo-asset";
|
||||
|
||||
export type RealWeatherFrame = Readonly<{
|
||||
label: "T0" | "T1" | "T2" | "T3";
|
||||
cycle: "2026080700" | "2026080706" | "2026080712" | "2026080718";
|
||||
metadataJson: string;
|
||||
validTime: number;
|
||||
generation: number;
|
||||
payloadModule: number;
|
||||
}>;
|
||||
|
||||
type CanonicalMetadata = Readonly<{ validTime: number; generation: number }>;
|
||||
|
||||
// Static requires register all four raw .f32 assets with Metro/Expo; payload bytes never enter JS memory.
|
||||
const metadataT0 = require("../assets/weather/ecmwf-hres-wind-20260807/2026080700/metadata.json") as CanonicalMetadata;
|
||||
const metadataT1 = require("../assets/weather/ecmwf-hres-wind-20260807/2026080706/metadata.json") as CanonicalMetadata;
|
||||
const metadataT2 = require("../assets/weather/ecmwf-hres-wind-20260807/2026080712/metadata.json") as CanonicalMetadata;
|
||||
const metadataT3 = require("../assets/weather/ecmwf-hres-wind-20260807/2026080718/metadata.json") as CanonicalMetadata;
|
||||
|
||||
function frame(label: RealWeatherFrame["label"], cycle: RealWeatherFrame["cycle"], metadata: CanonicalMetadata, payloadModule: number): RealWeatherFrame {
|
||||
return { label, cycle, metadataJson: JSON.stringify(metadata), validTime: metadata.validTime, generation: metadata.generation, payloadModule };
|
||||
}
|
||||
|
||||
export const realWeatherFrames = [
|
||||
frame("T0", "2026080700", metadataT0, require("../assets/weather/ecmwf-hres-wind-20260807/2026080700/field.f32")),
|
||||
frame("T1", "2026080706", metadataT1, require("../assets/weather/ecmwf-hres-wind-20260807/2026080706/field.f32")),
|
||||
frame("T2", "2026080712", metadataT2, require("../assets/weather/ecmwf-hres-wind-20260807/2026080712/field.f32")),
|
||||
frame("T3", "2026080718", metadataT3, require("../assets/weather/ecmwf-hres-wind-20260807/2026080718/field.f32")),
|
||||
] as const;
|
||||
|
||||
export async function materializeRealWeatherFrames(): Promise<ReadonlyArray<RealWeatherFrame & { payloadFileUri: string }>> {
|
||||
return Promise.all(realWeatherFrames.map(async (weatherFrame) => {
|
||||
const asset = Asset.fromModule(weatherFrame.payloadModule);
|
||||
await asset.downloadAsync();
|
||||
if (asset.localUri == null || !asset.localUri.startsWith("file://")) {
|
||||
throw new Error(`${weatherFrame.label} Asset.downloadAsync did not provide a local file:// URI`);
|
||||
}
|
||||
return { ...weatherFrame, payloadFileUri: asset.localUri };
|
||||
}));
|
||||
}
|
||||
@@ -1,15 +1,25 @@
|
||||
/** No field is embedded here: RasterArraySource is reserved for STEP-061 binary submission. */
|
||||
/** No field is embedded here: RasterArraySource is reserved for STEP-061 file-reference submission. */
|
||||
export const WEATHER_SOURCE_ID = "rn-weather-fields";
|
||||
export const WEATHER_COLOR_LAYER_ID = "rn-weather-color";
|
||||
export const PARTICLE_LAYER_ID = "rn-weather-particles";
|
||||
export const GSI_SOURCE_ID = "gsi-standard";
|
||||
export const GSI_LAYER_ID = "gsi-standard";
|
||||
|
||||
export const weatherTestStyle = JSON.stringify({
|
||||
version: 8,
|
||||
sources: {
|
||||
[GSI_SOURCE_ID]: {
|
||||
type: "raster",
|
||||
tiles: ["https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png"],
|
||||
tileSize: 256,
|
||||
maxzoom: 18,
|
||||
attribution: "\u56fd\u571f\u5730\u7406\u9662 / GSI"
|
||||
},
|
||||
[WEATHER_SOURCE_ID]: { type: "raster-array", "host-field-submission": true }
|
||||
},
|
||||
layers: [
|
||||
{ id: "rn-basemap", type: "background", paint: { "background-color": "#10243a" } },
|
||||
{ id: GSI_LAYER_ID, type: "raster", source: GSI_SOURCE_ID, paint: { "raster-opacity": 1 } },
|
||||
{ id: WEATHER_COLOR_LAYER_ID, type: "weather-color", source: WEATHER_SOURCE_ID,
|
||||
paint: { "weather-color-opacity": 0.62 } },
|
||||
{ id: PARTICLE_LAYER_ID, type: "raster-particle", source: WEATHER_SOURCE_ID,
|
||||
|
||||
Reference in New Issue
Block a user