Files
MapLibreParticleLab/integration/react-native/MapLibreParticleRNTestHost/App.tsx
tei 9e20233288
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
docs: close STEP-083 ios RN validation
2026-09-18 22:42:47 +09:00

193 lines
16 KiB
TypeScript

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, 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<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 [v4Dataset, setV4Dataset] = useState<V4Dataset>(qbinGoldenDatasets[0]);
const [v4Frames, setV4Frames] = useState<Record<"T0" | "T1", string>>({ T0: "PENDING", T1: "PENDING" });
const [v4Bindings, setV4Bindings] = useState("PENDING");
const [v4Transport, setV4Transport] = useState("PENDING: raw QBIN bundled asset → local file:// URI");
const [finalControlRunning, setFinalControlRunning] = useState(false);
const [finalControlSummary, setFinalControlSummary] = useState("FINAL CONTROL REGRESSION: not run");
const finalControlRunRef = useRef(false);
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 prepareV4 = useCallback(async (tag: number) => {
setV4Transport("LOADING: clear → binding → T0 → T1 → midpoint");
setV4Frames({ T0: "PENDING", T1: "PENDING" });
setV4Bindings("PENDING");
const clear = await FieldRuntime.clearAllEncodedFields(tag);
if (clear !== "ACCEPTED") throw new Error(`clearAllEncodedFields expected ACCEPTED, got ${clear}`);
const color = await FieldRuntime.setEncodedColorBinding(tag, v4Dataset.colorBinding);
if (color !== "ACCEPTED") throw new Error(`ColorBinding expected ACCEPTED, got ${color}`);
const particle = v4Dataset.particleBinding == null ? "N/A" : await FieldRuntime.setEncodedParticleBinding(tag, 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));
const submitted: string[] = [];
for (const frame of localFrames) {
const submit = await FieldRuntime.submitEncodedField(tag, v4Dataset.fieldKey, frame.metadata, v4Dataset.profileContract, frame.qbinFileUri);
submitted.push(`${frame.label}=${submit}`);
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(tag, v4Dataset.midpointMs);
if (midpoint !== "ACCEPTED") throw new Error(`midpoint seek expected ACCEPTED, got ${midpoint}`);
await refresh(tag);
setV4Transport("READY: T0/T1 ACCEPTED_INSERTED; midpoint command ACCEPTED (mix=0.5 is not exposed)");
return `clearAll=${clear}; color=${color}; particle=${particle}; ${submitted.join(", ")}; midpoint=${midpoint}`;
}, [refresh, v4Dataset]);
const loadV4Dataset = useCallback(async () => {
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
try {
await prepareV4(reactTag);
setResult(`${v4Dataset.title}: V4 raw QBIN load accepted`);
} catch (error) {
setV4Transport(`ERROR: ${String(error)}`);
setResult(`LOAD V4 ${v4Dataset.id}: ${String(error)}`);
}
}, [prepareV4, reactTag, v4Dataset]);
const runV4FinalControlRegression = useCallback(async () => {
if (reactTag == null || !Number.isInteger(reactTag) || reactTag <= 0) return setResult("MAP_NOT_READY: wait for a valid reactTag/style ready");
if (finalControlRunRef.current) return;
const steps: string[] = [];
const accepted = (step: string, value: string) => {
if (value !== "ACCEPTED") throw new Error(`${step} expected ACCEPTED, got ${value}`);
steps.push(`${step}=${value}`);
};
const prepare = async (attempt: number) => {
try { steps.push(`prepare#${attempt} PASS (${await prepareV4(reactTag)})`); }
catch (error) { throw new Error(`prepare#${attempt}: ${String(error)}`); }
};
finalControlRunRef.current = true;
setFinalControlRunning(true);
setFinalControlSummary("FINAL CONTROL REGRESSION: RUNNING");
try {
await prepare(1);
accepted("PLAY", await FieldRuntime.setEncodedForecastPlaybackState(reactTag, "PLAY"));
await new Promise<void>((resolve) => setTimeout(resolve, 500));
steps.push("PLAY wait≈500ms=PASS");
accepted("PAUSE", await FieldRuntime.setEncodedForecastPlaybackState(reactTag, "PAUSE"));
accepted("clear selected", await FieldRuntime.clearEncodedField(reactTag, v4Dataset.fieldKey));
await prepare(2);
accepted("clear all", await FieldRuntime.clearAllEncodedFields(reactTag));
try { await FieldRuntime.detach(reactTag); }
catch (error) { throw new Error(`detach: ${String(error)}`); }
steps.push("detach=PASS");
let attached: RuntimeSnapshot;
try { attached = await FieldRuntime.attach(reactTag); }
catch (error) { throw new Error(`attach: ${String(error)}`); }
setSnapshot(attached);
steps.push("attach=PASS");
await prepare(3);
try { await refresh(reactTag); }
catch (error) { throw new Error(`refresh: ${String(error)}`); }
steps.push("refresh=PASS");
const summary = steps.join(" | ");
setFinalControlSummary(`FINAL CONTROL REGRESSION PASS: ${summary}`);
setResult("FINAL CONTROL REGRESSION PASS");
} catch (error) {
const failure = `FAIL: ${String(error)}`;
steps.push(failure);
setFinalControlSummary(`FINAL CONTROL REGRESSION ${steps.join(" | ")}`);
setResult(`FINAL CONTROL REGRESSION ${failure}`);
} finally {
finalControlRunRef.current = false;
setFinalControlRunning(false);
}
}, [prepareV4, reactTag, refresh, v4Dataset.fieldKey]);
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>
<Text>V4 QBIN Golden: {v4Dataset.title}</Text><Text selectable>V4 raw QBIN transport = {v4Transport}</Text><Text selectable>V4 bindings: {v4Bindings}</Text><Text selectable>{v4Dataset.particleNote}</Text>
<Text selectable>V4 T0: {v4Frames.T0}</Text><Text selectable>V4 T1: {v4Frames.T1}</Text>
<View style={styles.row}>{qbinGoldenDatasets.map((item) => <Button key={item.id} title={`V4 ${item.id}`} onPress={() => setV4Dataset(item)} />)}</View><Button title="LOAD SELECTED V4 DATASET" onPress={() => void loadV4Dataset()} />
<Button title={finalControlRunning ? "RUN V4 FINAL CONTROL REGRESSION (RUNNING)" : "RUN V4 FINAL CONTROL REGRESSION"} disabled={finalControlRunning} onPress={() => void runV4FinalControlRegression()} />
<Text selectable>{finalControlSummary}</Text>
<View style={styles.row}><Button title="V4 seek T0" onPress={() => void command("V4 seek T0", (tag) => FieldRuntime.setEncodedForecastTime(tag, v4Dataset.frames[0].validTimeMs))} /><Button title="V4 seek midpoint" onPress={() => void command("V4 seek midpoint (mix not exposed)", (tag) => FieldRuntime.setEncodedForecastTime(tag, v4Dataset.midpointMs))} /><Button title="V4 seek T1" onPress={() => void command("V4 seek T1", (tag) => FieldRuntime.setEncodedForecastTime(tag, v4Dataset.frames[1].validTimeMs))} /></View>
<View style={styles.row}><Button title="V4 PLAY" onPress={() => void command("V4 PLAY", (tag) => FieldRuntime.setEncodedForecastPlaybackState(tag, "PLAY"))} /><Button title="V4 PAUSE" onPress={() => void command("V4 PAUSE", (tag) => FieldRuntime.setEncodedForecastPlaybackState(tag, "PAUSE"))} /><Button title="V4 speed 1.0" onPress={() => void command("V4 speed 1.0", (tag) => FieldRuntime.setEncodedForecastPlaybackSpeed(tag, 1.0))} /><Button title="V4 clear selected" onPress={() => void command("V4 clear selected", (tag) => FieldRuntime.clearEncodedField(tag, v4Dataset.fieldKey))} /><Button title="V4 clear all" onPress={() => void command("V4 clear all", (tag) => FieldRuntime.clearAllEncodedFields(tag))} /></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 } });