test: integrate particle.10 online QBIN host
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -1,2 +1,4 @@
|
|||||||
# Expo/RN prebuild output is generated locally and must not enter this source host.
|
# Expo/RN prebuild output is generated locally and must not enter this source host.
|
||||||
/ios/
|
/ios/
|
||||||
|
|
||||||
|
/.expo/
|
||||||
|
|||||||
@@ -1,192 +1,453 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native";
|
import {
|
||||||
|
Button,
|
||||||
|
SafeAreaView,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
View,
|
||||||
|
findNodeHandle,
|
||||||
|
} from "react-native";
|
||||||
import { Camera, Map } from "@maplibre/maplibre-react-native";
|
import { Camera, Map } from "@maplibre/maplibre-react-native";
|
||||||
import { FieldRuntime, type FieldColorStop, type RuntimeSnapshot, type TargetFps } from "@navsea/maplibre-particle-rn";
|
import { FieldRuntime, type RuntimeSnapshot } from "@navsea/maplibre-particle-rn";
|
||||||
import { fieldDatasets, materializeV3Frame, type V3Dataset } from "./src/fieldDatasets";
|
import {
|
||||||
import { materializeV4Frame, qbinGoldenDatasets, type V4Dataset } from "./src/qbinGoldenFixtures";
|
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";
|
import { weatherTestStyle } from "./src/weatherStyle";
|
||||||
|
const initialSnapshot: RuntimeSnapshot = {
|
||||||
|
state: "WAITING_FOR_FIELDS",
|
||||||
|
playbackState: "PAUSE",
|
||||||
|
targetFps: "AUTO",
|
||||||
|
disposed: false,
|
||||||
|
};
|
||||||
|
|
||||||
const initialSnapshot: RuntimeSnapshot = { state: "WAITING_FOR_FIELDS", playbackState: "PAUSE", targetFps: "AUTO", disposed: false };
|
const initialVisual = {
|
||||||
const initialVisual = { count: "4096", width: "1.5", length: "0.012", opacity: "0.9", trailOpacity: "0.8", trailFade: "0.94", weatherOpacity: "0.62" };
|
count: "4096",
|
||||||
const frozenStops: FieldColorStop[] = [
|
width: "1.8",
|
||||||
{ 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 },
|
length: "0.028",
|
||||||
{ 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 },
|
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() {
|
export default function App() {
|
||||||
const [reactTag, setReactTag] = useState<number | null>(null);
|
const [reactTag, setReactTag] = useState<number | null>(null);
|
||||||
const [snapshot, setSnapshot] = useState<RuntimeSnapshot>(initialSnapshot);
|
const [snapshot, setSnapshot] = useState<RuntimeSnapshot>(initialSnapshot);
|
||||||
const [result, setResult] = useState("Style is loading");
|
const [mapStatus, setMapStatus] = useState("LOADING");
|
||||||
const [basemap, setBasemap] = useState("LOADING");
|
const [weatherStatus, setWeatherStatus] = useState("WAITING");
|
||||||
const [frames, setFrames] = useState<Record<string, string>>({ T0: "PENDING", T1: "PENDING" });
|
const [result, setResult] = useState("Waiting for map/style");
|
||||||
const [dataset, setDataset] = useState<V3Dataset>(fieldDatasets[0]);
|
const [activeSource, setActiveSource] = useState("none");
|
||||||
const [transport, setTransport] = useState("PENDING: V2 FILE_REF / FLOAT32_LE");
|
const [format, setFormat] = useState<QbinFormat>("V3");
|
||||||
const [v4Dataset, setV4Dataset] = useState<V4Dataset>(qbinGoldenDatasets[0]);
|
const [datasetId, setDatasetId] = useState<WeatherDatasetId>("wind");
|
||||||
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 [weatherVisible, setWeatherVisible] = useState(true);
|
||||||
const [particleVisible, setParticleVisible] = useState(true);
|
const [particleVisible, setParticleVisible] = useState(true);
|
||||||
|
const [controlsCollapsed, setControlsCollapsed] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [visual, setVisual] = useState(initialVisual);
|
const [visual, setVisual] = useState(initialVisual);
|
||||||
|
const autoLoadTagRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const selectedDataset = useMemo(
|
||||||
|
() => datasetFor(format, datasetId),
|
||||||
|
[format, datasetId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const refresh = useCallback(async (tag: number) => {
|
||||||
|
setSnapshot(await FieldRuntime.getState(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 refresh = useCallback(async (tag: number) => setSnapshot(await FieldRuntime.getState(tag)), []);
|
|
||||||
const attach = useCallback(async (tag: number) => {
|
const attach = useCallback(async (tag: number) => {
|
||||||
try { await FieldRuntime.attach(tag); await refresh(tag); setReactTag(tag); setResult("FieldRuntime attached; style ready"); }
|
try {
|
||||||
catch (error) { setResult(`style/attach error: ${String(error)}`); }
|
await FieldRuntime.attach(tag);
|
||||||
|
setReactTag(tag);
|
||||||
|
await refresh(tag);
|
||||||
|
setResult("FieldRuntime attached / particle.10");
|
||||||
|
} catch (error) {
|
||||||
|
setResult("attach error: " + String(error));
|
||||||
|
}
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
const onStyleReady = useCallback((event: unknown) => {
|
const onStyleReady = useCallback((event: unknown) => {
|
||||||
const tag = findNodeHandle((event as { target?: unknown }).target as never);
|
const tag = findNodeHandle((event as { target?: unknown }).target as never);
|
||||||
if (tag != null) void attach(tag);
|
if (tag != null) void attach(tag);
|
||||||
}, [attach]);
|
}, [attach]);
|
||||||
useEffect(() => () => { if (reactTag != null) void FieldRuntime.detach(reactTag).catch(() => undefined); }, [reactTag]);
|
|
||||||
|
|
||||||
const command = useCallback(async (name: string, operation: (tag: number) => Promise<unknown>) => {
|
useEffect(() => {
|
||||||
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
|
if (reactTag == null || autoLoadTagRef.current === reactTag) return;
|
||||||
try { const value = await operation(reactTag); await refresh(reactTag); setResult(`${name}: ${String(value)}`); }
|
autoLoadTagRef.current = reactTag;
|
||||||
catch (error) { setResult(`${name}: ${String(error)}`); }
|
void loadBundled(reactTag, "V3", "wind");
|
||||||
}, [reactTag, refresh]);
|
}, [loadBundled, reactTag]);
|
||||||
|
|
||||||
const loadDataset = useCallback(async () => {
|
useEffect(() => () => {
|
||||||
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
|
if (reactTag != null) void FieldRuntime.detach(reactTag).catch(() => undefined);
|
||||||
const expected = ["ACCEPTED_PENDING_INITIAL_WINDOW", "INITIAL_WINDOW_READY"];
|
}, [reactTag]);
|
||||||
|
|
||||||
|
const applyVisual = useCallback(async () => {
|
||||||
|
if (reactTag == null) return;
|
||||||
try {
|
try {
|
||||||
setTransport("LOADING: V2 metadata + validity/scalar/vector local file:// URIs"); setFrames({ T0: "PENDING", T1: "PENDING" });
|
await applyVisualToTag(reactTag);
|
||||||
if (await FieldRuntime.clearFields(reactTag) !== "ACCEPTED") throw new Error("clearFields was rejected");
|
await refresh(reactTag);
|
||||||
await FieldRuntime.setFieldValueSource(reactTag, dataset.fieldValueSource); await FieldRuntime.setParticleVisible(reactTag, dataset.particle); await FieldRuntime.setParticleAdvectionScale(reactTag, dataset.advectionScale);
|
setResult("Visual parameters applied");
|
||||||
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) {
|
} catch (error) {
|
||||||
setV4Transport(`ERROR: ${String(error)}`);
|
setResult("Visual error: " + String(error));
|
||||||
setResult(`LOAD V4 ${v4Dataset.id}: ${String(error)}`);
|
|
||||||
}
|
}
|
||||||
}, [prepareV4, reactTag, v4Dataset]);
|
}, [applyVisualToTag, reactTag, refresh]);
|
||||||
|
|
||||||
const runV4FinalControlRegression = useCallback(async () => {
|
const toggleWeather = useCallback(async () => {
|
||||||
if (reactTag == null || !Number.isInteger(reactTag) || reactTag <= 0) return setResult("MAP_NOT_READY: wait for a valid reactTag/style ready");
|
if (reactTag == null) return;
|
||||||
if (finalControlRunRef.current) return;
|
const next = !weatherVisible;
|
||||||
const steps: string[] = [];
|
if (await FieldRuntime.setFieldVisible(reactTag, next) === "ACCEPTED") {
|
||||||
const accepted = (step: string, value: string) => {
|
setWeatherVisible(next);
|
||||||
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]);
|
}, [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 updateVisual = useCallback((key: keyof typeof initialVisual, value: string) => setVisual((current) => ({ ...current, [key]: value })), []);
|
const updateVisual = useCallback(
|
||||||
const applyVisual = useCallback(() => void command("particle visual", async (tag) => {
|
(key: keyof typeof initialVisual, value: string) => {
|
||||||
const results = await Promise.all([
|
setVisual((current) => ({ ...current, [key]: value }));
|
||||||
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(() => [
|
return (
|
||||||
["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")],
|
<SafeAreaView style={styles.root}>
|
||||||
["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)],
|
<View style={styles.map}>
|
||||||
] as const, []);
|
<Map
|
||||||
|
mapStyle={weatherTestStyle}
|
||||||
|
onDidFinishLoadingStyle={onStyleReady}
|
||||||
|
onDidFinishLoadingMap={() => setMapStatus("READY")}
|
||||||
|
onDidFailLoadingMap={() => {
|
||||||
|
setMapStatus("ERROR");
|
||||||
|
setResult("map loading error");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Camera initialViewState={{ center: [137.5, 36.2], zoom: 4.8 }} />
|
||||||
|
</Map>
|
||||||
|
</View>
|
||||||
|
<View style={styles.controlPanel}>
|
||||||
|
<View style={styles.controlHeader}>
|
||||||
|
<Text style={styles.controlHeaderTitle}>
|
||||||
|
Particle V10 Test Controls
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
title={controlsCollapsed ? "展开 ▼" : "收起 ▲"}
|
||||||
|
onPress={() => setControlsCollapsed((current) => !current)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
return <SafeAreaView style={styles.root}>
|
{!controlsCollapsed && (
|
||||||
<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}>
|
||||||
<ScrollView style={styles.controls} contentContainerStyle={styles.content}>
|
<Text>
|
||||||
<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>
|
Map: {mapStatus} / Weather: {weatherStatus} / Runtime: {snapshot.state}
|
||||||
{dataset.frames.map((frame) => <Text selectable key={frame.label}>{frame.label}: {frames[frame.label]}</Text>)}
|
</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()} />
|
<Text>
|
||||||
<View style={styles.row}>{controls.map(([label, operation]) => <Button key={label} title={label} onPress={() => void command(label, operation)} />)}</View>
|
Native: RN 13.4.1-particle.10 / iOS 6.28.0-particle.10
|
||||||
<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>
|
</Text>
|
||||||
<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 selectable>Source: {activeSource}</Text>
|
||||||
<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 selectable>{result}</Text>
|
||||||
<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>
|
<Text>QBIN Format</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()} />
|
<View style={styles.row}>
|
||||||
<Button title={finalControlRunning ? "RUN V4 FINAL CONTROL REGRESSION (RUNNING)" : "RUN V4 FINAL CONTROL REGRESSION"} disabled={finalControlRunning} onPress={() => void runV4FinalControlRegression()} />
|
{(["V2", "V3"] as const).map((item) => (
|
||||||
<Text selectable>{finalControlSummary}</Text>
|
<Button
|
||||||
<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>
|
key={item}
|
||||||
<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>
|
title={(format === item ? "✓ " : "") + item}
|
||||||
</ScrollView>
|
onPress={() => setFormat(item)}
|
||||||
</SafeAreaView>;
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
<Text>Dataset</Text>
|
||||||
|
<View style={styles.row}>
|
||||||
|
{(["wind", "wave", "current", "rain"] as const).map((item) => (
|
||||||
|
<Button
|
||||||
|
key={item}
|
||||||
|
title={(datasetId === item ? "✓ " : "") + item}
|
||||||
|
onPress={() => setDatasetId(item)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
{valueSourceFor(datasetId)} / {legendFor(datasetId)}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Button
|
||||||
|
title={"LOAD BUNDLED " + format}
|
||||||
|
disabled={reactTag == null || loading}
|
||||||
|
onPress={() => reactTag != null &&
|
||||||
|
void loadBundled(reactTag, format, datasetId)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
title={"ONLINE GET " + format}
|
||||||
|
disabled={reactTag == null || loading}
|
||||||
|
onPress={() => reactTag != null &&
|
||||||
|
void loadOnline(reactTag, format, datasetId)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
title={weatherVisible ? "Weather ON" : "Weather OFF"}
|
||||||
|
disabled={reactTag == null || loading}
|
||||||
|
onPress={() => void toggleWeather()}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
title={particleVisible ? "Particle ON" : "Particle OFF"}
|
||||||
|
disabled={
|
||||||
|
reactTag == null ||
|
||||||
|
loading ||
|
||||||
|
selectedDataset.particleBinding == null
|
||||||
|
}
|
||||||
|
onPress={() => void toggleParticle()}
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
disabled={reactTag == null || loading}
|
||||||
|
onPress={() => void applyVisual()}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
const styles = StyleSheet.create({
|
||||||
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 } });
|
root: { flex: 1 },
|
||||||
|
map: { flex: 1 },
|
||||||
|
controlPanel: { backgroundColor: "#fff" },
|
||||||
|
controlHeader: {
|
||||||
|
minHeight: 44,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 4,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderTopColor: "#c7c7c7",
|
||||||
|
},
|
||||||
|
controlHeaderTitle: { fontSize: 15, fontWeight: "600" },
|
||||||
|
controls: { maxHeight: 360, backgroundColor: "#fff" },
|
||||||
|
content: { gap: 7, padding: 10, paddingTop: 4 },
|
||||||
|
row: {
|
||||||
|
flexDirection: "row",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 6,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
input: { borderWidth: 1, minWidth: 48, padding: 5 },
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,7 +5,18 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"orientation": "landscape",
|
"orientation": "landscape",
|
||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"plugins": ["./plugins/withParticleNativeAndroid"],
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@maplibre/maplibre-react-native",
|
||||||
|
{
|
||||||
|
"ios": {
|
||||||
|
"spmSpec": "{ url: \"https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git\", requirement: { kind: \"exactVersion\", version: \"6.28.0-particle.10\" }, product_name: \"MapLibre\" }"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"./plugins/withParticleNativeIos",
|
||||||
|
"./plugins/withParticleNativeAndroid"
|
||||||
|
],
|
||||||
"android": { "package": "org.maplibre.particle.rntesthost", "minSdkVersion": 24 },
|
"android": { "package": "org.maplibre.particle.rntesthost", "minSdkVersion": 24 },
|
||||||
"ios": { "bundleIdentifier": "org.maplibre.particle.rntesthost", "supportsTablet": true }
|
"ios": { "bundleIdentifier": "org.maplibre.particle.rntesthost", "supportsTablet": true }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@maplibre/maplibre-react-native": "11.3.6",
|
"@maplibre/maplibre-react-native": "11.3.6",
|
||||||
"@navsea/maplibre-particle-rn": "file:../../../packages/maplibre-particle-rn",
|
"@navsea/maplibre-particle-rn": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.10.tgz",
|
||||||
"expo": "56.0.4",
|
"expo": "56.0.4",
|
||||||
"expo-asset": "~56.0.14",
|
"expo-asset": "~56.0.14",
|
||||||
"expo-file-system": "56.0.11",
|
"expo-file-system": "56.0.11",
|
||||||
@@ -21,24 +21,6 @@
|
|||||||
"typescript": "5.9.3"
|
"typescript": "5.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../../../packages/maplibre-particle-rn": {
|
|
||||||
"name": "@navsea/maplibre-particle-rn",
|
|
||||||
"version": "13.4.1-particle.3",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"devDependencies": {
|
|
||||||
"@maplibre/maplibre-react-native": "11.3.6"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@maplibre/maplibre-react-native": "11.3.6",
|
|
||||||
"react": ">=19.1.0",
|
|
||||||
"react-native": ">=0.80.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"expo": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||||
@@ -1780,8 +1762,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@navsea/maplibre-particle-rn": {
|
"node_modules/@navsea/maplibre-particle-rn": {
|
||||||
"resolved": "../../../packages/maplibre-particle-rn",
|
"version": "13.4.1-particle.10",
|
||||||
"link": true
|
"resolved": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.10.tgz",
|
||||||
|
"integrity": "sha512-SNXvZ71A+vhrWzTxYCLGLGNkg6evtl8hydgdMUzB2Ft78sf5o+SCLQFtJ7ieLJl9r4kVEYdmprG8uwRdJDlhYg==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@maplibre/maplibre-react-native": "11.3.6",
|
||||||
|
"expo": ">=54.0.0",
|
||||||
|
"react": ">=19.1.0",
|
||||||
|
"react-native": ">=0.80.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"expo": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-native/assets-registry": {
|
"node_modules/@react-native/assets-registry": {
|
||||||
"version": "0.85.2",
|
"version": "0.85.2",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"android": "expo run:android"
|
"android": "expo run:android"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@navsea/maplibre-particle-rn": "file:../../../packages/maplibre-particle-rn",
|
"@navsea/maplibre-particle-rn": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.10.tgz",
|
||||||
"@maplibre/maplibre-react-native": "11.3.6",
|
"@maplibre/maplibre-react-native": "11.3.6",
|
||||||
"expo": "56.0.4",
|
"expo": "56.0.4",
|
||||||
"expo-asset": "~56.0.14",
|
"expo-asset": "~56.0.14",
|
||||||
|
|||||||
21
integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeIos.js
vendored
Normal file
21
integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeIos.js
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { withPodfile } = require("@expo/config-plugins");
|
||||||
|
|
||||||
|
const marker = "# MapLibreParticleRNTestHost Particle iOS SPM hook";
|
||||||
|
|
||||||
|
module.exports = function withParticleNativeIos(config) {
|
||||||
|
return withPodfile(config, (project) => {
|
||||||
|
const contents = project.modResults.contents;
|
||||||
|
if (contents.includes(marker)) return project;
|
||||||
|
|
||||||
|
const needle = " post_install do |installer|";
|
||||||
|
if (!contents.includes(needle)) {
|
||||||
|
throw new Error("Expo post_install block was not generated.");
|
||||||
|
}
|
||||||
|
|
||||||
|
project.modResults.contents = contents.replace(
|
||||||
|
needle,
|
||||||
|
needle + "\n " + marker + "\n $MLRN_PARTICLE.post_install(installer)",
|
||||||
|
);
|
||||||
|
return project;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { File, Paths } from "expo-file-system";
|
||||||
|
import type { WeatherDatasetId } from "./weatherPalettes";
|
||||||
|
|
||||||
|
export type QbinFormat = "V2" | "V3";
|
||||||
|
|
||||||
|
type WeatherManifest = Readonly<{
|
||||||
|
l: Record<string, Readonly<{
|
||||||
|
d: string;
|
||||||
|
ds: readonly Readonly<{ id: string; r: string; iu: string }>[];
|
||||||
|
}>>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type WeatherTimeIndex = Readonly<{
|
||||||
|
d: string;
|
||||||
|
l: string;
|
||||||
|
r: string;
|
||||||
|
t: readonly string[];
|
||||||
|
x: Readonly<{
|
||||||
|
q2f?: string;
|
||||||
|
q2v?: number;
|
||||||
|
q3f?: string;
|
||||||
|
q3v?: number;
|
||||||
|
q3bw?: number;
|
||||||
|
q3bh?: number;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type OnlineQbinFrame = Readonly<{
|
||||||
|
label: "T0" | "T1";
|
||||||
|
targetTime: string;
|
||||||
|
validTimeMs: number;
|
||||||
|
metadata: Readonly<{ fieldID: string; generation: number; dataVersion: number }>;
|
||||||
|
qbinFileUri: string;
|
||||||
|
sourceUrl: string;
|
||||||
|
}>;
|
||||||
|
export type OnlineQbinSelection = Readonly<{
|
||||||
|
datasetId: string;
|
||||||
|
serverLayer: string;
|
||||||
|
refTime: string;
|
||||||
|
frames: readonly OnlineQbinFrame[];
|
||||||
|
midpointMs: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const BASE = "https://weather.jpcn.ccwu.cc/";
|
||||||
|
const HEADERS = { "User-Agent": "Mozilla/5.0" };
|
||||||
|
|
||||||
|
function layerFor(id: WeatherDatasetId): string {
|
||||||
|
if (id === "wave") return "waves";
|
||||||
|
if (id === "current") return "tide";
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeMs(value: string): number {
|
||||||
|
if (!/^\d{10}$/.test(value)) throw new Error("Invalid forecast time: " + value);
|
||||||
|
return Date.UTC(
|
||||||
|
Number(value.slice(0, 4)),
|
||||||
|
Number(value.slice(4, 6)) - 1,
|
||||||
|
Number(value.slice(6, 8)),
|
||||||
|
Number(value.slice(8, 10)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseTimes(times: readonly string[]): readonly [string, string] {
|
||||||
|
if (times.length < 2) throw new Error("time_index has fewer than two target times");
|
||||||
|
const now = Date.now();
|
||||||
|
let index = times.findIndex((value) => timeMs(value) >= now);
|
||||||
|
if (index < 0) index = times.length - 1;
|
||||||
|
if (index === 0) return [times[0], times[1]];
|
||||||
|
return [times[index - 1], times[index]];
|
||||||
|
}
|
||||||
|
async function fetchJson<T>(url: string): Promise<T> {
|
||||||
|
const response = await fetch(url, { headers: HEADERS });
|
||||||
|
if (!response.ok) throw new Error("HTTP " + response.status + ": " + url);
|
||||||
|
return await response.json() as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadOnlineQbin(
|
||||||
|
format: QbinFormat,
|
||||||
|
id: WeatherDatasetId,
|
||||||
|
): Promise<OnlineQbinSelection> {
|
||||||
|
const manifest = await fetchJson<WeatherManifest>(BASE + "weather_manifest.json");
|
||||||
|
const serverLayer = layerFor(id);
|
||||||
|
const layer = manifest.l[serverLayer];
|
||||||
|
if (!layer) throw new Error("manifest layer missing: " + serverLayer);
|
||||||
|
|
||||||
|
const dataset = layer.ds.find((item) => item.id === layer.d) ?? layer.ds[0];
|
||||||
|
if (!dataset) throw new Error("manifest dataset missing: " + serverLayer);
|
||||||
|
|
||||||
|
const indexUrl = BASE + dataset.iu.replace(/^\/+/, "");
|
||||||
|
const timeIndex = await fetchJson<WeatherTimeIndex>(indexUrl);
|
||||||
|
const template = format === "V3" ? timeIndex.x.q3f : timeIndex.x.q2f;
|
||||||
|
const version = format === "V3" ? timeIndex.x.q3v : timeIndex.x.q2v;
|
||||||
|
if (!template) throw new Error(format + " frame template missing");
|
||||||
|
if (version !== (format === "V3" ? 3 : 2)) {
|
||||||
|
throw new Error(format + " version mismatch: " + String(version));
|
||||||
|
}
|
||||||
|
|
||||||
|
const [t0, t1] = chooseTimes(timeIndex.t);
|
||||||
|
const baseUrl = indexUrl.slice(0, indexUrl.lastIndexOf("/") + 1);
|
||||||
|
const targets = [t0, t1] as const;
|
||||||
|
const frames: OnlineQbinFrame[] = [];
|
||||||
|
for (let index = 0; index < targets.length; index += 1) {
|
||||||
|
const targetTime = targets[index];
|
||||||
|
const relative = template.replace("{target_time}", targetTime);
|
||||||
|
const sourceUrl = baseUrl + relative;
|
||||||
|
const destination = new File(
|
||||||
|
Paths.cache,
|
||||||
|
"online-" + serverLayer + "-" + format.toLowerCase() + "-" + targetTime + ".qbin",
|
||||||
|
);
|
||||||
|
const downloaded = await File.downloadFileAsync(
|
||||||
|
sourceUrl,
|
||||||
|
destination,
|
||||||
|
{ headers: HEADERS, idempotent: true },
|
||||||
|
);
|
||||||
|
frames.push({
|
||||||
|
label: index === 0 ? "T0" : "T1",
|
||||||
|
targetTime,
|
||||||
|
validTimeMs: timeMs(targetTime),
|
||||||
|
metadata: {
|
||||||
|
fieldID: "online-" + timeIndex.d + "-" + serverLayer,
|
||||||
|
generation: index + 1,
|
||||||
|
dataVersion: index + 1,
|
||||||
|
},
|
||||||
|
qbinFileUri: downloaded.uri,
|
||||||
|
sourceUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
datasetId: timeIndex.d,
|
||||||
|
serverLayer,
|
||||||
|
refTime: timeIndex.r,
|
||||||
|
frames,
|
||||||
|
midpointMs: Math.round((frames[0].validTimeMs + frames[1].validTimeMs) / 2),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { Asset } from "expo-asset";
|
||||||
|
|
||||||
|
export type V4CommandResult = "ACCEPTED" | "REJECTED";
|
||||||
|
|
||||||
|
type RequiredPlane = Readonly<{
|
||||||
|
semanticID: number;
|
||||||
|
valueClass: "SCALAR" | "VELOCITY_COMPONENT" | "DIRECTION_ANGLE" | "VALIDITY";
|
||||||
|
unitID: number;
|
||||||
|
groupID: number;
|
||||||
|
component: "NONE" | "U_EAST" | "V_NORTH" | "W_UP";
|
||||||
|
requiredFlag: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type V4Dataset = Readonly<{
|
||||||
|
id: "wind" | "wave" | "current" | "rain";
|
||||||
|
title: string;
|
||||||
|
fieldKey: string;
|
||||||
|
profileContract: Readonly<{
|
||||||
|
contractSchemaVersion: 1;
|
||||||
|
profileID: string;
|
||||||
|
profileVersion: 1;
|
||||||
|
allowAdditionalPlanes: boolean;
|
||||||
|
sharedValidityRequired: boolean;
|
||||||
|
timeRelation: "VALID_GTE_REFERENCE";
|
||||||
|
requiredPlanes: readonly RequiredPlane[];
|
||||||
|
}>;
|
||||||
|
frames: readonly V4Frame[];
|
||||||
|
midpointMs: number;
|
||||||
|
colorBinding: Readonly<{
|
||||||
|
fieldKey: string;
|
||||||
|
semanticID: number;
|
||||||
|
groupID: number;
|
||||||
|
expectedValueClass: "SCALAR" | "VELOCITY_COMPONENT" | "DIRECTION_ANGLE";
|
||||||
|
expectedUnitID: number;
|
||||||
|
}>;
|
||||||
|
particleBinding?: Readonly<{
|
||||||
|
fieldKey: string;
|
||||||
|
mode: "VECTOR_COMPONENTS" | "DIRECTION_ANGLE";
|
||||||
|
uPlane?: Readonly<{ semanticID: number; groupID: number }>;
|
||||||
|
vPlane?: Readonly<{ semanticID: number; groupID: number }>;
|
||||||
|
directionPlane?: Readonly<{ semanticID: number; groupID: number }>;
|
||||||
|
expectedUnitID: number;
|
||||||
|
}>;
|
||||||
|
particleNote: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type V4Frame = Readonly<{
|
||||||
|
label: "T0" | "T1";
|
||||||
|
validTimeMs: number;
|
||||||
|
metadata: Readonly<{ fieldID: string; generation: number; dataVersion: number }>;
|
||||||
|
assetModule: number;
|
||||||
|
sourceRelativePath: string;
|
||||||
|
bytes: number;
|
||||||
|
sha256: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const sharedValidity: RequiredPlane = {
|
||||||
|
semanticID: 3, valueClass: "VALIDITY", unitID: 0, groupID: 0, component: "NONE", requiredFlag: true,
|
||||||
|
};
|
||||||
|
const vectorPlanes: readonly RequiredPlane[] = [
|
||||||
|
{ semanticID: 1, valueClass: "VELOCITY_COMPONENT", unitID: 1, groupID: 1, component: "U_EAST", requiredFlag: true },
|
||||||
|
{ semanticID: 2, valueClass: "VELOCITY_COMPONENT", unitID: 1, groupID: 1, component: "V_NORTH", requiredFlag: true },
|
||||||
|
sharedValidity,
|
||||||
|
];
|
||||||
|
const wavePlanes: readonly RequiredPlane[] = [
|
||||||
|
{ semanticID: 10, valueClass: "DIRECTION_ANGLE", unitID: 4, groupID: 0, component: "NONE", requiredFlag: true },
|
||||||
|
{ semanticID: 20, valueClass: "SCALAR", unitID: 2, groupID: 0, component: "NONE", requiredFlag: true },
|
||||||
|
{ semanticID: 21, valueClass: "SCALAR", unitID: 3, groupID: 0, component: "NONE", requiredFlag: true },
|
||||||
|
sharedValidity,
|
||||||
|
];
|
||||||
|
const rainPlanes: readonly RequiredPlane[] = [
|
||||||
|
{ semanticID: 30, valueClass: "SCALAR", unitID: 5, groupID: 0, component: "NONE", requiredFlag: true },
|
||||||
|
sharedValidity,
|
||||||
|
];
|
||||||
|
|
||||||
|
function contract(profileID: string, requiredPlanes: readonly RequiredPlane[]) {
|
||||||
|
return {
|
||||||
|
contractSchemaVersion: 1 as const,
|
||||||
|
profileID,
|
||||||
|
profileVersion: 1 as const,
|
||||||
|
allowAdditionalPlanes: true,
|
||||||
|
sharedValidityRequired: true,
|
||||||
|
timeRelation: "VALID_GTE_REFERENCE" as const,
|
||||||
|
requiredPlanes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function frame(label: "T0" | "T1", fieldID: string, validTimeMs: number, assetModule: number, sourceRelativePath: string, bytes: number, sha256: string): V4Frame {
|
||||||
|
const revision = label === "T0" ? 1 : 2;
|
||||||
|
return { label, validTimeMs, metadata: { fieldID, generation: revision, dataVersion: revision }, assetModule, sourceRelativePath, bytes, sha256 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const windFrames = [
|
||||||
|
frame("T0", "fixture-wind", 1789239600000, require("../assets/qbin-v3-golden/wind/T0.qbin"), "wind/frames/2026091219.qbin", 621120, "e815d94c18711c70e27c570cfd89acda3a27d64646d04e0b29846404ca325979"),
|
||||||
|
frame("T1", "fixture-wind", 1789243200000, require("../assets/qbin-v3-golden/wind/T1.qbin"), "wind/frames/2026091220.qbin", 622832, "bb2dbde0a8984d2fda4140cb6393744ede14fc9327663091e7afd4d2e37606da"),
|
||||||
|
] as const;
|
||||||
|
const waveFrames = [
|
||||||
|
frame("T0", "fixture-wave", 1789239600000, require("../assets/qbin-v3-golden/wave/T0.qbin"), "wave/frames/2026091219.qbin", 520688, "e4409ba0b09003e663dacdce8fc2ef35a1b929b5ccba8dc82e1ad0fc8c2f1202"),
|
||||||
|
frame("T1", "fixture-wave", 1789243200000, require("../assets/qbin-v3-golden/wave/T1.qbin"), "wave/frames/2026091220.qbin", 520016, "4827e4c9a7917da944cdf65212cc6371b0b7276fb9ae3487625f1c539b27f178"),
|
||||||
|
] as const;
|
||||||
|
const currentFrames = [
|
||||||
|
frame("T0", "fixture-current", 1789174800000, require("../assets/qbin-v3-golden/current/T0.qbin"), "tide/frames/2026091201.qbin", 104592, "b1b6d4a1dbb98971c3daca6b781587464e3528dd929da25196409b4b3b2bc052"),
|
||||||
|
frame("T1", "fixture-current", 1789178400000, require("../assets/qbin-v3-golden/current/T1.qbin"), "tide/frames/2026091202.qbin", 104112, "0588b3b1ed9e2d491adefa308f7d0b0707a38d773cc8d0802cded77a24c2aa24"),
|
||||||
|
] as const;
|
||||||
|
const rainFrames = [
|
||||||
|
frame("T0", "fixture-rain", 1789239600000, require("../assets/qbin-v3-golden/rain/T0.qbin"), "rain/frames/2026091219.qbin", 153904, "567d99c6309d185184193f203f6eb7b2891572730ec9299371e6a0db0a067d20"),
|
||||||
|
frame("T1", "fixture-rain", 1789243200000, require("../assets/qbin-v3-golden/rain/T1.qbin"), "rain/frames/2026091220.qbin", 153904, "8688d0373534d1c67ab4eed836e1314f14eba639594666683e6c22ff091e14fc"),
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const vectorColor = (fieldKey: string) => ({ fieldKey, semanticID: 1, groupID: 1, expectedValueClass: "VELOCITY_COMPONENT" as const, expectedUnitID: 1 });
|
||||||
|
const vectorParticle = (fieldKey: string) => ({ fieldKey, mode: "VECTOR_COMPONENTS" as const, uPlane: { semanticID: 1, groupID: 1 }, vPlane: { semanticID: 2, groupID: 1 }, expectedUnitID: 1 });
|
||||||
|
|
||||||
|
/** QBIN V3 Golden fixtures for the encoded-field public contract. Production block geometry is fixed at 16x16. */
|
||||||
|
export const qbinV3GoldenDatasets: readonly V4Dataset[] = [
|
||||||
|
{ id: "wind", title: "Wind QBIN V3", fieldKey: "fixture.wind", profileContract: contract("wind", vectorPlanes), frames: windFrames, midpointMs: 1789241400000, colorBinding: vectorColor("fixture.wind"), particleBinding: vectorParticle("fixture.wind"), particleNote: "U ColorBinding (VELOCITY_COMPONENT); U/V particle vectors." },
|
||||||
|
{ id: "wave", title: "Wave QBIN V3", fieldKey: "fixture.wave", profileContract: contract("wave", wavePlanes), frames: waveFrames, midpointMs: 1789241400000, colorBinding: { fieldKey: "fixture.wave", semanticID: 20, groupID: 0, expectedValueClass: "SCALAR", expectedUnitID: 2 }, particleBinding: { fieldKey: "fixture.wave", mode: "DIRECTION_ANGLE", directionPlane: { semanticID: 10, groupID: 0 }, expectedUnitID: 4 }, particleNote: "WAVE_HEIGHT color; DIRECTION angle particle (no direction conversion)." },
|
||||||
|
{ id: "current", title: "Current QBIN V3", fieldKey: "fixture.current", profileContract: contract("current", vectorPlanes), frames: currentFrames, midpointMs: 1789176600000, colorBinding: vectorColor("fixture.current"), particleBinding: vectorParticle("fixture.current"), particleNote: "U ColorBinding (VELOCITY_COMPONENT); U/V particle vectors." },
|
||||||
|
{ id: "rain", title: "Rain QBIN V3", fieldKey: "fixture.rain", profileContract: contract("rain", rainPlanes), frames: rainFrames, midpointMs: 1789241400000, colorBinding: { fieldKey: "fixture.rain", semanticID: 30, groupID: 0, expectedValueClass: "SCALAR", expectedUnitID: 5 }, particleNote: "Particle N/A: fixture has no particle-compatible plane." },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export async function materializeV4Frame(input: V4Frame): Promise<V4Frame & { qbinFileUri: string }> {
|
||||||
|
const asset = Asset.fromModule(input.assetModule);
|
||||||
|
await asset.downloadAsync();
|
||||||
|
if (!asset.localUri?.startsWith("file://")) throw new Error(`${input.label} QBIN asset did not materialize a local file:// URI`);
|
||||||
|
return { ...input, qbinFileUri: asset.localUri };
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { FieldColorStop } from "@navsea/maplibre-particle-rn";
|
||||||
|
|
||||||
|
export type WeatherDatasetId = "wind" | "wave" | "current" | "rain";
|
||||||
|
|
||||||
|
const wind: readonly FieldColorStop[] = [
|
||||||
|
{ value: 0.00000, red: 0.16, green: 0.35, blue: 0.78, alpha: 1.00 },
|
||||||
|
{ value: 2.57222, red: 0.10, green: 0.58, blue: 0.78, alpha: 1.00 },
|
||||||
|
{ value: 5.14444, red: 0.10, green: 0.68, blue: 0.34, alpha: 1.00 },
|
||||||
|
{ value: 10.28888, red: 0.70, green: 0.68, blue: 0.08, alpha: 1.00 },
|
||||||
|
{ value: 15.43332, red: 0.88, green: 0.38, blue: 0.12, alpha: 1.00 },
|
||||||
|
{ value: 20.57776, red: 0.78, green: 0.15, blue: 0.38, alpha: 1.00 },
|
||||||
|
{ value: 30.86664, red: 0.48, green: 0.18, blue: 0.70, alpha: 1.00 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const wave: readonly FieldColorStop[] = [
|
||||||
|
{ value: 0.5, red: 0.18, green: 0.58, blue: 0.68, alpha: 1.00 },
|
||||||
|
{ value: 1.0, red: 0.18, green: 0.38, blue: 0.55, alpha: 1.00 },
|
||||||
|
{ value: 1.5, red: 0.21, green: 0.38, blue: 0.71, alpha: 1.00 },
|
||||||
|
{ value: 2.0, red: 0.22, green: 0.23, blue: 0.53, alpha: 1.00 },
|
||||||
|
{ value: 6.0, red: 0.74, green: 0.29, blue: 0.35, alpha: 1.00 },
|
||||||
|
{ value: 9.0, red: 0.73, green: 0.62, blue: 0.60, alpha: 1.00 },
|
||||||
|
];
|
||||||
|
const current: readonly FieldColorStop[] = [
|
||||||
|
{ value: 0.000000, red: 0.20, green: 0.25, blue: 0.46, alpha: 1.00 },
|
||||||
|
{ value: 0.205778, red: 0.19, green: 0.50, blue: 0.18, alpha: 1.00 },
|
||||||
|
{ value: 0.411555, red: 0.50, green: 0.40, blue: 0.18, alpha: 1.00 },
|
||||||
|
{ value: 0.823110, red: 0.37, green: 0.24, blue: 0.47, alpha: 1.00 },
|
||||||
|
{ value: 1.028889, red: 0.24, green: 0.39, blue: 0.42, alpha: 1.00 },
|
||||||
|
{ value: 1.646221, red: 0.53, green: 0.53, blue: 0.53, alpha: 1.00 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rain: readonly FieldColorStop[] = [
|
||||||
|
{ value: 1.5, red: 0.21, green: 0.44, blue: 0.57, alpha: 1.00 },
|
||||||
|
{ value: 2.0, red: 0.19, green: 0.42, blue: 0.53, alpha: 1.00 },
|
||||||
|
{ value: 3.0, red: 0.20, green: 0.47, blue: 0.56, alpha: 1.00 },
|
||||||
|
{ value: 7.0, red: 0.18, green: 0.58, blue: 0.38, alpha: 1.00 },
|
||||||
|
{ value: 10.0, red: 0.47, green: 0.59, blue: 0.22, alpha: 1.00 },
|
||||||
|
{ value: 20.0, red: 0.58, green: 0.22, blue: 0.22, alpha: 1.00 },
|
||||||
|
{ value: 30.0, red: 0.58, green: 0.21, blue: 0.55, alpha: 1.00 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function colorStopsFor(id: WeatherDatasetId): readonly FieldColorStop[] {
|
||||||
|
if (id === "wind") return wind;
|
||||||
|
if (id === "wave") return wave;
|
||||||
|
if (id === "current") return current;
|
||||||
|
return rain;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function legendFor(id: WeatherDatasetId): string {
|
||||||
|
if (id === "wind") return "0 / 5 / 10 / 20 / 30 / 40 / 60 kt";
|
||||||
|
if (id === "wave") return "0.5 / 1 / 1.5 / 2 / 6 / 9 m";
|
||||||
|
if (id === "current") return "0 / 0.4 / 0.8 / 1.6 / 2 / 3.2 kt";
|
||||||
|
return "1.5 / 2 / 3 / 7 / 10 / 20 / 30 mm";
|
||||||
|
}
|
||||||
@@ -1,31 +1,56 @@
|
|||||||
/** No field is embedded here: RasterArraySource is reserved for STEP-061 file-reference submission. */
|
/** Fixed visual baseline: WeatherColor -> Particle -> local darkmap PNG. */
|
||||||
|
export const DARKMAP_SOURCE_ID = "navsea_darkmap_png";
|
||||||
|
export const DARKMAP_TILE_URL =
|
||||||
|
"http://192.168.200.133:8000/windy_tiles_japan/{z}/{x}/{y}.png";
|
||||||
|
|
||||||
export const WEATHER_SOURCE_ID = "rn-weather-fields";
|
export const WEATHER_SOURCE_ID = "rn-weather-fields";
|
||||||
export const WEATHER_COLOR_LAYER_ID = "rn-weather-color";
|
export const WEATHER_COLOR_LAYER_ID = "rn-weather-color";
|
||||||
export const PARTICLE_LAYER_ID = "rn-weather-particles";
|
export const PARTICLE_LAYER_ID = "rn-weather-particles";
|
||||||
export const GSI_SOURCE_ID = "gsi-standard";
|
|
||||||
export const GSI_LAYER_ID = "gsi-standard";
|
|
||||||
|
|
||||||
export const weatherTestStyle = JSON.stringify({
|
export const weatherTestStyle = JSON.stringify({
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
[GSI_SOURCE_ID]: {
|
[DARKMAP_SOURCE_ID]: {
|
||||||
type: "raster",
|
type: "raster",
|
||||||
tiles: ["https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png"],
|
tiles: [DARKMAP_TILE_URL],
|
||||||
tileSize: 256,
|
minzoom: 4,
|
||||||
maxzoom: 18,
|
maxzoom: 9,
|
||||||
attribution: "\u56fd\u571f\u5730\u7406\u9662 / GSI"
|
tileSize: 256
|
||||||
},
|
},
|
||||||
[WEATHER_SOURCE_ID]: { type: "raster-array", "host-field-submission": true }
|
[WEATHER_SOURCE_ID]: {
|
||||||
},
|
type: "raster-array",
|
||||||
layers: [
|
"host-field-submission": true
|
||||||
{ id: "rn-basemap", type: "background", paint: { "background-color": "#10243a" } },
|
}
|
||||||
{ id: GSI_LAYER_ID, type: "raster", source: GSI_SOURCE_ID, paint: { "raster-opacity": 1 } },
|
}, layers: [
|
||||||
{ id: WEATHER_COLOR_LAYER_ID, type: "weather-color", source: WEATHER_SOURCE_ID,
|
{
|
||||||
paint: { "weather-color-opacity": 0.62 } },
|
id: WEATHER_COLOR_LAYER_ID,
|
||||||
{ id: PARTICLE_LAYER_ID, type: "raster-particle", source: WEATHER_SOURCE_ID,
|
type: "weather-color",
|
||||||
paint: { "raster-particle-active": true, "raster-particle-particle-count": 4096,
|
source: WEATHER_SOURCE_ID,
|
||||||
"raster-particle-line-width": 1.5, "raster-particle-particle-length": 0.012,
|
paint: {
|
||||||
"raster-particle-particle-opacity": 0.9, "raster-particle-trail-opacity": 0.8,
|
"weather-color-opacity": 1.0
|
||||||
"raster-particle-trail-fade": 0.94 } }
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: PARTICLE_LAYER_ID,
|
||||||
|
type: "raster-particle",
|
||||||
|
source: WEATHER_SOURCE_ID,
|
||||||
|
paint: {
|
||||||
|
"raster-particle-active": true,
|
||||||
|
"raster-particle-particle-count": 4096,
|
||||||
|
"raster-particle-line-width": 1.8,
|
||||||
|
"raster-particle-particle-length": 0.028,
|
||||||
|
"raster-particle-particle-opacity": 0.85,
|
||||||
|
"raster-particle-trail-opacity": 0.72,
|
||||||
|
"raster-particle-trail-fade": 0.975
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
id: "darkmap-basemap",
|
||||||
|
type: "raster",
|
||||||
|
source: DARKMAP_SOURCE_ID,
|
||||||
|
paint: {
|
||||||
|
"raster-opacity": 1,
|
||||||
|
"raster-fade-duration": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user