diff --git a/docs/viewer/app/globals.css b/docs/viewer/app/globals.css index 84a086b..a9f59d2 100644 --- a/docs/viewer/app/globals.css +++ b/docs/viewer/app/globals.css @@ -186,6 +186,16 @@ } } +/* remove native number-input spinners across browsers */ +input[type=number] { + -moz-appearance: textfield; /* Firefox */ +} +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + .glow-effect { animation: glow-pulse 2s ease-in-out infinite; border-radius: 0.375rem; diff --git a/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx b/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx new file mode 100644 index 0000000..1063891 --- /dev/null +++ b/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx @@ -0,0 +1,603 @@ +import React, { + useState, useRef, useCallback, useMemo, useEffect, + type RefObject, +} from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +// Config +const CONFIG = { + fadePx: 36, + cellH: 20, + overscan: 3, + maxViewH: 220, + minViewW: 80, + scrollSlop: 1, + rhPadRight: 12, + colCellPad: 12, + colHeadPad: 4, + rhExtraChar: 2, + rhPadPx: 12, + fontSize: 12, + precision: 4, // significant figures for float display +} as const; + +const DIM_COLORS = ["#a78bfa", "#f87171", "#fb923c", "#facc15"] as const; + +// Shared styles +const STYLES = { + mono: { + fontFamily: `monospace`, + fontSize: CONFIG.fontSize, + lineHeight: `${CONFIG.cellH}px`, + whiteSpace: "nowrap", + } satisfies React.CSSProperties, + + monoXs: { + fontFamily: `monospace`, + fontSize: CONFIG.fontSize, + lineHeight: "16px", + } satisfies React.CSSProperties, + + value: { color: "var(--foreground)" } satisfies React.CSSProperties, + muted: { color: "var(--muted-foreground)" } satisfies React.CSSProperties, + accent: { color: "var(--muted-foreground)" } satisfies React.CSSProperties, +} as const; + +// Character width estimate for column sizing. text-align:right handles +// actual in-cell alignment independently. +const CHW = 7.2; + +// Glass-edge CSS + +const GLASS_CSS = ` +.ad-glass-edge { + position: absolute; + pointer-events: none; + z-index: 2; + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + background: color-mix(in srgb, var(--background) 40%, transparent); +} +.ad-glass-edge-L { top: 0; left: 0; height: 100%; width: ${CONFIG.fadePx}px; } +.ad-glass-edge-R { top: 0; right: 0; height: 100%; width: ${CONFIG.fadePx}px; } +.ad-glass-edge-T { top: 0; left: 0; width: 100%; height: ${CONFIG.fadePx}px; } +.ad-glass-edge-B { bottom: 0; left: 0; width: 100%; height: ${CONFIG.fadePx}px; } +`; + +let glassCSSInjected = false; +function ensureGlassCSS(): void { + if (glassCSSInjected) return; + const el = document.createElement("style"); + el.textContent = GLASS_CSS; + document.head.appendChild(el); + glassCSSInjected = true; +} + +// Types +type Dtype = string | undefined; +type DataArray = ArrayLike; + +type ScrollEdges = { + scrollLeft: number; + scrollTop: number; + L: boolean; + R: boolean; + T: boolean; + B: boolean; +}; + +interface FrozenMatrixProps { + data: DataArray; + offset: number; + rows: number; + cols: number; + rowHeaders: string[]; + colHeaders: string[]; + rowDim: string; + colDim: string; + dtype: Dtype; + showHeader: boolean; + containerWidth: number; +} + +interface MatrixDisplayProps { + data: DataArray; + rows: number; + cols: number; + rowDim: string; + colDim: string; + dtype: Dtype; + offset?: number; + showHeader?: boolean; + containerWidth: number; +} + +interface VectorDisplayProps { + data: DataArray; + len: number; + dimName: string; + dtype: Dtype; + containerWidth: number; +} + +interface NDDisplayProps { + data: DataArray; + shape: number[]; + dimNames: string[]; + dtype: Dtype; + containerWidth: number; +} + +interface FooterProps { + varName?: string; + shape: number[]; + totalShape?: number[]; + dtype: Dtype; +} + +export interface ArrayDisplayProps { + data: DataArray; + shape: number[]; + dimNames?: string[]; + varName?: string; + dtype?: Dtype; + totalShape?: number[]; +} + +// Helpers +function fmtVal(v: number | bigint | string, dtype: Dtype): string { + if (typeof v === "bigint") return String(v); + if (typeof v === "string") return v; + if (!Number.isFinite(v)) return String(v); + if (dtype?.startsWith("int") || dtype?.startsWith("uint")) return String(Math.trunc(v)); + const abs = Math.abs(v); + if (abs === 0) return "0"; + if (abs >= 1e5 || (abs < 1e-3 && abs > 0)) return v.toExponential(3); + return v.toPrecision(8).replace(/\.?0+$/, ""); +} + +function formatBytes(b: number): string { + const units = ["bytes", "KB", "MB", "GB"]; + let v = b, i = 0; + while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; } + return `${v.toFixed(2)} ${units[i]}`; +} + +function dtypeBytes(d: Dtype): number { + if (!d) return 4; + if (d === "S1") return 1; + if (d === "str" || d === "NAT") return 0; + const m = d.match(/^[iufc](\d+)$/); + if (m) return parseInt(m[1], 10); + if (d.includes("64")) return 8; + if (d.includes("32")) return 4; + if (d.includes("16")) return 2; + if (d.includes("8")) return 1; + return 4; +} + +// useContainerWidth +function useContainerWidth(ref: RefObject): number { + const [width, setWidth] = useState(0); + useEffect(() => { + const el = ref.current; + if (!el) return; + const ro = new ResizeObserver(entries => setWidth(entries[0].contentRect.width)); + ro.observe(el); + return () => ro.disconnect(); + }, [ref]); + return width; +} + +// useScrollState +function useScrollState(ref: RefObject): ScrollEdges { + const [state, setState] = useState({ + scrollLeft: 0, scrollTop: 0, + L: false, R: false, T: false, B: false, + }); + + const update = useCallback((): void => { + const el = ref.current; + if (!el) return; + const { scrollLeft, scrollTop, scrollWidth, scrollHeight, clientWidth, clientHeight } = el; + setState({ + scrollLeft, scrollTop, + L: scrollLeft > CONFIG.scrollSlop, + R: scrollLeft < scrollWidth - clientWidth - CONFIG.scrollSlop, + T: scrollTop > CONFIG.scrollSlop, + B: scrollTop < scrollHeight - clientHeight - CONFIG.scrollSlop, + }); + }, [ref]); + + useEffect(() => { + const el = ref.current; + if (!el) return; + update(); + el.addEventListener("scroll", update, { passive: true }); + const ro = new ResizeObserver(update); + ro.observe(el); + return () => { el.removeEventListener("scroll", update); ro.disconnect(); }; + }, [update]); + + return state; +} + +// GlassEdge +type EdgeDir = "L" | "R" | "T" | "B"; + +const GRAD_DIR: Record = { + L: "to right", R: "to left", T: "to bottom", B: "to top", +}; + +function GlassEdge({ dir }: { dir: EdgeDir }): React.ReactElement { + useEffect(ensureGlassCSS, []); + const mask = `linear-gradient(${GRAD_DIR[dir]}, black 0%, transparent 100%)`; + return ( +
+ ); +} + +// FrozenMatrix +function FrozenMatrix({ + data, offset, rows, cols, + rowHeaders, colHeaders, + rowDim, colDim, dtype, + showHeader, containerWidth, +}: FrozenMatrixProps): React.ReactElement { + + const cw = useMemo(() => { + const widths = Array.from({ length: cols }, (_, ci) => colHeaders[ci].length); + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const len = fmtVal(data[offset + r * cols + c], dtype).length; + if (len > widths[c]) widths[c] = len; + } + } + return widths; + }, [data, offset, rows, cols, colHeaders, dtype]); + + const colOffsets = useMemo(() => { + const offs = [0]; + for (let c = 0; c < cols; c++) { + offs.push(offs[c] + Math.ceil(cw[c] * CHW) + CONFIG.colCellPad); + } + return offs; + }, [cw, cols]); + + const rhW_ch = useMemo(() => + Math.max(rowDim.length + CONFIG.rhExtraChar, ...rowHeaders.map(s => s.length)), + [rowDim, rowHeaders]); + + const rhW = Math.ceil(rhW_ch * CHW) + CONFIG.rhPadPx; + const totalGridW = colOffsets[cols]; + const totalGridH = (Number.isFinite(rows) ? rows : 0) * CONFIG.cellH; + const viewW = containerWidth ? Math.max(CONFIG.minViewW, containerWidth - rhW) : 200; + const viewH = Math.min(totalGridH, CONFIG.maxViewH); + + const gridRef = useRef(null); + const colHeadRef = useRef(null); + const rowHeadRef = useRef(null); + + const scroll = useScrollState(gridRef as RefObject); + + const onGridScroll = useCallback((): void => { + const g = gridRef.current; + if (!g) return; + if (colHeadRef.current) colHeadRef.current.scrollLeft = g.scrollLeft; + if (rowHeadRef.current) rowHeadRef.current.scrollTop = g.scrollTop; + }, []); + + const rowStart = Math.max(0, Math.floor(scroll.scrollTop / CONFIG.cellH) - CONFIG.overscan); + const rowEnd = Math.min(rows, Math.ceil((scroll.scrollTop + viewH) / CONFIG.cellH) + CONFIG.overscan); + + const colStart = useMemo(() => { + let lo = 0, hi = cols; + while (lo < hi) { + const mid = (lo + hi) >> 1; + if (colOffsets[mid + 1] <= scroll.scrollLeft) { + lo = mid + 1; + } else { + hi = mid; + } + } + return Math.max(0, lo - CONFIG.overscan); + }, [colOffsets, cols, scroll.scrollLeft]); + + const colEnd = useMemo(() => { + let lo = colStart, hi = cols; + while (lo < hi) { + const mid = (lo + hi) >> 1; + if (colOffsets[mid] < scroll.scrollLeft + viewW) { + lo = mid + 1; + } else { + hi = mid; + } + } + return Math.min(cols, lo + CONFIG.overscan); + }, [colOffsets, cols, colStart, scroll.scrollLeft, viewW]); + + const paddingTop = rowStart * CONFIG.cellH; + const paddingBottom = (rows - rowEnd) * CONFIG.cellH; + const paddingLeft = colOffsets[colStart]; + const paddingRight = totalGridW - colOffsets[colEnd]; + + return ( +
+ + {/* dim label row */} +
+
+ ↓ {rowDim} +
+
+
+ → {colDim}{showHeader && dtype ? ` ${dtype}` : ""} +
+
+
+ + {/* col-index header */} +
+
+
+
+ {paddingLeft > 0 &&
} + {Array.from({ length: colEnd - colStart }, (_, i) => { + const ci = colStart + i; + return ( +
+ {colHeaders[ci]} +
+ ); + })} + {paddingRight > 0 &&
} +
+
+
+ + {/* main area */} +
+ + {/* frozen row-index column */} +
+
+ {paddingTop > 0 &&
} + {Array.from({ length: rowEnd - rowStart }, (_, i) => { + const ri = rowStart + i; + return ( +
+ {rowHeaders[ri]} +
+ ); + })} + {paddingBottom > 0 &&
} +
+
+ + {/* scrollable cell grid */} +
+ {scroll.L && } + {scroll.R && } + {scroll.T && } + {scroll.B && } + +
+
+ {paddingTop > 0 &&
} + + {Array.from({ length: rowEnd - rowStart }, (_, i) => { + const ri = rowStart + i; + return ( +
+ {paddingLeft > 0 &&
} + + {Array.from({ length: colEnd - colStart }, (_, j) => { + const ci = colStart + j; + const v = fmtVal(data[offset + ri * cols + ci], dtype); + return ( +
+ {String(v)} +
+ ); + })} + + {paddingRight > 0 &&
} +
+ ); + })} + + {paddingBottom > 0 &&
} +
+
+
+ +
+
+ ); +} + +// MatrixDisplay +function MatrixDisplay({ + data, rows, cols, rowDim, colDim, dtype, + offset = 0, showHeader = true, containerWidth, +}: MatrixDisplayProps): React.ReactElement { + const colHeaders = useMemo(() => Array.from({ length: cols }, (_, i) => String(i)), [cols]); + const rowHeaders = useMemo(() => Array.from({ length: rows }, (_, i) => String(i)), [rows]); + + return ( + + ); +} + +// VectorDisplay +function VectorDisplay({ data, len, dimName, dtype, containerWidth }: VectorDisplayProps): React.ReactElement { + const rowHeaders = useMemo(() => Array.from({ length: len }, (_, i) => String(i)), [len]); + const colHeaders = useMemo(() => [dimName], [dimName]); + + return ( + + ); +} + +// NDDisplay +const BTN_STYLE: React.CSSProperties = { + background: "none", border: "none", cursor: "pointer", + color: "inherit", padding: 0, display: "flex", alignItems: "center", +}; + +function NDDisplay({ data, shape, dimNames, dtype, containerWidth }: NDDisplayProps): React.ReactElement { + const ndim = shape.length; + const rows = shape[ndim - 2]; + const cols = shape[ndim - 1]; + const rowDim = dimNames[ndim - 2] ?? `dim_${ndim - 2}`; + const colDim = dimNames[ndim - 1] ?? `dim_${ndim - 1}`; + + const outerShape = useMemo(() => shape.slice(0, ndim - 2), [shape, ndim]); + const outerDims = useMemo(() => dimNames.slice(0, ndim - 2), [dimNames, ndim]); + const numSlices = outerShape.reduce((a, b) => a * b, 1); + + const [sliceIdx, setSliceIdx] = useState(0); + + const outerIdx = useMemo(() => { + const idx: number[] = []; + let rem = sliceIdx; + for (let d = outerShape.length - 1; d >= 0; d--) { + idx[d] = rem % outerShape[d]; + rem = Math.floor(rem / outerShape[d]); + } + return idx; + }, [sliceIdx, outerShape]); + + const offset = sliceIdx * rows * cols; + const colHeaders = useMemo(() => Array.from({ length: cols }, (_, i) => String(i)), [cols]); + const rowHeaders = useMemo(() => Array.from({ length: rows }, (_, i) => String(i)), [rows]); + + return ( +
+
+ + + + {"["} + {outerIdx.map((idx, i) => ( + + {i > 0 && , } + {idx} + + ))} + {outerIdx.length > 0 ? ", " : ""}:, : + {"]"} + + + {outerDims.map((d, i) => ( + + {d}={outerIdx[i]} + + ))} + + + + {sliceIdx + 1}/{numSlices} +
+ + +
+ ); +} + +// Footer +function Footer({ varName, shape, totalShape, dtype }: FooterProps): React.ReactElement { + const bpp = dtypeBytes(dtype); + const sb = shape.reduce((a, b) => a * b, 1) * bpp; + const has = (totalShape?.length ?? 0) > 0; + const tb = has && totalShape ? totalShape.reduce((a, b) => a * b, 1) * bpp : null; + + return ( +
+ {varName && {varName}} + + [{shape.join(", ")}] + {formatBytes(sb)} + + {has && tb !== null && <> + / + + [{totalShape!.join(", ")}] + {formatBytes(tb)} + + } +
+ ); +} + +// ArrayDisplay +export function ArrayDisplay({ + data, shape, dimNames = [], varName, dtype, totalShape, +}: ArrayDisplayProps): React.ReactElement { + const containerRef = useRef(null); + const containerWidth = useContainerWidth(containerRef as RefObject); + + const ndim = shape.length; + const total = shape.reduce((a, b) => a * b, 1); + const names = shape.map((_, i) => dimNames[i] ?? `dim_${i}`); + + let body: React.ReactElement; + if (ndim === 0 || total === 1) { + body =
{fmtVal(data[0], dtype)}
; + } else if (ndim === 1) { + body = ; + } else if (ndim === 2) { + body = ; + } else { + body = ; + } + + return ( +
+ {body} +
+
+ ); +} + +export default ArrayDisplay; \ No newline at end of file diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx new file mode 100644 index 0000000..cec809a --- /dev/null +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -0,0 +1,389 @@ +'use client'; +import React from 'react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { ButtonGroup } from '@/components/ui/button-group'; +import { PlusIcon, MinusIcon } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Terminal, ChevronRight, ChevronDown } from 'lucide-react'; +import { all, Slice, DimSelection, resolveDim } from '@earthyscience/netcdf4-wasm'; +import { VariableInfo, VariableArrayData } from './types'; +import ArrayDisplay from './ArrayDisplay'; + +// Types +export type SelectionMode = 'all' | 'scalar' | 'slice'; + +export interface SliceSelectionState { + mode: SelectionMode; + scalar: string; + start: string; + stop: string; + step: string; +} + +export function defaultSelection(): SliceSelectionState { + return { mode: 'all', scalar: '0', start: '0', stop: '', step: '1' }; +} + +const MAX_ELEMENTS = 10_000_000; + +const MODE_ACCENT: Record = { + all: 'border-l-muted-foreground/30', + scalar: 'border-l-teal-700', + slice: 'border-l-[#644FF0]', +}; + +const MODE_BADGE: Record = { + all: 'text-muted-foreground/50', + scalar: 'text-teal-700', + slice: 'text-[#644FF0]', +}; + +// Translate UI state into a DimSelection — all resolution is delegated to resolveDim +export function buildSelection( + sels: SliceSelectionState[], +): DimSelection[] { + return sels.map(s => { + if (s.mode === 'all') return all(); + if (s.mode === 'scalar') return parseInt(s.scalar) || 0; + + const start = s.start !== '' ? parseInt(s.start) : undefined; + const stop = s.stop !== '' ? parseInt(s.stop) : undefined; + const step = s.step !== '' ? parseInt(s.step) : undefined; + return new Slice(start, stop, step); + }); +} + +// Compute output shape using resolveDim — stays in sync with the library by construction +export function resultShape( + sels: SliceSelectionState[], + shape: Array +): number[] { + return sels.flatMap((s, i) => { + const sel: DimSelection = + s.mode === 'scalar' ? (parseInt(s.scalar) || 0) : + s.mode === 'all' ? all() : + new Slice( + s.start !== '' ? parseInt(s.start) : undefined, + s.stop !== '' ? parseInt(s.stop) : undefined, + s.step !== '' ? parseInt(s.step) : undefined, + ); + const resolved = resolveDim(sel, shape[i]); + return resolved.collapsed ? [] : [resolved.count]; + }); +} + +function dimBadge(s: SliceSelectionState, dimSize: number): string | null { + if (s.mode === 'scalar') return s.scalar || '0'; + if (s.mode === 'all') return 'all'; + const start = s.start !== '' ? s.start : '0'; + const stop = s.stop !== '' ? s.stop : String(dimSize); + const step = s.step !== '' ? s.step : '1'; + return `${start}:${step}:${stop}`; +} + +function resolvedBadge(s: SliceSelectionState, dimSize: number): string | null { + if (s.mode === 'scalar') { + const raw = parseInt(s.scalar); + if (Number.isNaN(raw) || raw >= 0) return null; + return String(dimSize + raw); + } + if (s.mode !== 'slice') return null; + const rawStart = s.start !== '' ? parseInt(s.start) : 0; + const rawStop = s.stop !== '' ? parseInt(s.stop) : dimSize; + const step = s.step !== '' ? s.step : '1'; + if (Number.isNaN(rawStart) || Number.isNaN(rawStop)) return null; + const absStart = rawStart < 0 ? Math.max(0, dimSize + rawStart) : rawStart; + const absStop = rawStop < 0 ? Math.max(0, dimSize + rawStop) : rawStop; + const [resolvedStart, resolvedStop] = absStart > absStop ? [absStop, absStart] : [absStart, absStop]; + if (resolvedStart === rawStart && resolvedStop === rawStop) return null; + return `${resolvedStart}:${step}:${resolvedStop}`; +} + +function clampStep(sel: SliceSelectionState, dimSize: number): SliceSelectionState { + if (sel.mode !== 'slice') return sel; + let step = sel.step !== '' ? parseInt(sel.step) : 1; + if (Number.isNaN(step) || step <= 0) step = 1; + const clamped = Math.min(dimSize, step); + return sel.step === String(clamped) ? sel : { ...sel, step: String(clamped) }; +} + +function fmtCount(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} M`; + if (n >= 1_000) return `${(n / 1_000).toFixed(1)} K`; + return String(n); +} + +interface SliceTesterSectionProps { + info: VariableInfo; + sliceSelections: SliceSelectionState[]; + setSliceSelections: React.Dispatch>; + expandedSliceTester: boolean; + setExpandedSliceTester: (v: boolean) => void; + sliceResult: VariableArrayData | null; + sliceError: string | null; + loadingSlice: boolean; + onRun: () => void; +} + +const SliceTester: React.FC = ({ + info, + sliceSelections, + setSliceSelections, + expandedSliceTester, + setExpandedSliceTester, + sliceResult, + sliceError, + loadingSlice, + onRun, +}) => { + if (!info?.shape || info.shape.length === 0) return null; + + const shape: number[] = info.shape.map(Number); + + const parseOr = (v: string, fallback: number) => { + const n = parseInt(v); + return Number.isNaN(n) ? fallback : n; + }; + + const updateSel = (i: number, patch: Partial) => + setSliceSelections(prev => prev.map((s, idx) => { + if (idx !== i) return s; + const next = { ...s, ...patch }; + return clampStep(next, Number(shape[i])); + })); + + const changeBy = (i: number, key: keyof Omit, delta: number) => { + setSliceSelections(prev => prev.map((s, idx) => { + if (idx !== i) return s; + const dimSize = Number(shape[i]); + if (key === 'step') { + let step = s.step !== '' ? parseOr(s.step, 1) : 1; + if (step <= 0) step = 1; + const nextStep = Math.max(1, Math.min(dimSize, step + delta)); + const next = { ...s, step: String(nextStep) }; + return clampStep(next, dimSize); + } else { + let val = parseInt(s[key] || '0'); + if (Number.isNaN(val)) val = 0; + val += delta; + const lo = -dimSize; + const hi = key === 'stop' ? dimSize : dimSize - 1; + val = Math.max(lo, Math.min(hi, val)); + const next = { ...s, [key]: String(val) } as SliceSelectionState; + return clampStep(next, dimSize); + } + })); + }; + + const rShape = resultShape(sliceSelections, info.shape); + const rDims = info.dimensions?.filter((_, i) => sliceSelections[i]?.mode !== 'scalar'); + const nElements = rShape.reduce((a, b) => a * b, 1); + const tooMany = nElements > MAX_ELEMENTS; + + return ( +
+ {/* Header */} + + + {expandedSliceTester && ( +
+ + {/* Dimension rows */} +
+ {shape.map((dimSize, i) => { + const dimName = info.dimensions?.[i] ?? `dim_${i}`; + const sel = sliceSelections[i] ?? defaultSelection(); + const badge = dimBadge(sel, dimSize); + const resBadge = resolvedBadge(sel, dimSize); + return ( +
+ {/* Row: dim name + badge + mode tabs */} +
+
+ + {dimName} + [{dimSize}] + + {badge !== null && ( + + → {badge} + + )} + {resBadge !== null && ( + + [{resBadge}] + + )} +
+ + {/* Mode tabs */} +
+ {(['all', 'scalar', 'slice'] as SelectionMode[]).map(m => ( + + ))} +
+
+ + {/* Scalar input */} + {sel.mode === 'scalar' && ( +
+ + + updateSel(i, { scalar: e.target.value })} + className="h-7 text-xs w-16 font-mono text-center appearance-none" + placeholder="0" + /> + + + + ({-dimSize} to {dimSize - 1}) + +
+ )} + + {/* Slice inputs */} + {sel.mode === 'slice' && ( +
+ {[ + { label: 'start', key: 'start' as const, placeholder: '0', min: -dimSize, max: dimSize - 1 }, + { label: 'stop', key: 'stop' as const, placeholder: String(dimSize), min: -dimSize, max: dimSize }, + { label: 'step', key: 'step' as const, placeholder: '1', min: 1, max: dimSize }, + ].map(({ label, key, placeholder, min, max }) => ( +
+ {label} + + + updateSel(i, { [key]: e.target.value })} + className="h-7 text-xs w-16 font-mono text-center appearance-none" + placeholder={placeholder} + /> + + +
+ ))} +
+ )} +
+ ); + })} +
+ + {/* Selection preview + Run */} +
+
+ {`dataset.get("${info.name}", [${ + sliceSelections.map((s, i) => { + if (s.mode === 'all') return 'all'; + if (s.mode === 'scalar') return s.scalar || '0'; + const dimSize = shape[i]; + const parts: string[] = [s.start || '0', s.stop || String(dimSize)]; + if (s.step && s.step !== '1') parts.push(s.step); + return `slice(${parts.join(', ')})`; + }).join(', ') + }])`} +
+
+ + {tooMany ? ( + + {fmtCount(nElements)} elements — too many to display at once. Narrow your selection, try {'<'}10 M. + + ) : ( + + {fmtCount(nElements)} elements + + )} + {sliceResult && !tooMany && ( + + {sliceResult.length ?? 0} loaded + + )} +
+
+ + {/* Error */} + {sliceError && ( + + + {sliceError} + + )} + + {/* Result display */} + {sliceResult && ( + + )} + +
+ )} +
+ ); +}; + +export { SliceTester }; +export default SliceTester; \ No newline at end of file diff --git a/docs/viewer/components/loading/netcdf/VariableDataLoader.tsx b/docs/viewer/components/loading/netcdf/VariableDataLoader.tsx index c679a67..0fa72ad 100644 --- a/docs/viewer/components/loading/netcdf/VariableDataLoader.tsx +++ b/docs/viewer/components/loading/netcdf/VariableDataLoader.tsx @@ -1,20 +1,20 @@ 'use client'; import React from 'react'; import { Label } from '@/components/ui/label'; -import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Spinner } from '@/components/ui/spinner'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Card, CardContent } from '@/components/ui/card'; import { VariableData, VariableArrayData } from './types'; type ArrayElement = number | bigint | string; +const DEFAULT_SLICE_SIZE = 2; + interface VariableDataLoaderProps { variableName: string; variable: VariableData; loadingVariable: string | null; - sliceSize: string; - onSliceSizeChange: (value: string) => void; onLoadSlice: (name: string, size: number) => void; onLoadAll: (name: string) => void; } @@ -46,8 +46,6 @@ export const VariableDataLoader = ({ variableName, variable, loadingVariable, - sliceSize, - onSliceSizeChange, onLoadSlice, onLoadAll, }: VariableDataLoaderProps) => { @@ -65,39 +63,34 @@ export const VariableDataLoader = ({
{!isStringType && ( <> -
- - onSliceSizeChange(e.target.value)} - className="h-9 text-sm" - placeholder="10" - /> -
- + + + + + + +

Loading all data may be slow or crash for large variables.

+
+
+
{isLoading && (
@@ -117,9 +110,9 @@ export const VariableDataLoader = ({ variant="outline" onClick={() => onLoadAll(variableName)} disabled={isLoading} - className="w-full sm:w-auto" + className="w-full sm:w-auto cursor-pointer" > - Load All + Load String Data {isLoading && (
diff --git a/docs/viewer/components/loading/netcdf/Viewer.tsx b/docs/viewer/components/loading/netcdf/Viewer.tsx index c6377a0..f2acb61 100644 --- a/docs/viewer/components/loading/netcdf/Viewer.tsx +++ b/docs/viewer/components/loading/netcdf/Viewer.tsx @@ -13,11 +13,11 @@ import { SearchBar, VariableDetails, VariableDataLoader, + SliceTester, DimensionsCard, AttributesCard, } from '@/components/loading/netcdf'; - -import { useViewerState } from '@/components/loading/netcdf'; +import { useViewerState } from '@/components/loading/netcdf/useViewerState'; const Viewer = () => { const { @@ -72,6 +72,14 @@ const Viewer = () => { handleToggleGroupExpand, handleSearchInputChange, selectSearchResult, + sliceSelections, + setSliceSelections, + expandedSliceTester, + setExpandedSliceTester, + sliceResult, + sliceError, + loadingSlice, + handleRunSlice, // Derived breadcrumbs, groupSummary, @@ -233,14 +241,27 @@ const Viewer = () => { /> )} - {/* Variable Data Loader */} + {/* Slice Tester */} + {selectedVariable && variables[selectedVariable]?.info && ( + + )} + + {/* Variable Data Loader */} {selectedVariable && variables[selectedVariable]?.info && ( diff --git a/docs/viewer/components/loading/netcdf/index.ts b/docs/viewer/components/loading/netcdf/index.ts index 605ec52..857b46a 100644 --- a/docs/viewer/components/loading/netcdf/index.ts +++ b/docs/viewer/components/loading/netcdf/index.ts @@ -6,6 +6,7 @@ export { VariableMenuTrigger, VariableMenuPanel } from './VariableMenu'; export { SearchBar } from './SearchBar'; export { VariableDetails } from './VariableDetails'; export { VariableDataLoader } from './VariableDataLoader'; +export { SliceTester } from './SliceTester'; export { DimensionsCard } from './DimensionsCard'; export { AttributesCard } from './AttributesCard'; export type { VariableData, VariableInfo, VariableArrayData, Dimension, EnumType } from './types'; \ No newline at end of file diff --git a/docs/viewer/components/loading/netcdf/useViewerState.ts b/docs/viewer/components/loading/netcdf/useViewerState.ts index 9c56b1f..2b7a31c 100644 --- a/docs/viewer/components/loading/netcdf/useViewerState.ts +++ b/docs/viewer/components/loading/netcdf/useViewerState.ts @@ -2,6 +2,7 @@ import { ChangeEvent, useState, useEffect, useCallback } from 'react'; import { NetCDF4, DataTree } from '@earthyscience/netcdf4-wasm'; import { VariableData, VariableInfo, VariableArrayData, Dimension } from './types'; +import { SliceSelectionState, defaultSelection, buildSelection } from './SliceTester'; const NETCDF_EXT_REGEX = /\.(nc|netcdf|nc3|nc4)$/i; @@ -252,6 +253,45 @@ export const useViewerState = () => { setSearchResults([]); }; + // Slice tester state + const [sliceSelections, setSliceSelections] = useState([]); + const [expandedSliceTester, setExpandedSliceTester] = useState(true); + const [sliceResult, setSliceResult] = useState(null); + const [sliceError, setSliceError] = useState(null); + const [loadingSlice, setLoadingSlice] = useState(false); + + // Reset slice tester when selected variable changes + useEffect(() => { + if (!selectedVariable) return; + const info = variables[selectedVariable]?.info; + if (!info?.shape) return; + setSliceSelections(info.shape.map(() => defaultSelection())); + setSliceResult(null); + setSliceError(null); + }, [selectedVariable, variables]); + + const handleRunSlice = useCallback(async () => { + if (!dataset || !selectedVariable) return; + const info = variables[selectedVariable]?.info; + if (!info?.shape) return; + setLoadingSlice(true); + setSliceError(null); + try { + const selection = buildSelection(sliceSelections); + // or, await (dataset as NetCDF4).get(...) if you prefer an explicit cast. + const data = await dataset.get( + selectedVariable, + selection, + currentGroupPath === '/' ? undefined : currentGroupPath + ) as VariableArrayData; + setSliceResult(data); + } catch (err) { + setSliceError(err instanceof Error ? err.message : String(err)); + } finally { + setLoadingSlice(false); + } + }, [dataset, selectedVariable, variables, sliceSelections, currentGroupPath]); + // Derived values const breadcrumbs = tree ? tree.getBreadcrumbs(currentGroupPath) : []; @@ -310,6 +350,15 @@ export const useViewerState = () => { handleToggleGroupExpand, handleSearchInputChange, selectSearchResult, + // Slice tester + sliceSelections, + setSliceSelections, + expandedSliceTester, + setExpandedSliceTester, + sliceResult, + sliceError, + loadingSlice, + handleRunSlice, // Derived breadcrumbs, groupSummary, diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index b199f0f..f2caa99 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -963,6 +963,76 @@ int nc_get_var_ulonglong_wrapper(int ncid, int varid, unsigned long long* value) return nc_get_var_ulonglong(ncid, varid, value); } +// Strided Data Access (nc_get_vars_*) + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_schar_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, signed char* value) { + return nc_get_vars_schar(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_uchar_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, unsigned char* value) { + return nc_get_vars_uchar(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_short_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, short* value) { + return nc_get_vars_short(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_ushort_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, unsigned short* value) { + return nc_get_vars_ushort(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_int_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, int* value) { + return nc_get_vars_int(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_uint_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, unsigned int* value) { + return nc_get_vars_uint(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_float_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, float* value) { + return nc_get_vars_float(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_double_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, double* value) { + return nc_get_vars_double(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_longlong_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, long long* value) { + return nc_get_vars_longlong(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_ulonglong_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, unsigned long long* value) { + return nc_get_vars_ulonglong(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_string_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, char** value) { + return nc_get_vars_string(ncid, varid, start, count, stride, value); +} + +// Generic strided wrapper — mirrors nc_get_vara_wrapper. +// nc_get_vars (no type suffix) reads raw bytes into void* without type checking, +// so it works for enum variables (declared type >= 32) unlike nc_get_vars_schar etc. +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, void* value) { + return nc_get_vars(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_text_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, char* value) { + return nc_get_vars_text(ncid, varid, start, count, stride, value); +} + // ========================= // Data Writing // ========================= diff --git a/src/__tests__/test-slice.test.ts b/src/__tests__/test-slice.test.ts new file mode 100644 index 0000000..7b30631 --- /dev/null +++ b/src/__tests__/test-slice.test.ts @@ -0,0 +1,16 @@ +import { all, slice, resolveDim } from '../slice.js'; + +describe('slice selection compatibility', () => { + test('all() behaves like slice(0, dimSize, 1)', () => { + const dimSize = 17; + expect(resolveDim(all(), dimSize)).toEqual(resolveDim(slice(0, dimSize, 1), dimSize)); + }); + + test('null and "all" behave like all()', () => { + const dimSize = 5; + const expected = resolveDim(all(), dimSize); + expect(resolveDim(null, dimSize)).toEqual(expected); + expect(resolveDim("all", dimSize)).toEqual(expected); + }); +}); + diff --git a/src/index.ts b/src/index.ts index 7920845..5507e1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,8 @@ // Export all classes and types export { NetCDF4, DataTree } from './netcdf4.js'; +export { slice, Slice, all, isAll, resolveDim } from './slice.js'; +export type { DimSelection, ResolvedDim } from './slice.js'; export type { GroupNode } from './netcdf4.js'; export { Variable } from './variable.js'; export { Dimension } from './dimension.js'; diff --git a/src/netcdf-getters.ts b/src/netcdf-getters.ts index bdcb719..2c88155 100644 --- a/src/netcdf-getters.ts +++ b/src/netcdf-getters.ts @@ -1,5 +1,6 @@ import { NC_CONSTANTS, DATA_TYPE_SIZE, CONSTANT_DTYPE_MAP } from './constants.js'; import type { NetCDF4Module } from './types.js'; +import { DimSelection, ResolvedDim, resolveDim } from "./slice.js"; export function getGroupVariables( module: NetCDF4Module, @@ -645,6 +646,169 @@ export function getSlicedVariableArray( return arrayData.data; } +type AnyTypedArray = + | Int8Array | Uint8Array + | Int16Array | Uint16Array + | Int32Array | Uint32Array + | Float32Array | Float64Array + | BigInt64Array | BigUint64Array + | string[]; + +export function getVariableArrayWithSelection( + module: NetCDF4Module, + ncid: number, + variable: number | string, + selection: DimSelection[], + groupPath?: string, + options?: { convertEnumsToNames?: boolean } +): AnyTypedArray { + const workingNcid = groupPath ? getGroupNCID(module, ncid, groupPath) : ncid; + + // Resolve varid + let varid: number; + if (typeof variable === "number") { + varid = variable; + } else { + const r = module.nc_inq_varid(workingNcid, variable); + if (r.result !== NC_CONSTANTS.NC_NOERR) { + throw new Error(`Failed to get variable id for '${variable}' (error: ${r.result})`); + } + varid = r.varid as number; + } + + // Query variable dimensions + const varInfo = module.nc_inq_var(workingNcid, varid); + if (varInfo.result !== NC_CONSTANTS.NC_NOERR) { + throw new Error(`Failed to query variable info (error: ${varInfo.result})`); + } + + const dimids: number[] = Array.from(varInfo.dimids ?? []); + const ndim = dimids.length; + + if (selection.length !== ndim) { + throw new Error( + `Selection has ${selection.length} dimension(s) but variable has ${ndim}` + ); + } + + // Fetch each dimension's size — resolveDim handles BigInt safely + const dimSizes = dimids.map((dimid: number) => { + const r = module.nc_inq_dimlen(workingNcid, dimid); + if (r.result !== NC_CONSTANTS.NC_NOERR) { + throw new Error(`Failed to query dim length for dimid ${dimid} (error: ${r.result})`); + } + return r.len as number | bigint; + }); + + // Resolve each selection + const resolved: ResolvedDim[] = selection.map((sel, i) => + resolveDim(sel, dimSizes[i]) + ); + + const start = resolved.map(d => d.start); + const count = resolved.map(d => d.count); + const stride = resolved.map(d => d.step); + + // Validate parameters against dimension sizes + for (let i = 0; i < start.length; i++) { + const dimSize = Number(dimSizes[i]); + if (start[i] < 0 || start[i] >= dimSize) { + throw new Error( + `Invalid start[${i}] = ${start[i]} for dimension size ${dimSize}` + ); + } + if (count[i] === 0) continue; // valid empty slice — skip further checks + const lastIdx = start[i] + (count[i] - 1) * stride[i]; + if (lastIdx >= dimSize) { + throw new Error( + `Stride read would exceed dimension bounds: start[${i}]=${start[i]}, ` + + `count[${i}]=${count[i]}, stride[${i}]=${stride[i]}, last_idx=${lastIdx}, dimSize=${dimSize}` + ); + } + } + + // Guard against integer overflow in WASM — total elements must fit in a signed 32-bit int + // not sure how well is this, ~2 billion elements, will perform in JS but may cause issues in WASM memory allocation or indexing + const totalElements = count.reduce((acc, c) => acc * c, 1); + if (totalElements > 2 ** 31 - 1) { + throw new Error( + `Selection would read ${totalElements} elements, exceeding the safe limit of 2^31 - 1` + ); + } + + // Resolve variable type — use workingNcid so enum lookup is in the right group + const { enumCtx } = resolveVariableType(module, workingNcid, varid); + const arrayType = enumCtx.baseType; + + // Return appropriately typed empty array if any dimension has count 0 + if (count.some(c => c === 0)) { + const emptyReaders: Record AnyTypedArray> = { + [NC_CONSTANTS.NC_BYTE]: () => new Int8Array(0), + [NC_CONSTANTS.NC_UBYTE]: () => new Uint8Array(0), + [NC_CONSTANTS.NC_SHORT]: () => new Int16Array(0), + [NC_CONSTANTS.NC_USHORT]: () => new Uint16Array(0), + [NC_CONSTANTS.NC_INT]: () => new Int32Array(0), + [NC_CONSTANTS.NC_UINT]: () => new Uint32Array(0), + [NC_CONSTANTS.NC_FLOAT]: () => new Float32Array(0), + [NC_CONSTANTS.NC_DOUBLE]: () => new Float64Array(0), + [NC_CONSTANTS.NC_INT64]: () => new BigInt64Array(0), + [NC_CONSTANTS.NC_LONGLONG]: () => new BigInt64Array(0), + [NC_CONSTANTS.NC_UINT64]: () => new BigUint64Array(0), + [NC_CONSTANTS.NC_ULONGLONG]: () => new BigUint64Array(0), + [NC_CONSTANTS.NC_STRING]: () => [], + }; + return (emptyReaders[arrayType] ?? (() => new Float64Array(0)))(); + } + + let arrayData: { result: number; data?: any }; + + if (enumCtx.isEnum) { + arrayData = module.nc_get_vars_generic(workingNcid, varid, start, count, stride, arrayType); + } else { + type VarsArgs = [number, number, number[], number[], number[]]; + type VarsResult = { result: number; data?: any }; + + const readers: Record VarsResult> = { + [NC_CONSTANTS.NC_CHAR]: (...args) => module.nc_get_vars_text(...args), + [NC_CONSTANTS.NC_BYTE]: (...args) => module.nc_get_vars_schar(...args), + [NC_CONSTANTS.NC_UBYTE]: (...args) => module.nc_get_vars_uchar(...args), + [NC_CONSTANTS.NC_SHORT]: (...args) => module.nc_get_vars_short(...args), + [NC_CONSTANTS.NC_USHORT]: (...args) => module.nc_get_vars_ushort(...args), + [NC_CONSTANTS.NC_INT]: (...args) => module.nc_get_vars_int(...args), + [NC_CONSTANTS.NC_UINT]: (...args) => module.nc_get_vars_uint(...args), + [NC_CONSTANTS.NC_FLOAT]: (...args) => module.nc_get_vars_float(...args), + [NC_CONSTANTS.NC_DOUBLE]: (...args) => module.nc_get_vars_double(...args), + [NC_CONSTANTS.NC_INT64]: (...args) => module.nc_get_vars_longlong(...args), + [NC_CONSTANTS.NC_LONGLONG]: (...args) => module.nc_get_vars_longlong(...args), + [NC_CONSTANTS.NC_UINT64]: (...args) => module.nc_get_vars_ulonglong(...args), + [NC_CONSTANTS.NC_ULONGLONG]: (...args) => module.nc_get_vars_ulonglong(...args), + [NC_CONSTANTS.NC_STRING]: (...args) => module.nc_get_vars_string(...args), + }; + + const reader = readers[arrayType]; + if (!reader) { + arrayData = module.nc_get_vars_double(workingNcid, varid, start, count, stride); + } else { + arrayData = reader(workingNcid, varid, start, count, stride); + } + } + + if (arrayData.result !== NC_CONSTANTS.NC_NOERR) { + throw new Error(`Failed to read array data (error: ${arrayData.result})`); + } + if (!arrayData.data) { + throw new Error("nc_get_vars returned no data"); + } + + let result: AnyTypedArray = arrayData.data; + + if (enumCtx.isEnum && options?.convertEnumsToNames && enumCtx.enumDict) { + result = convertEnumValuesToNames(result, enumCtx.enumDict); + } + + return result; +} + //---- Group Functions ----// /** diff --git a/src/netcdf-worker.ts b/src/netcdf-worker.ts index ff85a19..662b186 100644 --- a/src/netcdf-worker.ts +++ b/src/netcdf-worker.ts @@ -136,6 +136,17 @@ self.onmessage = async (e: MessageEvent) => { result = NCGet.getAttributeValues(mod, data.ncid, data.varid, data.attname); break; + case 'getVariableArrayWithSelection': + result = NCGet.getVariableArrayWithSelection( + mod, + data.ncid, + data.variable, + data.selection, + data.groupPath, + data.options + ); + break; + // ---- Arrays ---- case 'getVariableArray': result = NCGet.getVariableArray(mod, data.ncid, data.variable, data.groupPath); diff --git a/src/netcdf4.ts b/src/netcdf4.ts index 78e261a..088c74f 100644 --- a/src/netcdf4.ts +++ b/src/netcdf4.ts @@ -5,6 +5,7 @@ import { WasmModuleLoader } from './wasm-module.js'; import { NC_CONSTANTS } from './constants.js'; import type { NetCDF4Module, DatasetOptions, MemoryDatasetSource } from './types.js'; import * as NCGet from './netcdf-getters.js' +import { DimSelection } from './slice.js'; export class NetCDF4 extends Group { private module: NetCDF4Module | null = null; @@ -444,6 +445,58 @@ export class NetCDF4 extends Group { } } + /** + * slicing and indexing convenience method. + * + * Each element of `selection` corresponds to one dimension of the variable: + * - 'all' or `null` → all elements of that dimension + * - `number` → scalar index (dimension is collapsed) + * - `slice(stop)` → elements [0, stop) + * - `slice(start, stop)` → elements [start, stop) + * - `slice(start, stop, step)` → strided subset; steps are always positive, reading is always forward, you can achieve negative step by reversing the dimension after reading + * + * Strided reads use nc_get_vars_* under the hood. + * Returns a flat typed array; caller infers shape from non-collapsed dims. + * + * @example + * // Variable shape [time, lat, lon] + * // Read 10 time steps, all lats, first lon: + * const data = await dataset.get("temperature", [slice(0, 10), 'all', 0]); + * + * @example + * // Every other element along time: + * const data = await dataset.get("temperature", [slice(0, 100, 2), null, null]); + * + */ + async get( + variable: number | string, + selection: DimSelection[], + groupPath?: string, + options?: { convertEnumsToNames?: boolean } + ): Promise< + Int8Array | Uint8Array | + Int16Array | Uint16Array | + Int32Array | Uint32Array | + Float32Array | Float64Array | + BigInt64Array | BigUint64Array | + string[] + > { + if (this.worker) { + return this.callWorker('getVariableArrayWithSelection', { + ncid: this.ncid, variable, selection, groupPath, options + }); + } else { + return NCGet.getVariableArrayWithSelection( + this.module as NetCDF4Module, + this.ncid, + variable, + selection, + groupPath, + options + ); + } + } + // Group functions async getGroups(ncid: number = this.ncid): Promise> { if (this.worker) { diff --git a/src/slice.ts b/src/slice.ts new file mode 100644 index 0000000..15f9654 --- /dev/null +++ b/src/slice.ts @@ -0,0 +1,121 @@ +/** + * Represents selecting the entire dimension. + */ +export interface All { + readonly type: "all"; +} + +/** Canonical full-dimension selector */ +export function all(): All { + return { type: "all" }; +} + +export function isAll(v: unknown): v is All { + return typeof v === "object" && v !== null && (v as any).type === "all"; +} + +/** + * Represents a dimension slice. + * Undefined fields are resolved against the actual dimension size at read time. + */ +export class Slice { + constructor( + public readonly start?: number, + public readonly stop?: number, + public readonly step?: number + ) {} +} + +/** + * slice(stop) + * slice(start, stop) + * slice(start, stop, step) + */ +export function slice(stop: number): Slice; +export function slice(start: number, stop: number): Slice; +export function slice(start: number, stop: number, step: number): Slice; +export function slice( + startOrStop: number, + stop?: number, + step?: number +): Slice { + if (stop === undefined) { + return new Slice(undefined, startOrStop, undefined); + } + return new Slice(startOrStop, stop, step); +} + +/** + * A single dimension selection: + * - all() → full dimension + * - "all" → full dimension (compat) + * - null → full dimension (compat) + * - number → scalar index + * - Slice → range selection + */ +export type DimSelection = All | "all" | null | number | Slice; + +/** + * Resolved, concrete read parameters for one dimension after applying a + * DimSelection against a known dimension size. + */ +export interface ResolvedDim { + /** Start index in the NetCDF dimension */ + start: number; + + /** Number of elements to read from NetCDF (contiguous span, always >= 0) */ + count: number; + + /** Step. Always positive. Never 0. */ + step: number; + + /** True when the selection was a scalar index — dimension is collapsed */ + collapsed: boolean; +} + +/** + * Resolve a DimSelection against a concrete dimension size. + * Accepts BigInt dimension sizes (from nc_inq_dimlen i64 reads) and coerces safely. + * Step is always treated as positive; start/stop are swapped if out of order. + */ +export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): ResolvedDim { + + const dimSize = Number(dimSizeRaw); + + // full dimension + if (sel === null || sel === "all" || isAll(sel)) { + return { start: 0, count: dimSize, step: 1, collapsed: false }; + } + + // scalar index + if (typeof sel === "number") { + const idx = sel < 0 ? dimSize + sel : sel; + const clamped = Math.max(0, Math.min(idx, dimSize - 1)); + return { start: clamped, count: 1, step: 1, collapsed: true }; + } + + // Slice — step is always positive + const step = sel.step ?? 1; + if (step <= 0) throw new Error("Slice step must be a positive integer"); + + const rawStart = sel.start ?? 0; + const rawStop = sel.stop ?? dimSize; + + const start = Math.max( + 0, + Math.min(rawStart < 0 ? dimSize + rawStart : rawStart, dimSize) + ); + + const stop = Math.max( + 0, + Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize) + ); + + // Swap if caller passed start > stop — resolve to the correct forward range + const lo = Math.min(start, stop); + const hi = Math.max(start, stop); + + const span = hi - lo; + const count = span === 0 ? 0 : Math.ceil(span / step); + return { start: lo, count, step, collapsed: false }; +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 7379e3b..1c01e77 100644 --- a/src/types.ts +++ b/src/types.ts @@ -119,6 +119,23 @@ export interface NetCDF4Module extends EmscriptenModule { nc_get_vara_ulonglong: (ncid: number, varid: number, startp: number[], countp: number[]) => { result: number; data?: BigUint64Array }; nc_get_vara_string: (ncid: number, varid: number, startp: number[], countp: number[]) => { result: number; data?: string[] }; + + // Strided Variable Getters (nc_get_vars_*) + // stride[i] = step along dimension i (must be >= 1; negatives handled at higher level) + nc_get_vars_schar: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Int8Array }; + nc_get_vars_uchar: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Uint8Array }; + nc_get_vars_short: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Int16Array }; + nc_get_vars_ushort: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Uint16Array }; + nc_get_vars_int: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Int32Array }; + nc_get_vars_uint: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Uint32Array }; + nc_get_vars_float: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Float32Array }; + nc_get_vars_double: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Float64Array }; + nc_get_vars_longlong: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: BigInt64Array }; + nc_get_vars_ulonglong: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: BigUint64Array }; + nc_get_vars_string: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: string[] }; + nc_get_vars_generic: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[], nctype: number) => { result: number; data?: Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array | string[] }; + nc_get_vars_text: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { result: number; data?: string[] }; + // group types and functions nc_inq_grps: (ncid: number) => { result: number; numgrps?: number; grpids?: Int32Array }; nc_inq_grp_ncid: (ncid: number, grp_name: string) => { result: number; grp_ncid?: number }; diff --git a/src/wasm-module.ts b/src/wasm-module.ts index 649397e..54fb51e 100644 --- a/src/wasm-module.ts +++ b/src/wasm-module.ts @@ -7,6 +7,23 @@ const NC_MAX_NAME = 256; const NC_MAX_DIMS = 1024; const NC_MAX_VARS = 8192; +function stridedLength(count: number[]): number { + return count.reduce((acc, c) => acc * c, 1); +} + +// Helper: validates that the total byte allocation won't overflow a WASM32 uint32. +// Throws a RangeError if the allocation would be unsafe. +function safeByteLength(totalLength: number, elementSize: number): number { + const byteLength = totalLength * elementSize; + if (byteLength > 0xFFFFFFFF) { + throw new RangeError( + `Allocation size ${byteLength} exceeds WASM32 heap limit (0xFFFFFFFF). ` + + `Requested ${totalLength} elements of ${elementSize} bytes each.` + ); + } + return byteLength; +} + export class WasmModuleLoader { static async loadModule(options: NetCDF4WasmOptions = {}): Promise { try { @@ -130,6 +147,22 @@ export class WasmModuleLoader { const nc_get_var_ulonglong_wrapper = module.cwrap('nc_get_var_ulonglong_wrapper', 'number', ['number', 'number', 'number']); const nc_get_var_string_wrapper = module.cwrap('nc_get_var_string_wrapper', 'number', ['number', 'number', 'number']); + // Stride inquiry wrappers + + const nc_get_vars_schar_wrapper = module.cwrap('nc_get_vars_schar_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_uchar_wrapper = module.cwrap('nc_get_vars_uchar_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_short_wrapper = module.cwrap('nc_get_vars_short_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_ushort_wrapper = module.cwrap('nc_get_vars_ushort_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_int_wrapper = module.cwrap('nc_get_vars_int_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_uint_wrapper = module.cwrap('nc_get_vars_uint_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_float_wrapper = module.cwrap('nc_get_vars_float_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_double_wrapper = module.cwrap('nc_get_vars_double_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_longlong_wrapper = module.cwrap('nc_get_vars_longlong_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_ulonglong_wrapper = module.cwrap('nc_get_vars_ulonglong_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_string_wrapper = module.cwrap('nc_get_vars_string_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_wrapper = module.cwrap('nc_get_vars_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_text_wrapper = module.cwrap('nc_get_vars_text_wrapper', 'number', ['number','number','number','number','number','number']); + // const nc_get_vars_as_type_wrapper = module.cwrap('nc_get_vars_as_type_wrapper', 'number', ['number','number','number','number','number','number','number']); // Group inquiry wrappers const nc_inq_grps_wrapper = module.cwrap('nc_inq_grps_wrapper', 'number', ['number', 'number', 'number']); @@ -212,12 +245,14 @@ export class WasmModuleLoader { nc_inq_dim: (ncid: number, dimid: number) => { const namePtr = module._malloc(NC_MAX_NAME + 1); - const lenPtr = module._malloc(8); // size_t (use 8 bytes for safety in wasm64) + // In Emscripten wasm32 builds, size_t is 32-bit. + // (If we ever build wasm64, this will need revisiting.) + const lenPtr = module._malloc(4); const result = nc_inq_dim_wrapper(ncid, dimid, namePtr, lenPtr); let name, len; if (result === NC_CONSTANTS.NC_NOERR) { name = module.UTF8ToString(namePtr); - len = module.getValue(lenPtr, 'i32'); // or 'i32' if your build uses 32-bit size_t + len = module.getValue(lenPtr, 'i32') >>> 0; } module._free(namePtr); module._free(lenPtr); @@ -233,9 +268,14 @@ export class WasmModuleLoader { }, nc_inq_dimlen: (ncid: number, dimid: number) => { - const lenPtr = module._malloc(8); + // In Emscripten wasm32 builds, size_t is 32-bit. + // Allocate 4 bytes and read as unsigned i32 to avoid garbage high bits. + const lenPtr = module._malloc(4); const result = nc_inq_dimlen_wrapper(ncid, dimid, lenPtr); - const len = result === NC_CONSTANTS.NC_NOERR ? module.getValue(lenPtr, 'i64') : undefined; + let len: number | undefined; + if (result === NC_CONSTANTS.NC_NOERR) { + len = module.getValue(lenPtr, 'i32') >>> 0; + } module._free(lenPtr); return { result, len }; }, @@ -1294,6 +1334,282 @@ export class WasmModuleLoader { module._free(countPtr); return { result, data }; }, + + // start/count/stride → i32 / 4 bytes per element (size_t and ptrdiff_t are 4 bytes in wasm32) + + nc_get_vars_schar: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count); + const dataPtr = module._malloc(safeByteLength(totalLength, 1)); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_schar_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Int8Array(module.HEAP8.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_uchar: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count); + const dataPtr = module._malloc(safeByteLength(totalLength, 1)); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_uchar_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Uint8Array(module.HEAPU8.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_short: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count); + const dataPtr = module._malloc(safeByteLength(totalLength, 2)); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_short_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Int16Array(module.HEAP16.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_ushort: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count); + const dataPtr = module._malloc(safeByteLength(totalLength, 2)); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_ushort_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Uint16Array(module.HEAPU16.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_int: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count); + const dataPtr = module._malloc(safeByteLength(totalLength, 4)); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_int_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Int32Array(module.HEAP32.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_uint: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count); + const dataPtr = module._malloc(safeByteLength(totalLength, 4)); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_uint_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Uint32Array(module.HEAPU32.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_float: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count); + const dataPtr = module._malloc(safeByteLength(totalLength, 4)); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_float_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Float32Array(module.HEAPF32.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_double: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count); + const dataPtr = module._malloc(safeByteLength(totalLength, 8)); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_double_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Float64Array(module.HEAPF64.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_longlong: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count); + const dataPtr = module._malloc(safeByteLength(totalLength, 8)); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_longlong_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new BigInt64Array(module.HEAP64.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_ulonglong: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count); + const dataPtr = module._malloc(safeByteLength(totalLength, 8)); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_ulonglong_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new BigUint64Array(module.HEAPU64.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_string: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count); + // validate allocation size won't overflow WASM32 uint32. + // char* pointers are 4 bytes in wasm32, so elementSize = 4. + const dataPtr = module._malloc(safeByteLength(totalLength, 4)); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_string_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + let data: string[] | undefined; + if (result === NC_CONSTANTS.NC_NOERR) { + data = []; + for (let i = 0; i < totalLength; i++) { + const strPtr = module.getValue(dataPtr + i * 4, '*'); + data.push(module.UTF8ToString(strPtr)); + } + // free the individual strings allocated by NetCDF-C before + // freeing the pointer array. nc_free_string_wrapper handles both. + nc_free_string_wrapper(totalLength, dataPtr); + } else { + module._free(dataPtr); + } + module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_generic: (ncid: number, varid: number, start: number[], count: number[], stride: number[], nctype: number) => { + const elementSize = DATA_TYPE_SIZE[nctype]; + const totalLength = stridedLength(count); + + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + + if (nctype === NC_CONSTANTS.NC_STRING) { + // validate allocation size won't overflow WASM32 uint32. + const dataPtr = module._malloc(safeByteLength(totalLength, 4)); + const result = nc_get_vars_string_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + let data: string[] | undefined; + if (result === NC_CONSTANTS.NC_NOERR) { + data = []; + for (let i = 0; i < totalLength; i++) { + const strPtr = module.getValue(dataPtr + i * 4, '*'); + data.push(module.UTF8ToString(strPtr)); + } + // free the individual strings allocated by NetCDF-C before + // freeing the pointer array. nc_free_string_wrapper handles both. + nc_free_string_wrapper(totalLength, dataPtr); + } else { + module._free(dataPtr); + } + module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + } + + // validate allocation size won't overflow WASM32 uint32. + const dataPtr = module._malloc(safeByteLength(totalLength, elementSize)); + const result = nc_get_vars_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + + let data; + if (result === NC_CONSTANTS.NC_NOERR) { + switch (nctype) { + case NC_CONSTANTS.NC_BYTE: data = new Int8Array (module.HEAP8.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_UBYTE: data = new Uint8Array (module.HEAPU8.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_SHORT: data = new Int16Array (module.HEAP16.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_USHORT: data = new Uint16Array (module.HEAPU16.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_INT: data = new Int32Array (module.HEAP32.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_UINT: data = new Uint32Array (module.HEAPU32.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_FLOAT: data = new Float32Array (module.HEAPF32.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_DOUBLE: data = new Float64Array (module.HEAPF64.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_INT64: data = new BigInt64Array (module.HEAP64.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_UINT64: data = new BigUint64Array(module.HEAPU64.buffer, dataPtr, totalLength).slice(); break; + } + } + + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + nc_get_vars_text: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count); + const dataPtr = module._malloc(safeByteLength(totalLength, 1)); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_text_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? Array.from( + new TextDecoder().decode(module.HEAPU8.subarray(dataPtr, dataPtr + totalLength)), + char => char === '\0' ? '' : char + ) + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, }; } } \ No newline at end of file