import { useCallback, useEffect, useMemo, useState } from "react"; import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native"; import { Camera, Map, WeatherRuntime, type ColorStop, type RuntimeSnapshot, type TargetFps } from "@navsea/maplibre-particle-rn"; import { materializeRealWeatherFrames, realWeatherFrames } from "./src/realWeatherDataset"; import { loadHttpWeatherV2 } from "./src/httpWeatherV2Demo"; 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(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", T2: "PENDING", T3: "PENDING" }); const [transport, setTransport] = useState("PENDING: FILE_REF / FLOAT32_LE / INTERLEAVED_UV"); const [httpTransport, setHttpTransport] = useState("PENDING: WeatherV2 HTTP → canonical U/V → WeatherRuntime"); const [weatherVisible, setWeatherVisible] = useState(true); const [particleVisible, setParticleVisible] = useState(true); const [visual, setVisual] = useState(initialVisual); const refresh = useCallback(async (tag: number) => setSnapshot(await WeatherRuntime.getState(tag)), []); const attach = useCallback(async (tag: number) => { try { await WeatherRuntime.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 WeatherRuntime.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 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 WeatherRuntime.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 WeatherRuntime.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 WeatherRuntime.getState(reactTag); setSnapshot(native); if (native.state !== "READY") throw new Error(`Native WeatherRuntime 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 loadHttpWeather = useCallback(async () => { if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready"); try { setHttpTransport("LOADING: manifest → time index → WVECBIN1 → canonical U/V"); const loaded = await loadHttpWeatherV2(reactTag); setSnapshot(loaded.runtime); setFrames(Object.fromEntries(loaded.frames.map((frame, index) => [`T${index}`, frame.submitResult])) as Record); setHttpTransport(`READY: ${loaded.frames.length} HTTP frames / ${loaded.context.referenceTime}`); setResult(`HTTP WeatherV2 READY: ${loaded.frames.map((frame) => frame.fieldId).join(", ")}`); } catch (error) { setHttpTransport(`ERROR: ${String(error)}`); setResult(`LOAD HTTP WEATHER: ${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([ WeatherRuntime.setParticleCount(tag, Number(visual.count)), WeatherRuntime.setParticleLineWidth(tag, Number(visual.width)), WeatherRuntime.setParticleLength(tag, Number(visual.length)), WeatherRuntime.setParticleOpacity(tag, Number(visual.opacity)), WeatherRuntime.setTrailOpacity(tag, Number(visual.trailOpacity)), WeatherRuntime.setTrailFade(tag, Number(visual.trailFade)), WeatherRuntime.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) => WeatherRuntime.setPlaybackState(tag, "PLAY")], ["PAUSE", (tag: number) => WeatherRuntime.setPlaybackState(tag, "PAUSE")], ["HALF", (tag: number) => WeatherRuntime.setPlaybackSpeed(tag, "HALF")], ["NORMAL", (tag: number) => WeatherRuntime.setPlaybackSpeed(tag, "NORMAL")], ["DOUBLE", (tag: number) => WeatherRuntime.setPlaybackSpeed(tag, "DOUBLE")], ["FPS AUTO", (tag: number) => WeatherRuntime.setTargetFps(tag, "AUTO" as TargetFps)], ["FPS 30", (tag: number) => WeatherRuntime.setTargetFps(tag, "FPS_30" as TargetFps)], ["FPS 60", (tag: number) => WeatherRuntime.setTargetFps(tag, "FPS_60" as TargetFps)], ["Weather stops", (tag: number) => WeatherRuntime.setWeatherColorStops(tag, frozenStops)], ["Particle color", (tag: number) => WeatherRuntime.setParticleColor(tag, 1, 1, 1, 0.95)], ["Trail color", (tag: number) => WeatherRuntime.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: ECMWF HRES 20260807 · 513×769 · 3.01 MiB/frameRuntime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps}REAL DATA TRANSPORT = {transport} {realWeatherFrames.map((frame) => {frame.label} {frame.cycle}: {frames[frame.label]})} {result}