91 lines
8.9 KiB
TypeScript
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 } });
|