@aguacerowx/javascript-sdk 0.0.18 → 0.0.21
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 +893 -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 +26 -1
- package/src/nexradTilts.js +128 -0
- package/src/nexrad_level3_catalog.js +26 -0
- package/src/nexrad_support.js +287 -0
- package/src/satellite_support.js +376 -0
package/src/AguaceroCore.js
CHANGED
|
@@ -7,7 +7,32 @@ 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
|
+
buildSatelliteTimelineForSelection,
|
|
15
|
+
formatTimelineDurationValue,
|
|
16
|
+
resolveSatelliteSectorLabel,
|
|
17
|
+
resolveSatelliteDurationOption,
|
|
18
|
+
parseTimelineDurationHours,
|
|
19
|
+
} from './satellite_support.js';
|
|
20
|
+
import {
|
|
21
|
+
fetchNexradTimesListing,
|
|
22
|
+
inferNexradDataSourceForProduct,
|
|
23
|
+
nexradColormapFldKey,
|
|
24
|
+
variableToNexradGroup,
|
|
25
|
+
getAvailableNexradTilts,
|
|
26
|
+
} from './nexrad_support.js';
|
|
27
|
+
import { NEXRAD_LEVEL3_ELEV, getNexradLevel3EntryByRadarKey } from './nexrad_level3_catalog.js';
|
|
28
|
+
import {
|
|
29
|
+
setRadarTiltsManifest,
|
|
30
|
+
fetchRadarTiltsManifestFromNetwork,
|
|
31
|
+
getDefaultRadarTilt,
|
|
32
|
+
formatTiltForApi,
|
|
33
|
+
clampNexradTiltForVariable,
|
|
34
|
+
getRadarTilts,
|
|
35
|
+
} from './nexradTilts.js';
|
|
11
36
|
|
|
12
37
|
// --- Non-UI Helper Functions ---
|
|
13
38
|
function hrdpsObliqueTransform(rotated_lon, rotated_lat) {
|
|
@@ -41,26 +66,67 @@ function findLatestModelRun(modelsData, modelName) {
|
|
|
41
66
|
return null;
|
|
42
67
|
}
|
|
43
68
|
|
|
69
|
+
/**
|
|
70
|
+
* model-status JSON uses string keys for runs (often zero-padded: "00", "06").
|
|
71
|
+
* Direct lookup modelStatus[model][date][run] fails if state.run is "6" but the key is "06".
|
|
72
|
+
* Returns the hour list and the run key that matched.
|
|
73
|
+
*/
|
|
74
|
+
function resolveModelRunHours(modelStatus, model, date, run) {
|
|
75
|
+
const runs = modelStatus?.[model]?.[date];
|
|
76
|
+
if (!runs || run == null || run === '') {
|
|
77
|
+
return {
|
|
78
|
+
hours: [],
|
|
79
|
+
matchedRunKey: null,
|
|
80
|
+
availableRunKeys: runs ? Object.keys(runs) : [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const runStr = String(run);
|
|
84
|
+
const candidates = new Set([runStr]);
|
|
85
|
+
const n = parseInt(runStr, 10);
|
|
86
|
+
if (!Number.isNaN(n)) {
|
|
87
|
+
candidates.add(String(n));
|
|
88
|
+
candidates.add(String(n).padStart(2, '0'));
|
|
89
|
+
candidates.add(String(n).padStart(3, '0'));
|
|
90
|
+
}
|
|
91
|
+
for (const key of candidates) {
|
|
92
|
+
const h = runs[key];
|
|
93
|
+
if (h && Array.isArray(h) && h.length > 0) {
|
|
94
|
+
return { hours: h, matchedRunKey: key, availableRunKeys: Object.keys(runs) };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!Number.isNaN(n)) {
|
|
98
|
+
for (const k of Object.keys(runs)) {
|
|
99
|
+
const kn = parseInt(k, 10);
|
|
100
|
+
if (!Number.isNaN(kn) && kn === n) {
|
|
101
|
+
const h = runs[k];
|
|
102
|
+
if (h && Array.isArray(h) && h.length > 0) {
|
|
103
|
+
return { hours: h, matchedRunKey: k, availableRunKeys: Object.keys(runs) };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { hours: [], matchedRunKey: null, availableRunKeys: Object.keys(runs) };
|
|
109
|
+
}
|
|
110
|
+
|
|
44
111
|
export class AguaceroCore extends EventEmitter {
|
|
45
112
|
constructor(options = {}) {
|
|
46
113
|
super();
|
|
47
114
|
this.isReactNative = typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
48
115
|
this.apiKey = options.apiKey;
|
|
116
|
+
/** Passed as CloudFront `userId` for satellite KTX2 URLs (production uses the authenticated account id). */
|
|
117
|
+
this.userId = options.userId ?? 'sdk-user';
|
|
49
118
|
this.bundleId = getBundleId();
|
|
50
119
|
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
|
-
}
|
|
120
|
+
/** @type {Worker | null} */
|
|
121
|
+
this._gridDecodeWorker = null;
|
|
122
|
+
/** When true, skip Worker and use {@link processCompressedGrid} on the main thread. */
|
|
123
|
+
this._gridDecodeWorkerDisabled = false;
|
|
124
|
+
this._gridDecodeMsgId = 0;
|
|
61
125
|
this.statusUrl = 'https://d3dc62msmxkrd7.cloudfront.net/model-status';
|
|
62
126
|
this.modelStatus = null;
|
|
63
127
|
this.mrmsStatus = null;
|
|
128
|
+
/** @type {{ objects?: Array<{ key: string }> } | null} */
|
|
129
|
+
this.satelliteListing = null;
|
|
64
130
|
this.dataCache = new Map();
|
|
65
131
|
this.abortControllers = new Map();
|
|
66
132
|
this.isPlaying = false;
|
|
@@ -73,31 +139,237 @@ export class AguaceroCore extends EventEmitter {
|
|
|
73
139
|
const initialMode = userLayerOptions.mode || 'model';
|
|
74
140
|
const initialVariable = userLayerOptions.variable || null;
|
|
75
141
|
|
|
142
|
+
const initialSatellite = initialMode === 'satellite';
|
|
143
|
+
const initialNexrad = initialMode === 'nexrad';
|
|
144
|
+
const initialNexradProd = userLayerOptions.nexradProduct || 'REF';
|
|
145
|
+
const initialNexradDs =
|
|
146
|
+
userLayerOptions.nexradDataSource != null
|
|
147
|
+
? userLayerOptions.nexradDataSource === 'level3'
|
|
148
|
+
? 'level3'
|
|
149
|
+
: 'level2'
|
|
150
|
+
: inferNexradDataSourceForProduct(initialNexradProd);
|
|
151
|
+
const initialNexradFld = initialNexrad
|
|
152
|
+
? nexradColormapFldKey(initialNexradDs, initialNexradProd)
|
|
153
|
+
: initialVariable;
|
|
154
|
+
let initialSatelliteInstrumentId = null;
|
|
155
|
+
let initialSatelliteSectorLabel = null;
|
|
156
|
+
let initialSatelliteChannel = null;
|
|
157
|
+
if (initialSatellite) {
|
|
158
|
+
initialSatelliteInstrumentId = userLayerOptions.satelliteId ?? 'GOES19-EAST';
|
|
159
|
+
initialSatelliteSectorLabel = resolveSatelliteSectorLabel(
|
|
160
|
+
userLayerOptions.satelliteSector ?? userLayerOptions.sector ?? 'conus',
|
|
161
|
+
);
|
|
162
|
+
initialSatelliteChannel =
|
|
163
|
+
userLayerOptions.satelliteProduct ??
|
|
164
|
+
userLayerOptions.satelliteChannel ??
|
|
165
|
+
initialVariable ??
|
|
166
|
+
'C13';
|
|
167
|
+
}
|
|
76
168
|
this.state = {
|
|
77
169
|
model: userLayerOptions.model || 'gfs',
|
|
78
170
|
// EDIT: Set isMRMS based on the initial mode
|
|
79
|
-
isMRMS: initialMode === 'mrms',
|
|
171
|
+
isMRMS: initialMode === 'mrms' && !initialSatellite && !initialNexrad,
|
|
80
172
|
mrmsTimestamp: null,
|
|
81
|
-
variable:
|
|
173
|
+
variable: initialNexrad
|
|
174
|
+
? initialNexradFld
|
|
175
|
+
: initialSatellite && initialSatelliteInstrumentId
|
|
176
|
+
? initialSatelliteChannel
|
|
177
|
+
: initialVariable,
|
|
82
178
|
date: null,
|
|
83
179
|
run: null,
|
|
84
180
|
forecastHour: 0,
|
|
85
181
|
visible: true,
|
|
86
182
|
opacity: userLayerOptions.opacity ?? 1,
|
|
87
183
|
units: options.initialUnit || 'imperial',
|
|
88
|
-
shaderSmoothingEnabled: options.shaderSmoothingEnabled ?? true
|
|
184
|
+
shaderSmoothingEnabled: options.shaderSmoothingEnabled ?? true,
|
|
185
|
+
isSatellite: initialSatellite,
|
|
186
|
+
satelliteInstrumentId: initialSatelliteInstrumentId,
|
|
187
|
+
satelliteSectorLabel: initialSatelliteSectorLabel,
|
|
188
|
+
satelliteChannel: initialSatelliteChannel,
|
|
189
|
+
satelliteTimestamp: userLayerOptions.satelliteTimestamp != null ? Number(userLayerOptions.satelliteTimestamp) : null,
|
|
190
|
+
satelliteTier: userLayerOptions.satelliteTier || 'basic',
|
|
191
|
+
satelliteDurationValue: formatTimelineDurationValue(
|
|
192
|
+
userLayerOptions.satelliteDurationValue != null ? userLayerOptions.satelliteDurationValue : '1',
|
|
193
|
+
),
|
|
194
|
+
mrmsDurationValue: formatTimelineDurationValue(
|
|
195
|
+
userLayerOptions.mrmsDurationValue != null ? userLayerOptions.mrmsDurationValue : '1',
|
|
196
|
+
),
|
|
197
|
+
nexradDurationValue: formatTimelineDurationValue(
|
|
198
|
+
userLayerOptions.nexradDurationValue != null ? userLayerOptions.nexradDurationValue : '1',
|
|
199
|
+
),
|
|
200
|
+
isNexrad: initialNexrad,
|
|
201
|
+
nexradSite: userLayerOptions.nexradSite ?? null,
|
|
202
|
+
nexradDataSource: initialNexradDs,
|
|
203
|
+
nexradProduct: initialNexradProd,
|
|
204
|
+
nexradTilt:
|
|
205
|
+
userLayerOptions.nexradTilt != null
|
|
206
|
+
? Number(userLayerOptions.nexradTilt)
|
|
207
|
+
: userLayerOptions.nexradSite
|
|
208
|
+
? getDefaultRadarTilt(userLayerOptions.nexradSite)
|
|
209
|
+
: null,
|
|
210
|
+
nexradTimestamp: userLayerOptions.nexradTimestamp != null ? Number(userLayerOptions.nexradTimestamp) : null,
|
|
211
|
+
nexradStormRelative: userLayerOptions.nexradStormRelative === true,
|
|
212
|
+
/** When true, mapsgl shows clickable NEXRAD site markers (independent of selected site). */
|
|
213
|
+
nexradShowSitesPicker: userLayerOptions.nexradShowSitesPicker !== false,
|
|
89
214
|
};
|
|
90
215
|
|
|
91
216
|
this.autoRefreshEnabled = options.autoRefresh ?? false;
|
|
92
217
|
this.autoRefreshIntervalSeconds = options.autoRefreshInterval ?? 60;
|
|
93
218
|
this.autoRefreshIntervalId = null;
|
|
219
|
+
|
|
220
|
+
/** @type {Record<string, { unixTimes?: number[]; timeToKeyMap?: Record<string, string>; listWindowHours?: number }>} */
|
|
221
|
+
this.nexradTimesByStation = {};
|
|
94
222
|
}
|
|
95
223
|
|
|
96
224
|
async setState(newState) {
|
|
97
|
-
|
|
225
|
+
const patch = { ...newState };
|
|
226
|
+
if ('satelliteKey' in patch) delete patch.satelliteKey;
|
|
227
|
+
if ('forecastHour' in patch && patch.forecastHour != null) {
|
|
228
|
+
patch.forecastHour = Number(patch.forecastHour);
|
|
229
|
+
}
|
|
230
|
+
if ('mrmsTimestamp' in patch && patch.mrmsTimestamp != null) {
|
|
231
|
+
patch.mrmsTimestamp = Number(patch.mrmsTimestamp);
|
|
232
|
+
}
|
|
233
|
+
if ('satelliteTimestamp' in patch && patch.satelliteTimestamp != null) {
|
|
234
|
+
patch.satelliteTimestamp = Number(patch.satelliteTimestamp);
|
|
235
|
+
}
|
|
236
|
+
if ('satelliteDurationValue' in patch && patch.satelliteDurationValue != null) {
|
|
237
|
+
patch.satelliteDurationValue = formatTimelineDurationValue(patch.satelliteDurationValue);
|
|
238
|
+
}
|
|
239
|
+
if ('mrmsDurationValue' in patch && patch.mrmsDurationValue != null) {
|
|
240
|
+
patch.mrmsDurationValue = formatTimelineDurationValue(patch.mrmsDurationValue);
|
|
241
|
+
}
|
|
242
|
+
if ('nexradDurationValue' in patch && patch.nexradDurationValue != null) {
|
|
243
|
+
patch.nexradDurationValue = formatTimelineDurationValue(patch.nexradDurationValue);
|
|
244
|
+
}
|
|
245
|
+
if ('nexradTimestamp' in patch && patch.nexradTimestamp != null) {
|
|
246
|
+
patch.nexradTimestamp = Number(patch.nexradTimestamp);
|
|
247
|
+
}
|
|
248
|
+
if ('nexradTilt' in patch && patch.nexradTilt != null) {
|
|
249
|
+
patch.nexradTilt = Number(patch.nexradTilt);
|
|
250
|
+
}
|
|
251
|
+
Object.assign(this.state, patch);
|
|
98
252
|
this._emitStateChange();
|
|
99
253
|
}
|
|
100
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Forecast hours for the current model/date/run (normalized numbers).
|
|
257
|
+
* Tolerates model-status run keys like "06" vs state.run "6" so preload and slider stay in sync.
|
|
258
|
+
*/
|
|
259
|
+
getAvailableForecastHours() {
|
|
260
|
+
if (this.state.isMRMS || this.state.isSatellite || this.state.isNexrad) return [];
|
|
261
|
+
if (!this.state.model || this.state.date == null || this.state.run == null) return [];
|
|
262
|
+
|
|
263
|
+
const resolved = resolveModelRunHours(
|
|
264
|
+
this.modelStatus,
|
|
265
|
+
this.state.model,
|
|
266
|
+
this.state.date,
|
|
267
|
+
this.state.run
|
|
268
|
+
);
|
|
269
|
+
let hours = resolved.hours || [];
|
|
270
|
+
|
|
271
|
+
if (hours.length > 0) {
|
|
272
|
+
hours = hours.map(h => (typeof h === 'string' ? parseInt(h, 10) : Number(h))).filter(h => !Number.isNaN(h));
|
|
273
|
+
}
|
|
274
|
+
if (this.state.variable === 'ptypeRefl' && this.state.model === 'hrrr' && hours.length > 0) {
|
|
275
|
+
hours = hours.filter(hour => hour !== 0);
|
|
276
|
+
}
|
|
277
|
+
return hours;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_computeSatelliteTimeline() {
|
|
281
|
+
const { satelliteInstrumentId, satelliteSectorLabel, satelliteChannel } = this.state;
|
|
282
|
+
if (!satelliteInstrumentId || !satelliteSectorLabel || !satelliteChannel || !this.satelliteListing?.objects) {
|
|
283
|
+
return { unixTimes: [], timeToFileMap: {} };
|
|
284
|
+
}
|
|
285
|
+
const allFiles = this.satelliteListing.objects.map((o) => o.key);
|
|
286
|
+
const sectorName = satelliteSectorLabel;
|
|
287
|
+
const tier = this.state.satelliteTier || 'basic';
|
|
288
|
+
const durationOpt = resolveSatelliteDurationOption(sectorName, tier, this.state.satelliteDurationValue);
|
|
289
|
+
return buildSatelliteTimelineForSelection(
|
|
290
|
+
{
|
|
291
|
+
satelliteInstrumentId,
|
|
292
|
+
satelliteSectorLabel,
|
|
293
|
+
satelliteChannel,
|
|
294
|
+
},
|
|
295
|
+
allFiles,
|
|
296
|
+
durationOpt,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* MRMS timestamps for a variable, oldest first (matches satellite timelines and left→right time sliders).
|
|
302
|
+
* Limited to the last N hours relative to the newest frame (see mrmsDurationValue).
|
|
303
|
+
* @param {string} variable
|
|
304
|
+
* @returns {number[]}
|
|
305
|
+
*/
|
|
306
|
+
_getFilteredMrmsTimestampsForVariable(variable) {
|
|
307
|
+
const raw = this.mrmsStatus?.[variable];
|
|
308
|
+
if (!raw || !raw.length) return [];
|
|
309
|
+
const hours = parseTimelineDurationHours(this.state.mrmsDurationValue);
|
|
310
|
+
let list = [...raw]
|
|
311
|
+
.map((t) => Number(t))
|
|
312
|
+
.filter((t) => !Number.isNaN(t))
|
|
313
|
+
.sort((a, b) => a - b);
|
|
314
|
+
if (hours > 0 && list.length > 0) {
|
|
315
|
+
const latest = list[list.length - 1];
|
|
316
|
+
const cutoff = latest - hours * 3600;
|
|
317
|
+
list = list.filter((t) => t >= cutoff);
|
|
318
|
+
}
|
|
319
|
+
return list;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_nexradListingWindowHours() {
|
|
323
|
+
return parseTimelineDurationHours(this.state.nexradDurationValue);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
_getFilteredNexradTimestampsForVariable(rawList) {
|
|
327
|
+
if (!rawList || !rawList.length) return [];
|
|
328
|
+
const hours = this._nexradListingWindowHours();
|
|
329
|
+
let list = [...rawList]
|
|
330
|
+
.map((t) => Number(t))
|
|
331
|
+
.filter((t) => !Number.isNaN(t))
|
|
332
|
+
.sort((a, b) => a - b);
|
|
333
|
+
if (hours > 0 && list.length > 0) {
|
|
334
|
+
const latest = list[list.length - 1];
|
|
335
|
+
const cutoff = latest - hours * 3600;
|
|
336
|
+
list = list.filter((t) => t >= cutoff);
|
|
337
|
+
}
|
|
338
|
+
return list;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Cache key for {@link this.nexradTimesByStation} — matches frontend composite keys (tilt + variable group/source).
|
|
343
|
+
*/
|
|
344
|
+
_nexradTimesCacheKey() {
|
|
345
|
+
const s = this.state;
|
|
346
|
+
if (!s.isNexrad || !s.nexradSite) return null;
|
|
347
|
+
const site = s.nexradSite;
|
|
348
|
+
const variable = s.nexradProduct || 'REF';
|
|
349
|
+
const ds = s.nexradDataSource || 'level2';
|
|
350
|
+
const tiltNum = s.nexradTilt != null ? s.nexradTilt : getDefaultRadarTilt(site);
|
|
351
|
+
const elevNormUse =
|
|
352
|
+
ds === 'level3'
|
|
353
|
+
? NEXRAD_LEVEL3_ELEV
|
|
354
|
+
: formatTiltForApi(clampNexradTiltForVariable(site, variable, tiltNum));
|
|
355
|
+
const group = ds === 'level3' ? 'l3' : variableToNexradGroup(variable);
|
|
356
|
+
const l3Product =
|
|
357
|
+
ds === 'level3'
|
|
358
|
+
? getNexradLevel3EntryByRadarKey(variable)?.product ?? (variable === 'VEL' ? 'N0G' : variable)
|
|
359
|
+
: '';
|
|
360
|
+
if (ds === 'level3') {
|
|
361
|
+
return `${site}_l3_${l3Product}_${elevNormUse}`;
|
|
362
|
+
}
|
|
363
|
+
return `${site}_${group}_${elevNormUse}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Storm-relative Level-III velocity (N0G + N0S) — matches aguacero-frontend `level3StormRelative` for VEL. */
|
|
367
|
+
_nexradStormRelativeFor(nexradDataSource, nexradProduct) {
|
|
368
|
+
const ds = nexradDataSource === 'level3' ? 'level3' : 'level2';
|
|
369
|
+
const p = (nexradProduct || 'REF').toUpperCase();
|
|
370
|
+
return ds === 'level3' && p === 'VEL';
|
|
371
|
+
}
|
|
372
|
+
|
|
101
373
|
_emitStateChange() {
|
|
102
374
|
const { colormap, baseUnit } = this._getColormapForVariable(this.state.variable);
|
|
103
375
|
const toUnit = this._getTargetUnit(baseUnit, this.state.units);
|
|
@@ -105,18 +377,40 @@ export class AguaceroCore extends EventEmitter {
|
|
|
105
377
|
|
|
106
378
|
let availableTimestamps = [];
|
|
107
379
|
if (this.state.isMRMS && this.state.variable && this.mrmsStatus) {
|
|
108
|
-
|
|
109
|
-
availableTimestamps = [...timestamps].reverse();
|
|
380
|
+
availableTimestamps = this._getFilteredMrmsTimestampsForVariable(this.state.variable);
|
|
110
381
|
}
|
|
111
382
|
|
|
112
|
-
let
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
383
|
+
let availableSatelliteTimestamps = [];
|
|
384
|
+
let satelliteTimeToFileMap = {};
|
|
385
|
+
if (this.state.isSatellite && this.state.satelliteInstrumentId) {
|
|
386
|
+
const timeline = this._computeSatelliteTimeline();
|
|
387
|
+
satelliteTimeToFileMap = timeline.timeToFileMap || {};
|
|
388
|
+
availableSatelliteTimestamps = [...(timeline.unixTimes || [])]
|
|
389
|
+
.map((t) => Number(t))
|
|
390
|
+
.filter((t) => !Number.isNaN(t))
|
|
391
|
+
.sort((a, b) => a - b);
|
|
118
392
|
}
|
|
119
393
|
|
|
394
|
+
let availableNexradTimestamps = [];
|
|
395
|
+
let nexradTimeToKeyMap = {};
|
|
396
|
+
let nexradLevel3MotionTimeToKeyMap = {};
|
|
397
|
+
let availableNexradTilts = [];
|
|
398
|
+
if (this.state.isNexrad && this.state.nexradSite) {
|
|
399
|
+
const nk = this._nexradTimesCacheKey();
|
|
400
|
+
const ent = nk ? this.nexradTimesByStation[nk] : null;
|
|
401
|
+
const raw = ent?.unixTimes || [];
|
|
402
|
+
availableNexradTimestamps = this._getFilteredNexradTimestampsForVariable(raw);
|
|
403
|
+
nexradTimeToKeyMap = ent?.timeToKeyMap || {};
|
|
404
|
+
nexradLevel3MotionTimeToKeyMap = ent?.level3MotionTimeToKeyMap || {};
|
|
405
|
+
availableNexradTilts = getAvailableNexradTilts(
|
|
406
|
+
this.state.nexradSite,
|
|
407
|
+
this.state.nexradDataSource || 'level2',
|
|
408
|
+
this.state.nexradProduct || 'REF',
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const availableHours = this.getAvailableForecastHours();
|
|
413
|
+
|
|
120
414
|
const eventPayload = {
|
|
121
415
|
...this.state,
|
|
122
416
|
availableModels: this.modelStatus ? Object.keys(this.modelStatus).sort() : [],
|
|
@@ -126,6 +420,12 @@ export class AguaceroCore extends EventEmitter {
|
|
|
126
420
|
// We need to confirm this line is working as expected.
|
|
127
421
|
availableMRMSVariables: this.getAvailableVariables('mrms'),
|
|
128
422
|
availableTimestamps: availableTimestamps,
|
|
423
|
+
availableSatelliteTimestamps,
|
|
424
|
+
satelliteTimeToFileMap,
|
|
425
|
+
availableNexradTimestamps,
|
|
426
|
+
nexradTimeToKeyMap,
|
|
427
|
+
nexradLevel3MotionTimeToKeyMap,
|
|
428
|
+
availableNexradTilts,
|
|
129
429
|
isPlaying: this.isPlaying,
|
|
130
430
|
colormap: displayColormap,
|
|
131
431
|
colormapBaseUnit: toUnit,
|
|
@@ -137,27 +437,36 @@ export class AguaceroCore extends EventEmitter {
|
|
|
137
437
|
async initialize(options = {}) {
|
|
138
438
|
await this.fetchModelStatus(true);
|
|
139
439
|
await this.fetchMRMSStatus(true);
|
|
140
|
-
|
|
440
|
+
await this.fetchSatelliteListing(true);
|
|
441
|
+
|
|
141
442
|
let initialState = { ...this.state };
|
|
142
443
|
|
|
444
|
+
if (initialState.isSatellite && initialState.satelliteInstrumentId) {
|
|
445
|
+
const timeline = this._computeSatelliteTimeline();
|
|
446
|
+
const tsList = [...(timeline.unixTimes || [])].sort((a, b) => a - b);
|
|
447
|
+
if (initialState.satelliteTimestamp == null && tsList.length > 0) {
|
|
448
|
+
initialState.satelliteTimestamp = tsList[tsList.length - 1];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
143
452
|
// ADD: Logic to handle an initial MRMS state
|
|
144
453
|
if (initialState.isMRMS) {
|
|
145
454
|
const variable = initialState.variable;
|
|
146
455
|
if (variable && this.mrmsStatus && this.mrmsStatus[variable]) {
|
|
147
|
-
const sortedTimestamps =
|
|
148
|
-
initialState.mrmsTimestamp =
|
|
456
|
+
const sortedTimestamps = this._getFilteredMrmsTimestampsForVariable(variable);
|
|
457
|
+
initialState.mrmsTimestamp =
|
|
458
|
+
sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1] : null;
|
|
149
459
|
} else {
|
|
150
|
-
// Fallback if the provided variable is not valid
|
|
151
|
-
console.warn(`Initial MRMS variable '${variable}' not found. Using default.`);
|
|
152
460
|
const availableMRMSVars = this.getAvailableVariables('mrms');
|
|
153
461
|
if (availableMRMSVars.length > 0) {
|
|
154
462
|
const firstVar = availableMRMSVars[0];
|
|
155
463
|
initialState.variable = firstVar;
|
|
156
|
-
const sortedTimestamps =
|
|
157
|
-
initialState.mrmsTimestamp =
|
|
464
|
+
const sortedTimestamps = this._getFilteredMrmsTimestampsForVariable(firstVar);
|
|
465
|
+
initialState.mrmsTimestamp =
|
|
466
|
+
sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1] : null;
|
|
158
467
|
}
|
|
159
468
|
}
|
|
160
|
-
} else {
|
|
469
|
+
} else if (!initialState.isSatellite && !initialState.isNexrad) {
|
|
161
470
|
// EDIT: This is the existing logic, now in an else block
|
|
162
471
|
const latestRun = findLatestModelRun(this.modelStatus, initialState.model);
|
|
163
472
|
if (latestRun) {
|
|
@@ -172,6 +481,14 @@ export class AguaceroCore extends EventEmitter {
|
|
|
172
481
|
}
|
|
173
482
|
|
|
174
483
|
await this.setState(initialState);
|
|
484
|
+
|
|
485
|
+
if (this.state.isNexrad) {
|
|
486
|
+
const manifest = await fetchRadarTiltsManifestFromNetwork();
|
|
487
|
+
if (manifest) setRadarTiltsManifest(manifest);
|
|
488
|
+
if (this.state.nexradSite) {
|
|
489
|
+
await this.refreshNexradTimes();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
175
492
|
if (options.autoRefresh ?? this.autoRefreshEnabled) {
|
|
176
493
|
this.startAutoRefresh(options.refreshInterval ?? this.autoRefreshIntervalSeconds);
|
|
177
494
|
}
|
|
@@ -181,11 +498,11 @@ export class AguaceroCore extends EventEmitter {
|
|
|
181
498
|
this.pause();
|
|
182
499
|
this.stopAutoRefresh();
|
|
183
500
|
this.dataCache.clear();
|
|
184
|
-
if (this.worker) {
|
|
185
|
-
this.worker.terminate();
|
|
186
|
-
}
|
|
187
501
|
this.callbacks = {};
|
|
188
|
-
|
|
502
|
+
if (this._gridDecodeWorker) {
|
|
503
|
+
this._gridDecodeWorker.terminate();
|
|
504
|
+
this._gridDecodeWorker = null;
|
|
505
|
+
}
|
|
189
506
|
}
|
|
190
507
|
|
|
191
508
|
// --- Public API Methods ---
|
|
@@ -213,24 +530,40 @@ export class AguaceroCore extends EventEmitter {
|
|
|
213
530
|
}
|
|
214
531
|
|
|
215
532
|
step(direction = 1) {
|
|
533
|
+
if (this.state.isSatellite) {
|
|
534
|
+
const timeline = this._computeSatelliteTimeline();
|
|
535
|
+
const availableTimestamps = [...(timeline.unixTimes || [])]
|
|
536
|
+
.sort((a, b) => a - b)
|
|
537
|
+
.map((t) => Number(t));
|
|
538
|
+
if (availableTimestamps.length === 0) return;
|
|
539
|
+
|
|
540
|
+
const ts = this.state.satelliteTimestamp == null ? null : Number(this.state.satelliteTimestamp);
|
|
541
|
+
const currentIndex = ts == null ? -1 : availableTimestamps.indexOf(ts);
|
|
542
|
+
|
|
543
|
+
let nextIndex = currentIndex === -1 ? availableTimestamps.length - 1 : currentIndex + direction;
|
|
544
|
+
const maxIndex = availableTimestamps.length - 1;
|
|
545
|
+
if (nextIndex > maxIndex) nextIndex = 0;
|
|
546
|
+
if (nextIndex < 0) nextIndex = maxIndex;
|
|
547
|
+
|
|
548
|
+
this.setState({ satelliteTimestamp: availableTimestamps[nextIndex] });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
216
551
|
// --- THIS IS THE CORRECTED MRMS LOGIC ---
|
|
217
552
|
if (this.state.isMRMS) {
|
|
218
553
|
const { variable, mrmsTimestamp } = this.state;
|
|
219
554
|
if (!this.mrmsStatus || !this.mrmsStatus[variable]) {
|
|
220
|
-
console.warn('[Core.step] MRMS status or variable not available.');
|
|
221
555
|
return;
|
|
222
556
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// The step logic MUST use the same reversed array for indexes to match.
|
|
226
|
-
const availableTimestamps = [...(this.mrmsStatus[variable] || [])].reverse();
|
|
557
|
+
|
|
558
|
+
const availableTimestamps = this._getFilteredMrmsTimestampsForVariable(variable);
|
|
227
559
|
if (availableTimestamps.length === 0) return;
|
|
228
560
|
|
|
229
|
-
const
|
|
561
|
+
const ts = mrmsTimestamp == null ? null : Number(mrmsTimestamp);
|
|
562
|
+
const currentIndex = ts == null ? -1 : availableTimestamps.indexOf(ts);
|
|
230
563
|
|
|
231
564
|
if (currentIndex === -1) {
|
|
232
|
-
// If not found, reset to the
|
|
233
|
-
this.setState({ mrmsTimestamp: availableTimestamps[
|
|
565
|
+
// If not found, reset to the latest frame (end of ascending list)
|
|
566
|
+
this.setState({ mrmsTimestamp: availableTimestamps[availableTimestamps.length - 1] });
|
|
234
567
|
return;
|
|
235
568
|
}
|
|
236
569
|
|
|
@@ -244,12 +577,28 @@ export class AguaceroCore extends EventEmitter {
|
|
|
244
577
|
const newTimestamp = availableTimestamps[nextIndex];
|
|
245
578
|
this.setState({ mrmsTimestamp: newTimestamp });
|
|
246
579
|
|
|
580
|
+
} else if (this.state.isNexrad) {
|
|
581
|
+
const nk = this._nexradTimesCacheKey();
|
|
582
|
+
const raw = nk ? this.nexradTimesByStation[nk]?.unixTimes : [];
|
|
583
|
+
const availableTimestamps = this._getFilteredNexradTimestampsForVariable(raw || []);
|
|
584
|
+
if (availableTimestamps.length === 0) return;
|
|
585
|
+
|
|
586
|
+
const ts = this.state.nexradTimestamp == null ? null : Number(this.state.nexradTimestamp);
|
|
587
|
+
const currentIndex = ts == null ? -1 : availableTimestamps.indexOf(ts);
|
|
588
|
+
|
|
589
|
+
let nextIndex = currentIndex === -1 ? availableTimestamps.length - 1 : currentIndex + direction;
|
|
590
|
+
const maxIndex = availableTimestamps.length - 1;
|
|
591
|
+
if (nextIndex > maxIndex) nextIndex = 0;
|
|
592
|
+
if (nextIndex < 0) nextIndex = maxIndex;
|
|
593
|
+
|
|
594
|
+
this.setState({ nexradTimestamp: availableTimestamps[nextIndex] });
|
|
247
595
|
} else {
|
|
248
|
-
const {
|
|
249
|
-
const forecastHours = this.
|
|
596
|
+
const { forecastHour } = this.state;
|
|
597
|
+
const forecastHours = this.getAvailableForecastHours();
|
|
250
598
|
if (!forecastHours || forecastHours.length === 0) return;
|
|
251
599
|
|
|
252
|
-
const
|
|
600
|
+
const fh = Number(forecastHour);
|
|
601
|
+
const currentIndex = forecastHours.indexOf(fh);
|
|
253
602
|
if (currentIndex === -1) return;
|
|
254
603
|
|
|
255
604
|
const maxIndex = forecastHours.length - 1;
|
|
@@ -283,7 +632,12 @@ export class AguaceroCore extends EventEmitter {
|
|
|
283
632
|
async setVariable(variable) {
|
|
284
633
|
// --- NEW CODE: Handle switching TO ptypeRefl on HRRR ---
|
|
285
634
|
if (variable === 'ptypeRefl' && this.state.model === 'hrrr' && this.state.forecastHour === 0) {
|
|
286
|
-
const availableHours =
|
|
635
|
+
const availableHours = resolveModelRunHours(
|
|
636
|
+
this.modelStatus,
|
|
637
|
+
this.state.model,
|
|
638
|
+
this.state.date,
|
|
639
|
+
this.state.run
|
|
640
|
+
).hours || [];
|
|
287
641
|
const firstValidHour = availableHours.find(hour => hour !== 0) || 0;
|
|
288
642
|
await this.setState({ variable, forecastHour: firstValidHour });
|
|
289
643
|
return;
|
|
@@ -295,6 +649,15 @@ export class AguaceroCore extends EventEmitter {
|
|
|
295
649
|
|
|
296
650
|
async setModel(modelName) {
|
|
297
651
|
if (modelName === this.state.model || !this.modelStatus?.[modelName]) return;
|
|
652
|
+
if (this.state.isSatellite) {
|
|
653
|
+
await this.setState({
|
|
654
|
+
isSatellite: false,
|
|
655
|
+
satelliteInstrumentId: null,
|
|
656
|
+
satelliteSectorLabel: null,
|
|
657
|
+
satelliteChannel: null,
|
|
658
|
+
satelliteTimestamp: null,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
298
661
|
const latestRun = findLatestModelRun(this.modelStatus, modelName);
|
|
299
662
|
if (latestRun) {
|
|
300
663
|
// --- NEW CODE: Determine initial forecast hour ---
|
|
@@ -302,8 +665,12 @@ export class AguaceroCore extends EventEmitter {
|
|
|
302
665
|
|
|
303
666
|
// If switching to HRRR with ptypeRefl, start at hour 1 instead of 0
|
|
304
667
|
if (modelName === 'hrrr' && this.state.variable === 'ptypeRefl') {
|
|
305
|
-
const availableHours =
|
|
306
|
-
|
|
668
|
+
const availableHours = resolveModelRunHours(
|
|
669
|
+
this.modelStatus,
|
|
670
|
+
modelName,
|
|
671
|
+
latestRun.date,
|
|
672
|
+
latestRun.run
|
|
673
|
+
).hours || [];
|
|
307
674
|
initialHour = availableHours.find(hour => hour !== 0) || 0;
|
|
308
675
|
}
|
|
309
676
|
// --- END NEW CODE ---
|
|
@@ -330,12 +697,18 @@ export class AguaceroCore extends EventEmitter {
|
|
|
330
697
|
}
|
|
331
698
|
|
|
332
699
|
async setMRMSVariable(variable) {
|
|
333
|
-
const sortedTimestamps =
|
|
334
|
-
const initialTimestamp =
|
|
700
|
+
const sortedTimestamps = this._getFilteredMrmsTimestampsForVariable(variable);
|
|
701
|
+
const initialTimestamp =
|
|
702
|
+
sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1] : null;
|
|
335
703
|
|
|
336
704
|
await this.setState({
|
|
337
705
|
variable,
|
|
338
706
|
isMRMS: true,
|
|
707
|
+
isSatellite: false,
|
|
708
|
+
satelliteInstrumentId: null,
|
|
709
|
+
satelliteSectorLabel: null,
|
|
710
|
+
satelliteChannel: null,
|
|
711
|
+
satelliteTimestamp: null,
|
|
339
712
|
mrmsTimestamp: initialTimestamp,
|
|
340
713
|
});
|
|
341
714
|
}
|
|
@@ -345,33 +718,175 @@ export class AguaceroCore extends EventEmitter {
|
|
|
345
718
|
await this.setState({ mrmsTimestamp: timestamp });
|
|
346
719
|
}
|
|
347
720
|
|
|
721
|
+
async setSatelliteTimestamp(timestamp) {
|
|
722
|
+
if (!this.state.isSatellite) return;
|
|
723
|
+
await this.setState({ satelliteTimestamp: timestamp != null ? Number(timestamp) : null });
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* How many hours of satellite frames to include in the timeline (positive, at most 12 hours).
|
|
728
|
+
* API default: `layerOptions.satelliteDurationValue` on construction.
|
|
729
|
+
*/
|
|
730
|
+
async setSatelliteDurationValue(value) {
|
|
731
|
+
const v = formatTimelineDurationValue(value);
|
|
732
|
+
await this.setState({ satelliteDurationValue: v });
|
|
733
|
+
if (!this.state.isSatellite || !this.state.satelliteInstrumentId) return;
|
|
734
|
+
const timeline = this._computeSatelliteTimeline();
|
|
735
|
+
const tsList = [...(timeline.unixTimes || [])]
|
|
736
|
+
.map((t) => Number(t))
|
|
737
|
+
.filter((t) => !Number.isNaN(t))
|
|
738
|
+
.sort((a, b) => a - b);
|
|
739
|
+
if (tsList.length === 0) return;
|
|
740
|
+
const cur = this.state.satelliteTimestamp;
|
|
741
|
+
const curN = cur == null ? null : Number(cur);
|
|
742
|
+
if (curN == null || !tsList.includes(curN)) {
|
|
743
|
+
await this.setState({ satelliteTimestamp: tsList[tsList.length - 1] });
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Set satellite view using spacecraft id, sector, and channel/product.
|
|
749
|
+
* Omitted fields keep the current selection; when not in satellite mode, missing fields use GOES-19 East CONUS / C13 defaults.
|
|
750
|
+
* @param {{ satelliteId?: string, sector?: string, satelliteSector?: string, satelliteProduct?: string, satelliteChannel?: string, satelliteTimestamp?: number|null }} opts
|
|
751
|
+
*/
|
|
752
|
+
async setSatelliteSelection(opts = {}) {
|
|
753
|
+
const cur = this.state.isSatellite ? this.state : null;
|
|
754
|
+
const tsArg =
|
|
755
|
+
opts.satelliteTimestamp !== undefined
|
|
756
|
+
? opts.satelliteTimestamp
|
|
757
|
+
: this.state.isSatellite && this.state.satelliteTimestamp != null
|
|
758
|
+
? Number(this.state.satelliteTimestamp)
|
|
759
|
+
: undefined;
|
|
760
|
+
return this.switchMode({
|
|
761
|
+
mode: 'satellite',
|
|
762
|
+
satelliteId: opts.satelliteId ?? cur?.satelliteInstrumentId ?? 'GOES19-EAST',
|
|
763
|
+
satelliteSector: opts.satelliteSector ?? opts.sector ?? cur?.satelliteSectorLabel ?? 'conus',
|
|
764
|
+
satelliteProduct:
|
|
765
|
+
opts.satelliteProduct ?? opts.satelliteChannel ?? cur?.satelliteChannel ?? 'C13',
|
|
766
|
+
satelliteTimestamp: tsArg,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* How many hours of MRMS frames to include in the timeline (positive, at most 12 hours).
|
|
772
|
+
* API default: `layerOptions.mrmsDurationValue` on construction.
|
|
773
|
+
*/
|
|
774
|
+
async setMRMSDurationValue(value) {
|
|
775
|
+
const v = formatTimelineDurationValue(value);
|
|
776
|
+
await this.setState({ mrmsDurationValue: v });
|
|
777
|
+
if (!this.state.isMRMS || !this.state.variable) return;
|
|
778
|
+
const filtered = this._getFilteredMrmsTimestampsForVariable(this.state.variable);
|
|
779
|
+
if (filtered.length === 0) return;
|
|
780
|
+
const cur = this.state.mrmsTimestamp;
|
|
781
|
+
const curN = cur == null ? null : Number(cur);
|
|
782
|
+
if (curN == null || !filtered.includes(curN)) {
|
|
783
|
+
await this.setState({ mrmsTimestamp: filtered[filtered.length - 1] });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* NEXRAD sweep listing / scrub window in hours (independent of MRMS duration).
|
|
789
|
+
* API default: `layerOptions.nexradDurationValue` on construction.
|
|
790
|
+
*/
|
|
791
|
+
async setNexradDurationValue(value) {
|
|
792
|
+
const v = formatTimelineDurationValue(value);
|
|
793
|
+
await this.setState({ nexradDurationValue: v });
|
|
794
|
+
if (!this.state.isNexrad || !this.state.nexradSite) return;
|
|
795
|
+
await this.refreshNexradTimes();
|
|
796
|
+
const nk = this._nexradTimesCacheKey();
|
|
797
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
798
|
+
nk ? this.nexradTimesByStation[nk]?.unixTimes || [] : [],
|
|
799
|
+
);
|
|
800
|
+
if (filtered.length === 0) return;
|
|
801
|
+
const cur = this.state.nexradTimestamp;
|
|
802
|
+
const curN = cur == null ? null : Number(cur);
|
|
803
|
+
if (curN == null || !filtered.includes(curN)) {
|
|
804
|
+
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
348
808
|
async switchMode(options) {
|
|
349
|
-
|
|
350
|
-
if (!mode
|
|
351
|
-
console.error("switchMode requires 'mode' ('mrms' | 'model') and 'variable' properties.");
|
|
809
|
+
let { mode, variable, model, forecastHour, mrmsTimestamp, satelliteTimestamp } = options;
|
|
810
|
+
if (!mode) {
|
|
352
811
|
return;
|
|
353
812
|
}
|
|
354
813
|
if (mode === 'model' && !model) {
|
|
355
|
-
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if ((mode === 'mrms' || mode === 'model') && !variable) {
|
|
356
817
|
return;
|
|
357
818
|
}
|
|
358
819
|
let targetState = {};
|
|
359
|
-
if (mode === '
|
|
820
|
+
if (mode === 'satellite') {
|
|
821
|
+
const satelliteInstrumentId = options.satelliteId ?? 'GOES19-EAST';
|
|
822
|
+
const satelliteSectorLabel = resolveSatelliteSectorLabel(
|
|
823
|
+
options.satelliteSector ?? options.sector ?? 'conus',
|
|
824
|
+
);
|
|
825
|
+
const satelliteChannel =
|
|
826
|
+
options.satelliteProduct ?? options.satelliteChannel ?? variable ?? 'C13';
|
|
827
|
+
const channelToken = satelliteChannel;
|
|
828
|
+
// Emit satellite mode immediately so map layers (e.g. model grid) clear before listing fetch finishes.
|
|
829
|
+
await this.setState({
|
|
830
|
+
isSatellite: true,
|
|
831
|
+
isMRMS: false,
|
|
832
|
+
isNexrad: false,
|
|
833
|
+
satelliteInstrumentId,
|
|
834
|
+
satelliteSectorLabel,
|
|
835
|
+
satelliteChannel,
|
|
836
|
+
variable: channelToken,
|
|
837
|
+
satelliteTimestamp: null,
|
|
838
|
+
mrmsTimestamp: null,
|
|
839
|
+
date: null,
|
|
840
|
+
run: null,
|
|
841
|
+
forecastHour: 0,
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
await this.fetchSatelliteListing(true);
|
|
845
|
+
const allFiles = this.satelliteListing?.objects?.map((o) => o.key) || [];
|
|
846
|
+
const sectorName = satelliteSectorLabel;
|
|
847
|
+
const tier = this.state.satelliteTier || 'basic';
|
|
848
|
+
const durationOpt = resolveSatelliteDurationOption(sectorName, tier, this.state.satelliteDurationValue);
|
|
849
|
+
const timeline = buildSatelliteTimelineForSelection(
|
|
850
|
+
{ satelliteInstrumentId, satelliteSectorLabel, satelliteChannel },
|
|
851
|
+
allFiles,
|
|
852
|
+
durationOpt,
|
|
853
|
+
);
|
|
854
|
+
const tsList = [...(timeline.unixTimes || [])].sort((a, b) => a - b);
|
|
855
|
+
let finalTs = satelliteTimestamp !== undefined ? satelliteTimestamp : null;
|
|
856
|
+
if (finalTs == null && tsList.length > 0) {
|
|
857
|
+
finalTs = tsList[tsList.length - 1];
|
|
858
|
+
}
|
|
859
|
+
await this.setState({
|
|
860
|
+
satelliteTimestamp: finalTs != null ? Number(finalTs) : null,
|
|
861
|
+
});
|
|
862
|
+
return;
|
|
863
|
+
} else if (mode === 'mrms') {
|
|
864
|
+
const sortedTimestamps = this._getFilteredMrmsTimestampsForVariable(variable);
|
|
360
865
|
let finalTimestamp = mrmsTimestamp;
|
|
361
866
|
if (finalTimestamp === undefined) {
|
|
362
|
-
|
|
363
|
-
|
|
867
|
+
finalTimestamp =
|
|
868
|
+
sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1] : null;
|
|
869
|
+
} else if (sortedTimestamps.length > 0) {
|
|
870
|
+
const n = Number(finalTimestamp);
|
|
871
|
+
if (!sortedTimestamps.includes(n)) {
|
|
872
|
+
finalTimestamp = sortedTimestamps[sortedTimestamps.length - 1];
|
|
873
|
+
}
|
|
364
874
|
}
|
|
365
875
|
targetState = {
|
|
366
876
|
isMRMS: true,
|
|
877
|
+
isSatellite: false,
|
|
878
|
+
isNexrad: false,
|
|
367
879
|
variable: variable,
|
|
368
880
|
mrmsTimestamp: finalTimestamp,
|
|
369
881
|
model: this.state.model, date: null, run: null, forecastHour: 0,
|
|
882
|
+
satelliteInstrumentId: null,
|
|
883
|
+
satelliteSectorLabel: null,
|
|
884
|
+
satelliteChannel: null,
|
|
885
|
+
satelliteTimestamp: null,
|
|
370
886
|
};
|
|
371
887
|
} else if (mode === 'model') {
|
|
372
888
|
const latestRun = findLatestModelRun(this.modelStatus, model);
|
|
373
889
|
if (!latestRun) {
|
|
374
|
-
console.error(`Could not find a valid run for model: ${model}`);
|
|
375
890
|
return;
|
|
376
891
|
}
|
|
377
892
|
|
|
@@ -380,50 +895,329 @@ export class AguaceroCore extends EventEmitter {
|
|
|
380
895
|
|
|
381
896
|
// If switching to HRRR with ptypeRefl and hour is 0, use hour 1
|
|
382
897
|
if (model === 'hrrr' && variable === 'ptypeRefl' && initialHour === 0) {
|
|
383
|
-
const availableHours =
|
|
898
|
+
const availableHours = resolveModelRunHours(
|
|
899
|
+
this.modelStatus,
|
|
900
|
+
model,
|
|
901
|
+
latestRun.date,
|
|
902
|
+
latestRun.run
|
|
903
|
+
).hours || [];
|
|
384
904
|
initialHour = availableHours.find(hour => hour !== 0) || 0;
|
|
385
905
|
}
|
|
386
906
|
// --- END NEW CODE ---
|
|
387
907
|
|
|
388
908
|
targetState = {
|
|
389
909
|
isMRMS: false,
|
|
910
|
+
isSatellite: false,
|
|
911
|
+
isNexrad: false,
|
|
390
912
|
model: model,
|
|
391
913
|
variable: variable,
|
|
392
914
|
date: latestRun.date,
|
|
393
915
|
run: latestRun.run,
|
|
394
916
|
forecastHour: initialHour, // <-- Changed
|
|
395
917
|
mrmsTimestamp: null,
|
|
918
|
+
satelliteInstrumentId: null,
|
|
919
|
+
satelliteSectorLabel: null,
|
|
920
|
+
satelliteChannel: null,
|
|
921
|
+
satelliteTimestamp: null,
|
|
396
922
|
};
|
|
923
|
+
} else if (mode === 'nexrad') {
|
|
924
|
+
const nexradProduct = options.nexradProduct || 'REF';
|
|
925
|
+
const nexradDataSource =
|
|
926
|
+
options.nexradDataSource != null
|
|
927
|
+
? options.nexradDataSource === 'level3'
|
|
928
|
+
? 'level3'
|
|
929
|
+
: 'level2'
|
|
930
|
+
: inferNexradDataSourceForProduct(nexradProduct);
|
|
931
|
+
const fld = nexradColormapFldKey(nexradDataSource, nexradProduct);
|
|
932
|
+
const site = options.nexradSite ?? null;
|
|
933
|
+
let tilt =
|
|
934
|
+
options.nexradTilt != null
|
|
935
|
+
? Number(options.nexradTilt)
|
|
936
|
+
: site != null
|
|
937
|
+
? getDefaultRadarTilt(site)
|
|
938
|
+
: null;
|
|
939
|
+
await this.setState({
|
|
940
|
+
isNexrad: true,
|
|
941
|
+
isMRMS: false,
|
|
942
|
+
isSatellite: false,
|
|
943
|
+
variable: fld,
|
|
944
|
+
nexradSite: site,
|
|
945
|
+
nexradDataSource,
|
|
946
|
+
nexradProduct,
|
|
947
|
+
nexradTilt: tilt,
|
|
948
|
+
nexradTimestamp: options.nexradTimestamp != null ? Number(options.nexradTimestamp) : null,
|
|
949
|
+
nexradStormRelative: options.nexradStormRelative === true,
|
|
950
|
+
nexradShowSitesPicker: options.nexradShowSitesPicker !== false,
|
|
951
|
+
...(options.nexradDurationValue != null
|
|
952
|
+
? { nexradDurationValue: formatTimelineDurationValue(options.nexradDurationValue) }
|
|
953
|
+
: {}),
|
|
954
|
+
mrmsTimestamp: null,
|
|
955
|
+
satelliteInstrumentId: null,
|
|
956
|
+
satelliteSectorLabel: null,
|
|
957
|
+
satelliteChannel: null,
|
|
958
|
+
satelliteTimestamp: null,
|
|
959
|
+
date: null,
|
|
960
|
+
run: null,
|
|
961
|
+
forecastHour: 0,
|
|
962
|
+
});
|
|
963
|
+
const manifest = await fetchRadarTiltsManifestFromNetwork();
|
|
964
|
+
if (manifest) setRadarTiltsManifest(manifest);
|
|
965
|
+
if (site) {
|
|
966
|
+
await this.refreshNexradTimes();
|
|
967
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
968
|
+
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
969
|
+
);
|
|
970
|
+
let ts =
|
|
971
|
+
options.nexradTimestamp != null
|
|
972
|
+
? Number(options.nexradTimestamp)
|
|
973
|
+
: this.state.nexradTimestamp;
|
|
974
|
+
if (ts == null && filtered.length > 0) ts = filtered[filtered.length - 1];
|
|
975
|
+
else if (ts != null && filtered.length > 0 && !filtered.includes(ts)) {
|
|
976
|
+
ts = filtered[filtered.length - 1];
|
|
977
|
+
}
|
|
978
|
+
await this.setState({ nexradTimestamp: ts });
|
|
979
|
+
}
|
|
980
|
+
return;
|
|
397
981
|
} else {
|
|
398
|
-
console.error(`Invalid mode specified in switchMode: '${mode}'`);
|
|
399
982
|
return;
|
|
400
983
|
}
|
|
401
984
|
await this.setState(targetState);
|
|
402
985
|
}
|
|
403
986
|
|
|
404
|
-
|
|
987
|
+
/**
|
|
988
|
+
* Keep `nexradTilt` on an angle returned by {@link getAvailableNexradTilts} (matches aguacero-frontend elevation buttons).
|
|
989
|
+
*/
|
|
990
|
+
async _snapNexradTiltToAvailableOptions() {
|
|
991
|
+
const s = this.state;
|
|
992
|
+
if (!s.isNexrad || !s.nexradSite) return;
|
|
993
|
+
const tilts = getAvailableNexradTilts(
|
|
994
|
+
s.nexradSite,
|
|
995
|
+
s.nexradDataSource || 'level2',
|
|
996
|
+
s.nexradProduct || 'REF',
|
|
997
|
+
);
|
|
998
|
+
if (!tilts.length) return;
|
|
999
|
+
const t = s.nexradTilt;
|
|
1000
|
+
const match = (a, b) => Math.abs(Number(a) - Number(b)) < 1e-4;
|
|
1001
|
+
if (t != null && tilts.some((x) => match(x, t))) return;
|
|
1002
|
+
const target = t != null && Number.isFinite(Number(t)) ? Number(t) : getDefaultRadarTilt(s.nexradSite);
|
|
1003
|
+
let best = tilts[0];
|
|
1004
|
+
for (const x of tilts) {
|
|
1005
|
+
if (Math.abs(x - target) < Math.abs(best - target)) best = x;
|
|
1006
|
+
}
|
|
1007
|
+
await this.setState({ nexradTilt: best });
|
|
1008
|
+
}
|
|
405
1009
|
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
1010
|
+
async refreshNexradTimes() {
|
|
1011
|
+
const s0 = this.state;
|
|
1012
|
+
if (!s0.isNexrad || !s0.nexradSite) return;
|
|
1013
|
+
await this._snapNexradTiltToAvailableOptions();
|
|
1014
|
+
const s = this.state;
|
|
1015
|
+
const listingHours = this._nexradListingWindowHours();
|
|
1016
|
+
const out = await fetchNexradTimesListing({
|
|
1017
|
+
stationId: s.nexradSite,
|
|
1018
|
+
variable: s.nexradProduct || 'REF',
|
|
1019
|
+
elev: s.nexradTilt,
|
|
1020
|
+
source: s.nexradDataSource || 'level2',
|
|
1021
|
+
level3StormRelative: s.nexradStormRelative,
|
|
1022
|
+
level3Product: getNexradLevel3EntryByRadarKey(s.nexradProduct)?.product,
|
|
1023
|
+
listingWindowHours: listingHours,
|
|
1024
|
+
});
|
|
1025
|
+
const nk = this._nexradTimesCacheKey();
|
|
1026
|
+
if (!nk) return;
|
|
1027
|
+
this.nexradTimesByStation[nk] = {
|
|
1028
|
+
unixTimes: out.unixTimes,
|
|
1029
|
+
timeToKeyMap: out.timeToKeyMap,
|
|
1030
|
+
level3MotionTimeToKeyMap: out.level3MotionTimeToKeyMap,
|
|
1031
|
+
listWindowHours: listingHours,
|
|
1032
|
+
};
|
|
1033
|
+
if (out.level3MotionKey && out.level3MotionUnixTimes?.length) {
|
|
1034
|
+
this.nexradTimesByStation[out.level3MotionKey] = {
|
|
1035
|
+
unixTimes: out.level3MotionUnixTimes,
|
|
1036
|
+
timeToKeyMap: out.level3MotionTimeToKeyMap,
|
|
1037
|
+
listWindowHours: listingHours,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (out.l2StormMotionListKey && out.level3MotionUnixTimes?.length) {
|
|
1041
|
+
this.nexradTimesByStation[out.l2StormMotionListKey] = {
|
|
1042
|
+
unixTimes: out.level3MotionUnixTimes,
|
|
1043
|
+
timeToKeyMap: out.level3MotionTimeToKeyMap,
|
|
1044
|
+
listWindowHours: listingHours,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
this._emitStateChange();
|
|
1048
|
+
}
|
|
409
1049
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
1050
|
+
async setNexradSite(siteId) {
|
|
1051
|
+
if (!this.state.isNexrad) return;
|
|
1052
|
+
const tilt = siteId ? getDefaultRadarTilt(siteId) : null;
|
|
1053
|
+
await this.setState({
|
|
1054
|
+
nexradSite: siteId || null,
|
|
1055
|
+
nexradTilt: tilt,
|
|
1056
|
+
nexradTimestamp: null,
|
|
1057
|
+
});
|
|
1058
|
+
await this.refreshNexradTimes();
|
|
1059
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
1060
|
+
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
1061
|
+
);
|
|
1062
|
+
if (filtered.length > 0) {
|
|
1063
|
+
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
413
1066
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
1067
|
+
async setNexradProduct(product) {
|
|
1068
|
+
if (!this.state.isNexrad) return;
|
|
1069
|
+
const p = (product || 'REF').toUpperCase();
|
|
1070
|
+
const ds = inferNexradDataSourceForProduct(p);
|
|
1071
|
+
const fld = nexradColormapFldKey(ds, p);
|
|
1072
|
+
const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
|
|
1073
|
+
await this.setState({ nexradProduct: p, nexradDataSource: ds, variable: fld, nexradStormRelative });
|
|
1074
|
+
await this.refreshNexradTimes();
|
|
1075
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
1076
|
+
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
1077
|
+
);
|
|
1078
|
+
if (filtered.length > 0) {
|
|
1079
|
+
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
async setNexradDataSource(source) {
|
|
1084
|
+
if (!this.state.isNexrad) return;
|
|
1085
|
+
const ds = source === 'level3' ? 'level3' : 'level2';
|
|
1086
|
+
const p = (this.state.nexradProduct || 'REF').toUpperCase();
|
|
1087
|
+
const fld = nexradColormapFldKey(ds, p);
|
|
1088
|
+
const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
|
|
1089
|
+
await this.setState({ nexradDataSource: ds, variable: fld, nexradStormRelative });
|
|
1090
|
+
await this.refreshNexradTimes();
|
|
1091
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
1092
|
+
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
1093
|
+
);
|
|
1094
|
+
if (filtered.length > 0) {
|
|
1095
|
+
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Sets NEXRAD product and archive source together (single refresh). Prefer {@link setNexradProduct}
|
|
1101
|
+
* for product-only changes (Level II vs III is inferred). Use this when you must force a specific
|
|
1102
|
+
* archive source (rare).
|
|
1103
|
+
* @param {'level2'|'level3'} dataSource
|
|
1104
|
+
* @param {string} product - Level-II variable (REF, VEL, …) or Level-III radar key (N0H, HHC, …)
|
|
1105
|
+
*/
|
|
1106
|
+
async setNexradProductMode(dataSource, product) {
|
|
1107
|
+
if (!this.state.isNexrad) return;
|
|
1108
|
+
const ds = dataSource === 'level3' ? 'level3' : 'level2';
|
|
1109
|
+
const p = (product || 'REF').toUpperCase();
|
|
1110
|
+
const fld = nexradColormapFldKey(ds, p);
|
|
1111
|
+
const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
|
|
1112
|
+
await this.setState({ nexradDataSource: ds, nexradProduct: p, variable: fld, nexradStormRelative });
|
|
1113
|
+
await this.refreshNexradTimes();
|
|
1114
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
1115
|
+
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
1116
|
+
);
|
|
1117
|
+
if (filtered.length > 0) {
|
|
1118
|
+
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async setNexradTilt(tilt) {
|
|
1123
|
+
if (!this.state.isNexrad || !this.state.nexradSite) return;
|
|
1124
|
+
await this.setState({ nexradTilt: tilt != null ? Number(tilt) : null });
|
|
1125
|
+
await this.refreshNexradTimes();
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
async setNexradTimestamp(ts) {
|
|
1129
|
+
if (!this.state.isNexrad) return;
|
|
1130
|
+
await this.setState({ nexradTimestamp: ts != null ? Number(ts) : null });
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// --- Data and Calculation Methods ---
|
|
1134
|
+
|
|
1135
|
+
_ensureGridDecodeWorker() {
|
|
1136
|
+
if (this._gridDecodeWorkerDisabled) {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
if (this._gridDecodeWorker) {
|
|
1140
|
+
return this._gridDecodeWorker;
|
|
1141
|
+
}
|
|
1142
|
+
if (typeof Worker === 'undefined') {
|
|
1143
|
+
this._gridDecodeWorkerDisabled = true;
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
this._gridDecodeWorker = new Worker(new URL('./gridDecodeWorker.js', import.meta.url), {
|
|
1148
|
+
type: 'module',
|
|
1149
|
+
});
|
|
1150
|
+
this._gridDecodeWorker.addEventListener('error', () => {
|
|
1151
|
+
this._gridDecodeWorkerDisabled = true;
|
|
1152
|
+
if (this._gridDecodeWorker) {
|
|
1153
|
+
this._gridDecodeWorker.terminate();
|
|
1154
|
+
this._gridDecodeWorker = null;
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
} catch {
|
|
1158
|
+
this._gridDecodeWorkerDisabled = true;
|
|
1159
|
+
return null;
|
|
419
1160
|
}
|
|
420
|
-
|
|
421
|
-
|
|
1161
|
+
return this._gridDecodeWorker;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Offloads zstd + delta decode + transform to a Worker when available; falls back to main-thread
|
|
1166
|
+
* {@link processCompressedGrid}. Uses a copy for postMessage transfer so the original `compressedData`
|
|
1167
|
+
* stays valid if the Worker path fails.
|
|
1168
|
+
*/
|
|
1169
|
+
_decodeGridPayload(compressedData, encoding) {
|
|
1170
|
+
const worker = this._ensureGridDecodeWorker();
|
|
1171
|
+
if (!worker) {
|
|
1172
|
+
return Promise.resolve(processCompressedGrid(compressedData, encoding));
|
|
1173
|
+
}
|
|
1174
|
+
const payload = compressedData.slice();
|
|
1175
|
+
return new Promise((resolve, reject) => {
|
|
1176
|
+
const id = ++this._gridDecodeMsgId;
|
|
1177
|
+
const onMsg = (e) => {
|
|
1178
|
+
if (!e.data || e.data.id !== id) {
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
worker.removeEventListener('message', onMsg);
|
|
1182
|
+
worker.removeEventListener('error', onErr);
|
|
1183
|
+
if (e.data.error) {
|
|
1184
|
+
reject(new Error(e.data.error));
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const data = new Uint8Array(e.data.dataBuffer, e.data.dataByteOffset, e.data.dataByteLength);
|
|
1188
|
+
resolve({ data, encoding: e.data.encoding });
|
|
1189
|
+
};
|
|
1190
|
+
const onErr = (err) => {
|
|
1191
|
+
worker.removeEventListener('message', onMsg);
|
|
1192
|
+
worker.removeEventListener('error', onErr);
|
|
1193
|
+
reject(err);
|
|
1194
|
+
};
|
|
1195
|
+
worker.addEventListener('message', onMsg);
|
|
1196
|
+
worker.addEventListener('error', onErr);
|
|
1197
|
+
try {
|
|
1198
|
+
worker.postMessage(
|
|
1199
|
+
{
|
|
1200
|
+
id,
|
|
1201
|
+
encoding,
|
|
1202
|
+
compressedBuffer: payload.buffer,
|
|
1203
|
+
compressedByteOffset: payload.byteOffset,
|
|
1204
|
+
compressedByteLength: payload.byteLength,
|
|
1205
|
+
},
|
|
1206
|
+
[payload.buffer]
|
|
1207
|
+
);
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
worker.removeEventListener('message', onMsg);
|
|
1210
|
+
worker.removeEventListener('error', onErr);
|
|
1211
|
+
reject(err);
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
422
1214
|
}
|
|
423
1215
|
|
|
424
1216
|
async _loadGridData(state) {
|
|
425
1217
|
if (this.isReactNative) {
|
|
426
|
-
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
if (state.isNexrad) {
|
|
427
1221
|
return null;
|
|
428
1222
|
}
|
|
429
1223
|
const { model, date, run, forecastHour, variable, isMRMS, mrmsTimestamp } = state;
|
|
@@ -447,18 +1241,7 @@ export class AguaceroCore extends EventEmitter {
|
|
|
447
1241
|
if (this.dataCache.has(dataUrlIdentifier)) {
|
|
448
1242
|
return this.dataCache.get(dataUrlIdentifier);
|
|
449
1243
|
}
|
|
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
|
-
|
|
1244
|
+
|
|
462
1245
|
const abortController = new AbortController();
|
|
463
1246
|
this.abortControllers.set(dataUrlIdentifier, abortController);
|
|
464
1247
|
|
|
@@ -485,31 +1268,18 @@ export class AguaceroCore extends EventEmitter {
|
|
|
485
1268
|
const { data: b64Data, encoding } = await response.json();
|
|
486
1269
|
const compressedData = Uint8Array.from(atob(b64Data), c => c.charCodeAt(0));
|
|
487
1270
|
|
|
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;
|
|
1271
|
+
let gridPayload;
|
|
1272
|
+
try {
|
|
1273
|
+
gridPayload = await this._decodeGridPayload(compressedData, encoding);
|
|
1274
|
+
} catch {
|
|
1275
|
+
gridPayload = processCompressedGrid(compressedData, encoding);
|
|
501
1276
|
}
|
|
502
1277
|
|
|
503
1278
|
this.abortControllers.delete(dataUrlIdentifier);
|
|
504
1279
|
|
|
505
|
-
return { data:
|
|
1280
|
+
return { data: gridPayload.data, encoding: gridPayload.encoding };
|
|
506
1281
|
|
|
507
1282
|
} 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
1283
|
this.dataCache.delete(dataUrlIdentifier);
|
|
514
1284
|
this.abortControllers.delete(dataUrlIdentifier);
|
|
515
1285
|
return null;
|
|
@@ -528,11 +1298,12 @@ export class AguaceroCore extends EventEmitter {
|
|
|
528
1298
|
// Clear both maps
|
|
529
1299
|
this.abortControllers.clear();
|
|
530
1300
|
this.dataCache.clear();
|
|
531
|
-
|
|
532
|
-
console.log('All pending requests cancelled');
|
|
533
1301
|
}
|
|
534
1302
|
|
|
535
1303
|
async getValueAtLngLat(lng, lat) {
|
|
1304
|
+
if (this.state.isSatellite || this.state.isNexrad) {
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
536
1307
|
const { variable, isMRMS, mrmsTimestamp, model, date, run, forecastHour, units } = this.state;
|
|
537
1308
|
if (!variable) return null;
|
|
538
1309
|
|
|
@@ -814,74 +1585,11 @@ export class AguaceroCore extends EventEmitter {
|
|
|
814
1585
|
const y = t_y * (ny - 1);
|
|
815
1586
|
return { x, y };
|
|
816
1587
|
} catch (error) {
|
|
817
|
-
console.warn(`[GridAccessor] RGEM polar stereographic conversion failed for ${lat}, ${lon}:`, error);
|
|
818
1588
|
return { x: -1, y: -1 };
|
|
819
1589
|
}
|
|
820
1590
|
}
|
|
821
1591
|
|
|
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
|
-
}
|
|
1592
|
+
// --- Status Methods ---
|
|
885
1593
|
|
|
886
1594
|
async fetchModelStatus(force = false) {
|
|
887
1595
|
if (!this.modelStatus || force) {
|
|
@@ -906,6 +1614,19 @@ export class AguaceroCore extends EventEmitter {
|
|
|
906
1614
|
return this.mrmsStatus;
|
|
907
1615
|
}
|
|
908
1616
|
|
|
1617
|
+
async fetchSatelliteListing(force = false) {
|
|
1618
|
+
if (!this.satelliteListing || force) {
|
|
1619
|
+
try {
|
|
1620
|
+
const response = await fetch(SATELLITE_FRAMES_URL);
|
|
1621
|
+
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
|
1622
|
+
this.satelliteListing = await response.json();
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
this.satelliteListing = null;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return this.satelliteListing;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
909
1630
|
startAutoRefresh(intervalSeconds) {
|
|
910
1631
|
this.stopAutoRefresh();
|
|
911
1632
|
this.autoRefreshIntervalId = setInterval(async () => {
|