Files
MapLibreParticleLab/integration/react-native/MapLibreParticleRNTestHost/App.tsx
OpenAI Codex 8588e6fcb8
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
feat: add V3 field fixtures and RN regression host
2026-09-11 09:56:53 +08:00

91 lines
8.9 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from "react";
import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native";
import { Camera, Map } from "@maplibre/maplibre-react-native";
import { FieldRuntime, type FieldColorStop, type RuntimeSnapshot, type TargetFps } from "@navsea/maplibre-particle-rn";
import { fieldDatasets, materializeV3Frame, type V3Dataset } from "./src/fieldDatasets";
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: FieldColorStop[] = [
{ value: 0, red: 0.08, green: 0.2, blue: 0.8, alpha: 0.72 }, { value: 8, red: 0, green: 0.85, blue: 0.9, alpha: 0.72 },
{ value: 16, red: 1, green: 0.85, blue: 0.05, alpha: 0.72 }, { value: 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 [basemap, setBasemap] = useState("LOADING");
const [frames, setFrames] = useState<Record<string, string>>({ T0: "PENDING", T1: "PENDING" });
const [dataset, setDataset] = useState<V3Dataset>(fieldDatasets[0]);
const [transport, setTransport] = useState("PENDING: V2 FILE_REF / FLOAT32_LE");
const [weatherVisible, setWeatherVisible] = useState(true);
const [particleVisible, setParticleVisible] = useState(true);
const [visual, setVisual] = useState(initialVisual);
const refresh = useCallback(async (tag: number) => setSnapshot(await FieldRuntime.getState(tag)), []);
const attach = useCallback(async (tag: number) => {
try { await FieldRuntime.attach(tag); await refresh(tag); setReactTag(tag); setResult("FieldRuntime 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 FieldRuntime.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 GSI/style ready");
try { const value = await operation(reactTag); await refresh(reactTag); setResult(`${name}: ${String(value)}`); }
catch (error) { setResult(`${name}: ${String(error)}`); }
}, [reactTag, refresh]);
const loadDataset = useCallback(async () => {
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
const expected = ["ACCEPTED_PENDING_INITIAL_WINDOW", "INITIAL_WINDOW_READY"];
try {
setTransport("LOADING: V2 metadata + validity/scalar/vector local file:// URIs"); setFrames({ T0: "PENDING", T1: "PENDING" });
if (await FieldRuntime.clearFields(reactTag) !== "ACCEPTED") throw new Error("clearFields was rejected");
await FieldRuntime.setFieldValueSource(reactTag, dataset.fieldValueSource); await FieldRuntime.setParticleVisible(reactTag, dataset.particle); await FieldRuntime.setParticleAdvectionScale(reactTag, dataset.advectionScale);
const localFrames = await Promise.all(dataset.frames.map(materializeV3Frame));
for (let index = 0; index < localFrames.length; index += 1) {
const frame = localFrames[index]; const submit = await FieldRuntime.submitField(reactTag, frame.metadataJson, frame.payloadRefs);
setFrames((current) => ({ ...current, [frame.label]: submit }));
if (submit !== expected[index]) throw new Error(`${frame.label} expected ${expected[index]}, got ${submit}`);
}
const native = await FieldRuntime.getState(reactTag); setSnapshot(native);
if (native.state !== "READY") throw new Error(`Native WeatherRuntime expected READY, got ${native.state}`);
setTransport("READY: V2 FILE_REF / FLOAT32_LE / shared validity"); setResult(`${dataset.title}: T0/T1 accepted; FieldRuntime READY`);
} catch (error) { setTransport(`ERROR: ${String(error)}`); setResult(`LOAD ${dataset.id}: ${String(error)}`); }
}, [reactTag, dataset]);
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([
FieldRuntime.setParticleCount(tag, Number(visual.count)), FieldRuntime.setParticleLineWidth(tag, Number(visual.width)), FieldRuntime.setParticleLength(tag, Number(visual.length)), FieldRuntime.setParticleOpacity(tag, Number(visual.opacity)), FieldRuntime.setTrailOpacity(tag, Number(visual.trailOpacity)), FieldRuntime.setTrailFade(tag, Number(visual.trailFade)), FieldRuntime.setFieldOpacity(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) => FieldRuntime.setPlaybackState(tag, "PLAY")], ["PAUSE", (tag: number) => FieldRuntime.setPlaybackState(tag, "PAUSE")], ["HALF", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "HALF")], ["NORMAL", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "NORMAL")], ["DOUBLE", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "DOUBLE")],
["FPS AUTO", (tag: number) => FieldRuntime.setTargetFps(tag, "AUTO" as TargetFps)], ["FPS 30", (tag: number) => FieldRuntime.setTargetFps(tag, "FPS_30" as TargetFps)], ["FPS 60", (tag: number) => FieldRuntime.setTargetFps(tag, "FPS_60" as TargetFps)], ["Field stops", (tag: number) => FieldRuntime.setFieldColorStops(tag, frozenStops)], ["Particle color", (tag: number) => FieldRuntime.setParticleColor(tag, 1, 1, 1, 0.95)], ["Trail color", (tag: number) => FieldRuntime.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} 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: {dataset.title}</Text><Text>Runtime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps}</Text><Text selectable>V2 FILE TRANSPORT = {transport}</Text>
{dataset.frames.map((frame) => <Text selectable key={frame.label}>{frame.label}: {frames[frame.label]}</Text>)}
<Text selectable>{result}</Text><View style={styles.row}>{fieldDatasets.map((item) => <Button key={item.id} title={item.id} onPress={() => setDataset(item)} />)}</View><Button title="LOAD SELECTED V3 DATASET" onPress={() => void loadDataset()} />
<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) => FieldRuntime.setFieldVisible(tag, next)); }} /><Button title={`Particle ${particleVisible ? "visible" : "hidden"}`} onPress={() => { const next = !particleVisible; setParticleVisible(next); void command("particle visible", (tag) => FieldRuntime.setParticleVisible(tag, next)); }} /></View>
<View style={styles.row}>{dataset.frames.map((frame) => <Button key={`seek-${frame.label}`} title={`seek ${frame.label}`} onPress={() => void command(`seek ${frame.label}`, (tag) => FieldRuntime.setTargetTime(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: 310, backgroundColor: "#fff" }, content: { gap: 7, padding: 10 }, row: { flexDirection: "row", flexWrap: "wrap", gap: 6, alignItems: "center" }, input: { borderWidth: 1, minWidth: 48, padding: 5 } });