@aguacerowx/mapsgl 0.0.53 → 0.0.54
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 +31 -31
- package/src/NexradWeatherController.js +510 -510
- package/src/NwsWatchesWarningsOverlay.js +54 -3
- package/src/WeatherLayerManager.js +14 -1
- package/src/nexrad/nexradArchiveCache.ts +286 -66
- package/src/nexrad/nexradLevel3Products.ts +581 -581
- package/src/nexrad/radarArchiveCore.bundled.js +83 -5
- package/src/nwsAlertsFetchSpec.js +114 -0
- package/src/nwsAlertsSupport.js +28 -0
|
@@ -2895,16 +2895,94 @@ function applyLevel3StormRelativeToFrame(frame, stormSpeedMs, stormDirectionDeg)
|
|
|
2895
2895
|
}
|
|
2896
2896
|
|
|
2897
2897
|
// src/nexrad/nexradArchiveCache.ts
|
|
2898
|
-
var
|
|
2898
|
+
var NEXRAD_ARCHIVE_LEVEL2_BASE_URL = "https://d3dc62msmxkrd7.cloudfront.net/level-2";
|
|
2899
|
+
var NEXRAD_ARCHIVE_LEVEL3_BASE_URL = "https://unidata-nexrad-level3.s3.amazonaws.com";
|
|
2900
|
+
var MAX_ARCHIVE_CACHE_ENTRIES = 400;
|
|
2901
|
+
var MAX_ARCHIVE_CACHE_BYTES = 180 * 1024 * 1024;
|
|
2899
2902
|
var archiveCache = /* @__PURE__ */ new Map();
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2903
|
+
var pruneAllowlistObjectKeys = null;
|
|
2904
|
+
var pruneSupplementalObjectKeys = /* @__PURE__ */ new Set();
|
|
2905
|
+
function canInsertCacheKey(cacheKey, frame) {
|
|
2906
|
+
if (frame == null) return true;
|
|
2907
|
+
if (pruneAllowlistObjectKeys == null) return true;
|
|
2908
|
+
const o = extractObjectKeyFromCacheKey(cacheKey);
|
|
2909
|
+
if (o == null) return true;
|
|
2910
|
+
if (pruneAllowlistObjectKeys.has(o)) return true;
|
|
2911
|
+
if (pruneSupplementalObjectKeys.has(o)) return true;
|
|
2912
|
+
return false;
|
|
2913
|
+
}
|
|
2914
|
+
function estimateFrameBytes(value) {
|
|
2915
|
+
if (value == null) return 0;
|
|
2916
|
+
const g = value.gateData;
|
|
2917
|
+
const r = value.rayBoundariesDeg;
|
|
2918
|
+
const a = value.azimuthsDeg;
|
|
2919
|
+
return (g?.byteLength ?? 0) + (r?.byteLength ?? 0) + (a?.byteLength ?? 0);
|
|
2920
|
+
}
|
|
2921
|
+
var cacheBytesTotal = 0;
|
|
2922
|
+
var onArchiveCacheKeysRemoved = null;
|
|
2923
|
+
function emitArchiveCacheKeysRemoved(keys) {
|
|
2924
|
+
if (keys.length === 0) return;
|
|
2925
|
+
onArchiveCacheKeysRemoved?.(keys);
|
|
2926
|
+
}
|
|
2927
|
+
function recomputeCacheBytes() {
|
|
2928
|
+
let n = 0;
|
|
2929
|
+
for (const v of archiveCache.values()) {
|
|
2930
|
+
n += estimateFrameBytes(v);
|
|
2931
|
+
}
|
|
2932
|
+
cacheBytesTotal = n;
|
|
2933
|
+
}
|
|
2934
|
+
function evictOldestWhileOverCap() {
|
|
2935
|
+
const removed = [];
|
|
2936
|
+
while (archiveCache.size > MAX_ARCHIVE_CACHE_ENTRIES || cacheBytesTotal > MAX_ARCHIVE_CACHE_BYTES) {
|
|
2904
2937
|
const oldestKey = archiveCache.keys().next().value;
|
|
2905
2938
|
if (!oldestKey) break;
|
|
2939
|
+
const v = archiveCache.get(oldestKey);
|
|
2906
2940
|
archiveCache.delete(oldestKey);
|
|
2941
|
+
removed.push(oldestKey);
|
|
2942
|
+
cacheBytesTotal -= estimateFrameBytes(v);
|
|
2943
|
+
if (cacheBytesTotal < 0) recomputeCacheBytes();
|
|
2944
|
+
}
|
|
2945
|
+
emitArchiveCacheKeysRemoved(removed);
|
|
2946
|
+
}
|
|
2947
|
+
function setArchiveCache(cacheKey, archive) {
|
|
2948
|
+
if (archive && !canInsertCacheKey(cacheKey, archive)) {
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
const prev = archiveCache.get(cacheKey);
|
|
2952
|
+
if (prev !== void 0) {
|
|
2953
|
+
cacheBytesTotal -= estimateFrameBytes(prev);
|
|
2954
|
+
}
|
|
2955
|
+
archiveCache.delete(cacheKey);
|
|
2956
|
+
archiveCache.set(cacheKey, archive);
|
|
2957
|
+
if (archive) {
|
|
2958
|
+
cacheBytesTotal += estimateFrameBytes(archive);
|
|
2907
2959
|
}
|
|
2960
|
+
evictOldestWhileOverCap();
|
|
2961
|
+
}
|
|
2962
|
+
function extractObjectKeyFromCacheKey(cacheKey) {
|
|
2963
|
+
const m = cacheKey.match(/:([A-Za-z0-9_]+):((?:level2|level3))(?:\|.*)?$/i);
|
|
2964
|
+
if (!m || m.index == null || m.index < 1) return null;
|
|
2965
|
+
const urlPart = cacheKey.slice(0, m.index);
|
|
2966
|
+
return objectKeyFromArchiveUrlString(urlPart);
|
|
2967
|
+
}
|
|
2968
|
+
function objectKeyFromArchiveUrlString(url) {
|
|
2969
|
+
for (const base of [NEXRAD_ARCHIVE_LEVEL2_BASE_URL, NEXRAD_ARCHIVE_LEVEL3_BASE_URL]) {
|
|
2970
|
+
const prefix = base.endsWith("/") ? base : `${base}/`;
|
|
2971
|
+
if (url.startsWith(prefix)) {
|
|
2972
|
+
return url.slice(prefix.length);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
if (url.startsWith("http")) {
|
|
2976
|
+
try {
|
|
2977
|
+
const u = new URL(url);
|
|
2978
|
+
const p = u.pathname.replace(/^\/+/, "");
|
|
2979
|
+
if (p.startsWith("level-2/")) return p.slice("level-2/".length);
|
|
2980
|
+
return p || null;
|
|
2981
|
+
} catch {
|
|
2982
|
+
return null;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
return null;
|
|
2908
2986
|
}
|
|
2909
2987
|
|
|
2910
2988
|
// src/nexrad/loadNexradSites.ts
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NWWS `/alerts?hours=` sizing (aguacero-frontend baseline + overlap window parity).
|
|
3
|
+
*
|
|
4
|
+
* Hours follow the **active** observational mode only: NEXRAD → `nexradDurationValue`,
|
|
5
|
+
* MRMS → `mrmsDurationValue`, satellite → `satelliteDurationValue`, otherwise `1` (e.g. model-only).
|
|
6
|
+
* Tier cap uses {@link AguaceroCore} `satelliteTier` — the only subscription-style field on core today
|
|
7
|
+
* (same values as the frontend board tier: basic / enthusiast / professional).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MAX_ALERT_HISTORY_HOURS = 8760;
|
|
11
|
+
|
|
12
|
+
/** Matches javascript-sdk `satellite_support.js` timeline clamp. */
|
|
13
|
+
const TIMELINE_DURATION_MAX_HOURS = 12;
|
|
14
|
+
|
|
15
|
+
const LEGACY_DURATION_ALIAS = {
|
|
16
|
+
'0.5': '1',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Same rules as `parseTimelineDurationHours` in `@aguacerowx/javascript-sdk` (inlined so mapsgl
|
|
21
|
+
* does not depend on a package subpath that Vite may not resolve).
|
|
22
|
+
*
|
|
23
|
+
* @param {string | number | null | undefined} value
|
|
24
|
+
* @returns {number}
|
|
25
|
+
*/
|
|
26
|
+
function parseTimelineDurationHours(value) {
|
|
27
|
+
let s = value == null ? '1' : String(value).trim();
|
|
28
|
+
s = LEGACY_DURATION_ALIAS[s] || s;
|
|
29
|
+
const n = Number(s);
|
|
30
|
+
if (!Number.isFinite(n) || n <= 0) return 1;
|
|
31
|
+
if (n > TIMELINE_DURATION_MAX_HOURS) return TIMELINE_DURATION_MAX_HOURS;
|
|
32
|
+
return n;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Max option span (hours) per tier from frontend `RADAR_DURATION_CONFIG`. */
|
|
36
|
+
const TIER_MAX_HOURS = {
|
|
37
|
+
professional: 12,
|
|
38
|
+
enthusiast: 4,
|
|
39
|
+
basic: 1,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string | undefined} tier
|
|
44
|
+
* @returns {'professional'|'enthusiast'|'basic'}
|
|
45
|
+
*/
|
|
46
|
+
export function normalizeNwsSubscriptionTier(tier) {
|
|
47
|
+
const t = String(tier || 'basic').toLowerCase();
|
|
48
|
+
if (t === 'commercial' || t === 'lifetime' || t === 'partner') return 'professional';
|
|
49
|
+
if (t === 'professional' || t === 'enthusiast' || t === 'basic') return t;
|
|
50
|
+
return 'basic';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string | undefined} tier
|
|
55
|
+
* @returns {number}
|
|
56
|
+
*/
|
|
57
|
+
export function getMaxRadarHistoryHoursForTier(tier) {
|
|
58
|
+
const k = normalizeNwsSubscriptionTier(tier);
|
|
59
|
+
return TIER_MAX_HOURS[k] ?? TIER_MAX_HOURS.basic;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {object | null | undefined} state - {@link AguaceroCore} `state`
|
|
64
|
+
* @returns {number} integer hours in [1, MAX_ALERT_HISTORY_HOURS]
|
|
65
|
+
*/
|
|
66
|
+
export function computeNwsAlertsFetchHoursFromAguaceroState(state) {
|
|
67
|
+
const tierMax = getMaxRadarHistoryHoursForTier(state?.satelliteTier);
|
|
68
|
+
let desired = 1;
|
|
69
|
+
if (state?.isNexrad) {
|
|
70
|
+
desired = parseTimelineDurationHours(state.nexradDurationValue);
|
|
71
|
+
} else if (state?.isMRMS) {
|
|
72
|
+
desired = parseTimelineDurationHours(state.mrmsDurationValue);
|
|
73
|
+
} else if (state?.isSatellite) {
|
|
74
|
+
desired = parseTimelineDurationHours(state.satelliteDurationValue);
|
|
75
|
+
}
|
|
76
|
+
const clamped = Math.min(tierMax, Math.max(1, desired));
|
|
77
|
+
return Math.min(MAX_ALERT_HISTORY_HOURS, Math.floor(clamped));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {number} hours
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
export function nwsAlertsFetchSpecCacheKey(hours) {
|
|
85
|
+
return `h${hours}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Unix window [start, end] for client-side validity overlap (wall-clock end at “now”).
|
|
90
|
+
*
|
|
91
|
+
* @param {number} hours
|
|
92
|
+
* @param {number | null} [anchorSec] - reserved; default now (matches frontend `anchorSec: null`)
|
|
93
|
+
* @returns {{ winStartSec: number; winEndSec: number }}
|
|
94
|
+
*/
|
|
95
|
+
export function nwwsAlertsFetchUnixWindow(hours, anchorSec = null) {
|
|
96
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
97
|
+
const winEndSec = anchorSec ?? nowSec;
|
|
98
|
+
const winStartSec = winEndSec - hours * 3600;
|
|
99
|
+
return { winStartSec, winEndSec };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} baseUrl - `/alerts` root (no query)
|
|
104
|
+
* @param {number} hours
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
export function buildNwwsActiveAlertsUrl(baseUrl, hours) {
|
|
108
|
+
const h = Math.max(1, Math.floor(Number(hours) || 1));
|
|
109
|
+
const params = new URLSearchParams();
|
|
110
|
+
params.set('hours', String(h));
|
|
111
|
+
const q = params.toString();
|
|
112
|
+
const base = String(baseUrl || '').replace(/\/$/, '');
|
|
113
|
+
return q ? `${base}?${q}` : base;
|
|
114
|
+
}
|
package/src/nwsAlertsSupport.js
CHANGED
|
@@ -155,6 +155,34 @@ export function parseNwsTimeToUnix(value) {
|
|
|
155
155
|
return null;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/** CAP validity start fields (order matches aguacero-frontend `nwsWarningsModalHelpers`). */
|
|
159
|
+
const NWS_VALIDITY_START_PROP_KEYS = ['onset', 'effective', 'issued_at', 'issued', 'sent'];
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* True if CAP-validity [start, end] overlaps [winStartSec, winEndSec] (inclusive).
|
|
163
|
+
* Missing end time is treated as ongoing.
|
|
164
|
+
*
|
|
165
|
+
* @param {{ properties?: Record<string, unknown> }} feature
|
|
166
|
+
* @param {number} winStartSec
|
|
167
|
+
* @param {number} winEndSec
|
|
168
|
+
* @returns {boolean}
|
|
169
|
+
*/
|
|
170
|
+
export function nwsFeatureOverlapsUnixWindow(feature, winStartSec, winEndSec) {
|
|
171
|
+
const p = feature?.properties;
|
|
172
|
+
if (!p) return false;
|
|
173
|
+
const start =
|
|
174
|
+
typeof p.start_unix === 'number' && Number.isFinite(p.start_unix)
|
|
175
|
+
? Math.floor(p.start_unix)
|
|
176
|
+
: parseNwsTimeToUnix(getNwsTimeProp(p, NWS_VALIDITY_START_PROP_KEYS));
|
|
177
|
+
const end =
|
|
178
|
+
typeof p.end_unix === 'number' && Number.isFinite(p.end_unix)
|
|
179
|
+
? Math.floor(p.end_unix)
|
|
180
|
+
: parseNwsTimeToUnix(getNwsTimeProp(p, [...NWS_ALERT_END_TIME_PROP_KEYS]));
|
|
181
|
+
const s = typeof start === 'number' && Number.isFinite(start) ? start : 0;
|
|
182
|
+
const e = typeof end === 'number' && Number.isFinite(end) ? end : Number.POSITIVE_INFINITY;
|
|
183
|
+
return s <= winEndSec && e >= winStartSec;
|
|
184
|
+
}
|
|
185
|
+
|
|
158
186
|
const NWS_START_UNIX_KEYS = ['issued_at', 'issued', 'sent', 'onset', 'effective'];
|
|
159
187
|
const NWS_ACTIVE_START_UNIX_KEYS = ['onset', 'effective', 'issued_at', 'issued', 'sent'];
|
|
160
188
|
|