@aguacerowx/javascript-sdk 0.0.20 → 0.0.22
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 +6 -1
- package/src/AguaceroCore.js +185 -143
- package/src/index.js +7 -2
- package/src/nexradTiltCoalesce.js +95 -0
- package/src/nexrad_support.js +31 -2
- package/src/satellite_support.js +96 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aguacerowx/javascript-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
"import": "./src/nexrad_support.js",
|
|
26
26
|
"require": "./src/nexrad_support.js",
|
|
27
27
|
"default": "./src/nexrad_support.js"
|
|
28
|
+
},
|
|
29
|
+
"./nexradTiltCoalesce.js": {
|
|
30
|
+
"import": "./src/nexradTiltCoalesce.js",
|
|
31
|
+
"require": "./src/nexradTiltCoalesce.js",
|
|
32
|
+
"default": "./src/nexradTiltCoalesce.js"
|
|
28
33
|
}
|
|
29
34
|
},
|
|
30
35
|
"files": [
|
package/src/AguaceroCore.js
CHANGED
|
@@ -11,18 +11,21 @@ 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,
|
|
26
|
+
getRawNexradTiltsForCoalesce,
|
|
25
27
|
} from './nexrad_support.js';
|
|
28
|
+
import { nexradLayerTiltToDisplayOption } from './nexradTiltCoalesce.js';
|
|
26
29
|
import { NEXRAD_LEVEL3_ELEV, getNexradLevel3EntryByRadarKey } from './nexrad_level3_catalog.js';
|
|
27
30
|
import {
|
|
28
31
|
setRadarTiltsManifest,
|
|
@@ -140,17 +143,35 @@ export class AguaceroCore extends EventEmitter {
|
|
|
140
143
|
|
|
141
144
|
const initialSatellite = initialMode === 'satellite';
|
|
142
145
|
const initialNexrad = initialMode === 'nexrad';
|
|
143
|
-
const initialNexradDs = userLayerOptions.nexradDataSource || 'level2';
|
|
144
146
|
const initialNexradProd = userLayerOptions.nexradProduct || 'REF';
|
|
147
|
+
const initialNexradDs = inferNexradDataSourceForProduct(initialNexradProd);
|
|
145
148
|
const initialNexradFld = initialNexrad
|
|
146
149
|
? nexradColormapFldKey(initialNexradDs, initialNexradProd)
|
|
147
150
|
: initialVariable;
|
|
151
|
+
let initialSatelliteInstrumentId = null;
|
|
152
|
+
let initialSatelliteSectorLabel = null;
|
|
153
|
+
let initialSatelliteChannel = null;
|
|
154
|
+
if (initialSatellite) {
|
|
155
|
+
initialSatelliteInstrumentId = userLayerOptions.satelliteId ?? 'GOES19-EAST';
|
|
156
|
+
initialSatelliteSectorLabel = resolveSatelliteSectorLabel(
|
|
157
|
+
userLayerOptions.satelliteSector ?? userLayerOptions.sector ?? 'conus',
|
|
158
|
+
);
|
|
159
|
+
initialSatelliteChannel =
|
|
160
|
+
userLayerOptions.satelliteProduct ??
|
|
161
|
+
userLayerOptions.satelliteChannel ??
|
|
162
|
+
initialVariable ??
|
|
163
|
+
'C13';
|
|
164
|
+
}
|
|
148
165
|
this.state = {
|
|
149
166
|
model: userLayerOptions.model || 'gfs',
|
|
150
167
|
// EDIT: Set isMRMS based on the initial mode
|
|
151
168
|
isMRMS: initialMode === 'mrms' && !initialSatellite && !initialNexrad,
|
|
152
169
|
mrmsTimestamp: null,
|
|
153
|
-
variable: initialNexrad
|
|
170
|
+
variable: initialNexrad
|
|
171
|
+
? initialNexradFld
|
|
172
|
+
: initialSatellite && initialSatelliteInstrumentId
|
|
173
|
+
? initialSatelliteChannel
|
|
174
|
+
: initialVariable,
|
|
154
175
|
date: null,
|
|
155
176
|
run: null,
|
|
156
177
|
forecastHour: 0,
|
|
@@ -159,21 +180,20 @@ export class AguaceroCore extends EventEmitter {
|
|
|
159
180
|
units: options.initialUnit || 'imperial',
|
|
160
181
|
shaderSmoothingEnabled: options.shaderSmoothingEnabled ?? true,
|
|
161
182
|
isSatellite: initialSatellite,
|
|
162
|
-
|
|
183
|
+
satelliteInstrumentId: initialSatelliteInstrumentId,
|
|
184
|
+
satelliteSectorLabel: initialSatelliteSectorLabel,
|
|
185
|
+
satelliteChannel: initialSatelliteChannel,
|
|
163
186
|
satelliteTimestamp: userLayerOptions.satelliteTimestamp != null ? Number(userLayerOptions.satelliteTimestamp) : null,
|
|
164
187
|
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
|
-
})(),
|
|
188
|
+
satelliteDurationValue: formatTimelineDurationValue(
|
|
189
|
+
userLayerOptions.satelliteDurationValue != null ? userLayerOptions.satelliteDurationValue : '1',
|
|
190
|
+
),
|
|
191
|
+
mrmsDurationValue: formatTimelineDurationValue(
|
|
192
|
+
userLayerOptions.mrmsDurationValue != null ? userLayerOptions.mrmsDurationValue : '1',
|
|
193
|
+
),
|
|
194
|
+
nexradDurationValue: formatTimelineDurationValue(
|
|
195
|
+
userLayerOptions.nexradDurationValue != null ? userLayerOptions.nexradDurationValue : '1',
|
|
196
|
+
),
|
|
177
197
|
isNexrad: initialNexrad,
|
|
178
198
|
nexradSite: userLayerOptions.nexradSite ?? null,
|
|
179
199
|
nexradDataSource: initialNexradDs,
|
|
@@ -200,6 +220,20 @@ export class AguaceroCore extends EventEmitter {
|
|
|
200
220
|
|
|
201
221
|
async setState(newState) {
|
|
202
222
|
const patch = { ...newState };
|
|
223
|
+
if ('satelliteKey' in patch) delete patch.satelliteKey;
|
|
224
|
+
if ('nexradDataSource' in patch && !('nexradProduct' in patch)) {
|
|
225
|
+
delete patch.nexradDataSource;
|
|
226
|
+
}
|
|
227
|
+
const willBeNexrad =
|
|
228
|
+
patch.isNexrad !== undefined ? Boolean(patch.isNexrad) : this.state.isNexrad;
|
|
229
|
+
if (willBeNexrad && 'nexradProduct' in patch && patch.nexradProduct != null) {
|
|
230
|
+
const p = (patch.nexradProduct || 'REF').toUpperCase();
|
|
231
|
+
const ds = inferNexradDataSourceForProduct(p);
|
|
232
|
+
patch.nexradProduct = p;
|
|
233
|
+
patch.nexradDataSource = ds;
|
|
234
|
+
patch.variable = nexradColormapFldKey(ds, p);
|
|
235
|
+
patch.nexradStormRelative = this._nexradStormRelativeFor(ds, p);
|
|
236
|
+
}
|
|
203
237
|
if ('forecastHour' in patch && patch.forecastHour != null) {
|
|
204
238
|
patch.forecastHour = Number(patch.forecastHour);
|
|
205
239
|
}
|
|
@@ -210,12 +244,13 @@ export class AguaceroCore extends EventEmitter {
|
|
|
210
244
|
patch.satelliteTimestamp = Number(patch.satelliteTimestamp);
|
|
211
245
|
}
|
|
212
246
|
if ('satelliteDurationValue' in patch && patch.satelliteDurationValue != null) {
|
|
213
|
-
|
|
214
|
-
patch.satelliteDurationValue = TIMELINE_DURATION_HOUR_VALUES.includes(n) ? n : '1';
|
|
247
|
+
patch.satelliteDurationValue = formatTimelineDurationValue(patch.satelliteDurationValue);
|
|
215
248
|
}
|
|
216
249
|
if ('mrmsDurationValue' in patch && patch.mrmsDurationValue != null) {
|
|
217
|
-
|
|
218
|
-
|
|
250
|
+
patch.mrmsDurationValue = formatTimelineDurationValue(patch.mrmsDurationValue);
|
|
251
|
+
}
|
|
252
|
+
if ('nexradDurationValue' in patch && patch.nexradDurationValue != null) {
|
|
253
|
+
patch.nexradDurationValue = formatTimelineDurationValue(patch.nexradDurationValue);
|
|
219
254
|
}
|
|
220
255
|
if ('nexradTimestamp' in patch && patch.nexradTimestamp != null) {
|
|
221
256
|
patch.nexradTimestamp = Number(patch.nexradTimestamp);
|
|
@@ -253,23 +288,23 @@ export class AguaceroCore extends EventEmitter {
|
|
|
253
288
|
}
|
|
254
289
|
|
|
255
290
|
_computeSatelliteTimeline() {
|
|
256
|
-
|
|
291
|
+
const { satelliteInstrumentId, satelliteSectorLabel, satelliteChannel } = this.state;
|
|
292
|
+
if (!satelliteInstrumentId || !satelliteSectorLabel || !satelliteChannel || !this.satelliteListing?.objects) {
|
|
257
293
|
return { unixTimes: [], timeToFileMap: {} };
|
|
258
294
|
}
|
|
259
295
|
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';
|
|
296
|
+
const sectorName = satelliteSectorLabel;
|
|
264
297
|
const tier = this.state.satelliteTier || 'basic';
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
298
|
+
const durationOpt = resolveSatelliteDurationOption(sectorName, tier, this.state.satelliteDurationValue);
|
|
299
|
+
return buildSatelliteTimelineForSelection(
|
|
300
|
+
{
|
|
301
|
+
satelliteInstrumentId,
|
|
302
|
+
satelliteSectorLabel,
|
|
303
|
+
satelliteChannel,
|
|
304
|
+
},
|
|
305
|
+
allFiles,
|
|
306
|
+
durationOpt,
|
|
307
|
+
);
|
|
273
308
|
}
|
|
274
309
|
|
|
275
310
|
/**
|
|
@@ -295,7 +330,7 @@ export class AguaceroCore extends EventEmitter {
|
|
|
295
330
|
}
|
|
296
331
|
|
|
297
332
|
_nexradListingWindowHours() {
|
|
298
|
-
return parseTimelineDurationHours(this.state.
|
|
333
|
+
return parseTimelineDurationHours(this.state.nexradDurationValue);
|
|
299
334
|
}
|
|
300
335
|
|
|
301
336
|
_getFilteredNexradTimestampsForVariable(rawList) {
|
|
@@ -357,7 +392,7 @@ export class AguaceroCore extends EventEmitter {
|
|
|
357
392
|
|
|
358
393
|
let availableSatelliteTimestamps = [];
|
|
359
394
|
let satelliteTimeToFileMap = {};
|
|
360
|
-
if (this.state.isSatellite && this.state.
|
|
395
|
+
if (this.state.isSatellite && this.state.satelliteInstrumentId) {
|
|
361
396
|
const timeline = this._computeSatelliteTimeline();
|
|
362
397
|
satelliteTimeToFileMap = timeline.timeToFileMap || {};
|
|
363
398
|
availableSatelliteTimestamps = [...(timeline.unixTimes || [])]
|
|
@@ -416,7 +451,7 @@ export class AguaceroCore extends EventEmitter {
|
|
|
416
451
|
|
|
417
452
|
let initialState = { ...this.state };
|
|
418
453
|
|
|
419
|
-
if (initialState.isSatellite && initialState.
|
|
454
|
+
if (initialState.isSatellite && initialState.satelliteInstrumentId) {
|
|
420
455
|
const timeline = this._computeSatelliteTimeline();
|
|
421
456
|
const tsList = [...(timeline.unixTimes || [])].sort((a, b) => a - b);
|
|
422
457
|
if (initialState.satelliteTimestamp == null && tsList.length > 0) {
|
|
@@ -625,7 +660,13 @@ export class AguaceroCore extends EventEmitter {
|
|
|
625
660
|
async setModel(modelName) {
|
|
626
661
|
if (modelName === this.state.model || !this.modelStatus?.[modelName]) return;
|
|
627
662
|
if (this.state.isSatellite) {
|
|
628
|
-
await this.setState({
|
|
663
|
+
await this.setState({
|
|
664
|
+
isSatellite: false,
|
|
665
|
+
satelliteInstrumentId: null,
|
|
666
|
+
satelliteSectorLabel: null,
|
|
667
|
+
satelliteChannel: null,
|
|
668
|
+
satelliteTimestamp: null,
|
|
669
|
+
});
|
|
629
670
|
}
|
|
630
671
|
const latestRun = findLatestModelRun(this.modelStatus, modelName);
|
|
631
672
|
if (latestRun) {
|
|
@@ -674,7 +715,9 @@ export class AguaceroCore extends EventEmitter {
|
|
|
674
715
|
variable,
|
|
675
716
|
isMRMS: true,
|
|
676
717
|
isSatellite: false,
|
|
677
|
-
|
|
718
|
+
satelliteInstrumentId: null,
|
|
719
|
+
satelliteSectorLabel: null,
|
|
720
|
+
satelliteChannel: null,
|
|
678
721
|
satelliteTimestamp: null,
|
|
679
722
|
mrmsTimestamp: initialTimestamp,
|
|
680
723
|
});
|
|
@@ -691,14 +734,13 @@ export class AguaceroCore extends EventEmitter {
|
|
|
691
734
|
}
|
|
692
735
|
|
|
693
736
|
/**
|
|
694
|
-
* How many hours of satellite frames to include in the timeline (
|
|
737
|
+
* How many hours of satellite frames to include in the timeline (positive, at most 12 hours).
|
|
695
738
|
* API default: `layerOptions.satelliteDurationValue` on construction.
|
|
696
739
|
*/
|
|
697
740
|
async setSatelliteDurationValue(value) {
|
|
698
|
-
const v =
|
|
699
|
-
if (!TIMELINE_DURATION_HOUR_VALUES.includes(v)) return;
|
|
741
|
+
const v = formatTimelineDurationValue(value);
|
|
700
742
|
await this.setState({ satelliteDurationValue: v });
|
|
701
|
-
if (!this.state.isSatellite || !this.state.
|
|
743
|
+
if (!this.state.isSatellite || !this.state.satelliteInstrumentId) return;
|
|
702
744
|
const timeline = this._computeSatelliteTimeline();
|
|
703
745
|
const tsList = [...(timeline.unixTimes || [])]
|
|
704
746
|
.map((t) => Number(t))
|
|
@@ -713,30 +755,35 @@ export class AguaceroCore extends EventEmitter {
|
|
|
713
755
|
}
|
|
714
756
|
|
|
715
757
|
/**
|
|
716
|
-
*
|
|
758
|
+
* Set satellite view using spacecraft id, sector, and channel/product.
|
|
759
|
+
* Omitted fields keep the current selection; when not in satellite mode, missing fields use GOES-19 East CONUS / C13 defaults.
|
|
760
|
+
* @param {{ satelliteId?: string, sector?: string, satelliteSector?: string, satelliteProduct?: string, satelliteChannel?: string, satelliteTimestamp?: number|null }} opts
|
|
761
|
+
*/
|
|
762
|
+
async setSatelliteSelection(opts = {}) {
|
|
763
|
+
const cur = this.state.isSatellite ? this.state : null;
|
|
764
|
+
const tsArg =
|
|
765
|
+
opts.satelliteTimestamp !== undefined
|
|
766
|
+
? opts.satelliteTimestamp
|
|
767
|
+
: this.state.isSatellite && this.state.satelliteTimestamp != null
|
|
768
|
+
? Number(this.state.satelliteTimestamp)
|
|
769
|
+
: undefined;
|
|
770
|
+
return this.switchMode({
|
|
771
|
+
mode: 'satellite',
|
|
772
|
+
satelliteId: opts.satelliteId ?? cur?.satelliteInstrumentId ?? 'GOES19-EAST',
|
|
773
|
+
satelliteSector: opts.satelliteSector ?? opts.sector ?? cur?.satelliteSectorLabel ?? 'conus',
|
|
774
|
+
satelliteProduct:
|
|
775
|
+
opts.satelliteProduct ?? opts.satelliteChannel ?? cur?.satelliteChannel ?? 'C13',
|
|
776
|
+
satelliteTimestamp: tsArg,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* How many hours of MRMS frames to include in the timeline (positive, at most 12 hours).
|
|
717
782
|
* API default: `layerOptions.mrmsDurationValue` on construction.
|
|
718
783
|
*/
|
|
719
784
|
async setMRMSDurationValue(value) {
|
|
720
|
-
const v =
|
|
721
|
-
if (!TIMELINE_DURATION_HOUR_VALUES.includes(v)) return;
|
|
785
|
+
const v = formatTimelineDurationValue(value);
|
|
722
786
|
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
787
|
if (!this.state.isMRMS || !this.state.variable) return;
|
|
741
788
|
const filtered = this._getFilteredMrmsTimestampsForVariable(this.state.variable);
|
|
742
789
|
if (filtered.length === 0) return;
|
|
@@ -747,8 +794,29 @@ export class AguaceroCore extends EventEmitter {
|
|
|
747
794
|
}
|
|
748
795
|
}
|
|
749
796
|
|
|
797
|
+
/**
|
|
798
|
+
* NEXRAD sweep listing / scrub window in hours (independent of MRMS duration).
|
|
799
|
+
* API default: `layerOptions.nexradDurationValue` on construction.
|
|
800
|
+
*/
|
|
801
|
+
async setNexradDurationValue(value) {
|
|
802
|
+
const v = formatTimelineDurationValue(value);
|
|
803
|
+
await this.setState({ nexradDurationValue: v });
|
|
804
|
+
if (!this.state.isNexrad || !this.state.nexradSite) return;
|
|
805
|
+
await this.refreshNexradTimes();
|
|
806
|
+
const nk = this._nexradTimesCacheKey();
|
|
807
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
808
|
+
nk ? this.nexradTimesByStation[nk]?.unixTimes || [] : [],
|
|
809
|
+
);
|
|
810
|
+
if (filtered.length === 0) return;
|
|
811
|
+
const cur = this.state.nexradTimestamp;
|
|
812
|
+
const curN = cur == null ? null : Number(cur);
|
|
813
|
+
if (curN == null || !filtered.includes(curN)) {
|
|
814
|
+
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
750
818
|
async switchMode(options) {
|
|
751
|
-
|
|
819
|
+
let { mode, variable, model, forecastHour, mrmsTimestamp, satelliteTimestamp } = options;
|
|
752
820
|
if (!mode) {
|
|
753
821
|
return;
|
|
754
822
|
}
|
|
@@ -758,18 +826,23 @@ export class AguaceroCore extends EventEmitter {
|
|
|
758
826
|
if ((mode === 'mrms' || mode === 'model') && !variable) {
|
|
759
827
|
return;
|
|
760
828
|
}
|
|
761
|
-
if (mode === 'satellite' && !satelliteKey) {
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
829
|
let targetState = {};
|
|
765
830
|
if (mode === 'satellite') {
|
|
766
|
-
const
|
|
831
|
+
const satelliteInstrumentId = options.satelliteId ?? 'GOES19-EAST';
|
|
832
|
+
const satelliteSectorLabel = resolveSatelliteSectorLabel(
|
|
833
|
+
options.satelliteSector ?? options.sector ?? 'conus',
|
|
834
|
+
);
|
|
835
|
+
const satelliteChannel =
|
|
836
|
+
options.satelliteProduct ?? options.satelliteChannel ?? variable ?? 'C13';
|
|
837
|
+
const channelToken = satelliteChannel;
|
|
767
838
|
// Emit satellite mode immediately so map layers (e.g. model grid) clear before listing fetch finishes.
|
|
768
839
|
await this.setState({
|
|
769
840
|
isSatellite: true,
|
|
770
841
|
isMRMS: false,
|
|
771
842
|
isNexrad: false,
|
|
772
|
-
|
|
843
|
+
satelliteInstrumentId,
|
|
844
|
+
satelliteSectorLabel,
|
|
845
|
+
satelliteChannel,
|
|
773
846
|
variable: channelToken,
|
|
774
847
|
satelliteTimestamp: null,
|
|
775
848
|
mrmsTimestamp: null,
|
|
@@ -780,19 +853,14 @@ export class AguaceroCore extends EventEmitter {
|
|
|
780
853
|
|
|
781
854
|
await this.fetchSatelliteListing(true);
|
|
782
855
|
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';
|
|
856
|
+
const sectorName = satelliteSectorLabel;
|
|
787
857
|
const tier = this.state.satelliteTier || 'basic';
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
tierConfig[0];
|
|
795
|
-
const timeline = buildSatelliteTimelineForKey(satelliteKey, allFiles, durationOpt);
|
|
858
|
+
const durationOpt = resolveSatelliteDurationOption(sectorName, tier, this.state.satelliteDurationValue);
|
|
859
|
+
const timeline = buildSatelliteTimelineForSelection(
|
|
860
|
+
{ satelliteInstrumentId, satelliteSectorLabel, satelliteChannel },
|
|
861
|
+
allFiles,
|
|
862
|
+
durationOpt,
|
|
863
|
+
);
|
|
796
864
|
const tsList = [...(timeline.unixTimes || [])].sort((a, b) => a - b);
|
|
797
865
|
let finalTs = satelliteTimestamp !== undefined ? satelliteTimestamp : null;
|
|
798
866
|
if (finalTs == null && tsList.length > 0) {
|
|
@@ -821,7 +889,9 @@ export class AguaceroCore extends EventEmitter {
|
|
|
821
889
|
variable: variable,
|
|
822
890
|
mrmsTimestamp: finalTimestamp,
|
|
823
891
|
model: this.state.model, date: null, run: null, forecastHour: 0,
|
|
824
|
-
|
|
892
|
+
satelliteInstrumentId: null,
|
|
893
|
+
satelliteSectorLabel: null,
|
|
894
|
+
satelliteChannel: null,
|
|
825
895
|
satelliteTimestamp: null,
|
|
826
896
|
};
|
|
827
897
|
} else if (mode === 'model') {
|
|
@@ -855,13 +925,14 @@ export class AguaceroCore extends EventEmitter {
|
|
|
855
925
|
run: latestRun.run,
|
|
856
926
|
forecastHour: initialHour, // <-- Changed
|
|
857
927
|
mrmsTimestamp: null,
|
|
858
|
-
|
|
928
|
+
satelliteInstrumentId: null,
|
|
929
|
+
satelliteSectorLabel: null,
|
|
930
|
+
satelliteChannel: null,
|
|
859
931
|
satelliteTimestamp: null,
|
|
860
932
|
};
|
|
861
933
|
} else if (mode === 'nexrad') {
|
|
862
|
-
const nexradDataSource = options.nexradDataSource || 'level2';
|
|
863
934
|
const nexradProduct = options.nexradProduct || 'REF';
|
|
864
|
-
const
|
|
935
|
+
const p = nexradProduct.toUpperCase();
|
|
865
936
|
const site = options.nexradSite ?? null;
|
|
866
937
|
let tilt =
|
|
867
938
|
options.nexradTilt != null
|
|
@@ -873,16 +944,19 @@ export class AguaceroCore extends EventEmitter {
|
|
|
873
944
|
isNexrad: true,
|
|
874
945
|
isMRMS: false,
|
|
875
946
|
isSatellite: false,
|
|
876
|
-
variable: fld,
|
|
877
947
|
nexradSite: site,
|
|
878
|
-
|
|
879
|
-
nexradProduct,
|
|
948
|
+
nexradProduct: p,
|
|
880
949
|
nexradTilt: tilt,
|
|
881
950
|
nexradTimestamp: options.nexradTimestamp != null ? Number(options.nexradTimestamp) : null,
|
|
882
951
|
nexradStormRelative: options.nexradStormRelative === true,
|
|
883
952
|
nexradShowSitesPicker: options.nexradShowSitesPicker !== false,
|
|
953
|
+
...(options.nexradDurationValue != null
|
|
954
|
+
? { nexradDurationValue: formatTimelineDurationValue(options.nexradDurationValue) }
|
|
955
|
+
: {}),
|
|
884
956
|
mrmsTimestamp: null,
|
|
885
|
-
|
|
957
|
+
satelliteInstrumentId: null,
|
|
958
|
+
satelliteSectorLabel: null,
|
|
959
|
+
satelliteChannel: null,
|
|
886
960
|
satelliteTimestamp: null,
|
|
887
961
|
date: null,
|
|
888
962
|
run: null,
|
|
@@ -913,26 +987,23 @@ export class AguaceroCore extends EventEmitter {
|
|
|
913
987
|
}
|
|
914
988
|
|
|
915
989
|
/**
|
|
916
|
-
* Keep `nexradTilt` on
|
|
990
|
+
* Keep `nexradTilt` on a coalesced elevation (same rules as aguacero-frontend tilt controls).
|
|
917
991
|
*/
|
|
918
992
|
async _snapNexradTiltToAvailableOptions() {
|
|
919
993
|
const s = this.state;
|
|
920
994
|
if (!s.isNexrad || !s.nexradSite) return;
|
|
921
|
-
const
|
|
995
|
+
const raw = getRawNexradTiltsForCoalesce(
|
|
922
996
|
s.nexradSite,
|
|
923
997
|
s.nexradDataSource || 'level2',
|
|
924
998
|
s.nexradProduct || 'REF',
|
|
925
999
|
);
|
|
926
|
-
if (!
|
|
1000
|
+
if (!raw.length) return;
|
|
927
1001
|
const t = s.nexradTilt;
|
|
928
|
-
const match = (a, b) => Math.abs(Number(a) - Number(b)) < 1e-4;
|
|
929
|
-
if (t != null && tilts.some((x) => match(x, t))) return;
|
|
930
1002
|
const target = t != null && Number.isFinite(Number(t)) ? Number(t) : getDefaultRadarTilt(s.nexradSite);
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}
|
|
935
|
-
await this.setState({ nexradTilt: best });
|
|
1003
|
+
const canonical = nexradLayerTiltToDisplayOption(target, raw);
|
|
1004
|
+
const match = (a, b) => Math.abs(Number(a) - Number(b)) < 1e-4;
|
|
1005
|
+
if (match(s.nexradTilt, canonical)) return;
|
|
1006
|
+
await this.setState({ nexradTilt: canonical });
|
|
936
1007
|
}
|
|
937
1008
|
|
|
938
1009
|
async refreshNexradTimes() {
|
|
@@ -972,6 +1043,18 @@ export class AguaceroCore extends EventEmitter {
|
|
|
972
1043
|
listWindowHours: listingHours,
|
|
973
1044
|
};
|
|
974
1045
|
}
|
|
1046
|
+
const filtered = this._getFilteredNexradTimestampsForVariable(out.unixTimes || []);
|
|
1047
|
+
const map = out.timeToKeyMap || {};
|
|
1048
|
+
if (filtered.length > 0) {
|
|
1049
|
+
const cur = this.state.nexradTimestamp != null ? Number(this.state.nexradTimestamp) : null;
|
|
1050
|
+
const hasKey = cur != null && Object.prototype.hasOwnProperty.call(map, String(cur));
|
|
1051
|
+
const inWindow = cur != null && filtered.includes(cur);
|
|
1052
|
+
if (cur == null || !inWindow || !hasKey) {
|
|
1053
|
+
this.state.nexradTimestamp = filtered[filtered.length - 1];
|
|
1054
|
+
}
|
|
1055
|
+
} else if (this.state.nexradTimestamp != null) {
|
|
1056
|
+
this.state.nexradTimestamp = null;
|
|
1057
|
+
}
|
|
975
1058
|
this._emitStateChange();
|
|
976
1059
|
}
|
|
977
1060
|
|
|
@@ -995,48 +1078,7 @@ export class AguaceroCore extends EventEmitter {
|
|
|
995
1078
|
async setNexradProduct(product) {
|
|
996
1079
|
if (!this.state.isNexrad) return;
|
|
997
1080
|
const p = (product || 'REF').toUpperCase();
|
|
998
|
-
|
|
999
|
-
const fld = nexradColormapFldKey(ds, p);
|
|
1000
|
-
const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
|
|
1001
|
-
await this.setState({ nexradProduct: p, variable: fld, nexradStormRelative });
|
|
1002
|
-
await this.refreshNexradTimes();
|
|
1003
|
-
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
1004
|
-
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
1005
|
-
);
|
|
1006
|
-
if (filtered.length > 0) {
|
|
1007
|
-
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
async setNexradDataSource(source) {
|
|
1012
|
-
if (!this.state.isNexrad) return;
|
|
1013
|
-
const ds = source === 'level3' ? 'level3' : 'level2';
|
|
1014
|
-
const p = (this.state.nexradProduct || 'REF').toUpperCase();
|
|
1015
|
-
const fld = nexradColormapFldKey(ds, p);
|
|
1016
|
-
const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
|
|
1017
|
-
await this.setState({ nexradDataSource: ds, variable: fld, nexradStormRelative });
|
|
1018
|
-
await this.refreshNexradTimes();
|
|
1019
|
-
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
1020
|
-
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
|
1021
|
-
);
|
|
1022
|
-
if (filtered.length > 0) {
|
|
1023
|
-
await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
/**
|
|
1028
|
-
* Sets NEXRAD product and archive source together (single refresh). Used when the UI exposes one
|
|
1029
|
-
* combined menu instead of separate “Level II / Level III” controls.
|
|
1030
|
-
* @param {'level2'|'level3'} dataSource
|
|
1031
|
-
* @param {string} product - Level-II variable (REF, VEL, …) or Level-III radar key (N0H, HHC, …)
|
|
1032
|
-
*/
|
|
1033
|
-
async setNexradProductMode(dataSource, product) {
|
|
1034
|
-
if (!this.state.isNexrad) return;
|
|
1035
|
-
const ds = dataSource === 'level3' ? 'level3' : 'level2';
|
|
1036
|
-
const p = (product || 'REF').toUpperCase();
|
|
1037
|
-
const fld = nexradColormapFldKey(ds, p);
|
|
1038
|
-
const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
|
|
1039
|
-
await this.setState({ nexradDataSource: ds, nexradProduct: p, variable: fld, nexradStormRelative });
|
|
1081
|
+
await this.setState({ nexradProduct: p });
|
|
1040
1082
|
await this.refreshNexradTimes();
|
|
1041
1083
|
const filtered = this._getFilteredNexradTimestampsForVariable(
|
|
1042
1084
|
this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
|
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.
|
|
@@ -35,4 +39,5 @@ export {
|
|
|
35
39
|
|
|
36
40
|
/** NEXRAD tilt + listing helpers (also importable via subpaths; root re-export fixes Vite/esbuild subpath resolution). */
|
|
37
41
|
export * from './nexradTilts.js';
|
|
42
|
+
export * from './nexradTiltCoalesce.js';
|
|
38
43
|
export * from './nexrad_support.js';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NEXRAD tilt coalescing (matches aguacero-frontend `nexradTiltCoalesce.ts`).
|
|
3
|
+
* Adjacent manifest tilts within {@link NEXRAD_TILT_COALESCE_MAX_GAP}° are merged for UI/listings:
|
|
4
|
+
* pairs → higher tilt; runs of three or more consecutive steps merge the first two into the lower tilt.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Adjacent tilts (sorted) within this gap share one listing / control. */
|
|
8
|
+
export const NEXRAD_TILT_COALESCE_MAX_GAP = 0.1;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {number[]} segment
|
|
12
|
+
* @param {Map<number, number>} map
|
|
13
|
+
*/
|
|
14
|
+
function mergeCoalesceSegment(segment, map) {
|
|
15
|
+
if (segment.length === 0) return;
|
|
16
|
+
if (segment.length === 1) {
|
|
17
|
+
map.set(segment[0], segment[0]);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (segment.length === 2) {
|
|
21
|
+
const canonical = Math.max(segment[0], segment[1]);
|
|
22
|
+
map.set(segment[0], canonical);
|
|
23
|
+
map.set(segment[1], canonical);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const low = segment[0];
|
|
27
|
+
map.set(segment[0], low);
|
|
28
|
+
map.set(segment[1], low);
|
|
29
|
+
mergeCoalesceSegment(segment.slice(2), map);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {number[]} sortedUnique
|
|
34
|
+
* @returns {Map<number, number>}
|
|
35
|
+
*/
|
|
36
|
+
export function chainCoalesceToCanonical(sortedUnique) {
|
|
37
|
+
const map = new Map();
|
|
38
|
+
let i = 0;
|
|
39
|
+
while (i < sortedUnique.length) {
|
|
40
|
+
let j = i + 1;
|
|
41
|
+
while (
|
|
42
|
+
j < sortedUnique.length &&
|
|
43
|
+
sortedUnique[j] - sortedUnique[j - 1] <= NEXRAD_TILT_COALESCE_MAX_GAP + 1e-9
|
|
44
|
+
) {
|
|
45
|
+
j++;
|
|
46
|
+
}
|
|
47
|
+
const segment = sortedUnique.slice(i, j);
|
|
48
|
+
mergeCoalesceSegment(segment, map);
|
|
49
|
+
i = j;
|
|
50
|
+
}
|
|
51
|
+
return map;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Manifest tilt list with adjacent options merged (pairs → higher; triple+ splits per merge rules).
|
|
56
|
+
* @param {number[]} tilts
|
|
57
|
+
* @returns {number[]}
|
|
58
|
+
*/
|
|
59
|
+
export function coalesceNexradTiltOptionsForDisplay(tilts) {
|
|
60
|
+
const sorted = [...new Set(tilts)]
|
|
61
|
+
.filter((t) => Number.isFinite(t))
|
|
62
|
+
.sort((a, b) => a - b);
|
|
63
|
+
if (sorted.length <= 1) return sorted;
|
|
64
|
+
const tiltToCanonical = chainCoalesceToCanonical(sorted);
|
|
65
|
+
const reps = [];
|
|
66
|
+
let prevRep;
|
|
67
|
+
for (const t of sorted) {
|
|
68
|
+
const c = tiltToCanonical.get(t);
|
|
69
|
+
if (c === undefined) continue;
|
|
70
|
+
if (c !== prevRep) {
|
|
71
|
+
reps.push(c);
|
|
72
|
+
prevRep = c;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return reps;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Map stored/clamped tilt to the coalesced elevation value (same rules as {@link coalesceNexradTiltOptionsForDisplay}).
|
|
80
|
+
* @param {number} layerTilt
|
|
81
|
+
* @param {number[]} manifestTilts raw manifest tilts (before coalescing)
|
|
82
|
+
* @returns {number}
|
|
83
|
+
*/
|
|
84
|
+
export function nexradLayerTiltToDisplayOption(layerTilt, manifestTilts) {
|
|
85
|
+
const sorted = [...new Set(manifestTilts)]
|
|
86
|
+
.filter((t) => Number.isFinite(t))
|
|
87
|
+
.sort((a, b) => a - b);
|
|
88
|
+
if (sorted.length === 0) return layerTilt;
|
|
89
|
+
const tiltToCanonical = chainCoalesceToCanonical(sorted);
|
|
90
|
+
if (tiltToCanonical.has(layerTilt)) return tiltToCanonical.get(layerTilt);
|
|
91
|
+
const nearest = sorted.reduce((best, x) =>
|
|
92
|
+
Math.abs(x - layerTilt) < Math.abs(best - layerTilt) ? x : best,
|
|
93
|
+
sorted[0]);
|
|
94
|
+
return tiltToCanonical.get(nearest) ?? layerTilt;
|
|
95
|
+
}
|
package/src/nexrad_support.js
CHANGED
|
@@ -8,12 +8,24 @@ import {
|
|
|
8
8
|
getDefaultRadarTilt,
|
|
9
9
|
getRadarTilts,
|
|
10
10
|
} from './nexradTilts.js';
|
|
11
|
+
import { coalesceNexradTiltOptionsForDisplay } from './nexradTiltCoalesce.js';
|
|
11
12
|
import {
|
|
12
13
|
NEXRAD_LEVEL3_ELEV,
|
|
13
14
|
NEXRAD_LEVEL3_MOTION_PRODUCT,
|
|
14
15
|
getNexradLevel3EntryByRadarKey,
|
|
15
16
|
} from './nexrad_level3_catalog.js';
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Whether listings and archive fetches for this product use Level-II sweep lambda vs Level-III S3 products.
|
|
20
|
+
* AguaceroCore derives this from the product key only (not user-configurable).
|
|
21
|
+
* @param {string} [nexradProduct] - Radar variable key (e.g. REF, KDP, VEL).
|
|
22
|
+
* @returns {'level2'|'level3'}
|
|
23
|
+
*/
|
|
24
|
+
export function inferNexradDataSourceForProduct(nexradProduct) {
|
|
25
|
+
const p = (nexradProduct || 'REF').toUpperCase();
|
|
26
|
+
return getNexradLevel3EntryByRadarKey(p) ? 'level3' : 'level2';
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
/** Colormap / DICTIONARIES.fld key for customColormaps (matches frontend userDefaultColormap). */
|
|
18
30
|
export function nexradColormapFldKey(nexradDataSource, nexradProduct) {
|
|
19
31
|
const p = (nexradProduct || 'REF').toUpperCase();
|
|
@@ -252,13 +264,15 @@ export async function fetchNexradTimesListing(opts) {
|
|
|
252
264
|
const L3_TILT_INDEX_MANIFEST_SLOTS = 6;
|
|
253
265
|
|
|
254
266
|
/**
|
|
255
|
-
*
|
|
267
|
+
* Raw manifest tilt list before coalescing (same rules as aguacero-frontend `radarTiltOptionsRaw`).
|
|
268
|
+
* Use {@link coalesceNexradTiltOptionsForDisplay} or {@link getAvailableNexradTilts} for UI controls.
|
|
269
|
+
*
|
|
256
270
|
* @param {string} siteId
|
|
257
271
|
* @param {'level2'|'level3'} nexradDataSource
|
|
258
272
|
* @param {string} [nexradProduct]
|
|
259
273
|
* @returns {number[]}
|
|
260
274
|
*/
|
|
261
|
-
export function
|
|
275
|
+
export function getRawNexradTiltsForCoalesce(siteId, nexradDataSource, nexradProduct) {
|
|
262
276
|
if (!siteId) return [];
|
|
263
277
|
const v = (nexradProduct || 'REF').toUpperCase();
|
|
264
278
|
const ds = nexradDataSource === 'level3' ? 'level3' : 'level2';
|
|
@@ -274,3 +288,18 @@ export function getAvailableNexradTilts(siteId, nexradDataSource, nexradProduct)
|
|
|
274
288
|
}
|
|
275
289
|
return [];
|
|
276
290
|
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Tilt angles for the UI: manifest tilts with adjacent 0.1° steps merged like aguacero-frontend
|
|
294
|
+
* ({@link coalesceNexradTiltOptionsForDisplay}).
|
|
295
|
+
*
|
|
296
|
+
* @param {string} siteId
|
|
297
|
+
* @param {'level2'|'level3'} nexradDataSource
|
|
298
|
+
* @param {string} [nexradProduct]
|
|
299
|
+
* @returns {number[]}
|
|
300
|
+
*/
|
|
301
|
+
export function getAvailableNexradTilts(siteId, nexradDataSource, nexradProduct) {
|
|
302
|
+
const raw = getRawNexradTiltsForCoalesce(siteId, nexradDataSource, nexradProduct);
|
|
303
|
+
if (!raw.length) return [];
|
|
304
|
+
return coalesceNexradTiltOptionsForDisplay(raw);
|
|
305
|
+
}
|
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)))}`;
|