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 { materializeV4Frame, qbinGoldenDatasets, type V4Dataset } from "./src/qbinGoldenFixtures"; 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(null); const [snapshot, setSnapshot] = useState(initialSnapshot); const [result, setResult] = useState("Style is loading"); const [basemap, setBasemap] = useState("LOADING"); const [frames, setFrames] = useState>({ T0: "PENDING", T1: "PENDING" }); const [dataset, setDataset] = useState(fieldDatasets[0]); const [transport, setTransport] = useState("PENDING: V2 FILE_REF / FLOAT32_LE"); const [v4Dataset, setV4Dataset] = useState(qbinGoldenDatasets[0]); const [v4Frames, setV4Frames] = useState>({ T0: "PENDING", T1: "PENDING" }); const [v4Bindings, setV4Bindings] = useState("PENDING"); const [v4Transport, setV4Transport] = useState("PENDING: raw QBIN bundled asset → local file:// URI"); 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) => { 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 loadV4Dataset = useCallback(async () => { if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready"); try { setV4Transport("LOADING: clear → binding → T0 → T1 → midpoint"); setV4Frames({ T0: "PENDING", T1: "PENDING" }); setV4Bindings("PENDING"); const clear = await FieldRuntime.clearAllEncodedFields(reactTag); if (clear !== "ACCEPTED") throw new Error(`clearAllEncodedFields expected ACCEPTED, got ${clear}`); const color = await FieldRuntime.setEncodedColorBinding(reactTag, v4Dataset.colorBinding); if (color !== "ACCEPTED") throw new Error(`ColorBinding expected ACCEPTED, got ${color}`); const particle = v4Dataset.particleBinding == null ? "N/A" : await FieldRuntime.setEncodedParticleBinding(reactTag, v4Dataset.particleBinding); if (particle !== "N/A" && particle !== "ACCEPTED") throw new Error(`ParticleBinding expected ACCEPTED, got ${particle}`); setV4Bindings(`clearAll=${clear}; color=${color}; particle=${particle}`); const localFrames = await Promise.all(v4Dataset.frames.map(materializeV4Frame)); for (const frame of localFrames) { const submit = await FieldRuntime.submitEncodedField(reactTag, v4Dataset.fieldKey, frame.metadata, v4Dataset.profileContract, frame.qbinFileUri); setV4Frames((current) => ({ ...current, [frame.label]: submit })); if (submit !== "ACCEPTED_INSERTED") throw new Error(`${frame.label} expected ACCEPTED_INSERTED, got ${submit}`); } const midpoint = await FieldRuntime.setEncodedForecastTime(reactTag, v4Dataset.midpointMs); if (midpoint !== "ACCEPTED") throw new Error(`midpoint seek expected ACCEPTED, got ${midpoint}`); await refresh(reactTag); setV4Transport("READY: T0/T1 ACCEPTED_INSERTED; midpoint command ACCEPTED (mix=0.5 is not exposed)"); setResult(`${v4Dataset.title}: V4 raw QBIN load accepted`); } catch (error) { setV4Transport(`ERROR: ${String(error)}`); setResult(`LOAD V4 ${v4Dataset.id}: ${String(error)}`); } }, [reactTag, refresh, v4Dataset]); 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 setBasemap("READY")} onDidFailLoadingMap={() => { setBasemap("ERROR"); setResult("map loading error"); }}> Basemap: GSI {basemap}Dataset: {dataset.title}Runtime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps}V2 FILE TRANSPORT = {transport} {dataset.frames.map((frame) => {frame.label}: {frames[frame.label]})} {result}{fieldDatasets.map((item) =>