@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.
- package/dist/AguaceroCore.js +207 -794
- package/dist/default-colormaps.js +437 -556
- package/dist/dictionaries.js +2413 -2071
- package/dist/events.js +2 -2
- package/dist/getBundleId.js +20 -9
- package/dist/index.js +1 -172
- package/dist/unitConversions.js +10 -10
- package/package.json +99 -99
- package/src/coordinate_configs.js +374 -374
- package/src/default-colormaps.js +3369 -3369
- package/src/dictionaries.js +3857 -3857
- package/src/events.js +31 -31
- package/src/fill-layer-worker.js +26 -26
- package/src/getBundleId.js +8 -8
- package/src/getBundleId.native.js +14 -14
- package/src/gridDecodePipeline.js +32 -32
- package/src/gridDecodeWorker.js +24 -24
- package/src/index.js +48 -48
- package/src/map-styles.js +101 -101
- package/src/nexradTiltCoalesce.js +95 -95
- package/src/nexradTilts.js +128 -128
- package/src/nexrad_level3_catalog.js +26 -26
- package/src/nws/nwsAlertsFetchSpec.js +93 -93
- package/src/nws/nwsEventColorsDefaults.js +133 -133
- package/src/nws/nwsSdkConstants.js +368 -368
- package/src/satellite_support.js +376 -376
- package/src/spawnGridDecodeWorker.js +5 -5
- package/src/unitConversions.js +102 -102
- package/dist/getBundleId.native.js +0 -18
- package/dist/gridDecodePipeline.js +0 -37
- package/dist/gridDecodeWorker.js +0 -31
- package/dist/nexradTiltCoalesce.js +0 -95
- package/dist/nexradTilts.js +0 -129
- package/dist/nexrad_level3_catalog.js +0 -56
- package/dist/nexrad_support.js +0 -269
- package/dist/satellite_support.js +0 -395
package/src/satellite_support.js
CHANGED
|
@@ -1,376 +1,376 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Satellite listing / timeline helpers aligned with aguacero-frontend (ActiveTimeContext + satelliteAvailability).
|
|
3
|
-
* Used by AguaceroCore for GOES-East WebGL satellite mode.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const SATELLITE_FRAMES_URL = 'https://rhnq3edhcry5n6nljn6my4o3h40zwxze.lambda-url.us-east-2.on.aws/';
|
|
7
|
-
|
|
8
|
-
/** Same sector labels as production bundle / ActiveTimeContext SECTOR_CODE_MAP (inverse). */
|
|
9
|
-
export const GOES_EAST_SATELLITE_SECTORS = [
|
|
10
|
-
'GOES-EAST CONUS',
|
|
11
|
-
'GOES-EAST FULL DISK',
|
|
12
|
-
'GOES-EAST MESOSCALE 1',
|
|
13
|
-
'GOES-EAST MESOSCALE 2',
|
|
14
|
-
];
|
|
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
|
-
|
|
43
|
-
/** Flat channel ids matching CHANNEL_CATEGORIES in frontend dictionaries. */
|
|
44
|
-
export const GOES_SATELLITE_CHANNELS = [
|
|
45
|
-
...['true_color', 'geocolor', 'ntmicro', 'day_cloud_phase', 'day_land_cloud_fire', 'air_mass', 'sandwich', 'simple_water_vapor', 'dust', 'fire_temperature'],
|
|
46
|
-
'C01', 'C02', 'C03', 'C04', 'C05', 'C06', 'C07', 'C08', 'C09', 'C10', 'C11', 'C12', 'C13', 'C14', 'C15', 'C16',
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Human-readable labels for {@link GOES_SATELLITE_CHANNELS}, aligned with aguacero-frontend `CHANNEL_LABELS`.
|
|
51
|
-
*/
|
|
52
|
-
export const GOES_SATELLITE_CHANNEL_LABELS = {
|
|
53
|
-
true_color: 'True Color',
|
|
54
|
-
ntmicro: 'Night Microphysics',
|
|
55
|
-
day_cloud_phase: 'Day Cloud Phase',
|
|
56
|
-
day_land_cloud_fire: 'Natural Color Fire',
|
|
57
|
-
air_mass: 'Air Mass',
|
|
58
|
-
sandwich: 'Sandwich',
|
|
59
|
-
simple_water_vapor: 'Simple Water Vapor',
|
|
60
|
-
dust: 'Dust',
|
|
61
|
-
geocolor: 'Geocolor',
|
|
62
|
-
fire_temperature: 'Fire Temperature',
|
|
63
|
-
C01: 'Blue - Visible',
|
|
64
|
-
C02: 'Red - Visible',
|
|
65
|
-
C03: 'Veggie - Near IR',
|
|
66
|
-
C04: 'Cirrus - Near IR',
|
|
67
|
-
C05: 'Snow/Ice - Near IR',
|
|
68
|
-
C06: 'Cloud Particle - Near IR',
|
|
69
|
-
C07: 'Shortwave Window - IR',
|
|
70
|
-
C08: 'Upper-Level Water Vapor - IR',
|
|
71
|
-
C09: 'Mid-Level Water Vapor - IR',
|
|
72
|
-
C10: 'Lower-Level Water Vapor - IR',
|
|
73
|
-
C11: 'Cloud Top - IR',
|
|
74
|
-
C12: 'Ozone - IR',
|
|
75
|
-
C13: 'Clean Longwave Window - IR',
|
|
76
|
-
C14: 'Longwave Window - IR',
|
|
77
|
-
C15: 'Dirty Longwave Window - IR',
|
|
78
|
-
C16: 'CO2 Longwave - IR',
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const SECTOR_CODE_MAP = {
|
|
82
|
-
'GOES-EAST CONUS': 'C',
|
|
83
|
-
'GOES-EAST FULL DISK': 'F',
|
|
84
|
-
'GOES-EAST MESOSCALE 1': 'M1',
|
|
85
|
-
'GOES-EAST MESOSCALE 2': 'M2',
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const LUMPED_MULTIBAND_SATELLITE_SECTORS = new Set(['GOES-EAST CONUS', 'GOES-EAST FULL DISK']);
|
|
89
|
-
|
|
90
|
-
/** Uppercase RGB channel tokens as in production (CHANNEL_CATEGORIES.RGB). */
|
|
91
|
-
const SATELLITE_RGB_PRODUCT_UPPER = new Set([
|
|
92
|
-
'TRUE_COLOR',
|
|
93
|
-
'GEOCOLOR',
|
|
94
|
-
'NTMICRO',
|
|
95
|
-
'DAY_CLOUD_PHASE',
|
|
96
|
-
'DAY_LAND_CLOUD_FIRE',
|
|
97
|
-
'AIR_MASS',
|
|
98
|
-
'SANDWICH',
|
|
99
|
-
'SIMPLE_WATER_VAPOR',
|
|
100
|
-
'DUST',
|
|
101
|
-
'FIRE_TEMPERATURE',
|
|
102
|
-
]);
|
|
103
|
-
|
|
104
|
-
/**
|
|
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}.
|
|
111
|
-
*/
|
|
112
|
-
export const TIMELINE_DURATION_HOUR_VALUES = ['1', '4', '6', '12'];
|
|
113
|
-
|
|
114
|
-
const LEGACY_SATELLITE_DURATION_ALIASES = {
|
|
115
|
-
'0.5': '1',
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
/** Tier keys used in SATELLITE_DURATION_CONFIG (frontend dictionaries). */
|
|
119
|
-
export const SATELLITE_DURATION_CONFIG = {
|
|
120
|
-
CONUS: {
|
|
121
|
-
basic: [
|
|
122
|
-
{ label: '1 Hr', value: '1', interval: 300 },
|
|
123
|
-
{ label: '4 Hr', value: '4', interval: 300 },
|
|
124
|
-
{ label: '6 Hr', value: '6', interval: 300 },
|
|
125
|
-
{ label: '12 Hr', value: '12', interval: 300 },
|
|
126
|
-
],
|
|
127
|
-
},
|
|
128
|
-
FULL_DISK: {
|
|
129
|
-
basic: [
|
|
130
|
-
{ label: '1 Hr', value: '1', interval: 600 },
|
|
131
|
-
{ label: '4 Hr', value: '4', interval: 600 },
|
|
132
|
-
{ label: '6 Hr', value: '6', interval: 600 },
|
|
133
|
-
{ label: '12 Hr', value: '12', interval: 600 },
|
|
134
|
-
],
|
|
135
|
-
},
|
|
136
|
-
MESOSCALE: {
|
|
137
|
-
basic: [
|
|
138
|
-
{ label: '1 Hr', value: '1', interval: 120 },
|
|
139
|
-
{ label: '4 Hr', value: '4', interval: 120 },
|
|
140
|
-
{ label: '6 Hr', value: '6', interval: 120 },
|
|
141
|
-
{ label: '12 Hr', value: '12', interval: 120 },
|
|
142
|
-
],
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/**
|
|
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>).
|
|
162
|
-
* @param {string|number} value
|
|
163
|
-
* @returns {string}
|
|
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). */
|
|
172
|
-
export function normalizeTimelineDurationValue(value) {
|
|
173
|
-
return formatTimelineDurationValue(value);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
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 }}
|
|
182
|
-
*/
|
|
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 };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function getDefaultSatelliteDurationOption(sectorName, tier = 'basic') {
|
|
198
|
-
let sectorType = 'CONUS';
|
|
199
|
-
if (sectorName.includes('FULL DISK')) sectorType = 'FULL_DISK';
|
|
200
|
-
else if (sectorName.includes('MESOSCALE')) sectorType = 'MESOSCALE';
|
|
201
|
-
const tierConfig = SATELLITE_DURATION_CONFIG[sectorType]?.[tier] || SATELLITE_DURATION_CONFIG.CONUS.basic;
|
|
202
|
-
return tierConfig.find((o) => o.value === '1') || tierConfig[0];
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function calculateUnixTimeFromSatelliteKey(fileKey) {
|
|
206
|
-
try {
|
|
207
|
-
const match = fileKey.match(/_(\d{10})(?:\.ktx2)?$/);
|
|
208
|
-
if (match) return parseInt(match[1], 10);
|
|
209
|
-
const oldMatch = fileKey.match(/_s(\d{4})(\d{3})(\d{2})(\d{2})(\d{2})/);
|
|
210
|
-
if (oldMatch) {
|
|
211
|
-
const [, year, dayOfYear, hour, minute, second] = oldMatch.map(Number);
|
|
212
|
-
const date = new Date(Date.UTC(year, 0, 1));
|
|
213
|
-
date.setUTCDate(dayOfYear);
|
|
214
|
-
date.setUTCHours(hour, minute, second, 0);
|
|
215
|
-
return date.getTime() / 1000;
|
|
216
|
-
}
|
|
217
|
-
return 0;
|
|
218
|
-
} catch {
|
|
219
|
-
return 0;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* @param {{ satelliteInstrumentId: string, satelliteSectorLabel: string, satelliteChannel: string }} selection
|
|
225
|
-
* @param {string[]} allFiles S3 keys from satellite listing API
|
|
226
|
-
* @param {{ value: string, interval: number }} durationOption
|
|
227
|
-
*/
|
|
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) {
|
|
233
|
-
return { unixTimes: [], fileList: [], timeToFileMap: {} };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const SECTOR_CODE_MAP_LOCAL = {
|
|
237
|
-
'GOES-EAST CONUS': 'C',
|
|
238
|
-
'GOES-EAST FULL DISK': 'F',
|
|
239
|
-
'GOES-EAST MESOSCALE 1': 'M1',
|
|
240
|
-
'GOES-EAST MESOSCALE 2': 'M2',
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const satelliteNum = satelliteName.replace('GOES', '').replace('-EAST', '').replace('-WEST', '');
|
|
244
|
-
const satId = `G${satelliteNum}`;
|
|
245
|
-
const sectorCode = SECTOR_CODE_MAP_LOCAL[categoryName] || 'C';
|
|
246
|
-
|
|
247
|
-
let sectorType = 'CONUS';
|
|
248
|
-
if (categoryName.includes('FULL DISK')) sectorType = 'FULL_DISK';
|
|
249
|
-
else if (categoryName.includes('MESOSCALE')) sectorType = 'MESOSCALE';
|
|
250
|
-
|
|
251
|
-
const intervalSeconds = durationOption?.interval || 300;
|
|
252
|
-
const durationHours = durationOption?.value ? parseFloat(durationOption.value) : 1;
|
|
253
|
-
|
|
254
|
-
const filePrefix = `${sectorCode}_${satId}_`;
|
|
255
|
-
|
|
256
|
-
let searchProduct = channelName.toUpperCase();
|
|
257
|
-
if (!/^C\d{2}$/.test(searchProduct)) {
|
|
258
|
-
if (SATELLITE_RGB_PRODUCT_UPPER.has(searchProduct)) {
|
|
259
|
-
searchProduct = LUMPED_MULTIBAND_SATELLITE_SECTORS.has(categoryName) ? 'MULTI' : searchProduct;
|
|
260
|
-
} else {
|
|
261
|
-
searchProduct = 'MULTI';
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const matchString = `${filePrefix}${searchProduct}`;
|
|
266
|
-
|
|
267
|
-
let relevantFiles = allFiles.filter((file) => file.startsWith(matchString));
|
|
268
|
-
|
|
269
|
-
if (relevantFiles.length === 0) {
|
|
270
|
-
return { unixTimes: [], fileList: [], timeToFileMap: {} };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
relevantFiles.sort((a, b) => {
|
|
274
|
-
const timeA = calculateUnixTimeFromSatelliteKey(a);
|
|
275
|
-
const timeB = calculateUnixTimeFromSatelliteKey(b);
|
|
276
|
-
return timeA - timeB;
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
let cutoffTime = 0;
|
|
280
|
-
if (durationHours > 0) {
|
|
281
|
-
const latestTime = calculateUnixTimeFromSatelliteKey(relevantFiles[relevantFiles.length - 1]);
|
|
282
|
-
cutoffTime = latestTime - durationHours * 3600;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
let processedFiles = relevantFiles.filter((f) => calculateUnixTimeFromSatelliteKey(f) >= cutoffTime);
|
|
286
|
-
|
|
287
|
-
if (intervalSeconds > 0 && processedFiles.length > 0) {
|
|
288
|
-
const intervalMinutes = intervalSeconds / 60;
|
|
289
|
-
|
|
290
|
-
if (sectorType === 'MESOSCALE') {
|
|
291
|
-
processedFiles = processedFiles.filter((file) => {
|
|
292
|
-
if (intervalMinutes <= 1.1) return true;
|
|
293
|
-
const t = calculateUnixTimeFromSatelliteKey(file);
|
|
294
|
-
const date = new Date(t * 1000);
|
|
295
|
-
const minutes = date.getUTCMinutes();
|
|
296
|
-
return minutes % intervalMinutes === 0;
|
|
297
|
-
});
|
|
298
|
-
} else {
|
|
299
|
-
if (intervalSeconds >= 300) {
|
|
300
|
-
processedFiles = processedFiles.filter((file) => {
|
|
301
|
-
if (Math.abs(intervalMinutes - 5) < 0.1) return true;
|
|
302
|
-
const t = calculateUnixTimeFromSatelliteKey(file);
|
|
303
|
-
const minutes = new Date(t * 1000).getUTCMinutes();
|
|
304
|
-
const remainder = minutes % intervalMinutes;
|
|
305
|
-
const distToInterval = Math.min(remainder, intervalMinutes - remainder);
|
|
306
|
-
return distToInterval < 2.5;
|
|
307
|
-
});
|
|
308
|
-
} else {
|
|
309
|
-
processedFiles = processedFiles.filter((file) => {
|
|
310
|
-
const t = calculateUnixTimeFromSatelliteKey(file);
|
|
311
|
-
const minutes = new Date(t * 1000).getUTCMinutes();
|
|
312
|
-
return minutes % intervalMinutes === 0;
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (processedFiles.length === 0 && relevantFiles.length > 0) {
|
|
318
|
-
processedFiles = relevantFiles.slice(-1);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const unixTimes = [];
|
|
323
|
-
const timeToFileMap = {};
|
|
324
|
-
processedFiles.forEach((file) => {
|
|
325
|
-
const unixTime = calculateUnixTimeFromSatelliteKey(file);
|
|
326
|
-
if (unixTime > 0) {
|
|
327
|
-
unixTimes.push(unixTime);
|
|
328
|
-
timeToFileMap[unixTime] = file;
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// Chronological (oldest → newest): matches standard time sliders (earlier left, later right).
|
|
333
|
-
unixTimes.sort((a, b) => a - b);
|
|
334
|
-
|
|
335
|
-
return { unixTimes, fileList: processedFiles, timeToFileMap };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
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 }>}
|
|
342
|
-
*/
|
|
343
|
-
export function getAllGoesEastSatelliteSelections(satelliteInstrumentId = 'GOES19-EAST') {
|
|
344
|
-
const out = [];
|
|
345
|
-
for (const sector of GOES_EAST_SATELLITE_SECTORS) {
|
|
346
|
-
for (const ch of GOES_SATELLITE_CHANNELS) {
|
|
347
|
-
out.push({
|
|
348
|
-
satelliteInstrumentId,
|
|
349
|
-
satelliteSectorLabel: sector,
|
|
350
|
-
satelliteChannel: ch,
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return out;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* @param {string} satelliteChannel — band or RGB id (third segment of the archive descriptor)
|
|
359
|
-
*/
|
|
360
|
-
export function resolveSatelliteS3FileName(satelliteChannel, timeToFileMap, satelliteTimestamp) {
|
|
361
|
-
const fileFromMap = timeToFileMap?.[satelliteTimestamp];
|
|
362
|
-
if (fileFromMap) return fileFromMap;
|
|
363
|
-
const channelName = satelliteChannel;
|
|
364
|
-
const channelNameUpper = String(channelName).toUpperCase();
|
|
365
|
-
let name = fileFromMap || '';
|
|
366
|
-
if (!name && satelliteTimestamp != null) {
|
|
367
|
-
const suffix = `_${String(Math.floor(Number(satelliteTimestamp)))}`;
|
|
368
|
-
const multiName = Object.values(timeToFileMap || {}).find((f) => f.includes('MULTI') && f.includes(suffix));
|
|
369
|
-
if (multiName) {
|
|
370
|
-
name = multiName.replace('MULTI', channelNameUpper);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return name;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
export { SECTOR_CODE_MAP };
|
|
1
|
+
/**
|
|
2
|
+
* Satellite listing / timeline helpers aligned with aguacero-frontend (ActiveTimeContext + satelliteAvailability).
|
|
3
|
+
* Used by AguaceroCore for GOES-East WebGL satellite mode.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const SATELLITE_FRAMES_URL = 'https://rhnq3edhcry5n6nljn6my4o3h40zwxze.lambda-url.us-east-2.on.aws/';
|
|
7
|
+
|
|
8
|
+
/** Same sector labels as production bundle / ActiveTimeContext SECTOR_CODE_MAP (inverse). */
|
|
9
|
+
export const GOES_EAST_SATELLITE_SECTORS = [
|
|
10
|
+
'GOES-EAST CONUS',
|
|
11
|
+
'GOES-EAST FULL DISK',
|
|
12
|
+
'GOES-EAST MESOSCALE 1',
|
|
13
|
+
'GOES-EAST MESOSCALE 2',
|
|
14
|
+
];
|
|
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
|
+
|
|
43
|
+
/** Flat channel ids matching CHANNEL_CATEGORIES in frontend dictionaries. */
|
|
44
|
+
export const GOES_SATELLITE_CHANNELS = [
|
|
45
|
+
...['true_color', 'geocolor', 'ntmicro', 'day_cloud_phase', 'day_land_cloud_fire', 'air_mass', 'sandwich', 'simple_water_vapor', 'dust', 'fire_temperature'],
|
|
46
|
+
'C01', 'C02', 'C03', 'C04', 'C05', 'C06', 'C07', 'C08', 'C09', 'C10', 'C11', 'C12', 'C13', 'C14', 'C15', 'C16',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Human-readable labels for {@link GOES_SATELLITE_CHANNELS}, aligned with aguacero-frontend `CHANNEL_LABELS`.
|
|
51
|
+
*/
|
|
52
|
+
export const GOES_SATELLITE_CHANNEL_LABELS = {
|
|
53
|
+
true_color: 'True Color',
|
|
54
|
+
ntmicro: 'Night Microphysics',
|
|
55
|
+
day_cloud_phase: 'Day Cloud Phase',
|
|
56
|
+
day_land_cloud_fire: 'Natural Color Fire',
|
|
57
|
+
air_mass: 'Air Mass',
|
|
58
|
+
sandwich: 'Sandwich',
|
|
59
|
+
simple_water_vapor: 'Simple Water Vapor',
|
|
60
|
+
dust: 'Dust',
|
|
61
|
+
geocolor: 'Geocolor',
|
|
62
|
+
fire_temperature: 'Fire Temperature',
|
|
63
|
+
C01: 'Blue - Visible',
|
|
64
|
+
C02: 'Red - Visible',
|
|
65
|
+
C03: 'Veggie - Near IR',
|
|
66
|
+
C04: 'Cirrus - Near IR',
|
|
67
|
+
C05: 'Snow/Ice - Near IR',
|
|
68
|
+
C06: 'Cloud Particle - Near IR',
|
|
69
|
+
C07: 'Shortwave Window - IR',
|
|
70
|
+
C08: 'Upper-Level Water Vapor - IR',
|
|
71
|
+
C09: 'Mid-Level Water Vapor - IR',
|
|
72
|
+
C10: 'Lower-Level Water Vapor - IR',
|
|
73
|
+
C11: 'Cloud Top - IR',
|
|
74
|
+
C12: 'Ozone - IR',
|
|
75
|
+
C13: 'Clean Longwave Window - IR',
|
|
76
|
+
C14: 'Longwave Window - IR',
|
|
77
|
+
C15: 'Dirty Longwave Window - IR',
|
|
78
|
+
C16: 'CO2 Longwave - IR',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const SECTOR_CODE_MAP = {
|
|
82
|
+
'GOES-EAST CONUS': 'C',
|
|
83
|
+
'GOES-EAST FULL DISK': 'F',
|
|
84
|
+
'GOES-EAST MESOSCALE 1': 'M1',
|
|
85
|
+
'GOES-EAST MESOSCALE 2': 'M2',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const LUMPED_MULTIBAND_SATELLITE_SECTORS = new Set(['GOES-EAST CONUS', 'GOES-EAST FULL DISK']);
|
|
89
|
+
|
|
90
|
+
/** Uppercase RGB channel tokens as in production (CHANNEL_CATEGORIES.RGB). */
|
|
91
|
+
const SATELLITE_RGB_PRODUCT_UPPER = new Set([
|
|
92
|
+
'TRUE_COLOR',
|
|
93
|
+
'GEOCOLOR',
|
|
94
|
+
'NTMICRO',
|
|
95
|
+
'DAY_CLOUD_PHASE',
|
|
96
|
+
'DAY_LAND_CLOUD_FIRE',
|
|
97
|
+
'AIR_MASS',
|
|
98
|
+
'SANDWICH',
|
|
99
|
+
'SIMPLE_WATER_VAPOR',
|
|
100
|
+
'DUST',
|
|
101
|
+
'FIRE_TEMPERATURE',
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
/**
|
|
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}.
|
|
111
|
+
*/
|
|
112
|
+
export const TIMELINE_DURATION_HOUR_VALUES = ['1', '4', '6', '12'];
|
|
113
|
+
|
|
114
|
+
const LEGACY_SATELLITE_DURATION_ALIASES = {
|
|
115
|
+
'0.5': '1',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Tier keys used in SATELLITE_DURATION_CONFIG (frontend dictionaries). */
|
|
119
|
+
export const SATELLITE_DURATION_CONFIG = {
|
|
120
|
+
CONUS: {
|
|
121
|
+
basic: [
|
|
122
|
+
{ label: '1 Hr', value: '1', interval: 300 },
|
|
123
|
+
{ label: '4 Hr', value: '4', interval: 300 },
|
|
124
|
+
{ label: '6 Hr', value: '6', interval: 300 },
|
|
125
|
+
{ label: '12 Hr', value: '12', interval: 300 },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
FULL_DISK: {
|
|
129
|
+
basic: [
|
|
130
|
+
{ label: '1 Hr', value: '1', interval: 600 },
|
|
131
|
+
{ label: '4 Hr', value: '4', interval: 600 },
|
|
132
|
+
{ label: '6 Hr', value: '6', interval: 600 },
|
|
133
|
+
{ label: '12 Hr', value: '12', interval: 600 },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
MESOSCALE: {
|
|
137
|
+
basic: [
|
|
138
|
+
{ label: '1 Hr', value: '1', interval: 120 },
|
|
139
|
+
{ label: '4 Hr', value: '4', interval: 120 },
|
|
140
|
+
{ label: '6 Hr', value: '6', interval: 120 },
|
|
141
|
+
{ label: '12 Hr', value: '12', interval: 120 },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
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>).
|
|
162
|
+
* @param {string|number} value
|
|
163
|
+
* @returns {string}
|
|
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). */
|
|
172
|
+
export function normalizeTimelineDurationValue(value) {
|
|
173
|
+
return formatTimelineDurationValue(value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
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 }}
|
|
182
|
+
*/
|
|
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 };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function getDefaultSatelliteDurationOption(sectorName, tier = 'basic') {
|
|
198
|
+
let sectorType = 'CONUS';
|
|
199
|
+
if (sectorName.includes('FULL DISK')) sectorType = 'FULL_DISK';
|
|
200
|
+
else if (sectorName.includes('MESOSCALE')) sectorType = 'MESOSCALE';
|
|
201
|
+
const tierConfig = SATELLITE_DURATION_CONFIG[sectorType]?.[tier] || SATELLITE_DURATION_CONFIG.CONUS.basic;
|
|
202
|
+
return tierConfig.find((o) => o.value === '1') || tierConfig[0];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function calculateUnixTimeFromSatelliteKey(fileKey) {
|
|
206
|
+
try {
|
|
207
|
+
const match = fileKey.match(/_(\d{10})(?:\.ktx2)?$/);
|
|
208
|
+
if (match) return parseInt(match[1], 10);
|
|
209
|
+
const oldMatch = fileKey.match(/_s(\d{4})(\d{3})(\d{2})(\d{2})(\d{2})/);
|
|
210
|
+
if (oldMatch) {
|
|
211
|
+
const [, year, dayOfYear, hour, minute, second] = oldMatch.map(Number);
|
|
212
|
+
const date = new Date(Date.UTC(year, 0, 1));
|
|
213
|
+
date.setUTCDate(dayOfYear);
|
|
214
|
+
date.setUTCHours(hour, minute, second, 0);
|
|
215
|
+
return date.getTime() / 1000;
|
|
216
|
+
}
|
|
217
|
+
return 0;
|
|
218
|
+
} catch {
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {{ satelliteInstrumentId: string, satelliteSectorLabel: string, satelliteChannel: string }} selection
|
|
225
|
+
* @param {string[]} allFiles S3 keys from satellite listing API
|
|
226
|
+
* @param {{ value: string, interval: number }} durationOption
|
|
227
|
+
*/
|
|
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) {
|
|
233
|
+
return { unixTimes: [], fileList: [], timeToFileMap: {} };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const SECTOR_CODE_MAP_LOCAL = {
|
|
237
|
+
'GOES-EAST CONUS': 'C',
|
|
238
|
+
'GOES-EAST FULL DISK': 'F',
|
|
239
|
+
'GOES-EAST MESOSCALE 1': 'M1',
|
|
240
|
+
'GOES-EAST MESOSCALE 2': 'M2',
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const satelliteNum = satelliteName.replace('GOES', '').replace('-EAST', '').replace('-WEST', '');
|
|
244
|
+
const satId = `G${satelliteNum}`;
|
|
245
|
+
const sectorCode = SECTOR_CODE_MAP_LOCAL[categoryName] || 'C';
|
|
246
|
+
|
|
247
|
+
let sectorType = 'CONUS';
|
|
248
|
+
if (categoryName.includes('FULL DISK')) sectorType = 'FULL_DISK';
|
|
249
|
+
else if (categoryName.includes('MESOSCALE')) sectorType = 'MESOSCALE';
|
|
250
|
+
|
|
251
|
+
const intervalSeconds = durationOption?.interval || 300;
|
|
252
|
+
const durationHours = durationOption?.value ? parseFloat(durationOption.value) : 1;
|
|
253
|
+
|
|
254
|
+
const filePrefix = `${sectorCode}_${satId}_`;
|
|
255
|
+
|
|
256
|
+
let searchProduct = channelName.toUpperCase();
|
|
257
|
+
if (!/^C\d{2}$/.test(searchProduct)) {
|
|
258
|
+
if (SATELLITE_RGB_PRODUCT_UPPER.has(searchProduct)) {
|
|
259
|
+
searchProduct = LUMPED_MULTIBAND_SATELLITE_SECTORS.has(categoryName) ? 'MULTI' : searchProduct;
|
|
260
|
+
} else {
|
|
261
|
+
searchProduct = 'MULTI';
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const matchString = `${filePrefix}${searchProduct}`;
|
|
266
|
+
|
|
267
|
+
let relevantFiles = allFiles.filter((file) => file.startsWith(matchString));
|
|
268
|
+
|
|
269
|
+
if (relevantFiles.length === 0) {
|
|
270
|
+
return { unixTimes: [], fileList: [], timeToFileMap: {} };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
relevantFiles.sort((a, b) => {
|
|
274
|
+
const timeA = calculateUnixTimeFromSatelliteKey(a);
|
|
275
|
+
const timeB = calculateUnixTimeFromSatelliteKey(b);
|
|
276
|
+
return timeA - timeB;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
let cutoffTime = 0;
|
|
280
|
+
if (durationHours > 0) {
|
|
281
|
+
const latestTime = calculateUnixTimeFromSatelliteKey(relevantFiles[relevantFiles.length - 1]);
|
|
282
|
+
cutoffTime = latestTime - durationHours * 3600;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let processedFiles = relevantFiles.filter((f) => calculateUnixTimeFromSatelliteKey(f) >= cutoffTime);
|
|
286
|
+
|
|
287
|
+
if (intervalSeconds > 0 && processedFiles.length > 0) {
|
|
288
|
+
const intervalMinutes = intervalSeconds / 60;
|
|
289
|
+
|
|
290
|
+
if (sectorType === 'MESOSCALE') {
|
|
291
|
+
processedFiles = processedFiles.filter((file) => {
|
|
292
|
+
if (intervalMinutes <= 1.1) return true;
|
|
293
|
+
const t = calculateUnixTimeFromSatelliteKey(file);
|
|
294
|
+
const date = new Date(t * 1000);
|
|
295
|
+
const minutes = date.getUTCMinutes();
|
|
296
|
+
return minutes % intervalMinutes === 0;
|
|
297
|
+
});
|
|
298
|
+
} else {
|
|
299
|
+
if (intervalSeconds >= 300) {
|
|
300
|
+
processedFiles = processedFiles.filter((file) => {
|
|
301
|
+
if (Math.abs(intervalMinutes - 5) < 0.1) return true;
|
|
302
|
+
const t = calculateUnixTimeFromSatelliteKey(file);
|
|
303
|
+
const minutes = new Date(t * 1000).getUTCMinutes();
|
|
304
|
+
const remainder = minutes % intervalMinutes;
|
|
305
|
+
const distToInterval = Math.min(remainder, intervalMinutes - remainder);
|
|
306
|
+
return distToInterval < 2.5;
|
|
307
|
+
});
|
|
308
|
+
} else {
|
|
309
|
+
processedFiles = processedFiles.filter((file) => {
|
|
310
|
+
const t = calculateUnixTimeFromSatelliteKey(file);
|
|
311
|
+
const minutes = new Date(t * 1000).getUTCMinutes();
|
|
312
|
+
return minutes % intervalMinutes === 0;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (processedFiles.length === 0 && relevantFiles.length > 0) {
|
|
318
|
+
processedFiles = relevantFiles.slice(-1);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const unixTimes = [];
|
|
323
|
+
const timeToFileMap = {};
|
|
324
|
+
processedFiles.forEach((file) => {
|
|
325
|
+
const unixTime = calculateUnixTimeFromSatelliteKey(file);
|
|
326
|
+
if (unixTime > 0) {
|
|
327
|
+
unixTimes.push(unixTime);
|
|
328
|
+
timeToFileMap[unixTime] = file;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Chronological (oldest → newest): matches standard time sliders (earlier left, later right).
|
|
333
|
+
unixTimes.sort((a, b) => a - b);
|
|
334
|
+
|
|
335
|
+
return { unixTimes, fileList: processedFiles, timeToFileMap };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
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 }>}
|
|
342
|
+
*/
|
|
343
|
+
export function getAllGoesEastSatelliteSelections(satelliteInstrumentId = 'GOES19-EAST') {
|
|
344
|
+
const out = [];
|
|
345
|
+
for (const sector of GOES_EAST_SATELLITE_SECTORS) {
|
|
346
|
+
for (const ch of GOES_SATELLITE_CHANNELS) {
|
|
347
|
+
out.push({
|
|
348
|
+
satelliteInstrumentId,
|
|
349
|
+
satelliteSectorLabel: sector,
|
|
350
|
+
satelliteChannel: ch,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* @param {string} satelliteChannel — band or RGB id (third segment of the archive descriptor)
|
|
359
|
+
*/
|
|
360
|
+
export function resolveSatelliteS3FileName(satelliteChannel, timeToFileMap, satelliteTimestamp) {
|
|
361
|
+
const fileFromMap = timeToFileMap?.[satelliteTimestamp];
|
|
362
|
+
if (fileFromMap) return fileFromMap;
|
|
363
|
+
const channelName = satelliteChannel;
|
|
364
|
+
const channelNameUpper = String(channelName).toUpperCase();
|
|
365
|
+
let name = fileFromMap || '';
|
|
366
|
+
if (!name && satelliteTimestamp != null) {
|
|
367
|
+
const suffix = `_${String(Math.floor(Number(satelliteTimestamp)))}`;
|
|
368
|
+
const multiName = Object.values(timeToFileMap || {}).find((f) => f.includes('MULTI') && f.includes(suffix));
|
|
369
|
+
if (multiName) {
|
|
370
|
+
name = multiName.replace('MULTI', channelNameUpper);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return name;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export { SECTOR_CODE_MAP };
|