import { useCallback, useEffect, useMemo, useRef, 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, RendererRuntime, type RetainedUnderlayDiagnostics, type RuntimeSnapshot, } from "@navsea/maplibre-particle-rn"; import { materializeV4Frame as materializeV2Frame, qbinGoldenDatasets, } from "./src/qbinGoldenFixtures"; import { materializeV4Frame as materializeV3Frame, qbinV3GoldenDatasets, type V4Dataset, } from "./src/qbinV3GoldenFixtures"; import { colorStopsFor, legendFor, type WeatherDatasetId, } from "./src/weatherPalettes"; import { downloadOnlineQbin, type QbinFormat, } from "./src/onlineQbin"; import { weatherTestStyle } from "./src/weatherStyle"; const initialSnapshot: RuntimeSnapshot = { state: "WAITING_FOR_FIELDS", playbackState: "PAUSE", targetFps: "AUTO", disposed: false, }; const initialVisual = { count: "4096", width: "1.8", length: "0.028", opacity: "0.85", trailOpacity: "0.72", trailFade: "0.975", weatherOpacity: "1.0", }; function datasetFor(format: QbinFormat, id: WeatherDatasetId): V4Dataset { const datasets = format === "V3" ? qbinV3GoldenDatasets : qbinGoldenDatasets; const found = datasets.find((item) => item.id === id); if (!found) throw new Error("Dataset missing: " + format + " / " + id); return found as V4Dataset; } function valueSourceFor(id: WeatherDatasetId): "SCALAR" | "VECTOR_MAGNITUDE" { return id === "wind" || id === "current" ? "VECTOR_MAGNITUDE" : "SCALAR"; } export default function App() { const [reactTag, setReactTag] = useState(null); const [snapshot, setSnapshot] = useState(initialSnapshot); const [retainedSnapshot, setRetainedSnapshot] = useState(null); const [mapStatus, setMapStatus] = useState("LOADING"); const [weatherStatus, setWeatherStatus] = useState("WAITING"); const [result, setResult] = useState("Waiting for map/style"); const [activeSource, setActiveSource] = useState("none"); const [format, setFormat] = useState("V3"); const [datasetId, setDatasetId] = useState("wind"); const [weatherVisible, setWeatherVisible] = useState(true); const [particleVisible, setParticleVisible] = useState(true); const [controlsCollapsed, setControlsCollapsed] = useState(false); const [loading, setLoading] = useState(false); const [visual, setVisual] = useState(initialVisual); const autoLoadTagRef = useRef(null); const selectedDataset = useMemo( () => datasetFor(format, datasetId), [format, datasetId], ); const refresh = useCallback(async (tag: number) => { setSnapshot(await FieldRuntime.getState(tag)); }, []); const refreshRetained = useCallback(async (tag: number) => { setRetainedSnapshot(await RendererRuntime.getRetainedUnderlayDiagnostics(tag)); }, []); const applyVisualToTag = useCallback(async (tag: number) => { 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(", ")); } }, [visual]); const prepareDataset = useCallback(async ( tag: number, dataset: V4Dataset, ) => { const clearEncoded = await FieldRuntime.clearAllEncodedFields(tag); if (clearEncoded !== "ACCEPTED") throw new Error("clearAllEncodedFields rejected"); const clearCanonical = await FieldRuntime.clearFields(tag); if (clearCanonical !== "ACCEPTED") throw new Error("clearFields rejected"); const valueSource = await FieldRuntime.setFieldValueSource( tag, valueSourceFor(dataset.id), ); if (valueSource !== "ACCEPTED") throw new Error("setFieldValueSource rejected"); const stops = await FieldRuntime.setFieldColorStops( tag, colorStopsFor(dataset.id), ); if (stops !== "ACCEPTED") throw new Error("setFieldColorStops rejected"); const color = await FieldRuntime.setEncodedColorBinding(tag, dataset.colorBinding); if (color !== "ACCEPTED") throw new Error("setEncodedColorBinding rejected"); if (dataset.particleBinding != null) { const particle = await FieldRuntime.setEncodedParticleBinding( tag, dataset.particleBinding, ); if (particle !== "ACCEPTED") { throw new Error("setEncodedParticleBinding rejected"); } } const hasParticle = dataset.particleBinding != null; await FieldRuntime.setFieldVisible(tag, true); await FieldRuntime.setParticleVisible(tag, hasParticle); await applyVisualToTag(tag); setWeatherVisible(true); setParticleVisible(hasParticle); }, [applyVisualToTag]); const finishLoad = useCallback(async ( tag: number, dataset: V4Dataset, midpointMs: number, sourceLabel: string, ) => { const midpoint = await FieldRuntime.setEncodedForecastTime(tag, midpointMs); if (midpoint !== "ACCEPTED") throw new Error("setEncodedForecastTime rejected"); await FieldRuntime.setFieldVisible(tag, true); await FieldRuntime.setParticleVisible(tag, dataset.particleBinding != null); await refresh(tag); setWeatherStatus("READY"); setActiveSource(sourceLabel); setResult( dataset.id + " READY / " + valueSourceFor(dataset.id) + " / " + legendFor(dataset.id), ); }, [refresh]); const loadBundled = useCallback(async ( tag: number, loadFormat: QbinFormat, id: WeatherDatasetId, ) => { const dataset = datasetFor(loadFormat, id); try { setLoading(true); setWeatherStatus("LOADING"); setResult("Loading bundled " + loadFormat + " " + id); await prepareDataset(tag, dataset); const materialize = loadFormat === "V3" ? materializeV3Frame : materializeV2Frame; const frames = await Promise.all( dataset.frames.map((frame) => materialize(frame as never)), ); for (const frame of frames) { const submit = await FieldRuntime.submitEncodedField( tag, dataset.fieldKey, frame.metadata, dataset.profileContract, frame.qbinFileUri, ); if (submit !== "ACCEPTED_INSERTED") { throw new Error(frame.label + " submit=" + submit); } } await finishLoad( tag, dataset, dataset.midpointMs, "BUNDLED " + loadFormat + " / " + id, ); } catch (error) { setWeatherStatus("ERROR"); setResult("Bundled load error: " + String(error)); } finally { setLoading(false); } }, [finishLoad, prepareDataset]); const loadOnline = useCallback(async ( tag: number, loadFormat: QbinFormat, id: WeatherDatasetId, ) => { const dataset = datasetFor(loadFormat, id); try { setLoading(true); setWeatherStatus("DOWNLOADING"); setResult("Discovering online " + loadFormat + " " + id); await prepareDataset(tag, dataset); const online = await downloadOnlineQbin(loadFormat, id); setResult( "Downloading " + online.datasetId + "/" + online.serverLayer + " ref=" + online.refTime, ); for (const frame of online.frames) { const submit = await FieldRuntime.submitEncodedField( tag, dataset.fieldKey, frame.metadata, dataset.profileContract, frame.qbinFileUri, ); if (submit !== "ACCEPTED_INSERTED") { throw new Error(frame.label + " submit=" + submit); } } const times = online.frames .map((frame) => frame.targetTime) .join(" → "); await finishLoad( tag, dataset, online.midpointMs, "ONLINE " + loadFormat + " / " + online.datasetId + " / " + online.serverLayer + " / ref=" + online.refTime + " / " + times, ); } catch (error) { setWeatherStatus("ERROR"); setResult("Online load error: " + String(error)); } finally { setLoading(false); } }, [finishLoad, prepareDataset]); const attach = useCallback(async (tag: number) => { try { await FieldRuntime.attach(tag); setReactTag(tag); await Promise.all([refresh(tag), refreshRetained(tag)]); setResult("FieldRuntime attached / particle.10"); } catch (error) { setResult("attach error: " + String(error)); } }, [refresh, refreshRetained]); 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 || autoLoadTagRef.current === reactTag) return; autoLoadTagRef.current = reactTag; void loadBundled(reactTag, "V3", "wind"); }, [loadBundled, reactTag]); useEffect(() => () => { if (reactTag != null) void FieldRuntime.detach(reactTag).catch(() => undefined); }, [reactTag]); const applyVisual = useCallback(async () => { if (reactTag == null) return; try { await applyVisualToTag(reactTag); await refresh(reactTag); setResult("Visual parameters applied"); } catch (error) { setResult("Visual error: " + String(error)); } }, [applyVisualToTag, reactTag, refresh]); const toggleWeather = useCallback(async () => { if (reactTag == null) return; const next = !weatherVisible; if (await FieldRuntime.setFieldVisible(reactTag, next) === "ACCEPTED") { setWeatherVisible(next); } }, [reactTag, weatherVisible]); const toggleParticle = useCallback(async () => { if (reactTag == null) return; const next = !particleVisible; if (await FieldRuntime.setParticleVisible(reactTag, next) === "ACCEPTED") { setParticleVisible(next); } }, [particleVisible, reactTag]); const toggleRetained = useCallback(async () => { if (reactTag == null) return; const next = !(retainedSnapshot?.enabled ?? true); const result = await RendererRuntime.setRetainedUnderlayEnabled(reactTag, next); if (result === "ACCEPTED") await refreshRetained(reactTag); }, [reactTag, refreshRetained, retainedSnapshot?.enabled]); const updateVisual = useCallback( (key: keyof typeof initialVisual, value: string) => { setVisual((current) => ({ ...current, [key]: value })); }, [], ); return ( setMapStatus("READY")} onDidFailLoadingMap={() => { setMapStatus("ERROR"); setResult("map loading error"); }} > Particle V10 Test Controls