@aguacerowx/javascript-sdk 0.0.27 → 0.0.28

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.
@@ -10,13 +10,7 @@ var _unitConversions = require("./unitConversions.js");
10
10
  var _dictionaries = require("./dictionaries.js");
11
11
  var _defaultColormaps = require("./default-colormaps.js");
12
12
  var _proj = _interopRequireDefault(require("proj4"));
13
- var _gridDecodePipeline = require("./gridDecodePipeline.js");
14
13
  var _getBundleId = require("./getBundleId");
15
- var _satellite_support = require("./satellite_support.js");
16
- var _nexrad_support = require("./nexrad_support.js");
17
- var _nexradTiltCoalesce = require("./nexradTiltCoalesce.js");
18
- var _nexrad_level3_catalog = require("./nexrad_level3_catalog.js");
19
- var _nexradTilts = require("./nexradTilts.js");
20
14
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
21
15
  // AguaceroCore.js - The Headless "Engine"
22
16
 
@@ -60,80 +54,26 @@ function findLatestModelRun(modelsData, modelName) {
60
54
  }
61
55
  return null;
62
56
  }
63
-
64
- /**
65
- * model-status JSON uses string keys for runs (often zero-padded: "00", "06").
66
- * Direct lookup modelStatus[model][date][run] fails if state.run is "6" but the key is "06".
67
- * Returns the hour list and the run key that matched.
68
- */
69
- function resolveModelRunHours(modelStatus, model, date, run) {
70
- var _modelStatus$model;
71
- const runs = modelStatus === null || modelStatus === void 0 || (_modelStatus$model = modelStatus[model]) === null || _modelStatus$model === void 0 ? void 0 : _modelStatus$model[date];
72
- if (!runs || run == null || run === '') {
73
- return {
74
- hours: [],
75
- matchedRunKey: null,
76
- availableRunKeys: runs ? Object.keys(runs) : []
77
- };
78
- }
79
- const runStr = String(run);
80
- const candidates = new Set([runStr]);
81
- const n = parseInt(runStr, 10);
82
- if (!Number.isNaN(n)) {
83
- candidates.add(String(n));
84
- candidates.add(String(n).padStart(2, '0'));
85
- candidates.add(String(n).padStart(3, '0'));
86
- }
87
- for (const key of candidates) {
88
- const h = runs[key];
89
- if (h && Array.isArray(h) && h.length > 0) {
90
- return {
91
- hours: h,
92
- matchedRunKey: key,
93
- availableRunKeys: Object.keys(runs)
94
- };
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 {
104
- hours: h,
105
- matchedRunKey: k,
106
- availableRunKeys: Object.keys(runs)
107
- };
108
- }
109
- }
110
- }
111
- }
112
- return {
113
- hours: [],
114
- matchedRunKey: null,
115
- availableRunKeys: Object.keys(runs)
116
- };
117
- }
118
57
  class AguaceroCore extends _events.EventEmitter {
119
58
  constructor(options = {}) {
120
59
  super();
121
60
  this.isReactNative = typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
122
61
  this.apiKey = options.apiKey;
123
- /** Passed as CloudFront `userId` for satellite KTX2 URLs (production uses the authenticated account id). */
124
- this.userId = options.userId ?? 'sdk-user';
125
62
  this.bundleId = (0, _getBundleId.getBundleId)();
126
63
  this.baseGridUrl = 'https://d3dc62msmxkrd7.cloudfront.net';
127
- /** @type {Worker | null} */
128
- this._gridDecodeWorker = null;
129
- /** When true, skip Worker and use {@link processCompressedGrid} on the main thread. */
130
- this._gridDecodeWorkerDisabled = false;
131
- this._gridDecodeMsgId = 0;
64
+ if (!this.isReactNative) {
65
+ this.worker = this.createWorker();
66
+ this.workerRequestId = 0;
67
+ this.workerResolvers = new Map();
68
+ this.worker.addEventListener('message', this._handleWorkerMessage.bind(this));
69
+ this.resultQueue = [];
70
+ this.isProcessingQueue = false;
71
+ } else {
72
+ this.worker = null;
73
+ }
132
74
  this.statusUrl = 'https://d3dc62msmxkrd7.cloudfront.net/model-status';
133
75
  this.modelStatus = null;
134
76
  this.mrmsStatus = null;
135
- /** @type {{ objects?: Array<{ key: string }> } | null} */
136
- this.satelliteListing = null;
137
77
  this.dataCache = new Map();
138
78
  this.abortControllers = new Map();
139
79
  this.isPlaying = false;
@@ -144,207 +84,30 @@ class AguaceroCore extends _events.EventEmitter {
144
84
  // EDIT: Determine initial mode from options
145
85
  const initialMode = userLayerOptions.mode || 'model';
146
86
  const initialVariable = userLayerOptions.variable || null;
147
- const initialSatellite = initialMode === 'satellite';
148
- const initialNexrad = initialMode === 'nexrad';
149
- const initialNexradProd = userLayerOptions.nexradProduct || 'REF';
150
- const initialNexradDs = (0, _nexrad_support.inferNexradDataSourceForProduct)(initialNexradProd);
151
- const initialNexradFld = initialNexrad ? (0, _nexrad_support.nexradColormapFldKey)(initialNexradDs, initialNexradProd) : initialVariable;
152
- let initialSatelliteInstrumentId = null;
153
- let initialSatelliteSectorLabel = null;
154
- let initialSatelliteChannel = null;
155
- if (initialSatellite) {
156
- initialSatelliteInstrumentId = userLayerOptions.satelliteId ?? 'GOES19-EAST';
157
- initialSatelliteSectorLabel = (0, _satellite_support.resolveSatelliteSectorLabel)(userLayerOptions.satelliteSector ?? userLayerOptions.sector ?? 'conus');
158
- initialSatelliteChannel = userLayerOptions.satelliteProduct ?? userLayerOptions.satelliteChannel ?? initialVariable ?? 'C13';
159
- }
160
87
  this.state = {
161
88
  model: userLayerOptions.model || 'gfs',
162
89
  // EDIT: Set isMRMS based on the initial mode
163
- isMRMS: initialMode === 'mrms' && !initialSatellite && !initialNexrad,
90
+ isMRMS: initialMode === 'mrms',
164
91
  mrmsTimestamp: null,
165
- variable: initialNexrad ? initialNexradFld : initialSatellite && initialSatelliteInstrumentId ? initialSatelliteChannel : initialVariable,
92
+ variable: initialVariable,
166
93
  date: null,
167
94
  run: null,
168
95
  forecastHour: 0,
169
96
  visible: true,
170
97
  opacity: userLayerOptions.opacity ?? 1,
171
98
  units: options.initialUnit || 'imperial',
172
- shaderSmoothingEnabled: options.shaderSmoothingEnabled ?? true,
173
- isSatellite: initialSatellite,
174
- satelliteInstrumentId: initialSatelliteInstrumentId,
175
- satelliteSectorLabel: initialSatelliteSectorLabel,
176
- satelliteChannel: initialSatelliteChannel,
177
- satelliteTimestamp: userLayerOptions.satelliteTimestamp != null ? Number(userLayerOptions.satelliteTimestamp) : null,
178
- satelliteTier: userLayerOptions.satelliteTier || 'basic',
179
- satelliteDurationValue: (0, _satellite_support.formatTimelineDurationValue)(userLayerOptions.satelliteDurationValue != null ? userLayerOptions.satelliteDurationValue : '1'),
180
- mrmsDurationValue: (0, _satellite_support.formatTimelineDurationValue)(userLayerOptions.mrmsDurationValue != null ? userLayerOptions.mrmsDurationValue : '1'),
181
- nexradDurationValue: (0, _satellite_support.formatTimelineDurationValue)(userLayerOptions.nexradDurationValue != null ? userLayerOptions.nexradDurationValue : '1'),
182
- isNexrad: initialNexrad,
183
- nexradSite: userLayerOptions.nexradSite ?? null,
184
- nexradDataSource: initialNexradDs,
185
- nexradProduct: initialNexradProd,
186
- nexradTilt: userLayerOptions.nexradTilt != null ? Number(userLayerOptions.nexradTilt) : userLayerOptions.nexradSite ? (0, _nexradTilts.getDefaultRadarTilt)(userLayerOptions.nexradSite) : 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
99
+ shaderSmoothingEnabled: options.shaderSmoothingEnabled ?? true
191
100
  };
192
101
  this.autoRefreshEnabled = options.autoRefresh ?? false;
193
102
  this.autoRefreshIntervalSeconds = options.autoRefreshInterval ?? 60;
194
103
  this.autoRefreshIntervalId = null;
195
-
196
- /** @type {Record<string, { unixTimes?: number[]; timeToKeyMap?: Record<string, string>; listWindowHours?: number }>} */
197
- this.nexradTimesByStation = {};
198
104
  }
199
105
  async setState(newState) {
200
- const patch = {
201
- ...newState
202
- };
203
- if ('satelliteKey' in patch) delete patch.satelliteKey;
204
- if ('nexradDataSource' in patch && !('nexradProduct' in patch)) {
205
- delete patch.nexradDataSource;
206
- }
207
- const willBeNexrad = patch.isNexrad !== undefined ? Boolean(patch.isNexrad) : this.state.isNexrad;
208
- if (willBeNexrad && 'nexradProduct' in patch && patch.nexradProduct != null) {
209
- const p = (patch.nexradProduct || 'REF').toUpperCase();
210
- const prevP = (this.state.nexradProduct || 'REF').toUpperCase();
211
- const ds = (0, _nexrad_support.inferNexradDataSourceForProduct)(p);
212
- patch.nexradProduct = p;
213
- patch.nexradDataSource = ds;
214
- patch.variable = (0, _nexrad_support.nexradColormapFldKey)(ds, p);
215
- if (!('nexradStormRelative' in newState)) {
216
- // Default: base radial velocity (L3 N0G only) — one fetch per time; fast scrub.
217
- // SRV (N0G + N0S) is opt-in: setNexradStormRelative(true) or pass nexradStormRelative in setState.
218
- // Re-selecting the same product (e.g. VEL → VEL) leaves the current SRV flag alone.
219
- if (p !== 'VEL' || prevP !== 'VEL') {
220
- patch.nexradStormRelative = false;
221
- }
222
- }
223
- }
224
- if ('forecastHour' in patch && patch.forecastHour != null) {
225
- patch.forecastHour = Number(patch.forecastHour);
226
- }
227
- if ('mrmsTimestamp' in patch && patch.mrmsTimestamp != null) {
228
- patch.mrmsTimestamp = Number(patch.mrmsTimestamp);
229
- }
230
- if ('satelliteTimestamp' in patch && patch.satelliteTimestamp != null) {
231
- patch.satelliteTimestamp = Number(patch.satelliteTimestamp);
232
- }
233
- if ('satelliteDurationValue' in patch && patch.satelliteDurationValue != null) {
234
- patch.satelliteDurationValue = (0, _satellite_support.formatTimelineDurationValue)(patch.satelliteDurationValue);
235
- }
236
- if ('mrmsDurationValue' in patch && patch.mrmsDurationValue != null) {
237
- patch.mrmsDurationValue = (0, _satellite_support.formatTimelineDurationValue)(patch.mrmsDurationValue);
238
- }
239
- if ('nexradDurationValue' in patch && patch.nexradDurationValue != null) {
240
- patch.nexradDurationValue = (0, _satellite_support.formatTimelineDurationValue)(patch.nexradDurationValue);
241
- }
242
- if ('nexradTimestamp' in patch && patch.nexradTimestamp != null) {
243
- patch.nexradTimestamp = Number(patch.nexradTimestamp);
244
- }
245
- if ('nexradTilt' in patch && patch.nexradTilt != null) {
246
- patch.nexradTilt = Number(patch.nexradTilt);
247
- }
248
- Object.assign(this.state, patch);
106
+ Object.assign(this.state, newState);
249
107
  this._emitStateChange();
250
108
  }
251
-
252
- /**
253
- * Forecast hours for the current model/date/run (normalized numbers).
254
- * Tolerates model-status run keys like "06" vs state.run "6" so preload and slider stay in sync.
255
- */
256
- getAvailableForecastHours() {
257
- if (this.state.isMRMS || this.state.isSatellite || this.state.isNexrad) return [];
258
- if (!this.state.model || this.state.date == null || this.state.run == null) return [];
259
- const resolved = resolveModelRunHours(this.modelStatus, this.state.model, this.state.date, this.state.run);
260
- let hours = resolved.hours || [];
261
- if (hours.length > 0) {
262
- hours = hours.map(h => typeof h === 'string' ? parseInt(h, 10) : Number(h)).filter(h => !Number.isNaN(h));
263
- }
264
- if (this.state.variable === 'ptypeRefl' && this.state.model === 'hrrr' && hours.length > 0) {
265
- hours = hours.filter(hour => hour !== 0);
266
- }
267
- return hours;
268
- }
269
- _computeSatelliteTimeline() {
270
- var _this$satelliteListin;
271
- const {
272
- satelliteInstrumentId,
273
- satelliteSectorLabel,
274
- satelliteChannel
275
- } = this.state;
276
- if (!satelliteInstrumentId || !satelliteSectorLabel || !satelliteChannel || !((_this$satelliteListin = this.satelliteListing) !== null && _this$satelliteListin !== void 0 && _this$satelliteListin.objects)) {
277
- return {
278
- unixTimes: [],
279
- timeToFileMap: {}
280
- };
281
- }
282
- const allFiles = this.satelliteListing.objects.map(o => o.key);
283
- const sectorName = satelliteSectorLabel;
284
- const tier = this.state.satelliteTier || 'basic';
285
- const durationOpt = (0, _satellite_support.resolveSatelliteDurationOption)(sectorName, tier, this.state.satelliteDurationValue);
286
- return (0, _satellite_support.buildSatelliteTimelineForSelection)({
287
- satelliteInstrumentId,
288
- satelliteSectorLabel,
289
- satelliteChannel
290
- }, allFiles, durationOpt);
291
- }
292
-
293
- /**
294
- * MRMS timestamps for a variable, oldest first (matches satellite timelines and left→right time sliders).
295
- * Limited to the last N hours relative to the newest frame (see mrmsDurationValue).
296
- * @param {string} variable
297
- * @returns {number[]}
298
- */
299
- _getFilteredMrmsTimestampsForVariable(variable) {
300
- var _this$mrmsStatus;
301
- const raw = (_this$mrmsStatus = this.mrmsStatus) === null || _this$mrmsStatus === void 0 ? void 0 : _this$mrmsStatus[variable];
302
- if (!raw || !raw.length) return [];
303
- const hours = (0, _satellite_support.parseTimelineDurationHours)(this.state.mrmsDurationValue);
304
- let list = [...raw].map(t => Number(t)).filter(t => !Number.isNaN(t)).sort((a, b) => a - b);
305
- if (hours > 0 && list.length > 0) {
306
- const latest = list[list.length - 1];
307
- const cutoff = latest - hours * 3600;
308
- list = list.filter(t => t >= cutoff);
309
- }
310
- return list;
311
- }
312
- _nexradListingWindowHours() {
313
- return (0, _satellite_support.parseTimelineDurationHours)(this.state.nexradDurationValue);
314
- }
315
- _getFilteredNexradTimestampsForVariable(rawList) {
316
- if (!rawList || !rawList.length) return [];
317
- const hours = this._nexradListingWindowHours();
318
- let list = [...rawList].map(t => Number(t)).filter(t => !Number.isNaN(t)).sort((a, b) => a - b);
319
- if (hours > 0 && list.length > 0) {
320
- const latest = list[list.length - 1];
321
- const cutoff = latest - hours * 3600;
322
- list = list.filter(t => t >= cutoff);
323
- }
324
- return list;
325
- }
326
-
327
- /**
328
- * Cache key for {@link this.nexradTimesByStation} — matches frontend composite keys (tilt + variable group/source).
329
- */
330
- _nexradTimesCacheKey() {
331
- var _getNexradLevel3Entry;
332
- const s = this.state;
333
- if (!s.isNexrad || !s.nexradSite) return null;
334
- const site = s.nexradSite;
335
- const variable = s.nexradProduct || 'REF';
336
- const ds = s.nexradDataSource || 'level2';
337
- const tiltNum = s.nexradTilt != null ? s.nexradTilt : (0, _nexradTilts.getDefaultRadarTilt)(site);
338
- const elevNormUse = ds === 'level3' ? _nexrad_level3_catalog.NEXRAD_LEVEL3_ELEV : (0, _nexradTilts.formatTiltForApi)((0, _nexradTilts.clampNexradTiltForVariable)(site, variable, tiltNum));
339
- const group = ds === 'level3' ? 'l3' : (0, _nexrad_support.variableToNexradGroup)(variable);
340
- const l3Product = ds === 'level3' ? ((_getNexradLevel3Entry = (0, _nexrad_level3_catalog.getNexradLevel3EntryByRadarKey)(variable)) === null || _getNexradLevel3Entry === void 0 ? void 0 : _getNexradLevel3Entry.product) ?? (variable === 'VEL' ? 'N0G' : variable) : '';
341
- if (ds === 'level3') {
342
- return `${site}_l3_${l3Product}_${elevNormUse}`;
343
- }
344
- return `${site}_${group}_${elevNormUse}`;
345
- }
346
109
  _emitStateChange() {
347
- var _this$modelStatus;
110
+ var _this$modelStatus, _this$modelStatus2;
348
111
  const {
349
112
  colormap,
350
113
  baseUnit
@@ -353,45 +116,23 @@ class AguaceroCore extends _events.EventEmitter {
353
116
  const displayColormap = this._convertColormapUnits(colormap, baseUnit, toUnit);
354
117
  let availableTimestamps = [];
355
118
  if (this.state.isMRMS && this.state.variable && this.mrmsStatus) {
356
- availableTimestamps = this._getFilteredMrmsTimestampsForVariable(this.state.variable);
119
+ const timestamps = this.mrmsStatus[this.state.variable] || [];
120
+ availableTimestamps = [...timestamps].reverse();
357
121
  }
358
- let availableSatelliteTimestamps = [];
359
- let satelliteTimeToFileMap = {};
360
- if (this.state.isSatellite && this.state.satelliteInstrumentId) {
361
- const timeline = this._computeSatelliteTimeline();
362
- satelliteTimeToFileMap = timeline.timeToFileMap || {};
363
- availableSatelliteTimestamps = [...(timeline.unixTimes || [])].map(t => Number(t)).filter(t => !Number.isNaN(t)).sort((a, b) => a - b);
122
+ let availableHours = this.state.isMRMS ? [] : ((_this$modelStatus = this.modelStatus) === null || _this$modelStatus === void 0 || (_this$modelStatus = _this$modelStatus[this.state.model]) === null || _this$modelStatus === void 0 || (_this$modelStatus = _this$modelStatus[this.state.date]) === null || _this$modelStatus === void 0 ? void 0 : _this$modelStatus[this.state.run]) || [];
123
+ if (!this.state.isMRMS && this.state.variable === 'ptypeRefl' && this.state.model === 'hrrr' && availableHours.length > 0) {
124
+ availableHours = availableHours.filter(hour => hour !== 0);
364
125
  }
365
- let availableNexradTimestamps = [];
366
- let nexradTimeToKeyMap = {};
367
- let nexradLevel3MotionTimeToKeyMap = {};
368
- let availableNexradTilts = [];
369
- if (this.state.isNexrad && this.state.nexradSite) {
370
- const nk = this._nexradTimesCacheKey();
371
- const ent = nk ? this.nexradTimesByStation[nk] : null;
372
- const raw = (ent === null || ent === void 0 ? void 0 : ent.unixTimes) || [];
373
- availableNexradTimestamps = this._getFilteredNexradTimestampsForVariable(raw);
374
- nexradTimeToKeyMap = (ent === null || ent === void 0 ? void 0 : ent.timeToKeyMap) || {};
375
- nexradLevel3MotionTimeToKeyMap = (ent === null || ent === void 0 ? void 0 : ent.level3MotionTimeToKeyMap) || {};
376
- availableNexradTilts = (0, _nexrad_support.getAvailableNexradTilts)(this.state.nexradSite, this.state.nexradDataSource || 'level2', this.state.nexradProduct || 'REF');
377
- }
378
- const availableHours = this.getAvailableForecastHours();
379
126
  const eventPayload = {
380
127
  ...this.state,
381
128
  availableModels: this.modelStatus ? Object.keys(this.modelStatus).sort() : [],
382
- availableRuns: ((_this$modelStatus = this.modelStatus) === null || _this$modelStatus === void 0 ? void 0 : _this$modelStatus[this.state.model]) || {},
129
+ availableRuns: ((_this$modelStatus2 = this.modelStatus) === null || _this$modelStatus2 === void 0 ? void 0 : _this$modelStatus2[this.state.model]) || {},
383
130
  availableHours: availableHours,
384
131
  // <-- Changed from inline calculation
385
132
  availableVariables: this.getAvailableVariables(this.state.isMRMS ? 'mrms' : this.state.model),
386
133
  // We need to confirm this line is working as expected.
387
134
  availableMRMSVariables: this.getAvailableVariables('mrms'),
388
135
  availableTimestamps: availableTimestamps,
389
- availableSatelliteTimestamps,
390
- satelliteTimeToFileMap,
391
- availableNexradTimestamps,
392
- nexradTimeToKeyMap,
393
- nexradLevel3MotionTimeToKeyMap,
394
- availableNexradTilts,
395
136
  isPlaying: this.isPlaying,
396
137
  colormap: displayColormap,
397
138
  colormapBaseUnit: toUnit
@@ -401,34 +142,28 @@ class AguaceroCore extends _events.EventEmitter {
401
142
  async initialize(options = {}) {
402
143
  await this.fetchModelStatus(true);
403
144
  await this.fetchMRMSStatus(true);
404
- await this.fetchSatelliteListing(true);
405
145
  let initialState = {
406
146
  ...this.state
407
147
  };
408
- if (initialState.isSatellite && initialState.satelliteInstrumentId) {
409
- const timeline = this._computeSatelliteTimeline();
410
- const tsList = [...(timeline.unixTimes || [])].sort((a, b) => a - b);
411
- if (initialState.satelliteTimestamp == null && tsList.length > 0) {
412
- initialState.satelliteTimestamp = tsList[tsList.length - 1];
413
- }
414
- }
415
148
 
416
149
  // ADD: Logic to handle an initial MRMS state
417
150
  if (initialState.isMRMS) {
418
151
  const variable = initialState.variable;
419
152
  if (variable && this.mrmsStatus && this.mrmsStatus[variable]) {
420
- const sortedTimestamps = this._getFilteredMrmsTimestampsForVariable(variable);
421
- initialState.mrmsTimestamp = sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1] : null;
153
+ const sortedTimestamps = [...(this.mrmsStatus[variable] || [])].sort((a, b) => b - a);
154
+ initialState.mrmsTimestamp = sortedTimestamps.length > 0 ? sortedTimestamps[0] : null;
422
155
  } else {
156
+ // Fallback if the provided variable is not valid
157
+ console.warn(`Initial MRMS variable '${variable}' not found. Using default.`);
423
158
  const availableMRMSVars = this.getAvailableVariables('mrms');
424
159
  if (availableMRMSVars.length > 0) {
425
160
  const firstVar = availableMRMSVars[0];
426
161
  initialState.variable = firstVar;
427
- const sortedTimestamps = this._getFilteredMrmsTimestampsForVariable(firstVar);
428
- initialState.mrmsTimestamp = sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1] : null;
162
+ const sortedTimestamps = [...(this.mrmsStatus[firstVar] || [])].sort((a, b) => b - a);
163
+ initialState.mrmsTimestamp = sortedTimestamps.length > 0 ? sortedTimestamps[0] : null;
429
164
  }
430
165
  }
431
- } else if (!initialState.isSatellite && !initialState.isNexrad) {
166
+ } else {
432
167
  // EDIT: This is the existing logic, now in an else block
433
168
  const latestRun = findLatestModelRun(this.modelStatus, initialState.model);
434
169
  if (latestRun) {
@@ -446,13 +181,6 @@ class AguaceroCore extends _events.EventEmitter {
446
181
  }
447
182
  }
448
183
  await this.setState(initialState);
449
- if (this.state.isNexrad) {
450
- const manifest = await (0, _nexradTilts.fetchRadarTiltsManifestFromNetwork)();
451
- if (manifest) (0, _nexradTilts.setRadarTiltsManifest)(manifest);
452
- if (this.state.nexradSite) {
453
- await this.refreshNexradTimes();
454
- }
455
- }
456
184
  if (options.autoRefresh ?? this.autoRefreshEnabled) {
457
185
  this.startAutoRefresh(options.refreshInterval ?? this.autoRefreshIntervalSeconds);
458
186
  }
@@ -461,11 +189,11 @@ class AguaceroCore extends _events.EventEmitter {
461
189
  this.pause();
462
190
  this.stopAutoRefresh();
463
191
  this.dataCache.clear();
464
- this.callbacks = {};
465
- if (this._gridDecodeWorker) {
466
- this._gridDecodeWorker.terminate();
467
- this._gridDecodeWorker = null;
192
+ if (this.worker) {
193
+ this.worker.terminate();
468
194
  }
195
+ this.callbacks = {};
196
+ console.log(`AguaceroCore has been destroyed.`);
469
197
  }
470
198
 
471
199
  // --- Public API Methods ---
@@ -494,21 +222,6 @@ class AguaceroCore extends _events.EventEmitter {
494
222
  this.isPlaying ? this.pause() : this.play();
495
223
  }
496
224
  step(direction = 1) {
497
- if (this.state.isSatellite) {
498
- const timeline = this._computeSatelliteTimeline();
499
- const availableTimestamps = [...(timeline.unixTimes || [])].sort((a, b) => a - b).map(t => Number(t));
500
- if (availableTimestamps.length === 0) return;
501
- const ts = this.state.satelliteTimestamp == null ? null : Number(this.state.satelliteTimestamp);
502
- const currentIndex = ts == null ? -1 : availableTimestamps.indexOf(ts);
503
- let nextIndex = currentIndex === -1 ? availableTimestamps.length - 1 : currentIndex + direction;
504
- const maxIndex = availableTimestamps.length - 1;
505
- if (nextIndex > maxIndex) nextIndex = 0;
506
- if (nextIndex < 0) nextIndex = maxIndex;
507
- this.setState({
508
- satelliteTimestamp: availableTimestamps[nextIndex]
509
- });
510
- return;
511
- }
512
225
  // --- THIS IS THE CORRECTED MRMS LOGIC ---
513
226
  if (this.state.isMRMS) {
514
227
  const {
@@ -516,16 +229,19 @@ class AguaceroCore extends _events.EventEmitter {
516
229
  mrmsTimestamp
517
230
  } = this.state;
518
231
  if (!this.mrmsStatus || !this.mrmsStatus[variable]) {
232
+ console.warn('[Core.step] MRMS status or variable not available.');
519
233
  return;
520
234
  }
521
- const availableTimestamps = this._getFilteredMrmsTimestampsForVariable(variable);
235
+
236
+ // CRITICAL FIX: The UI and state emissions use a REVERSED array (newest first).
237
+ // The step logic MUST use the same reversed array for indexes to match.
238
+ const availableTimestamps = [...(this.mrmsStatus[variable] || [])].reverse();
522
239
  if (availableTimestamps.length === 0) return;
523
- const ts = mrmsTimestamp == null ? null : Number(mrmsTimestamp);
524
- const currentIndex = ts == null ? -1 : availableTimestamps.indexOf(ts);
240
+ const currentIndex = availableTimestamps.indexOf(mrmsTimestamp);
525
241
  if (currentIndex === -1) {
526
- // If not found, reset to the latest frame (end of ascending list)
242
+ // If not found, reset to the first (newest) frame
527
243
  this.setState({
528
- mrmsTimestamp: availableTimestamps[availableTimestamps.length - 1]
244
+ mrmsTimestamp: availableTimestamps[0]
529
245
  });
530
246
  return;
531
247
  }
@@ -539,29 +255,17 @@ class AguaceroCore extends _events.EventEmitter {
539
255
  this.setState({
540
256
  mrmsTimestamp: newTimestamp
541
257
  });
542
- } else if (this.state.isNexrad) {
543
- var _this$nexradTimesBySt;
544
- const nk = this._nexradTimesCacheKey();
545
- const raw = nk ? (_this$nexradTimesBySt = this.nexradTimesByStation[nk]) === null || _this$nexradTimesBySt === void 0 ? void 0 : _this$nexradTimesBySt.unixTimes : [];
546
- const availableTimestamps = this._getFilteredNexradTimestampsForVariable(raw || []);
547
- if (availableTimestamps.length === 0) return;
548
- const ts = this.state.nexradTimestamp == null ? null : Number(this.state.nexradTimestamp);
549
- const currentIndex = ts == null ? -1 : availableTimestamps.indexOf(ts);
550
- let nextIndex = currentIndex === -1 ? availableTimestamps.length - 1 : currentIndex + direction;
551
- const maxIndex = availableTimestamps.length - 1;
552
- if (nextIndex > maxIndex) nextIndex = 0;
553
- if (nextIndex < 0) nextIndex = maxIndex;
554
- this.setState({
555
- nexradTimestamp: availableTimestamps[nextIndex]
556
- });
557
258
  } else {
259
+ var _this$modelStatus3;
558
260
  const {
261
+ model,
262
+ date,
263
+ run,
559
264
  forecastHour
560
265
  } = this.state;
561
- const forecastHours = this.getAvailableForecastHours();
266
+ const forecastHours = (_this$modelStatus3 = this.modelStatus) === null || _this$modelStatus3 === void 0 || (_this$modelStatus3 = _this$modelStatus3[model]) === null || _this$modelStatus3 === void 0 || (_this$modelStatus3 = _this$modelStatus3[date]) === null || _this$modelStatus3 === void 0 ? void 0 : _this$modelStatus3[run];
562
267
  if (!forecastHours || forecastHours.length === 0) return;
563
- const fh = Number(forecastHour);
564
- const currentIndex = forecastHours.indexOf(fh);
268
+ const currentIndex = forecastHours.indexOf(forecastHour);
565
269
  if (currentIndex === -1) return;
566
270
  const maxIndex = forecastHours.length - 1;
567
271
  let nextIndex = currentIndex + direction;
@@ -595,7 +299,8 @@ class AguaceroCore extends _events.EventEmitter {
595
299
  async setVariable(variable) {
596
300
  // --- NEW CODE: Handle switching TO ptypeRefl on HRRR ---
597
301
  if (variable === 'ptypeRefl' && this.state.model === 'hrrr' && this.state.forecastHour === 0) {
598
- const availableHours = resolveModelRunHours(this.modelStatus, this.state.model, this.state.date, this.state.run).hours || [];
302
+ var _this$modelStatus4;
303
+ const availableHours = ((_this$modelStatus4 = this.modelStatus) === null || _this$modelStatus4 === void 0 || (_this$modelStatus4 = _this$modelStatus4[this.state.model]) === null || _this$modelStatus4 === void 0 || (_this$modelStatus4 = _this$modelStatus4[this.state.date]) === null || _this$modelStatus4 === void 0 ? void 0 : _this$modelStatus4[this.state.run]) || [];
599
304
  const firstValidHour = availableHours.find(hour => hour !== 0) || 0;
600
305
  await this.setState({
601
306
  variable,
@@ -610,17 +315,8 @@ class AguaceroCore extends _events.EventEmitter {
610
315
  });
611
316
  }
612
317
  async setModel(modelName) {
613
- var _this$modelStatus2;
614
- if (modelName === this.state.model || !((_this$modelStatus2 = this.modelStatus) !== null && _this$modelStatus2 !== void 0 && _this$modelStatus2[modelName])) return;
615
- if (this.state.isSatellite) {
616
- await this.setState({
617
- isSatellite: false,
618
- satelliteInstrumentId: null,
619
- satelliteSectorLabel: null,
620
- satelliteChannel: null,
621
- satelliteTimestamp: null
622
- });
623
- }
318
+ var _this$modelStatus5;
319
+ if (modelName === this.state.model || !((_this$modelStatus5 = this.modelStatus) !== null && _this$modelStatus5 !== void 0 && _this$modelStatus5[modelName])) return;
624
320
  const latestRun = findLatestModelRun(this.modelStatus, modelName);
625
321
  if (latestRun) {
626
322
  // --- NEW CODE: Determine initial forecast hour ---
@@ -628,7 +324,9 @@ class AguaceroCore extends _events.EventEmitter {
628
324
 
629
325
  // If switching to HRRR with ptypeRefl, start at hour 1 instead of 0
630
326
  if (modelName === 'hrrr' && this.state.variable === 'ptypeRefl') {
631
- const availableHours = resolveModelRunHours(this.modelStatus, modelName, latestRun.date, latestRun.run).hours || [];
327
+ var _this$modelStatus6;
328
+ const availableHours = ((_this$modelStatus6 = this.modelStatus) === null || _this$modelStatus6 === void 0 || (_this$modelStatus6 = _this$modelStatus6[modelName]) === null || _this$modelStatus6 === void 0 || (_this$modelStatus6 = _this$modelStatus6[latestRun.date]) === null || _this$modelStatus6 === void 0 ? void 0 : _this$modelStatus6[latestRun.run]) || [];
329
+ // Find the first valid hour (should be 1)
632
330
  initialHour = availableHours.find(hour => hour !== 0) || 0;
633
331
  }
634
332
  // --- END NEW CODE ---
@@ -658,16 +356,11 @@ class AguaceroCore extends _events.EventEmitter {
658
356
  });
659
357
  }
660
358
  async setMRMSVariable(variable) {
661
- const sortedTimestamps = this._getFilteredMrmsTimestampsForVariable(variable);
662
- const initialTimestamp = sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1] : null;
359
+ const sortedTimestamps = [...(this.mrmsStatus[variable] || [])].sort((a, b) => b - a);
360
+ const initialTimestamp = sortedTimestamps.length > 0 ? sortedTimestamps[0] : null;
663
361
  await this.setState({
664
362
  variable,
665
363
  isMRMS: true,
666
- isSatellite: false,
667
- satelliteInstrumentId: null,
668
- satelliteSectorLabel: null,
669
- satelliteChannel: null,
670
- satelliteTimestamp: null,
671
364
  mrmsTimestamp: initialTimestamp
672
365
  });
673
366
  }
@@ -677,184 +370,42 @@ class AguaceroCore extends _events.EventEmitter {
677
370
  mrmsTimestamp: timestamp
678
371
  });
679
372
  }
680
- async setSatelliteTimestamp(timestamp) {
681
- if (!this.state.isSatellite) return;
682
- await this.setState({
683
- satelliteTimestamp: timestamp != null ? Number(timestamp) : null
684
- });
685
- }
686
-
687
- /**
688
- * How many hours of satellite frames to include in the timeline (positive, at most 12 hours).
689
- * API default: `layerOptions.satelliteDurationValue` on construction.
690
- */
691
- async setSatelliteDurationValue(value) {
692
- const v = (0, _satellite_support.formatTimelineDurationValue)(value);
693
- await this.setState({
694
- satelliteDurationValue: v
695
- });
696
- if (!this.state.isSatellite || !this.state.satelliteInstrumentId) return;
697
- const timeline = this._computeSatelliteTimeline();
698
- const tsList = [...(timeline.unixTimes || [])].map(t => Number(t)).filter(t => !Number.isNaN(t)).sort((a, b) => a - b);
699
- if (tsList.length === 0) return;
700
- const cur = this.state.satelliteTimestamp;
701
- const curN = cur == null ? null : Number(cur);
702
- if (curN == null || !tsList.includes(curN)) {
703
- await this.setState({
704
- satelliteTimestamp: tsList[tsList.length - 1]
705
- });
706
- }
707
- }
708
-
709
- /**
710
- * Set satellite view using spacecraft id, sector, and channel/product.
711
- * Omitted fields keep the current selection; when not in satellite mode, missing fields use GOES-19 East CONUS / C13 defaults.
712
- * @param {{ satelliteId?: string, sector?: string, satelliteSector?: string, satelliteProduct?: string, satelliteChannel?: string, satelliteTimestamp?: number|null }} opts
713
- */
714
- async setSatelliteSelection(opts = {}) {
715
- const cur = this.state.isSatellite ? this.state : null;
716
- const tsArg = opts.satelliteTimestamp !== undefined ? opts.satelliteTimestamp : this.state.isSatellite && this.state.satelliteTimestamp != null ? Number(this.state.satelliteTimestamp) : undefined;
717
- return this.switchMode({
718
- mode: 'satellite',
719
- satelliteId: opts.satelliteId ?? (cur === null || cur === void 0 ? void 0 : cur.satelliteInstrumentId) ?? 'GOES19-EAST',
720
- satelliteSector: opts.satelliteSector ?? opts.sector ?? (cur === null || cur === void 0 ? void 0 : cur.satelliteSectorLabel) ?? 'conus',
721
- satelliteProduct: opts.satelliteProduct ?? opts.satelliteChannel ?? (cur === null || cur === void 0 ? void 0 : cur.satelliteChannel) ?? 'C13',
722
- satelliteTimestamp: tsArg
723
- });
724
- }
725
-
726
- /**
727
- * How many hours of MRMS frames to include in the timeline (positive, at most 12 hours).
728
- * API default: `layerOptions.mrmsDurationValue` on construction.
729
- */
730
- async setMRMSDurationValue(value) {
731
- const v = (0, _satellite_support.formatTimelineDurationValue)(value);
732
- await this.setState({
733
- mrmsDurationValue: v
734
- });
735
- if (!this.state.isMRMS || !this.state.variable) return;
736
- const filtered = this._getFilteredMrmsTimestampsForVariable(this.state.variable);
737
- if (filtered.length === 0) return;
738
- const cur = this.state.mrmsTimestamp;
739
- const curN = cur == null ? null : Number(cur);
740
- if (curN == null || !filtered.includes(curN)) {
741
- await this.setState({
742
- mrmsTimestamp: filtered[filtered.length - 1]
743
- });
744
- }
745
- }
746
-
747
- /**
748
- * NEXRAD sweep listing / scrub window in hours (independent of MRMS duration).
749
- * API default: `layerOptions.nexradDurationValue` on construction.
750
- */
751
- async setNexradDurationValue(value) {
752
- var _this$nexradTimesBySt2;
753
- const v = (0, _satellite_support.formatTimelineDurationValue)(value);
754
- await this.setState({
755
- nexradDurationValue: v
756
- });
757
- if (!this.state.isNexrad || !this.state.nexradSite) return;
758
- await this.refreshNexradTimes();
759
- const nk = this._nexradTimesCacheKey();
760
- const filtered = this._getFilteredNexradTimestampsForVariable(nk ? ((_this$nexradTimesBySt2 = this.nexradTimesByStation[nk]) === null || _this$nexradTimesBySt2 === void 0 ? void 0 : _this$nexradTimesBySt2.unixTimes) || [] : []);
761
- if (filtered.length === 0) return;
762
- const cur = this.state.nexradTimestamp;
763
- const curN = cur == null ? null : Number(cur);
764
- if (curN == null || !filtered.includes(curN)) {
765
- await this.setState({
766
- nexradTimestamp: filtered[filtered.length - 1]
767
- });
768
- }
769
- }
770
373
  async switchMode(options) {
771
- let {
374
+ const {
772
375
  mode,
773
376
  variable,
774
377
  model,
775
378
  forecastHour,
776
- mrmsTimestamp,
777
- satelliteTimestamp
379
+ mrmsTimestamp
778
380
  } = options;
779
- if (!mode) {
381
+ if (!mode || !variable) {
382
+ console.error("switchMode requires 'mode' ('mrms' | 'model') and 'variable' properties.");
780
383
  return;
781
384
  }
782
385
  if (mode === 'model' && !model) {
783
- return;
784
- }
785
- if ((mode === 'mrms' || mode === 'model') && !variable) {
386
+ console.error("switchMode with mode 'model' requires a 'model' property.");
786
387
  return;
787
388
  }
788
389
  let targetState = {};
789
- if (mode === 'satellite') {
790
- var _this$satelliteListin2;
791
- const satelliteInstrumentId = options.satelliteId ?? 'GOES19-EAST';
792
- const satelliteSectorLabel = (0, _satellite_support.resolveSatelliteSectorLabel)(options.satelliteSector ?? options.sector ?? 'conus');
793
- const satelliteChannel = options.satelliteProduct ?? options.satelliteChannel ?? variable ?? 'C13';
794
- const channelToken = satelliteChannel;
795
- // Emit satellite mode immediately so map layers (e.g. model grid) clear before listing fetch finishes.
796
- await this.setState({
797
- isSatellite: true,
798
- isMRMS: false,
799
- isNexrad: false,
800
- satelliteInstrumentId,
801
- satelliteSectorLabel,
802
- satelliteChannel,
803
- variable: channelToken,
804
- satelliteTimestamp: null,
805
- mrmsTimestamp: null,
806
- date: null,
807
- run: null,
808
- forecastHour: 0
809
- });
810
- await this.fetchSatelliteListing(true);
811
- const allFiles = ((_this$satelliteListin2 = this.satelliteListing) === null || _this$satelliteListin2 === void 0 || (_this$satelliteListin2 = _this$satelliteListin2.objects) === null || _this$satelliteListin2 === void 0 ? void 0 : _this$satelliteListin2.map(o => o.key)) || [];
812
- const sectorName = satelliteSectorLabel;
813
- const tier = this.state.satelliteTier || 'basic';
814
- const durationOpt = (0, _satellite_support.resolveSatelliteDurationOption)(sectorName, tier, this.state.satelliteDurationValue);
815
- const timeline = (0, _satellite_support.buildSatelliteTimelineForSelection)({
816
- satelliteInstrumentId,
817
- satelliteSectorLabel,
818
- satelliteChannel
819
- }, allFiles, durationOpt);
820
- const tsList = [...(timeline.unixTimes || [])].sort((a, b) => a - b);
821
- let finalTs = satelliteTimestamp !== undefined ? satelliteTimestamp : null;
822
- if (finalTs == null && tsList.length > 0) {
823
- finalTs = tsList[tsList.length - 1];
824
- }
825
- await this.setState({
826
- satelliteTimestamp: finalTs != null ? Number(finalTs) : null
827
- });
828
- return;
829
- } else if (mode === 'mrms') {
830
- const sortedTimestamps = this._getFilteredMrmsTimestampsForVariable(variable);
390
+ if (mode === 'mrms') {
831
391
  let finalTimestamp = mrmsTimestamp;
832
392
  if (finalTimestamp === undefined) {
833
- finalTimestamp = sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1] : null;
834
- } else if (sortedTimestamps.length > 0) {
835
- const n = Number(finalTimestamp);
836
- if (!sortedTimestamps.includes(n)) {
837
- finalTimestamp = sortedTimestamps[sortedTimestamps.length - 1];
838
- }
393
+ const sortedTimestamps = [...(this.mrmsStatus[variable] || [])].sort((a, b) => b - a);
394
+ finalTimestamp = sortedTimestamps.length > 0 ? sortedTimestamps[0] : null;
839
395
  }
840
396
  targetState = {
841
397
  isMRMS: true,
842
- isSatellite: false,
843
- isNexrad: false,
844
398
  variable: variable,
845
399
  mrmsTimestamp: finalTimestamp,
846
400
  model: this.state.model,
847
401
  date: null,
848
402
  run: null,
849
- forecastHour: 0,
850
- satelliteInstrumentId: null,
851
- satelliteSectorLabel: null,
852
- satelliteChannel: null,
853
- satelliteTimestamp: null
403
+ forecastHour: 0
854
404
  };
855
405
  } else if (mode === 'model') {
856
406
  const latestRun = findLatestModelRun(this.modelStatus, model);
857
407
  if (!latestRun) {
408
+ console.error(`Could not find a valid run for model: ${model}`);
858
409
  return;
859
410
  }
860
411
 
@@ -863,289 +414,50 @@ class AguaceroCore extends _events.EventEmitter {
863
414
 
864
415
  // If switching to HRRR with ptypeRefl and hour is 0, use hour 1
865
416
  if (model === 'hrrr' && variable === 'ptypeRefl' && initialHour === 0) {
866
- const availableHours = resolveModelRunHours(this.modelStatus, model, latestRun.date, latestRun.run).hours || [];
417
+ var _this$modelStatus7;
418
+ const availableHours = ((_this$modelStatus7 = this.modelStatus) === null || _this$modelStatus7 === void 0 || (_this$modelStatus7 = _this$modelStatus7[model]) === null || _this$modelStatus7 === void 0 || (_this$modelStatus7 = _this$modelStatus7[latestRun.date]) === null || _this$modelStatus7 === void 0 ? void 0 : _this$modelStatus7[latestRun.run]) || [];
867
419
  initialHour = availableHours.find(hour => hour !== 0) || 0;
868
420
  }
869
421
  // --- END NEW CODE ---
870
422
 
871
423
  targetState = {
872
424
  isMRMS: false,
873
- isSatellite: false,
874
- isNexrad: false,
875
425
  model: model,
876
426
  variable: variable,
877
427
  date: latestRun.date,
878
428
  run: latestRun.run,
879
429
  forecastHour: initialHour,
880
430
  // <-- Changed
881
- mrmsTimestamp: null,
882
- satelliteInstrumentId: null,
883
- satelliteSectorLabel: null,
884
- satelliteChannel: null,
885
- satelliteTimestamp: null
431
+ mrmsTimestamp: null
886
432
  };
887
- } else if (mode === 'nexrad') {
888
- const nexradProduct = options.nexradProduct || 'REF';
889
- const p = nexradProduct.toUpperCase();
890
- const site = options.nexradSite ?? null;
891
- let tilt = options.nexradTilt != null ? Number(options.nexradTilt) : site != null ? (0, _nexradTilts.getDefaultRadarTilt)(site) : null;
892
- await this.setState({
893
- isNexrad: true,
894
- isMRMS: false,
895
- isSatellite: false,
896
- nexradSite: site,
897
- nexradProduct: p,
898
- nexradTilt: tilt,
899
- nexradTimestamp: options.nexradTimestamp != null ? Number(options.nexradTimestamp) : null,
900
- nexradStormRelative: options.nexradStormRelative === true,
901
- nexradShowSitesPicker: options.nexradShowSitesPicker !== false,
902
- ...(options.nexradDurationValue != null ? {
903
- nexradDurationValue: (0, _satellite_support.formatTimelineDurationValue)(options.nexradDurationValue)
904
- } : {}),
905
- mrmsTimestamp: null,
906
- satelliteInstrumentId: null,
907
- satelliteSectorLabel: null,
908
- satelliteChannel: null,
909
- satelliteTimestamp: null,
910
- date: null,
911
- run: null,
912
- forecastHour: 0
913
- });
914
- const manifest = await (0, _nexradTilts.fetchRadarTiltsManifestFromNetwork)();
915
- if (manifest) (0, _nexradTilts.setRadarTiltsManifest)(manifest);
916
- if (site) {
917
- var _this$nexradTimesBySt3;
918
- await this.refreshNexradTimes();
919
- const filtered = this._getFilteredNexradTimestampsForVariable(((_this$nexradTimesBySt3 = this.nexradTimesByStation[this._nexradTimesCacheKey()]) === null || _this$nexradTimesBySt3 === void 0 ? void 0 : _this$nexradTimesBySt3.unixTimes) || []);
920
- let ts = options.nexradTimestamp != null ? Number(options.nexradTimestamp) : this.state.nexradTimestamp;
921
- if (ts == null && filtered.length > 0) ts = filtered[filtered.length - 1];else if (ts != null && filtered.length > 0 && !filtered.includes(ts)) {
922
- ts = filtered[filtered.length - 1];
923
- }
924
- await this.setState({
925
- nexradTimestamp: ts
926
- });
927
- }
928
- return;
929
433
  } else {
434
+ console.error(`Invalid mode specified in switchMode: '${mode}'`);
930
435
  return;
931
436
  }
932
437
  await this.setState(targetState);
933
438
  }
934
439
 
935
- /**
936
- * Keep `nexradTilt` on a coalesced elevation (same rules as aguacero-frontend tilt controls).
937
- */
938
- async _snapNexradTiltToAvailableOptions() {
939
- const s = this.state;
940
- if (!s.isNexrad || !s.nexradSite) return;
941
- const raw = (0, _nexrad_support.getRawNexradTiltsForCoalesce)(s.nexradSite, s.nexradDataSource || 'level2', s.nexradProduct || 'REF');
942
- if (!raw.length) return;
943
- const t = s.nexradTilt;
944
- const target = t != null && Number.isFinite(Number(t)) ? Number(t) : (0, _nexradTilts.getDefaultRadarTilt)(s.nexradSite);
945
- const canonical = (0, _nexradTiltCoalesce.nexradLayerTiltToDisplayOption)(target, raw);
946
- const match = (a, b) => Math.abs(Number(a) - Number(b)) < 1e-4;
947
- if (match(s.nexradTilt, canonical)) return;
948
- await this.setState({
949
- nexradTilt: canonical
950
- });
951
- }
952
- async refreshNexradTimes() {
953
- var _getNexradLevel3Entry2, _out$level3MotionUnix, _out$level3MotionUnix2;
954
- const s0 = this.state;
955
- if (!s0.isNexrad || !s0.nexradSite) return;
956
- await this._snapNexradTiltToAvailableOptions();
957
- const s = this.state;
958
- const listingHours = this._nexradListingWindowHours();
959
- const out = await (0, _nexrad_support.fetchNexradTimesListing)({
960
- stationId: s.nexradSite,
961
- variable: s.nexradProduct || 'REF',
962
- elev: s.nexradTilt,
963
- source: s.nexradDataSource || 'level2',
964
- level3StormRelative: s.nexradStormRelative,
965
- level3Product: (_getNexradLevel3Entry2 = (0, _nexrad_level3_catalog.getNexradLevel3EntryByRadarKey)(s.nexradProduct)) === null || _getNexradLevel3Entry2 === void 0 ? void 0 : _getNexradLevel3Entry2.product,
966
- listingWindowHours: listingHours
967
- });
968
- const nk = this._nexradTimesCacheKey();
969
- if (!nk) return;
970
- this.nexradTimesByStation[nk] = {
971
- unixTimes: out.unixTimes,
972
- timeToKeyMap: out.timeToKeyMap,
973
- level3MotionTimeToKeyMap: out.level3MotionTimeToKeyMap,
974
- listWindowHours: listingHours
975
- };
976
- if (out.level3MotionKey && (_out$level3MotionUnix = out.level3MotionUnixTimes) !== null && _out$level3MotionUnix !== void 0 && _out$level3MotionUnix.length) {
977
- this.nexradTimesByStation[out.level3MotionKey] = {
978
- unixTimes: out.level3MotionUnixTimes,
979
- timeToKeyMap: out.level3MotionTimeToKeyMap,
980
- listWindowHours: listingHours
981
- };
982
- }
983
- if (out.l2StormMotionListKey && (_out$level3MotionUnix2 = out.level3MotionUnixTimes) !== null && _out$level3MotionUnix2 !== void 0 && _out$level3MotionUnix2.length) {
984
- this.nexradTimesByStation[out.l2StormMotionListKey] = {
985
- unixTimes: out.level3MotionUnixTimes,
986
- timeToKeyMap: out.level3MotionTimeToKeyMap,
987
- listWindowHours: listingHours
988
- };
989
- }
990
- const filtered = this._getFilteredNexradTimestampsForVariable(out.unixTimes || []);
991
- const map = out.timeToKeyMap || {};
992
- if (filtered.length > 0) {
993
- const cur = this.state.nexradTimestamp != null ? Number(this.state.nexradTimestamp) : null;
994
- const hasKey = cur != null && Object.prototype.hasOwnProperty.call(map, String(cur));
995
- const inWindow = cur != null && filtered.includes(cur);
996
- if (cur == null || !inWindow || !hasKey) {
997
- this.state.nexradTimestamp = filtered[filtered.length - 1];
998
- }
999
- } else if (this.state.nexradTimestamp != null) {
1000
- this.state.nexradTimestamp = null;
1001
- }
1002
- this._emitStateChange();
1003
- }
1004
- async setNexradSite(siteId) {
1005
- var _this$nexradTimesBySt4;
1006
- if (!this.state.isNexrad) return;
1007
- const tilt = siteId ? (0, _nexradTilts.getDefaultRadarTilt)(siteId) : null;
1008
- await this.setState({
1009
- nexradSite: siteId || null,
1010
- nexradTilt: tilt,
1011
- nexradTimestamp: null
1012
- });
1013
- await this.refreshNexradTimes();
1014
- const filtered = this._getFilteredNexradTimestampsForVariable(((_this$nexradTimesBySt4 = this.nexradTimesByStation[this._nexradTimesCacheKey()]) === null || _this$nexradTimesBySt4 === void 0 ? void 0 : _this$nexradTimesBySt4.unixTimes) || []);
1015
- if (filtered.length > 0) {
1016
- await this.setState({
1017
- nexradTimestamp: filtered[filtered.length - 1]
1018
- });
1019
- }
1020
- }
1021
- async setNexradProduct(product) {
1022
- var _this$nexradTimesBySt5;
1023
- if (!this.state.isNexrad) return;
1024
- const p = (product || 'REF').toUpperCase();
1025
- await this.setState({
1026
- nexradProduct: p
1027
- });
1028
- await this.refreshNexradTimes();
1029
- const filtered = this._getFilteredNexradTimestampsForVariable(((_this$nexradTimesBySt5 = this.nexradTimesByStation[this._nexradTimesCacheKey()]) === null || _this$nexradTimesBySt5 === void 0 ? void 0 : _this$nexradTimesBySt5.unixTimes) || []);
1030
- if (filtered.length > 0) {
1031
- await this.setState({
1032
- nexradTimestamp: filtered[filtered.length - 1]
1033
- });
1034
- }
1035
- }
1036
- async setNexradTilt(tilt) {
1037
- if (!this.state.isNexrad || !this.state.nexradSite) return;
1038
- await this.setState({
1039
- nexradTilt: tilt != null ? Number(tilt) : null
1040
- });
1041
- await this.refreshNexradTimes();
1042
- }
1043
-
1044
- /**
1045
- * Opt-in storm-relative velocity (L3: N0G + N0S). When off (default), only base radial velocity is used (faster).
1046
- * @param {boolean} enabled
1047
- */
1048
- async setNexradStormRelative(enabled) {
1049
- if (!this.state.isNexrad) return;
1050
- await this.setState({
1051
- nexradStormRelative: Boolean(enabled)
1052
- });
1053
- await this.refreshNexradTimes();
1054
- }
1055
- async setNexradTimestamp(ts) {
1056
- if (!this.state.isNexrad) return;
1057
- await this.setState({
1058
- nexradTimestamp: ts != null ? Number(ts) : null
1059
- });
1060
- }
1061
-
1062
440
  // --- Data and Calculation Methods ---
1063
441
 
1064
- _ensureGridDecodeWorker() {
1065
- if (this._gridDecodeWorkerDisabled) {
1066
- return null;
1067
- }
1068
- if (this._gridDecodeWorker) {
1069
- return this._gridDecodeWorker;
1070
- }
1071
- if (typeof Worker === 'undefined') {
1072
- this._gridDecodeWorkerDisabled = true;
1073
- return null;
1074
- }
1075
- try {
1076
- this._gridDecodeWorker = new Worker(new URL('./gridDecodeWorker.js', import.meta.url), {
1077
- type: 'module'
1078
- });
1079
- this._gridDecodeWorker.addEventListener('error', () => {
1080
- this._gridDecodeWorkerDisabled = true;
1081
- if (this._gridDecodeWorker) {
1082
- this._gridDecodeWorker.terminate();
1083
- this._gridDecodeWorker = null;
1084
- }
1085
- });
1086
- } catch {
1087
- this._gridDecodeWorkerDisabled = true;
1088
- return null;
1089
- }
1090
- return this._gridDecodeWorker;
1091
- }
442
+ _reconstructData(decompressedDeltas, encoding) {
443
+ const expectedLength = encoding.length;
444
+ const reconstructedData = new Int8Array(expectedLength);
445
+ if (decompressedDeltas.length > 0 && expectedLength > 0) {
446
+ // First value is absolute
447
+ reconstructedData[0] = decompressedDeltas[0] > 127 ? decompressedDeltas[0] - 256 : decompressedDeltas[0];
1092
448
 
1093
- /**
1094
- * Offloads zstd + delta decode + transform to a Worker when available; falls back to main-thread
1095
- * {@link processCompressedGrid}. Uses a copy for postMessage transfer so the original `compressedData`
1096
- * stays valid if the Worker path fails.
1097
- */
1098
- _decodeGridPayload(compressedData, encoding) {
1099
- const worker = this._ensureGridDecodeWorker();
1100
- if (!worker) {
1101
- return Promise.resolve((0, _gridDecodePipeline.processCompressedGrid)(compressedData, encoding));
1102
- }
1103
- const payload = compressedData.slice();
1104
- return new Promise((resolve, reject) => {
1105
- const id = ++this._gridDecodeMsgId;
1106
- const onMsg = e => {
1107
- if (!e.data || e.data.id !== id) {
1108
- return;
1109
- }
1110
- worker.removeEventListener('message', onMsg);
1111
- worker.removeEventListener('error', onErr);
1112
- if (e.data.error) {
1113
- reject(new Error(e.data.error));
1114
- return;
1115
- }
1116
- const data = new Uint8Array(e.data.dataBuffer, e.data.dataByteOffset, e.data.dataByteLength);
1117
- resolve({
1118
- data,
1119
- encoding: e.data.encoding
1120
- });
1121
- };
1122
- const onErr = err => {
1123
- worker.removeEventListener('message', onMsg);
1124
- worker.removeEventListener('error', onErr);
1125
- reject(err);
1126
- };
1127
- worker.addEventListener('message', onMsg);
1128
- worker.addEventListener('error', onErr);
1129
- try {
1130
- worker.postMessage({
1131
- id,
1132
- encoding,
1133
- compressedBuffer: payload.buffer,
1134
- compressedByteOffset: payload.byteOffset,
1135
- compressedByteLength: payload.byteLength
1136
- }, [payload.buffer]);
1137
- } catch (err) {
1138
- worker.removeEventListener('message', onMsg);
1139
- worker.removeEventListener('error', onErr);
1140
- reject(err);
449
+ // Subsequent values are deltas from the previous one
450
+ for (let i = 1; i < expectedLength; i++) {
451
+ const delta = decompressedDeltas[i] > 127 ? decompressedDeltas[i] - 256 : decompressedDeltas[i];
452
+ reconstructedData[i] = reconstructedData[i - 1] + delta;
1141
453
  }
1142
- });
454
+ }
455
+ // Return as a Uint8Array, which is what the rest of the code expects
456
+ return new Uint8Array(reconstructedData.buffer);
1143
457
  }
1144
458
  async _loadGridData(state) {
1145
459
  if (this.isReactNative) {
1146
- return null;
1147
- }
1148
- if (state.isNexrad) {
460
+ console.warn(`[AguaceroCore] _loadGridData was called in React Native. This is a bypass. Data loading is handled natively.`);
1149
461
  return null;
1150
462
  }
1151
463
  const {
@@ -1179,6 +491,17 @@ class AguaceroCore extends _events.EventEmitter {
1179
491
  if (this.dataCache.has(dataUrlIdentifier)) {
1180
492
  return this.dataCache.get(dataUrlIdentifier);
1181
493
  }
494
+
495
+ // --- EDITED ---
496
+ // If we are in React Native, this function should NOT do any work.
497
+ // The native WeatherFrameProcessorModule is now responsible for all data loading.
498
+ // This function might still be called by a "cache miss" fallback, but it
499
+ // should not fetch data from JS anymore. We return null so the fallback knows
500
+ // that the native module is the only source of truth for new data.
501
+ if (this.isReactNative) {
502
+ console.warn(`_loadGridData was called in React Native for ${dataUrlIdentifier}. This should be handled by the native module. Returning null.`);
503
+ return null;
504
+ }
1182
505
  const abortController = new AbortController();
1183
506
  this.abortControllers.set(dataUrlIdentifier, abortController);
1184
507
  const loadPromise = (async () => {
@@ -1206,18 +529,38 @@ class AguaceroCore extends _events.EventEmitter {
1206
529
  encoding
1207
530
  } = await response.json();
1208
531
  const compressedData = Uint8Array.from(atob(b64Data), c => c.charCodeAt(0));
1209
- let gridPayload;
1210
- try {
1211
- gridPayload = await this._decodeGridPayload(compressedData, encoding);
1212
- } catch {
1213
- gridPayload = (0, _gridDecodePipeline.processCompressedGrid)(compressedData, encoding);
532
+
533
+ // This path is now ONLY for the web worker
534
+ const requestId = this.workerRequestId++;
535
+ const workerPromise = new Promise((resolve, reject) => {
536
+ this.workerResolvers.set(requestId, {
537
+ resolve,
538
+ reject
539
+ });
540
+ });
541
+ this.worker.postMessage({
542
+ requestId,
543
+ compressedData,
544
+ encoding
545
+ }, [compressedData.buffer]);
546
+ const result = await workerPromise;
547
+ const finalData = result.data;
548
+ const transformedData = new Uint8Array(finalData.length);
549
+ for (let i = 0; i < finalData.length; i++) {
550
+ const signedValue = finalData[i] > 127 ? finalData[i] - 256 : finalData[i];
551
+ transformedData[i] = signedValue + 128;
1214
552
  }
1215
553
  this.abortControllers.delete(dataUrlIdentifier);
1216
554
  return {
1217
- data: gridPayload.data,
1218
- encoding: gridPayload.encoding
555
+ data: transformedData,
556
+ encoding
1219
557
  };
1220
558
  } catch (error) {
559
+ if (error.name === 'AbortError') {
560
+ console.log(`Request cancelled for ${resourcePath}`);
561
+ } else {
562
+ console.error(`Failed to load data for path ${resourcePath}:`, error);
563
+ }
1221
564
  this.dataCache.delete(dataUrlIdentifier);
1222
565
  this.abortControllers.delete(dataUrlIdentifier);
1223
566
  return null;
@@ -1235,11 +578,9 @@ class AguaceroCore extends _events.EventEmitter {
1235
578
  // Clear both maps
1236
579
  this.abortControllers.clear();
1237
580
  this.dataCache.clear();
581
+ console.log('All pending requests cancelled');
1238
582
  }
1239
583
  async getValueAtLngLat(lng, lat) {
1240
- if (this.state.isSatellite || this.state.isNexrad) {
1241
- return null;
1242
- }
1243
584
  const {
1244
585
  variable,
1245
586
  isMRMS,
@@ -1627,6 +968,7 @@ class AguaceroCore extends _events.EventEmitter {
1627
968
  y
1628
969
  };
1629
970
  } catch (error) {
971
+ console.warn(`[GridAccessor] RGEM polar stereographic conversion failed for ${lat}, ${lon}:`, error);
1630
972
  return {
1631
973
  x: -1,
1632
974
  y: -1
@@ -1634,8 +976,91 @@ class AguaceroCore extends _events.EventEmitter {
1634
976
  }
1635
977
  }
1636
978
 
1637
- // --- Status Methods ---
979
+ // --- Worker and Status Methods ---
980
+
981
+ createWorker() {
982
+ if (this.isReactNative) return null;
983
+ const workerCode = `
984
+ import { decompress } from 'https://cdn.skypack.dev/fzstd@0.1.1';
1638
985
 
986
+ function _reconstructData(decompressedDeltas, encoding) {
987
+ const expectedLength = encoding.length;
988
+ const reconstructedData = new Int8Array(expectedLength);
989
+ if (decompressedDeltas.length > 0 && expectedLength > 0) {
990
+ reconstructedData[0] = decompressedDeltas[0] > 127 ? decompressedDeltas[0] - 256 : decompressedDeltas[0];
991
+ for (let i = 1; i < expectedLength; i++) {
992
+ const delta = decompressedDeltas[i] > 127 ? decompressedDeltas[i] - 256 : decompressedDeltas[i];
993
+ reconstructedData[i] = reconstructedData[i - 1] + delta;
994
+ }
995
+ }
996
+ return new Uint8Array(reconstructedData.buffer);
997
+ }
998
+
999
+ self.onmessage = async (e) => {
1000
+ const { requestId, compressedData, encoding } = e.data;
1001
+ try {
1002
+ const decompressedDeltas = await decompress(compressedData);
1003
+ const finalData = _reconstructData(decompressedDeltas, encoding);
1004
+ self.postMessage({ success: true, requestId: requestId, decompressedData: finalData, encoding: encoding }, [finalData.buffer]);
1005
+ } catch (error) {
1006
+ self.postMessage({ success: false, requestId: requestId, error: error.message });
1007
+ }
1008
+ };
1009
+ `;
1010
+ const blob = new Blob([workerCode], {
1011
+ type: 'application/javascript'
1012
+ });
1013
+ return new Worker(URL.createObjectURL(blob), {
1014
+ type: 'module'
1015
+ });
1016
+ }
1017
+ _processResultQueue() {
1018
+ while (this.resultQueue.length > 0) {
1019
+ const {
1020
+ success,
1021
+ requestId,
1022
+ decompressedData,
1023
+ encoding,
1024
+ error
1025
+ } = this.resultQueue.shift();
1026
+ if (this.workerResolvers.has(requestId)) {
1027
+ const {
1028
+ resolve,
1029
+ reject
1030
+ } = this.workerResolvers.get(requestId);
1031
+ if (success) {
1032
+ resolve({
1033
+ data: decompressedData
1034
+ }); // Return as { data: ... }
1035
+ } else {
1036
+ reject(new Error(error));
1037
+ }
1038
+ this.workerResolvers.delete(requestId);
1039
+ }
1040
+ }
1041
+ this.isProcessingQueue = false;
1042
+ }
1043
+ _handleWorkerMessage(e) {
1044
+ if (this.isReactNative) return;
1045
+ const {
1046
+ success,
1047
+ requestId,
1048
+ decompressedData,
1049
+ encoding,
1050
+ error
1051
+ } = e.data;
1052
+ this.resultQueue.push({
1053
+ success,
1054
+ requestId,
1055
+ decompressedData,
1056
+ encoding,
1057
+ error
1058
+ });
1059
+ if (!this.isProcessingQueue) {
1060
+ this.isProcessingQueue = true;
1061
+ requestAnimationFrame(() => this._processResultQueue());
1062
+ }
1063
+ }
1639
1064
  async fetchModelStatus(force = false) {
1640
1065
  if (!this.modelStatus || force) {
1641
1066
  try {
@@ -1661,18 +1086,6 @@ class AguaceroCore extends _events.EventEmitter {
1661
1086
  }
1662
1087
  return this.mrmsStatus;
1663
1088
  }
1664
- async fetchSatelliteListing(force = false) {
1665
- if (!this.satelliteListing || force) {
1666
- try {
1667
- const response = await fetch(_satellite_support.SATELLITE_FRAMES_URL);
1668
- if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
1669
- this.satelliteListing = await response.json();
1670
- } catch (error) {
1671
- this.satelliteListing = null;
1672
- }
1673
- }
1674
- return this.satelliteListing;
1675
- }
1676
1089
  startAutoRefresh(intervalSeconds) {
1677
1090
  this.stopAutoRefresh();
1678
1091
  this.autoRefreshIntervalId = setInterval(async () => {