@aguacerowx/mapsgl 0.0.49 → 0.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/NwsWatchesWarningsOverlay.js +85 -22
- package/src/WeatherLayerManager.js +61 -290
- package/src/nwsAlertsSupport.js +92 -0
package/package.json
CHANGED
|
@@ -19,9 +19,12 @@ import {
|
|
|
19
19
|
enrichWarningsWithColors,
|
|
20
20
|
filterNwwsTerminalStatusFeatures,
|
|
21
21
|
filterNwsAlertsByScope,
|
|
22
|
+
getNwwsProductKeyFromFeature,
|
|
22
23
|
normalizeFeatureCollectionForMapboxGl,
|
|
23
24
|
normalizeNwwsHttpAlertFeature,
|
|
25
|
+
nwsRevisionVisibilityActive,
|
|
24
26
|
parseNwwsWsDelta,
|
|
27
|
+
pickBestNwsRevisionAmongConflictingFeatures,
|
|
25
28
|
unwrapNwwsFeatureCollectionRoot,
|
|
26
29
|
} from './nwsAlertsSupport.js';
|
|
27
30
|
|
|
@@ -131,6 +134,8 @@ export class NwsWatchesWarningsOverlay {
|
|
|
131
134
|
|
|
132
135
|
/** @type {number | null | undefined} */
|
|
133
136
|
this._timelineUnix = undefined;
|
|
137
|
+
/** Timeline unix list for the active layer (satellite / MRMS / NEXRAD) — used to match frontend “live edge” behavior. */
|
|
138
|
+
this._timelineTimes = null;
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
getAlertsUrl() {
|
|
@@ -290,9 +295,29 @@ export class NwsWatchesWarningsOverlay {
|
|
|
290
295
|
return this._nwsAlertSettings.flashStyles ?? {};
|
|
291
296
|
}
|
|
292
297
|
|
|
298
|
+
_isFollowingLatestTimelineAnchor() {
|
|
299
|
+
const current = this._timelineUnix;
|
|
300
|
+
if (current == null || !Number.isFinite(current)) return false;
|
|
301
|
+
const times = this._timelineTimes;
|
|
302
|
+
if (!Array.isArray(times) || times.length === 0) return false;
|
|
303
|
+
let latest = -Infinity;
|
|
304
|
+
for (const t of times) {
|
|
305
|
+
if (typeof t === 'number' && Number.isFinite(t) && t > latest) {
|
|
306
|
+
latest = t;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!Number.isFinite(latest)) return false;
|
|
310
|
+
return Math.abs(current - latest) <= 1;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Matches aguacero-frontend {@link WatchesWarningsLayer.resolveNwsDisplayTimeMode}: at the last slider
|
|
315
|
+
* step, use wall clock so SSE/stream updates appear even when the timeline tick lags.
|
|
316
|
+
*/
|
|
293
317
|
_resolveDisplayTimeMode() {
|
|
294
318
|
const userPinnedRealtime = this._nwsAlertSettings.activeOnlyRealtime === true;
|
|
295
|
-
const
|
|
319
|
+
const followRealtimeAtLatestAnchor = !userPinnedRealtime && this._isFollowingLatestTimelineAnchor();
|
|
320
|
+
const useRealtime = userPinnedRealtime || followRealtimeAtLatestAnchor;
|
|
296
321
|
return {
|
|
297
322
|
useRealtime,
|
|
298
323
|
timelineUnix: useRealtime ? undefined : this._timelineUnix,
|
|
@@ -309,35 +334,66 @@ export class NwsWatchesWarningsOverlay {
|
|
|
309
334
|
: timeMode.timelineUnix;
|
|
310
335
|
const useOnsetWindow = this._nwsAlertSettings.activeOnlyRealtime === true;
|
|
311
336
|
|
|
312
|
-
/**
|
|
313
|
-
|
|
314
|
-
* time we must not show every alert at full opacity — previously `isActive` defaulted to
|
|
315
|
-
* true for all features when `referenceUnix` was still null (race before first timestamp).
|
|
316
|
-
*/
|
|
317
|
-
if (!timeMode.useRealtime && (referenceUnix == null || !Number.isFinite(referenceUnix))) {
|
|
318
|
-
for (const f of this._workingFc.features ?? []) {
|
|
319
|
-
if (f.id == null) continue;
|
|
320
|
-
try {
|
|
321
|
-
m.setFeatureState({ source: this._sourceId, id: f.id }, { active: false });
|
|
322
|
-
} catch {
|
|
323
|
-
/* ignore */
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
337
|
+
/** @type {Map<number, boolean>} */
|
|
338
|
+
const tentativeActive = new Map();
|
|
328
339
|
|
|
329
340
|
for (const f of this._workingFc.features ?? []) {
|
|
330
341
|
if (f.id == null) continue;
|
|
342
|
+
const fid = typeof f.id === 'number' ? f.id : Number(f.id);
|
|
343
|
+
if (!Number.isFinite(fid)) continue;
|
|
344
|
+
|
|
331
345
|
let isActive = true;
|
|
332
346
|
if (referenceUnix != null && Number.isFinite(referenceUnix)) {
|
|
333
347
|
const p = f.properties ?? {};
|
|
334
|
-
const
|
|
335
|
-
if (
|
|
336
|
-
isActive = false;
|
|
337
|
-
} else if (p.end_unix != null && referenceUnix > p.end_unix) {
|
|
348
|
+
const rev = nwsRevisionVisibilityActive(p, referenceUnix);
|
|
349
|
+
if (rev === false) {
|
|
338
350
|
isActive = false;
|
|
351
|
+
} else {
|
|
352
|
+
const startWindow = useOnsetWindow ? (p.active_start_unix ?? p.start_unix) : p.start_unix;
|
|
353
|
+
if (startWindow != null && referenceUnix < startWindow) {
|
|
354
|
+
isActive = false;
|
|
355
|
+
} else if (p.end_unix != null && referenceUnix > p.end_unix) {
|
|
356
|
+
isActive = false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
tentativeActive.set(fid, isActive);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (referenceUnix != null && Number.isFinite(referenceUnix)) {
|
|
365
|
+
const keyToFeatures = new globalThis.Map();
|
|
366
|
+
for (const f of this._workingFc.features ?? []) {
|
|
367
|
+
if (f.id == null) continue;
|
|
368
|
+
const fid = typeof f.id === 'number' ? f.id : Number(f.id);
|
|
369
|
+
if (!Number.isFinite(fid) || !tentativeActive.get(fid)) continue;
|
|
370
|
+
const pk = getNwwsProductKeyFromFeature(f);
|
|
371
|
+
if (!pk) continue;
|
|
372
|
+
let bucket = keyToFeatures.get(pk);
|
|
373
|
+
if (!bucket) {
|
|
374
|
+
bucket = [];
|
|
375
|
+
keyToFeatures.set(pk, bucket);
|
|
339
376
|
}
|
|
377
|
+
bucket.push(f);
|
|
340
378
|
}
|
|
379
|
+
for (const [, group] of keyToFeatures) {
|
|
380
|
+
if (group.length <= 1) continue;
|
|
381
|
+
const winner = pickBestNwsRevisionAmongConflictingFeatures(group, referenceUnix);
|
|
382
|
+
const winId = typeof winner?.id === 'number' ? winner.id : Number(winner?.id);
|
|
383
|
+
for (const f of group) {
|
|
384
|
+
const gfid = typeof f.id === 'number' ? f.id : Number(f.id);
|
|
385
|
+
if (Number.isFinite(gfid) && Number.isFinite(winId) && gfid !== winId) {
|
|
386
|
+
tentativeActive.set(gfid, false);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for (const f of this._workingFc.features ?? []) {
|
|
393
|
+
if (f.id == null) continue;
|
|
394
|
+
const fid = typeof f.id === 'number' ? f.id : Number(f.id);
|
|
395
|
+
if (!Number.isFinite(fid)) continue;
|
|
396
|
+
const isActive = tentativeActive.get(fid) ?? true;
|
|
341
397
|
try {
|
|
342
398
|
m.setFeatureState({ source: this._sourceId, id: f.id }, { active: isActive });
|
|
343
399
|
} catch {
|
|
@@ -759,10 +815,17 @@ export class NwsWatchesWarningsOverlay {
|
|
|
759
815
|
|
|
760
816
|
/**
|
|
761
817
|
* Called by WeatherLayerManager when radar/satellite/MRMS mode is active.
|
|
818
|
+
* @param {object} opts
|
|
819
|
+
* @param {boolean} opts.enabled
|
|
820
|
+
* @param {number | null | undefined} opts.timelineUnix - Active scrubber unix for satellite / MRMS / NEXRAD.
|
|
821
|
+
* @param {number[] | null | undefined} [opts.timelineTimes] - Full timeline for the current layer (for “live edge” = wall clock).
|
|
762
822
|
*/
|
|
763
|
-
syncWithMode({ enabled, timelineUnix }) {
|
|
823
|
+
syncWithMode({ enabled, timelineUnix, timelineTimes }) {
|
|
764
824
|
if (this._destroyed) return;
|
|
765
825
|
this.options.enabled = !!enabled;
|
|
826
|
+
if (timelineTimes !== undefined) {
|
|
827
|
+
this._timelineTimes = Array.isArray(timelineTimes) ? timelineTimes : null;
|
|
828
|
+
}
|
|
766
829
|
this.setTimelineUnix(timelineUnix);
|
|
767
830
|
if (!this.options.enabled) {
|
|
768
831
|
this._started = false;
|
|
@@ -10,48 +10,6 @@ import WorkerPool from './WorkerPool.js';
|
|
|
10
10
|
|
|
11
11
|
import { DEFAULT_BASIS_BASE_URL } from './defaultBasisBaseUrl.js';
|
|
12
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
13
|
function findLatestModelRun(modelsData, modelName) {
|
|
56
14
|
const model = modelsData?.[modelName];
|
|
57
15
|
if (!model) return null;
|
|
@@ -77,7 +35,7 @@ function findLatestModelRun(modelsData, modelName) {
|
|
|
77
35
|
* @param {string} [options.belowID] - Style layer id to insert Aguacero weather layers **below** (default `AML_-_terrain` when present). Alias of `weatherBeforeLayerId`.
|
|
78
36
|
* @param {string} [options.weatherBeforeLayerId] - Same as `belowID`.
|
|
79
37
|
* @param {string} [options.nexradLayerId] - Override Mapbox id for the NEXRAD custom layer (default: derived from `layerId`).
|
|
80
|
-
* @param {boolean} [options.
|
|
38
|
+
* @param {boolean} [options.mrmsTimelineLog] - When `true`, logs MRMS duration / raw vs filtered timeline counts (prefix `[WeatherLayerManager MRMS]`). Off by default.
|
|
81
39
|
* @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
40
|
*/
|
|
83
41
|
export class WeatherLayerManager extends EventEmitter {
|
|
@@ -85,10 +43,10 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
85
43
|
super();
|
|
86
44
|
if (!map) throw new Error('A Mapbox GL map instance is required.');
|
|
87
45
|
this.map = map;
|
|
88
|
-
/** @private
|
|
89
|
-
this.
|
|
90
|
-
/** @private
|
|
91
|
-
this.
|
|
46
|
+
/** @private MRMS-only timeline diagnostics (duration, raw vs filtered counts). */
|
|
47
|
+
this._mrmsTimelineLog = options.mrmsTimelineLog === true;
|
|
48
|
+
/** @private Dedupes repeated MRMS timeline log lines. */
|
|
49
|
+
this._mrmsLogKey = '';
|
|
92
50
|
this.layerId =
|
|
93
51
|
options.layerId ||
|
|
94
52
|
options.id ||
|
|
@@ -159,30 +117,6 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
159
117
|
// 1. CREATE an instance of the core engine
|
|
160
118
|
this.core = new AguaceroCore(options);
|
|
161
119
|
|
|
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
|
-
nexradDurationValue: this.core.state?.nexradDurationValue,
|
|
181
|
-
satelliteDurationValue: this.core.state?.satelliteDurationValue,
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
120
|
// 2. LISTEN for events from the core engine
|
|
187
121
|
this.core.on('state:change', (newState) => {
|
|
188
122
|
this._lastEmittedState = newState;
|
|
@@ -203,17 +137,44 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
203
137
|
}
|
|
204
138
|
|
|
205
139
|
/**
|
|
206
|
-
*
|
|
207
|
-
* @param {
|
|
208
|
-
* @param {object} [data]
|
|
140
|
+
* Logs MRMS timeline facts when `options.mrmsTimelineLog` is enabled (once per distinct snapshot).
|
|
141
|
+
* @param {object} state
|
|
209
142
|
*/
|
|
210
|
-
|
|
211
|
-
if (!this.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
143
|
+
_logMrmsTimelineIfEnabled(state) {
|
|
144
|
+
if (!this._mrmsTimelineLog || !state.isMRMS || !state.variable) return;
|
|
145
|
+
const raw = this.core.mrmsStatus?.[state.variable];
|
|
146
|
+
const rawLen = Array.isArray(raw) ? raw.length : 0;
|
|
147
|
+
const avail = state.availableTimestamps || [];
|
|
148
|
+
const filteredLen = avail.length;
|
|
149
|
+
let spanH = null;
|
|
150
|
+
let minT = null;
|
|
151
|
+
let maxT = null;
|
|
152
|
+
if (filteredLen) {
|
|
153
|
+
const nums = avail.map(Number).filter(Number.isFinite).sort((a, b) => a - b);
|
|
154
|
+
minT = nums[0];
|
|
155
|
+
maxT = nums[nums.length - 1];
|
|
156
|
+
spanH = (maxT - minT) / 3600;
|
|
157
|
+
}
|
|
158
|
+
const key = `${state.variable}|${state.mrmsDurationValue}|${state.mrmsTimestamp}|${rawLen}|${filteredLen}|${minT}|${maxT}`;
|
|
159
|
+
if (key === this._mrmsLogKey) return;
|
|
160
|
+
this._mrmsLogKey = key;
|
|
161
|
+
const rawSpanH =
|
|
162
|
+
rawLen > 1 && Array.isArray(raw)
|
|
163
|
+
? (Math.max(...raw.map(Number)) - Math.min(...raw.map(Number))) / 3600
|
|
164
|
+
: null;
|
|
165
|
+
console.log('[WeatherLayerManager MRMS]', {
|
|
166
|
+
variable: state.variable,
|
|
167
|
+
durationSettingHours: state.mrmsDurationValue,
|
|
168
|
+
selectedUnix: state.mrmsTimestamp,
|
|
169
|
+
rawStatusScanCount: rawLen,
|
|
170
|
+
rawWallClockSpanHoursApprox: rawSpanH != null ? Number(rawSpanH.toFixed(2)) : null,
|
|
171
|
+
filteredTimelineCount: filteredLen,
|
|
172
|
+
filteredSpanHoursApprox: spanH != null ? Number(spanH.toFixed(2)) : null,
|
|
173
|
+
filteredWindow:
|
|
174
|
+
minT != null && maxT != null ? { minUnix: minT, maxUnix: maxT } : null,
|
|
175
|
+
note:
|
|
176
|
+
'Timeline cannot extend beyond scans returned by the MRMS status feed; widening "hours" only includes more of that list when older scans exist.',
|
|
177
|
+
});
|
|
217
178
|
}
|
|
218
179
|
|
|
219
180
|
/**
|
|
@@ -262,38 +223,11 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
262
223
|
* @private
|
|
263
224
|
*/
|
|
264
225
|
_handleStateChange(state) {
|
|
265
|
-
const seq = this._debug ? ++this._debugStateSeq : 0;
|
|
266
226
|
/** 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. */
|
|
267
227
|
let deferNwsSyncUntilNexradReady = false;
|
|
268
228
|
try {
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
? _debugSummarizeNumericSeries(state.availableTimestamps || [])
|
|
272
|
-
: null;
|
|
273
|
-
this._debugLog('state:change', {
|
|
274
|
-
seq,
|
|
275
|
-
mode: state.isSatellite
|
|
276
|
-
? 'satellite'
|
|
277
|
-
: state.isNexrad
|
|
278
|
-
? 'nexrad'
|
|
279
|
-
: state.isMRMS
|
|
280
|
-
? 'mrms'
|
|
281
|
-
: 'model',
|
|
282
|
-
model: state.model,
|
|
283
|
-
variable: state.variable,
|
|
284
|
-
runKey: `${state.model}-${state.date}-${state.run}-${state.variable}`,
|
|
285
|
-
timeKey: state.isMRMS
|
|
286
|
-
? (state.mrmsTimestamp == null ? null : Number(state.mrmsTimestamp))
|
|
287
|
-
: Number(state.forecastHour),
|
|
288
|
-
mrmsTimestamp: state.mrmsTimestamp,
|
|
289
|
-
mrmsDurationValue: state.mrmsDurationValue,
|
|
290
|
-
availableTimestampsSummary: tsSummary,
|
|
291
|
-
availableHoursLen: Array.isArray(state.availableHours) ? state.availableHours.length : 0,
|
|
292
|
-
shaderLayerRunKey: this.shaderLayer?.runKey ?? null,
|
|
293
|
-
currentLoadedTimeKey: this.currentLoadedTimeKey,
|
|
294
|
-
rebuildId: this.currentRebuildId,
|
|
295
|
-
initialGridLoadPending: this._initialGridLoadPending,
|
|
296
|
-
});
|
|
229
|
+
if (state.isMRMS) {
|
|
230
|
+
this._logMrmsTimelineIfEnabled(state);
|
|
297
231
|
}
|
|
298
232
|
|
|
299
233
|
if (state.isSatellite) {
|
|
@@ -325,21 +259,6 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
325
259
|
: Number(state.forecastHour);
|
|
326
260
|
const runKey = `${state.model}-${state.date}-${state.run}-${state.variable}`;
|
|
327
261
|
|
|
328
|
-
if (this._debug && state.isMRMS) {
|
|
329
|
-
this._debugLog('grid path (mrms/model)', {
|
|
330
|
-
seq,
|
|
331
|
-
prevMrmsDurationValue: prevMrmsDur,
|
|
332
|
-
mrmsDurationChanged,
|
|
333
|
-
willRebuild: !this.shaderLayer || this.shaderLayer.runKey !== runKey,
|
|
334
|
-
willUpdateData:
|
|
335
|
-
this.shaderLayer &&
|
|
336
|
-
this.shaderLayer.runKey === runKey &&
|
|
337
|
-
this.currentLoadedTimeKey !== timeKey,
|
|
338
|
-
shaderRunKey: this.shaderLayer?.runKey,
|
|
339
|
-
targetRunKey: runKey,
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
|
|
343
262
|
if (!this.shaderLayer || this.shaderLayer.runKey !== runKey) {
|
|
344
263
|
this._rebuildLayerAndPreload(state);
|
|
345
264
|
} else if (this.currentLoadedTimeKey !== timeKey) {
|
|
@@ -349,12 +268,6 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
349
268
|
timeKey === this._rebuildTargetTimeKey;
|
|
350
269
|
if (!duplicateBeforeFirstPaint) {
|
|
351
270
|
this._updateLayerData(state);
|
|
352
|
-
} else if (this._debug) {
|
|
353
|
-
this._debugLog('skip _updateLayerData (duplicate before first paint)', {
|
|
354
|
-
seq,
|
|
355
|
-
timeKey,
|
|
356
|
-
_rebuildTargetTimeKey: this._rebuildTargetTimeKey,
|
|
357
|
-
});
|
|
358
271
|
}
|
|
359
272
|
}
|
|
360
273
|
|
|
@@ -365,13 +278,6 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
365
278
|
}
|
|
366
279
|
|
|
367
280
|
if (state.isMRMS && this.shaderLayer && mrmsDurationChanged) {
|
|
368
|
-
if (this._debug) {
|
|
369
|
-
this._debugLog('_preloadAllTimeSteps (mrms duration changed)', {
|
|
370
|
-
seq,
|
|
371
|
-
from: prevMrmsDur,
|
|
372
|
-
to: state.mrmsDurationValue,
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
281
|
this._preloadAllTimeSteps(state);
|
|
376
282
|
}
|
|
377
283
|
} finally {
|
|
@@ -418,12 +324,17 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
418
324
|
const enabled = masterEnabled && !isModelMode;
|
|
419
325
|
|
|
420
326
|
let timelineUnix = null;
|
|
327
|
+
/** Same list the core uses for the time slider — enables NWS “live edge” (wall clock at latest step) like aguacero-frontend. */
|
|
328
|
+
let timelineTimes = null;
|
|
421
329
|
if (state.isSatellite) {
|
|
422
330
|
timelineUnix = state.satelliteTimestamp == null ? null : Number(state.satelliteTimestamp);
|
|
331
|
+
timelineTimes = state.availableSatelliteTimestamps;
|
|
423
332
|
} else if (state.isNexrad) {
|
|
424
333
|
timelineUnix = state.nexradTimestamp == null ? null : Number(state.nexradTimestamp);
|
|
334
|
+
timelineTimes = state.availableNexradTimestamps;
|
|
425
335
|
} else if (state.isMRMS) {
|
|
426
336
|
timelineUnix = state.mrmsTimestamp == null ? null : Number(state.mrmsTimestamp);
|
|
337
|
+
timelineTimes = state.availableTimestamps;
|
|
427
338
|
}
|
|
428
339
|
|
|
429
340
|
if (!this._nwsOverlay) {
|
|
@@ -453,7 +364,7 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
453
364
|
...(wopt.activeOnlyRealtime !== undefined ? { activeOnlyRealtime: wopt.activeOnlyRealtime } : {}),
|
|
454
365
|
},
|
|
455
366
|
});
|
|
456
|
-
this._nwsOverlay.syncWithMode({ enabled, timelineUnix });
|
|
367
|
+
this._nwsOverlay.syncWithMode({ enabled, timelineUnix, timelineTimes });
|
|
457
368
|
}
|
|
458
369
|
|
|
459
370
|
/**
|
|
@@ -784,68 +695,10 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
784
695
|
// to the core engine. This keeps the API consistent for your users.
|
|
785
696
|
|
|
786
697
|
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
698
|
if (this.autoRefreshEnabled) {
|
|
806
699
|
this.setAutoRefresh(true, this.autoRefreshIntervalSeconds);
|
|
807
700
|
}
|
|
808
|
-
|
|
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
|
-
}
|
|
701
|
+
return this.core.initialize({ ...options, autoRefresh: false });
|
|
849
702
|
}
|
|
850
703
|
async setState(newState) { return this.core.setState(newState); }
|
|
851
704
|
play() { this.core.play(); }
|
|
@@ -983,17 +836,6 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
983
836
|
const out = (timeSteps || [])
|
|
984
837
|
.map(t => Number(t))
|
|
985
838
|
.filter(t => !Number.isNaN(t));
|
|
986
|
-
if (this._debug && state.isMRMS) {
|
|
987
|
-
this._debugLog('_collectNormalizedTimelineSteps', {
|
|
988
|
-
variable: state.variable,
|
|
989
|
-
mrmsDurationValue: state.mrmsDurationValue,
|
|
990
|
-
fromStateLen: fromState.length,
|
|
991
|
-
fromCoreLen: fromCore.length,
|
|
992
|
-
chosenSource: mrmsTimelineSource || (state.isMRMS ? 'n/a' : 'model'),
|
|
993
|
-
normalizedLen: out.length,
|
|
994
|
-
summary: _debugSummarizeNumericSeries(out),
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
839
|
return out;
|
|
998
840
|
}
|
|
999
841
|
|
|
@@ -1008,50 +850,27 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1008
850
|
if (mode === 'rebuild') {
|
|
1009
851
|
this._initialGridLoadPending = false;
|
|
1010
852
|
}
|
|
1011
|
-
this._debugLog('_runParallelGridFrameLoads:skip', {
|
|
1012
|
-
reason: !times.length ? 'no times' : 'no shaderLayer',
|
|
1013
|
-
mode,
|
|
1014
|
-
rebuildId,
|
|
1015
|
-
timesLen: times.length,
|
|
1016
|
-
});
|
|
1017
853
|
return;
|
|
1018
854
|
}
|
|
1019
855
|
|
|
1020
|
-
// Number(null) === 0
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
856
|
+
// Number(null) === 0 — treat null as NaN. For MRMS, when the active timestep is unset,
|
|
857
|
+
// prefer the **newest** scan in the timeline (matches AguaceroCore default); model grids
|
|
858
|
+
// keep using the first forecast hour in the list.
|
|
1023
859
|
const _rawFrameTime = state.isMRMS ? state.mrmsTimestamp : state.forecastHour;
|
|
1024
860
|
const currentFrameTime = _rawFrameTime == null ? NaN : Number(_rawFrameTime);
|
|
1025
|
-
/** When the active timestep is unset/NaN, paint the first timeline step (legacy single-fetch behavior). */
|
|
1026
861
|
let primaryTimeForRebuild = Number.NaN;
|
|
1027
862
|
if (mode === 'rebuild') {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
863
|
+
if (Number.isFinite(currentFrameTime)) {
|
|
864
|
+
primaryTimeForRebuild = currentFrameTime;
|
|
865
|
+
} else if (times.length > 0) {
|
|
866
|
+
const finite = times.map(Number).filter(Number.isFinite);
|
|
867
|
+
primaryTimeForRebuild = state.isMRMS ? Math.max(...finite) : Number(times[0]);
|
|
868
|
+
}
|
|
1031
869
|
}
|
|
1032
870
|
const tsKey = state.isMRMS ? 'mrmsTimestamp' : 'forecastHour';
|
|
1033
871
|
const gridModel = state.isMRMS ? 'mrms' : state.model;
|
|
1034
872
|
const { gridDef } = this.core._getGridCornersAndDef(gridModel);
|
|
1035
873
|
|
|
1036
|
-
this._debugLog('_runParallelGridFrameLoads', {
|
|
1037
|
-
mode,
|
|
1038
|
-
rebuildId,
|
|
1039
|
-
gridModel,
|
|
1040
|
-
timesLen: times.length,
|
|
1041
|
-
timesSummary: _debugSummarizeNumericSeries(times),
|
|
1042
|
-
currentFrameTime,
|
|
1043
|
-
primaryTimeForRebuild:
|
|
1044
|
-
mode === 'rebuild'
|
|
1045
|
-
? Number.isFinite(currentFrameTime)
|
|
1046
|
-
? currentFrameTime
|
|
1047
|
-
: times[0]
|
|
1048
|
-
: undefined,
|
|
1049
|
-
tsKey,
|
|
1050
|
-
gridNxNy: gridDef?.grid_params
|
|
1051
|
-
? { nx: gridDef.grid_params.nx, ny: gridDef.grid_params.ny }
|
|
1052
|
-
: null,
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
874
|
times.forEach((time) => {
|
|
1056
875
|
const stateForTime = { ...state, [tsKey]: time };
|
|
1057
876
|
this.core._loadGridData(stateForTime)
|
|
@@ -1080,21 +899,10 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1080
899
|
: Number(this.core.state.mrmsTimestamp))
|
|
1081
900
|
: Number(this.core.state.forecastHour);
|
|
1082
901
|
if (coreTimeKey !== this._rebuildTargetTimeKey) {
|
|
1083
|
-
this._debugLog('_loadGridData:skip primary (core time drifted vs rebuild target)', {
|
|
1084
|
-
time,
|
|
1085
|
-
coreTimeKey,
|
|
1086
|
-
_rebuildTargetTimeKey: this._rebuildTargetTimeKey,
|
|
1087
|
-
rebuildId,
|
|
1088
|
-
});
|
|
1089
902
|
return;
|
|
1090
903
|
}
|
|
1091
904
|
}
|
|
1092
905
|
if (!grid?.data) {
|
|
1093
|
-
this._debugLog('_loadGridData:primary frame missing grid.data', {
|
|
1094
|
-
time,
|
|
1095
|
-
rebuildId,
|
|
1096
|
-
mode,
|
|
1097
|
-
});
|
|
1098
906
|
this._initialGridLoadPending = false;
|
|
1099
907
|
return;
|
|
1100
908
|
}
|
|
@@ -1127,17 +935,9 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1127
935
|
this.currentLoadedTimeKey = time;
|
|
1128
936
|
this.map.triggerRepaint();
|
|
1129
937
|
}
|
|
1130
|
-
} else if (this._debug && mode === 'append') {
|
|
1131
|
-
this._debugLog('_loadGridData:empty grid (append)', { time, mode, rebuildId });
|
|
1132
938
|
}
|
|
1133
939
|
})
|
|
1134
|
-
.catch((
|
|
1135
|
-
this._debugLog('_loadGridData:error', {
|
|
1136
|
-
time,
|
|
1137
|
-
mode,
|
|
1138
|
-
rebuildId,
|
|
1139
|
-
message: err?.message || String(err),
|
|
1140
|
-
});
|
|
940
|
+
.catch(() => {
|
|
1141
941
|
if (
|
|
1142
942
|
mode === 'rebuild' &&
|
|
1143
943
|
Number.isFinite(primaryTimeForRebuild) &&
|
|
@@ -1215,26 +1015,8 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1215
1015
|
if (timesToLoad.length === 0 && !Number.isNaN(currentFrameTime)) {
|
|
1216
1016
|
timesToLoad = [currentFrameTime];
|
|
1217
1017
|
}
|
|
1218
|
-
this._debugLog('_rebuildLayerAndPreload:timeline', {
|
|
1219
|
-
rebuildId,
|
|
1220
|
-
isMRMS: state.isMRMS,
|
|
1221
|
-
variable: state.variable,
|
|
1222
|
-
normalizedLen: normalized.length,
|
|
1223
|
-
normalizedSummary: _debugSummarizeNumericSeries(normalized),
|
|
1224
|
-
currentFrameTime,
|
|
1225
|
-
timesToLoadLen: timesToLoad.length,
|
|
1226
|
-
timesToLoadSummary: _debugSummarizeNumericSeries(timesToLoad),
|
|
1227
|
-
insertBeforeId: beforeId ?? '(stack top)',
|
|
1228
|
-
});
|
|
1229
1018
|
if (timesToLoad.length === 0) {
|
|
1230
1019
|
this._initialGridLoadPending = false;
|
|
1231
|
-
this._debugLog('_rebuildLayerAndPreload:no times to load — check availableTimestamps / duration window', {
|
|
1232
|
-
rebuildId,
|
|
1233
|
-
availableTimestampsLen: Array.isArray(state.availableTimestamps)
|
|
1234
|
-
? state.availableTimestamps.length
|
|
1235
|
-
: 0,
|
|
1236
|
-
mrmsDurationValue: state.mrmsDurationValue,
|
|
1237
|
-
});
|
|
1238
1020
|
return;
|
|
1239
1021
|
}
|
|
1240
1022
|
|
|
@@ -1249,21 +1031,11 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1249
1031
|
const currentFrameTime = _rawPft == null ? NaN : Number(_rawPft);
|
|
1250
1032
|
const stepsToPreload = normalized.filter(t => t !== currentFrameTime);
|
|
1251
1033
|
|
|
1252
|
-
this._debugLog('_preloadAllTimeSteps', {
|
|
1253
|
-
normalizedLen: normalized.length,
|
|
1254
|
-
currentFrameTime,
|
|
1255
|
-
stepsToPreloadLen: stepsToPreload.length,
|
|
1256
|
-
stepsSummary: _debugSummarizeNumericSeries(stepsToPreload),
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
1034
|
if (normalized.length === 0) {
|
|
1260
1035
|
return;
|
|
1261
1036
|
}
|
|
1262
1037
|
|
|
1263
1038
|
if (stepsToPreload.length === 0) {
|
|
1264
|
-
this._debugLog('_preloadAllTimeSteps:skip (nothing to preload besides current)', {
|
|
1265
|
-
currentFrameTime,
|
|
1266
|
-
});
|
|
1267
1039
|
return;
|
|
1268
1040
|
}
|
|
1269
1041
|
|
|
@@ -1510,7 +1282,6 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1510
1282
|
* Cleans up all resources.
|
|
1511
1283
|
*/
|
|
1512
1284
|
destroy() {
|
|
1513
|
-
this._debugLog('destroy');
|
|
1514
1285
|
this.setAutoRefresh(false);
|
|
1515
1286
|
|
|
1516
1287
|
// 1. Unbind the map's mousemove event listener
|
package/src/nwsAlertsSupport.js
CHANGED
|
@@ -442,6 +442,98 @@ export function getNwsAlertInstanceIdFromFeature(feature) {
|
|
|
442
442
|
);
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/**
|
|
446
|
+
* SVR revision visibility: superseded geometry only before cutover unix; updated geometry only at/after cutover.
|
|
447
|
+
* Returns null when this feature does not use revision split (fall back to start_unix / end_unix).
|
|
448
|
+
* Matches aguacero-frontend {@link nwsRevisionVisibilityActive}.
|
|
449
|
+
*/
|
|
450
|
+
export function nwsRevisionVisibilityActive(properties, referenceUnix) {
|
|
451
|
+
if (!properties) return null;
|
|
452
|
+
const role = properties._nws_revision_role;
|
|
453
|
+
const cut = properties._nws_revision_cutover_unix;
|
|
454
|
+
if (cut == null || typeof cut !== 'number' || !role) return null;
|
|
455
|
+
if (role === 'superseded') {
|
|
456
|
+
return referenceUnix < cut;
|
|
457
|
+
}
|
|
458
|
+
if (role === 'update') {
|
|
459
|
+
return referenceUnix >= cut;
|
|
460
|
+
}
|
|
461
|
+
if (role === 'current') {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** nwws-server stores ids as `${vtecBaseId}.${issuedTs}`; strip the issued suffix for revision grouping. */
|
|
468
|
+
function stripNwwsIssuedMillisSuffixFromId(id) {
|
|
469
|
+
const m = String(id).match(/^(.+)\.(\d{10,})$/);
|
|
470
|
+
return m ? m[1] : id;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* One key per logical warning product (VTEC sequence / base id). Matches aguacero-frontend
|
|
475
|
+
* {@link getNwwsLogicalProductKey} for deduping superseded vs update rows on the time slider.
|
|
476
|
+
*/
|
|
477
|
+
export function getNwwsLogicalProductKey(feature) {
|
|
478
|
+
const p = feature?.properties ?? {};
|
|
479
|
+
const params = p.parameters;
|
|
480
|
+
const vtecRaw = params != null && typeof params === 'object' && !Array.isArray(params) ? params.VTEC : undefined;
|
|
481
|
+
const vtec = Array.isArray(vtecRaw)
|
|
482
|
+
? vtecRaw[0]
|
|
483
|
+
: p.vtec ?? (p.details && typeof p.details === 'object' ? p.details.pvtec : undefined);
|
|
484
|
+
if (typeof vtec === 'string' && vtec.trim()) {
|
|
485
|
+
const parts = vtec.split('.');
|
|
486
|
+
if (parts.length >= 6) {
|
|
487
|
+
return `${parts[2]}.${parts[3]}.${parts[4]}.${parts[5]}`;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const base = getNwsBaseAlertIdFromFeature(feature);
|
|
491
|
+
if (base) return stripNwwsIssuedMillisSuffixFromId(base);
|
|
492
|
+
if (feature?.id != null && feature.id !== '') return stripNwwsIssuedMillisSuffixFromId(String(feature.id));
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function getNwwsProductKeyFromFeature(feature) {
|
|
497
|
+
const pk = feature?.properties?.nws_product_key;
|
|
498
|
+
if (typeof pk === 'string' && pk.trim() !== '') return pk.trim();
|
|
499
|
+
return getNwwsLogicalProductKey(feature);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* When superseded + update rows both pass time checks, pick one per reference instant (same product key).
|
|
504
|
+
* Matches aguacero-frontend {@link pickBestNwsRevisionAmongConflictingFeatures}.
|
|
505
|
+
*/
|
|
506
|
+
export function pickBestNwsRevisionAmongConflictingFeatures(features, referenceUnix) {
|
|
507
|
+
if (!Array.isArray(features) || features.length === 0) return null;
|
|
508
|
+
if (features.length === 1) return features[0];
|
|
509
|
+
const scored = features.map((f) => {
|
|
510
|
+
const p = f?.properties ?? {};
|
|
511
|
+
const role = String(p._nws_revision_role ?? '');
|
|
512
|
+
const cut =
|
|
513
|
+
typeof p._nws_revision_cutover_unix === 'number' && Number.isFinite(p._nws_revision_cutover_unix)
|
|
514
|
+
? p._nws_revision_cutover_unix
|
|
515
|
+
: null;
|
|
516
|
+
let slotScore = 0;
|
|
517
|
+
if (role === 'superseded' && cut != null) {
|
|
518
|
+
slotScore = referenceUnix < cut ? 2 : 0;
|
|
519
|
+
} else if (role === 'update' && cut != null) {
|
|
520
|
+
slotScore = referenceUnix >= cut ? 2 : 0;
|
|
521
|
+
} else if (role === 'current') {
|
|
522
|
+
slotScore = 1;
|
|
523
|
+
} else {
|
|
524
|
+
slotScore = 1;
|
|
525
|
+
}
|
|
526
|
+
const t =
|
|
527
|
+
parseNwsTimeToUnix(getNwsTimeProp(p, ['sent', 'updated_at', 'issued_at', 'issued'])) ?? 0;
|
|
528
|
+
return { f, slotScore, t };
|
|
529
|
+
});
|
|
530
|
+
scored.sort((a, b) => {
|
|
531
|
+
if (b.slotScore !== a.slotScore) return b.slotScore - a.slotScore;
|
|
532
|
+
return b.t - a.t;
|
|
533
|
+
});
|
|
534
|
+
return scored[0].f;
|
|
535
|
+
}
|
|
536
|
+
|
|
445
537
|
export function isUsableNwwsAlertGeometry(geometry) {
|
|
446
538
|
if (geometry == null || typeof geometry !== 'object') return false;
|
|
447
539
|
const g = geometry;
|