@aguacerowx/javascript-sdk 0.0.18 → 0.0.20
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/package.json +13 -2
- package/src/AguaceroCore.js +820 -172
- package/src/default-colormaps.js +86 -0
- package/src/dictionaries.js +73 -0
- package/src/getBundleId.js +8 -22
- package/src/getBundleId.native.js +14 -0
- package/src/gridDecodePipeline.js +32 -0
- package/src/gridDecodeWorker.js +24 -0
- package/src/index.js +22 -1
- package/src/nexradTilts.js +128 -0
- package/src/nexrad_level3_catalog.js +26 -0
- package/src/nexrad_support.js +276 -0
- package/src/satellite_support.js +305 -0
package/src/AguaceroCore.js
CHANGED
|
@@ -7,7 +7,31 @@ import { getUnitConversionFunction } from './unitConversions.js';
|
|
|
7
7
|
import { DICTIONARIES, MODEL_CONFIGS } from './dictionaries.js';
|
|
8
8
|
import { DEFAULT_COLORMAPS } from './default-colormaps.js';
|
|
9
9
|
import proj4 from 'proj4';
|
|
10
|
+
import { processCompressedGrid } from './gridDecodePipeline.js';
|
|
10
11
|
import { getBundleId } from './getBundleId';
|
|
12
|
+
import {
|
|
13
|
+
SATELLITE_FRAMES_URL,
|
|
14
|
+
buildSatelliteTimelineForKey,
|
|
15
|
+
SATELLITE_DURATION_CONFIG,
|
|
16
|
+
TIMELINE_DURATION_HOUR_VALUES,
|
|
17
|
+
normalizeTimelineDurationValue,
|
|
18
|
+
parseTimelineDurationHours,
|
|
19
|
+
} from './satellite_support.js';
|
|
20
|
+
import {
|
|
21
|
+
fetchNexradTimesListing,
|
|
22
|
+
nexradColormapFldKey,
|
|
23
|
+
variableToNexradGroup,
|
|
24
|
+
getAvailableNexradTilts,
|
|
25
|
+
} from './nexrad_support.js';
|
|
26
|
+
import { NEXRAD_LEVEL3_ELEV, getNexradLevel3EntryByRadarKey } from './nexrad_level3_catalog.js';
|
|
27
|
+
import {
|
|
28
|
+
setRadarTiltsManifest,
|
|
29
|
+
fetchRadarTiltsManifestFromNetwork,
|
|
30
|
+
getDefaultRadarTilt,
|
|
31
|
+
formatTiltForApi,
|
|
32
|
+
clampNexradTiltForVariable,
|
|
33
|
+
getRadarTilts,
|
|
34
|
+
} from './nexradTilts.js';
|
|
11
35
|
|
|
12
36
|
// --- Non-UI Helper Functions ---
|
|
13
37
|
function hrdpsObliqueTransform(rotated_lon, rotated_lat) {
|
|
@@ -41,26 +65,67 @@ function findLatestModelRun(modelsData, modelName) {
|
|
|
41
65
|
return null;
|
|
42
66
|
}
|
|
43
67
|
|
|
68
|
+
/**
|
|
69
|
+
* model-status JSON uses string keys for runs (often zero-padded: "00", "06").
|
|
70
|
+
* Direct lookup modelStatus[model][date][run] fails if state.run is "6" but the key is "06".
|
|
71
|
+
* Returns the hour list and the run key that matched.
|
|
72
|
+
*/
|
|
73
|
+
function resolveModelRunHours(modelStatus, model, date, run) {
|
|
74
|
+
const runs = modelStatus?.[model]?.[date];
|
|
75
|
+
if (!runs || run == null || run === '') {
|
|
76
|
+
return {
|
|
77
|
+
hours: [],
|
|
78
|
+
matchedRunKey: null,
|
|
79
|
+
availableRunKeys: runs ? Object.keys(runs) : [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const runStr = String(run);
|
|
83
|
+
const candidates = new Set([runStr]);
|
|
84
|
+
const n = parseInt(runStr, 10);
|
|
85
|
+
if (!Number.isNaN(n)) {
|
|
86
|
+
candidates.add(String(n));
|
|
87
|
+
candidates.add(String(n).padStart(2, '0'));
|
|
88
|
+
candidates.add(String(n).padStart(3, '0'));
|
|
89
|
+
}
|
|
90
|
+
for (const key of candidates) {
|
|
91
|
+
const h = runs[key];
|
|
92
|
+
if (h && Array.isArray(h) && h.length > 0) {
|
|
93
|
+
return { hours: h, matchedRunKey: key, availableRunKeys: Object.keys(runs) };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!Number.isNaN(n)) {
|
|
97
|
+
for (const k of Object.keys(runs)) {
|
|
98
|
+
const kn = parseInt(k, 10);
|
|
99
|
+
if (!Number.isNaN(kn) && kn === n) {
|
|
100
|
+
const h = runs[k];
|
|
101
|
+
if (h && Array.isArray(h) && h.length > 0) {
|
|
102
|
+
return { hours: h, matchedRunKey: k, availableRunKeys: Object.keys(runs) };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { hours: [], matchedRunKey: null, availableRunKeys: Object.keys(runs) };
|
|
108
|
+
}
|
|
109
|
+
|
|
44
110
|
export class AguaceroCore extends EventEmitter {
|
|
45
111
|
constructor(options = {}) {
|
|
46
112
|
super();
|
|
47
113
|
this.isReactNative = typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
48
114
|
this.apiKey = options.apiKey;
|
|
115
|
+
/** Passed as CloudFront `userId` for satellite KTX2 URLs (production uses the authenticated account id). */
|
|
116
|
+
this.userId = options.userId ?? 'sdk-user';
|
|
49
117
|
this.bundleId = getBundleId();
|
|
50
118
|
this.baseGridUrl = 'https://d3dc62msmxkrd7.cloudfront.net';
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.resultQueue = [];
|
|
57
|
-
this.isProcessingQueue = false;
|
|
58
|
-
} else {
|
|
59
|
-
this.worker = null;
|
|
60
|
-
}
|
|
119
|
+
/** @type {Worker | null} */
|
|
120
|
+
this._gridDecodeWorker = null;
|
|
121
|
+
/** When true, skip Worker and use {@link processCompressedGrid} on the main thread. */
|
|
122
|
+
this._gridDecodeWorkerDisabled = false;
|
|
123
|
+
this._gridDecodeMsgId = 0;
|
|
61
124
|
this.statusUrl = 'https://d3dc62msmxkrd7.cloudfront.net/model-status';
|
|
62
125
|
this.modelStatus = null;
|
|
63
126
|
this.mrmsStatus = null;
|
|
127
|
+
/** @type {{ objects?: Array<{ key: string }> } | null} */
|
|
128
|
+
this.satelliteListing = null;
|
|
64
129
|
this.dataCache = new Map();
|
|
65
130
|
this.abortControllers = new Map();
|
|
66
131
|
this.isPlaying = false;
|
|
@@ -73,31 +138,213 @@ export class AguaceroCore extends EventEmitter {
|
|
|
73
138
|
const initialMode = userLayerOptions.mode || 'model';
|
|
74
139
|
const initialVariable = userLayerOptions.variable || null;
|
|
75
140
|
|
|
141
|
+
const initialSatellite = initialMode === 'satellite';
|
|
142
|
+
const initialNexrad = initialMode === 'nexrad';
|
|
143
|
+
const initialNexradDs = userLayerOptions.nexradDataSource || 'level2';
|
|
144
|
+
const initialNexradProd = userLayerOptions.nexradProduct || 'REF';
|
|
145
|
+
const initialNexradFld = initialNexrad
|
|
146
|
+
? nexradColormapFldKey(initialNexradDs, initialNexradProd)
|
|
147
|
+
: initialVariable;
|
|
76
148
|
this.state = {
|
|
77
149
|
model: userLayerOptions.model || 'gfs',
|
|
78
150
|
// EDIT: Set isMRMS based on the initial mode
|
|
79
|
-
isMRMS: initialMode === 'mrms',
|
|
151
|
+
isMRMS: initialMode === 'mrms' && !initialSatellite && !initialNexrad,
|
|
80
152
|
mrmsTimestamp: null,
|
|
81
|
-
variable: initialVariable,
|
|
153
|
+
variable: initialNexrad ? initialNexradFld : initialVariable,
|
|
82
154
|
date: null,
|
|
83
155
|
run: null,
|
|
84
156
|
forecastHour: 0,
|
|
85
157
|
visible: true,
|
|
86
158
|
opacity: userLayerOptions.opacity ?? 1,
|
|
87
159
|
units: options.initialUnit || 'imperial',
|
|
88
|
-
shaderSmoothingEnabled: options.shaderSmoothingEnabled ?? true
|
|
160
|
+
shaderSmoothingEnabled: options.shaderSmoothingEnabled ?? true,
|
|
161
|
+
isSatellite: initialSatellite,
|
|
162
|
+
satelliteKey: userLayerOptions.satelliteKey || null,
|
|
163
|
+
satelliteTimestamp: userLayerOptions.satelliteTimestamp != null ? Number(userLayerOptions.satelliteTimestamp) : null,
|
|
164
|
+
satelliteTier: userLayerOptions.satelliteTier || 'basic',
|
|
165
|
+
satelliteDurationValue: (() => {
|
|
166
|
+
const n = normalizeTimelineDurationValue(
|
|
167
|
+
userLayerOptions.satelliteDurationValue != null ? userLayerOptions.satelliteDurationValue : '1'
|
|
168
|
+
);
|
|
169
|
+
return TIMELINE_DURATION_HOUR_VALUES.includes(n) ? n : '1';
|
|
170
|
+
})(),
|
|
171
|
+
mrmsDurationValue: (() => {
|
|
172
|
+
const n = normalizeTimelineDurationValue(
|
|
173
|
+
userLayerOptions.mrmsDurationValue != null ? userLayerOptions.mrmsDurationValue : '1'
|
|
174
|
+
);
|
|
175
|
+
return TIMELINE_DURATION_HOUR_VALUES.includes(n) ? n : '1';
|
|
176
|
+
})(),
|
|
177
|
+
isNexrad: initialNexrad,
|
|
178
|
+
nexradSite: userLayerOptions.nexradSite ?? null,
|
|
179
|
+
nexradDataSource: initialNexradDs,
|
|
180
|
+
nexradProduct: initialNexradProd,
|
|
181
|
+
nexradTilt:
|
|
182
|
+
userLayerOptions.nexradTilt != null
|
|
183
|
+
? Number(userLayerOptions.nexradTilt)
|
|
184
|
+
: userLayerOptions.nexradSite
|
|
185
|
+
? getDefaultRadarTilt(userLayerOptions.nexradSite)
|
|
186
|
+
: null,
|
|
187
|
+
nexradTimestamp: userLayerOptions.nexradTimestamp != null ? Number(userLayerOptions.nexradTimestamp) : null,
|
|
188
|
+
nexradStormRelative: userLayerOptions.nexradStormRelative === true,
|
|
189
|
+
/** When true, mapsgl shows clickable NEXRAD site markers (independent of selected site). */
|
|
190
|
+
nexradShowSitesPicker: userLayerOptions.nexradShowSitesPicker !== false,
|
|
89
191
|
};
|
|
90
192
|
|
|
91
193
|
this.autoRefreshEnabled = options.autoRefresh ?? false;
|
|
92
194
|
this.autoRefreshIntervalSeconds = options.autoRefreshInterval ?? 60;
|
|
93
195
|
this.autoRefreshIntervalId = null;
|
|
196
|
+
|
|
197
|
+
/** @type {Record<string, { unixTimes?: number[]; timeToKeyMap?: Record<string, string>; listWindowHours?: number }>} */
|
|
198
|
+
this.nexradTimesByStation = {};
|
|
94
199
|
}
|
|
95
200
|
|
|
96
201
|
async setState(newState) {
|
|
97
|
-
|
|
202
|
+
const patch = { ...newState };
|
|
203
|
+
if ('forecastHour' in patch && patch.forecastHour != null) {
|
|
204
|
+
patch.forecastHour = Number(patch.forecastHour);
|
|
205
|
+
}
|
|
206
|
+
if ('mrmsTimestamp' in patch && patch.mrmsTimestamp != null) {
|
|
207
|
+
patch.mrmsTimestamp = Number(patch.mrmsTimestamp);
|
|
208
|
+
}
|
|
209
|
+
if ('satelliteTimestamp' in patch && patch.satelliteTimestamp != null) {
|
|
210
|
+
patch.satelliteTimestamp = Number(patch.satelliteTimestamp);
|
|
211
|
+
}
|
|
212
|
+
if ('satelliteDurationValue' in patch && patch.satelliteDurationValue != null) {
|
|
213
|
+
const n = normalizeTimelineDurationValue(patch.satelliteDurationValue);
|
|
214
|
+
patch.satelliteDurationValue = TIMELINE_DURATION_HOUR_VALUES.includes(n) ? n : '1';
|
|
215
|
+
}
|
|
216
|
+
if ('mrmsDurationValue' in patch && patch.mrmsDurationValue != null) {
|
|
217
|
+
const n = normalizeTimelineDurationValue(patch.mrmsDurationValue);
|
|
218
|
+
patch.mrmsDurationValue = TIMELINE_DURATION_HOUR_VALUES.includes(n) ? n : '1';
|
|
219
|
+
}
|
|
220
|
+
if ('nexradTimestamp' in patch && patch.nexradTimestamp != null) {
|
|
221
|
+
patch.nexradTimestamp = Number(patch.nexradTimestamp);
|
|
222
|
+
}
|
|
223
|
+
if ('nexradTilt' in patch && patch.nexradTilt != null) {
|
|
224
|
+
patch.nexradTilt = Number(patch.nexradTilt);
|
|
225
|
+
}
|
|
226
|
+
Object.assign(this.state, patch);
|
|
98
227
|
this._emitStateChange();
|
|
99
228
|
}
|
|
100
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Forecast hours for the current model/date/run (normalized numbers).
|
|
232
|
+
* Tolerates model-status run keys like "06" vs state.run "6" so preload and slider stay in sync.
|
|
233
|
+
*/
|
|
234
|
+
getAvailableForecastHours() {
|
|
235
|
+
if (this.state.isMRMS || this.state.isSatellite || this.state.isNexrad) return [];
|
|
236
|
+
if (!this.state.model || this.state.date == null || this.state.run == null) return [];
|
|
237
|
+
|
|
238
|
+
const resolved = resolveModelRunHours(
|
|
239
|
+
this.modelStatus,
|
|
240
|
+
this.state.model,
|
|
241
|
+
this.state.date,
|
|
242
|
+
this.state.run
|
|
243
|
+
);
|
|
244
|
+
let hours = resolved.hours || [];
|
|
245
|
+
|
|
246
|
+
if (hours.length > 0) {
|
|
247
|
+
hours = hours.map(h => (typeof h === 'string' ? parseInt(h, 10) : Number(h))).filter(h => !Number.isNaN(h));
|
|
248
|
+
}
|
|
249
|
+
if (this.state.variable === 'ptypeRefl' && this.state.model === 'hrrr' && hours.length > 0) {
|
|
250
|
+
hours = hours.filter(hour => hour !== 0);
|
|
251
|
+
}
|
|
252
|
+
return hours;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_computeSatelliteTimeline() {
|
|
256
|
+
if (!this.state.satelliteKey || !this.satelliteListing?.objects) {
|
|
257
|
+
return { unixTimes: [], timeToFileMap: {} };
|
|
258
|
+
}
|
|
259
|
+
const allFiles = this.satelliteListing.objects.map((o) => o.key);
|
|
260
|
+
const sectorName = this.state.satelliteKey.split('.')[1] || 'GOES-EAST CONUS';
|
|
261
|
+
let sectorType = 'CONUS';
|
|
262
|
+
if (sectorName.includes('FULL DISK')) sectorType = 'FULL_DISK';
|
|
263
|
+
else if (sectorName.includes('MESOSCALE')) sectorType = 'MESOSCALE';
|
|
264
|
+
const tier = this.state.satelliteTier || 'basic';
|
|
265
|
+
const tierConfig =
|
|
266
|
+
SATELLITE_DURATION_CONFIG[sectorType]?.[tier] || SATELLITE_DURATION_CONFIG.CONUS.basic;
|
|
267
|
+
const durationKey = normalizeTimelineDurationValue(this.state.satelliteDurationValue);
|
|
268
|
+
const durationOpt =
|
|
269
|
+
tierConfig.find((o) => String(o.value) === String(durationKey)) ||
|
|
270
|
+
tierConfig.find((o) => o.value === '1') ||
|
|
271
|
+
tierConfig[0];
|
|
272
|
+
return buildSatelliteTimelineForKey(this.state.satelliteKey, allFiles, durationOpt);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* MRMS timestamps for a variable, oldest first (matches satellite timelines and left→right time sliders).
|
|
277
|
+
* Limited to the last N hours relative to the newest frame (see mrmsDurationValue).
|
|
278
|
+
* @param {string} variable
|
|
279
|
+
* @returns {number[]}
|
|
280
|
+
*/
|
|
281
|
+
_getFilteredMrmsTimestampsForVariable(variable) {
|
|
282
|
+
const raw = this.mrmsStatus?.[variable];
|
|
283
|
+
if (!raw || !raw.length) return [];
|
|
284
|
+
const hours = parseTimelineDurationHours(this.state.mrmsDurationValue);
|
|
285
|
+
let list = [...raw]
|
|
286
|
+
.map((t) => Number(t))
|
|
287
|
+
.filter((t) => !Number.isNaN(t))
|
|
288
|
+
.sort((a, b) => a - b);
|
|
289
|
+
if (hours > 0 && list.length > 0) {
|
|
290
|
+
const latest = list[list.length - 1];
|
|
291
|
+
const cutoff = latest - hours * 3600;
|
|
292
|
+
list = list.filter((t) => t >= cutoff);
|
|
293
|
+
}
|
|
294
|
+
return list;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_nexradListingWindowHours() {
|
|
298
|
+
return parseTimelineDurationHours(this.state.mrmsDurationValue);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
_getFilteredNexradTimestampsForVariable(rawList) {
|
|
302
|
+
if (!rawList || !rawList.length) return [];
|
|
303
|
+
const hours = this._nexradListingWindowHours();
|
|
304
|
+
let list = [...rawList]
|
|
305
|
+
.map((t) => Number(t))
|
|
306
|
+
.filter((t) => !Number.isNaN(t))
|
|
307
|
+
.sort((a, b) => a - b);
|
|
308
|
+
if (hours > 0 && list.length > 0) {
|
|
309
|
+
const latest = list[list.length - 1];
|
|
310
|
+
const cutoff = latest - hours * 3600;
|
|
311
|
+
list = list.filter((t) => t >= cutoff);
|
|
312
|
+
}
|
|
313
|
+
return list;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Cache key for {@link this.nexradTimesByStation} — matches frontend composite keys (tilt + variable group/source).
|
|
318
|
+
*/
|
|
319
|
+
_nexradTimesCacheKey() {
|
|
320
|
+
const s = this.state;
|
|
321
|
+
if (!s.isNexrad || !s.nexradSite) return null;
|
|
322
|
+
const site = s.nexradSite;
|
|
323
|
+
const variable = s.nexradProduct || 'REF';
|
|
324
|
+
const ds = s.nexradDataSource || 'level2';
|
|
325
|
+
const tiltNum = s.nexradTilt != null ? s.nexradTilt : getDefaultRadarTilt(site);
|
|
326
|
+
const elevNormUse =
|
|
327
|
+
ds === 'level3'
|
|
328
|
+
? NEXRAD_LEVEL3_ELEV
|
|
329
|
+
: formatTiltForApi(clampNexradTiltForVariable(site, variable, tiltNum));
|
|
330
|
+
const group = ds === 'level3' ? 'l3' : variableToNexradGroup(variable);
|
|
331
|
+
const l3Product =
|
|
332
|
+
ds === 'level3'
|
|
333
|
+
? getNexradLevel3EntryByRadarKey(variable)?.product ?? (variable === 'VEL' ? 'N0G' : variable)
|
|
334
|
+
: '';
|
|
335
|
+
if (ds === 'level3') {
|
|
336
|
+
return `${site}_l3_${l3Product}_${elevNormUse}`;
|
|
337
|
+
}
|
|
338
|
+
return `${site}_${group}_${elevNormUse}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Storm-relative Level-III velocity (N0G + N0S) — matches aguacero-frontend `level3StormRelative` for VEL. */
|
|
342
|
+
_nexradStormRelativeFor(nexradDataSource, nexradProduct) {
|
|
343
|
+
const ds = nexradDataSource === 'level3' ? 'level3' : 'level2';
|
|
344
|
+
const p = (nexradProduct || 'REF').toUpperCase();
|
|
345
|
+
return ds === 'level3' && p === 'VEL';
|
|
346
|
+
}
|
|
347
|
+
|
|
101
348
|
_emitStateChange() {
|
|
102
349
|
const { colormap, baseUnit } = this._getColormapForVariable(this.state.variable);
|
|
103
350
|
const toUnit = this._getTargetUnit(baseUnit, this.state.units);
|
|
@@ -105,18 +352,40 @@ export class AguaceroCore extends EventEmitter {
|
|
|
105
352
|
|
|
106
353
|
let availableTimestamps = [];
|
|
107
354
|
if (this.state.isMRMS && this.state.variable && this.mrmsStatus) {
|
|
108
|
-
|
|
109
|
-
availableTimestamps = [...timestamps].reverse();
|
|
355
|
+
availableTimestamps = this._getFilteredMrmsTimestampsForVariable(this.state.variable);
|
|
110
356
|
}
|
|
111
357
|
|
|
112
|
-
let
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
358
|
+
let availableSatelliteTimestamps = [];
|
|
359
|
+
let satelliteTimeToFileMap = {};
|
|
360
|
+
if (this.state.isSatellite && this.state.satelliteKey) {
|
|
361
|
+
const timeline = this._computeSatelliteTimeline();
|
|
362
|
+
satelliteTimeToFileMap = timeline.timeToFileMap || {};
|
|
363
|
+
availableSatelliteTimestamps = [...(timeline.unixTimes || [])]
|
|
364
|
+
.map((t) => Number(t))
|
|
365
|
+
.filter((t) => !Number.isNaN(t))
|
|
366
|
+
.sort((a, b) => a - b);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let availableNexradTimestamps = [];
|
|
370
|
+
let nexradTimeToKeyMap = {};
|
|
371
|
+
let nexradLevel3MotionTimeToKeyMap = {};
|
|
372
|
+
let availableNexradTilts = [];
|
|
373
|
+
if (this.state.isNexrad && this.state.nexradSite) {
|
|
374
|
+
const nk = this._nexradTimesCacheKey();
|
|
375
|
+
const ent = nk ? this.nexradTimesByStation[nk] : null;
|
|
376
|
+
const raw = ent?.unixTimes || [];
|
|
377
|
+
availableNexradTimestamps = this._getFilteredNexradTimestampsForVariable(raw);
|
|
378
|
+
nexradTimeToKeyMap = ent?.timeToKeyMap || {};
|
|
379
|
+
nexradLevel3MotionTimeToKeyMap = ent?.level3MotionTimeToKeyMap || {};
|
|
380
|
+
availableNexradTilts = getAvailableNexradTilts(
|
|
381
|
+
this.state.nexradSite,
|
|
382
|
+
this.state.nexradDataSource || 'level2',
|
|
383
|
+
this.state.nexradProduct || 'REF',
|
|
384
|
+
);
|
|
118
385
|
}
|
|
119
386
|
|
|
387
|
+
const availableHours = this.getAvailableForecastHours();
|
|
388
|
+
|
|
120
389
|
const eventPayload = {
|
|
121
390
|
...this.state,
|
|
122
391
|
availableModels: this.modelStatus ? Object.keys(this.modelStatus).sort() : [],
|
|
@@ -126,6 +395,12 @@ export class AguaceroCore extends EventEmitter {
|
|
|
126
395
|
// We need to confirm this line is working as expected.
|
|
127
396
|
availableMRMSVariables: this.getAvailableVariables('mrms'),
|
|
128
397
|
availableTimestamps: availableTimestamps,
|
|
398
|
+
availableSatelliteTimestamps,
|
|
399
|
+
satelliteTimeToFileMap,
|
|
400
|
+
availableNexradTimestamps,
|
|
401
|
+
nexradTimeToKeyMap,
|
|
402
|
+
nexradLevel3MotionTimeToKeyMap,
|
|
403
|
+
availableNexradTilts,
|
|
129
404
|
isPlaying: this.isPlaying,
|
|
130
405
|
colormap: displayColormap,
|
|
131
406
|
colormapBaseUnit: toUnit,
|
|
@@ -137,27 +412,36 @@ export class AguaceroCore extends EventEmitter {
|
|
|
137
412
|
async initialize(options = {}) {
|
|
138
413
|
await this.fetchModelStatus(true);
|
|
139
414
|
await this.fetchMRMSStatus(true);
|
|
140
|
-
|
|
415
|
+
await this.fetchSatelliteListing(true);
|
|
416
|
+
|
|
141
417
|
let initialState = { ...this.state };
|
|
142
418
|
|
|
419
|
+
if (initialState.isSatellite && initialState.satelliteKey) {
|
|
420
|
+
const timeline = this._computeSatelliteTimeline();
|
|
421
|
+
const tsList = [...(timeline.unixTimes || [])].sort((a, b) => a - b);
|
|
422
|
+
if (initialState.satelliteTimestamp == null && tsList.length > 0) {
|
|
423
|
+
initialState.satelliteTimestamp = tsList[tsList.length - 1];
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
143
427
|
// ADD: Logic to handle an initial MRMS state
|
|
144
428
|
if (initialState.isMRMS) {
|
|
145
429
|
const variable = initialState.variable;
|
|
146
430
|
if (variable && this.mrmsStatus && this.mrmsStatus[variable]) {
|
|
147
|
-
const sortedTimestamps =
|
|
148
|
-
initialState.mrmsTimestamp =
|
|
431
|
+
const sortedTimestamps = this._getFilteredMrmsTimestampsForVariable(variable);
|
|
432
|
+
initialState.mrmsTimestamp =
|
|
433
|
+
sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1] : null;
|
|
149
434
|
} else {
|
|
150
|
-
// Fallback if the provided variable is not valid
|
|
151
|
-
console.warn(`Initial MRMS variable '${variable}' not found. Using default.`);
|
|
152
435
|
const availableMRMSVars = this.getAvailableVariables('mrms');
|
|
153
436
|
if (availableMRMSVars.length > 0) {
|
|
154
437
|
const firstVar = availableMRMSVars[0];
|
|
155
438
|
initialState.variable = firstVar;
|
|
156
|
-
const sortedTimestamps =
|
|
157
|
-
initialState.mrmsTimestamp =
|
|
439
|
+
const sortedTimestamps = this._getFilteredMrmsTimestampsForVariable(firstVar);
|
|
440
|
+
initialState.mrmsTimestamp =
|
|
441
|
+
sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1] : null;
|
|
158
442
|
}
|
|
159
443
|
}
|
|
160
|
-
} else {
|
|
444
|
+
} else if (!initialState.isSatellite && !initialState.isNexrad) {
|
|
161
445
|
// EDIT: This is the existing logic, now in an else block
|
|
162
446
|
const latestRun = findLatestModelRun(this.modelStatus, initialState.model);
|
|
163
447
|
if (latestRun) {
|
|
@@ -172,6 +456,14 @@ export class AguaceroCore extends EventEmitter {
|
|
|
172
456
|
}
|
|
173
457
|
|
|
174
458
|
await this.setState(initialState);
|
|
459
|
+
|
|
460
|
+
if (this.state.isNexrad) {
|
|
461
|
+
const manifest = await fetchRadarTiltsManifestFromNetwork();
|
|
462
|
+
if (manifest) setRadarTiltsManifest(manifest);
|
|
463
|
+
if (this.state.nexradSite) {
|
|
464
|
+
await this.refreshNexradTimes();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
175
467
|
if (options.autoRefresh ?? this.autoRefreshEnabled) {
|
|
176
468
|
this.startAutoRefresh(options.refreshInterval ?? this.autoRefreshIntervalSeconds);
|
|
177
469
|
}
|
|
@@ -181,11 +473,11 @@ export class AguaceroCore extends EventEmitter {
|
|
|
181
473
|
this.pause();
|
|
182
474
|
this.stopAutoRefresh();
|
|
183
475
|
this.dataCache.clear();
|
|
184
|
-
if (this.worker) {
|
|
185
|
-
this.worker.terminate();
|
|
186
|
-
}
|
|
187
476
|
this.callbacks = {};
|
|
188
|
-
|
|
477
|
+
if (this._gridDecodeWorker) {
|
|
478
|
+
this._gridDecodeWorker.terminate();
|
|
479
|
+
this._gridDecodeWorker = null;
|
|
480
|
+
}
|
|
189
481
|
}
|
|
190
482
|
|
|
191
483
|
// --- Public API Methods ---
|
|
@@ -213,24 +505,40 @@ export class AguaceroCore extends EventEmitter {
|
|
|
213
505
|
}
|
|
214
506
|
|
|
215
507
|
step(direction = 1) {
|
|
508
|
+
if (this.state.isSatellite) {
|
|
509
|
+
const timeline = this._computeSatelliteTimeline();
|
|
510
|
+
const availableTimestamps = [...(timeline.unixTimes || [])]
|
|
511
|
+
.sort((a, b) => a - b)
|
|
512
|
+
.map((t) => Number(t));
|
|
513
|
+
if (availableTimestamps.length === 0) return;
|
|
514
|
+
|
|
515
|
+
const ts = this.state.satelliteTimestamp == null ? null : Number(this.state.satelliteTimestamp);
|
|
516
|
+
const currentIndex = ts == null ? -1 : availableTimestamps.indexOf(ts);
|
|
517
|
+
|
|
518
|
+
let nextIndex = currentIndex === -1 ? availableTimestamps.length - 1 : currentIndex + direction;
|
|
519
|
+
const maxIndex = availableTimestamps.length - 1;
|
|
520
|
+
if (nextIndex > maxIndex) nextIndex = 0;
|
|
521
|
+
if (nextIndex < 0) nextIndex = maxIndex;
|
|
522
|
+
|
|
523
|
+
this.setState({ satelliteTimestamp: availableTimestamps[nextIndex] });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
216
526
|
// --- THIS IS THE CORRECTED MRMS LOGIC ---
|
|
217
527
|
if (this.state.isMRMS) {
|
|
218
528
|
const { variable, mrmsTimestamp } = this.state;
|
|
219
529
|
if (!this.mrmsStatus || !this.mrmsStatus[variable]) {
|
|
220
|
-
console.warn('[Core.step] MRMS status or variable not available.');
|
|
221
530
|
return;
|
|
222
531
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// The step logic MUST use the same reversed array for indexes to match.
|
|
226
|
-
const availableTimestamps = [...(this.mrmsStatus[variable] || [])].reverse();
|
|
532
|
+
|
|
533
|
+
const availableTimestamps = this._getFilteredMrmsTimestampsForVariable(variable);
|
|
227
534
|
if (availableTimestamps.length === 0) return;
|
|
228
535
|
|
|
229
|
-
const
|
|
536
|
+
const ts = mrmsTimestamp == null ? null : Number(mrmsTimestamp);
|
|
537
|
+
const currentIndex = ts == null ? -1 : availableTimestamps.indexOf(ts);
|
|
230
538
|
|
|
231
539
|
if (currentIndex === -1) {
|
|
232
|
-
// If not found, reset to the
|
|
233
|
-
this.setState({ mrmsTimestamp: availableTimestamps[
|
|
540
|
+
// If not found, reset to the latest frame (end of ascending list)
|
|
541
|
+
this.setState({ mrmsTimestamp: availableTimestamps[availableTimestamps.length - 1] });
|
|
234
542
|
return;
|
|
235
543
|
}
|
|
236
544
|
|
|
@@ -244,12 +552,28 @@ export class AguaceroCore extends EventEmitter {
|
|
|
244
552
|
const newTimestamp = availableTimestamps[nextIndex];
|
|
245
553
|
this.setState({ mrmsTimestamp: newTimestamp });
|
|
246
554
|
|
|
555
|
+
} else if (this.state.isNexrad) {
|
|
556
|
+
const nk = this._nexradTimesCacheKey();
|
|
557
|
+
const raw = nk ? this.nexradTimesByStation[nk]?.unixTimes : [];
|
|
558
|
+
const availableTimestamps = this._getFilteredNexradTimestampsForVariable(raw || []);
|
|
559
|
+
if (availableTimestamps.length === 0) return;
|
|
560
|
+
|
|
561
|
+
const ts = this.state.nexradTimestamp == null ? null : Number(this.state.nexradTimestamp);
|
|
562
|
+
const currentIndex = ts == null ? -1 : availableTimestamps.indexOf(ts);
|
|
563
|
+
|
|
564
|
+
let nextIndex = currentIndex === -1 ? availableTimestamps.length - 1 : currentIndex + direction;
|
|
565
|
+
const maxIndex = availableTimestamps.length - 1;
|
|
566
|
+
if (nextIndex > maxIndex) nextIndex = 0;
|
|
567
|
+
if (nextIndex < 0) nextIndex = maxIndex;
|
|
568
|
+
|
|
569
|
+
this.setState({ nexradTimestamp: availableTimestamps[nextIndex] });
|
|
247
570
|
} else {
|
|
248
|
-
const {
|
|
249
|
-
const forecastHours = this.
|
|
571
|
+
const { forecastHour } = this.state;
|
|
572
|
+
const forecastHours = this.getAvailableForecastHours();
|
|
250
573
|
if (!forecastHours || forecastHours.length === 0) return;
|
|
251
574
|
|
|
252
|
-
const
|
|
575
|
+
const fh = Number(forecastHour);
|
|
576
|
+
const currentIndex = forecastHours.indexOf(fh);
|
|
253
577
|
if (currentIndex === -1) return;
|
|
254
578
|
|
|
255
579
|
const maxIndex = forecastHours.length - 1;
|
|
@@ -283,7 +607,12 @@ export class AguaceroCore extends EventEmitter {
|
|
|
283
607
|
async setVariable(variable) {
|
|
284
608
|
// --- NEW CODE: Handle switching TO ptypeRefl on HRRR ---
|
|
285
609
|
if (variable === 'ptypeRefl' && this.state.model === 'hrrr' && this.state.forecastHour === 0) {
|
|
286
|
-
const availableHours =
|
|
610
|
+
const availableHours = resolveModelRunHours(
|
|
611
|
+
this.modelStatus,
|
|
612
|
+
this.state.model,
|
|
613
|
+
this.state.date,
|
|
614
|
+
this.state.run
|
|
615
|
+
).hours || [];
|
|
287
616
|
const firstValidHour = availableHours.find(hour => hour !== 0) || 0;
|
|
288
617
|
await this.setState({ variable, forecastHour: firstValidHour });
|
|
289
618
|
return;
|
|
@@ -295,6 +624,9 @@ export class AguaceroCore extends EventEmitter {
|
|
|
295
624
|
|
|
296
625
|
async setModel(modelName) {
|
|
297
626
|
if (modelName === this.state.model || !this.modelStatus?.[modelName]) return;
|
|
627
|
+
if (this.state.isSatellite) {
|
|
628
|
+
await this.setState({ isSatellite: false, satelliteKey: null, satelliteTimestamp: null });
|
|
629
|
+
}
|
|
298
630
|
const latestRun = findLatestModelRun(this.modelStatus, modelName);
|
|
299
631
|
if (latestRun) {
|
|
300
632
|
// --- NEW CODE: Determine initial forecast hour ---
|
|
@@ -302,8 +634,12 @@ export class AguaceroCore extends EventEmitter {
|
|
|
302
634
|
|
|
303
635
|
// If switching to HRRR with ptypeRefl, start at hour 1 instead of 0
|
|
304
636
|
if (modelName === 'hrrr' && this.state.variable === 'ptypeRefl') {
|
|
305
|
-
const availableHours =
|
|
306
|
-
|
|
637
|
+
const availableHours = resolveModelRunHours(
|
|
638
|
+
this.modelStatus,
|
|
639
|
+
modelName,
|
|
640
|
+
latestRun.date,
|
|
641
|
+
latestRun.run
|
|
642
|
+
).hours || [];
|
|
307
643
|
initialHour = availableHours.find(hour => hour !== 0) || 0;
|
|
308
644
|
}
|
|
309
645
|
// --- END NEW CODE ---
|
|
@@ -330,12 +666,16 @@ export class AguaceroCore extends EventEmitter {
|
|
|
330
666
|
}
|
|
331
667
|
|
|
332
668
|
async setMRMSVariable(variable) {
|
|
333
|
-
const sortedTimestamps =
|
|
334
|
-
const initialTimestamp =
|
|
669
|
+
const sortedTimestamps = this._getFilteredMrmsTimestampsForVariable(variable);
|
|
670
|
+
const initialTimestamp =
|
|
671
|
+
sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1] : null;
|
|
335
672
|
|
|
336
673
|
await this.setState({
|
|
337
674
|
variable,
|
|
338
675
|
isMRMS: true,
|
|
676
|
+
isSatellite: false,
|
|
677
|
+
satelliteKey: null,
|
|
678
|
+
satelliteTimestamp: null,
|
|
339
679
|
mrmsTimestamp: initialTimestamp,
|
|
340
680
|
});
|
|
341
681
|
}
|
|
@@ -345,33 +685,148 @@ export class AguaceroCore extends EventEmitter {
|
|
|
345
685
|
await this.setState({ mrmsTimestamp: timestamp });
|
|
346
686
|
}
|
|
347
687
|
|
|
688
|
+
async setSatelliteTimestamp(timestamp) {
|
|
689
|
+
if (!this.state.isSatellite) return;
|
|
690
|
+
await this.setState({ satelliteTimestamp: timestamp != null ? Number(timestamp) : null });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* How many hours of satellite frames to include in the timeline (1, 4, 6, or 12).
|
|
695
|
+
* API default: `layerOptions.satelliteDurationValue` on construction.
|
|
696
|
+
*/
|
|
697
|
+
async setSatelliteDurationValue(value) {
|
|
698
|
+
const v = normalizeTimelineDurationValue(value);
|
|
699
|
+
if (!TIMELINE_DURATION_HOUR_VALUES.includes(v)) return;
|
|
700
|
+
await this.setState({ satelliteDurationValue: v });
|
|
701
|
+
if (!this.state.isSatellite || !this.state.satelliteKey) return;
|
|
702
|
+
const timeline = this._computeSatelliteTimeline();
|
|
703
|
+
const tsList = [...(timeline.unixTimes || [])]
|
|
704
|
+
.map((t) => Number(t))
|
|
705
|
+
.filter((t) => !Number.isNaN(t))
|
|
706
|
+
.sort((a, b) => a - b);
|
|
707
|
+
if (tsList.length === 0) return;
|
|
708
|
+
const cur = this.state.satelliteTimestamp;
|
|
709
|
+
const curN = cur == null ? null : Number(cur);
|
|
710
|
+
if (curN == null || !tsList.includes(curN)) {
|
|
711
|
+
await this.setState({ satelliteTimestamp: tsList[tsList.length - 1] });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* How many hours of MRMS frames to include in the timeline (1, 4, 6, or 12).
|
|
717
|
+
* API default: `layerOptions.mrmsDurationValue` on construction.
|
|
718
|
+
*/
|
|
719
|
+
async setMRMSDurationValue(value) {
|
|
720
|
+
const v = normalizeTimelineDurationValue(value);
|
|
721
|
+
if (!TIMELINE_DURATION_HOUR_VALUES.includes(v)) return;
|
|
722
|
+
await this.setState({ mrmsDurationValue: v });
|
|
723
|
+
// NEXRAD listings are fetched with `hours` from this value (see _nexradListingWindowHours). Without a
|
|
724
|
+
// refresh, expanding 1h→4h only re-filters the old in-memory sweep list — new frames never appear until
|
|
725
|
+
// site/product changes trigger refreshNexradTimes.
|
|
726
|
+
if (this.state.isNexrad && this.state.nexradSite) {
|
|
727
|
+
await this.refreshNexradTimes();
|
|
728
|
+
const nk = this._nexradTimesCacheKey();
|
|
729
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
730
|
+
nk ? this.nexradTimesByStation[nk]?.unixTimes || [] : [],
|
|
731
|
+
);
|
|
732
|
+
if (filtered.length === 0) return;
|
|
733
|
+
const cur = this.state.nexradTimestamp;
|
|
734
|
+
const curN = cur == null ? null : Number(cur);
|
|
735
|
+
if (curN == null || !filtered.includes(curN)) {
|
|
736
|
+
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
737
|
+
}
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (!this.state.isMRMS || !this.state.variable) return;
|
|
741
|
+
const filtered = this._getFilteredMrmsTimestampsForVariable(this.state.variable);
|
|
742
|
+
if (filtered.length === 0) return;
|
|
743
|
+
const cur = this.state.mrmsTimestamp;
|
|
744
|
+
const curN = cur == null ? null : Number(cur);
|
|
745
|
+
if (curN == null || !filtered.includes(curN)) {
|
|
746
|
+
await this.setState({ mrmsTimestamp: filtered[filtered.length - 1] });
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
348
750
|
async switchMode(options) {
|
|
349
|
-
const { mode, variable, model, forecastHour, mrmsTimestamp } = options;
|
|
350
|
-
if (!mode
|
|
351
|
-
console.error("switchMode requires 'mode' ('mrms' | 'model') and 'variable' properties.");
|
|
751
|
+
const { mode, variable, model, forecastHour, mrmsTimestamp, satelliteKey, satelliteTimestamp } = options;
|
|
752
|
+
if (!mode) {
|
|
352
753
|
return;
|
|
353
754
|
}
|
|
354
755
|
if (mode === 'model' && !model) {
|
|
355
|
-
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if ((mode === 'mrms' || mode === 'model') && !variable) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (mode === 'satellite' && !satelliteKey) {
|
|
356
762
|
return;
|
|
357
763
|
}
|
|
358
764
|
let targetState = {};
|
|
359
|
-
if (mode === '
|
|
765
|
+
if (mode === 'satellite') {
|
|
766
|
+
const channelToken = satelliteKey.split('.').pop() || variable || 'C13';
|
|
767
|
+
// Emit satellite mode immediately so map layers (e.g. model grid) clear before listing fetch finishes.
|
|
768
|
+
await this.setState({
|
|
769
|
+
isSatellite: true,
|
|
770
|
+
isMRMS: false,
|
|
771
|
+
isNexrad: false,
|
|
772
|
+
satelliteKey,
|
|
773
|
+
variable: channelToken,
|
|
774
|
+
satelliteTimestamp: null,
|
|
775
|
+
mrmsTimestamp: null,
|
|
776
|
+
date: null,
|
|
777
|
+
run: null,
|
|
778
|
+
forecastHour: 0,
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
await this.fetchSatelliteListing(true);
|
|
782
|
+
const allFiles = this.satelliteListing?.objects?.map((o) => o.key) || [];
|
|
783
|
+
const sectorName = satelliteKey.split('.')[1] || 'GOES-EAST CONUS';
|
|
784
|
+
let sectorType = 'CONUS';
|
|
785
|
+
if (sectorName.includes('FULL DISK')) sectorType = 'FULL_DISK';
|
|
786
|
+
else if (sectorName.includes('MESOSCALE')) sectorType = 'MESOSCALE';
|
|
787
|
+
const tier = this.state.satelliteTier || 'basic';
|
|
788
|
+
const tierConfig =
|
|
789
|
+
SATELLITE_DURATION_CONFIG[sectorType]?.[tier] || SATELLITE_DURATION_CONFIG.CONUS.basic;
|
|
790
|
+
const durationKey = normalizeTimelineDurationValue(this.state.satelliteDurationValue);
|
|
791
|
+
const durationOpt =
|
|
792
|
+
tierConfig.find((o) => String(o.value) === String(durationKey)) ||
|
|
793
|
+
tierConfig.find((o) => o.value === '1') ||
|
|
794
|
+
tierConfig[0];
|
|
795
|
+
const timeline = buildSatelliteTimelineForKey(satelliteKey, allFiles, durationOpt);
|
|
796
|
+
const tsList = [...(timeline.unixTimes || [])].sort((a, b) => a - b);
|
|
797
|
+
let finalTs = satelliteTimestamp !== undefined ? satelliteTimestamp : null;
|
|
798
|
+
if (finalTs == null && tsList.length > 0) {
|
|
799
|
+
finalTs = tsList[tsList.length - 1];
|
|
800
|
+
}
|
|
801
|
+
await this.setState({
|
|
802
|
+
satelliteTimestamp: finalTs != null ? Number(finalTs) : null,
|
|
803
|
+
});
|
|
804
|
+
return;
|
|
805
|
+
} else if (mode === 'mrms') {
|
|
806
|
+
const sortedTimestamps = this._getFilteredMrmsTimestampsForVariable(variable);
|
|
360
807
|
let finalTimestamp = mrmsTimestamp;
|
|
361
808
|
if (finalTimestamp === undefined) {
|
|
362
|
-
|
|
363
|
-
|
|
809
|
+
finalTimestamp =
|
|
810
|
+
sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1] : null;
|
|
811
|
+
} else if (sortedTimestamps.length > 0) {
|
|
812
|
+
const n = Number(finalTimestamp);
|
|
813
|
+
if (!sortedTimestamps.includes(n)) {
|
|
814
|
+
finalTimestamp = sortedTimestamps[sortedTimestamps.length - 1];
|
|
815
|
+
}
|
|
364
816
|
}
|
|
365
817
|
targetState = {
|
|
366
818
|
isMRMS: true,
|
|
819
|
+
isSatellite: false,
|
|
820
|
+
isNexrad: false,
|
|
367
821
|
variable: variable,
|
|
368
822
|
mrmsTimestamp: finalTimestamp,
|
|
369
823
|
model: this.state.model, date: null, run: null, forecastHour: 0,
|
|
824
|
+
satelliteKey: null,
|
|
825
|
+
satelliteTimestamp: null,
|
|
370
826
|
};
|
|
371
827
|
} else if (mode === 'model') {
|
|
372
828
|
const latestRun = findLatestModelRun(this.modelStatus, model);
|
|
373
829
|
if (!latestRun) {
|
|
374
|
-
console.error(`Could not find a valid run for model: ${model}`);
|
|
375
830
|
return;
|
|
376
831
|
}
|
|
377
832
|
|
|
@@ -380,50 +835,316 @@ export class AguaceroCore extends EventEmitter {
|
|
|
380
835
|
|
|
381
836
|
// If switching to HRRR with ptypeRefl and hour is 0, use hour 1
|
|
382
837
|
if (model === 'hrrr' && variable === 'ptypeRefl' && initialHour === 0) {
|
|
383
|
-
const availableHours =
|
|
838
|
+
const availableHours = resolveModelRunHours(
|
|
839
|
+
this.modelStatus,
|
|
840
|
+
model,
|
|
841
|
+
latestRun.date,
|
|
842
|
+
latestRun.run
|
|
843
|
+
).hours || [];
|
|
384
844
|
initialHour = availableHours.find(hour => hour !== 0) || 0;
|
|
385
845
|
}
|
|
386
846
|
// --- END NEW CODE ---
|
|
387
847
|
|
|
388
848
|
targetState = {
|
|
389
849
|
isMRMS: false,
|
|
850
|
+
isSatellite: false,
|
|
851
|
+
isNexrad: false,
|
|
390
852
|
model: model,
|
|
391
853
|
variable: variable,
|
|
392
854
|
date: latestRun.date,
|
|
393
855
|
run: latestRun.run,
|
|
394
856
|
forecastHour: initialHour, // <-- Changed
|
|
395
857
|
mrmsTimestamp: null,
|
|
858
|
+
satelliteKey: null,
|
|
859
|
+
satelliteTimestamp: null,
|
|
396
860
|
};
|
|
861
|
+
} else if (mode === 'nexrad') {
|
|
862
|
+
const nexradDataSource = options.nexradDataSource || 'level2';
|
|
863
|
+
const nexradProduct = options.nexradProduct || 'REF';
|
|
864
|
+
const fld = nexradColormapFldKey(nexradDataSource, nexradProduct);
|
|
865
|
+
const site = options.nexradSite ?? null;
|
|
866
|
+
let tilt =
|
|
867
|
+
options.nexradTilt != null
|
|
868
|
+
? Number(options.nexradTilt)
|
|
869
|
+
: site != null
|
|
870
|
+
? getDefaultRadarTilt(site)
|
|
871
|
+
: null;
|
|
872
|
+
await this.setState({
|
|
873
|
+
isNexrad: true,
|
|
874
|
+
isMRMS: false,
|
|
875
|
+
isSatellite: false,
|
|
876
|
+
variable: fld,
|
|
877
|
+
nexradSite: site,
|
|
878
|
+
nexradDataSource,
|
|
879
|
+
nexradProduct,
|
|
880
|
+
nexradTilt: tilt,
|
|
881
|
+
nexradTimestamp: options.nexradTimestamp != null ? Number(options.nexradTimestamp) : null,
|
|
882
|
+
nexradStormRelative: options.nexradStormRelative === true,
|
|
883
|
+
nexradShowSitesPicker: options.nexradShowSitesPicker !== false,
|
|
884
|
+
mrmsTimestamp: null,
|
|
885
|
+
satelliteKey: null,
|
|
886
|
+
satelliteTimestamp: null,
|
|
887
|
+
date: null,
|
|
888
|
+
run: null,
|
|
889
|
+
forecastHour: 0,
|
|
890
|
+
});
|
|
891
|
+
const manifest = await fetchRadarTiltsManifestFromNetwork();
|
|
892
|
+
if (manifest) setRadarTiltsManifest(manifest);
|
|
893
|
+
if (site) {
|
|
894
|
+
await this.refreshNexradTimes();
|
|
895
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
896
|
+
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
897
|
+
);
|
|
898
|
+
let ts =
|
|
899
|
+
options.nexradTimestamp != null
|
|
900
|
+
? Number(options.nexradTimestamp)
|
|
901
|
+
: this.state.nexradTimestamp;
|
|
902
|
+
if (ts == null && filtered.length > 0) ts = filtered[filtered.length - 1];
|
|
903
|
+
else if (ts != null && filtered.length > 0 && !filtered.includes(ts)) {
|
|
904
|
+
ts = filtered[filtered.length - 1];
|
|
905
|
+
}
|
|
906
|
+
await this.setState({ nexradTimestamp: ts });
|
|
907
|
+
}
|
|
908
|
+
return;
|
|
397
909
|
} else {
|
|
398
|
-
console.error(`Invalid mode specified in switchMode: '${mode}'`);
|
|
399
910
|
return;
|
|
400
911
|
}
|
|
401
912
|
await this.setState(targetState);
|
|
402
913
|
}
|
|
403
914
|
|
|
404
|
-
|
|
915
|
+
/**
|
|
916
|
+
* Keep `nexradTilt` on an angle returned by {@link getAvailableNexradTilts} (matches aguacero-frontend elevation buttons).
|
|
917
|
+
*/
|
|
918
|
+
async _snapNexradTiltToAvailableOptions() {
|
|
919
|
+
const s = this.state;
|
|
920
|
+
if (!s.isNexrad || !s.nexradSite) return;
|
|
921
|
+
const tilts = getAvailableNexradTilts(
|
|
922
|
+
s.nexradSite,
|
|
923
|
+
s.nexradDataSource || 'level2',
|
|
924
|
+
s.nexradProduct || 'REF',
|
|
925
|
+
);
|
|
926
|
+
if (!tilts.length) return;
|
|
927
|
+
const t = s.nexradTilt;
|
|
928
|
+
const match = (a, b) => Math.abs(Number(a) - Number(b)) < 1e-4;
|
|
929
|
+
if (t != null && tilts.some((x) => match(x, t))) return;
|
|
930
|
+
const target = t != null && Number.isFinite(Number(t)) ? Number(t) : getDefaultRadarTilt(s.nexradSite);
|
|
931
|
+
let best = tilts[0];
|
|
932
|
+
for (const x of tilts) {
|
|
933
|
+
if (Math.abs(x - target) < Math.abs(best - target)) best = x;
|
|
934
|
+
}
|
|
935
|
+
await this.setState({ nexradTilt: best });
|
|
936
|
+
}
|
|
405
937
|
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
938
|
+
async refreshNexradTimes() {
|
|
939
|
+
const s0 = this.state;
|
|
940
|
+
if (!s0.isNexrad || !s0.nexradSite) return;
|
|
941
|
+
await this._snapNexradTiltToAvailableOptions();
|
|
942
|
+
const s = this.state;
|
|
943
|
+
const listingHours = this._nexradListingWindowHours();
|
|
944
|
+
const out = await fetchNexradTimesListing({
|
|
945
|
+
stationId: s.nexradSite,
|
|
946
|
+
variable: s.nexradProduct || 'REF',
|
|
947
|
+
elev: s.nexradTilt,
|
|
948
|
+
source: s.nexradDataSource || 'level2',
|
|
949
|
+
level3StormRelative: s.nexradStormRelative,
|
|
950
|
+
level3Product: getNexradLevel3EntryByRadarKey(s.nexradProduct)?.product,
|
|
951
|
+
listingWindowHours: listingHours,
|
|
952
|
+
});
|
|
953
|
+
const nk = this._nexradTimesCacheKey();
|
|
954
|
+
if (!nk) return;
|
|
955
|
+
this.nexradTimesByStation[nk] = {
|
|
956
|
+
unixTimes: out.unixTimes,
|
|
957
|
+
timeToKeyMap: out.timeToKeyMap,
|
|
958
|
+
level3MotionTimeToKeyMap: out.level3MotionTimeToKeyMap,
|
|
959
|
+
listWindowHours: listingHours,
|
|
960
|
+
};
|
|
961
|
+
if (out.level3MotionKey && out.level3MotionUnixTimes?.length) {
|
|
962
|
+
this.nexradTimesByStation[out.level3MotionKey] = {
|
|
963
|
+
unixTimes: out.level3MotionUnixTimes,
|
|
964
|
+
timeToKeyMap: out.level3MotionTimeToKeyMap,
|
|
965
|
+
listWindowHours: listingHours,
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
if (out.l2StormMotionListKey && out.level3MotionUnixTimes?.length) {
|
|
969
|
+
this.nexradTimesByStation[out.l2StormMotionListKey] = {
|
|
970
|
+
unixTimes: out.level3MotionUnixTimes,
|
|
971
|
+
timeToKeyMap: out.level3MotionTimeToKeyMap,
|
|
972
|
+
listWindowHours: listingHours,
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
this._emitStateChange();
|
|
976
|
+
}
|
|
409
977
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
978
|
+
async setNexradSite(siteId) {
|
|
979
|
+
if (!this.state.isNexrad) return;
|
|
980
|
+
const tilt = siteId ? getDefaultRadarTilt(siteId) : null;
|
|
981
|
+
await this.setState({
|
|
982
|
+
nexradSite: siteId || null,
|
|
983
|
+
nexradTilt: tilt,
|
|
984
|
+
nexradTimestamp: null,
|
|
985
|
+
});
|
|
986
|
+
await this.refreshNexradTimes();
|
|
987
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
988
|
+
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
989
|
+
);
|
|
990
|
+
if (filtered.length > 0) {
|
|
991
|
+
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
992
|
+
}
|
|
993
|
+
}
|
|
413
994
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
995
|
+
async setNexradProduct(product) {
|
|
996
|
+
if (!this.state.isNexrad) return;
|
|
997
|
+
const p = (product || 'REF').toUpperCase();
|
|
998
|
+
const ds = this.state.nexradDataSource || 'level2';
|
|
999
|
+
const fld = nexradColormapFldKey(ds, p);
|
|
1000
|
+
const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
|
|
1001
|
+
await this.setState({ nexradProduct: p, variable: fld, nexradStormRelative });
|
|
1002
|
+
await this.refreshNexradTimes();
|
|
1003
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
1004
|
+
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
1005
|
+
);
|
|
1006
|
+
if (filtered.length > 0) {
|
|
1007
|
+
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
419
1008
|
}
|
|
420
|
-
|
|
421
|
-
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async setNexradDataSource(source) {
|
|
1012
|
+
if (!this.state.isNexrad) return;
|
|
1013
|
+
const ds = source === 'level3' ? 'level3' : 'level2';
|
|
1014
|
+
const p = (this.state.nexradProduct || 'REF').toUpperCase();
|
|
1015
|
+
const fld = nexradColormapFldKey(ds, p);
|
|
1016
|
+
const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
|
|
1017
|
+
await this.setState({ nexradDataSource: ds, variable: fld, nexradStormRelative });
|
|
1018
|
+
await this.refreshNexradTimes();
|
|
1019
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
1020
|
+
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
1021
|
+
);
|
|
1022
|
+
if (filtered.length > 0) {
|
|
1023
|
+
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Sets NEXRAD product and archive source together (single refresh). Used when the UI exposes one
|
|
1029
|
+
* combined menu instead of separate “Level II / Level III” controls.
|
|
1030
|
+
* @param {'level2'|'level3'} dataSource
|
|
1031
|
+
* @param {string} product - Level-II variable (REF, VEL, …) or Level-III radar key (N0H, HHC, …)
|
|
1032
|
+
*/
|
|
1033
|
+
async setNexradProductMode(dataSource, product) {
|
|
1034
|
+
if (!this.state.isNexrad) return;
|
|
1035
|
+
const ds = dataSource === 'level3' ? 'level3' : 'level2';
|
|
1036
|
+
const p = (product || 'REF').toUpperCase();
|
|
1037
|
+
const fld = nexradColormapFldKey(ds, p);
|
|
1038
|
+
const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
|
|
1039
|
+
await this.setState({ nexradDataSource: ds, nexradProduct: p, variable: fld, nexradStormRelative });
|
|
1040
|
+
await this.refreshNexradTimes();
|
|
1041
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
1042
|
+
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
1043
|
+
);
|
|
1044
|
+
if (filtered.length > 0) {
|
|
1045
|
+
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async setNexradTilt(tilt) {
|
|
1050
|
+
if (!this.state.isNexrad || !this.state.nexradSite) return;
|
|
1051
|
+
await this.setState({ nexradTilt: tilt != null ? Number(tilt) : null });
|
|
1052
|
+
await this.refreshNexradTimes();
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
async setNexradTimestamp(ts) {
|
|
1056
|
+
if (!this.state.isNexrad) return;
|
|
1057
|
+
await this.setState({ nexradTimestamp: ts != null ? Number(ts) : null });
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// --- Data and Calculation Methods ---
|
|
1061
|
+
|
|
1062
|
+
_ensureGridDecodeWorker() {
|
|
1063
|
+
if (this._gridDecodeWorkerDisabled) {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
if (this._gridDecodeWorker) {
|
|
1067
|
+
return this._gridDecodeWorker;
|
|
1068
|
+
}
|
|
1069
|
+
if (typeof Worker === 'undefined') {
|
|
1070
|
+
this._gridDecodeWorkerDisabled = true;
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
try {
|
|
1074
|
+
this._gridDecodeWorker = new Worker(new URL('./gridDecodeWorker.js', import.meta.url), {
|
|
1075
|
+
type: 'module',
|
|
1076
|
+
});
|
|
1077
|
+
this._gridDecodeWorker.addEventListener('error', () => {
|
|
1078
|
+
this._gridDecodeWorkerDisabled = true;
|
|
1079
|
+
if (this._gridDecodeWorker) {
|
|
1080
|
+
this._gridDecodeWorker.terminate();
|
|
1081
|
+
this._gridDecodeWorker = null;
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
} catch {
|
|
1085
|
+
this._gridDecodeWorkerDisabled = true;
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
return this._gridDecodeWorker;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Offloads zstd + delta decode + transform to a Worker when available; falls back to main-thread
|
|
1093
|
+
* {@link processCompressedGrid}. Uses a copy for postMessage transfer so the original `compressedData`
|
|
1094
|
+
* stays valid if the Worker path fails.
|
|
1095
|
+
*/
|
|
1096
|
+
_decodeGridPayload(compressedData, encoding) {
|
|
1097
|
+
const worker = this._ensureGridDecodeWorker();
|
|
1098
|
+
if (!worker) {
|
|
1099
|
+
return Promise.resolve(processCompressedGrid(compressedData, encoding));
|
|
1100
|
+
}
|
|
1101
|
+
const payload = compressedData.slice();
|
|
1102
|
+
return new Promise((resolve, reject) => {
|
|
1103
|
+
const id = ++this._gridDecodeMsgId;
|
|
1104
|
+
const onMsg = (e) => {
|
|
1105
|
+
if (!e.data || e.data.id !== id) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
worker.removeEventListener('message', onMsg);
|
|
1109
|
+
worker.removeEventListener('error', onErr);
|
|
1110
|
+
if (e.data.error) {
|
|
1111
|
+
reject(new Error(e.data.error));
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
const data = new Uint8Array(e.data.dataBuffer, e.data.dataByteOffset, e.data.dataByteLength);
|
|
1115
|
+
resolve({ data, encoding: e.data.encoding });
|
|
1116
|
+
};
|
|
1117
|
+
const onErr = (err) => {
|
|
1118
|
+
worker.removeEventListener('message', onMsg);
|
|
1119
|
+
worker.removeEventListener('error', onErr);
|
|
1120
|
+
reject(err);
|
|
1121
|
+
};
|
|
1122
|
+
worker.addEventListener('message', onMsg);
|
|
1123
|
+
worker.addEventListener('error', onErr);
|
|
1124
|
+
try {
|
|
1125
|
+
worker.postMessage(
|
|
1126
|
+
{
|
|
1127
|
+
id,
|
|
1128
|
+
encoding,
|
|
1129
|
+
compressedBuffer: payload.buffer,
|
|
1130
|
+
compressedByteOffset: payload.byteOffset,
|
|
1131
|
+
compressedByteLength: payload.byteLength,
|
|
1132
|
+
},
|
|
1133
|
+
[payload.buffer]
|
|
1134
|
+
);
|
|
1135
|
+
} catch (err) {
|
|
1136
|
+
worker.removeEventListener('message', onMsg);
|
|
1137
|
+
worker.removeEventListener('error', onErr);
|
|
1138
|
+
reject(err);
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
422
1141
|
}
|
|
423
1142
|
|
|
424
1143
|
async _loadGridData(state) {
|
|
425
1144
|
if (this.isReactNative) {
|
|
426
|
-
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
if (state.isNexrad) {
|
|
427
1148
|
return null;
|
|
428
1149
|
}
|
|
429
1150
|
const { model, date, run, forecastHour, variable, isMRMS, mrmsTimestamp } = state;
|
|
@@ -447,18 +1168,7 @@ export class AguaceroCore extends EventEmitter {
|
|
|
447
1168
|
if (this.dataCache.has(dataUrlIdentifier)) {
|
|
448
1169
|
return this.dataCache.get(dataUrlIdentifier);
|
|
449
1170
|
}
|
|
450
|
-
|
|
451
|
-
// --- EDITED ---
|
|
452
|
-
// If we are in React Native, this function should NOT do any work.
|
|
453
|
-
// The native WeatherFrameProcessorModule is now responsible for all data loading.
|
|
454
|
-
// This function might still be called by a "cache miss" fallback, but it
|
|
455
|
-
// should not fetch data from JS anymore. We return null so the fallback knows
|
|
456
|
-
// that the native module is the only source of truth for new data.
|
|
457
|
-
if (this.isReactNative) {
|
|
458
|
-
console.warn(`_loadGridData was called in React Native for ${dataUrlIdentifier}. This should be handled by the native module. Returning null.`);
|
|
459
|
-
return null;
|
|
460
|
-
}
|
|
461
|
-
|
|
1171
|
+
|
|
462
1172
|
const abortController = new AbortController();
|
|
463
1173
|
this.abortControllers.set(dataUrlIdentifier, abortController);
|
|
464
1174
|
|
|
@@ -485,31 +1195,18 @@ export class AguaceroCore extends EventEmitter {
|
|
|
485
1195
|
const { data: b64Data, encoding } = await response.json();
|
|
486
1196
|
const compressedData = Uint8Array.from(atob(b64Data), c => c.charCodeAt(0));
|
|
487
1197
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
this.worker.postMessage({ requestId, compressedData, encoding }, [compressedData.buffer]);
|
|
494
|
-
const result = await workerPromise;
|
|
495
|
-
const finalData = result.data;
|
|
496
|
-
|
|
497
|
-
const transformedData = new Uint8Array(finalData.length);
|
|
498
|
-
for (let i = 0; i < finalData.length; i++) {
|
|
499
|
-
const signedValue = finalData[i] > 127 ? finalData[i] - 256 : finalData[i];
|
|
500
|
-
transformedData[i] = signedValue + 128;
|
|
1198
|
+
let gridPayload;
|
|
1199
|
+
try {
|
|
1200
|
+
gridPayload = await this._decodeGridPayload(compressedData, encoding);
|
|
1201
|
+
} catch {
|
|
1202
|
+
gridPayload = processCompressedGrid(compressedData, encoding);
|
|
501
1203
|
}
|
|
502
1204
|
|
|
503
1205
|
this.abortControllers.delete(dataUrlIdentifier);
|
|
504
1206
|
|
|
505
|
-
return { data:
|
|
1207
|
+
return { data: gridPayload.data, encoding: gridPayload.encoding };
|
|
506
1208
|
|
|
507
1209
|
} catch (error) {
|
|
508
|
-
if (error.name === 'AbortError') {
|
|
509
|
-
console.log(`Request cancelled for ${resourcePath}`);
|
|
510
|
-
} else {
|
|
511
|
-
console.error(`Failed to load data for path ${resourcePath}:`, error);
|
|
512
|
-
}
|
|
513
1210
|
this.dataCache.delete(dataUrlIdentifier);
|
|
514
1211
|
this.abortControllers.delete(dataUrlIdentifier);
|
|
515
1212
|
return null;
|
|
@@ -528,11 +1225,12 @@ export class AguaceroCore extends EventEmitter {
|
|
|
528
1225
|
// Clear both maps
|
|
529
1226
|
this.abortControllers.clear();
|
|
530
1227
|
this.dataCache.clear();
|
|
531
|
-
|
|
532
|
-
console.log('All pending requests cancelled');
|
|
533
1228
|
}
|
|
534
1229
|
|
|
535
1230
|
async getValueAtLngLat(lng, lat) {
|
|
1231
|
+
if (this.state.isSatellite || this.state.isNexrad) {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
536
1234
|
const { variable, isMRMS, mrmsTimestamp, model, date, run, forecastHour, units } = this.state;
|
|
537
1235
|
if (!variable) return null;
|
|
538
1236
|
|
|
@@ -814,74 +1512,11 @@ export class AguaceroCore extends EventEmitter {
|
|
|
814
1512
|
const y = t_y * (ny - 1);
|
|
815
1513
|
return { x, y };
|
|
816
1514
|
} catch (error) {
|
|
817
|
-
console.warn(`[GridAccessor] RGEM polar stereographic conversion failed for ${lat}, ${lon}:`, error);
|
|
818
1515
|
return { x: -1, y: -1 };
|
|
819
1516
|
}
|
|
820
1517
|
}
|
|
821
1518
|
|
|
822
|
-
// ---
|
|
823
|
-
|
|
824
|
-
createWorker() {
|
|
825
|
-
if (this.isReactNative) return null;
|
|
826
|
-
|
|
827
|
-
const workerCode = `
|
|
828
|
-
import { decompress } from 'https://cdn.skypack.dev/fzstd@0.1.1';
|
|
829
|
-
|
|
830
|
-
function _reconstructData(decompressedDeltas, encoding) {
|
|
831
|
-
const expectedLength = encoding.length;
|
|
832
|
-
const reconstructedData = new Int8Array(expectedLength);
|
|
833
|
-
if (decompressedDeltas.length > 0 && expectedLength > 0) {
|
|
834
|
-
reconstructedData[0] = decompressedDeltas[0] > 127 ? decompressedDeltas[0] - 256 : decompressedDeltas[0];
|
|
835
|
-
for (let i = 1; i < expectedLength; i++) {
|
|
836
|
-
const delta = decompressedDeltas[i] > 127 ? decompressedDeltas[i] - 256 : decompressedDeltas[i];
|
|
837
|
-
reconstructedData[i] = reconstructedData[i - 1] + delta;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
return new Uint8Array(reconstructedData.buffer);
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
self.onmessage = async (e) => {
|
|
844
|
-
const { requestId, compressedData, encoding } = e.data;
|
|
845
|
-
try {
|
|
846
|
-
const decompressedDeltas = await decompress(compressedData);
|
|
847
|
-
const finalData = _reconstructData(decompressedDeltas, encoding);
|
|
848
|
-
self.postMessage({ success: true, requestId: requestId, decompressedData: finalData, encoding: encoding }, [finalData.buffer]);
|
|
849
|
-
} catch (error) {
|
|
850
|
-
self.postMessage({ success: false, requestId: requestId, error: error.message });
|
|
851
|
-
}
|
|
852
|
-
};
|
|
853
|
-
`;
|
|
854
|
-
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
855
|
-
return new Worker(URL.createObjectURL(blob), { type: 'module' });
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
_processResultQueue() {
|
|
859
|
-
while (this.resultQueue.length > 0) {
|
|
860
|
-
const { success, requestId, decompressedData, encoding, error } = this.resultQueue.shift();
|
|
861
|
-
if (this.workerResolvers.has(requestId)) {
|
|
862
|
-
const { resolve, reject } = this.workerResolvers.get(requestId);
|
|
863
|
-
if (success) {
|
|
864
|
-
resolve({ data: decompressedData }); // Return as { data: ... }
|
|
865
|
-
} else {
|
|
866
|
-
reject(new Error(error));
|
|
867
|
-
}
|
|
868
|
-
this.workerResolvers.delete(requestId);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
this.isProcessingQueue = false;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
_handleWorkerMessage(e) {
|
|
875
|
-
if (this.isReactNative) return;
|
|
876
|
-
|
|
877
|
-
const { success, requestId, decompressedData, encoding, error } = e.data;
|
|
878
|
-
|
|
879
|
-
this.resultQueue.push({ success, requestId, decompressedData, encoding, error });
|
|
880
|
-
if (!this.isProcessingQueue) {
|
|
881
|
-
this.isProcessingQueue = true;
|
|
882
|
-
requestAnimationFrame(() => this._processResultQueue());
|
|
883
|
-
}
|
|
884
|
-
}
|
|
1519
|
+
// --- Status Methods ---
|
|
885
1520
|
|
|
886
1521
|
async fetchModelStatus(force = false) {
|
|
887
1522
|
if (!this.modelStatus || force) {
|
|
@@ -906,6 +1541,19 @@ export class AguaceroCore extends EventEmitter {
|
|
|
906
1541
|
return this.mrmsStatus;
|
|
907
1542
|
}
|
|
908
1543
|
|
|
1544
|
+
async fetchSatelliteListing(force = false) {
|
|
1545
|
+
if (!this.satelliteListing || force) {
|
|
1546
|
+
try {
|
|
1547
|
+
const response = await fetch(SATELLITE_FRAMES_URL);
|
|
1548
|
+
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
|
1549
|
+
this.satelliteListing = await response.json();
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
this.satelliteListing = null;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return this.satelliteListing;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
909
1557
|
startAutoRefresh(intervalSeconds) {
|
|
910
1558
|
this.stopAutoRefresh();
|
|
911
1559
|
this.autoRefreshIntervalId = setInterval(async () => {
|