@aguacerowx/javascript-sdk 0.0.0 → 0.0.1
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/index.js +33 -0
- package/package.json +16 -7
- package/src/AguaceroCore.js +783 -0
- package/src/coordinate_configs.js +374 -0
- package/src/default-colormaps.js +2913 -0
- package/src/dictionaries.js +4267 -0
- package/src/events.js +20 -0
- package/src/fill-layer-worker.js +27 -0
- package/src/getBundleId.js +22 -0
- package/src/index.js +15 -0
- package/src/map-styles.js +102 -0
- package/src/unitConversions.js +103 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
// AguaceroCore.js - The Headless "Engine"
|
|
2
|
+
|
|
3
|
+
// --- Non-UI Imports ---
|
|
4
|
+
import { EventEmitter } from './events.js';
|
|
5
|
+
import { COORDINATE_CONFIGS } from './coordinate_configs.js';
|
|
6
|
+
import { getUnitConversionFunction } from './unitConversions.js';
|
|
7
|
+
import { DICTIONARIES, MODEL_CONFIGS } from './dictionaries.js';
|
|
8
|
+
import { DEFAULT_COLORMAPS } from './default-colormaps.js';
|
|
9
|
+
import proj4 from 'proj4';
|
|
10
|
+
import { decompress } from 'fzstd';
|
|
11
|
+
import { getBundleId } from './getBundleId';
|
|
12
|
+
|
|
13
|
+
// --- Non-UI Helper Functions ---
|
|
14
|
+
function hrdpsObliqueTransform(rotated_lon, rotated_lat) {
|
|
15
|
+
const o_lat_p = 53.91148; const o_lon_p = 245.305142;
|
|
16
|
+
const DEG_TO_RAD = Math.PI / 180.0; const RAD_TO_DEG = 180.0 / Math.PI;
|
|
17
|
+
const o_lat_p_rad = o_lat_p * DEG_TO_RAD;
|
|
18
|
+
const rot_lon_rad = rotated_lon * DEG_TO_RAD;
|
|
19
|
+
const rot_lat_rad = rotated_lat * DEG_TO_RAD;
|
|
20
|
+
const sin_rot_lat = Math.sin(rot_lat_rad); const cos_rot_lat = Math.cos(rot_lat_rad);
|
|
21
|
+
const sin_rot_lon = Math.sin(rot_lon_rad); const cos_rot_lon = Math.cos(rot_lon_rad);
|
|
22
|
+
const sin_o_lat_p = Math.sin(o_lat_p_rad); const cos_o_lat_p = Math.cos(o_lat_p_rad);
|
|
23
|
+
const sin_lat = cos_o_lat_p * sin_rot_lat + sin_o_lat_p * cos_rot_lat * cos_rot_lon;
|
|
24
|
+
let lat = Math.asin(sin_lat) * RAD_TO_DEG;
|
|
25
|
+
const sin_lon_num = cos_rot_lat * sin_rot_lon;
|
|
26
|
+
const sin_lon_den = -sin_o_lat_p * sin_rot_lat + cos_o_lat_p * cos_rot_lat * cos_rot_lon;
|
|
27
|
+
let lon = Math.atan2(sin_lon_num, sin_lon_den) * RAD_TO_DEG + o_lon_p;
|
|
28
|
+
if (lon > 180) lon -= 360; else if (lon < -180) lon += 360;
|
|
29
|
+
return [lon, lat];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findLatestModelRun(modelsData, modelName) {
|
|
33
|
+
const model = modelsData?.[modelName];
|
|
34
|
+
if (!model) return null;
|
|
35
|
+
const availableDates = Object.keys(model).sort((a, b) => b.localeCompare(a));
|
|
36
|
+
for (const date of availableDates) {
|
|
37
|
+
const runs = model[date];
|
|
38
|
+
if (!runs) continue;
|
|
39
|
+
const availableRuns = Object.keys(runs).sort((a, b) => b.localeCompare(a));
|
|
40
|
+
if (availableRuns.length > 0) return { date: date, run: availableRuns[0] };
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class AguaceroCore extends EventEmitter {
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
super();
|
|
48
|
+
this.isReactNative = typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
49
|
+
this.apiKey = options.apiKey;
|
|
50
|
+
this.bundleId = getBundleId();
|
|
51
|
+
this.baseGridUrl = 'https://d3dc62msmxkrd7.cloudfront.net';
|
|
52
|
+
if (!this.isReactNative) {
|
|
53
|
+
this.worker = this.createWorker();
|
|
54
|
+
this.workerRequestId = 0;
|
|
55
|
+
this.workerResolvers = new Map();
|
|
56
|
+
this.worker.addEventListener('message', this._handleWorkerMessage.bind(this));
|
|
57
|
+
this.resultQueue = [];
|
|
58
|
+
this.isProcessingQueue = false;
|
|
59
|
+
} else {
|
|
60
|
+
this.worker = null;
|
|
61
|
+
}
|
|
62
|
+
this.statusUrl = 'https://d3dc62msmxkrd7.cloudfront.net/model-status';
|
|
63
|
+
this.modelStatus = null;
|
|
64
|
+
this.mrmsStatus = null;
|
|
65
|
+
this.dataCache = new Map();
|
|
66
|
+
this.abortControllers = new Map();
|
|
67
|
+
this.isPlaying = false;
|
|
68
|
+
this.playIntervalId = null;
|
|
69
|
+
this.playbackSpeed = options.playbackSpeed || 500;
|
|
70
|
+
this.customColormaps = options.customColormaps || {};
|
|
71
|
+
|
|
72
|
+
const userLayerOptions = options.layerOptions || {};
|
|
73
|
+
const initialVariable = userLayerOptions.variable || null;
|
|
74
|
+
|
|
75
|
+
this.state = {
|
|
76
|
+
model: userLayerOptions.model || 'gfs',
|
|
77
|
+
isMRMS: false,
|
|
78
|
+
mrmsTimestamp: null,
|
|
79
|
+
variable: initialVariable,
|
|
80
|
+
date: null,
|
|
81
|
+
run: null,
|
|
82
|
+
forecastHour: 0,
|
|
83
|
+
visible: true,
|
|
84
|
+
opacity: userLayerOptions.opacity ?? 0.85,
|
|
85
|
+
units: options.initialUnit || 'imperial',
|
|
86
|
+
shaderSmoothingEnabled: options.shaderSmoothingEnabled ?? true
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
this.autoRefreshEnabled = options.autoRefresh ?? false;
|
|
90
|
+
this.autoRefreshIntervalSeconds = options.autoRefreshInterval ?? 60;
|
|
91
|
+
this.autoRefreshIntervalId = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async setState(newState) {
|
|
95
|
+
Object.assign(this.state, newState);
|
|
96
|
+
this._emitStateChange();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_emitStateChange() {
|
|
100
|
+
const { colormap, baseUnit } = this._getColormapForVariable(this.state.variable);
|
|
101
|
+
const toUnit = this._getTargetUnit(baseUnit, this.state.units);
|
|
102
|
+
const displayColormap = this._convertColormapUnits(colormap, baseUnit, toUnit);
|
|
103
|
+
|
|
104
|
+
let availableTimestamps = [];
|
|
105
|
+
if (this.state.isMRMS && this.state.variable && this.mrmsStatus) {
|
|
106
|
+
const timestamps = this.mrmsStatus[this.state.variable] || [];
|
|
107
|
+
availableTimestamps = [...timestamps].reverse();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.emit('state:change', {
|
|
111
|
+
...this.state,
|
|
112
|
+
availableModels: this.modelStatus ? Object.keys(this.modelStatus).sort() : [],
|
|
113
|
+
availableRuns: this.modelStatus?.[this.state.model] || {},
|
|
114
|
+
availableHours: this.state.isMRMS ? [] : (this.modelStatus?.[this.state.model]?.[this.state.date]?.[this.state.run] || []),
|
|
115
|
+
availableVariables: this.getAvailableVariables(this.state.isMRMS ? 'mrms' : this.state.model),
|
|
116
|
+
availableTimestamps: availableTimestamps,
|
|
117
|
+
isPlaying: this.isPlaying,
|
|
118
|
+
colormap: displayColormap,
|
|
119
|
+
colormapBaseUnit: toUnit,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async initialize(options = {}) {
|
|
124
|
+
await this.fetchModelStatus(true);
|
|
125
|
+
await this.fetchMRMSStatus(true);
|
|
126
|
+
|
|
127
|
+
const latestRun = findLatestModelRun(this.modelStatus, this.state.model);
|
|
128
|
+
let initialState = this.state;
|
|
129
|
+
|
|
130
|
+
if (latestRun && !this.state.isMRMS) {
|
|
131
|
+
initialState = { ...this.state, ...latestRun, forecastHour: 0 };
|
|
132
|
+
|
|
133
|
+
const availableVariables = this.getAvailableVariables(initialState.model);
|
|
134
|
+
if (availableVariables && availableVariables.length > 0) {
|
|
135
|
+
initialState.variable = availableVariables[0];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await this.setState(initialState);
|
|
140
|
+
if (options.autoRefresh ?? this.autoRefreshEnabled) {
|
|
141
|
+
this.startAutoRefresh(options.refreshInterval ?? this.autoRefreshIntervalSeconds);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
destroy() {
|
|
146
|
+
this.pause();
|
|
147
|
+
this.stopAutoRefresh();
|
|
148
|
+
this.dataCache.clear();
|
|
149
|
+
if (this.worker) {
|
|
150
|
+
this.worker.terminate();
|
|
151
|
+
}
|
|
152
|
+
this.callbacks = {};
|
|
153
|
+
console.log(`AguaceroCore has been destroyed.`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- Public API Methods ---
|
|
157
|
+
|
|
158
|
+
play() {
|
|
159
|
+
if (this.isPlaying) return;
|
|
160
|
+
this.isPlaying = true;
|
|
161
|
+
clearInterval(this.playIntervalId);
|
|
162
|
+
this.playIntervalId = setInterval(() => { this.step(1); }, this.playbackSpeed);
|
|
163
|
+
this.emit('playback:start', { speed: this.playbackSpeed });
|
|
164
|
+
this._emitStateChange(); // Notify UI that isPlaying is now true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
pause() {
|
|
168
|
+
if (!this.isPlaying) return;
|
|
169
|
+
this.isPlaying = false;
|
|
170
|
+
clearInterval(this.playIntervalId);
|
|
171
|
+
this.playIntervalId = null;
|
|
172
|
+
this.emit('playback:stop');
|
|
173
|
+
this._emitStateChange(); // Notify UI that isPlaying is now false
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
togglePlay() { this.isPlaying ? this.pause() : this.play(); }
|
|
177
|
+
|
|
178
|
+
step(direction = 1) {
|
|
179
|
+
const { model, date, run, forecastHour } = this.state;
|
|
180
|
+
const forecastHours = this.modelStatus?.[model]?.[date]?.[run];
|
|
181
|
+
if (!forecastHours || forecastHours.length === 0) return;
|
|
182
|
+
const currentIndex = forecastHours.indexOf(forecastHour);
|
|
183
|
+
if (currentIndex === -1) return;
|
|
184
|
+
const maxIndex = forecastHours.length - 1;
|
|
185
|
+
let nextIndex = currentIndex + direction;
|
|
186
|
+
if (nextIndex > maxIndex) nextIndex = 0;
|
|
187
|
+
if (nextIndex < 0) nextIndex = maxIndex;
|
|
188
|
+
this.setState({ forecastHour: forecastHours[nextIndex] });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
setPlaybackSpeed(speed) {
|
|
192
|
+
if (speed > 0) {
|
|
193
|
+
this.playbackSpeed = speed;
|
|
194
|
+
if (this.isPlaying) this.play();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async setShaderSmoothing(enabled) {
|
|
199
|
+
if (typeof enabled !== 'boolean' || enabled === this.state.shaderSmoothingEnabled) return;
|
|
200
|
+
await this.setState({ shaderSmoothingEnabled: enabled });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async setOpacity(newOpacity) {
|
|
204
|
+
const clampedOpacity = Math.max(0, Math.min(1, newOpacity));
|
|
205
|
+
if (clampedOpacity === this.state.opacity) return;
|
|
206
|
+
await this.setState({ opacity: clampedOpacity });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async setVariable(variable) {
|
|
210
|
+
await this.setState({ variable });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async setModel(modelName) {
|
|
214
|
+
if (modelName === this.state.model || !this.modelStatus?.[modelName]) return;
|
|
215
|
+
const latestRun = findLatestModelRun(this.modelStatus, modelName);
|
|
216
|
+
if (latestRun) {
|
|
217
|
+
await this.setState({ model: modelName, date: latestRun.date, run: latestRun.run, forecastHour: 0 });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async setRun(runString) {
|
|
222
|
+
const [date, run] = runString.split(':');
|
|
223
|
+
if (date !== this.state.date || run !== this.state.run) {
|
|
224
|
+
await this.setState({ date, run, forecastHour: 0 });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async setUnits(newUnits) {
|
|
229
|
+
if (newUnits === this.state.units || !['metric', 'imperial'].includes(newUnits)) return;
|
|
230
|
+
await this.setState({ units: newUnits });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async setMRMSVariable(variable) {
|
|
234
|
+
const sortedTimestamps = [...(this.mrmsStatus[variable] || [])].sort((a, b) => b - a);
|
|
235
|
+
const initialTimestamp = sortedTimestamps.length > 0 ? sortedTimestamps[0] : null;
|
|
236
|
+
|
|
237
|
+
await this.setState({
|
|
238
|
+
variable,
|
|
239
|
+
isMRMS: true,
|
|
240
|
+
mrmsTimestamp: initialTimestamp,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async setMRMSTimestamp(timestamp) {
|
|
245
|
+
if (!this.state.isMRMS) return;
|
|
246
|
+
await this.setState({ mrmsTimestamp: timestamp });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async switchMode(options) {
|
|
250
|
+
const { mode, variable, model, forecastHour, mrmsTimestamp } = options;
|
|
251
|
+
if (!mode || !variable) {
|
|
252
|
+
console.error("switchMode requires 'mode' ('mrms' | 'model') and 'variable' properties.");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (mode === 'model' && !model) {
|
|
256
|
+
console.error("switchMode with mode 'model' requires a 'model' property.");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
let targetState = {};
|
|
260
|
+
if (mode === 'mrms') {
|
|
261
|
+
let finalTimestamp = mrmsTimestamp;
|
|
262
|
+
if (finalTimestamp === undefined) {
|
|
263
|
+
const sortedTimestamps = [...(this.mrmsStatus[variable] || [])].sort((a, b) => b - a);
|
|
264
|
+
finalTimestamp = sortedTimestamps.length > 0 ? sortedTimestamps[0] : null;
|
|
265
|
+
}
|
|
266
|
+
targetState = {
|
|
267
|
+
isMRMS: true,
|
|
268
|
+
variable: variable,
|
|
269
|
+
mrmsTimestamp: finalTimestamp,
|
|
270
|
+
model: this.state.model, date: null, run: null, forecastHour: 0,
|
|
271
|
+
};
|
|
272
|
+
} else if (mode === 'model') {
|
|
273
|
+
const latestRun = findLatestModelRun(this.modelStatus, model);
|
|
274
|
+
if (!latestRun) {
|
|
275
|
+
console.error(`Could not find a valid run for model: ${model}`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
targetState = {
|
|
279
|
+
isMRMS: false,
|
|
280
|
+
model: model,
|
|
281
|
+
variable: variable,
|
|
282
|
+
date: latestRun.date,
|
|
283
|
+
run: latestRun.run,
|
|
284
|
+
forecastHour: forecastHour !== undefined ? forecastHour : 0,
|
|
285
|
+
mrmsTimestamp: null,
|
|
286
|
+
};
|
|
287
|
+
} else {
|
|
288
|
+
console.error(`Invalid mode specified in switchMode: '${mode}'`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
await this.setState(targetState);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// --- Data and Calculation Methods ---
|
|
295
|
+
|
|
296
|
+
_reconstructData(decompressedDeltas, encoding) {
|
|
297
|
+
const expectedLength = encoding.length;
|
|
298
|
+
const reconstructedData = new Int8Array(expectedLength);
|
|
299
|
+
|
|
300
|
+
if (decompressedDeltas.length > 0 && expectedLength > 0) {
|
|
301
|
+
// First value is absolute
|
|
302
|
+
reconstructedData[0] = decompressedDeltas[0] > 127 ? decompressedDeltas[0] - 256 : decompressedDeltas[0];
|
|
303
|
+
|
|
304
|
+
// Subsequent values are deltas from the previous one
|
|
305
|
+
for (let i = 1; i < expectedLength; i++) {
|
|
306
|
+
const delta = decompressedDeltas[i] > 127 ? decompressedDeltas[i] - 256 : decompressedDeltas[i];
|
|
307
|
+
reconstructedData[i] = reconstructedData[i - 1] + delta;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Return as a Uint8Array, which is what the rest of the code expects
|
|
311
|
+
return new Uint8Array(reconstructedData.buffer);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async _loadGridData(state) {
|
|
315
|
+
const { model, date, run, forecastHour, variable, isMRMS, mrmsTimestamp } = state;
|
|
316
|
+
let effectiveSmoothing = 0;
|
|
317
|
+
const customVariableSettings = this.customColormaps[variable];
|
|
318
|
+
if (customVariableSettings && typeof customVariableSettings.smoothing === 'number') {
|
|
319
|
+
effectiveSmoothing = customVariableSettings.smoothing;
|
|
320
|
+
}
|
|
321
|
+
let resourcePath;
|
|
322
|
+
let dataUrlIdentifier;
|
|
323
|
+
if (isMRMS) {
|
|
324
|
+
if (!mrmsTimestamp) return null;
|
|
325
|
+
const mrmsDate = new Date(mrmsTimestamp * 1000);
|
|
326
|
+
const y = mrmsDate.getUTCFullYear(), m = (mrmsDate.getUTCMonth() + 1).toString().padStart(2, '0'), d = mrmsDate.getUTCDate().toString().padStart(2, '0');
|
|
327
|
+
dataUrlIdentifier = `mrms-${mrmsTimestamp}-${variable}-${effectiveSmoothing}`;
|
|
328
|
+
resourcePath = `/grids/mrms/${y}${m}${d}/${mrmsTimestamp}/0/${variable}/${effectiveSmoothing}`;
|
|
329
|
+
} else {
|
|
330
|
+
dataUrlIdentifier = `${model}-${date}-${run}-${forecastHour}-${variable}-${effectiveSmoothing}`;
|
|
331
|
+
resourcePath = `/grids/${model}/${date}/${run}/${forecastHour}/${variable}/${effectiveSmoothing}`;
|
|
332
|
+
}
|
|
333
|
+
if (this.dataCache.has(dataUrlIdentifier)) {
|
|
334
|
+
return this.dataCache.get(dataUrlIdentifier);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Create and store abort controller BEFORE creating the promise
|
|
338
|
+
const abortController = new AbortController();
|
|
339
|
+
this.abortControllers.set(dataUrlIdentifier, abortController);
|
|
340
|
+
|
|
341
|
+
const loadPromise = (async () => {
|
|
342
|
+
if (!this.apiKey) {
|
|
343
|
+
throw new Error('API key is not configured.');
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const directUrl = `${this.baseGridUrl}${resourcePath}`;
|
|
347
|
+
|
|
348
|
+
// --- START OF LOGGING CODE ---
|
|
349
|
+
const headers = { 'x-api-key': this.apiKey };
|
|
350
|
+
if (this.bundleId) {
|
|
351
|
+
headers['x-app-identifier'] = this.bundleId;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Use the headers object we just logged
|
|
355
|
+
const response = await fetch(directUrl, {
|
|
356
|
+
headers: headers,
|
|
357
|
+
signal: abortController.signal
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
if (!response.ok) {
|
|
361
|
+
// This is where your error is being thrown from
|
|
362
|
+
throw new Error(`Failed to fetch grid data: ${response.status} ${response.statusText}`);
|
|
363
|
+
}
|
|
364
|
+
const { data: b64Data, encoding } = await response.json();
|
|
365
|
+
const compressedData = Uint8Array.from(atob(b64Data), c => c.charCodeAt(0));
|
|
366
|
+
|
|
367
|
+
let finalData;
|
|
368
|
+
|
|
369
|
+
if (this.isReactNative) {
|
|
370
|
+
const decompressedDeltas = decompress(compressedData);
|
|
371
|
+
finalData = this._reconstructData(decompressedDeltas, encoding);
|
|
372
|
+
} else {
|
|
373
|
+
const requestId = this.workerRequestId++;
|
|
374
|
+
const workerPromise = new Promise((resolve, reject) => {
|
|
375
|
+
this.workerResolvers.set(requestId, { resolve, reject });
|
|
376
|
+
});
|
|
377
|
+
this.worker.postMessage({ requestId, compressedData, encoding }, [compressedData.buffer]);
|
|
378
|
+
const result = await workerPromise;
|
|
379
|
+
finalData = result.data;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const transformedData = new Uint8Array(finalData.length);
|
|
383
|
+
for (let i = 0; i < finalData.length; i++) {
|
|
384
|
+
const signedValue = finalData[i] > 127 ? finalData[i] - 256 : finalData[i];
|
|
385
|
+
transformedData[i] = signedValue + 128;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Clean up abort controller on success
|
|
389
|
+
this.abortControllers.delete(dataUrlIdentifier);
|
|
390
|
+
|
|
391
|
+
return { data: transformedData, encoding };
|
|
392
|
+
|
|
393
|
+
} catch (error) {
|
|
394
|
+
if (error.name === 'AbortError') {
|
|
395
|
+
console.log(`Request cancelled for ${resourcePath}`);
|
|
396
|
+
} else {
|
|
397
|
+
console.error(`Failed to load data for path ${resourcePath}:`, error);
|
|
398
|
+
}
|
|
399
|
+
this.dataCache.delete(dataUrlIdentifier);
|
|
400
|
+
this.abortControllers.delete(dataUrlIdentifier);
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
})();
|
|
404
|
+
this.dataCache.set(dataUrlIdentifier, loadPromise);
|
|
405
|
+
return loadPromise;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
cancelAllRequests() {
|
|
409
|
+
// Abort all in-flight requests
|
|
410
|
+
this.abortControllers.forEach((controller, key) => {
|
|
411
|
+
controller.abort();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Clear both maps
|
|
415
|
+
this.abortControllers.clear();
|
|
416
|
+
this.dataCache.clear();
|
|
417
|
+
|
|
418
|
+
console.log('All pending requests cancelled');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async getValueAtLngLat(lng, lat) {
|
|
422
|
+
const { variable, isMRMS, mrmsTimestamp, model, date, run, forecastHour, units } = this.state;
|
|
423
|
+
if (!variable) return null;
|
|
424
|
+
|
|
425
|
+
const gridIndices = this._getGridIndexFromLngLat(lng, lat);
|
|
426
|
+
if (!gridIndices) return null;
|
|
427
|
+
|
|
428
|
+
const { i, j } = gridIndices;
|
|
429
|
+
const gridModel = isMRMS ? 'mrms' : model;
|
|
430
|
+
const normalizedGridModel = this._normalizeModelName(gridModel);
|
|
431
|
+
const { nx } = COORDINATE_CONFIGS[normalizedGridModel].grid_params;
|
|
432
|
+
|
|
433
|
+
const customSettings = this.customColormaps[variable];
|
|
434
|
+
const effectiveSmoothing = (customSettings && typeof customSettings.smoothing === 'number') ? customSettings.smoothing : 0;
|
|
435
|
+
|
|
436
|
+
const dataUrlIdentifier = isMRMS
|
|
437
|
+
? `mrms-${mrmsTimestamp}-${variable}-${effectiveSmoothing}`
|
|
438
|
+
: `${model}-${date}-${run}-${forecastHour}-${variable}-${effectiveSmoothing}`;
|
|
439
|
+
|
|
440
|
+
const gridDataPromise = this.dataCache.get(dataUrlIdentifier);
|
|
441
|
+
if (!gridDataPromise) return null;
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const gridData = await gridDataPromise;
|
|
445
|
+
if (!gridData || !gridData.data) return null;
|
|
446
|
+
|
|
447
|
+
const index1D = j * nx + i;
|
|
448
|
+
const byteValue = gridData.data[index1D];
|
|
449
|
+
const signedQuantizedValue = byteValue - 128;
|
|
450
|
+
const { scale, offset, missing_quantized } = gridData.encoding;
|
|
451
|
+
|
|
452
|
+
if (signedQuantizedValue === missing_quantized) return null;
|
|
453
|
+
|
|
454
|
+
const nativeValue = signedQuantizedValue * scale + offset;
|
|
455
|
+
const { baseUnit } = this._getColormapForVariable(variable);
|
|
456
|
+
let dataNativeUnit = baseUnit || (DICTIONARIES.fld[variable] || {}).defaultUnit || 'none';
|
|
457
|
+
|
|
458
|
+
const displayUnit = this._getTargetUnit(dataNativeUnit, units);
|
|
459
|
+
const conversionFunc = getUnitConversionFunction(dataNativeUnit, displayUnit);
|
|
460
|
+
const displayValue = conversionFunc ? conversionFunc(nativeValue) : nativeValue;
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
lngLat: { lng, lat },
|
|
464
|
+
variable: {
|
|
465
|
+
code: variable,
|
|
466
|
+
name: this.getVariableDisplayName(variable),
|
|
467
|
+
},
|
|
468
|
+
value: displayValue,
|
|
469
|
+
unit: displayUnit,
|
|
470
|
+
};
|
|
471
|
+
} catch (error) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
getAvailableVariables(modelName = null) {
|
|
477
|
+
const model = modelName || this.state.model;
|
|
478
|
+
return MODEL_CONFIGS[model]?.vars || [];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
getVariableDisplayName(variableCode) {
|
|
482
|
+
const varInfo = DICTIONARIES.fld[variableCode];
|
|
483
|
+
return varInfo?.displayName || varInfo?.name || variableCode;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
_getColormapForVariable(variable) {
|
|
487
|
+
if (!variable) return { colormap: [], baseUnit: '' };
|
|
488
|
+
if (this.customColormaps[variable] && this.customColormaps[variable].colormap) {
|
|
489
|
+
return {
|
|
490
|
+
colormap: this.customColormaps[variable].colormap,
|
|
491
|
+
baseUnit: this.customColormaps[variable].baseUnit || ''
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const colormapKey = DICTIONARIES.variable_cmap[variable] || variable;
|
|
495
|
+
const customColormap = this.customColormaps[colormapKey];
|
|
496
|
+
if (customColormap && customColormap.colormap) {
|
|
497
|
+
return { colormap: customColormap.colormap, baseUnit: customColormap.baseUnit || '' };
|
|
498
|
+
}
|
|
499
|
+
const defaultColormapData = DEFAULT_COLORMAPS[colormapKey];
|
|
500
|
+
if (defaultColormapData && defaultColormapData.units) {
|
|
501
|
+
const availableUnits = Object.keys(defaultColormapData.units);
|
|
502
|
+
if (availableUnits.length > 0) {
|
|
503
|
+
const baseUnit = availableUnits[0];
|
|
504
|
+
const unitData = defaultColormapData.units[baseUnit];
|
|
505
|
+
if (unitData && unitData.colormap) {
|
|
506
|
+
return { colormap: unitData.colormap, baseUnit: baseUnit };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return { colormap: [], baseUnit: '' };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
_convertColormapUnits(colormap, fromUnits, toUnits) {
|
|
514
|
+
if (fromUnits === toUnits) return colormap;
|
|
515
|
+
const conversionFunc = getUnitConversionFunction(fromUnits, toUnits);
|
|
516
|
+
if (!conversionFunc) return colormap;
|
|
517
|
+
const newColormap = [];
|
|
518
|
+
for (let i = 0; i < colormap.length; i += 2) {
|
|
519
|
+
newColormap.push(conversionFunc(colormap[i]), colormap[i + 1]);
|
|
520
|
+
}
|
|
521
|
+
return newColormap;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
_normalizeModelName(modelName) {
|
|
525
|
+
const mapping = { 'hrrr': ['mpashn', 'mpasrt', 'mpasht', 'hrrrsub', 'rrfs', 'namnest', 'mpasrn', 'mpasrn3', 'mpasht2'], 'arw': ['arw2', 'fv3', 'href'], 'rtma': ['nbm'], 'ecmwf': ['ecmwfaifs'], 'gfs': ['arpege', 'graphcastgfs'] };
|
|
526
|
+
for (const [normalized, aliases] of Object.entries(mapping)) { if (aliases.includes(modelName)) return normalized; }
|
|
527
|
+
return modelName;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
_getGridCornersAndDef(model) {
|
|
531
|
+
const normalizedModel = this._normalizeModelName(model);
|
|
532
|
+
const gridDef = { ...COORDINATE_CONFIGS[normalizedModel], modelName: model };
|
|
533
|
+
if (!gridDef) return null;
|
|
534
|
+
const { nx, ny } = gridDef.grid_params;
|
|
535
|
+
const gridType = gridDef.type;
|
|
536
|
+
let corners;
|
|
537
|
+
if (gridType === 'latlon') {
|
|
538
|
+
let { lon_first, lat_first, lat_last, lon_last, dx_degrees, dy_degrees } = gridDef.grid_params;
|
|
539
|
+
corners = {
|
|
540
|
+
lon_tl: lon_first, lat_tl: lat_first,
|
|
541
|
+
lon_tr: lon_last !== undefined ? lon_last : (lon_first + (nx - 1) * dx_degrees), lat_tr: lat_first,
|
|
542
|
+
lon_bl: lon_first, lat_bl: lat_last !== undefined ? lat_last : (lat_first + (ny - 1) * dy_degrees),
|
|
543
|
+
lon_br: lon_last !== undefined ? lon_last : (lon_first + (nx - 1) * dx_degrees), lat_br: lat_last !== undefined ? lat_last : (lat_first + (ny - 1) * dy_degrees),
|
|
544
|
+
};
|
|
545
|
+
} else if (gridType === 'rotated_latlon') {
|
|
546
|
+
const [lon_tl, lat_tl] = hrdpsObliqueTransform(gridDef.grid_params.lon_first, gridDef.grid_params.lat_first);
|
|
547
|
+
const [lon_tr, lat_tr] = hrdpsObliqueTransform(gridDef.grid_params.lon_first + (nx - 1) * gridDef.grid_params.dx_degrees, gridDef.grid_params.lat_first);
|
|
548
|
+
const [lon_bl, lat_bl] = hrdpsObliqueTransform(gridDef.grid_params.lon_first, gridDef.grid_params.lat_first + (ny - 1) * gridDef.grid_params.dy_degrees);
|
|
549
|
+
const [lon_br, lat_br] = hrdpsObliqueTransform(gridDef.grid_params.lon_first + (nx - 1) * gridDef.grid_params.dx_degrees, gridDef.grid_params.lat_first + (ny - 1) * gridDef.grid_params.dy_degrees);
|
|
550
|
+
corners = { lon_tl, lat_tl, lon_tr, lat_tr, lon_bl, lat_bl, lon_br, lat_br };
|
|
551
|
+
} else if (gridType === 'lambert_conformal_conic' || gridType === 'polar_ stereographic') {
|
|
552
|
+
let projString = Object.entries(gridDef.proj_params).map(([k,v]) => `+${k}=${v}`).join(' ');
|
|
553
|
+
if(gridType === 'polar_stereographic') projString += ' +lat_0=90';
|
|
554
|
+
const { x_origin, y_origin, dx, dy } = gridDef.grid_params;
|
|
555
|
+
const [lon_tl, lat_tl] = proj4(projString, 'EPSG:4326', [x_origin, y_origin]);
|
|
556
|
+
const [lon_tr, lat_tr] = proj4(projString, 'EPSG:4326', [x_origin + (nx - 1) * dx, y_origin]);
|
|
557
|
+
const [lon_bl, lat_bl] = proj4(projString, 'EPSG:4326', [x_origin, y_origin + (ny - 1) * dy]);
|
|
558
|
+
const [lon_br, lat_br] = proj4(projString, 'EPSG:4326', [x_origin + (nx - 1) * dx, y_origin + (ny - 1) * dy]);
|
|
559
|
+
corners = { lon_tl, lat_tl, lon_tr, lat_tr, lon_bl, lat_bl, lon_br, lat_br };
|
|
560
|
+
} else {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
return { corners, gridDef };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
_getTargetUnit(defaultUnit, system) {
|
|
567
|
+
if (system === 'metric') {
|
|
568
|
+
if (['°F', '°C'].includes(defaultUnit)) return 'celsius';
|
|
569
|
+
if (['kts', 'mph', 'm/s'].includes(defaultUnit)) return 'km/h';
|
|
570
|
+
if (['in', 'mm', 'cm'].includes(defaultUnit)) return 'mm';
|
|
571
|
+
}
|
|
572
|
+
if (['°F', '°C'].includes(defaultUnit)) return 'fahrenheit';
|
|
573
|
+
if (['kts', 'mph', 'm/s'].includes(defaultUnit)) return 'mph';
|
|
574
|
+
if (['in', 'mm', 'cm'].includes(defaultUnit)) return 'in';
|
|
575
|
+
return defaultUnit;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
_getGridIndexFromLngLat(lng, lat) {
|
|
579
|
+
const gridModel = this.state.isMRMS ? 'mrms' : this.state.model;
|
|
580
|
+
const normalizedGridModel = this._normalizeModelName(gridModel);
|
|
581
|
+
const gridDef = COORDINATE_CONFIGS[normalizedGridModel];
|
|
582
|
+
if (!gridDef) return null;
|
|
583
|
+
const { nx, ny } = gridDef.grid_params;
|
|
584
|
+
const pixelCoords = this.latLonToGridPixel(lat, lng, gridDef, gridModel);
|
|
585
|
+
if (!pixelCoords || !isFinite(pixelCoords.x) || !isFinite(pixelCoords.y) || pixelCoords.x < 0 || pixelCoords.y < 0) return null;
|
|
586
|
+
const i = Math.round(pixelCoords.x);
|
|
587
|
+
const j = Math.round(pixelCoords.y);
|
|
588
|
+
if (i >= 0 && i < nx && j >= 0 && j < ny) return { i, j };
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
latLonToProjected(lat, lon, gridDef) {
|
|
593
|
+
if (!isFinite(lat) || !isFinite(lon)) throw new Error(`Invalid coordinates: lat=${lat}, lon=${lon}`);
|
|
594
|
+
const gridType = gridDef.type;
|
|
595
|
+
if (gridType === 'latlon') return { x: lon, y: lat };
|
|
596
|
+
let projString = Object.entries(gridDef.proj_params).map(([k,v]) => `+${k}=${v}`).join(' ');
|
|
597
|
+
if(gridType === 'polar_stereographic') projString += ' +lat_0=90';
|
|
598
|
+
const projected = proj4('EPSG:4326', projString, [lon, lat]);
|
|
599
|
+
return { x: projected[0], y: projected[1] };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
latLonToGridPixel(lat, lon, gridDef, modelName) {
|
|
603
|
+
if (!gridDef) return null;
|
|
604
|
+
if (modelName === 'rgem' && gridDef.type === 'polar_stereographic') return this.latLonToGridPixelPolarStereographic(lat, lon, gridDef);
|
|
605
|
+
const projected = this.latLonToProjected(lat, lon, gridDef);
|
|
606
|
+
let x, y;
|
|
607
|
+
const gridOrigin = { x: gridDef.grid_params.lon_first, y: gridDef.grid_params.lat_first };
|
|
608
|
+
const gridPixelSize = { x: gridDef.grid_params.dx_degrees, y: gridDef.grid_params.dy_degrees };
|
|
609
|
+
if (gridDef.type === 'latlon' || gridDef.type === 'rotated_latlon') {
|
|
610
|
+
let adjustedLon = projected.x;
|
|
611
|
+
if (modelName === 'mrms') {
|
|
612
|
+
if (adjustedLon < 0) adjustedLon += 360;
|
|
613
|
+
x = (adjustedLon - gridOrigin.x) / gridPixelSize.x;
|
|
614
|
+
y = (gridOrigin.y - projected.y) / gridPixelSize.y;
|
|
615
|
+
} else {
|
|
616
|
+
const isGFSType = gridDef.grid_params && gridDef.grid_params.lon_first === 0.0 && Math.abs(gridDef.grid_params.lat_first) === 90.0;
|
|
617
|
+
const isECMWFType = gridDef.grid_params && gridDef.grid_params.lon_first === 180.0 && gridDef.grid_params.lat_first === 90.0;
|
|
618
|
+
const isGEMType = modelName === 'gem' || (gridDef.grid_params.lon_first === 180.0 && gridDef.grid_params.lat_first === -90.0 && gridDef.grid_params.lon_last === 179.85);
|
|
619
|
+
if (isGEMType) {
|
|
620
|
+
while (adjustedLon < gridOrigin.x) adjustedLon += 360;
|
|
621
|
+
x = (adjustedLon - gridOrigin.x) / gridPixelSize.x;
|
|
622
|
+
y = (projected.y - gridOrigin.y) / gridPixelSize.y;
|
|
623
|
+
return { x, y };
|
|
624
|
+
}
|
|
625
|
+
let isFlippedGrid = isECMWFType ? true : (gridDef.grid_params.lat_first < (gridDef.grid_params.lat_last || (gridDef.grid_params.ny - 1) * gridDef.grid_params.dy_degrees));
|
|
626
|
+
if (isGFSType) adjustedLon = projected.x + 180;
|
|
627
|
+
else if (isECMWFType) { if (adjustedLon < gridOrigin.x) adjustedLon += 360; }
|
|
628
|
+
else if (['arome1', 'arome25', 'arpegeeu', 'iconeu', 'icond2'].includes(modelName)) {
|
|
629
|
+
while (adjustedLon < 0) adjustedLon += 360;
|
|
630
|
+
while (adjustedLon >= 360) adjustedLon -= 360;
|
|
631
|
+
x = (adjustedLon >= gridOrigin.x) ? (adjustedLon - gridOrigin.x) / gridPixelSize.x : (adjustedLon + 360 - gridOrigin.x) / gridPixelSize.x;
|
|
632
|
+
if (['arome1', 'arome25', 'arpegeeu'].includes(modelName)) y = (gridOrigin.y - projected.y) / Math.abs(gridPixelSize.y);
|
|
633
|
+
else if (['iconeu', 'icond2'].includes(modelName)) y = (projected.y - gridOrigin.y) / gridPixelSize.y;
|
|
634
|
+
return { x, y };
|
|
635
|
+
} else {
|
|
636
|
+
const lonFirst = gridOrigin.x;
|
|
637
|
+
if (lonFirst > 180 && adjustedLon < 0) adjustedLon += 360;
|
|
638
|
+
else if (lonFirst < 0 && adjustedLon > 180) adjustedLon -= 360;
|
|
639
|
+
}
|
|
640
|
+
x = (adjustedLon - gridOrigin.x) / gridPixelSize.x;
|
|
641
|
+
if (isFlippedGrid) {
|
|
642
|
+
if (isECMWFType) y = (gridOrigin.y - projected.y) / Math.abs(gridPixelSize.y);
|
|
643
|
+
else {
|
|
644
|
+
const maxLat = gridDef.grid_params.lat_last || (gridDef.grid_params.ny - 1) * gridDef.grid_params.dy_degrees;
|
|
645
|
+
y = (maxLat - projected.y) / Math.abs(gridPixelSize.y);
|
|
646
|
+
}
|
|
647
|
+
} else y = (projected.y - gridOrigin.y) / gridPixelSize.y;
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
const projOrigin = { x: gridDef.grid_params.x_origin, y: gridDef.grid_params.y_origin };
|
|
651
|
+
const projPixelSize = { x: gridDef.grid_params.dx, y: gridDef.grid_params.dy };
|
|
652
|
+
x = (projected.x - projOrigin.x) / projPixelSize.x;
|
|
653
|
+
y = (projOrigin.y - projected.y) / Math.abs(projPixelSize.y);
|
|
654
|
+
}
|
|
655
|
+
return { x, y };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
latLonToGridPixelPolarStereographic(lat, lon, gridDef) {
|
|
659
|
+
try {
|
|
660
|
+
const projParams = gridDef.proj_params;
|
|
661
|
+
let projectionString = `+proj=${projParams.proj}`;
|
|
662
|
+
Object.keys(projParams).forEach(key => { if (key !== 'proj') projectionString += ` +${key}=${projParams[key]}`; });
|
|
663
|
+
projectionString += ' +lat_0=90 +no_defs';
|
|
664
|
+
const { nx, ny, dx, dy, x_origin, y_origin } = gridDef.grid_params;
|
|
665
|
+
const x_min = x_origin;
|
|
666
|
+
const x_max = x_origin + (nx - 1) * dx;
|
|
667
|
+
const y_max = y_origin;
|
|
668
|
+
const y_min = y_origin + (ny - 1) * dy;
|
|
669
|
+
const [proj_x, proj_y] = proj4('EPSG:4326', projectionString, [lon, lat]);
|
|
670
|
+
if (!isFinite(proj_x) || !isFinite(proj_y)) return { x: -1, y: -1 };
|
|
671
|
+
const t_x = (proj_x - x_min) / (x_max - x_min);
|
|
672
|
+
const t_y = (proj_y - y_max) / (y_min - y_max);
|
|
673
|
+
const x = t_x * (nx - 1);
|
|
674
|
+
const y = t_y * (ny - 1);
|
|
675
|
+
return { x, y };
|
|
676
|
+
} catch (error) {
|
|
677
|
+
console.warn(`[GridAccessor] RGEM polar stereographic conversion failed for ${lat}, ${lon}:`, error);
|
|
678
|
+
return { x: -1, y: -1 };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// --- Worker and Status Methods ---
|
|
683
|
+
|
|
684
|
+
createWorker() {
|
|
685
|
+
if (this.isReactNative) return null;
|
|
686
|
+
|
|
687
|
+
const workerCode = `
|
|
688
|
+
import { decompress } from 'https://cdn.skypack.dev/fzstd@0.1.1';
|
|
689
|
+
|
|
690
|
+
function _reconstructData(decompressedDeltas, encoding) {
|
|
691
|
+
const expectedLength = encoding.length;
|
|
692
|
+
const reconstructedData = new Int8Array(expectedLength);
|
|
693
|
+
if (decompressedDeltas.length > 0 && expectedLength > 0) {
|
|
694
|
+
reconstructedData[0] = decompressedDeltas[0] > 127 ? decompressedDeltas[0] - 256 : decompressedDeltas[0];
|
|
695
|
+
for (let i = 1; i < expectedLength; i++) {
|
|
696
|
+
const delta = decompressedDeltas[i] > 127 ? decompressedDeltas[i] - 256 : decompressedDeltas[i];
|
|
697
|
+
reconstructedData[i] = reconstructedData[i - 1] + delta;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return new Uint8Array(reconstructedData.buffer);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
self.onmessage = async (e) => {
|
|
704
|
+
const { requestId, compressedData, encoding } = e.data;
|
|
705
|
+
try {
|
|
706
|
+
const decompressedDeltas = await decompress(compressedData);
|
|
707
|
+
const finalData = _reconstructData(decompressedDeltas, encoding);
|
|
708
|
+
self.postMessage({ success: true, requestId: requestId, decompressedData: finalData, encoding: encoding }, [finalData.buffer]);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
self.postMessage({ success: false, requestId: requestId, error: error.message });
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
`;
|
|
714
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
715
|
+
return new Worker(URL.createObjectURL(blob), { type: 'module' });
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
_processResultQueue() {
|
|
719
|
+
while (this.resultQueue.length > 0) {
|
|
720
|
+
const { success, requestId, decompressedData, encoding, error } = this.resultQueue.shift();
|
|
721
|
+
if (this.workerResolvers.has(requestId)) {
|
|
722
|
+
const { resolve, reject } = this.workerResolvers.get(requestId);
|
|
723
|
+
if (success) {
|
|
724
|
+
resolve({ data: decompressedData }); // Return as { data: ... }
|
|
725
|
+
} else {
|
|
726
|
+
reject(new Error(error));
|
|
727
|
+
}
|
|
728
|
+
this.workerResolvers.delete(requestId);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
this.isProcessingQueue = false;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
_handleWorkerMessage(e) {
|
|
735
|
+
if (this.isReactNative) return;
|
|
736
|
+
|
|
737
|
+
const { success, requestId, decompressedData, encoding, error } = e.data;
|
|
738
|
+
|
|
739
|
+
this.resultQueue.push({ success, requestId, decompressedData, encoding, error });
|
|
740
|
+
if (!this.isProcessingQueue) {
|
|
741
|
+
this.isProcessingQueue = true;
|
|
742
|
+
requestAnimationFrame(() => this._processResultQueue());
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async fetchModelStatus(force = false) {
|
|
747
|
+
if (!this.modelStatus || force) {
|
|
748
|
+
try {
|
|
749
|
+
const response = await fetch(this.statusUrl);
|
|
750
|
+
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
|
751
|
+
this.modelStatus = (await response.json()).models;
|
|
752
|
+
} catch (error) { this.modelStatus = null; }
|
|
753
|
+
}
|
|
754
|
+
return this.modelStatus;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async fetchMRMSStatus(force = false) {
|
|
758
|
+
const mrmsStatusUrl = 'https://h3dfvh5pq6euq36ymlpz4zqiha0obqju.lambda-url.us-east-2.on.aws';
|
|
759
|
+
if (!this.mrmsStatus || force) {
|
|
760
|
+
try {
|
|
761
|
+
const response = await fetch(mrmsStatusUrl);
|
|
762
|
+
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
|
763
|
+
this.mrmsStatus = await response.json();
|
|
764
|
+
} catch (error) { this.mrmsStatus = null; }
|
|
765
|
+
}
|
|
766
|
+
return this.mrmsStatus;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
startAutoRefresh(intervalSeconds) {
|
|
770
|
+
this.stopAutoRefresh();
|
|
771
|
+
this.autoRefreshIntervalId = setInterval(async () => {
|
|
772
|
+
await this.fetchModelStatus(true);
|
|
773
|
+
this._emitStateChange();
|
|
774
|
+
}, (intervalSeconds || 60) * 1000);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
stopAutoRefresh() {
|
|
778
|
+
if (this.autoRefreshIntervalId) {
|
|
779
|
+
clearInterval(this.autoRefreshIntervalId);
|
|
780
|
+
this.autoRefreshIntervalId = null;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|