89 lines
8.6 KiB
TypeScript
89 lines
8.6 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from "react";
|
||
import { Camera, Map } from "@maplibre/maplibre-react-native";
|
||
import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native";
|
||
import { FieldRuntime, type FieldColorStop, type RuntimeSnapshot, type TargetFps } from "@navsea/maplibre-particle-rn";
|
||
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", fieldOpacity: "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", T2: "PENDING", T3: "PENDING" });
|
||
const [transport, setTransport] = useState("PENDING: FILE_REF / FLOAT32_LE / INTERLEAVED_UV");
|
||
const [fieldVisible, setFieldVisible] = 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 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 {
|
||
setTransport("LOADING: materializing Expo Asset local file:// URIs"); setFrames({ T0: "PENDING", T1: "PENDING", T2: "PENDING", T3: "PENDING" });
|
||
if (await FieldRuntime.clearFields(reactTag) !== "ACCEPTED") throw new Error("clearFields was rejected");
|
||
const localFrames = await materializeRealWeatherFrames();
|
||
for (let index = 0; index < localFrames.length; index += 1) {
|
||
const frame = localFrames[index]; const submit = await FieldRuntime.submitField(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 FieldRuntime.getState(reactTag); setSnapshot(native);
|
||
if (native.state !== "READY") throw new Error(`Native FieldRuntime expected READY, got ${native.state}`);
|
||
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 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.fieldOpacity)),
|
||
]);
|
||
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: 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={`Field ${fieldVisible ? "visible" : "hidden"}`} onPress={() => { const next = !fieldVisible; setFieldVisible(next); void command("field 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}>{realWeatherFrames.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 / field 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 } });
|