xm-netcdf-loader 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/component/colorLegend.d.ts +25 -0
- package/dist/component/loadFile.d.ts +43 -0
- package/dist/component/loadonMap.d.ts +46 -0
- package/dist/component/statusInfo.d.ts +41 -0
- package/dist/composables/useGridLabels.d.ts +13 -0
- package/dist/composables/useLeafletMap.d.ts +56 -0
- package/dist/composables/useMapRendering.d.ts +38 -0
- package/dist/composables/useNetCdf.d.ts +49 -0
- package/dist/index.d.ts +9 -0
- package/dist/netcdf4-wasm/CONTRIBUTING.md +160 -0
- package/dist/netcdf4-wasm/LICENSE +22 -0
- package/dist/netcdf4-wasm/README.md +81 -0
- package/dist/netcdf4-wasm/dist/constants.d.ts +158 -0
- package/dist/netcdf4-wasm/dist/constants.d.ts.map +1 -0
- package/dist/netcdf4-wasm/dist/constants.js +249 -0
- package/dist/netcdf4-wasm/dist/constants.js.map +1 -0
- package/dist/netcdf4-wasm/dist/dimension.d.ts +9 -0
- package/dist/netcdf4-wasm/dist/dimension.d.ts.map +1 -0
- package/dist/netcdf4-wasm/dist/dimension.js +19 -0
- package/dist/netcdf4-wasm/dist/dimension.js.map +1 -0
- package/dist/netcdf4-wasm/dist/group.d.ts +35 -0
- package/dist/netcdf4-wasm/dist/group.d.ts.map +1 -0
- package/dist/netcdf4-wasm/dist/group.js +189 -0
- package/dist/netcdf4-wasm/dist/group.js.map +1 -0
- package/dist/netcdf4-wasm/dist/index.d.ts +17 -0
- package/dist/netcdf4-wasm/dist/index.d.ts.map +1 -0
- package/dist/netcdf4-wasm/dist/index.js +49 -0
- package/dist/netcdf4-wasm/dist/index.js.map +1 -0
- package/dist/netcdf4-wasm/dist/netcdf-getters.d.ts +120 -0
- package/dist/netcdf4-wasm/dist/netcdf-getters.d.ts.map +1 -0
- package/dist/netcdf4-wasm/dist/netcdf-getters.js +816 -0
- package/dist/netcdf4-wasm/dist/netcdf-getters.js.map +1 -0
- package/dist/netcdf4-wasm/dist/netcdf-worker.d.ts +2 -0
- package/dist/netcdf4-wasm/dist/netcdf-worker.d.ts.map +1 -0
- package/dist/netcdf4-wasm/dist/netcdf-worker.js +154 -0
- package/dist/netcdf4-wasm/dist/netcdf-worker.js.map +1 -0
- package/dist/netcdf4-wasm/dist/netcdf4-wasm.js +2 -0
- package/dist/netcdf4-wasm/dist/netcdf4-wasm.wasm +0 -0
- package/dist/netcdf4-wasm/dist/netcdf4.d.ts +218 -0
- package/dist/netcdf4-wasm/dist/netcdf4.d.ts.map +1 -0
- package/dist/netcdf4-wasm/dist/netcdf4.js +1049 -0
- package/dist/netcdf4-wasm/dist/netcdf4.js.map +1 -0
- package/dist/netcdf4-wasm/dist/slice.d.ts +57 -0
- package/dist/netcdf4-wasm/dist/slice.d.ts.map +1 -0
- package/dist/netcdf4-wasm/dist/slice.js +60 -0
- package/dist/netcdf4-wasm/dist/slice.js.map +1 -0
- package/dist/netcdf4-wasm/dist/test-setup.d.ts +13 -0
- package/dist/netcdf4-wasm/dist/test-setup.d.ts.map +1 -0
- package/dist/netcdf4-wasm/dist/test-setup.js +78 -0
- package/dist/netcdf4-wasm/dist/test-setup.js.map +1 -0
- package/dist/netcdf4-wasm/dist/types.d.ts +444 -0
- package/dist/netcdf4-wasm/dist/types.d.ts.map +1 -0
- package/dist/netcdf4-wasm/dist/types.js +3 -0
- package/dist/netcdf4-wasm/dist/types.js.map +1 -0
- package/dist/netcdf4-wasm/dist/variable.d.ts +36 -0
- package/dist/netcdf4-wasm/dist/variable.d.ts.map +1 -0
- package/dist/netcdf4-wasm/dist/variable.js +152 -0
- package/dist/netcdf4-wasm/dist/variable.js.map +1 -0
- package/dist/netcdf4-wasm/dist/wasm-module.d.ts +6 -0
- package/dist/netcdf4-wasm/dist/wasm-module.d.ts.map +1 -0
- package/dist/netcdf4-wasm/dist/wasm-module.js +1502 -0
- package/dist/netcdf4-wasm/dist/wasm-module.js.map +1 -0
- package/dist/netcdf4-wasm/package.json +78 -0
- package/dist/netcdf4-wasm.wasm +0 -0
- package/dist/types/colorsJson.d.ts +36 -0
- package/dist/types/netcdf.d.ts +70 -0
- package/dist/utils/color.d.ts +277 -0
- package/dist/utils/colorScales.d.ts +28 -0
- package/dist/utils/colorsJsonService.d.ts +24 -0
- package/dist/utils/dataProcessing.d.ts +64 -0
- package/dist/utils/errorHandling.d.ts +69 -0
- package/dist/utils/imageUtils.d.ts +29 -0
- package/dist/utils/performance.d.ts +75 -0
- package/dist/wasm/constants.d.ts +158 -0
- package/dist/wasm/constants.d.ts.map +1 -0
- package/dist/wasm/constants.js +249 -0
- package/dist/wasm/constants.js.map +1 -0
- package/dist/wasm/dimension.d.ts +9 -0
- package/dist/wasm/dimension.d.ts.map +1 -0
- package/dist/wasm/dimension.js +19 -0
- package/dist/wasm/dimension.js.map +1 -0
- package/dist/wasm/group.d.ts +35 -0
- package/dist/wasm/group.d.ts.map +1 -0
- package/dist/wasm/group.js +189 -0
- package/dist/wasm/group.js.map +1 -0
- package/dist/wasm/index.d.ts +17 -0
- package/dist/wasm/index.d.ts.map +1 -0
- package/dist/wasm/index.js +49 -0
- package/dist/wasm/index.js.map +1 -0
- package/dist/wasm/netcdf-getters.d.ts +120 -0
- package/dist/wasm/netcdf-getters.d.ts.map +1 -0
- package/dist/wasm/netcdf-getters.js +816 -0
- package/dist/wasm/netcdf-getters.js.map +1 -0
- package/dist/wasm/netcdf-worker.d.ts +2 -0
- package/dist/wasm/netcdf-worker.d.ts.map +1 -0
- package/dist/wasm/netcdf-worker.js +154 -0
- package/dist/wasm/netcdf-worker.js.map +1 -0
- package/dist/wasm/netcdf4-wasm.js +2 -0
- package/dist/wasm/netcdf4-wasm.wasm +0 -0
- package/dist/wasm/netcdf4.d.ts +218 -0
- package/dist/wasm/netcdf4.d.ts.map +1 -0
- package/dist/wasm/netcdf4.js +1049 -0
- package/dist/wasm/netcdf4.js.map +1 -0
- package/dist/wasm/slice.d.ts +57 -0
- package/dist/wasm/slice.d.ts.map +1 -0
- package/dist/wasm/slice.js +60 -0
- package/dist/wasm/slice.js.map +1 -0
- package/dist/wasm/test-setup.d.ts +13 -0
- package/dist/wasm/test-setup.d.ts.map +1 -0
- package/dist/wasm/test-setup.js +78 -0
- package/dist/wasm/test-setup.js.map +1 -0
- package/dist/wasm/types.d.ts +444 -0
- package/dist/wasm/types.d.ts.map +1 -0
- package/dist/wasm/types.js +3 -0
- package/dist/wasm/types.js.map +1 -0
- package/dist/wasm/variable.d.ts +36 -0
- package/dist/wasm/variable.d.ts.map +1 -0
- package/dist/wasm/variable.js +152 -0
- package/dist/wasm/variable.js.map +1 -0
- package/dist/wasm/wasm-module.d.ts +6 -0
- package/dist/wasm/wasm-module.d.ts.map +1 -0
- package/dist/wasm/wasm-module.js +1502 -0
- package/dist/wasm/wasm-module.js.map +1 -0
- package/dist/xm-netcdf-loader.cjs.js +2 -0
- package/dist/xm-netcdf-loader.cjs.js.map +1 -0
- package/dist/xm-netcdf-loader.es.js +18532 -0
- package/dist/xm-netcdf-loader.es.js.map +1 -0
- package/dist/xm-netcdf-loader.umd.js +2 -0
- package/dist/xm-netcdf-loader.umd.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
// Main NetCDF4 class implementation
|
|
2
|
+
import { Group } from './group.js';
|
|
3
|
+
import { WasmModuleLoader } from './wasm-module.js';
|
|
4
|
+
import { NC_CONSTANTS } from './constants.js';
|
|
5
|
+
import * as NCGet from './netcdf-getters.js';
|
|
6
|
+
export class NetCDF4 extends Group {
|
|
7
|
+
filename;
|
|
8
|
+
mode;
|
|
9
|
+
options;
|
|
10
|
+
module = null;
|
|
11
|
+
initialized = false;
|
|
12
|
+
ncid = -1;
|
|
13
|
+
_isOpen = false;
|
|
14
|
+
memorySource;
|
|
15
|
+
workerSource;
|
|
16
|
+
worker;
|
|
17
|
+
workerReady;
|
|
18
|
+
constructor(filename, mode = 'r', options = {}) {
|
|
19
|
+
super(null, '', -1);
|
|
20
|
+
this.filename = filename;
|
|
21
|
+
this.mode = mode;
|
|
22
|
+
this.options = options;
|
|
23
|
+
// Set up self-reference for Group methods
|
|
24
|
+
this.netcdf = this;
|
|
25
|
+
}
|
|
26
|
+
async initialize() {
|
|
27
|
+
if (this.initialized)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
if (this.workerSource) {
|
|
31
|
+
// This now handles the WORKERFS mounting
|
|
32
|
+
await this.setupWorker();
|
|
33
|
+
this.initialized = true;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.module = await WasmModuleLoader.loadModule(this.options);
|
|
37
|
+
if (this.memorySource) {
|
|
38
|
+
await this.mountMemoryData();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
this.initialized = true;
|
|
42
|
+
// Automatically open the file if a filename was provided
|
|
43
|
+
if (this.filename && !this.workerSource) {
|
|
44
|
+
await this.open();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
|
49
|
+
this.module = this.createMockModule();
|
|
50
|
+
this.initialized = true;
|
|
51
|
+
if (this.filename)
|
|
52
|
+
await this.open();
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Python-like factory method
|
|
60
|
+
static async Dataset(filename, mode = 'r', options = {}) {
|
|
61
|
+
const dataset = new NetCDF4(filename, mode, options);
|
|
62
|
+
await dataset.initialize();
|
|
63
|
+
return dataset;
|
|
64
|
+
}
|
|
65
|
+
// Create dataset from Blob
|
|
66
|
+
static async fromBlob(blob, mode = 'r', options = {}) {
|
|
67
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
68
|
+
return NetCDF4.fromArrayBuffer(arrayBuffer, mode, options);
|
|
69
|
+
}
|
|
70
|
+
// Create dataset from ArrayBuffer
|
|
71
|
+
static async fromArrayBuffer(buffer, mode = 'r', options = {}) {
|
|
72
|
+
const data = new Uint8Array(buffer);
|
|
73
|
+
return NetCDF4.fromMemory(data, mode, options);
|
|
74
|
+
}
|
|
75
|
+
// Create dataset from memory data (Uint8Array or ArrayBuffer)
|
|
76
|
+
static async fromMemory(data, mode = 'r', options = {}, filename) {
|
|
77
|
+
if (!data) {
|
|
78
|
+
throw new Error('Data cannot be null or undefined');
|
|
79
|
+
}
|
|
80
|
+
if (!(data instanceof ArrayBuffer) && !(data instanceof Uint8Array)) {
|
|
81
|
+
throw new Error('Data must be ArrayBuffer or Uint8Array');
|
|
82
|
+
}
|
|
83
|
+
const uint8Data = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
84
|
+
const virtualFilename = filename || `/tmp/netcdf_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.nc`;
|
|
85
|
+
const dataset = new NetCDF4(virtualFilename, mode, options);
|
|
86
|
+
dataset.memorySource = {
|
|
87
|
+
data: uint8Data,
|
|
88
|
+
filename: virtualFilename
|
|
89
|
+
};
|
|
90
|
+
await dataset.initialize();
|
|
91
|
+
return dataset;
|
|
92
|
+
}
|
|
93
|
+
// New factory for Blob/File (local, no full preload)
|
|
94
|
+
static async fromBlobLazy(blob, options = {}) {
|
|
95
|
+
// IMPORTANT: Keep this path consistent with the mount logic in the worker
|
|
96
|
+
const mountPoint = '/working';
|
|
97
|
+
const baseName = `netcdf_lazy_${Date.now()}.nc`;
|
|
98
|
+
const fullPath = `${mountPoint}/${baseName}`;
|
|
99
|
+
const dataset = new NetCDF4(fullPath, 'r', options);
|
|
100
|
+
// Store the raw blob. The worker will mount it via WORKERFS
|
|
101
|
+
dataset.workerSource = { blob, filename: fullPath };
|
|
102
|
+
await dataset.initialize();
|
|
103
|
+
// After worker is set up, open the file
|
|
104
|
+
await dataset.open();
|
|
105
|
+
return dataset;
|
|
106
|
+
}
|
|
107
|
+
async open() {
|
|
108
|
+
if (this._isOpen)
|
|
109
|
+
return;
|
|
110
|
+
if (!this.filename || this.filename.trim() === '') {
|
|
111
|
+
throw new Error('No filename specified');
|
|
112
|
+
}
|
|
113
|
+
// Check for valid modes early, before any WASM operations
|
|
114
|
+
const validModes = ['r', 'w', 'w-', 'a', 'r+'];
|
|
115
|
+
if (!validModes.includes(this.mode)) {
|
|
116
|
+
throw new Error(`Unsupported mode: ${this.mode}`);
|
|
117
|
+
}
|
|
118
|
+
// Worker path
|
|
119
|
+
if (this.worker) {
|
|
120
|
+
// Wait for worker to be ready first
|
|
121
|
+
await this.workerReady;
|
|
122
|
+
const modeValue = this.mode === 'r' ? NC_CONSTANTS.NC_NOWRITE : NC_CONSTANTS.NC_WRITE;
|
|
123
|
+
this.ncid = await this.callWorker('open', { path: this.filename, modeValue });
|
|
124
|
+
this.groupId = this.ncid;
|
|
125
|
+
this._isOpen = true;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (this.mode === 'w' || this.mode === 'w-') {
|
|
129
|
+
// Create new file
|
|
130
|
+
let createMode = NC_CONSTANTS.NC_CLOBBER;
|
|
131
|
+
if (this.options.format === 'NETCDF4') {
|
|
132
|
+
createMode |= NC_CONSTANTS.NC_NETCDF4;
|
|
133
|
+
}
|
|
134
|
+
const result = await this.createFile(this.filename, createMode);
|
|
135
|
+
this.ncid = result;
|
|
136
|
+
this.groupId = result;
|
|
137
|
+
}
|
|
138
|
+
else if (this.mode === 'r' || this.mode === 'a' || this.mode === 'r+') {
|
|
139
|
+
// Open existing file
|
|
140
|
+
const modeValue = this.mode === 'r' ? NC_CONSTANTS.NC_NOWRITE : NC_CONSTANTS.NC_WRITE;
|
|
141
|
+
this.ncid = await this.openFile(this.filename, this.mode);
|
|
142
|
+
this.groupId = this.ncid;
|
|
143
|
+
// Load existing data from mock storage if in test mode
|
|
144
|
+
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
|
145
|
+
this.loadMockDimensions();
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
await this.load();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
this._isOpen = true;
|
|
152
|
+
}
|
|
153
|
+
// Property access similar to Python API
|
|
154
|
+
get file_format() {
|
|
155
|
+
return this.options.format || 'NETCDF4';
|
|
156
|
+
}
|
|
157
|
+
get disk_format() {
|
|
158
|
+
return this.file_format;
|
|
159
|
+
}
|
|
160
|
+
get filepath() {
|
|
161
|
+
return this.filename || '';
|
|
162
|
+
}
|
|
163
|
+
get isopen() {
|
|
164
|
+
return this._isOpen;
|
|
165
|
+
}
|
|
166
|
+
// Check if module is initialized
|
|
167
|
+
isInitialized() {
|
|
168
|
+
return this.initialized;
|
|
169
|
+
}
|
|
170
|
+
getModule() {
|
|
171
|
+
if (!this.module) {
|
|
172
|
+
throw new Error('NetCDF4 module not initialized. Call initialize() first.');
|
|
173
|
+
}
|
|
174
|
+
return this.module;
|
|
175
|
+
}
|
|
176
|
+
// Close method
|
|
177
|
+
async close() {
|
|
178
|
+
if (this._isOpen && this.ncid >= 0) {
|
|
179
|
+
await this.closeFile(this.ncid);
|
|
180
|
+
this._isOpen = false;
|
|
181
|
+
this.ncid = -1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Sync method (flush to disk)
|
|
185
|
+
async sync() {
|
|
186
|
+
if (this._isOpen) {
|
|
187
|
+
// TODO: Implement nc_sync when available
|
|
188
|
+
console.warn('sync() not yet implemented');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Context manager support (Python-like)
|
|
192
|
+
async __aenter__() {
|
|
193
|
+
if (!this.initialized) {
|
|
194
|
+
await this.initialize();
|
|
195
|
+
}
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
async __aexit__() {
|
|
199
|
+
await this.close();
|
|
200
|
+
}
|
|
201
|
+
// Low-level NetCDF operations (used by Group methods)
|
|
202
|
+
async openFile(path, mode = 'r') {
|
|
203
|
+
const module = this.getModule();
|
|
204
|
+
const modeValue = mode === 'r' ? NC_CONSTANTS.NC_NOWRITE :
|
|
205
|
+
mode === 'w' ? NC_CONSTANTS.NC_WRITE :
|
|
206
|
+
NC_CONSTANTS.NC_WRITE;
|
|
207
|
+
const result = module.nc_open(path, modeValue);
|
|
208
|
+
if (result.result !== NC_CONSTANTS.NC_NOERR) {
|
|
209
|
+
throw new Error(`Failed to open NetCDF file: ${path} (error: ${result.result})`);
|
|
210
|
+
}
|
|
211
|
+
return result.ncid;
|
|
212
|
+
}
|
|
213
|
+
async createFile(path, mode = NC_CONSTANTS.NC_CLOBBER) {
|
|
214
|
+
const module = this.getModule();
|
|
215
|
+
const result = module.nc_create(path, mode);
|
|
216
|
+
if (result.result !== NC_CONSTANTS.NC_NOERR) {
|
|
217
|
+
throw new Error(`Failed to create NetCDF file: ${path} (error: ${result.result})`);
|
|
218
|
+
}
|
|
219
|
+
return result.ncid;
|
|
220
|
+
}
|
|
221
|
+
async closeFile(ncid) {
|
|
222
|
+
if (this.worker) {
|
|
223
|
+
this.callWorker('close');
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
const module = this.module;
|
|
227
|
+
if (!module)
|
|
228
|
+
throw new Error("Failed to load module. Ensure module is initialized before calling methods");
|
|
229
|
+
const result = module.nc_close(ncid);
|
|
230
|
+
if (result !== NC_CONSTANTS.NC_NOERR) {
|
|
231
|
+
throw new Error(`Failed to close NetCDF file with ID: ${ncid} (error: ${result})`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
requestId = 0;
|
|
236
|
+
async callWorker(type, payload = {}) {
|
|
237
|
+
if (!this.worker)
|
|
238
|
+
throw new Error("Worker not initialized");
|
|
239
|
+
const id = ++this.requestId;
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
const handler = (e) => {
|
|
242
|
+
// Only handle messages that match our request ID
|
|
243
|
+
if (e.data.id !== id)
|
|
244
|
+
return;
|
|
245
|
+
if (e.data.success) {
|
|
246
|
+
resolve(e.data.result);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
reject(new Error(e.data.error || `Worker error in ${type}`));
|
|
250
|
+
}
|
|
251
|
+
this.worker.removeEventListener('message', handler);
|
|
252
|
+
};
|
|
253
|
+
this.worker.addEventListener('message', handler);
|
|
254
|
+
this.worker.postMessage({
|
|
255
|
+
id, // Include the ID in the request
|
|
256
|
+
type,
|
|
257
|
+
ncid: this.ncid,
|
|
258
|
+
...payload
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async getGlobalAttributes(groupPath) {
|
|
263
|
+
if (this.worker) {
|
|
264
|
+
return this.callWorker('getGlobalAttributes', { groupPath });
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
268
|
+
return NCGet.getGlobalAttributes(this.module, this.ncid, groupPath);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async getFullMetadata(groupPath) {
|
|
272
|
+
if (this.worker) {
|
|
273
|
+
return this.callWorker('getFullMetadata', { groupPath });
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
277
|
+
return NCGet.getFullMetadata(this.module, this.ncid, groupPath);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async getAttributeValues(varid, attname) {
|
|
281
|
+
if (this.worker) {
|
|
282
|
+
return this.callWorker('getAttributeValues', { varid, attname });
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
286
|
+
return NCGet.getAttributeValues(this.module, this.ncid, varid, attname);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async getDimCount(ncid = this.ncid) {
|
|
290
|
+
if (this.worker) {
|
|
291
|
+
return this.callWorker('getDimCount', { ncid });
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
295
|
+
return NCGet.getDimCount(this.module, ncid);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async getGroupVariables(groupPath) {
|
|
299
|
+
if (this.worker) {
|
|
300
|
+
return this.callWorker('getGroupVariables', { groupPath });
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
304
|
+
return NCGet.getGroupVariables(this.module, this.ncid, groupPath);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async getVarIDs(ncid = this.ncid) {
|
|
308
|
+
if (this.worker) {
|
|
309
|
+
return this.callWorker('getVarIDs', { ncid });
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
313
|
+
return NCGet.getVarIDs(this.module, ncid);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async getDimIDs(ncid = this.ncid) {
|
|
317
|
+
if (this.worker) {
|
|
318
|
+
return this.callWorker('getDimIDs', { ncid });
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
322
|
+
return NCGet.getDimIDs(this.module, ncid);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async getDim(dimid, ncid = this.ncid) {
|
|
326
|
+
if (this.worker) {
|
|
327
|
+
return this.callWorker('getDim', { dimid, ncid });
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
331
|
+
return NCGet.getDim(this.module, ncid, dimid);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async getDims(groupPath) {
|
|
335
|
+
if (this.worker) {
|
|
336
|
+
return this.callWorker('getDims', { groupPath });
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
340
|
+
return NCGet.getDims(this.module, this.ncid, groupPath);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async getVarCount(ncid = this.ncid) {
|
|
344
|
+
if (this.worker) {
|
|
345
|
+
return this.callWorker('getVarCount', { ncid });
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
349
|
+
return NCGet.getVarCount(this.module, ncid);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async getAttributeName(varid, attId) {
|
|
353
|
+
if (this.worker) {
|
|
354
|
+
return this.callWorker('getAttributeName', { varid, attId });
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
358
|
+
return NCGet.getAttributeName(this.module, this.ncid, varid, attId);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async getVariableInfo(variable, groupPath) {
|
|
362
|
+
if (this.worker) {
|
|
363
|
+
return this.callWorker('getVariableInfo', { variable, groupPath });
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
367
|
+
return NCGet.getVariableInfo(this.module, this.ncid, variable, groupPath);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async getVariableArray(variable, groupPath) {
|
|
371
|
+
if (this.worker) {
|
|
372
|
+
return this.callWorker('getVariableArray', { variable, groupPath });
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
376
|
+
return NCGet.getVariableArray(this.module, this.ncid, variable, groupPath);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async getSlicedVariableArray(variable, start, count, groupPath) {
|
|
380
|
+
if (this.worker) {
|
|
381
|
+
return this.callWorker('getSlicedVariableArray', { variable, start, count, groupPath });
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
// Main thread path is already synchronous (or could be wrapped in Promise.resolve)
|
|
385
|
+
return NCGet.getSlicedVariableArray(this.module, this.ncid, variable, start, count, groupPath);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* slicing and indexing convenience method.
|
|
390
|
+
*
|
|
391
|
+
* Each element of `selection` corresponds to one dimension of the variable:
|
|
392
|
+
* - 'all' or `null` → all elements of that dimension
|
|
393
|
+
* - `number` → scalar index (dimension is collapsed)
|
|
394
|
+
* - `slice(stop)` → elements [0, stop)
|
|
395
|
+
* - `slice(start, stop)` → elements [start, stop)
|
|
396
|
+
* - `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
|
|
397
|
+
*
|
|
398
|
+
* Strided reads use nc_get_vars_* under the hood.
|
|
399
|
+
* Returns a flat typed array; caller infers shape from non-collapsed dims.
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* // Variable shape [time, lat, lon]
|
|
403
|
+
* // Read 10 time steps, all lats, first lon:
|
|
404
|
+
* const data = await dataset.get("temperature", [slice(0, 10), 'all', 0]);
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* // Every other element along time:
|
|
408
|
+
* const data = await dataset.get("temperature", [slice(0, 100, 2), null, null]);
|
|
409
|
+
*
|
|
410
|
+
*/
|
|
411
|
+
async get(variable, selection, groupPath, options) {
|
|
412
|
+
if (this.worker) {
|
|
413
|
+
return this.callWorker('getVariableArrayWithSelection', {
|
|
414
|
+
ncid: this.ncid, variable, selection, groupPath, options
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
return NCGet.getVariableArrayWithSelection(this.module, this.ncid, variable, selection, groupPath, options);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Group functions
|
|
422
|
+
async getGroups(ncid = this.ncid) {
|
|
423
|
+
if (this.worker) {
|
|
424
|
+
return this.callWorker('getGroups', { ncid });
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
return NCGet.getGroups(this.module, ncid);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async getGroupsRecursive(ncid = this.ncid) {
|
|
431
|
+
if (this.worker) {
|
|
432
|
+
return this.callWorker('getGroupsRecursive', { ncid });
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
return NCGet.getGroupsRecursive(this.module, ncid);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async getGroupNCID(groupPath) {
|
|
439
|
+
if (this.worker) {
|
|
440
|
+
return this.callWorker('getGroupNCID', { groupPath });
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
return NCGet.getGroupNCID(this.module, this.ncid, groupPath);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async getGroupName(ncid = this.ncid) {
|
|
447
|
+
if (this.worker) {
|
|
448
|
+
return this.callWorker('getGroupName', { ncid });
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
return NCGet.getGroupName(this.module, ncid);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async getGroupPath(ncid = this.ncid) {
|
|
455
|
+
if (this.worker) {
|
|
456
|
+
return this.callWorker('getGroupPath', { ncid });
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
return NCGet.getGroupPath(this.module, ncid);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async defineDimension(ncid, name, size) {
|
|
463
|
+
const module = this.getModule();
|
|
464
|
+
const result = module.nc_def_dim(ncid, name, size);
|
|
465
|
+
if (result.result !== NC_CONSTANTS.NC_NOERR) {
|
|
466
|
+
throw new Error(`Failed to define dimension: ${name} (error: ${result.result})`);
|
|
467
|
+
}
|
|
468
|
+
return result.dimid;
|
|
469
|
+
}
|
|
470
|
+
async defineVariable(ncid, name, type, dimids) {
|
|
471
|
+
const module = this.getModule();
|
|
472
|
+
const result = module.nc_def_var(ncid, name, type, dimids.length, dimids);
|
|
473
|
+
if (result.result !== NC_CONSTANTS.NC_NOERR) {
|
|
474
|
+
throw new Error(`Failed to define variable: ${name} (error: ${result.result})`);
|
|
475
|
+
}
|
|
476
|
+
return result.varid;
|
|
477
|
+
}
|
|
478
|
+
async endDefineMode(ncid) {
|
|
479
|
+
const module = this.getModule();
|
|
480
|
+
const result = module.nc_enddef(ncid);
|
|
481
|
+
if (result !== NC_CONSTANTS.NC_NOERR) {
|
|
482
|
+
throw new Error(`Failed to end define mode (error: ${result})`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async putVariableDouble(ncid, varid, data) {
|
|
486
|
+
const module = this.getModule();
|
|
487
|
+
const result = module.nc_put_var_double(ncid, varid, data);
|
|
488
|
+
if (result !== NC_CONSTANTS.NC_NOERR) {
|
|
489
|
+
throw new Error(`Failed to write variable data (error: ${result})`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async getVariableDouble(ncid, varid, length) {
|
|
493
|
+
const module = this.getModule();
|
|
494
|
+
const result = module.nc_get_var_double(ncid, varid, length);
|
|
495
|
+
if (result.result !== NC_CONSTANTS.NC_NOERR) {
|
|
496
|
+
throw new Error(`Failed to read variable data (error: ${result.result})`);
|
|
497
|
+
}
|
|
498
|
+
if (!result.data) {
|
|
499
|
+
throw new Error("nc_get_var_double returned no data");
|
|
500
|
+
}
|
|
501
|
+
return result.data;
|
|
502
|
+
}
|
|
503
|
+
// Create a mock module for testing
|
|
504
|
+
createMockModule() {
|
|
505
|
+
// Global mock file storage to simulate persistence across instances
|
|
506
|
+
if (!global.__netcdf4_mock_files) {
|
|
507
|
+
global.__netcdf4_mock_files = {};
|
|
508
|
+
}
|
|
509
|
+
const mockFiles = global.__netcdf4_mock_files;
|
|
510
|
+
return {
|
|
511
|
+
nc_open: (path, mode) => {
|
|
512
|
+
// Mock implementation that simulates invalid filenames and unsupported modes
|
|
513
|
+
if (!path || path.trim() === '' || path.includes('unsupported') || !['r', 'w', 'a'].some(m => this.mode.includes(m))) {
|
|
514
|
+
return { result: -1, ncid: -1 };
|
|
515
|
+
}
|
|
516
|
+
// For reading mode, file should exist in mock storage, otherwise create a minimal entry
|
|
517
|
+
if (this.mode === 'r' && !mockFiles[path]) {
|
|
518
|
+
// For test purposes, allow reading non-existent files but initialize them empty
|
|
519
|
+
mockFiles[path] = {
|
|
520
|
+
attributes: {},
|
|
521
|
+
dimensions: {},
|
|
522
|
+
variables: {}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
return { result: NC_CONSTANTS.NC_NOERR, ncid: 1 };
|
|
526
|
+
},
|
|
527
|
+
nc_close: (ncid) => {
|
|
528
|
+
// In a real implementation, this would flush data to the file
|
|
529
|
+
// For our mock, we'll keep the data in memory
|
|
530
|
+
return NC_CONSTANTS.NC_NOERR;
|
|
531
|
+
},
|
|
532
|
+
nc_create: (path, mode) => {
|
|
533
|
+
if (path.includes('unsupported') || ['x', 'invalid'].some(m => this.mode.includes(m))) {
|
|
534
|
+
return { result: -1, ncid: -1 };
|
|
535
|
+
}
|
|
536
|
+
// Initialize mock file storage
|
|
537
|
+
mockFiles[path] = {
|
|
538
|
+
attributes: {},
|
|
539
|
+
dimensions: {},
|
|
540
|
+
variables: {}
|
|
541
|
+
};
|
|
542
|
+
return { result: NC_CONSTANTS.NC_NOERR, ncid: 1 };
|
|
543
|
+
},
|
|
544
|
+
nc_def_dim: (ncid, name, len) => {
|
|
545
|
+
// Store dimension in mock file
|
|
546
|
+
if (this.filename && mockFiles[this.filename]) {
|
|
547
|
+
mockFiles[this.filename].dimensions[name] = {
|
|
548
|
+
size: len,
|
|
549
|
+
unlimited: len === NC_CONSTANTS.NC_UNLIMITED
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return { result: NC_CONSTANTS.NC_NOERR, dimid: 1 };
|
|
553
|
+
},
|
|
554
|
+
nc_def_var: (ncid, name, xtype, ndims, dimids) => {
|
|
555
|
+
// Initialize variable storage
|
|
556
|
+
if (this.filename && mockFiles[this.filename]) {
|
|
557
|
+
mockFiles[this.filename].variables[name] = {
|
|
558
|
+
data: new Float64Array(0),
|
|
559
|
+
attributes: {}
|
|
560
|
+
};
|
|
561
|
+
// Return varid based on current variable count (1-based)
|
|
562
|
+
const varCount = Object.keys(mockFiles[this.filename].variables).length;
|
|
563
|
+
return { result: NC_CONSTANTS.NC_NOERR, varid: varCount };
|
|
564
|
+
}
|
|
565
|
+
return { result: NC_CONSTANTS.NC_NOERR, varid: 1 };
|
|
566
|
+
},
|
|
567
|
+
nc_put_var_double: (ncid, varid, data) => {
|
|
568
|
+
// Store data in mock file - try to map varid to variable name
|
|
569
|
+
if (this.filename && mockFiles[this.filename]) {
|
|
570
|
+
const variables = mockFiles[this.filename].variables;
|
|
571
|
+
const varNames = Object.keys(variables);
|
|
572
|
+
// Map varid to variable name (1-based indexing)
|
|
573
|
+
if (varNames.length > 0 && varid >= 1 && varid <= varNames.length) {
|
|
574
|
+
const varName = varNames[varid - 1]; // Convert to 0-based
|
|
575
|
+
variables[varName].data = new Float64Array(data);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return NC_CONSTANTS.NC_NOERR;
|
|
579
|
+
},
|
|
580
|
+
nc_get_var_double: (ncid, varid, size) => {
|
|
581
|
+
// Try to get actual stored data first
|
|
582
|
+
if (this.filename && mockFiles[this.filename]) {
|
|
583
|
+
const variables = mockFiles[this.filename].variables;
|
|
584
|
+
const varNames = Object.keys(variables);
|
|
585
|
+
// Map varid to variable name (1-based indexing)
|
|
586
|
+
if (varNames.length > 0 && varid >= 1 && varid <= varNames.length) {
|
|
587
|
+
const varName = varNames[varid - 1]; // Convert to 0-based
|
|
588
|
+
const storedData = variables[varName].data;
|
|
589
|
+
if (storedData && storedData.length > 0) {
|
|
590
|
+
// Return the stored data, resized to requested size if needed
|
|
591
|
+
if (size <= 0) {
|
|
592
|
+
return { result: NC_CONSTANTS.NC_NOERR, data: new Float64Array(0) };
|
|
593
|
+
}
|
|
594
|
+
const result = new Float64Array(size);
|
|
595
|
+
for (let i = 0; i < size && i < storedData.length; i++) {
|
|
596
|
+
result[i] = storedData[i];
|
|
597
|
+
}
|
|
598
|
+
return { result: NC_CONSTANTS.NC_NOERR, data: result };
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Fallback to test pattern if no data stored
|
|
603
|
+
if (size <= 0) {
|
|
604
|
+
return { result: NC_CONSTANTS.NC_NOERR, data: new Float64Array(0) };
|
|
605
|
+
}
|
|
606
|
+
const data = new Float64Array(size);
|
|
607
|
+
for (let i = 0; i < size; i++) {
|
|
608
|
+
data[i] = i * 0.1; // Simple test pattern
|
|
609
|
+
}
|
|
610
|
+
return { result: NC_CONSTANTS.NC_NOERR, data };
|
|
611
|
+
},
|
|
612
|
+
nc_enddef: (ncid) => NC_CONSTANTS.NC_NOERR,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
async setupWorker() {
|
|
616
|
+
if (!this.workerSource)
|
|
617
|
+
throw new Error('No worker source');
|
|
618
|
+
// 1. Instantiate the worker if it doesn't exist
|
|
619
|
+
if (!this.worker) {
|
|
620
|
+
// Option A: If using Vite/Webpack 5
|
|
621
|
+
this.worker = new Worker(new URL('./netcdf-worker.js', import.meta.url), { type: 'module' });
|
|
622
|
+
}
|
|
623
|
+
this.workerReady = new Promise((resolve, reject) => {
|
|
624
|
+
// Use a named function so we can remove the listener later
|
|
625
|
+
const initHandler = (e) => {
|
|
626
|
+
if (e.data.success) {
|
|
627
|
+
this.worker.removeEventListener('message', initHandler);
|
|
628
|
+
resolve();
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
this.worker.removeEventListener('message', initHandler);
|
|
632
|
+
reject(new Error(e.data.message));
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
this.worker.addEventListener('message', initHandler);
|
|
636
|
+
// 3. Send the initialization message
|
|
637
|
+
this.worker.postMessage({
|
|
638
|
+
type: 'init',
|
|
639
|
+
blob: this.workerSource.blob,
|
|
640
|
+
filename: this.workerSource.filename
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
return this.workerReady;
|
|
644
|
+
}
|
|
645
|
+
// Mount memory data in the WASM virtual file system
|
|
646
|
+
async mountMemoryData() {
|
|
647
|
+
if (!this.memorySource || !this.module) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
// Skip mounting in test mode (mock module doesn't have FS)
|
|
651
|
+
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
const module = this.getModule();
|
|
656
|
+
if (!module.FS) {
|
|
657
|
+
throw new Error('Emscripten FS not available');
|
|
658
|
+
}
|
|
659
|
+
// Ensure the /tmp directory exists
|
|
660
|
+
try {
|
|
661
|
+
module.FS.mkdir('/tmp');
|
|
662
|
+
}
|
|
663
|
+
catch (e) {
|
|
664
|
+
// Directory might already exist, ignore error
|
|
665
|
+
}
|
|
666
|
+
// Write the memory data to a virtual file
|
|
667
|
+
module.FS.writeFile(this.memorySource.filename, this.memorySource.data);
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
throw new Error(`Failed to mount memory data: ${error}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// Get data from memory or file as ArrayBuffer (for writing back to Blob)
|
|
674
|
+
async toArrayBuffer() {
|
|
675
|
+
if (!this.module) {
|
|
676
|
+
throw new Error('NetCDF4 module not initialized');
|
|
677
|
+
}
|
|
678
|
+
// Skip in test mode
|
|
679
|
+
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
|
680
|
+
// Return empty buffer in test mode
|
|
681
|
+
return new ArrayBuffer(0);
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
const module = this.getModule();
|
|
685
|
+
if (!module.FS || !this.filename) {
|
|
686
|
+
throw new Error('Cannot read file data');
|
|
687
|
+
}
|
|
688
|
+
// Read the file from the virtual file system
|
|
689
|
+
const data = module.FS.readFile(this.filename);
|
|
690
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
throw new Error(`Failed to read data as ArrayBuffer: ${error}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Convert to Blob
|
|
697
|
+
async toBlob(type = 'application/x-netcdf') {
|
|
698
|
+
const buffer = await this.toArrayBuffer();
|
|
699
|
+
return new Blob([buffer], { type });
|
|
700
|
+
}
|
|
701
|
+
toString() {
|
|
702
|
+
const status = this._isOpen ? 'open' : 'closed';
|
|
703
|
+
const source = this.memorySource ? '(in-memory)' : '';
|
|
704
|
+
return `<netCDF4.Dataset '${this.filename}'${source}: mode = '${this.mode}', file format = '${this.file_format}', ${status}>`;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Get complete hierarchy of groups, variables, dimensions, and attributes
|
|
708
|
+
* This is the unified method for exploring the entire file structure
|
|
709
|
+
* @param groupPath - Optional path to start from a specific group
|
|
710
|
+
*/
|
|
711
|
+
async getCompleteHierarchy(groupPath) {
|
|
712
|
+
if (this.worker) {
|
|
713
|
+
return this.callWorker('getCompleteHierarchy', { groupPath });
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
return NCGet.getCompleteHierarchy(this.module, this.ncid, groupPath);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Get all variables recursively from all groups
|
|
721
|
+
* Returns a flat dictionary with full path keys like "/group1/var1"
|
|
722
|
+
*/
|
|
723
|
+
async getVariables() {
|
|
724
|
+
if (this.worker) {
|
|
725
|
+
return this.callWorker('getVariables');
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
return NCGet.getVariables(this.module, this.ncid);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* UI-friendly wrapper around NetCDF4
|
|
734
|
+
* Builds a full dataTree of groups, variables, attributes with enhanced navigation
|
|
735
|
+
*/
|
|
736
|
+
export class DataTree {
|
|
737
|
+
dataset;
|
|
738
|
+
tree = {};
|
|
739
|
+
groupTreeCache = null;
|
|
740
|
+
constructor(dataset) {
|
|
741
|
+
this.dataset = dataset;
|
|
742
|
+
}
|
|
743
|
+
async buildTree() {
|
|
744
|
+
this.tree = await this.dataset.getCompleteHierarchy();
|
|
745
|
+
// Clear cache when tree is rebuilt
|
|
746
|
+
this.groupTreeCache = null;
|
|
747
|
+
}
|
|
748
|
+
// --------------------------------------------------
|
|
749
|
+
// Core navigation
|
|
750
|
+
// --------------------------------------------------
|
|
751
|
+
getGroup(groupPath = '/') {
|
|
752
|
+
if (!this.tree)
|
|
753
|
+
return null;
|
|
754
|
+
if (groupPath === '/' || !groupPath)
|
|
755
|
+
return this.tree;
|
|
756
|
+
const parts = groupPath.split('/').filter(Boolean);
|
|
757
|
+
let current = this.tree;
|
|
758
|
+
for (const part of parts) {
|
|
759
|
+
if (!current.groups || !current.groups[part])
|
|
760
|
+
return null;
|
|
761
|
+
current = current.groups[part];
|
|
762
|
+
}
|
|
763
|
+
return current;
|
|
764
|
+
}
|
|
765
|
+
getGroupName(groupPath) {
|
|
766
|
+
if (!groupPath || groupPath === '/')
|
|
767
|
+
return 'root';
|
|
768
|
+
const parts = groupPath.split('/').filter(Boolean);
|
|
769
|
+
return parts[parts.length - 1];
|
|
770
|
+
}
|
|
771
|
+
hasSubgroups(groupPath = '/') {
|
|
772
|
+
const group = this.getGroup(groupPath);
|
|
773
|
+
return group ? Object.keys(group.groups || {}).length > 0 : false;
|
|
774
|
+
}
|
|
775
|
+
// --------------------------------------------------
|
|
776
|
+
// Groups (for dropdowns)
|
|
777
|
+
// --------------------------------------------------
|
|
778
|
+
/** immediate children only */
|
|
779
|
+
listGroups(groupPath = '/') {
|
|
780
|
+
const group = this.getGroup(groupPath);
|
|
781
|
+
if (!group || !group.groups)
|
|
782
|
+
return [];
|
|
783
|
+
return Object.entries(group.groups).map(([name, g]) => ({
|
|
784
|
+
name,
|
|
785
|
+
path: g.path || `${groupPath === '/' ? '' : groupPath}/${name}`
|
|
786
|
+
}));
|
|
787
|
+
}
|
|
788
|
+
/** every group recursively */
|
|
789
|
+
listAllGroups() {
|
|
790
|
+
const result = [];
|
|
791
|
+
const walk = (g) => {
|
|
792
|
+
if (!g.groups)
|
|
793
|
+
return;
|
|
794
|
+
for (const [name, sub] of Object.entries(g.groups)) {
|
|
795
|
+
result.push({ name, path: sub.path });
|
|
796
|
+
walk(sub);
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
walk(this.tree);
|
|
800
|
+
return result;
|
|
801
|
+
}
|
|
802
|
+
// --------------------------------------------------
|
|
803
|
+
// Hierarchical Group Tree
|
|
804
|
+
// --------------------------------------------------
|
|
805
|
+
/**
|
|
806
|
+
* Build a hierarchical tree structure of all groups
|
|
807
|
+
* Returns a tree with parent-child relationships
|
|
808
|
+
* Results are cached until buildTree() is called again
|
|
809
|
+
*/
|
|
810
|
+
buildGroupTree() {
|
|
811
|
+
// Return cached version if available
|
|
812
|
+
if (this.groupTreeCache) {
|
|
813
|
+
return this.groupTreeCache;
|
|
814
|
+
}
|
|
815
|
+
const allGroups = this.listAllGroups();
|
|
816
|
+
// Create root node
|
|
817
|
+
const root = {
|
|
818
|
+
name: '/',
|
|
819
|
+
path: '/',
|
|
820
|
+
children: [],
|
|
821
|
+
hasVariables: this.hasVariables('/'),
|
|
822
|
+
hasAttributes: this.hasAttributes('/'),
|
|
823
|
+
variableCount: this.getVariableCount('/'),
|
|
824
|
+
attributeCount: this.getAttributeCount('/')
|
|
825
|
+
};
|
|
826
|
+
// Map to store all nodes by path for quick lookup
|
|
827
|
+
const nodeMap = new Map();
|
|
828
|
+
nodeMap.set('/', root);
|
|
829
|
+
// Sort groups by path depth to ensure parents are created before children
|
|
830
|
+
const sortedGroups = allGroups.sort((a, b) => {
|
|
831
|
+
const depthA = a.path.split('/').filter(Boolean).length;
|
|
832
|
+
const depthB = b.path.split('/').filter(Boolean).length;
|
|
833
|
+
return depthA - depthB;
|
|
834
|
+
});
|
|
835
|
+
// Build the tree
|
|
836
|
+
for (const { name, path } of sortedGroups) {
|
|
837
|
+
const node = {
|
|
838
|
+
name,
|
|
839
|
+
path,
|
|
840
|
+
children: [],
|
|
841
|
+
hasVariables: this.hasVariables(path),
|
|
842
|
+
hasAttributes: this.hasAttributes(path),
|
|
843
|
+
variableCount: this.getVariableCount(path),
|
|
844
|
+
attributeCount: this.getAttributeCount(path)
|
|
845
|
+
};
|
|
846
|
+
nodeMap.set(path, node);
|
|
847
|
+
// Find parent path
|
|
848
|
+
const pathParts = path.split('/').filter(Boolean);
|
|
849
|
+
const parentPath = pathParts.length === 1
|
|
850
|
+
? '/'
|
|
851
|
+
: '/' + pathParts.slice(0, -1).join('/');
|
|
852
|
+
const parent = nodeMap.get(parentPath);
|
|
853
|
+
if (parent) {
|
|
854
|
+
parent.children.push(node);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
// Cache the result
|
|
858
|
+
this.groupTreeCache = root;
|
|
859
|
+
return root;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Get a specific node from the group tree by path
|
|
863
|
+
*/
|
|
864
|
+
getGroupNode(path) {
|
|
865
|
+
const tree = this.buildGroupTree();
|
|
866
|
+
if (path === '/')
|
|
867
|
+
return tree;
|
|
868
|
+
const parts = path.split('/').filter(Boolean);
|
|
869
|
+
let current = tree;
|
|
870
|
+
for (const part of parts) {
|
|
871
|
+
const child = current.children.find(c => c.name === part);
|
|
872
|
+
if (!child)
|
|
873
|
+
return null;
|
|
874
|
+
current = child;
|
|
875
|
+
}
|
|
876
|
+
return current;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Get breadcrumb trail for a given path
|
|
880
|
+
* Returns array of {name, path} from root to target
|
|
881
|
+
*/
|
|
882
|
+
getBreadcrumbs(groupPath) {
|
|
883
|
+
if (groupPath === '/') {
|
|
884
|
+
return [{ name: 'root', path: '/' }];
|
|
885
|
+
}
|
|
886
|
+
const parts = groupPath.split('/').filter(Boolean);
|
|
887
|
+
const breadcrumbs = [
|
|
888
|
+
{ name: 'root', path: '/' }
|
|
889
|
+
];
|
|
890
|
+
let currentPath = '';
|
|
891
|
+
for (const part of parts) {
|
|
892
|
+
currentPath += '/' + part;
|
|
893
|
+
breadcrumbs.push({
|
|
894
|
+
name: part,
|
|
895
|
+
path: currentPath
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
return breadcrumbs;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Search for groups by name (case-insensitive)
|
|
902
|
+
*/
|
|
903
|
+
searchGroups(query) {
|
|
904
|
+
const lowerQuery = query.toLowerCase();
|
|
905
|
+
return this.listAllGroups().filter(g => g.name.toLowerCase().includes(lowerQuery) ||
|
|
906
|
+
g.path.toLowerCase().includes(lowerQuery));
|
|
907
|
+
}
|
|
908
|
+
// --------------------------------------------------
|
|
909
|
+
// Variables
|
|
910
|
+
// --------------------------------------------------
|
|
911
|
+
/** variables inside a group */
|
|
912
|
+
getAllVariables(groupPath = '/') {
|
|
913
|
+
const group = this.getGroup(groupPath);
|
|
914
|
+
return group?.variables || {};
|
|
915
|
+
}
|
|
916
|
+
/** check if group has variables */
|
|
917
|
+
hasVariables(groupPath = '/') {
|
|
918
|
+
const vars = this.getAllVariables(groupPath);
|
|
919
|
+
return Object.keys(vars).length > 0;
|
|
920
|
+
}
|
|
921
|
+
/** count variables in a group */
|
|
922
|
+
getVariableCount(groupPath = '/') {
|
|
923
|
+
return Object.keys(this.getAllVariables(groupPath)).length;
|
|
924
|
+
}
|
|
925
|
+
/** get variable names as array */
|
|
926
|
+
getVariableNames(groupPath = '/') {
|
|
927
|
+
return Object.keys(this.getAllVariables(groupPath));
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Search for variables by name across all groups
|
|
931
|
+
*/
|
|
932
|
+
searchVariables(query) {
|
|
933
|
+
const results = [];
|
|
934
|
+
const lowerQuery = query.toLowerCase();
|
|
935
|
+
const searchInGroup = (groupPath) => {
|
|
936
|
+
const vars = this.getAllVariables(groupPath);
|
|
937
|
+
for (const varName of Object.keys(vars)) {
|
|
938
|
+
if (varName.toLowerCase().includes(lowerQuery)) {
|
|
939
|
+
results.push({
|
|
940
|
+
name: varName,
|
|
941
|
+
groupPath,
|
|
942
|
+
path: `${groupPath === '/' ? '' : groupPath}/${varName}`
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// Recurse into subgroups
|
|
947
|
+
const subgroups = this.listGroups(groupPath);
|
|
948
|
+
for (const { path } of subgroups) {
|
|
949
|
+
searchInGroup(path);
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
searchInGroup('/');
|
|
953
|
+
return results;
|
|
954
|
+
}
|
|
955
|
+
// --------------------------------------------------
|
|
956
|
+
// Attributes
|
|
957
|
+
// --------------------------------------------------
|
|
958
|
+
/** attributes from the tree (fast) */
|
|
959
|
+
getAttributes(groupPath = '/') {
|
|
960
|
+
const group = this.getGroup(groupPath);
|
|
961
|
+
return group?.attributes || {};
|
|
962
|
+
}
|
|
963
|
+
/** check if group has attributes */
|
|
964
|
+
hasAttributes(groupPath = '/') {
|
|
965
|
+
const attrs = this.getAttributes(groupPath);
|
|
966
|
+
return Object.keys(attrs).length > 0;
|
|
967
|
+
}
|
|
968
|
+
/** count attributes in a group */
|
|
969
|
+
getAttributeCount(groupPath = '/') {
|
|
970
|
+
return Object.keys(this.getAttributes(groupPath)).length;
|
|
971
|
+
}
|
|
972
|
+
// --------------------------------------------------
|
|
973
|
+
// Dimensions
|
|
974
|
+
// --------------------------------------------------
|
|
975
|
+
/** get dimensions for a group */
|
|
976
|
+
getDimensions(groupPath = '/') {
|
|
977
|
+
const group = this.getGroup(groupPath);
|
|
978
|
+
return group?.dimensions || {};
|
|
979
|
+
}
|
|
980
|
+
/** check if group has dimensions */
|
|
981
|
+
hasDimensions(groupPath = '/') {
|
|
982
|
+
const dims = this.getDimensions(groupPath);
|
|
983
|
+
return Object.keys(dims).length > 0;
|
|
984
|
+
}
|
|
985
|
+
/** count dimensions in a group */
|
|
986
|
+
getDimensionCount(groupPath = '/') {
|
|
987
|
+
return Object.keys(this.getDimensions(groupPath)).length;
|
|
988
|
+
}
|
|
989
|
+
// --------------------------------------------------
|
|
990
|
+
// Statistics and Summaries
|
|
991
|
+
// --------------------------------------------------
|
|
992
|
+
/**
|
|
993
|
+
* Get summary statistics for a group
|
|
994
|
+
*/
|
|
995
|
+
getGroupSummary(groupPath = '/') {
|
|
996
|
+
const group = this.getGroup(groupPath);
|
|
997
|
+
if (!group)
|
|
998
|
+
return null;
|
|
999
|
+
return {
|
|
1000
|
+
path: groupPath,
|
|
1001
|
+
name: this.getGroupName(groupPath),
|
|
1002
|
+
variableCount: this.getVariableCount(groupPath),
|
|
1003
|
+
attributeCount: this.getAttributeCount(groupPath),
|
|
1004
|
+
dimensionCount: this.getDimensionCount(groupPath),
|
|
1005
|
+
subgroupCount: this.listGroups(groupPath).length,
|
|
1006
|
+
hasSubgroups: this.hasSubgroups(groupPath)
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Get complete statistics for the entire dataset
|
|
1011
|
+
*/
|
|
1012
|
+
getDatasetSummary() {
|
|
1013
|
+
let totalVariables = 0;
|
|
1014
|
+
let totalAttributes = 0;
|
|
1015
|
+
let totalDimensions = 0;
|
|
1016
|
+
let maxDepth = 0;
|
|
1017
|
+
const countInGroup = (groupPath, depth) => {
|
|
1018
|
+
totalVariables += this.getVariableCount(groupPath);
|
|
1019
|
+
totalAttributes += this.getAttributeCount(groupPath);
|
|
1020
|
+
totalDimensions += this.getDimensionCount(groupPath);
|
|
1021
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
1022
|
+
const subgroups = this.listGroups(groupPath);
|
|
1023
|
+
for (const { path } of subgroups) {
|
|
1024
|
+
countInGroup(path, depth + 1);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
countInGroup('/', 0);
|
|
1028
|
+
return {
|
|
1029
|
+
totalGroups: this.listAllGroups().length + 1, // +1 for root
|
|
1030
|
+
totalVariables,
|
|
1031
|
+
totalAttributes,
|
|
1032
|
+
totalDimensions,
|
|
1033
|
+
maxDepth
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
// --------------------------------------------------
|
|
1037
|
+
// Heavy operations → still go to dataset
|
|
1038
|
+
// --------------------------------------------------
|
|
1039
|
+
async getVariableArray(variable, groupPath) {
|
|
1040
|
+
return this.dataset.getVariableArray(variable, groupPath);
|
|
1041
|
+
}
|
|
1042
|
+
async getSlicedVariableArray(variable, start, count, groupPath) {
|
|
1043
|
+
return this.dataset.getSlicedVariableArray(variable, start, count, groupPath);
|
|
1044
|
+
}
|
|
1045
|
+
async getVariableInfo(variable, groupPath) {
|
|
1046
|
+
return this.dataset.getVariableInfo(variable, groupPath);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
//# sourceMappingURL=netcdf4.js.map
|