@aguacerowx/mapsgl 0.0.48 → 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 +2 -2
- package/src/NwsWatchesWarningsOverlay.js +85 -22
- package/src/WeatherLayerManager.js +79 -297
- package/src/defaultBasisBaseUrl.js +4 -33
- package/src/nwsAlertsSupport.js +92 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aguacerowx/mapsgl",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.50",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"esbuild": "^0.21.5"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@aguacerowx/javascript-sdk": "^0.0.
|
|
24
|
+
"@aguacerowx/javascript-sdk": "^0.0.21",
|
|
25
25
|
"buffer": "^6.0.3",
|
|
26
26
|
"fzstd": "^0.1.1",
|
|
27
27
|
"mapbox-gl": "^3.4.0",
|
|
@@ -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,29 +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
|
-
satelliteDurationValue: this.core.state?.satelliteDurationValue,
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
120
|
// 2. LISTEN for events from the core engine
|
|
186
121
|
this.core.on('state:change', (newState) => {
|
|
187
122
|
this._lastEmittedState = newState;
|
|
@@ -202,17 +137,44 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
202
137
|
}
|
|
203
138
|
|
|
204
139
|
/**
|
|
205
|
-
*
|
|
206
|
-
* @param {
|
|
207
|
-
* @param {object} [data]
|
|
140
|
+
* Logs MRMS timeline facts when `options.mrmsTimelineLog` is enabled (once per distinct snapshot).
|
|
141
|
+
* @param {object} state
|
|
208
142
|
*/
|
|
209
|
-
|
|
210
|
-
if (!this.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
});
|
|
216
178
|
}
|
|
217
179
|
|
|
218
180
|
/**
|
|
@@ -261,38 +223,11 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
261
223
|
* @private
|
|
262
224
|
*/
|
|
263
225
|
_handleStateChange(state) {
|
|
264
|
-
const seq = this._debug ? ++this._debugStateSeq : 0;
|
|
265
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. */
|
|
266
227
|
let deferNwsSyncUntilNexradReady = false;
|
|
267
228
|
try {
|
|
268
|
-
if (
|
|
269
|
-
|
|
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
|
-
});
|
|
229
|
+
if (state.isMRMS) {
|
|
230
|
+
this._logMrmsTimelineIfEnabled(state);
|
|
296
231
|
}
|
|
297
232
|
|
|
298
233
|
if (state.isSatellite) {
|
|
@@ -324,21 +259,6 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
324
259
|
: Number(state.forecastHour);
|
|
325
260
|
const runKey = `${state.model}-${state.date}-${state.run}-${state.variable}`;
|
|
326
261
|
|
|
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
262
|
if (!this.shaderLayer || this.shaderLayer.runKey !== runKey) {
|
|
343
263
|
this._rebuildLayerAndPreload(state);
|
|
344
264
|
} else if (this.currentLoadedTimeKey !== timeKey) {
|
|
@@ -348,12 +268,6 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
348
268
|
timeKey === this._rebuildTargetTimeKey;
|
|
349
269
|
if (!duplicateBeforeFirstPaint) {
|
|
350
270
|
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
271
|
}
|
|
358
272
|
}
|
|
359
273
|
|
|
@@ -364,13 +278,6 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
364
278
|
}
|
|
365
279
|
|
|
366
280
|
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
281
|
this._preloadAllTimeSteps(state);
|
|
375
282
|
}
|
|
376
283
|
} finally {
|
|
@@ -417,12 +324,17 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
417
324
|
const enabled = masterEnabled && !isModelMode;
|
|
418
325
|
|
|
419
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;
|
|
420
329
|
if (state.isSatellite) {
|
|
421
330
|
timelineUnix = state.satelliteTimestamp == null ? null : Number(state.satelliteTimestamp);
|
|
331
|
+
timelineTimes = state.availableSatelliteTimestamps;
|
|
422
332
|
} else if (state.isNexrad) {
|
|
423
333
|
timelineUnix = state.nexradTimestamp == null ? null : Number(state.nexradTimestamp);
|
|
334
|
+
timelineTimes = state.availableNexradTimestamps;
|
|
424
335
|
} else if (state.isMRMS) {
|
|
425
336
|
timelineUnix = state.mrmsTimestamp == null ? null : Number(state.mrmsTimestamp);
|
|
337
|
+
timelineTimes = state.availableTimestamps;
|
|
426
338
|
}
|
|
427
339
|
|
|
428
340
|
if (!this._nwsOverlay) {
|
|
@@ -452,7 +364,7 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
452
364
|
...(wopt.activeOnlyRealtime !== undefined ? { activeOnlyRealtime: wopt.activeOnlyRealtime } : {}),
|
|
453
365
|
},
|
|
454
366
|
});
|
|
455
|
-
this._nwsOverlay.syncWithMode({ enabled, timelineUnix });
|
|
367
|
+
this._nwsOverlay.syncWithMode({ enabled, timelineUnix, timelineTimes });
|
|
456
368
|
}
|
|
457
369
|
|
|
458
370
|
/**
|
|
@@ -570,13 +482,13 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
570
482
|
this.currentLoadedTimeKey = null;
|
|
571
483
|
}
|
|
572
484
|
|
|
573
|
-
if (!state.
|
|
485
|
+
if (!state.satelliteInstrumentId || state.satelliteTimestamp == null) {
|
|
574
486
|
this._tearDownSatellite();
|
|
575
487
|
this.map.triggerRepaint();
|
|
576
488
|
return;
|
|
577
489
|
}
|
|
578
490
|
|
|
579
|
-
const satRunKey = `${state.
|
|
491
|
+
const satRunKey = `${state.satelliteInstrumentId}|${state.satelliteSectorLabel}|${state.satelliteChannel}|${state.variable || ''}`;
|
|
580
492
|
if (!this.satelliteLayer || this._satelliteRunKey !== satRunKey) {
|
|
581
493
|
this._rebuildSatelliteLayer(state, satRunKey);
|
|
582
494
|
}
|
|
@@ -668,20 +580,19 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
668
580
|
|
|
669
581
|
_satelliteTimelineId(state) {
|
|
670
582
|
const keys = Object.keys(state.satelliteTimeToFileMap || {}).sort((a, b) => Number(a) - Number(b));
|
|
671
|
-
return `${state.
|
|
583
|
+
return `${state.satelliteInstrumentId}|${state.satelliteSectorLabel}|${state.satelliteChannel}|${state.variable || ''}|${keys.join(',')}`;
|
|
672
584
|
}
|
|
673
585
|
|
|
674
586
|
_getSatelliteFetchParts(state, satelliteTimestamp) {
|
|
675
587
|
const fileName = resolveSatelliteS3FileName(
|
|
676
|
-
state.
|
|
588
|
+
state.satelliteChannel,
|
|
677
589
|
state.satelliteTimeToFileMap,
|
|
678
590
|
satelliteTimestamp
|
|
679
591
|
);
|
|
680
592
|
if (!fileName) {
|
|
681
593
|
return null;
|
|
682
594
|
}
|
|
683
|
-
const
|
|
684
|
-
const channelName = parts[2] || state.variable;
|
|
595
|
+
const channelName = state.satelliteChannel || state.variable;
|
|
685
596
|
const channelNameUpper = String(channelName).toUpperCase();
|
|
686
597
|
let s3FileName = fileName.replace('MULTI', channelNameUpper);
|
|
687
598
|
if (!s3FileName.endsWith('.ktx2')) {
|
|
@@ -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(); }
|
|
@@ -871,9 +724,15 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
871
724
|
async setSatelliteDurationValue(value) {
|
|
872
725
|
return this.core.setSatelliteDurationValue(value);
|
|
873
726
|
}
|
|
727
|
+
async setSatelliteSelection(opts) {
|
|
728
|
+
return this.core.setSatelliteSelection(opts);
|
|
729
|
+
}
|
|
874
730
|
async setMRMSDurationValue(value) {
|
|
875
731
|
return this.core.setMRMSDurationValue(value);
|
|
876
732
|
}
|
|
733
|
+
async setNexradDurationValue(value) {
|
|
734
|
+
return this.core.setNexradDurationValue(value);
|
|
735
|
+
}
|
|
877
736
|
|
|
878
737
|
/** Refreshes NEXRAD object-key listings for the current site / product / tilt (NEXRAD mode only). */
|
|
879
738
|
async refreshNexradTimes() {
|
|
@@ -977,17 +836,6 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
977
836
|
const out = (timeSteps || [])
|
|
978
837
|
.map(t => Number(t))
|
|
979
838
|
.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
839
|
return out;
|
|
992
840
|
}
|
|
993
841
|
|
|
@@ -1002,50 +850,27 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1002
850
|
if (mode === 'rebuild') {
|
|
1003
851
|
this._initialGridLoadPending = false;
|
|
1004
852
|
}
|
|
1005
|
-
this._debugLog('_runParallelGridFrameLoads:skip', {
|
|
1006
|
-
reason: !times.length ? 'no times' : 'no shaderLayer',
|
|
1007
|
-
mode,
|
|
1008
|
-
rebuildId,
|
|
1009
|
-
timesLen: times.length,
|
|
1010
|
-
});
|
|
1011
853
|
return;
|
|
1012
854
|
}
|
|
1013
855
|
|
|
1014
|
-
// Number(null) === 0
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
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.
|
|
1017
859
|
const _rawFrameTime = state.isMRMS ? state.mrmsTimestamp : state.forecastHour;
|
|
1018
860
|
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
861
|
let primaryTimeForRebuild = Number.NaN;
|
|
1021
862
|
if (mode === 'rebuild') {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
+
}
|
|
1025
869
|
}
|
|
1026
870
|
const tsKey = state.isMRMS ? 'mrmsTimestamp' : 'forecastHour';
|
|
1027
871
|
const gridModel = state.isMRMS ? 'mrms' : state.model;
|
|
1028
872
|
const { gridDef } = this.core._getGridCornersAndDef(gridModel);
|
|
1029
873
|
|
|
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
874
|
times.forEach((time) => {
|
|
1050
875
|
const stateForTime = { ...state, [tsKey]: time };
|
|
1051
876
|
this.core._loadGridData(stateForTime)
|
|
@@ -1074,21 +899,10 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1074
899
|
: Number(this.core.state.mrmsTimestamp))
|
|
1075
900
|
: Number(this.core.state.forecastHour);
|
|
1076
901
|
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
902
|
return;
|
|
1084
903
|
}
|
|
1085
904
|
}
|
|
1086
905
|
if (!grid?.data) {
|
|
1087
|
-
this._debugLog('_loadGridData:primary frame missing grid.data', {
|
|
1088
|
-
time,
|
|
1089
|
-
rebuildId,
|
|
1090
|
-
mode,
|
|
1091
|
-
});
|
|
1092
906
|
this._initialGridLoadPending = false;
|
|
1093
907
|
return;
|
|
1094
908
|
}
|
|
@@ -1121,17 +935,9 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1121
935
|
this.currentLoadedTimeKey = time;
|
|
1122
936
|
this.map.triggerRepaint();
|
|
1123
937
|
}
|
|
1124
|
-
} else if (this._debug && mode === 'append') {
|
|
1125
|
-
this._debugLog('_loadGridData:empty grid (append)', { time, mode, rebuildId });
|
|
1126
938
|
}
|
|
1127
939
|
})
|
|
1128
|
-
.catch((
|
|
1129
|
-
this._debugLog('_loadGridData:error', {
|
|
1130
|
-
time,
|
|
1131
|
-
mode,
|
|
1132
|
-
rebuildId,
|
|
1133
|
-
message: err?.message || String(err),
|
|
1134
|
-
});
|
|
940
|
+
.catch(() => {
|
|
1135
941
|
if (
|
|
1136
942
|
mode === 'rebuild' &&
|
|
1137
943
|
Number.isFinite(primaryTimeForRebuild) &&
|
|
@@ -1209,26 +1015,8 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1209
1015
|
if (timesToLoad.length === 0 && !Number.isNaN(currentFrameTime)) {
|
|
1210
1016
|
timesToLoad = [currentFrameTime];
|
|
1211
1017
|
}
|
|
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
1018
|
if (timesToLoad.length === 0) {
|
|
1224
1019
|
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
1020
|
return;
|
|
1233
1021
|
}
|
|
1234
1022
|
|
|
@@ -1243,21 +1031,11 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1243
1031
|
const currentFrameTime = _rawPft == null ? NaN : Number(_rawPft);
|
|
1244
1032
|
const stepsToPreload = normalized.filter(t => t !== currentFrameTime);
|
|
1245
1033
|
|
|
1246
|
-
this._debugLog('_preloadAllTimeSteps', {
|
|
1247
|
-
normalizedLen: normalized.length,
|
|
1248
|
-
currentFrameTime,
|
|
1249
|
-
stepsToPreloadLen: stepsToPreload.length,
|
|
1250
|
-
stepsSummary: _debugSummarizeNumericSeries(stepsToPreload),
|
|
1251
|
-
});
|
|
1252
|
-
|
|
1253
1034
|
if (normalized.length === 0) {
|
|
1254
1035
|
return;
|
|
1255
1036
|
}
|
|
1256
1037
|
|
|
1257
1038
|
if (stepsToPreload.length === 0) {
|
|
1258
|
-
this._debugLog('_preloadAllTimeSteps:skip (nothing to preload besides current)', {
|
|
1259
|
-
currentFrameTime,
|
|
1260
|
-
});
|
|
1261
1039
|
return;
|
|
1262
1040
|
}
|
|
1263
1041
|
|
|
@@ -1389,7 +1167,7 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1389
1167
|
const s = this.core.state;
|
|
1390
1168
|
const { isMRMS, isSatellite, isNexrad, model: currentModel, variable: currentVariable, date, run } = s;
|
|
1391
1169
|
|
|
1392
|
-
if (isSatellite && s.
|
|
1170
|
+
if (isSatellite && s.satelliteInstrumentId) {
|
|
1393
1171
|
const prevTimeline = this.core._computeSatelliteTimeline();
|
|
1394
1172
|
const prevTimes = [...(prevTimeline.unixTimes || [])]
|
|
1395
1173
|
.map((t) => Number(t))
|
|
@@ -1413,7 +1191,12 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1413
1191
|
} else if (curSat != null && newMax != null && nextTimes.length && !nextTimes.includes(curSat)) {
|
|
1414
1192
|
await this.core.setSatelliteTimestamp(newMax);
|
|
1415
1193
|
}
|
|
1416
|
-
this.emit('data:updated', {
|
|
1194
|
+
this.emit('data:updated', {
|
|
1195
|
+
type: 'satellite',
|
|
1196
|
+
satelliteInstrumentId: s.satelliteInstrumentId,
|
|
1197
|
+
satelliteSectorLabel: s.satelliteSectorLabel,
|
|
1198
|
+
satelliteChannel: s.satelliteChannel,
|
|
1199
|
+
});
|
|
1417
1200
|
return;
|
|
1418
1201
|
}
|
|
1419
1202
|
|
|
@@ -1499,7 +1282,6 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1499
1282
|
* Cleans up all resources.
|
|
1500
1283
|
*/
|
|
1501
1284
|
destroy() {
|
|
1502
|
-
this._debugLog('destroy');
|
|
1503
1285
|
this.setAutoRefresh(false);
|
|
1504
1286
|
|
|
1505
1287
|
// 1. Unbind the map's mousemove event listener
|
|
@@ -1,40 +1,11 @@
|
|
|
1
|
-
import pkg from '../package.json' with { type: 'json' };
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* jsDelivr serves the `basis/` folder from the published npm package (no `public/basis/` copy).
|
|
5
3
|
* Use when you consume `@aguacerowx/mapsgl` from the registry and do not host transcoder assets yourself.
|
|
6
4
|
*/
|
|
7
|
-
export const JSDELIVR_BASIS_BASE_URL = `https://cdn.jsdelivr.net/npm/@aguacerowx/mapsgl
|
|
5
|
+
export const JSDELIVR_BASIS_BASE_URL = `https://cdn.jsdelivr.net/npm/@aguacerowx/mapsgl/basis/`;
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
|
-
* Default
|
|
11
|
-
*
|
|
12
|
-
* We cannot use `new URL('..', new URL('.', import.meta.url))`: Vite aliases `@aguacerowx/mapsgl` to
|
|
13
|
-
* `mapsgl/index.js`, so the "directory" of this module is often `.../mapsgl/` and `..` becomes the
|
|
14
|
-
* monorepo `packages/` folder — wrong (`packages/basis/` instead of `packages/mapsgl/basis/`).
|
|
15
|
-
* Webpack also rejects `new URL('../basis/', import.meta.url)` as an unresolved module, so we locate
|
|
16
|
-
* the `mapsgl` path segment and append `basis/`.
|
|
17
|
-
*
|
|
18
|
-
* In Vite production builds the folder URL becomes your app origin `/basis/`, so place copies under
|
|
19
|
-
* `public/basis/` (or sync from `node_modules/@aguacerowx/mapsgl/basis`).
|
|
20
|
-
* For CDN-only, pass {@link JSDELIVR_BASIS_BASE_URL} as `WeatherLayerManager({ basisBaseUrl: ... })`.
|
|
8
|
+
* Default to the jsDelivr CDN so users don't have to host the files themselves.
|
|
9
|
+
* You can override this by passing `basisBaseUrl` to `WeatherLayerManager`.
|
|
21
10
|
*/
|
|
22
|
-
const
|
|
23
|
-
const u = import.meta.url;
|
|
24
|
-
const slashIdx = u.indexOf('/mapsgl/');
|
|
25
|
-
if (slashIdx >= 0) {
|
|
26
|
-
return u.slice(0, slashIdx + '/mapsgl/'.length);
|
|
27
|
-
}
|
|
28
|
-
// e.g. cdn.jsdelivr.net/.../mapsgl@0.0.47/dist/... (no `/mapsgl/` substring)
|
|
29
|
-
const atMatch = u.match(/\/mapsgl@[^/]+\//);
|
|
30
|
-
if (atMatch && atMatch.index !== undefined) {
|
|
31
|
-
return u.slice(0, atMatch.index + atMatch[0].length);
|
|
32
|
-
}
|
|
33
|
-
const srcMarker = '/src/';
|
|
34
|
-
const j = u.lastIndexOf(srcMarker);
|
|
35
|
-
if (j >= 0) {
|
|
36
|
-
return `${u.slice(0, j)}/`;
|
|
37
|
-
}
|
|
38
|
-
return new URL('../', new URL('.', u)).href;
|
|
39
|
-
})();
|
|
40
|
-
export const DEFAULT_BASIS_BASE_URL = new URL('basis/', _mapsglRootHref).href;
|
|
11
|
+
export const DEFAULT_BASIS_BASE_URL = JSDELIVR_BASIS_BASE_URL;
|
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;
|