@aguacerowx/javascript-sdk 0.0.20 → 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 +1 -1
- package/src/AguaceroCore.js +164 -91
- package/src/index.js +6 -2
- package/src/nexrad_support.js +11 -0
- package/src/satellite_support.js +96 -25
package/package.json
CHANGED
package/src/AguaceroCore.js
CHANGED
|
@@ -11,14 +11,15 @@ import { processCompressedGrid } from './gridDecodePipeline.js';
|
|
|
11
11
|
import { getBundleId } from './getBundleId';
|
|
12
12
|
import {
|
|
13
13
|
SATELLITE_FRAMES_URL,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
buildSatelliteTimelineForSelection,
|
|
15
|
+
formatTimelineDurationValue,
|
|
16
|
+
resolveSatelliteSectorLabel,
|
|
17
|
+
resolveSatelliteDurationOption,
|
|
18
18
|
parseTimelineDurationHours,
|
|
19
19
|
} from './satellite_support.js';
|
|
20
20
|
import {
|
|
21
21
|
fetchNexradTimesListing,
|
|
22
|
+
inferNexradDataSourceForProduct,
|
|
22
23
|
nexradColormapFldKey,
|
|
23
24
|
variableToNexradGroup,
|
|
24
25
|
getAvailableNexradTilts,
|
|
@@ -140,17 +141,40 @@ export class AguaceroCore extends EventEmitter {
|
|
|
140
141
|
|
|
141
142
|
const initialSatellite = initialMode === 'satellite';
|
|
142
143
|
const initialNexrad = initialMode === 'nexrad';
|
|
143
|
-
const initialNexradDs = userLayerOptions.nexradDataSource || 'level2';
|
|
144
144
|
const initialNexradProd = userLayerOptions.nexradProduct || 'REF';
|
|
145
|
+
const initialNexradDs =
|
|
146
|
+
userLayerOptions.nexradDataSource != null
|
|
147
|
+
? userLayerOptions.nexradDataSource === 'level3'
|
|
148
|
+
? 'level3'
|
|
149
|
+
: 'level2'
|
|
150
|
+
: inferNexradDataSourceForProduct(initialNexradProd);
|
|
145
151
|
const initialNexradFld = initialNexrad
|
|
146
152
|
? nexradColormapFldKey(initialNexradDs, initialNexradProd)
|
|
147
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
|
+
}
|
|
148
168
|
this.state = {
|
|
149
169
|
model: userLayerOptions.model || 'gfs',
|
|
150
170
|
// EDIT: Set isMRMS based on the initial mode
|
|
151
171
|
isMRMS: initialMode === 'mrms' && !initialSatellite && !initialNexrad,
|
|
152
172
|
mrmsTimestamp: null,
|
|
153
|
-
variable: initialNexrad
|
|
173
|
+
variable: initialNexrad
|
|
174
|
+
? initialNexradFld
|
|
175
|
+
: initialSatellite && initialSatelliteInstrumentId
|
|
176
|
+
? initialSatelliteChannel
|
|
177
|
+
: initialVariable,
|
|
154
178
|
date: null,
|
|
155
179
|
run: null,
|
|
156
180
|
forecastHour: 0,
|
|
@@ -159,21 +183,20 @@ export class AguaceroCore extends EventEmitter {
|
|
|
159
183
|
units: options.initialUnit || 'imperial',
|
|
160
184
|
shaderSmoothingEnabled: options.shaderSmoothingEnabled ?? true,
|
|
161
185
|
isSatellite: initialSatellite,
|
|
162
|
-
|
|
186
|
+
satelliteInstrumentId: initialSatelliteInstrumentId,
|
|
187
|
+
satelliteSectorLabel: initialSatelliteSectorLabel,
|
|
188
|
+
satelliteChannel: initialSatelliteChannel,
|
|
163
189
|
satelliteTimestamp: userLayerOptions.satelliteTimestamp != null ? Number(userLayerOptions.satelliteTimestamp) : null,
|
|
164
190
|
satelliteTier: userLayerOptions.satelliteTier || 'basic',
|
|
165
|
-
satelliteDurationValue: (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
);
|
|
175
|
-
return TIMELINE_DURATION_HOUR_VALUES.includes(n) ? n : '1';
|
|
176
|
-
})(),
|
|
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
|
+
),
|
|
177
200
|
isNexrad: initialNexrad,
|
|
178
201
|
nexradSite: userLayerOptions.nexradSite ?? null,
|
|
179
202
|
nexradDataSource: initialNexradDs,
|
|
@@ -200,6 +223,7 @@ export class AguaceroCore extends EventEmitter {
|
|
|
200
223
|
|
|
201
224
|
async setState(newState) {
|
|
202
225
|
const patch = { ...newState };
|
|
226
|
+
if ('satelliteKey' in patch) delete patch.satelliteKey;
|
|
203
227
|
if ('forecastHour' in patch && patch.forecastHour != null) {
|
|
204
228
|
patch.forecastHour = Number(patch.forecastHour);
|
|
205
229
|
}
|
|
@@ -210,12 +234,13 @@ export class AguaceroCore extends EventEmitter {
|
|
|
210
234
|
patch.satelliteTimestamp = Number(patch.satelliteTimestamp);
|
|
211
235
|
}
|
|
212
236
|
if ('satelliteDurationValue' in patch && patch.satelliteDurationValue != null) {
|
|
213
|
-
|
|
214
|
-
patch.satelliteDurationValue = TIMELINE_DURATION_HOUR_VALUES.includes(n) ? n : '1';
|
|
237
|
+
patch.satelliteDurationValue = formatTimelineDurationValue(patch.satelliteDurationValue);
|
|
215
238
|
}
|
|
216
239
|
if ('mrmsDurationValue' in patch && patch.mrmsDurationValue != null) {
|
|
217
|
-
|
|
218
|
-
|
|
240
|
+
patch.mrmsDurationValue = formatTimelineDurationValue(patch.mrmsDurationValue);
|
|
241
|
+
}
|
|
242
|
+
if ('nexradDurationValue' in patch && patch.nexradDurationValue != null) {
|
|
243
|
+
patch.nexradDurationValue = formatTimelineDurationValue(patch.nexradDurationValue);
|
|
219
244
|
}
|
|
220
245
|
if ('nexradTimestamp' in patch && patch.nexradTimestamp != null) {
|
|
221
246
|
patch.nexradTimestamp = Number(patch.nexradTimestamp);
|
|
@@ -253,23 +278,23 @@ export class AguaceroCore extends EventEmitter {
|
|
|
253
278
|
}
|
|
254
279
|
|
|
255
280
|
_computeSatelliteTimeline() {
|
|
256
|
-
|
|
281
|
+
const { satelliteInstrumentId, satelliteSectorLabel, satelliteChannel } = this.state;
|
|
282
|
+
if (!satelliteInstrumentId || !satelliteSectorLabel || !satelliteChannel || !this.satelliteListing?.objects) {
|
|
257
283
|
return { unixTimes: [], timeToFileMap: {} };
|
|
258
284
|
}
|
|
259
285
|
const allFiles = this.satelliteListing.objects.map((o) => o.key);
|
|
260
|
-
const sectorName =
|
|
261
|
-
let sectorType = 'CONUS';
|
|
262
|
-
if (sectorName.includes('FULL DISK')) sectorType = 'FULL_DISK';
|
|
263
|
-
else if (sectorName.includes('MESOSCALE')) sectorType = 'MESOSCALE';
|
|
286
|
+
const sectorName = satelliteSectorLabel;
|
|
264
287
|
const tier = this.state.satelliteTier || 'basic';
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
);
|
|
273
298
|
}
|
|
274
299
|
|
|
275
300
|
/**
|
|
@@ -295,7 +320,7 @@ export class AguaceroCore extends EventEmitter {
|
|
|
295
320
|
}
|
|
296
321
|
|
|
297
322
|
_nexradListingWindowHours() {
|
|
298
|
-
return parseTimelineDurationHours(this.state.
|
|
323
|
+
return parseTimelineDurationHours(this.state.nexradDurationValue);
|
|
299
324
|
}
|
|
300
325
|
|
|
301
326
|
_getFilteredNexradTimestampsForVariable(rawList) {
|
|
@@ -357,7 +382,7 @@ export class AguaceroCore extends EventEmitter {
|
|
|
357
382
|
|
|
358
383
|
let availableSatelliteTimestamps = [];
|
|
359
384
|
let satelliteTimeToFileMap = {};
|
|
360
|
-
if (this.state.isSatellite && this.state.
|
|
385
|
+
if (this.state.isSatellite && this.state.satelliteInstrumentId) {
|
|
361
386
|
const timeline = this._computeSatelliteTimeline();
|
|
362
387
|
satelliteTimeToFileMap = timeline.timeToFileMap || {};
|
|
363
388
|
availableSatelliteTimestamps = [...(timeline.unixTimes || [])]
|
|
@@ -416,7 +441,7 @@ export class AguaceroCore extends EventEmitter {
|
|
|
416
441
|
|
|
417
442
|
let initialState = { ...this.state };
|
|
418
443
|
|
|
419
|
-
if (initialState.isSatellite && initialState.
|
|
444
|
+
if (initialState.isSatellite && initialState.satelliteInstrumentId) {
|
|
420
445
|
const timeline = this._computeSatelliteTimeline();
|
|
421
446
|
const tsList = [...(timeline.unixTimes || [])].sort((a, b) => a - b);
|
|
422
447
|
if (initialState.satelliteTimestamp == null && tsList.length > 0) {
|
|
@@ -625,7 +650,13 @@ export class AguaceroCore extends EventEmitter {
|
|
|
625
650
|
async setModel(modelName) {
|
|
626
651
|
if (modelName === this.state.model || !this.modelStatus?.[modelName]) return;
|
|
627
652
|
if (this.state.isSatellite) {
|
|
628
|
-
await this.setState({
|
|
653
|
+
await this.setState({
|
|
654
|
+
isSatellite: false,
|
|
655
|
+
satelliteInstrumentId: null,
|
|
656
|
+
satelliteSectorLabel: null,
|
|
657
|
+
satelliteChannel: null,
|
|
658
|
+
satelliteTimestamp: null,
|
|
659
|
+
});
|
|
629
660
|
}
|
|
630
661
|
const latestRun = findLatestModelRun(this.modelStatus, modelName);
|
|
631
662
|
if (latestRun) {
|
|
@@ -674,7 +705,9 @@ export class AguaceroCore extends EventEmitter {
|
|
|
674
705
|
variable,
|
|
675
706
|
isMRMS: true,
|
|
676
707
|
isSatellite: false,
|
|
677
|
-
|
|
708
|
+
satelliteInstrumentId: null,
|
|
709
|
+
satelliteSectorLabel: null,
|
|
710
|
+
satelliteChannel: null,
|
|
678
711
|
satelliteTimestamp: null,
|
|
679
712
|
mrmsTimestamp: initialTimestamp,
|
|
680
713
|
});
|
|
@@ -691,14 +724,13 @@ export class AguaceroCore extends EventEmitter {
|
|
|
691
724
|
}
|
|
692
725
|
|
|
693
726
|
/**
|
|
694
|
-
* How many hours of satellite frames to include in the timeline (
|
|
727
|
+
* How many hours of satellite frames to include in the timeline (positive, at most 12 hours).
|
|
695
728
|
* API default: `layerOptions.satelliteDurationValue` on construction.
|
|
696
729
|
*/
|
|
697
730
|
async setSatelliteDurationValue(value) {
|
|
698
|
-
const v =
|
|
699
|
-
if (!TIMELINE_DURATION_HOUR_VALUES.includes(v)) return;
|
|
731
|
+
const v = formatTimelineDurationValue(value);
|
|
700
732
|
await this.setState({ satelliteDurationValue: v });
|
|
701
|
-
if (!this.state.isSatellite || !this.state.
|
|
733
|
+
if (!this.state.isSatellite || !this.state.satelliteInstrumentId) return;
|
|
702
734
|
const timeline = this._computeSatelliteTimeline();
|
|
703
735
|
const tsList = [...(timeline.unixTimes || [])]
|
|
704
736
|
.map((t) => Number(t))
|
|
@@ -713,30 +745,35 @@ export class AguaceroCore extends EventEmitter {
|
|
|
713
745
|
}
|
|
714
746
|
|
|
715
747
|
/**
|
|
716
|
-
*
|
|
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).
|
|
717
772
|
* API default: `layerOptions.mrmsDurationValue` on construction.
|
|
718
773
|
*/
|
|
719
774
|
async setMRMSDurationValue(value) {
|
|
720
|
-
const v =
|
|
721
|
-
if (!TIMELINE_DURATION_HOUR_VALUES.includes(v)) return;
|
|
775
|
+
const v = formatTimelineDurationValue(value);
|
|
722
776
|
await this.setState({ mrmsDurationValue: v });
|
|
723
|
-
// NEXRAD listings are fetched with `hours` from this value (see _nexradListingWindowHours). Without a
|
|
724
|
-
// refresh, expanding 1h→4h only re-filters the old in-memory sweep list — new frames never appear until
|
|
725
|
-
// site/product changes trigger refreshNexradTimes.
|
|
726
|
-
if (this.state.isNexrad && this.state.nexradSite) {
|
|
727
|
-
await this.refreshNexradTimes();
|
|
728
|
-
const nk = this._nexradTimesCacheKey();
|
|
729
|
-
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
730
|
-
nk ? this.nexradTimesByStation[nk]?.unixTimes || [] : [],
|
|
731
|
-
);
|
|
732
|
-
if (filtered.length === 0) return;
|
|
733
|
-
const cur = this.state.nexradTimestamp;
|
|
734
|
-
const curN = cur == null ? null : Number(cur);
|
|
735
|
-
if (curN == null || !filtered.includes(curN)) {
|
|
736
|
-
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
737
|
-
}
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
777
|
if (!this.state.isMRMS || !this.state.variable) return;
|
|
741
778
|
const filtered = this._getFilteredMrmsTimestampsForVariable(this.state.variable);
|
|
742
779
|
if (filtered.length === 0) return;
|
|
@@ -747,8 +784,29 @@ export class AguaceroCore extends EventEmitter {
|
|
|
747
784
|
}
|
|
748
785
|
}
|
|
749
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
|
+
|
|
750
808
|
async switchMode(options) {
|
|
751
|
-
|
|
809
|
+
let { mode, variable, model, forecastHour, mrmsTimestamp, satelliteTimestamp } = options;
|
|
752
810
|
if (!mode) {
|
|
753
811
|
return;
|
|
754
812
|
}
|
|
@@ -758,18 +816,23 @@ export class AguaceroCore extends EventEmitter {
|
|
|
758
816
|
if ((mode === 'mrms' || mode === 'model') && !variable) {
|
|
759
817
|
return;
|
|
760
818
|
}
|
|
761
|
-
if (mode === 'satellite' && !satelliteKey) {
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
819
|
let targetState = {};
|
|
765
820
|
if (mode === 'satellite') {
|
|
766
|
-
const
|
|
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;
|
|
767
828
|
// Emit satellite mode immediately so map layers (e.g. model grid) clear before listing fetch finishes.
|
|
768
829
|
await this.setState({
|
|
769
830
|
isSatellite: true,
|
|
770
831
|
isMRMS: false,
|
|
771
832
|
isNexrad: false,
|
|
772
|
-
|
|
833
|
+
satelliteInstrumentId,
|
|
834
|
+
satelliteSectorLabel,
|
|
835
|
+
satelliteChannel,
|
|
773
836
|
variable: channelToken,
|
|
774
837
|
satelliteTimestamp: null,
|
|
775
838
|
mrmsTimestamp: null,
|
|
@@ -780,19 +843,14 @@ export class AguaceroCore extends EventEmitter {
|
|
|
780
843
|
|
|
781
844
|
await this.fetchSatelliteListing(true);
|
|
782
845
|
const allFiles = this.satelliteListing?.objects?.map((o) => o.key) || [];
|
|
783
|
-
const sectorName =
|
|
784
|
-
let sectorType = 'CONUS';
|
|
785
|
-
if (sectorName.includes('FULL DISK')) sectorType = 'FULL_DISK';
|
|
786
|
-
else if (sectorName.includes('MESOSCALE')) sectorType = 'MESOSCALE';
|
|
846
|
+
const sectorName = satelliteSectorLabel;
|
|
787
847
|
const tier = this.state.satelliteTier || 'basic';
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
tierConfig[0];
|
|
795
|
-
const timeline = buildSatelliteTimelineForKey(satelliteKey, allFiles, durationOpt);
|
|
848
|
+
const durationOpt = resolveSatelliteDurationOption(sectorName, tier, this.state.satelliteDurationValue);
|
|
849
|
+
const timeline = buildSatelliteTimelineForSelection(
|
|
850
|
+
{ satelliteInstrumentId, satelliteSectorLabel, satelliteChannel },
|
|
851
|
+
allFiles,
|
|
852
|
+
durationOpt,
|
|
853
|
+
);
|
|
796
854
|
const tsList = [...(timeline.unixTimes || [])].sort((a, b) => a - b);
|
|
797
855
|
let finalTs = satelliteTimestamp !== undefined ? satelliteTimestamp : null;
|
|
798
856
|
if (finalTs == null && tsList.length > 0) {
|
|
@@ -821,7 +879,9 @@ export class AguaceroCore extends EventEmitter {
|
|
|
821
879
|
variable: variable,
|
|
822
880
|
mrmsTimestamp: finalTimestamp,
|
|
823
881
|
model: this.state.model, date: null, run: null, forecastHour: 0,
|
|
824
|
-
|
|
882
|
+
satelliteInstrumentId: null,
|
|
883
|
+
satelliteSectorLabel: null,
|
|
884
|
+
satelliteChannel: null,
|
|
825
885
|
satelliteTimestamp: null,
|
|
826
886
|
};
|
|
827
887
|
} else if (mode === 'model') {
|
|
@@ -855,12 +915,19 @@ export class AguaceroCore extends EventEmitter {
|
|
|
855
915
|
run: latestRun.run,
|
|
856
916
|
forecastHour: initialHour, // <-- Changed
|
|
857
917
|
mrmsTimestamp: null,
|
|
858
|
-
|
|
918
|
+
satelliteInstrumentId: null,
|
|
919
|
+
satelliteSectorLabel: null,
|
|
920
|
+
satelliteChannel: null,
|
|
859
921
|
satelliteTimestamp: null,
|
|
860
922
|
};
|
|
861
923
|
} else if (mode === 'nexrad') {
|
|
862
|
-
const nexradDataSource = options.nexradDataSource || 'level2';
|
|
863
924
|
const nexradProduct = options.nexradProduct || 'REF';
|
|
925
|
+
const nexradDataSource =
|
|
926
|
+
options.nexradDataSource != null
|
|
927
|
+
? options.nexradDataSource === 'level3'
|
|
928
|
+
? 'level3'
|
|
929
|
+
: 'level2'
|
|
930
|
+
: inferNexradDataSourceForProduct(nexradProduct);
|
|
864
931
|
const fld = nexradColormapFldKey(nexradDataSource, nexradProduct);
|
|
865
932
|
const site = options.nexradSite ?? null;
|
|
866
933
|
let tilt =
|
|
@@ -881,8 +948,13 @@ export class AguaceroCore extends EventEmitter {
|
|
|
881
948
|
nexradTimestamp: options.nexradTimestamp != null ? Number(options.nexradTimestamp) : null,
|
|
882
949
|
nexradStormRelative: options.nexradStormRelative === true,
|
|
883
950
|
nexradShowSitesPicker: options.nexradShowSitesPicker !== false,
|
|
951
|
+
...(options.nexradDurationValue != null
|
|
952
|
+
? { nexradDurationValue: formatTimelineDurationValue(options.nexradDurationValue) }
|
|
953
|
+
: {}),
|
|
884
954
|
mrmsTimestamp: null,
|
|
885
|
-
|
|
955
|
+
satelliteInstrumentId: null,
|
|
956
|
+
satelliteSectorLabel: null,
|
|
957
|
+
satelliteChannel: null,
|
|
886
958
|
satelliteTimestamp: null,
|
|
887
959
|
date: null,
|
|
888
960
|
run: null,
|
|
@@ -995,10 +1067,10 @@ export class AguaceroCore extends EventEmitter {
|
|
|
995
1067
|
async setNexradProduct(product) {
|
|
996
1068
|
if (!this.state.isNexrad) return;
|
|
997
1069
|
const p = (product || 'REF').toUpperCase();
|
|
998
|
-
const ds =
|
|
1070
|
+
const ds = inferNexradDataSourceForProduct(p);
|
|
999
1071
|
const fld = nexradColormapFldKey(ds, p);
|
|
1000
1072
|
const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
|
|
1001
|
-
await this.setState({ nexradProduct: p, variable: fld, nexradStormRelative });
|
|
1073
|
+
await this.setState({ nexradProduct: p, nexradDataSource: ds, variable: fld, nexradStormRelative });
|
|
1002
1074
|
await this.refreshNexradTimes();
|
|
1003
1075
|
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
1004
1076
|
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
@@ -1025,8 +1097,9 @@ export class AguaceroCore extends EventEmitter {
|
|
|
1025
1097
|
}
|
|
1026
1098
|
|
|
1027
1099
|
/**
|
|
1028
|
-
* Sets NEXRAD product and archive source together (single refresh).
|
|
1029
|
-
*
|
|
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).
|
|
1030
1103
|
* @param {'level2'|'level3'} dataSource
|
|
1031
1104
|
* @param {string} product - Level-II variable (REF, VEL, …) or Level-III radar key (N0H, HHC, …)
|
|
1032
1105
|
*/
|
package/src/index.js
CHANGED
|
@@ -13,14 +13,18 @@ export {
|
|
|
13
13
|
GOES_SATELLITE_CHANNELS,
|
|
14
14
|
GOES_SATELLITE_CHANNEL_LABELS,
|
|
15
15
|
SATELLITE_DURATION_CONFIG,
|
|
16
|
+
TIMELINE_DURATION_MAX_HOURS,
|
|
16
17
|
TIMELINE_DURATION_HOUR_VALUES,
|
|
18
|
+
formatTimelineDurationValue,
|
|
17
19
|
normalizeTimelineDurationValue,
|
|
18
20
|
parseTimelineDurationHours,
|
|
21
|
+
resolveSatelliteDurationOption,
|
|
19
22
|
getDefaultSatelliteDurationOption,
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
buildSatelliteTimelineForSelection,
|
|
24
|
+
getAllGoesEastSatelliteSelections,
|
|
22
25
|
calculateUnixTimeFromSatelliteKey,
|
|
23
26
|
resolveSatelliteS3FileName,
|
|
27
|
+
resolveSatelliteSectorLabel,
|
|
24
28
|
} from './satellite_support.js';
|
|
25
29
|
|
|
26
30
|
// Now, export them all so other packages can import them.
|
package/src/nexrad_support.js
CHANGED
|
@@ -14,6 +14,17 @@ import {
|
|
|
14
14
|
getNexradLevel3EntryByRadarKey,
|
|
15
15
|
} from './nexrad_level3_catalog.js';
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Whether listings and archive fetches for this product use Level-II sweep lambda vs Level-III S3 products.
|
|
19
|
+
* AguaceroCore applies this from the product key; set `nexradDataSource` in layer options only to override (rare).
|
|
20
|
+
* @param {string} [nexradProduct] - Radar variable key (e.g. REF, KDP, VEL).
|
|
21
|
+
* @returns {'level2'|'level3'}
|
|
22
|
+
*/
|
|
23
|
+
export function inferNexradDataSourceForProduct(nexradProduct) {
|
|
24
|
+
const p = (nexradProduct || 'REF').toUpperCase();
|
|
25
|
+
return getNexradLevel3EntryByRadarKey(p) ? 'level3' : 'level2';
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
/** Colormap / DICTIONARIES.fld key for customColormaps (matches frontend userDefaultColormap). */
|
|
18
29
|
export function nexradColormapFldKey(nexradDataSource, nexradProduct) {
|
|
19
30
|
const p = (nexradProduct || 'REF').toUpperCase();
|
package/src/satellite_support.js
CHANGED
|
@@ -13,6 +13,33 @@ export const GOES_EAST_SATELLITE_SECTORS = [
|
|
|
13
13
|
'GOES-EAST MESOSCALE 2',
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
+
const SECTOR_SYNONYM_TO_LABEL = {
|
|
17
|
+
conus: 'GOES-EAST CONUS',
|
|
18
|
+
full_disk: 'GOES-EAST FULL DISK',
|
|
19
|
+
fulldisk: 'GOES-EAST FULL DISK',
|
|
20
|
+
mesoscale_1: 'GOES-EAST MESOSCALE 1',
|
|
21
|
+
mesoscale1: 'GOES-EAST MESOSCALE 1',
|
|
22
|
+
m1: 'GOES-EAST MESOSCALE 1',
|
|
23
|
+
mesoscale_2: 'GOES-EAST MESOSCALE 2',
|
|
24
|
+
mesoscale2: 'GOES-EAST MESOSCALE 2',
|
|
25
|
+
m2: 'GOES-EAST MESOSCALE 2',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalizes a sector argument to the listing sector label (middle segment; must match archive paths).
|
|
30
|
+
* Accepts short tokens (<code>conus</code>, <code>full_disk</code>, …), synonyms (<code>m1</code>, <code>fulldisk</code>, …), or full labels (passthrough for West / custom feeds).
|
|
31
|
+
* @param {string} [sector]
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function resolveSatelliteSectorLabel(sector) {
|
|
35
|
+
if (sector == null || sector === '') return 'GOES-EAST CONUS';
|
|
36
|
+
const raw = String(sector).trim();
|
|
37
|
+
const norm = raw.toLowerCase().replace(/[\s-]+/g, '_');
|
|
38
|
+
if (SECTOR_SYNONYM_TO_LABEL[norm]) return SECTOR_SYNONYM_TO_LABEL[norm];
|
|
39
|
+
if (GOES_EAST_SATELLITE_SECTORS.includes(raw)) return raw;
|
|
40
|
+
return raw;
|
|
41
|
+
}
|
|
42
|
+
|
|
16
43
|
/** Flat channel ids matching CHANNEL_CATEGORIES in frontend dictionaries. */
|
|
17
44
|
export const GOES_SATELLITE_CHANNELS = [
|
|
18
45
|
...['true_color', 'geocolor', 'ntmicro', 'day_cloud_phase', 'day_land_cloud_fire', 'air_mass', 'sandwich', 'simple_water_vapor', 'dust', 'fire_temperature'],
|
|
@@ -75,8 +102,12 @@ const SATELLITE_RGB_PRODUCT_UPPER = new Set([
|
|
|
75
102
|
]);
|
|
76
103
|
|
|
77
104
|
/**
|
|
78
|
-
*
|
|
79
|
-
|
|
105
|
+
* Maximum timeline window length in hours for satellite, MRMS, and NEXRAD. Values are clamped to (0, this].
|
|
106
|
+
*/
|
|
107
|
+
export const TIMELINE_DURATION_MAX_HOURS = 12;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Preset hour buttons (UI). Any positive duration ≤ {@link TIMELINE_DURATION_MAX_HOURS} is valid via {@link formatTimelineDurationValue}.
|
|
80
111
|
*/
|
|
81
112
|
export const TIMELINE_DURATION_HOUR_VALUES = ['1', '4', '6', '12'];
|
|
82
113
|
|
|
@@ -113,24 +144,54 @@ export const SATELLITE_DURATION_CONFIG = {
|
|
|
113
144
|
};
|
|
114
145
|
|
|
115
146
|
/**
|
|
116
|
-
*
|
|
147
|
+
* Parses a user duration to hours, then clamps to a positive value not exceeding {@link TIMELINE_DURATION_MAX_HOURS}.
|
|
148
|
+
* @param {string|number} value
|
|
149
|
+
* @returns {number}
|
|
150
|
+
*/
|
|
151
|
+
export function parseTimelineDurationHours(value) {
|
|
152
|
+
let s = value == null ? '1' : String(value).trim();
|
|
153
|
+
s = LEGACY_SATELLITE_DURATION_ALIASES[s] || s;
|
|
154
|
+
const n = Number(s);
|
|
155
|
+
if (!Number.isFinite(n) || n <= 0) return 1;
|
|
156
|
+
if (n > TIMELINE_DURATION_MAX_HOURS) return TIMELINE_DURATION_MAX_HOURS;
|
|
157
|
+
return n;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Canonical string for AguaceroCore state (stable for equality checks). Supports fractional hours (e.g. <code>'2.5'</code>).
|
|
117
162
|
* @param {string|number} value
|
|
118
163
|
* @returns {string}
|
|
119
164
|
*/
|
|
165
|
+
export function formatTimelineDurationValue(value) {
|
|
166
|
+
const n = parseTimelineDurationHours(value);
|
|
167
|
+
if (Math.abs(n - Math.round(n)) < 1e-6) return String(Math.round(n));
|
|
168
|
+
return String(Math.round(n * 10000) / 10000);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @returns {string} Same as {@link formatTimelineDurationValue} (backward-compatible name). */
|
|
120
172
|
export function normalizeTimelineDurationValue(value) {
|
|
121
|
-
|
|
122
|
-
return LEGACY_SATELLITE_DURATION_ALIASES[s] || s;
|
|
173
|
+
return formatTimelineDurationValue(value);
|
|
123
174
|
}
|
|
124
175
|
|
|
125
176
|
/**
|
|
126
|
-
*
|
|
127
|
-
* @
|
|
177
|
+
* Picks a preset GOES duration option when one matches; otherwise builds a custom option using the same cadence as the 1 hr preset for that sector/tier.
|
|
178
|
+
* @param {string} sectorName - e.g. <code>GOES-EAST CONUS</code>
|
|
179
|
+
* @param {string} [tier='basic']
|
|
180
|
+
* @param {string|number} durationValue
|
|
181
|
+
* @returns {{ label: string, value: string, interval: number }}
|
|
128
182
|
*/
|
|
129
|
-
export function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
183
|
+
export function resolveSatelliteDurationOption(sectorName, tier, durationValue) {
|
|
184
|
+
let sectorType = 'CONUS';
|
|
185
|
+
if (sectorName.includes('FULL DISK')) sectorType = 'FULL_DISK';
|
|
186
|
+
else if (sectorName.includes('MESOSCALE')) sectorType = 'MESOSCALE';
|
|
187
|
+
const t = tier || 'basic';
|
|
188
|
+
const tierConfig =
|
|
189
|
+
SATELLITE_DURATION_CONFIG[sectorType]?.[t] || SATELLITE_DURATION_CONFIG.CONUS.basic;
|
|
190
|
+
const key = formatTimelineDurationValue(durationValue);
|
|
191
|
+
const preset = tierConfig.find((o) => String(o.value) === String(key));
|
|
192
|
+
if (preset) return preset;
|
|
193
|
+
const interval = tierConfig.find((o) => o.value === '1')?.interval || 300;
|
|
194
|
+
return { label: `${key} Hr`, value: key, interval };
|
|
134
195
|
}
|
|
135
196
|
|
|
136
197
|
export function getDefaultSatelliteDurationOption(sectorName, tier = 'basic') {
|
|
@@ -160,16 +221,17 @@ export function calculateUnixTimeFromSatelliteKey(fileKey) {
|
|
|
160
221
|
}
|
|
161
222
|
|
|
162
223
|
/**
|
|
163
|
-
* @param {string
|
|
224
|
+
* @param {{ satelliteInstrumentId: string, satelliteSectorLabel: string, satelliteChannel: string }} selection
|
|
164
225
|
* @param {string[]} allFiles S3 keys from satellite listing API
|
|
165
226
|
* @param {{ value: string, interval: number }} durationOption
|
|
166
227
|
*/
|
|
167
|
-
export function
|
|
168
|
-
const
|
|
169
|
-
|
|
228
|
+
export function buildSatelliteTimelineForSelection(selection, allFiles, durationOption) {
|
|
229
|
+
const satelliteName = selection?.satelliteInstrumentId;
|
|
230
|
+
const categoryName = selection?.satelliteSectorLabel;
|
|
231
|
+
const channelName = selection?.satelliteChannel;
|
|
232
|
+
if (!satelliteName || !categoryName || !channelName) {
|
|
170
233
|
return { unixTimes: [], fileList: [], timeToFileMap: {} };
|
|
171
234
|
}
|
|
172
|
-
const [satelliteName, categoryName, channelName] = parts;
|
|
173
235
|
|
|
174
236
|
const SECTOR_CODE_MAP_LOCAL = {
|
|
175
237
|
'GOES-EAST CONUS': 'C',
|
|
@@ -274,23 +336,32 @@ export function buildSatelliteTimelineForKey(satelliteKey, allFiles, durationOpt
|
|
|
274
336
|
}
|
|
275
337
|
|
|
276
338
|
/**
|
|
277
|
-
*
|
|
339
|
+
* All GOES-East spacecraft / sector / channel combinations the SDK knows (same naming as production).
|
|
340
|
+
* @param {string} [satelliteInstrumentId='GOES19-EAST']
|
|
341
|
+
* @returns {Array<{ satelliteInstrumentId: string, satelliteSectorLabel: string, satelliteChannel: string }>}
|
|
278
342
|
*/
|
|
279
|
-
export function
|
|
280
|
-
const
|
|
343
|
+
export function getAllGoesEastSatelliteSelections(satelliteInstrumentId = 'GOES19-EAST') {
|
|
344
|
+
const out = [];
|
|
281
345
|
for (const sector of GOES_EAST_SATELLITE_SECTORS) {
|
|
282
346
|
for (const ch of GOES_SATELLITE_CHANNELS) {
|
|
283
|
-
|
|
347
|
+
out.push({
|
|
348
|
+
satelliteInstrumentId,
|
|
349
|
+
satelliteSectorLabel: sector,
|
|
350
|
+
satelliteChannel: ch,
|
|
351
|
+
});
|
|
284
352
|
}
|
|
285
353
|
}
|
|
286
|
-
return
|
|
354
|
+
return out;
|
|
287
355
|
}
|
|
288
356
|
|
|
289
|
-
|
|
357
|
+
/**
|
|
358
|
+
* @param {string} satelliteChannel — band or RGB id (third segment of the archive descriptor)
|
|
359
|
+
*/
|
|
360
|
+
export function resolveSatelliteS3FileName(satelliteChannel, timeToFileMap, satelliteTimestamp) {
|
|
290
361
|
const fileFromMap = timeToFileMap?.[satelliteTimestamp];
|
|
291
362
|
if (fileFromMap) return fileFromMap;
|
|
292
|
-
const
|
|
293
|
-
const channelNameUpper = channelName.toUpperCase();
|
|
363
|
+
const channelName = satelliteChannel;
|
|
364
|
+
const channelNameUpper = String(channelName).toUpperCase();
|
|
294
365
|
let name = fileFromMap || '';
|
|
295
366
|
if (!name && satelliteTimestamp != null) {
|
|
296
367
|
const suffix = `_${String(Math.floor(Number(satelliteTimestamp)))}`;
|