diff --git a/docs/next-js/components/loading/LocalNetCDFMeta.tsx b/docs/next-js/components/loading/LocalNetCDFMeta.tsx index 8146c3a..cd9cdcc 100644 --- a/docs/next-js/components/loading/LocalNetCDFMeta.tsx +++ b/docs/next-js/components/loading/LocalNetCDFMeta.tsx @@ -30,6 +30,11 @@ import { } from '@/components/ui/tooltip'; import { Badge } from '@/components/ui/badge'; import { NetCDF4, DataTree, GroupNode } from '@earthyscience/netcdf4-wasm'; +import SliceTesterSection, { + SliceSelectionState, + defaultSelection, + buildSelection, +} from './SliceTester'; const NETCDF_EXT_REGEX = /\.(nc|netcdf|nc3|nc4)$/i; @@ -68,9 +73,62 @@ const LocalNetCDFMeta = () => { const [showVariableMenu, setShowVariableMenu] = useState(false); const [expandedGroups, setExpandedGroups] = useState>(new Set(['/'])); - // --------------------------------------------------------------------------- + // ── Slice tester state ───────────────────────────────────────────────────── + const [expandedSliceTester, setExpandedSliceTester] = useState(false); + const [sliceSelections, setSliceSelections] = useState([]); + const [sliceResult, setSliceResult] = useState(null); + const [sliceError, setSliceError] = useState(null); + const [loadingSlice, setLoadingSlice] = useState(false); + + // Reset slice tester when selected variable changes + useEffect(() => { + setSliceResult(null); + setSliceError(null); + setExpandedSliceTester(false); + if (selectedVariable && variables[selectedVariable]?.info?.shape) { + setSliceSelections( + (variables[selectedVariable].info.shape as any[]).map(() => defaultSelection()) + ); + } else { + setSliceSelections([]); + } + }, [selectedVariable]); // eslint-disable-line react-hooks/exhaustive-deps + + // Init slice selections when info first loads for the selected variable + useEffect(() => { + if ( + selectedVariable && + variables[selectedVariable]?.info?.shape && + sliceSelections.length === 0 + ) { + setSliceSelections( + (variables[selectedVariable].info.shape as any[]).map(() => defaultSelection()) + ); + } + }, [variables, selectedVariable]); // eslint-disable-line react-hooks/exhaustive-deps + + const runSliceTest = useCallback(async () => { + if (!dataset || !selectedVariable) return; + setLoadingSlice(true); + setSliceError(null); + setSliceResult(null); + try { + const info = variables[selectedVariable].info; + const selection = buildSelection(sliceSelections, info.shape); + const data = await dataset.get( + selectedVariable, + selection, + currentGroupPath === '/' ? undefined : currentGroupPath + ); + setSliceResult(data); + } catch (e: any) { + setSliceError(e?.message ?? String(e)); + } finally { + setLoadingSlice(false); + } + }, [dataset, selectedVariable, variables, sliceSelections, currentGroupPath]); + // Helpers wrapped in useCallback - // --------------------------------------------------------------------------- const refreshGroup = useCallback((path: string, dataTree: DataTree) => { setCurrentGroupPath(path); @@ -139,7 +197,6 @@ const LocalNetCDFMeta = () => { // Create start and count arrays const start = new Array(shape.length).fill(0); const count = [...shape]; - // Slice along the first dimension count[0] = Math.min(actualSize, shape[0]); const data = await dataset.getSlicedVariableArray( varName, @@ -186,24 +243,18 @@ const LocalNetCDFMeta = () => { setError('Please enter a URL.'); return false; } - - // Check for common protocols const validProtocols = ['http://', 'https://', 's3://', 'gs://', 'ftp://']; const hasValidProtocol = validProtocols.some(protocol => urlString.toLowerCase().startsWith(protocol) ); - if (!hasValidProtocol) { setError('URL must start with a valid protocol (http://, https://, s3://, gs://, or ftp://)'); return false; } - - // Check if URL ends with NetCDF extension if (!NETCDF_EXT_REGEX.test(urlString)) { setError('URL should point to a NetCDF file (.nc, .netcdf, .nc3, .nc4)'); return false; } - return true; }; @@ -211,24 +262,18 @@ const LocalNetCDFMeta = () => { setError(null); const files = event.target.files; if (!files || files.length === 0) return; - const file = files[0]; - if (!NETCDF_EXT_REGEX.test(file.name)) { setError('Please select a valid NetCDF file.'); return; } - try { setIsLoading(true); - const ds = await NetCDF4.fromBlobLazy(file); const dt = new DataTree(ds); await dt.buildTree(); - setDataset(ds); setTree(dt); - refreshGroup('/', dt); } catch (err) { console.error(err); @@ -240,22 +285,14 @@ const LocalNetCDFMeta = () => { const handleUrlFetch = async () => { setError(null); - - if (!validateUrl(url)) { - return; - } - + if (!validateUrl(url)) return; try { setIsLoading(true); - // const urlTry = await NetCDF4.Dataset("s3://its-live-data/test-space/sample-data/sst.mnmean.nc") - // console.log(urlTry) const ds = await NetCDF4.Dataset(url); const dt = new DataTree(ds); await dt.buildTree(); - setDataset(ds); setTree(dt); - refreshGroup('/', dt); } catch (err) { console.error(err); @@ -272,12 +309,9 @@ const LocalNetCDFMeta = () => { const selectGroup = (path: string) => { if (!tree) return; refreshGroup(path, tree); - // Open the group menu and close the variable menu setShowGroupMenu(true); setShowVariableMenu(false); - // Expand the selected group and all its parents const newExpanded = new Set(expandedGroups); - // Add all parent paths const parts = path.split('/').filter(Boolean); let currentPath = '/'; newExpanded.add(currentPath); @@ -299,8 +333,6 @@ const LocalNetCDFMeta = () => { setShowSearchResults(false); return; } - - // Show partial matches while typing if (tree) { const results = tree.searchVariables(value); setSearchResults(results); @@ -414,10 +446,7 @@ const LocalNetCDFMeta = () => { size="sm" onClick={() => { setShowVariableMenu(!showVariableMenu); - // Close group menu when opening variable menu - if (!showVariableMenu) { - setShowGroupMenu(false); - } + if (!showVariableMenu) setShowGroupMenu(false); }} className="cursor-pointer flex-shrink-0" > @@ -435,8 +464,7 @@ const LocalNetCDFMeta = () => {

Variables in current group

- ) - } + )} {/* Search */}
@@ -476,12 +504,8 @@ const LocalNetCDFMeta = () => {
-
- {result.name} -
-
- {result.groupPath} -
+
{result.name}
+
{result.groupPath}
@@ -496,58 +520,50 @@ const LocalNetCDFMeta = () => { )}
+ {/* Group Summary */}
- {groupSummary && ( -
- - {groupSummary.variableCount} variables - - - {groupSummary.dimensionCount} dimensions - - - {groupSummary.attributeCount} attributes - - {groupSummary.subgroupCount > 0 && ( - - {groupSummary.subgroupCount} subgroups + {groupSummary && ( +
+ {groupSummary.variableCount} variables + {groupSummary.dimensionCount} dimensions + {groupSummary.attributeCount} attributes + {groupSummary.subgroupCount > 0 && ( + {groupSummary.subgroupCount} subgroups + )} +
+ )} + {/* Breadcrumbs */} +
+
+ {breadcrumbs.map((crumb, idx) => ( + + + {idx < breadcrumbs.length - 1 && ( + + )} + + ))} +
+ {selectedVariable && ( + + + {selectedVariable} )}
- )} - {/* Breadcrumbs */} -
-
- {breadcrumbs.map((crumb, idx) => ( - - - {idx < breadcrumbs.length - 1 && ( - - )} - - ))}
- - {selectedVariable && ( - - - {selectedVariable} - - )}
-
-
{/* Group Menu (Expandable) */} {showGroupMenu && ( @@ -557,18 +573,13 @@ const LocalNetCDFMeta = () => { const toggleGroup = (path: string) => { const newExpanded = new Set(expandedGroups); - if (newExpanded.has(path)) { - newExpanded.delete(path); - } else { - newExpanded.add(path); - } + if (newExpanded.has(path)) newExpanded.delete(path); + else newExpanded.add(path); setExpandedGroups(newExpanded); }; const handleVariableClick = (varName: string, groupPath: string) => { - if (currentGroupPath !== groupPath) { - selectGroup(groupPath); - } + if (currentGroupPath !== groupPath) selectGroup(groupPath); setPendingVariableLoad({ name: varName, groupPath }); setShowGroupMenu(false); }; @@ -583,7 +594,6 @@ const LocalNetCDFMeta = () => { return (
- {/* Group button - now just expands/collapses */}
- {/* Variables for this group (when expanded) */} {isExpanded && varNames.length > 0 && (
{varNames.map(name => ( @@ -640,21 +640,17 @@ const LocalNetCDFMeta = () => {
)} - {/* Child groups (when expanded) */} {isExpanded && hasChildren && ( -
- {node.children.map(child => renderGroupItem(child, level + 1))} -
+
{node.children.map(child => renderGroupItem(child, level + 1))}
)}
); }; - // Root group handling const rootVars = tree.getAllVariables('/'); const rootVarNames = Object.keys(rootVars); const isRootExpanded = expandedGroups.has('/'); - const ROOT_VARS_KEY = '/__root_vars__'; // Special key for root variables + const ROOT_VARS_KEY = '/__root_vars__'; const isRootVarsExpanded = expandedGroups.has(ROOT_VARS_KEY); return ( @@ -667,13 +663,10 @@ const LocalNetCDFMeta = () => { }`} >
- {/* Chevron for root */} {(groupTree.children.length > 0 || rootVarNames.length > 0) ? ( - isRootExpanded ? ( - - ) : ( - - ) + isRootExpanded + ? + : ) : (
)} @@ -688,22 +681,18 @@ const LocalNetCDFMeta = () => {
- {/* Root variables section (when root is expanded) */} {isRootExpanded && rootVarNames.length > 0 && (
- - {/* Root variables list (when variables section is expanded) */} {isRootVarsExpanded && (
{rootVarNames.map(name => ( @@ -721,7 +710,6 @@ const LocalNetCDFMeta = () => {
)} - {/* Root child groups (when expanded) */} {isRootExpanded && groupTree.children.map(child => renderGroupItem(child, 0))} ); @@ -766,133 +754,66 @@ const LocalNetCDFMeta = () => { className="w-full flex items-center justify-between p-3 hover:bg-accent/50 transition-colors cursor-pointer" > Variable Info - {expandedVariableInfo ? ( - - ) : ( - - )} + {expandedVariableInfo + ? + : + } {expandedVariableInfo && (
- {/* Name */} -
- name: - - {variables[selectedVariable].info.name} - -
- - {/* Data Type */} -
- dtype: - - {variables[selectedVariable].info.dtype} - -
- - {/* NC Type */} - {variables[selectedVariable].info.nctype !== undefined && ( -
- nctype: - - {variables[selectedVariable].info.nctype} - + {[ + { label: 'name', value: variables[selectedVariable].info.name }, + { label: 'dtype', value: variables[selectedVariable].info.dtype }, + ...(variables[selectedVariable].info.nctype !== undefined + ? [{ label: 'nctype', value: variables[selectedVariable].info.nctype }] + : []), + { label: 'shape', value: `[${variables[selectedVariable].info.shape.join(', ')}]` }, + { label: 'dimensions', value: `[${variables[selectedVariable].info.dimensions?.join(', ') || 'N/A'}]` }, + { label: 'size', value: variables[selectedVariable].info.size.toLocaleString() }, + ...(variables[selectedVariable].info.totalSize !== undefined + ? [{ label: 'totalSize', value: `${variables[selectedVariable].info.totalSize.toLocaleString()} bytes` }] + : []), + ...(variables[selectedVariable].info.chunked !== undefined + ? [{ label: 'chunked', value: String(variables[selectedVariable].info.chunked) }] + : []), + ...(variables[selectedVariable].info.chunks + ? [{ label: 'chunks', value: `[${variables[selectedVariable].info.chunks.join(', ')}]` }] + : []), + ...(variables[selectedVariable].info.chunkSize !== undefined + ? [{ label: 'chunkSize', value: `${variables[selectedVariable].info.chunkSize.toLocaleString()} bytes` }] + : []), + ].map(({ label, value }) => ( +
+ {label}: + {value}
- )} - - {/* Shape */} -
- shape: - - [{variables[selectedVariable].info.shape.join(', ')}] - -
- - {/* Dimensions */} -
- dimensions: - - [{variables[selectedVariable].info.dimensions?.join(', ') || 'N/A'}] - -
- - {/* Size */} -
- size: - - {variables[selectedVariable].info.size.toLocaleString()} - -
- - {/* Total Size */} - {variables[selectedVariable].info.totalSize !== undefined && ( -
- totalSize: - - {variables[selectedVariable].info.totalSize.toLocaleString()} bytes - -
- )} - - {/* Chunked */} - {variables[selectedVariable].info.chunked !== undefined && ( -
- chunked: - - {String(variables[selectedVariable].info.chunked)} - -
- )} - - {/* Chunks */} - {variables[selectedVariable].info.chunks && ( -
- chunks: - - [{variables[selectedVariable].info.chunks.join(', ')}] - -
- )} - - {/* Chunk Size */} - {variables[selectedVariable].info.chunkSize !== undefined && ( -
- chunkSize: - - {variables[selectedVariable].info.chunkSize.toLocaleString()} bytes - -
- )} + ))}
)}
{/* Collapsible Enum Dictionary */} {variables[selectedVariable].info.enum && - Object.keys(variables[selectedVariable].info.enum).length > 0 && ( + Object.keys(variables[selectedVariable].info.enum).length > 0 && (
- {expandedEnumDict && (
@@ -900,23 +821,18 @@ const LocalNetCDFMeta = () => { .sort(([a], [b]) => Number(a) - Number(b)) .map(([value, label]) => ( - - {value}: - - - {String(label)} - + {value}: + {String(label)} ))}
- {variables[selectedVariable].info.enumType && (
Base Type: {variables[selectedVariable].info.dtype_base || - `NC_TYPE_${variables[selectedVariable].info.enumType.baseType}`} + `NC_TYPE_${variables[selectedVariable].info.enumType.baseType}`}
@@ -937,24 +853,19 @@ const LocalNetCDFMeta = () => { Variable Attributes ({Object.keys(variables[selectedVariable].info.attributes).length}) - {expandedVariableAttrs ? ( - - ) : ( - - )} + {expandedVariableAttrs + ? + : + } - {expandedVariableAttrs && (
- {/*
*/} {Object.entries(variables[selectedVariable].info.attributes).map(([k, v]) => (
{k}: {typeof v === 'object' - ? JSON.stringify(v, (_k, val) => - typeof val === 'bigint' ? Number(val) : val - ) + ? JSON.stringify(v, (_k, val) => typeof val === 'bigint' ? Number(val) : val) : String(v)}
@@ -965,17 +876,12 @@ const LocalNetCDFMeta = () => { )} {/* Load data controls */} - {/* variables[selectedVariable].info.dtype !== 'str' */} {true && (
- {/* Slice controls - hide for S1 and char */} - {/* !['S1', 'char'].includes(variables[selectedVariable].info.dtype) */} {!['S1', 'char'].includes(variables[selectedVariable].info.dtype) && ( <>
- + {
)} - {/* Show only Load All for S1 and char */} {['S1', 'char'].includes(variables[selectedVariable].info.dtype) && (
)} + + {/* ── Slice & Index Tester ── */} +
)} @@ -1060,31 +978,25 @@ const LocalNetCDFMeta = () => { {/* Collapsible Dimensions */} {Object.keys(dimensions).length > 0 && ( - - {expandedDimensions && (
{Object.entries(dimensions).map(([name, dim]: [string, any]) => (
- {/*
*/} {name}: {dim.size || dim.len || dim.length || 'unlimited'} @@ -1100,25 +1012,20 @@ const LocalNetCDFMeta = () => { {/* Collapsible Attributes */} {Object.keys(attributes).length > 0 && ( - - {expandedAttributes && (
@@ -1127,9 +1034,7 @@ const LocalNetCDFMeta = () => { {k}: {typeof v === 'object' - ? JSON.stringify(v, (_k, val) => - typeof val === 'bigint' ? Number(val) : val - ) + ? JSON.stringify(v, (_k, val) => typeof val === 'bigint' ? Number(val) : val) : String(v)}
diff --git a/docs/next-js/components/loading/SliceTester.tsx b/docs/next-js/components/loading/SliceTester.tsx new file mode 100644 index 0000000..6e70f0b --- /dev/null +++ b/docs/next-js/components/loading/SliceTester.tsx @@ -0,0 +1,271 @@ +'use client'; +import React from 'react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Terminal, ChevronRight, ChevronDown } from 'lucide-react'; +import { slice as ncSlice } from '@earthyscience/netcdf4-wasm'; + +// 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' }; +} + +// buildSelection — converts UI state → DimSelection[] for dataset.get() + +export function buildSelection( + sels: SliceSelectionState[], + shape: Array +): Array> { + + return sels.map((s, i) => { + const dimSize = Number(shape[i]); + + // ALL + if (s.mode === 'all') { + return ncSlice(0, dimSize, 1); + } + + // SCALAR + if (s.mode === 'scalar') { + let idx = parseInt(s.scalar); + + if (Number.isNaN(idx)) idx = 0; + + // normalize negative indices + if (idx < 0) idx = dimSize + idx; + + if (idx < 0 || idx >= dimSize) { + throw new Error(`index ${idx} out of bounds for dim ${i} size ${dimSize}`); + } + + return idx; + } + + // SLICE + let start = s.start !== '' ? parseInt(s.start) : 0; + let stop = s.stop !== '' ? parseInt(s.stop) : dimSize; + let step = s.step !== '' ? parseInt(s.step) : 1; + + if (Number.isNaN(start)) start = 0; + if (Number.isNaN(stop)) stop = dimSize; + if (Number.isNaN(step)) step = 1; + + // normalize negatives + if (start < 0) start = dimSize + start; + if (stop < 0) stop = dimSize + stop; + + return ncSlice(start, stop, step); + }); +} + +// SliceTesterSection component + +interface SliceTesterSectionProps { + info: any; + sliceSelections: SliceSelectionState[]; + setSliceSelections: React.Dispatch>; + expandedSliceTester: boolean; + setExpandedSliceTester: (v: boolean) => void; + sliceResult: any; + sliceError: string | null; + loadingSlice: boolean; + onRun: () => void; +} + +const SliceTesterSection: React.FC = ({ + info, + sliceSelections, + setSliceSelections, + expandedSliceTester, + setExpandedSliceTester, + sliceResult, + sliceError, + loadingSlice, + onRun, +}) => { + if (!info?.shape || info.shape.length === 0) return null; + + // Coerce shape to plain numbers — nc_inq_dimlen uses i64 so values may be BigInt + const shape: number[] = (info.shape as any[]).map(Number); + + const updateSel = (i: number, patch: Partial) => + setSliceSelections(prev => prev.map((s, idx) => idx === i ? { ...s, ...patch } : s)); + + const resultPreview = sliceResult + ? (() => { + const arr = Array.isArray(sliceResult) ? sliceResult : Array.from(sliceResult as any); + const items = (arr as any[]).slice(0, 30).map((v: any) => + typeof v === 'number' ? v.toFixed(4) : String(v) + ); + const suffix = arr.length > 30 ? `, … (${arr.length} total)` : ''; + return `[${items.join(', ')}${suffix}]`; + })() + : null; + + const selectionPreview = sliceSelections.map((s, i) => { + if (s.mode === 'all') return 'null'; + if (s.mode === 'scalar') return s.scalar || '0'; + const parts: string[] = [s.start || '0', s.stop || String(shape[i])]; + if (s.step && s.step !== '1') parts.push(s.step); + return `slice(${parts.join(', ')})`; + }).join(', '); + + return ( +
+ {/* Header */} + + + {expandedSliceTester && ( +
+ + {/* Dimension rows */} +
+ {shape.map((dimSize: number, i: number) => { + const dimName = info.dimensions?.[i] ?? `dim_${i}`; + const sel = sliceSelections[i] ?? defaultSelection(); + + return ( +
+ {/* Label + mode tabs */} +
+ + {dimName} + [{dimSize}] + +
+ {(['all', 'scalar', 'slice'] as SelectionMode[]).map(m => ( + + ))} +
+
+ + {/* Scalar input */} + {sel.mode === 'scalar' && ( +
+ index + updateSel(i, { scalar: e.target.value })} + className="h-7 text-xs w-28 font-mono" + placeholder="0" + /> + + (0 … {dimSize - 1}, or negative) + +
+ )} + + {/* Slice inputs */} + {sel.mode === 'slice' && ( +
+ {[ + { label: 'start', key: 'start' as const, placeholder: '0' }, + { label: 'stop', key: 'stop' as const, placeholder: String(dimSize) }, + { label: 'step', key: 'step' as const, placeholder: '1' }, + ].map(({ label, key, placeholder }) => ( +
+ {label} + updateSel(i, { [key]: e.target.value })} + className="h-7 text-xs w-20 font-mono" + placeholder={placeholder} + /> +
+ ))} +
+ )} +
+ ); + })} +
+ + {/* Selection preview */} +
+ get("{info.name}", [{selectionPreview}]) +
+ + {/* Run + result count */} +
+ + {sliceResult && ( + + {(() => { + const arr = Array.isArray(sliceResult) ? sliceResult : Array.from(sliceResult as any); + return `${arr.length} elements`; + })()} + + )} +
+ + {/* Error */} + {sliceError && ( + + + {sliceError} + + )} + + {/* Result preview */} + {resultPreview && ( +
+              {resultPreview}
+            
+ )} +
+ )} +
+ ); +}; + +export default SliceTesterSection; \ No newline at end of file diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index b199f0f..a99ad75 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -963,6 +963,91 @@ 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_*) +// Add these alongside your existing nc_get_vara_*_wrapper functions +// ========================= + +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 your existing nc_get_vara_wrapper +// Uses nc_get_vars which accepts void* output and nc_type for dispatch +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) { + // nc_get_vars_* requires knowing the type — use nc_get_vara with the generic + // approach by getting the variable type first and dispatching + nc_type xtype; + int stat = nc_inq_vartype(ncid, varid, &xtype); + if (stat != NC_NOERR) return stat; + + switch (xtype) { + case NC_BYTE: return nc_get_vars_schar(ncid, varid, start, count, stride, (signed char*)value); + case NC_UBYTE: return nc_get_vars_uchar(ncid, varid, start, count, stride, (unsigned char*)value); + case NC_SHORT: return nc_get_vars_short(ncid, varid, start, count, stride, (short*)value); + case NC_USHORT: return nc_get_vars_ushort(ncid, varid, start, count, stride, (unsigned short*)value); + case NC_INT: return nc_get_vars_int(ncid, varid, start, count, stride, (int*)value); + case NC_UINT: return nc_get_vars_uint(ncid, varid, start, count, stride, (unsigned int*)value); + case NC_FLOAT: return nc_get_vars_float(ncid, varid, start, count, stride, (float*)value); + case NC_DOUBLE: return nc_get_vars_double(ncid, varid, start, count, stride, (double*)value); + case NC_INT64: return nc_get_vars_longlong(ncid, varid, start, count, stride, (long long*)value); + case NC_UINT64: return nc_get_vars_ulonglong(ncid, varid, start, count, stride, (unsigned long long*)value); + default: return NC_EBADTYPE; + } +} + // ========================= // Data Writing // ========================= diff --git a/src/index.ts b/src/index.ts index 7920845..6e5bf02 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 } 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 1ebf71d..3eaf841 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,201 @@ 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 — use workingNcid throughout + 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 — resolveDim coerces BigInt internally + 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 => Math.abs(d.step)); + + const useStride = stride.some(s => s !== 1); + const hasNegativeStep = resolved.some(d => d.step < 0); + + // Resolve variable type — use workingNcid so enum lookup is in the right group + const { enumCtx } = resolveVariableType(module, workingNcid, varid); + const arrayType = enumCtx.baseType; + + let arrayData: { result: number; data?: any }; + + // ── Read data ───────────────────────────────────────────────────────────── + // All reads go directly through workingNcid + varid — no group re-resolution. + + if (enumCtx.isEnum) { + if (useStride) { + arrayData = module.nc_get_vars_generic(workingNcid, varid, start, count, stride, arrayType); + } else { + arrayData = module.nc_get_vara_generic(workingNcid, varid, start, count, arrayType); + } + } else if (useStride) { + type VarsArgs = [number, number, number[], number[], number[]]; + type VarsResult = { result: number; data?: any }; + + const stridedReaders: Record VarsResult> = { + [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 = stridedReaders[arrayType]; + if (!reader) { + console.warn(`Unknown NetCDF type ${arrayType} for strided read, falling back to double`); + arrayData = module.nc_get_vars_double(workingNcid, varid, start, count, stride); + } else { + arrayData = reader(workingNcid, varid, start, count, stride); + } + } else { + type VaraArgs = [number, number, number[], number[]]; + type VaraResult = { result: number; data?: any }; + + const readers: Record VaraResult> = { + [NC_CONSTANTS.NC_BYTE]: (...args) => module.nc_get_vara_schar(...args), + [NC_CONSTANTS.NC_UBYTE]: (...args) => module.nc_get_vara_uchar(...args), + [NC_CONSTANTS.NC_SHORT]: (...args) => module.nc_get_vara_short(...args), + [NC_CONSTANTS.NC_USHORT]: (...args) => module.nc_get_vara_ushort(...args), + [NC_CONSTANTS.NC_INT]: (...args) => module.nc_get_vara_int(...args), + [NC_CONSTANTS.NC_UINT]: (...args) => module.nc_get_vara_uint(...args), + [NC_CONSTANTS.NC_FLOAT]: (...args) => module.nc_get_vara_float(...args), + [NC_CONSTANTS.NC_DOUBLE]: (...args) => module.nc_get_vara_double(...args), + [NC_CONSTANTS.NC_INT64]: (...args) => module.nc_get_vara_longlong(...args), + [NC_CONSTANTS.NC_LONGLONG]: (...args) => module.nc_get_vara_longlong(...args), + [NC_CONSTANTS.NC_UINT64]: (...args) => module.nc_get_vara_ulonglong(...args), + [NC_CONSTANTS.NC_ULONGLONG]: (...args) => module.nc_get_vara_ulonglong(...args), + [NC_CONSTANTS.NC_STRING]: (...args) => module.nc_get_vara_string(...args), + }; + + const reader = readers[arrayType]; + if (!reader) { + console.warn(`Unknown NetCDF type ${arrayType}, falling back to double`); + arrayData = module.nc_get_vara_double(workingNcid, varid, start, count); + } else { + arrayData = reader(workingNcid, varid, start, count); + } + } + + 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_vara/vars returned no data"); + } + + let result: AnyTypedArray = arrayData.data; + + if (enumCtx.isEnum && options?.convertEnumsToNames && enumCtx.enumDict) { + result = convertEnumValuesToNames(result, enumCtx.enumDict); + } + + // Reverse dimensions that had a negative step — cheap, operates on already-small output + if (hasNegativeStep) { + result = applyNegativeStepReversal(result, resolved); + } + + return result; +} + +// applyNegativeStepReversal + +function applyNegativeStepReversal( + data: T, + resolved: ResolvedDim[] +): T { + const ndim = resolved.length; + const outCount = resolved.map(d => + d.collapsed ? 1 : Math.ceil(d.count / Math.abs(d.step)) + ); + const totalOut = outCount.reduce((a, b) => a * b, 1); + + const outStrides = new Array(ndim); + outStrides[ndim - 1] = 1; + for (let i = ndim - 2; i >= 0; i--) { + outStrides[i] = outStrides[i + 1] * outCount[i + 1]; + } + + const out: AnyTypedArray = Array.isArray(data) + ? new Array(totalOut) + : new (data.constructor as any)(totalOut); + + for (let outIdx = 0; outIdx < totalOut; outIdx++) { + let rem = outIdx; + let srcIdx = 0; + for (let d = 0; d < ndim; d++) { + const logI = Math.floor(rem / outStrides[d]); + rem -= logI * outStrides[d]; + const srcI = resolved[d].step < 0 ? outCount[d] - 1 - logI : logI; + srcIdx += srcI * outStrides[d]; + } + (out as any)[outIdx] = (data as any)[srcIdx]; + } + + return out as T; +} + //---- 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..a8d5a3e 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,62 @@ export class NetCDF4 extends Group { } } + /** + * slicing and indexing convenience method. + * + * Each element of `selection` corresponds to one dimension of the variable: + * - `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; step may be negative + * + * Strided reads use nc_get_vars_* under the hood. + * Negative step reads forward then reverses the affected dimension. + * 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 arr.get("temperature", [slice(0, 10), null, 0]); + * + * @example + * // Every other element along time: + * const data = await arr.get("temperature", [slice(0, 100, 2), null, null]); + * + * @example + * // Reversed time axis: + * const data = await arr.get("temperature", [slice(null, null, -1), 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..4db996f --- /dev/null +++ b/src/slice.ts @@ -0,0 +1,94 @@ +/** + * Represents a dimension slice. + * Null fields are resolved against the actual dimension size at read time. + */ +export class Slice { + constructor( + public readonly start: number | null, + public readonly stop: number | null, + public readonly step: number | null + ) {} +} + +/** + * slice(stop) + * slice(start, stop) + * slice(start, stop, step) + * slice(null) — equivalent to null (all elements) + */ +export function slice(stop: number | null): Slice; +export function slice(start: number | null, stop: number | null): Slice; +export function slice(start: number | null, stop: number | null, step: number | null): Slice; +export function slice( + startOrStop: number | null, + stop?: number | null, + step?: number | null +): Slice { + if (stop === undefined) { + return new Slice(null, startOrStop, null); + } + return new Slice(startOrStop, stop ?? null, step ?? null); +} + +/** A single dimension selection: null = all, number = scalar index, Slice = range */ +export type DimSelection = 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 (always the lower bound, even for negative step) */ + start: number; + /** Number of elements to read from NetCDF (contiguous span, always positive) */ + count: number; + /** Step. Positive = forward, negative = reversed output. 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. + */ +export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): ResolvedDim { + // Coerce up front — nc_inq_dimlen returns i64 so dimSize may arrive as BigInt + const dimSize = Number(dimSizeRaw); + + // null or empty Slice → full dimension + if ( + sel === null || + (sel instanceof Slice && sel.start === null && sel.stop === null && sel.step === null) + ) { + return { start: 0, count: dimSize, step: 1, collapsed: false }; + } + + // Scalar index → collapsed dimension + 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 + const step = sel.step ?? 1; + if (step === 0) throw new Error("Slice step cannot be zero"); + + if (step > 0) { + 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)); + const count = Math.max(0, stop - start); + return { start, count, step, collapsed: false }; + } else { + // Negative step: read [stop+1 .. start] forward from NetCDF, reverse client-side + const rawStart = sel.start ?? dimSize - 1; + const rawStop = sel.stop ?? -1; + const start = Math.max(0, Math.min(rawStart < 0 ? dimSize + rawStart : rawStart, dimSize - 1)); + const stop = Math.max(-1, Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize - 1)); + const count = Math.max(0, start - stop); + return { start: stop + 1, count, step, collapsed: false }; + } +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 7379e3b..795d81d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -119,6 +119,22 @@ 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 | BigInt64Array | BigUint64Array }; + // 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 c7f3e1f..f501512 100644 --- a/src/wasm-module.ts +++ b/src/wasm-module.ts @@ -7,6 +7,10 @@ const NC_MAX_NAME = 256; const NC_MAX_DIMS = 1024; const NC_MAX_VARS = 8192; +function stridedLength(count: number[], stride: number[]): number { + return count.reduce((acc, c, i) => acc * Math.ceil(c / stride[i]), 1); +} + export class WasmModuleLoader { static async loadModule(options: NetCDF4WasmOptions = {}): Promise { try { @@ -130,6 +134,21 @@ 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']); + // Group inquiry wrappers const nc_inq_grps_wrapper = module.cwrap('nc_inq_grps_wrapper', 'number', ['number', 'number', 'number']); @@ -1294,6 +1313,230 @@ export class WasmModuleLoader { module._free(countPtr); return { result, data }; }, + + // start/count/stride → i64 / 8 bytes per element (matching nc_get_vara_double etc.) + + nc_get_vars_schar: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * 1); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + 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, stride); + const dataPtr = module._malloc(totalLength * 1); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + 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, stride); + const dataPtr = module._malloc(totalLength * 2); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + 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, stride); + const dataPtr = module._malloc(totalLength * 2); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + 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, stride); + const dataPtr = module._malloc(totalLength * 4); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + 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, stride); + const dataPtr = module._malloc(totalLength * 4); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + 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, stride); + const dataPtr = module._malloc(totalLength * 4); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + 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, stride); + const dataPtr = module._malloc(totalLength * 8); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + 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, stride); + const dataPtr = module._malloc(totalLength * 8); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + 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, stride); + const dataPtr = module._malloc(totalLength * 8); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + 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, stride); + const dataPtr = module._malloc(totalLength * 4); // char* pointers (4 bytes in wasm32) + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + 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)); + } + } + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + // Generic strided reader — mirrors nc_get_vara_generic but with stride. + // Uses i32 for start/count/stride to match nc_get_vara_generic's convention, should it be i64? + 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, stride); + const dataPtr = module._malloc(totalLength * elementSize); + 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_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_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 }; + }, }; } } \ No newline at end of file