@aguacerowx/mapsgl 0.0.46 → 0.0.47
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/basis/basis_transcoder.js +24 -0
- package/basis/basis_transcoder.wasm +0 -0
- package/index.js +3 -1
- package/package.json +3 -2
- package/src/NwsWatchesWarningsOverlay.js +7 -3
- package/src/SatelliteShaderManager.js +2 -1
- package/src/WeatherLayerManager.js +1540 -1536
- package/src/defaultBasisBaseUrl.js +14 -0
- package/src/nwsAlertsSupport.js +1156 -1111
- package/src/nwsWarningCustomizationKey.gen.js +3 -6
- package/src/satelliteKtxWorker.js +7 -0
|
@@ -1,1537 +1,1541 @@
|
|
|
1
|
-
// index.js - The main entry point for the @aguacerowx/web SDK
|
|
2
|
-
|
|
3
|
-
import { AguaceroCore, EventEmitter, DICTIONARIES, resolveSatelliteS3FileName } from '@aguacerowx/javascript-sdk';
|
|
4
|
-
import { GridRenderLayer } from './GridRenderLayer.js'; // <-- IMPORT THE WEB RENDERER!
|
|
5
|
-
import { SatelliteShaderManager } from './SatelliteShaderManager.js';
|
|
6
|
-
import { NexradWeatherController } from './NexradWeatherController.js';
|
|
7
|
-
import { NexradSitesOverlay } from './NexradSitesOverlay.js';
|
|
8
|
-
import { NwsWatchesWarningsOverlay } from './NwsWatchesWarningsOverlay.js';
|
|
9
|
-
import WorkerPool from './WorkerPool.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (typeof
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* @param {
|
|
74
|
-
* @param {
|
|
75
|
-
* @param {string} [options.
|
|
76
|
-
* @param {string} [options.
|
|
77
|
-
* @param {string} [options.
|
|
78
|
-
* @param {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.
|
|
103
|
-
this.
|
|
104
|
-
this.
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
this.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.
|
|
113
|
-
|
|
114
|
-
this.
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
this.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
this.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
this.
|
|
128
|
-
this.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
this.
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
this.
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this.
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
this.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
this.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
*
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
*
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
this.
|
|
347
|
-
|
|
348
|
-
this.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
this.
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
* @param {
|
|
389
|
-
* @param {
|
|
390
|
-
* @param {
|
|
391
|
-
* @param {
|
|
392
|
-
* @param {
|
|
393
|
-
* @param {
|
|
394
|
-
* @param {Record<string,
|
|
395
|
-
* @param {string} [partial.nwsAlertSettings.
|
|
396
|
-
* @param {
|
|
397
|
-
* @param {string
|
|
398
|
-
* @param {string} [partial.
|
|
399
|
-
* @param {string
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
timelineUnix = state.
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (this.
|
|
496
|
-
this.
|
|
497
|
-
this.
|
|
498
|
-
}
|
|
499
|
-
this.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
this.
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
this.
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
this.
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
this
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
* @param {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if (
|
|
634
|
-
this.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
this.
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
)
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
let
|
|
687
|
-
if (!
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (this.
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
timestamps.
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
const
|
|
743
|
-
if (!
|
|
744
|
-
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
this.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
:
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
this.
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
this.
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
async
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
async
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
/**
|
|
879
|
-
async
|
|
880
|
-
return this.core.
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
async
|
|
887
|
-
return this.core.
|
|
888
|
-
}
|
|
889
|
-
/** @param {'level2'|'level3'}
|
|
890
|
-
async
|
|
891
|
-
return this.core.
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
.
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
//
|
|
1064
|
-
// mrmsTimestamp
|
|
1065
|
-
//
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
this.
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
if (
|
|
1206
|
-
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
if (
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
if (!
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
this.shaderLayer.
|
|
1301
|
-
this.shaderLayer.
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
if (
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
const
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
//
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
const
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
this.
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
//
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1
|
+
// index.js - The main entry point for the @aguacerowx/web SDK
|
|
2
|
+
|
|
3
|
+
import { AguaceroCore, EventEmitter, DICTIONARIES, resolveSatelliteS3FileName } from '@aguacerowx/javascript-sdk';
|
|
4
|
+
import { GridRenderLayer } from './GridRenderLayer.js'; // <-- IMPORT THE WEB RENDERER!
|
|
5
|
+
import { SatelliteShaderManager } from './SatelliteShaderManager.js';
|
|
6
|
+
import { NexradWeatherController } from './NexradWeatherController.js';
|
|
7
|
+
import { NexradSitesOverlay } from './NexradSitesOverlay.js';
|
|
8
|
+
import { NwsWatchesWarningsOverlay } from './NwsWatchesWarningsOverlay.js';
|
|
9
|
+
import WorkerPool from './WorkerPool.js';
|
|
10
|
+
|
|
11
|
+
import { DEFAULT_BASIS_BASE_URL } from './defaultBasisBaseUrl.js';
|
|
12
|
+
|
|
13
|
+
const DEBUG_NS = '[WeatherLayerManager:debug]';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Redact secrets for console output.
|
|
17
|
+
* @param {object} options
|
|
18
|
+
*/
|
|
19
|
+
function _debugSanitizeOptions(options) {
|
|
20
|
+
if (!options || typeof options !== 'object') return options;
|
|
21
|
+
const o = { ...options };
|
|
22
|
+
if (typeof o.apiKey === 'string' && o.apiKey.length > 0) {
|
|
23
|
+
o.apiKey = `${o.apiKey.slice(0, 4)}…(${o.apiKey.length} chars)`;
|
|
24
|
+
}
|
|
25
|
+
return o;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compact summary of timestamp / hour arrays (MRMS timeline debugging).
|
|
30
|
+
* @param {unknown[]} arr
|
|
31
|
+
*/
|
|
32
|
+
function _debugSummarizeNumericSeries(arr) {
|
|
33
|
+
if (!Array.isArray(arr) || arr.length === 0) {
|
|
34
|
+
return { length: 0, min: null, max: null, spanSec: null, head: [], tail: undefined };
|
|
35
|
+
}
|
|
36
|
+
const nums = arr.map((x) => Number(x)).filter((n) => !Number.isNaN(n));
|
|
37
|
+
if (nums.length === 0) {
|
|
38
|
+
return { length: 0, min: null, max: null, spanSec: null, head: [], tail: undefined };
|
|
39
|
+
}
|
|
40
|
+
const sorted = [...nums].sort((a, b) => a - b);
|
|
41
|
+
const min = sorted[0];
|
|
42
|
+
const max = sorted[sorted.length - 1];
|
|
43
|
+
const head = sorted.slice(0, Math.min(8, sorted.length));
|
|
44
|
+
const tail = sorted.length > 16 ? sorted.slice(-8) : [];
|
|
45
|
+
return {
|
|
46
|
+
length: sorted.length,
|
|
47
|
+
min,
|
|
48
|
+
max,
|
|
49
|
+
spanSec: max != null && min != null ? max - min : null,
|
|
50
|
+
head,
|
|
51
|
+
tail: tail.length ? tail : undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function findLatestModelRun(modelsData, modelName) {
|
|
56
|
+
const model = modelsData?.[modelName];
|
|
57
|
+
if (!model) return null;
|
|
58
|
+
const availableDates = Object.keys(model).sort((a, b) => b.localeCompare(a));
|
|
59
|
+
for (const date of availableDates) {
|
|
60
|
+
const runs = model[date];
|
|
61
|
+
if (!runs) continue;
|
|
62
|
+
const availableRuns = Object.keys(runs).sort((a, b) => b.localeCompare(a));
|
|
63
|
+
if (availableRuns.length > 0) return { date: date, run: availableRuns[0] };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The WeatherLayerManager is the main class for the Aguacero Web SDK.
|
|
70
|
+
* It acts as a "controller" that connects the headless AguaceroCore engine
|
|
71
|
+
* to a visual Mapbox GL JS map instance.
|
|
72
|
+
*
|
|
73
|
+
* @param {import('mapbox-gl').Map} map
|
|
74
|
+
* @param {object} [options]
|
|
75
|
+
* @param {string} [options.layerId] - Mapbox **custom layer** id for model/MRMS grid (default: random `weather-layer-*`). Alias of `id`.
|
|
76
|
+
* @param {string} [options.id] - Same as `layerId` if `layerId` is omitted.
|
|
77
|
+
* @param {string} [options.belowID] - Style layer id to insert Aguacero weather layers **below** (default `AML_-_terrain` when present). Alias of `weatherBeforeLayerId`.
|
|
78
|
+
* @param {string} [options.weatherBeforeLayerId] - Same as `belowID`.
|
|
79
|
+
* @param {string} [options.nexradLayerId] - Override Mapbox id for the NEXRAD custom layer (default: derived from `layerId`).
|
|
80
|
+
* @param {boolean} [options.debug] - When `true`, logs detailed diagnostics to the console (prefix `[WeatherLayerManager:debug]`). Off by default.
|
|
81
|
+
* @param {string} [options.basisBaseUrl] - URL prefix for satellite KTX2 Basis transcoder assets (`basis_transcoder.js`, `basis_transcoder.wasm`). Defaults to jsDelivr for the published package version; override (e.g. `/basis/`) for strict CSP or offline.
|
|
82
|
+
*/
|
|
83
|
+
export class WeatherLayerManager extends EventEmitter {
|
|
84
|
+
constructor(map, options = {}) {
|
|
85
|
+
super();
|
|
86
|
+
if (!map) throw new Error('A Mapbox GL map instance is required.');
|
|
87
|
+
this.map = map;
|
|
88
|
+
/** @private When true, emit verbose `[WeatherLayerManager:debug]` logs. */
|
|
89
|
+
this._debug = options.debug === true;
|
|
90
|
+
/** @private Monotonic counter for correlating state:change logs. */
|
|
91
|
+
this._debugStateSeq = 0;
|
|
92
|
+
this.layerId =
|
|
93
|
+
options.layerId ||
|
|
94
|
+
options.id ||
|
|
95
|
+
`weather-layer-${Math.random().toString(36).substr(2, 9)}`;
|
|
96
|
+
/** Mapbox id for the NEXRAD custom layer (derived from `layerId` unless `nexradLayerId` is set). */
|
|
97
|
+
this._nexradLayerId =
|
|
98
|
+
options.nexradLayerId ||
|
|
99
|
+
(this.layerId.includes('weather-layer')
|
|
100
|
+
? this.layerId.replace('weather-layer', 'nexrad-layer')
|
|
101
|
+
: `${this.layerId}-nexrad`);
|
|
102
|
+
this.shaderLayer = null;
|
|
103
|
+
this.currentLoadedTimeKey = null;
|
|
104
|
+
this.autoRefreshEnabled = options.autoRefresh ?? false;
|
|
105
|
+
this.autoRefreshIntervalSeconds = options.autoRefreshInterval ?? 60; // Default to 30 seconds
|
|
106
|
+
this.autoRefreshIntervalId = null;
|
|
107
|
+
this.currentRunKey = null;
|
|
108
|
+
this.currentRebuildId = 0;
|
|
109
|
+
/** True while awaiting the first _loadGridData for the current shader layer rebuild */
|
|
110
|
+
this._initialGridLoadPending = false;
|
|
111
|
+
/** Timestep (forecast hour or MRMS ts) that the in-flight rebuild is fetching */
|
|
112
|
+
this._rebuildTargetTimeKey = null;
|
|
113
|
+
|
|
114
|
+
this.satelliteLayerId = options.satelliteLayerId || 'aguacero-satellite-layer';
|
|
115
|
+
this.basisBaseUrl = options.basisBaseUrl ?? DEFAULT_BASIS_BASE_URL;
|
|
116
|
+
this.satelliteLayer = null;
|
|
117
|
+
this.satelliteWorkerPool = null;
|
|
118
|
+
this._satelliteRunKey = null;
|
|
119
|
+
/** Prevents duplicate background fetches for the same satellite timeline (MRMS-style preload). */
|
|
120
|
+
this._satellitePreloadedTimelineId = null;
|
|
121
|
+
/** Tracks MRMS timeline window so expanding hours triggers background preload of new frames. */
|
|
122
|
+
this._prevMrmsDurationValue = undefined;
|
|
123
|
+
|
|
124
|
+
/** Optional satellite colormap overrides (flat `[norm, '#hex', ...]` arrays, norm 0–100). */
|
|
125
|
+
this._satelliteColormap = options.satelliteColormap ?? null;
|
|
126
|
+
this._satelliteColormapIR = options.satelliteColormapIR ?? null;
|
|
127
|
+
this._satelliteColormapWV = options.satelliteColormapWV ?? null;
|
|
128
|
+
this._interpolateSatelliteColormap = options.interpolateSatelliteColormap !== false;
|
|
129
|
+
|
|
130
|
+
this._nexradController = null;
|
|
131
|
+
this._nexradSites = null;
|
|
132
|
+
this._nexradSitesBound = false;
|
|
133
|
+
/** Override URL for the NEXRAD site list (default: `/data/nexrad.json` — serve from app `public/data/`). */
|
|
134
|
+
this._nexradSitesUrl = options.nexradSitesUrl ?? null;
|
|
135
|
+
|
|
136
|
+
/** Latest payload from {@link AguaceroCore} `state:change` (includes `colormap` for NEXRAD readouts). */
|
|
137
|
+
this._lastEmittedState = null;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Optional NWS watches/warnings overlay (all alert types by default; optional user allowlist).
|
|
141
|
+
* See {@link configureWatchesWarnings}; not a separate “data mode” — overlays satellite / MRMS / NEXRAD only.
|
|
142
|
+
*/
|
|
143
|
+
this._watchesWarningsOptions = {
|
|
144
|
+
alertInteractionEnabled: true,
|
|
145
|
+
...(options.watchesWarnings ?? {}),
|
|
146
|
+
};
|
|
147
|
+
this._nwsOverlay = null;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Style layer id to insert custom weather layers below when using a non-Aguacero Mapbox style
|
|
151
|
+
* (set via {@link MapManager}, or pass `belowID` / `weatherBeforeLayerId` here).
|
|
152
|
+
*/
|
|
153
|
+
this._weatherBeforeLayerId =
|
|
154
|
+
options.belowID ??
|
|
155
|
+
options.weatherBeforeLayerId ??
|
|
156
|
+
(map && map.__aguaceroMapsgl && map.__aguaceroMapsgl.weatherBeforeLayerId) ??
|
|
157
|
+
null;
|
|
158
|
+
|
|
159
|
+
// 1. CREATE an instance of the core engine
|
|
160
|
+
this.core = new AguaceroCore(options);
|
|
161
|
+
|
|
162
|
+
if (this._debug) {
|
|
163
|
+
const layerOpts = options.layerOptions || {};
|
|
164
|
+
console.log(DEBUG_NS, 'constructor', {
|
|
165
|
+
layerId: this.layerId,
|
|
166
|
+
nexradLayerId: this._nexradLayerId,
|
|
167
|
+
mapLoaded: typeof this.map?.loaded === 'function' ? this.map.loaded() : undefined,
|
|
168
|
+
styleLoaded: this.map?.isStyleLoaded?.() ?? undefined,
|
|
169
|
+
weatherBeforeLayerId: this._weatherBeforeLayerId,
|
|
170
|
+
options: _debugSanitizeOptions(options),
|
|
171
|
+
layerOptions: layerOpts,
|
|
172
|
+
coreStateSnapshot: {
|
|
173
|
+
isMRMS: this.core.state?.isMRMS,
|
|
174
|
+
isSatellite: this.core.state?.isSatellite,
|
|
175
|
+
isNexrad: this.core.state?.isNexrad,
|
|
176
|
+
model: this.core.state?.model,
|
|
177
|
+
variable: this.core.state?.variable,
|
|
178
|
+
mrmsDurationValue: this.core.state?.mrmsDurationValue,
|
|
179
|
+
mrmsTimestamp: this.core.state?.mrmsTimestamp,
|
|
180
|
+
satelliteDurationValue: this.core.state?.satelliteDurationValue,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2. LISTEN for events from the core engine
|
|
186
|
+
this.core.on('state:change', (newState) => {
|
|
187
|
+
this._lastEmittedState = newState;
|
|
188
|
+
// When the core's state changes, this controller's job
|
|
189
|
+
// is to update the visual map layers accordingly.
|
|
190
|
+
this._handleStateChange(newState);
|
|
191
|
+
|
|
192
|
+
// Re-emit the state:change event for the end-user
|
|
193
|
+
this.emit('state:change', newState);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Re-emit the data inspection event for the end-user
|
|
197
|
+
this.core.on('data:inspect', (payload) => this.emit('data:inspect', payload));
|
|
198
|
+
|
|
199
|
+
// 3. BIND the map's mousemove event to call the core's utility method
|
|
200
|
+
this._handleMouseMove = this._handleMouseMove.bind(this);
|
|
201
|
+
this.map.on('mousemove', this._handleMouseMove);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Structured debug log (no-op unless `options.debug === true` on construction).
|
|
206
|
+
* @param {string} scope
|
|
207
|
+
* @param {object} [data]
|
|
208
|
+
*/
|
|
209
|
+
_debugLog(scope, data) {
|
|
210
|
+
if (!this._debug) return;
|
|
211
|
+
if (data !== undefined) {
|
|
212
|
+
console.log(DEBUG_NS, scope, data);
|
|
213
|
+
} else {
|
|
214
|
+
console.log(DEBUG_NS, scope);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Resolves which style layer id to pass as Mapbox `addLayer(..., beforeId)` for satellite / grid / NEXRAD.
|
|
220
|
+
* Prefers an explicit id (custom styles), then the default Aguacero `AML_-_terrain` anchor when present.
|
|
221
|
+
* @returns {string | undefined}
|
|
222
|
+
*/
|
|
223
|
+
_weatherInsertBeforeId() {
|
|
224
|
+
const tryId = (id) => {
|
|
225
|
+
if (!id || typeof id !== 'string') {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
return this.map.getLayer(id) ? id : undefined;
|
|
230
|
+
} catch {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
return tryId(this._weatherBeforeLayerId) || tryId('AML_-_terrain') || undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Lng/lat under the cursor — prefer `map.unproject(e.point)` (aguacero-frontend parity) so readouts
|
|
239
|
+
* align with custom layers / Web Mercator.
|
|
240
|
+
* @param {import('mapbox-gl').MapMouseEvent | { lngLat: { lng: number; lat: number }; point: import('mapbox-gl').Point }} e
|
|
241
|
+
* @returns {{ lng: number; lat: number }}
|
|
242
|
+
*/
|
|
243
|
+
_mouseLngLatFromEvent(e) {
|
|
244
|
+
try {
|
|
245
|
+
if (this.map && typeof this.map.unproject === 'function') {
|
|
246
|
+
const u = this.map.unproject(e.point);
|
|
247
|
+
if (Number.isFinite(u.lng) && Number.isFinite(u.lat)) {
|
|
248
|
+
return { lng: u.lng, lat: u.lat };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
/* style or map not ready */
|
|
253
|
+
}
|
|
254
|
+
return { lng: e.lngLat.lng, lat: e.lngLat.lat };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The main visual controller. It receives the new state from the core
|
|
259
|
+
* and decides what visual update is needed.
|
|
260
|
+
* @param {object} state - The new state object from AguaceroCore.
|
|
261
|
+
* @private
|
|
262
|
+
*/
|
|
263
|
+
_handleStateChange(state) {
|
|
264
|
+
const seq = this._debug ? ++this._debugStateSeq : 0;
|
|
265
|
+
/** NEXRAD setup is async (`sites.show`, frame fetch). `finally` would run before the radar layer exists, so NWS fill stacks above terrain instead of under the custom layer — defer sync until the handler settles. */
|
|
266
|
+
let deferNwsSyncUntilNexradReady = false;
|
|
267
|
+
try {
|
|
268
|
+
if (this._debug) {
|
|
269
|
+
const tsSummary = state.isMRMS
|
|
270
|
+
? _debugSummarizeNumericSeries(state.availableTimestamps || [])
|
|
271
|
+
: null;
|
|
272
|
+
this._debugLog('state:change', {
|
|
273
|
+
seq,
|
|
274
|
+
mode: state.isSatellite
|
|
275
|
+
? 'satellite'
|
|
276
|
+
: state.isNexrad
|
|
277
|
+
? 'nexrad'
|
|
278
|
+
: state.isMRMS
|
|
279
|
+
? 'mrms'
|
|
280
|
+
: 'model',
|
|
281
|
+
model: state.model,
|
|
282
|
+
variable: state.variable,
|
|
283
|
+
runKey: `${state.model}-${state.date}-${state.run}-${state.variable}`,
|
|
284
|
+
timeKey: state.isMRMS
|
|
285
|
+
? (state.mrmsTimestamp == null ? null : Number(state.mrmsTimestamp))
|
|
286
|
+
: Number(state.forecastHour),
|
|
287
|
+
mrmsTimestamp: state.mrmsTimestamp,
|
|
288
|
+
mrmsDurationValue: state.mrmsDurationValue,
|
|
289
|
+
availableTimestampsSummary: tsSummary,
|
|
290
|
+
availableHoursLen: Array.isArray(state.availableHours) ? state.availableHours.length : 0,
|
|
291
|
+
shaderLayerRunKey: this.shaderLayer?.runKey ?? null,
|
|
292
|
+
currentLoadedTimeKey: this.currentLoadedTimeKey,
|
|
293
|
+
rebuildId: this.currentRebuildId,
|
|
294
|
+
initialGridLoadPending: this._initialGridLoadPending,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (state.isSatellite) {
|
|
299
|
+
this._handleSatelliteStateChange(state);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this._tearDownSatellite();
|
|
304
|
+
|
|
305
|
+
if (state.isNexrad) {
|
|
306
|
+
this._tearDownShaderGrid();
|
|
307
|
+
deferNwsSyncUntilNexradReady = true;
|
|
308
|
+
void this._handleNexradStateChange(state).finally(() => {
|
|
309
|
+
const st = this._lastEmittedState ?? state;
|
|
310
|
+
this._syncNwsOverlay(st);
|
|
311
|
+
});
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this._tearDownNexrad();
|
|
316
|
+
|
|
317
|
+
const prevMrmsDur = this._prevMrmsDurationValue;
|
|
318
|
+
this._prevMrmsDurationValue = state.mrmsDurationValue;
|
|
319
|
+
const mrmsDurationChanged =
|
|
320
|
+
state.isMRMS && prevMrmsDur !== undefined && prevMrmsDur !== state.mrmsDurationValue;
|
|
321
|
+
|
|
322
|
+
const timeKey = state.isMRMS
|
|
323
|
+
? (state.mrmsTimestamp == null ? null : Number(state.mrmsTimestamp))
|
|
324
|
+
: Number(state.forecastHour);
|
|
325
|
+
const runKey = `${state.model}-${state.date}-${state.run}-${state.variable}`;
|
|
326
|
+
|
|
327
|
+
if (this._debug && state.isMRMS) {
|
|
328
|
+
this._debugLog('grid path (mrms/model)', {
|
|
329
|
+
seq,
|
|
330
|
+
prevMrmsDurationValue: prevMrmsDur,
|
|
331
|
+
mrmsDurationChanged,
|
|
332
|
+
willRebuild: !this.shaderLayer || this.shaderLayer.runKey !== runKey,
|
|
333
|
+
willUpdateData:
|
|
334
|
+
this.shaderLayer &&
|
|
335
|
+
this.shaderLayer.runKey === runKey &&
|
|
336
|
+
this.currentLoadedTimeKey !== timeKey,
|
|
337
|
+
shaderRunKey: this.shaderLayer?.runKey,
|
|
338
|
+
targetRunKey: runKey,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!this.shaderLayer || this.shaderLayer.runKey !== runKey) {
|
|
343
|
+
this._rebuildLayerAndPreload(state);
|
|
344
|
+
} else if (this.currentLoadedTimeKey !== timeKey) {
|
|
345
|
+
const duplicateBeforeFirstPaint =
|
|
346
|
+
this.currentLoadedTimeKey === null &&
|
|
347
|
+
this._initialGridLoadPending &&
|
|
348
|
+
timeKey === this._rebuildTargetTimeKey;
|
|
349
|
+
if (!duplicateBeforeFirstPaint) {
|
|
350
|
+
this._updateLayerData(state);
|
|
351
|
+
} else if (this._debug) {
|
|
352
|
+
this._debugLog('skip _updateLayerData (duplicate before first paint)', {
|
|
353
|
+
seq,
|
|
354
|
+
timeKey,
|
|
355
|
+
_rebuildTargetTimeKey: this._rebuildTargetTimeKey,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (this.shaderLayer) {
|
|
361
|
+
this.shaderLayer.updateStyle({ opacity: state.opacity });
|
|
362
|
+
this.shaderLayer.setSmoothing(!state.shaderSmoothingEnabled);
|
|
363
|
+
this.map.triggerRepaint();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (state.isMRMS && this.shaderLayer && mrmsDurationChanged) {
|
|
367
|
+
if (this._debug) {
|
|
368
|
+
this._debugLog('_preloadAllTimeSteps (mrms duration changed)', {
|
|
369
|
+
seq,
|
|
370
|
+
from: prevMrmsDur,
|
|
371
|
+
to: state.mrmsDurationValue,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
this._preloadAllTimeSteps(state);
|
|
375
|
+
}
|
|
376
|
+
} finally {
|
|
377
|
+
if (!deferNwsSyncUntilNexradReady) {
|
|
378
|
+
this._syncNwsOverlay(state);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* NWS watches/warnings: same NWWS feed as aguacero-frontend, overlaid on satellite / MRMS / NEXRAD only
|
|
385
|
+
* (hidden on model grids). By default `nwsAlertSettings.alertScope` is `'all'`. Use `'user'` with
|
|
386
|
+
* `includedAlerts` (exact NWS `event_name` strings) to show only selected products.
|
|
387
|
+
*
|
|
388
|
+
* @param {object} partial
|
|
389
|
+
* @param {boolean} [partial.enabled] - Master switch; default false.
|
|
390
|
+
* @param {string} [partial.alertsBaseUrl] - API root (default `https://api.aguacerowx.com`).
|
|
391
|
+
* @param {boolean} [partial.alertInteractionEnabled=true] - When true, map fires `nws:alert:click` with alert details.
|
|
392
|
+
* @param {boolean} [partial.activeOnlyRealtime=false] - Match frontend “active only (realtime)” time filter.
|
|
393
|
+
* @param {object} [partial.nwsAlertSettings]
|
|
394
|
+
* @param {Record<string, string>} [partial.nwsAlertSettings.colors] - Fill color (`#rrggbb`) per `event_name` (same names as NWS / API).
|
|
395
|
+
* @param {Record<string, boolean>} [partial.nwsAlertSettings.fillHidden] - `true` = no fill for that product.
|
|
396
|
+
* @param {Record<string, boolean>} [partial.nwsAlertSettings.lineHidden] - `true` = no outline for that product.
|
|
397
|
+
* @param {Record<string, number>} [partial.nwsAlertSettings.fillOpacity] - 0–1 fill opacity multiplier per product (when fill is shown).
|
|
398
|
+
* @param {Record<string, object>} [partial.nwsAlertSettings.lineStyles] - Per-product line overrides (`innerColor`, `outerColor`, widths, `lineDash`, …).
|
|
399
|
+
* @param {string} [partial.nwsAlertSettings.lineDash] - Default dash style when a product has no `lineStyles` entry.
|
|
400
|
+
* @param {'all'|'user'} [partial.nwsAlertSettings.alertScope] - `all` (default): every alert with a resolved name. `user`: only `includedAlerts`.
|
|
401
|
+
* @param {string[]} [partial.nwsAlertSettings.includedAlerts] - When `alertScope` is `'user'`, list of exact NWS `event_name` values to render (optional colors per name in `colors`).
|
|
402
|
+
* @param {string} [partial.lineBeforeLayerId] - Mapbox style layer id to insert NWS **lines** below (default `AML_-_states`). Override if your style uses different ids.
|
|
403
|
+
* @param {string | null} [partial.fillBeforeLayerId] - Optional manual override: layer id NWS **fill** is inserted under. When omitted, placement uses the active weather layer and `AML_-_states` so fill stays under whichever is lower in the style.
|
|
404
|
+
*/
|
|
405
|
+
configureWatchesWarnings(partial) {
|
|
406
|
+
this._watchesWarningsOptions = { ...this._watchesWarningsOptions, ...partial };
|
|
407
|
+
const st = this._lastEmittedState ?? this.core?.state;
|
|
408
|
+
if (st) {
|
|
409
|
+
this._syncNwsOverlay(st);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
_syncNwsOverlay(state) {
|
|
414
|
+
const wopt = this._watchesWarningsOptions ?? {};
|
|
415
|
+
const masterEnabled = wopt.enabled === true;
|
|
416
|
+
const isModelMode = !state.isSatellite && !state.isNexrad && !state.isMRMS;
|
|
417
|
+
const enabled = masterEnabled && !isModelMode;
|
|
418
|
+
|
|
419
|
+
let timelineUnix = null;
|
|
420
|
+
if (state.isSatellite) {
|
|
421
|
+
timelineUnix = state.satelliteTimestamp == null ? null : Number(state.satelliteTimestamp);
|
|
422
|
+
} else if (state.isNexrad) {
|
|
423
|
+
timelineUnix = state.nexradTimestamp == null ? null : Number(state.nexradTimestamp);
|
|
424
|
+
} else if (state.isMRMS) {
|
|
425
|
+
timelineUnix = state.mrmsTimestamp == null ? null : Number(state.mrmsTimestamp);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!this._nwsOverlay) {
|
|
429
|
+
this._nwsOverlay = new NwsWatchesWarningsOverlay(this.map, {
|
|
430
|
+
...wopt,
|
|
431
|
+
enabled: false,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const weatherLayerId = this._resolveNwsFillBeforeLayerId(state);
|
|
436
|
+
const fillBeforeLayerId = Object.prototype.hasOwnProperty.call(wopt, 'fillBeforeLayerId')
|
|
437
|
+
? wopt.fillBeforeLayerId ?? null
|
|
438
|
+
: null;
|
|
439
|
+
|
|
440
|
+
this._nwsOverlay.updateOptions({
|
|
441
|
+
alertsBaseUrl: wopt.alertsBaseUrl,
|
|
442
|
+
alertInteractionEnabled: wopt.alertInteractionEnabled,
|
|
443
|
+
deltaDebounceMs: wopt.deltaDebounceMs,
|
|
444
|
+
fillOpacity: wopt.fillOpacity,
|
|
445
|
+
lineOpacity: wopt.lineOpacity,
|
|
446
|
+
lineWidth: wopt.lineWidth,
|
|
447
|
+
weatherLayerId,
|
|
448
|
+
fillBeforeLayerId,
|
|
449
|
+
lineBeforeLayerId: wopt.lineBeforeLayerId,
|
|
450
|
+
nwsAlertSettings: {
|
|
451
|
+
...(wopt.nwsAlertSettings ?? {}),
|
|
452
|
+
...(wopt.activeOnlyRealtime !== undefined ? { activeOnlyRealtime: wopt.activeOnlyRealtime } : {}),
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
this._nwsOverlay.syncWithMode({ enabled, timelineUnix });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Layer id for the active satellite / MRMS / NEXRAD custom layer (used with `AML_-_states` for NWS fill stacking).
|
|
460
|
+
* @param {object} state
|
|
461
|
+
* @returns {string | null}
|
|
462
|
+
*/
|
|
463
|
+
_resolveNwsFillBeforeLayerId(state) {
|
|
464
|
+
try {
|
|
465
|
+
if (state.isSatellite && this.satelliteLayer?.id && this.map.getLayer(this.satelliteLayer.id)) {
|
|
466
|
+
return this.satelliteLayer.id;
|
|
467
|
+
}
|
|
468
|
+
if (state.isMRMS && this.shaderLayer?.id && this.map.getLayer(this.shaderLayer.id)) {
|
|
469
|
+
return this.shaderLayer.id;
|
|
470
|
+
}
|
|
471
|
+
if (state.isNexrad && this._nexradController?.mapboxLayer?.id && this.map.getLayer(this._nexradController.mapboxLayer.id)) {
|
|
472
|
+
return this._nexradController.mapboxLayer.id;
|
|
473
|
+
}
|
|
474
|
+
} catch {
|
|
475
|
+
/* map not ready */
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
_tearDownShaderGrid() {
|
|
481
|
+
if (!this.shaderLayer) return;
|
|
482
|
+
if (this.map.getLayer(this.shaderLayer.id)) {
|
|
483
|
+
this.map.removeLayer(this.shaderLayer.id);
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
this.shaderLayer.onRemove?.();
|
|
487
|
+
} catch {
|
|
488
|
+
/* ignore */
|
|
489
|
+
}
|
|
490
|
+
this.shaderLayer = null;
|
|
491
|
+
this.currentLoadedTimeKey = null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
_tearDownNexrad() {
|
|
495
|
+
if (this._nexradController) {
|
|
496
|
+
this._nexradController.destroy();
|
|
497
|
+
this._nexradController = null;
|
|
498
|
+
}
|
|
499
|
+
if (this._nexradSites) {
|
|
500
|
+
this._nexradSites.destroy();
|
|
501
|
+
this._nexradSites = null;
|
|
502
|
+
}
|
|
503
|
+
this._nexradSitesBound = false;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async _handleNexradStateChange(state) {
|
|
507
|
+
if (!this._nexradController) {
|
|
508
|
+
this._nexradController = new NexradWeatherController(this.map, this.core, {
|
|
509
|
+
nexradLayerId: this._nexradLayerId,
|
|
510
|
+
interpolateNexradColormap: this.core.state?.shaderSmoothingEnabled !== false,
|
|
511
|
+
getInsertBeforeLayerId: () => this._weatherInsertBeforeId(),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const showSiteMarkers = state.nexradShowSitesPicker !== false;
|
|
516
|
+
if (showSiteMarkers) {
|
|
517
|
+
if (!this._nexradSites) {
|
|
518
|
+
this._nexradSites = new NexradSitesOverlay(this.map, {
|
|
519
|
+
nexradSitesUrl: this._nexradSitesUrl || undefined,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
await this._nexradSites.show();
|
|
523
|
+
if (!this._nexradSitesBound) {
|
|
524
|
+
this._nexradSites.bindClick((siteId) => {
|
|
525
|
+
void this.core.setNexradSite(siteId);
|
|
526
|
+
});
|
|
527
|
+
this._nexradSitesBound = true;
|
|
528
|
+
}
|
|
529
|
+
} else if (this._nexradSites) {
|
|
530
|
+
this._nexradSites.destroy();
|
|
531
|
+
this._nexradSites = null;
|
|
532
|
+
this._nexradSitesBound = false;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
this._nexradController.preloadAllAvailable(state);
|
|
536
|
+
await this._nexradController.sync(state);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
_tearDownSatellite() {
|
|
540
|
+
if (!this.satelliteLayer) return;
|
|
541
|
+
if (this.map.getLayer(this.satelliteLayer.id)) {
|
|
542
|
+
this.map.removeLayer(this.satelliteLayer.id);
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
this.satelliteLayer.onRemove?.();
|
|
546
|
+
} catch {
|
|
547
|
+
/* ignore */
|
|
548
|
+
}
|
|
549
|
+
this.satelliteLayer = null;
|
|
550
|
+
if (this.satelliteWorkerPool) {
|
|
551
|
+
this.satelliteWorkerPool.terminate();
|
|
552
|
+
this.satelliteWorkerPool = null;
|
|
553
|
+
}
|
|
554
|
+
this._satelliteRunKey = null;
|
|
555
|
+
this._satellitePreloadedTimelineId = null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
_handleSatelliteStateChange(state) {
|
|
559
|
+
this._tearDownNexrad();
|
|
560
|
+
if (this.shaderLayer) {
|
|
561
|
+
if (this.map.getLayer(this.shaderLayer.id)) {
|
|
562
|
+
this.map.removeLayer(this.shaderLayer.id);
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
this.shaderLayer.onRemove?.();
|
|
566
|
+
} catch {
|
|
567
|
+
/* ignore */
|
|
568
|
+
}
|
|
569
|
+
this.shaderLayer = null;
|
|
570
|
+
this.currentLoadedTimeKey = null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (!state.satelliteKey || state.satelliteTimestamp == null) {
|
|
574
|
+
this._tearDownSatellite();
|
|
575
|
+
this.map.triggerRepaint();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const satRunKey = `${state.satelliteKey}|${state.variable || ''}`;
|
|
580
|
+
if (!this.satelliteLayer || this._satelliteRunKey !== satRunKey) {
|
|
581
|
+
this._rebuildSatelliteLayer(state, satRunKey);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Preload the full timeline before syncing the visible frame. Slider / timestamp changes must never
|
|
585
|
+
// trigger fetches — only this preload pass (and listing refresh) may load KTX2 data.
|
|
586
|
+
if (this.satelliteLayer && state.satelliteTimeToFileMap) {
|
|
587
|
+
this._preloadAllSatelliteFrames(state);
|
|
588
|
+
}
|
|
589
|
+
this._syncSatelliteFrame(state);
|
|
590
|
+
|
|
591
|
+
if (this.satelliteLayer) {
|
|
592
|
+
this.satelliteLayer.updateLayerSettings(this._buildSatelliteLayerSettings(state));
|
|
593
|
+
this.map.triggerRepaint();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Merge core satellite state with optional colormap overrides for `SatelliteShaderManager`.
|
|
599
|
+
* @param {object} state - Core state snapshot
|
|
600
|
+
*/
|
|
601
|
+
_buildSatelliteLayerSettings(state) {
|
|
602
|
+
return {
|
|
603
|
+
satelliteOpacity: state.opacity ?? 1,
|
|
604
|
+
satelliteVisibility: state.visible !== false,
|
|
605
|
+
fillSmoothing: state.shaderSmoothingEnabled ? 1 : 0,
|
|
606
|
+
interpolateColormap: this._interpolateSatelliteColormap,
|
|
607
|
+
satelliteColormap: this._satelliteColormap ?? undefined,
|
|
608
|
+
satelliteColormapIR: this._satelliteColormapIR ?? undefined,
|
|
609
|
+
satelliteColormapWV: this._satelliteColormapWV ?? undefined,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Set custom IR / WV / legacy colormap stops for the satellite layer. Pass `null` to clear an override.
|
|
615
|
+
* Stops are alternating `[norm0to100, '#rrggbb', ...]` (same encoding as aguacero-frontend satellite colormaps).
|
|
616
|
+
*
|
|
617
|
+
* @param {object} opts
|
|
618
|
+
* @param {number[]|string[]|null} [opts.colormap] - Single list; interpreted using the active frame (IR vs WV).
|
|
619
|
+
* @param {number[]|string[]|null} [opts.colormapIR] - Longwave IR bands (C13–C16).
|
|
620
|
+
* @param {number[]|string[]|null} [opts.colormapWV] - Water vapor bands (C08–C10).
|
|
621
|
+
* @param {boolean} [opts.interpolate=true] - Interpolate between stops when building the 1D LUT texture.
|
|
622
|
+
*/
|
|
623
|
+
setSatelliteColormapOverrides(opts = {}) {
|
|
624
|
+
if (opts.colormap !== undefined) {
|
|
625
|
+
this._satelliteColormap = opts.colormap;
|
|
626
|
+
}
|
|
627
|
+
if (opts.colormapIR !== undefined) {
|
|
628
|
+
this._satelliteColormapIR = opts.colormapIR;
|
|
629
|
+
}
|
|
630
|
+
if (opts.colormapWV !== undefined) {
|
|
631
|
+
this._satelliteColormapWV = opts.colormapWV;
|
|
632
|
+
}
|
|
633
|
+
if (opts.interpolate !== undefined) {
|
|
634
|
+
this._interpolateSatelliteColormap = opts.interpolate;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (this.satelliteLayer && this.core?.state?.isSatellite) {
|
|
638
|
+
this.satelliteLayer.updateLayerSettings(this._buildSatelliteLayerSettings(this.core.state));
|
|
639
|
+
this.map.triggerRepaint();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
_rebuildSatelliteLayer(state, satRunKey) {
|
|
644
|
+
this._tearDownSatellite();
|
|
645
|
+
|
|
646
|
+
// Vite only rewrites workers when it sees `new Worker(new URL(..., import.meta.url))` here — not
|
|
647
|
+
// `super(url)` inside a Worker subclass (that leaves a file: URL and triggers fs allow-list errors).
|
|
648
|
+
this.satelliteWorkerPool = new WorkerPool(
|
|
649
|
+
() => new Worker(new URL('./satelliteKtxWorker.js', import.meta.url)),
|
|
650
|
+
Math.min(4, Math.max(2, Math.floor((typeof navigator !== 'undefined' && navigator.hardwareConcurrency) ? navigator.hardwareConcurrency * 0.5 : 2)))
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
this.satelliteLayer = new SatelliteShaderManager(
|
|
654
|
+
this.satelliteWorkerPool,
|
|
655
|
+
() => {},
|
|
656
|
+
{ basisBaseUrl: this.basisBaseUrl }
|
|
657
|
+
);
|
|
658
|
+
this.satelliteLayer.id = this.satelliteLayerId;
|
|
659
|
+
this._satelliteRunKey = satRunKey;
|
|
660
|
+
|
|
661
|
+
const beforeId = this._weatherInsertBeforeId();
|
|
662
|
+
if (beforeId) {
|
|
663
|
+
this.map.addLayer(this.satelliteLayer, beforeId);
|
|
664
|
+
} else {
|
|
665
|
+
this.map.addLayer(this.satelliteLayer);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
_satelliteTimelineId(state) {
|
|
670
|
+
const keys = Object.keys(state.satelliteTimeToFileMap || {}).sort((a, b) => Number(a) - Number(b));
|
|
671
|
+
return `${state.satelliteKey}|${state.variable || ''}|${keys.join(',')}`;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
_getSatelliteFetchParts(state, satelliteTimestamp) {
|
|
675
|
+
const fileName = resolveSatelliteS3FileName(
|
|
676
|
+
state.satelliteKey,
|
|
677
|
+
state.satelliteTimeToFileMap,
|
|
678
|
+
satelliteTimestamp
|
|
679
|
+
);
|
|
680
|
+
if (!fileName) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
const parts = state.satelliteKey.split('.');
|
|
684
|
+
const channelName = parts[2] || state.variable;
|
|
685
|
+
const channelNameUpper = String(channelName).toUpperCase();
|
|
686
|
+
let s3FileName = fileName.replace('MULTI', channelNameUpper);
|
|
687
|
+
if (!s3FileName.endsWith('.ktx2')) {
|
|
688
|
+
s3FileName += '.ktx2';
|
|
689
|
+
}
|
|
690
|
+
let shaderFileName = s3FileName;
|
|
691
|
+
if (!channelNameUpper.startsWith('C') || channelNameUpper.length > 3) {
|
|
692
|
+
const token = channelNameUpper.toLowerCase().replace(/_/g, '');
|
|
693
|
+
shaderFileName = `${s3FileName}_${token}_`;
|
|
694
|
+
}
|
|
695
|
+
const apiKey = this.core.apiKey;
|
|
696
|
+
if (!apiKey) {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
const uid = this.core.userId || 'sdk-user';
|
|
700
|
+
const url = `https://d3dc62msmxkrd7.cloudfront.net/satellite/${s3FileName}?userId=${encodeURIComponent(uid)}&apiKey=${encodeURIComponent(apiKey)}`;
|
|
701
|
+
return { url, shaderFileName, frameKey: Number(satelliteTimestamp), apiKey };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
_preloadAllSatelliteFrames(state) {
|
|
705
|
+
if (!this.satelliteLayer || !state.satelliteTimeToFileMap) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const timelineId = this._satelliteTimelineId(state);
|
|
709
|
+
if (this._satellitePreloadedTimelineId === timelineId) {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
this._satellitePreloadedTimelineId = timelineId;
|
|
713
|
+
|
|
714
|
+
const runKey = this._satelliteRunKey;
|
|
715
|
+
const timestamps = Object.keys(state.satelliteTimeToFileMap)
|
|
716
|
+
.map((k) => Number(k))
|
|
717
|
+
.filter((t) => !Number.isNaN(t));
|
|
718
|
+
|
|
719
|
+
timestamps.forEach((ts) => {
|
|
720
|
+
if (!this.satelliteLayer || this._satelliteRunKey !== runKey) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (this.satelliteLayer.frames.has(ts)) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
void this._fetchSatelliteFrameAt(state, ts, runKey);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async _fetchSatelliteFrameAt(state, satelliteTimestamp, runKey) {
|
|
731
|
+
const parts = this._getSatelliteFetchParts(state, satelliteTimestamp);
|
|
732
|
+
if (!parts || !this.satelliteLayer) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const { url, shaderFileName, frameKey, apiKey } = parts;
|
|
736
|
+
|
|
737
|
+
if (this.satelliteLayer.frames.has(frameKey)) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
const response = await fetch(url, { headers: { 'x-api-key': apiKey } });
|
|
743
|
+
if (!response.ok) {
|
|
744
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
745
|
+
}
|
|
746
|
+
const buffer = await response.arrayBuffer();
|
|
747
|
+
if (!this.satelliteLayer || this._satelliteRunKey !== runKey) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
this.satelliteLayer.addFrame(new Uint8Array(buffer), frameKey, shaderFileName);
|
|
751
|
+
this.map.triggerRepaint();
|
|
752
|
+
} catch (err) {
|
|
753
|
+
console.warn('[WeatherLayerManager] Satellite frame fetch failed:', err);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Selects which decoded frame to show. Never performs network I/O — preloading owns all fetches.
|
|
759
|
+
* If the frame is not decoded yet, SatelliteShaderManager keeps targetFrameKey until addFrame runs.
|
|
760
|
+
*/
|
|
761
|
+
_syncSatelliteFrame(state) {
|
|
762
|
+
if (!this.satelliteLayer) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (!this.core.apiKey) {
|
|
766
|
+
console.warn('[WeatherLayerManager] apiKey is required to load satellite KTX2 tiles.');
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const parts = this._getSatelliteFetchParts(state, state.satelliteTimestamp);
|
|
771
|
+
if (!parts) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const { frameKey } = parts;
|
|
775
|
+
|
|
776
|
+
this.satelliteLayer.setActiveFrame(frameKey);
|
|
777
|
+
if (this.satelliteLayer.frames.has(frameKey)) {
|
|
778
|
+
this.map.triggerRepaint();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// --- DELEGATION METHODS ---
|
|
783
|
+
// The public API of the WeatherLayerManager now simply "delegates" calls
|
|
784
|
+
// to the core engine. This keeps the API consistent for your users.
|
|
785
|
+
|
|
786
|
+
async initialize(options) {
|
|
787
|
+
const t0 =
|
|
788
|
+
typeof performance !== 'undefined' && typeof performance.now === 'function'
|
|
789
|
+
? performance.now()
|
|
790
|
+
: Date.now();
|
|
791
|
+
this._debugLog('initialize:start', {
|
|
792
|
+
autoRefreshEnabled: this.autoRefreshEnabled,
|
|
793
|
+
autoRefreshIntervalSeconds: this.autoRefreshIntervalSeconds,
|
|
794
|
+
passedOptions: options && typeof options === 'object' ? { ...options } : options,
|
|
795
|
+
coreStateBefore: {
|
|
796
|
+
isMRMS: this.core.state?.isMRMS,
|
|
797
|
+
variable: this.core.state?.variable,
|
|
798
|
+
mrmsTimestamp: this.core.state?.mrmsTimestamp,
|
|
799
|
+
mrmsDurationValue: this.core.state?.mrmsDurationValue,
|
|
800
|
+
},
|
|
801
|
+
mrmsStatusVariableLen: this.core.state?.variable
|
|
802
|
+
? (this.core.mrmsStatus?.[this.core.state.variable]?.length ?? 'n/a')
|
|
803
|
+
: 'n/a',
|
|
804
|
+
});
|
|
805
|
+
if (this.autoRefreshEnabled) {
|
|
806
|
+
this.setAutoRefresh(true, this.autoRefreshIntervalSeconds);
|
|
807
|
+
}
|
|
808
|
+
try {
|
|
809
|
+
const result = await this.core.initialize({ ...options, autoRefresh: false });
|
|
810
|
+
const t1 =
|
|
811
|
+
typeof performance !== 'undefined' && typeof performance.now === 'function'
|
|
812
|
+
? performance.now()
|
|
813
|
+
: Date.now();
|
|
814
|
+
this._debugLog('initialize:done', {
|
|
815
|
+
elapsedMs: Math.round(t1 - t0),
|
|
816
|
+
coreStateAfter: {
|
|
817
|
+
isMRMS: this.core.state?.isMRMS,
|
|
818
|
+
variable: this.core.state?.variable,
|
|
819
|
+
model: this.core.state?.model,
|
|
820
|
+
date: this.core.state?.date,
|
|
821
|
+
run: this.core.state?.run,
|
|
822
|
+
forecastHour: this.core.state?.forecastHour,
|
|
823
|
+
mrmsTimestamp: this.core.state?.mrmsTimestamp,
|
|
824
|
+
mrmsDurationValue: this.core.state?.mrmsDurationValue,
|
|
825
|
+
},
|
|
826
|
+
availableTimestampsSummary: _debugSummarizeNumericSeries(
|
|
827
|
+
this._lastEmittedState?.availableTimestamps || [],
|
|
828
|
+
),
|
|
829
|
+
availableHoursLen: Array.isArray(this._lastEmittedState?.availableHours)
|
|
830
|
+
? this._lastEmittedState.availableHours.length
|
|
831
|
+
: 0,
|
|
832
|
+
modelStatusLoaded: this.core.modelStatus != null,
|
|
833
|
+
mrmsStatusKeys:
|
|
834
|
+
this.core.mrmsStatus && typeof this.core.mrmsStatus === 'object'
|
|
835
|
+
? Object.keys(this.core.mrmsStatus).length
|
|
836
|
+
: 0,
|
|
837
|
+
mrmsStatusVariableLen: this.core.state?.variable
|
|
838
|
+
? (this.core.mrmsStatus?.[this.core.state.variable]?.length ?? 0)
|
|
839
|
+
: 0,
|
|
840
|
+
});
|
|
841
|
+
return result;
|
|
842
|
+
} catch (err) {
|
|
843
|
+
this._debugLog('initialize:error', {
|
|
844
|
+
message: err?.message || String(err),
|
|
845
|
+
stack: err?.stack,
|
|
846
|
+
});
|
|
847
|
+
throw err;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async setState(newState) { return this.core.setState(newState); }
|
|
851
|
+
play() { this.core.play(); }
|
|
852
|
+
pause() { this.core.pause(); }
|
|
853
|
+
togglePlay() { this.core.togglePlay(); }
|
|
854
|
+
step(direction) { this.core.step(direction); }
|
|
855
|
+
setPlaybackSpeed(speed) { this.core.setPlaybackSpeed(speed); }
|
|
856
|
+
async setOpacity(opacity) { return this.core.setOpacity(opacity); }
|
|
857
|
+
async setUnits(units) { return this.core.setUnits(units); }
|
|
858
|
+
async switchMode(options) { return this.core.switchMode(options); }
|
|
859
|
+
getAvailableVariables(model) { return this.core.getAvailableVariables(model); }
|
|
860
|
+
getVariableDisplayName(code) { return this.core.getVariableDisplayName(code); }
|
|
861
|
+
async setRun(runString) { return this.core.setRun(runString); }
|
|
862
|
+
setShaderSmoothing(enabled) {
|
|
863
|
+
return this.core.setShaderSmoothing(enabled);
|
|
864
|
+
}
|
|
865
|
+
async setMRMSTimestamp(timestamp) {
|
|
866
|
+
return this.core.setMRMSTimestamp(timestamp);
|
|
867
|
+
}
|
|
868
|
+
async setSatelliteTimestamp(timestamp) {
|
|
869
|
+
return this.core.setSatelliteTimestamp(timestamp);
|
|
870
|
+
}
|
|
871
|
+
async setSatelliteDurationValue(value) {
|
|
872
|
+
return this.core.setSatelliteDurationValue(value);
|
|
873
|
+
}
|
|
874
|
+
async setMRMSDurationValue(value) {
|
|
875
|
+
return this.core.setMRMSDurationValue(value);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** Refreshes NEXRAD object-key listings for the current site / product / tilt (NEXRAD mode only). */
|
|
879
|
+
async refreshNexradTimes() {
|
|
880
|
+
return this.core.refreshNexradTimes();
|
|
881
|
+
}
|
|
882
|
+
/** @param {string | null} siteId - ICAO id, or null to clear the selected site */
|
|
883
|
+
async setNexradSite(siteId) {
|
|
884
|
+
return this.core.setNexradSite(siteId);
|
|
885
|
+
}
|
|
886
|
+
async setNexradProduct(product) {
|
|
887
|
+
return this.core.setNexradProduct(product);
|
|
888
|
+
}
|
|
889
|
+
/** @param {'level2'|'level3'} dataSource @param {string} product */
|
|
890
|
+
async setNexradProductMode(dataSource, product) {
|
|
891
|
+
return this.core.setNexradProductMode(dataSource, product);
|
|
892
|
+
}
|
|
893
|
+
/** @param {'level2'|'level3'} source */
|
|
894
|
+
async setNexradDataSource(source) {
|
|
895
|
+
return this.core.setNexradDataSource(source);
|
|
896
|
+
}
|
|
897
|
+
async setNexradTilt(tilt) {
|
|
898
|
+
return this.core.setNexradTilt(tilt);
|
|
899
|
+
}
|
|
900
|
+
async setNexradTimestamp(timestamp) {
|
|
901
|
+
return this.core.setNexradTimestamp(timestamp);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// --- VISUAL RENDERING LOGIC ---
|
|
905
|
+
// These methods were removed from the core and now live exclusively
|
|
906
|
+
// in this web-specific presentation layer.
|
|
907
|
+
|
|
908
|
+
/** Swaps to a timestep that was already uploaded by rebuild or `_preloadAllTimeSteps`. */
|
|
909
|
+
_updateLayerDataWithToken(state) {
|
|
910
|
+
if (!this.shaderLayer || !state.variable) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const timeKey = state.isMRMS
|
|
915
|
+
? (state.mrmsTimestamp == null ? null : Number(state.mrmsTimestamp))
|
|
916
|
+
: Number(state.forecastHour);
|
|
917
|
+
if (this.shaderLayer.switchToPreloadedTexture(timeKey)) {
|
|
918
|
+
this.currentLoadedTimeKey = timeKey;
|
|
919
|
+
this.map.triggerRepaint();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// No GPU cache for this timestep (preload still in flight, or it failed e.g. 502). Never fetch
|
|
924
|
+
// here — scrubbing must not trigger `_loadGridData`; only rebuild + `_preloadAllTimeSteps` may.
|
|
925
|
+
// When a background preload finishes, `_preloadAllTimeSteps` applies the frame if it matches
|
|
926
|
+
// the active time.
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* MRMS timestamps or model forecast hours for the active timeline (ordering preserved from source lists).
|
|
931
|
+
* @returns {number[]}
|
|
932
|
+
*/
|
|
933
|
+
_collectNormalizedTimelineSteps(state) {
|
|
934
|
+
let fromCore = [];
|
|
935
|
+
try {
|
|
936
|
+
if (!state.isMRMS && typeof this.core.getAvailableForecastHours === 'function') {
|
|
937
|
+
fromCore = this.core.getAvailableForecastHours();
|
|
938
|
+
}
|
|
939
|
+
} catch (err) {
|
|
940
|
+
// ignore
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const fromState = state.isMRMS
|
|
944
|
+
? (state.availableTimestamps || [])
|
|
945
|
+
: (state.availableHours || []);
|
|
946
|
+
|
|
947
|
+
let timeSteps;
|
|
948
|
+
let mrmsTimelineSource = '';
|
|
949
|
+
if (state.isMRMS) {
|
|
950
|
+
if (fromState.length) {
|
|
951
|
+
timeSteps = fromState;
|
|
952
|
+
mrmsTimelineSource = 'state.availableTimestamps';
|
|
953
|
+
} else if (
|
|
954
|
+
state.variable &&
|
|
955
|
+
typeof this.core._getFilteredMrmsTimestampsForVariable === 'function'
|
|
956
|
+
) {
|
|
957
|
+
/**
|
|
958
|
+
* Same list {@link AguaceroCore} puts on `state:change` as `availableTimestamps`.
|
|
959
|
+
* When that array is missing or empty on this snapshot (ordering / first paint),
|
|
960
|
+
* derive from core so rebuild preload is not stuck with only `currentFrameTime`.
|
|
961
|
+
*/
|
|
962
|
+
try {
|
|
963
|
+
timeSteps = this.core._getFilteredMrmsTimestampsForVariable(state.variable);
|
|
964
|
+
mrmsTimelineSource = 'core._getFilteredMrmsTimestampsForVariable';
|
|
965
|
+
} catch {
|
|
966
|
+
timeSteps = fromCore;
|
|
967
|
+
mrmsTimelineSource = 'core forecast hours fallback (error)';
|
|
968
|
+
}
|
|
969
|
+
} else {
|
|
970
|
+
timeSteps = fromCore;
|
|
971
|
+
mrmsTimelineSource = 'empty';
|
|
972
|
+
}
|
|
973
|
+
} else {
|
|
974
|
+
timeSteps = fromCore.length > 0 ? fromCore : fromState;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const out = (timeSteps || [])
|
|
978
|
+
.map(t => Number(t))
|
|
979
|
+
.filter(t => !Number.isNaN(t));
|
|
980
|
+
if (this._debug && state.isMRMS) {
|
|
981
|
+
this._debugLog('_collectNormalizedTimelineSteps', {
|
|
982
|
+
variable: state.variable,
|
|
983
|
+
mrmsDurationValue: state.mrmsDurationValue,
|
|
984
|
+
fromStateLen: fromState.length,
|
|
985
|
+
fromCoreLen: fromCore.length,
|
|
986
|
+
chosenSource: mrmsTimelineSource || (state.isMRMS ? 'n/a' : 'model'),
|
|
987
|
+
normalizedLen: out.length,
|
|
988
|
+
summary: _debugSummarizeNumericSeries(out),
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
return out;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* @param {object} state
|
|
996
|
+
* @param {number[]} times
|
|
997
|
+
* @param {{ rebuildId: number, mode: 'rebuild' | 'append' }} options
|
|
998
|
+
*/
|
|
999
|
+
_runParallelGridFrameLoads(state, times, options) {
|
|
1000
|
+
const { rebuildId, mode } = options;
|
|
1001
|
+
if (!times.length || !this.shaderLayer) {
|
|
1002
|
+
if (mode === 'rebuild') {
|
|
1003
|
+
this._initialGridLoadPending = false;
|
|
1004
|
+
}
|
|
1005
|
+
this._debugLog('_runParallelGridFrameLoads:skip', {
|
|
1006
|
+
reason: !times.length ? 'no times' : 'no shaderLayer',
|
|
1007
|
+
mode,
|
|
1008
|
+
rebuildId,
|
|
1009
|
+
timesLen: times.length,
|
|
1010
|
+
});
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Number(null) === 0, which is falsy and causes _loadGridData to return null immediately
|
|
1015
|
+
// (the core guards with `if (!mrmsTimestamp) return null`). Treat null as NaN so that
|
|
1016
|
+
// primaryTimeForRebuild falls through to times[0] — the first real MRMS timestamp.
|
|
1017
|
+
const _rawFrameTime = state.isMRMS ? state.mrmsTimestamp : state.forecastHour;
|
|
1018
|
+
const currentFrameTime = _rawFrameTime == null ? NaN : Number(_rawFrameTime);
|
|
1019
|
+
/** When the active timestep is unset/NaN, paint the first timeline step (legacy single-fetch behavior). */
|
|
1020
|
+
let primaryTimeForRebuild = Number.NaN;
|
|
1021
|
+
if (mode === 'rebuild') {
|
|
1022
|
+
primaryTimeForRebuild = Number.isFinite(currentFrameTime)
|
|
1023
|
+
? currentFrameTime
|
|
1024
|
+
: times[0];
|
|
1025
|
+
}
|
|
1026
|
+
const tsKey = state.isMRMS ? 'mrmsTimestamp' : 'forecastHour';
|
|
1027
|
+
const gridModel = state.isMRMS ? 'mrms' : state.model;
|
|
1028
|
+
const { gridDef } = this.core._getGridCornersAndDef(gridModel);
|
|
1029
|
+
|
|
1030
|
+
this._debugLog('_runParallelGridFrameLoads', {
|
|
1031
|
+
mode,
|
|
1032
|
+
rebuildId,
|
|
1033
|
+
gridModel,
|
|
1034
|
+
timesLen: times.length,
|
|
1035
|
+
timesSummary: _debugSummarizeNumericSeries(times),
|
|
1036
|
+
currentFrameTime,
|
|
1037
|
+
primaryTimeForRebuild:
|
|
1038
|
+
mode === 'rebuild'
|
|
1039
|
+
? Number.isFinite(currentFrameTime)
|
|
1040
|
+
? currentFrameTime
|
|
1041
|
+
: times[0]
|
|
1042
|
+
: undefined,
|
|
1043
|
+
tsKey,
|
|
1044
|
+
gridNxNy: gridDef?.grid_params
|
|
1045
|
+
? { nx: gridDef.grid_params.nx, ny: gridDef.grid_params.ny }
|
|
1046
|
+
: null,
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
times.forEach((time) => {
|
|
1050
|
+
const stateForTime = { ...state, [tsKey]: time };
|
|
1051
|
+
this.core._loadGridData(stateForTime)
|
|
1052
|
+
.then((grid) => {
|
|
1053
|
+
if (rebuildId !== this.currentRebuildId || !this.shaderLayer) {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
const isPrimaryFrame =
|
|
1058
|
+
mode === 'rebuild' &&
|
|
1059
|
+
Number.isFinite(primaryTimeForRebuild) &&
|
|
1060
|
+
time === primaryTimeForRebuild;
|
|
1061
|
+
|
|
1062
|
+
if (isPrimaryFrame) {
|
|
1063
|
+
// Drift guard: when the rebuild started with an explicit target timestamp
|
|
1064
|
+
// (mrmsTimestamp / forecastHour was non-null at rebuild time) and the user
|
|
1065
|
+
// navigated to a different time while loading, skip applying the now-stale
|
|
1066
|
+
// primary frame. When _rebuildTargetTimeKey is null it means there was no
|
|
1067
|
+
// explicit timestamp at rebuild time (e.g. first MRMS load where
|
|
1068
|
+
// mrmsTimestamp starts as null); in that case we must always apply the
|
|
1069
|
+
// primary frame — skipping it leaves currentLoadedTimeKey null forever.
|
|
1070
|
+
if (this._rebuildTargetTimeKey !== null) {
|
|
1071
|
+
const coreTimeKey = state.isMRMS
|
|
1072
|
+
? (this.core.state.mrmsTimestamp == null
|
|
1073
|
+
? null
|
|
1074
|
+
: Number(this.core.state.mrmsTimestamp))
|
|
1075
|
+
: Number(this.core.state.forecastHour);
|
|
1076
|
+
if (coreTimeKey !== this._rebuildTargetTimeKey) {
|
|
1077
|
+
this._debugLog('_loadGridData:skip primary (core time drifted vs rebuild target)', {
|
|
1078
|
+
time,
|
|
1079
|
+
coreTimeKey,
|
|
1080
|
+
_rebuildTargetTimeKey: this._rebuildTargetTimeKey,
|
|
1081
|
+
rebuildId,
|
|
1082
|
+
});
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (!grid?.data) {
|
|
1087
|
+
this._debugLog('_loadGridData:primary frame missing grid.data', {
|
|
1088
|
+
time,
|
|
1089
|
+
rebuildId,
|
|
1090
|
+
mode,
|
|
1091
|
+
});
|
|
1092
|
+
this._initialGridLoadPending = false;
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
this.shaderLayer.updateDataTexture(
|
|
1096
|
+
grid.data,
|
|
1097
|
+
grid.encoding,
|
|
1098
|
+
gridDef.grid_params.nx,
|
|
1099
|
+
gridDef.grid_params.ny,
|
|
1100
|
+
);
|
|
1101
|
+
this.currentLoadedTimeKey = time;
|
|
1102
|
+
this.shaderLayer.registerCurrentDataTextureAsPreloaded(time);
|
|
1103
|
+
this._initialGridLoadPending = false;
|
|
1104
|
+
this.map.triggerRepaint();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
if (grid?.data) {
|
|
1109
|
+
this.shaderLayer.storePreloadedTexture(
|
|
1110
|
+
time,
|
|
1111
|
+
grid.data,
|
|
1112
|
+
grid.encoding,
|
|
1113
|
+
gridDef.grid_params.nx,
|
|
1114
|
+
gridDef.grid_params.ny,
|
|
1115
|
+
);
|
|
1116
|
+
const s = this.core.state;
|
|
1117
|
+
const activeTime = s.isMRMS
|
|
1118
|
+
? (s.mrmsTimestamp == null ? null : Number(s.mrmsTimestamp))
|
|
1119
|
+
: Number(s.forecastHour);
|
|
1120
|
+
if (time === activeTime && this.shaderLayer.switchToPreloadedTexture(time)) {
|
|
1121
|
+
this.currentLoadedTimeKey = time;
|
|
1122
|
+
this.map.triggerRepaint();
|
|
1123
|
+
}
|
|
1124
|
+
} else if (this._debug && mode === 'append') {
|
|
1125
|
+
this._debugLog('_loadGridData:empty grid (append)', { time, mode, rebuildId });
|
|
1126
|
+
}
|
|
1127
|
+
})
|
|
1128
|
+
.catch((err) => {
|
|
1129
|
+
this._debugLog('_loadGridData:error', {
|
|
1130
|
+
time,
|
|
1131
|
+
mode,
|
|
1132
|
+
rebuildId,
|
|
1133
|
+
message: err?.message || String(err),
|
|
1134
|
+
});
|
|
1135
|
+
if (
|
|
1136
|
+
mode === 'rebuild' &&
|
|
1137
|
+
Number.isFinite(primaryTimeForRebuild) &&
|
|
1138
|
+
time === primaryTimeForRebuild
|
|
1139
|
+
) {
|
|
1140
|
+
this._initialGridLoadPending = false;
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
_rebuildLayerAndPreload(state) {
|
|
1147
|
+
if (state.isSatellite) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
if (!state.variable) {
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const rebuildId = ++this.currentRebuildId;
|
|
1155
|
+
|
|
1156
|
+
this.core.cancelAllRequests();
|
|
1157
|
+
|
|
1158
|
+
if (this.shaderLayer) {
|
|
1159
|
+
this.map.removeLayer(this.shaderLayer.id);
|
|
1160
|
+
this.shaderLayer = null;
|
|
1161
|
+
}
|
|
1162
|
+
this.currentLoadedTimeKey = null;
|
|
1163
|
+
|
|
1164
|
+
if (
|
|
1165
|
+
!state.isMRMS &&
|
|
1166
|
+
state.forecastHour === 0 &&
|
|
1167
|
+
state.variable === 'ptypeRefl' &&
|
|
1168
|
+
state.model === 'hrrr'
|
|
1169
|
+
) {
|
|
1170
|
+
const availableHours = this.core.getAvailableForecastHours();
|
|
1171
|
+
const firstValidHour = availableHours.find(h => h !== 0);
|
|
1172
|
+
if (firstValidHour !== undefined) {
|
|
1173
|
+
state = { ...state, forecastHour: firstValidHour };
|
|
1174
|
+
this.core.state.forecastHour = firstValidHour;
|
|
1175
|
+
} else {
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (rebuildId !== this.currentRebuildId) return;
|
|
1181
|
+
|
|
1182
|
+
const runKey = `${state.model}-${state.date}-${state.run}-${state.variable}`;
|
|
1183
|
+
|
|
1184
|
+
this.shaderLayer = new GridRenderLayer(this.layerId);
|
|
1185
|
+
this.shaderLayer.runKey = runKey;
|
|
1186
|
+
const beforeId = this._weatherInsertBeforeId();
|
|
1187
|
+
if (beforeId) {
|
|
1188
|
+
this.map.addLayer(this.shaderLayer, beforeId);
|
|
1189
|
+
} else {
|
|
1190
|
+
this.map.addLayer(this.shaderLayer);
|
|
1191
|
+
}
|
|
1192
|
+
this._updateLayerStyle(state);
|
|
1193
|
+
|
|
1194
|
+
this._rebuildTargetTimeKey = state.isMRMS
|
|
1195
|
+
? (state.mrmsTimestamp == null ? null : Number(state.mrmsTimestamp))
|
|
1196
|
+
: Number(state.forecastHour);
|
|
1197
|
+
this._initialGridLoadPending = true;
|
|
1198
|
+
|
|
1199
|
+
const normalized = this._collectNormalizedTimelineSteps(state);
|
|
1200
|
+
// Same null-guard as in _runParallelGridFrameLoads: Number(null) === 0, which is a finite
|
|
1201
|
+
// value that would inject Unix epoch into timesToLoad and mis-identify the primary frame.
|
|
1202
|
+
const _rawCft = state.isMRMS ? state.mrmsTimestamp : state.forecastHour;
|
|
1203
|
+
const currentFrameTime = _rawCft == null ? NaN : Number(_rawCft);
|
|
1204
|
+
const timesSet = new Set(normalized);
|
|
1205
|
+
if (!Number.isNaN(currentFrameTime)) {
|
|
1206
|
+
timesSet.add(currentFrameTime);
|
|
1207
|
+
}
|
|
1208
|
+
let timesToLoad = [...timesSet];
|
|
1209
|
+
if (timesToLoad.length === 0 && !Number.isNaN(currentFrameTime)) {
|
|
1210
|
+
timesToLoad = [currentFrameTime];
|
|
1211
|
+
}
|
|
1212
|
+
this._debugLog('_rebuildLayerAndPreload:timeline', {
|
|
1213
|
+
rebuildId,
|
|
1214
|
+
isMRMS: state.isMRMS,
|
|
1215
|
+
variable: state.variable,
|
|
1216
|
+
normalizedLen: normalized.length,
|
|
1217
|
+
normalizedSummary: _debugSummarizeNumericSeries(normalized),
|
|
1218
|
+
currentFrameTime,
|
|
1219
|
+
timesToLoadLen: timesToLoad.length,
|
|
1220
|
+
timesToLoadSummary: _debugSummarizeNumericSeries(timesToLoad),
|
|
1221
|
+
insertBeforeId: beforeId ?? '(stack top)',
|
|
1222
|
+
});
|
|
1223
|
+
if (timesToLoad.length === 0) {
|
|
1224
|
+
this._initialGridLoadPending = false;
|
|
1225
|
+
this._debugLog('_rebuildLayerAndPreload:no times to load — check availableTimestamps / duration window', {
|
|
1226
|
+
rebuildId,
|
|
1227
|
+
availableTimestampsLen: Array.isArray(state.availableTimestamps)
|
|
1228
|
+
? state.availableTimestamps.length
|
|
1229
|
+
: 0,
|
|
1230
|
+
mrmsDurationValue: state.mrmsDurationValue,
|
|
1231
|
+
});
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (rebuildId === this.currentRebuildId) {
|
|
1236
|
+
this._runParallelGridFrameLoads(state, timesToLoad, { rebuildId, mode: 'rebuild' });
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
_preloadAllTimeSteps(state) {
|
|
1241
|
+
const normalized = this._collectNormalizedTimelineSteps(state);
|
|
1242
|
+
const _rawPft = state.isMRMS ? state.mrmsTimestamp : state.forecastHour;
|
|
1243
|
+
const currentFrameTime = _rawPft == null ? NaN : Number(_rawPft);
|
|
1244
|
+
const stepsToPreload = normalized.filter(t => t !== currentFrameTime);
|
|
1245
|
+
|
|
1246
|
+
this._debugLog('_preloadAllTimeSteps', {
|
|
1247
|
+
normalizedLen: normalized.length,
|
|
1248
|
+
currentFrameTime,
|
|
1249
|
+
stepsToPreloadLen: stepsToPreload.length,
|
|
1250
|
+
stepsSummary: _debugSummarizeNumericSeries(stepsToPreload),
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
if (normalized.length === 0) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (stepsToPreload.length === 0) {
|
|
1258
|
+
this._debugLog('_preloadAllTimeSteps:skip (nothing to preload besides current)', {
|
|
1259
|
+
currentFrameTime,
|
|
1260
|
+
});
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const capturedRebuildId = this.currentRebuildId;
|
|
1265
|
+
this._runParallelGridFrameLoads(state, stepsToPreload, { rebuildId: capturedRebuildId, mode: 'append' });
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
_updateLayerData(state) {
|
|
1269
|
+
return this._updateLayerDataWithToken(state);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
_updateLayerStyle(state) {
|
|
1273
|
+
if (!this.shaderLayer || !state.variable) return;
|
|
1274
|
+
|
|
1275
|
+
const gridModel = state.isMRMS ? 'mrms' : state.model;
|
|
1276
|
+
const geometry = this.core._getGridCornersAndDef(gridModel);
|
|
1277
|
+
if (!geometry) {
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const { corners, gridDef } = geometry;
|
|
1282
|
+
const { colormap, baseUnit } = this.core._getColormapForVariable(state.variable);
|
|
1283
|
+
const toUnit = this.core._getTargetUnit(baseUnit, state.units);
|
|
1284
|
+
|
|
1285
|
+
const finalColormap = this.core._convertColormapUnits(colormap, baseUnit, toUnit);
|
|
1286
|
+
|
|
1287
|
+
let dataRange;
|
|
1288
|
+
if (state.variable === 'ptypeRefl' || state.variable === 'ptypeRate') {
|
|
1289
|
+
if (state.isMRMS) {
|
|
1290
|
+
dataRange = [5, 380];
|
|
1291
|
+
} else {
|
|
1292
|
+
dataRange = [5, 380];
|
|
1293
|
+
}
|
|
1294
|
+
} else {
|
|
1295
|
+
dataRange = [finalColormap[0], finalColormap[finalColormap.length - 2]];
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const dataNativeUnit = (DICTIONARIES.fld[state.variable] || {}).defaultUnit || 'none';
|
|
1299
|
+
|
|
1300
|
+
this.shaderLayer.updateGeometry(corners, gridDef);
|
|
1301
|
+
this.shaderLayer.updateColormapTexture(finalColormap);
|
|
1302
|
+
this.shaderLayer.updateStyle({ opacity: state.opacity, dataRange });
|
|
1303
|
+
this.shaderLayer.setUnitConversion(dataNativeUnit, state.units);
|
|
1304
|
+
this.shaderLayer.setVariable(state.variable);
|
|
1305
|
+
this.shaderLayer.setIsMRMS(state.isMRMS); // NEW
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Handles the map's mousemove event by delegating calculations to the core.
|
|
1310
|
+
* @param {object} e - The Mapbox GL mouse event object.
|
|
1311
|
+
* @private
|
|
1312
|
+
*/
|
|
1313
|
+
async _handleMouseMove(e) {
|
|
1314
|
+
const { lng, lat } = this._mouseLngLatFromEvent(e);
|
|
1315
|
+
const state = this._lastEmittedState;
|
|
1316
|
+
if (state?.isNexrad) {
|
|
1317
|
+
if (this._nexradController) {
|
|
1318
|
+
const payload = this._nexradController.getInspectPayload(lng, lat, state);
|
|
1319
|
+
if (payload) {
|
|
1320
|
+
this.emit('data:inspect', {
|
|
1321
|
+
...payload,
|
|
1322
|
+
point: e.point,
|
|
1323
|
+
});
|
|
1324
|
+
} else {
|
|
1325
|
+
this.emit('data:inspect', null);
|
|
1326
|
+
}
|
|
1327
|
+
} else {
|
|
1328
|
+
this.emit('data:inspect', null);
|
|
1329
|
+
}
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Model / MRMS: delegate to the core grid sampler.
|
|
1334
|
+
const payload = await this.core.getValueAtLngLat(lng, lat);
|
|
1335
|
+
|
|
1336
|
+
if (payload) {
|
|
1337
|
+
this.emit('data:inspect', {
|
|
1338
|
+
...payload,
|
|
1339
|
+
point: e.point,
|
|
1340
|
+
});
|
|
1341
|
+
} else {
|
|
1342
|
+
this.emit('data:inspect', null);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Enables or disables automatic checking for new weather data for both models and MRMS.
|
|
1348
|
+
* When enabled, it periodically checks for and displays the latest available data.
|
|
1349
|
+
* @param {boolean} enabled - Whether to enable or disable auto-refresh.
|
|
1350
|
+
* @param {number} [intervalSeconds] - The interval in seconds to check for new data. Defaults to the value from constructor options or 30.
|
|
1351
|
+
*/
|
|
1352
|
+
setAutoRefresh(enabled, intervalSeconds) {
|
|
1353
|
+
// Stop any existing refresh interval
|
|
1354
|
+
if (this.autoRefreshIntervalId) {
|
|
1355
|
+
clearInterval(this.autoRefreshIntervalId);
|
|
1356
|
+
this.autoRefreshIntervalId = null;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Store the new state
|
|
1360
|
+
this.autoRefreshEnabled = enabled;
|
|
1361
|
+
if (typeof intervalSeconds === 'number' && intervalSeconds > 0) {
|
|
1362
|
+
this.autoRefreshIntervalSeconds = intervalSeconds;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
if (this.autoRefreshEnabled) {
|
|
1366
|
+
const intervalMilliseconds = this.autoRefreshIntervalSeconds * 1000;
|
|
1367
|
+
// Run the check immediately once, then start the interval
|
|
1368
|
+
this._checkForUpdates();
|
|
1369
|
+
this.autoRefreshIntervalId = setInterval(() => this._checkForUpdates(), intervalMilliseconds);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Manually triggers a check for new weather data.
|
|
1375
|
+
* This performs the same action as a single auto-refresh cycle, updating the
|
|
1376
|
+
* timeline and preloading new data in the background without changing the user's view.
|
|
1377
|
+
* @returns {Promise<void>}
|
|
1378
|
+
*/
|
|
1379
|
+
async refreshData() {
|
|
1380
|
+
await this._checkForUpdates();
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Checks for new data, updates the timeline, and proactively preloads the data for
|
|
1385
|
+
* any new time steps in the background.
|
|
1386
|
+
* @private
|
|
1387
|
+
*/
|
|
1388
|
+
async _checkForUpdates() {
|
|
1389
|
+
const s = this.core.state;
|
|
1390
|
+
const { isMRMS, isSatellite, isNexrad, model: currentModel, variable: currentVariable, date, run } = s;
|
|
1391
|
+
|
|
1392
|
+
if (isSatellite && s.satelliteKey) {
|
|
1393
|
+
const prevTimeline = this.core._computeSatelliteTimeline();
|
|
1394
|
+
const prevTimes = [...(prevTimeline.unixTimes || [])]
|
|
1395
|
+
.map((t) => Number(t))
|
|
1396
|
+
.filter((t) => !Number.isNaN(t))
|
|
1397
|
+
.sort((a, b) => a - b);
|
|
1398
|
+
const prevMax = prevTimes.length ? prevTimes[prevTimes.length - 1] : null;
|
|
1399
|
+
const curSat = s.satelliteTimestamp == null ? null : Number(s.satelliteTimestamp);
|
|
1400
|
+
|
|
1401
|
+
await this.core.fetchSatelliteListing(true);
|
|
1402
|
+
this.core._emitStateChange();
|
|
1403
|
+
|
|
1404
|
+
const nextTimeline = this.core._computeSatelliteTimeline();
|
|
1405
|
+
const nextTimes = [...(nextTimeline.unixTimes || [])]
|
|
1406
|
+
.map((t) => Number(t))
|
|
1407
|
+
.filter((t) => !Number.isNaN(t))
|
|
1408
|
+
.sort((a, b) => a - b);
|
|
1409
|
+
const newMax = nextTimes.length ? nextTimes[nextTimes.length - 1] : null;
|
|
1410
|
+
|
|
1411
|
+
if (prevMax != null && curSat != null && curSat === prevMax && newMax != null && newMax > prevMax) {
|
|
1412
|
+
await this.core.setSatelliteTimestamp(newMax);
|
|
1413
|
+
} else if (curSat != null && newMax != null && nextTimes.length && !nextTimes.includes(curSat)) {
|
|
1414
|
+
await this.core.setSatelliteTimestamp(newMax);
|
|
1415
|
+
}
|
|
1416
|
+
this.emit('data:updated', { type: 'satellite', satelliteKey: s.satelliteKey });
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
if (isNexrad && s.nexradSite) {
|
|
1421
|
+
const nk = this.core._nexradTimesCacheKey();
|
|
1422
|
+
const rawBefore = nk ? this.core.nexradTimesByStation[nk]?.unixTimes : [];
|
|
1423
|
+
const filteredBefore = this.core._getFilteredNexradTimestampsForVariable(rawBefore || []);
|
|
1424
|
+
const prevMax = filteredBefore.length ? filteredBefore[filteredBefore.length - 1] : null;
|
|
1425
|
+
const curNx = s.nexradTimestamp == null ? null : Number(s.nexradTimestamp);
|
|
1426
|
+
|
|
1427
|
+
await this.core.refreshNexradTimes();
|
|
1428
|
+
|
|
1429
|
+
const rawAfter = nk ? this.core.nexradTimesByStation[nk]?.unixTimes : [];
|
|
1430
|
+
const filteredAfter = this.core._getFilteredNexradTimestampsForVariable(rawAfter || []);
|
|
1431
|
+
const newMax = filteredAfter.length ? filteredAfter[filteredAfter.length - 1] : null;
|
|
1432
|
+
|
|
1433
|
+
if (prevMax != null && curNx != null && curNx === prevMax && newMax != null && newMax > prevMax) {
|
|
1434
|
+
await this.core.setNexradTimestamp(newMax);
|
|
1435
|
+
} else if (curNx != null && newMax != null && filteredAfter.length && !filteredAfter.includes(curNx)) {
|
|
1436
|
+
await this.core.setNexradTimestamp(newMax);
|
|
1437
|
+
}
|
|
1438
|
+
this.emit('data:updated', { type: 'nexrad', nexradSite: s.nexradSite });
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if (isMRMS) {
|
|
1443
|
+
const oldTimestamps = new Set(this.core.mrmsStatus?.[currentVariable] || []);
|
|
1444
|
+
|
|
1445
|
+
// 1. Fetch the absolute latest status
|
|
1446
|
+
const mrmsStatus = await this.core.fetchMRMSStatus(true);
|
|
1447
|
+
const newTimestamps = mrmsStatus?.[currentVariable] || [];
|
|
1448
|
+
if (newTimestamps.length === 0) return;
|
|
1449
|
+
|
|
1450
|
+
// 2. Identify exactly which timestamps are new
|
|
1451
|
+
const newTimestampsToPreload = newTimestamps.filter(ts => !oldTimestamps.has(ts));
|
|
1452
|
+
|
|
1453
|
+
if (newTimestampsToPreload.length > 0) {
|
|
1454
|
+
// 3. Update the core's internal status and emit the change to update the UI (slider)
|
|
1455
|
+
this.core.mrmsStatus = mrmsStatus;
|
|
1456
|
+
this.core._emitStateChange();
|
|
1457
|
+
|
|
1458
|
+
// 4. Proactively fetch the data for each new timestamp in the background.
|
|
1459
|
+
newTimestampsToPreload.forEach(timestamp => {
|
|
1460
|
+
const stateForPreload = { ...this.core.state, mrmsTimestamp: timestamp };
|
|
1461
|
+
this.core._loadGridData(stateForPreload)
|
|
1462
|
+
.catch(() => {});
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
// 5. Clean up the cache for any timestamps that are no longer available.
|
|
1466
|
+
const newTimestampsSet = new Set(newTimestamps);
|
|
1467
|
+
oldTimestamps.forEach(oldTs => {
|
|
1468
|
+
if (!newTimestampsSet.has(oldTs)) {
|
|
1469
|
+
const cacheKey = `mrms-${oldTs}-${currentVariable}-0`;
|
|
1470
|
+
this.core.dataCache.delete(cacheKey);
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
this.emit('data:updated', { type: 'mrms', variable: currentVariable, newTimestamps: newTimestampsToPreload });
|
|
1475
|
+
}
|
|
1476
|
+
} else {
|
|
1477
|
+
const previousStatus = this.core.modelStatus;
|
|
1478
|
+
const modelStatus = await this.core.fetchModelStatus(true);
|
|
1479
|
+
|
|
1480
|
+
// Only emit if the status actually changed
|
|
1481
|
+
const statusChanged = JSON.stringify(previousStatus) !== JSON.stringify(modelStatus);
|
|
1482
|
+
if (statusChanged) {
|
|
1483
|
+
this.core._emitStateChange();
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const latestRun = findLatestModelRun(modelStatus, currentModel);
|
|
1487
|
+
if (!latestRun) return;
|
|
1488
|
+
|
|
1489
|
+
const currentRunKey = `${date}:${run}`;
|
|
1490
|
+
const latestRunKey = `${latestRun.date}:${latestRun.run}`;
|
|
1491
|
+
|
|
1492
|
+
if (latestRunKey > currentRunKey) {
|
|
1493
|
+
this.emit('data:updated', { type: 'model', model: currentModel, newRun: latestRunKey });
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* Cleans up all resources.
|
|
1500
|
+
*/
|
|
1501
|
+
destroy() {
|
|
1502
|
+
this._debugLog('destroy');
|
|
1503
|
+
this.setAutoRefresh(false);
|
|
1504
|
+
|
|
1505
|
+
// 1. Unbind the map's mousemove event listener
|
|
1506
|
+
this.map.off('mousemove', this._handleMouseMove);
|
|
1507
|
+
|
|
1508
|
+
if (this._nwsOverlay) {
|
|
1509
|
+
try {
|
|
1510
|
+
this._nwsOverlay.destroy();
|
|
1511
|
+
} catch {
|
|
1512
|
+
/* ignore */
|
|
1513
|
+
}
|
|
1514
|
+
this._nwsOverlay = null;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
this._tearDownSatellite();
|
|
1518
|
+
this._tearDownNexrad();
|
|
1519
|
+
|
|
1520
|
+
// 2. Explicitly clean up and remove the visual layer
|
|
1521
|
+
if (this.shaderLayer) {
|
|
1522
|
+
// First, check if the layer still exists on the map before trying to remove it
|
|
1523
|
+
if (this.map.getLayer(this.shaderLayer.id)) {
|
|
1524
|
+
this.map.removeLayer(this.shaderLayer.id);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Manually call the layer's own cleanup method as a safeguard.
|
|
1528
|
+
// This guarantees the release of WebGL resources (textures, buffers).
|
|
1529
|
+
this.shaderLayer.onRemove();
|
|
1530
|
+
|
|
1531
|
+
// Nullify the reference to the custom layer object
|
|
1532
|
+
this.shaderLayer = null;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// 3. Tell the core engine to destroy its own resources (cache, workers, etc.)
|
|
1536
|
+
this.core.destroy();
|
|
1537
|
+
|
|
1538
|
+
// 4. Clear any remaining internal state
|
|
1539
|
+
this.currentLoadedTimeKey = null;
|
|
1540
|
+
}
|
|
1537
1541
|
}
|