@aguacerowx/mapsgl 0.0.53 → 0.0.55
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 +32 -31
- package/src/NexradWeatherController.js +520 -510
- package/src/NwsWatchesWarningsOverlay.js +66 -11
- package/src/WeatherLayerManager.js +14 -1
- package/src/nexrad/loadNexradSites.ts +85 -7
- package/src/nexrad/nexradArchiveCache.ts +286 -66
- package/src/nexrad/nexradArchiveDiag.ts +26 -0
- package/src/nexrad/nexradLevel3Products.ts +581 -581
- package/src/nexrad/nexradMapboxFrameOpts.bundled.js +3 -2
- package/src/nexrad/nexradMapboxFrameOpts.ts +3 -2
- package/src/nexrad/nexradSitesDefault.json +1700 -0
- package/src/nexrad/radarArchiveCore.bundled.js +2807 -42
- package/src/nexrad/radarArchiveCore.ts +149 -30
- package/src/nexrad/radarDecode.worker.bundled.js +130 -126
- package/src/nexrad/radarDecode.worker.ts +13 -215
- package/src/nexrad/radarDecodeSlot.ts +195 -0
- package/src/nwsAlertsFetchSpec.js +114 -0
- package/src/nwsAlertsSupport.js +28 -0
- package/src/nwsSdkConstants.js +8 -0
|
@@ -1,227 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type NexradSite } from './PreprocessedSweepParser.js';
|
|
1
|
+
import { decodeRadarSlotMessage, type DecodeSlotRequest } from './radarDecodeSlot.js';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
| {
|
|
6
|
-
type: 'DECODE_SLOT';
|
|
7
|
-
requestId: number;
|
|
8
|
-
objectKey: string;
|
|
9
|
-
slotBuffer: ArrayBuffer; // compressed slot blob
|
|
10
|
-
nRays: number;
|
|
11
|
-
nGates: number;
|
|
12
|
-
firstGateKm: number;
|
|
13
|
-
gateWidthKm: number;
|
|
14
|
-
azimuthsBuffer: ArrayBuffer; // Float32Array big-endian from file header
|
|
15
|
-
sites: NexradSite[];
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
type DecodeResponse = {
|
|
19
|
-
type: 'DECODE_RESULT';
|
|
20
|
-
requestId: number;
|
|
21
|
-
gateData: Uint8Array | null;
|
|
22
|
-
stationLat?: number;
|
|
23
|
-
stationLon?: number;
|
|
24
|
-
firstGateKm?: number;
|
|
25
|
-
gateWidthKm?: number;
|
|
26
|
-
valueScale?: number;
|
|
27
|
-
valueOffset?: number;
|
|
28
|
-
rayBoundariesDeg?: Float32Array;
|
|
29
|
-
nRays?: number;
|
|
30
|
-
nGates?: number;
|
|
31
|
-
error?: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const SLOT_HDR_BYTES = 28;
|
|
35
|
-
|
|
36
|
-
function isZstd(bytes: Uint8Array): boolean {
|
|
37
|
-
return bytes.length >= 4 &&
|
|
38
|
-
bytes[0] === 0x28 && bytes[1] === 0xb5 && bytes[2] === 0x2f && bytes[3] === 0xfd;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function parseObjectKey(objectKey: string): { stationId: string } | null {
|
|
42
|
-
const parts = objectKey.split('_');
|
|
43
|
-
if (parts.length < 2) return null;
|
|
44
|
-
return { stationId: parts[0] };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Calculates robust boundaries between rays.
|
|
49
|
-
* Using midpoints and a fallback spacing prevents precision gaps at the 0/360 seam
|
|
50
|
-
* which often causes "flashing" during map transforms.
|
|
51
|
-
*/
|
|
52
|
-
function buildRayBoundariesDeg(azimuths: Float32Array): Float32Array {
|
|
53
|
-
const nRays = azimuths.length;
|
|
54
|
-
const boundaries = new Float32Array(nRays + 1);
|
|
55
|
-
if (nRays === 0) return boundaries;
|
|
56
|
-
|
|
57
|
-
let totalSpacing = 0;
|
|
58
|
-
let validCount = 0;
|
|
59
|
-
for (let i = 1; i < nRays; i++) {
|
|
60
|
-
let diff = azimuths[i] - azimuths[i - 1];
|
|
61
|
-
while (diff < -180) diff += 360;
|
|
62
|
-
while (diff > 180) diff -= 360;
|
|
63
|
-
const absDiff = Math.abs(diff);
|
|
64
|
-
if (absDiff > 0 && absDiff < 10) {
|
|
65
|
-
totalSpacing += absDiff;
|
|
66
|
-
validCount++;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
let avgSpacing = (validCount > 0) ? (totalSpacing / validCount) : (360 / nRays);
|
|
71
|
-
if (avgSpacing <= 0 || isNaN(avgSpacing)) avgSpacing = 1.0;
|
|
72
|
-
|
|
73
|
-
boundaries[0] = azimuths[0] - (avgSpacing / 2);
|
|
74
|
-
for (let i = 1; i < nRays; i++) {
|
|
75
|
-
let diff = azimuths[i] - azimuths[i - 1];
|
|
76
|
-
while (diff < -180) diff += 360;
|
|
77
|
-
while (diff > 180) diff -= 360;
|
|
78
|
-
boundaries[i] = azimuths[i - 1] + (diff / 2);
|
|
79
|
-
}
|
|
80
|
-
boundaries[nRays] = azimuths[nRays - 1] + (avgSpacing / 2);
|
|
81
|
-
|
|
82
|
-
return boundaries;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
self.onmessage = (event: MessageEvent<DecodeRequest>) => {
|
|
3
|
+
self.onmessage = (event: MessageEvent<DecodeSlotRequest>) => {
|
|
86
4
|
const data = event.data;
|
|
87
5
|
if (!data || data.type !== 'DECODE_SLOT') return;
|
|
88
6
|
|
|
89
|
-
const { requestId, objectKey, slotBuffer, nRays, nGates,
|
|
90
|
-
firstGateKm, gateWidthKm, azimuthsBuffer, sites } = data;
|
|
91
|
-
|
|
92
7
|
try {
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
self.postMessage(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const decompressed = decompressZstd(compressed);
|
|
101
|
-
const buffer = decompressed.buffer.slice(
|
|
102
|
-
decompressed.byteOffset,
|
|
103
|
-
decompressed.byteOffset + decompressed.byteLength,
|
|
104
|
-
) as ArrayBuffer;
|
|
105
|
-
|
|
106
|
-
const view = new DataView(buffer);
|
|
107
|
-
const valueScale = view.getFloat32(0, false);
|
|
108
|
-
const valueOffset = view.getFloat32(4, false);
|
|
109
|
-
const present = view.getUint32(8, false);
|
|
110
|
-
|
|
111
|
-
if (present === 0) {
|
|
112
|
-
self.postMessage({ type: 'DECODE_RESULT', requestId, gateData: null,
|
|
113
|
-
error: `Slot for ${objectKey} marked not present` });
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const slotNRays = view.getUint16(12, false);
|
|
118
|
-
const slotNGates = view.getUint16(14, false);
|
|
119
|
-
|
|
120
|
-
const gateBytes = buffer.byteLength - SLOT_HDR_BYTES;
|
|
121
|
-
const expectedGates = slotNRays * slotNGates;
|
|
122
|
-
const bytesPerGate = gateBytes / expectedGates;
|
|
123
|
-
|
|
124
|
-
if (bytesPerGate !== 1 && bytesPerGate !== 2 && bytesPerGate !== 4) {
|
|
125
|
-
self.postMessage({ type: 'DECODE_RESULT', requestId, gateData: null,
|
|
126
|
-
error: `Unexpected bytesPerGate=${bytesPerGate} for ${objectKey}` });
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const keyParts = parseObjectKey(objectKey);
|
|
131
|
-
if (!keyParts) {
|
|
132
|
-
self.postMessage({ type: 'DECODE_RESULT', requestId, gateData: null,
|
|
133
|
-
error: `Unable to parse station from key: ${objectKey}` });
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const site = sites.find(s => s.id === keyParts.stationId);
|
|
138
|
-
if (!site) {
|
|
139
|
-
self.postMessage({ type: 'DECODE_RESULT', requestId, gateData: null,
|
|
140
|
-
error: `Station "${keyParts.stationId}" not found` });
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* FIX: We MUST use slotNRays (the actual data count) to build geometry.
|
|
146
|
-
* Using the header's nRays when the slot contains a different count
|
|
147
|
-
* creates UV misalignment that causes flickering on map movement.
|
|
148
|
-
*/
|
|
149
|
-
const azimuths = new Float32Array(slotNRays);
|
|
150
|
-
const azView = new DataView(azimuthsBuffer);
|
|
151
|
-
const headerNRays = azimuthsBuffer.byteLength / 4;
|
|
152
|
-
|
|
153
|
-
for (let i = 0; i < slotNRays; i++) {
|
|
154
|
-
if (i < headerNRays) {
|
|
155
|
-
azimuths[i] = azView.getFloat32(i * 4, false);
|
|
156
|
-
} else {
|
|
157
|
-
// Fallback for edge cases where data exceeds metadata
|
|
158
|
-
const prevAz = i > 0 ? azimuths[i - 1] : 0;
|
|
159
|
-
azimuths[i] = (prevAz + (360 / slotNRays)) % 360;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const rayBoundariesDeg = buildRayBoundariesDeg(azimuths);
|
|
164
|
-
|
|
165
|
-
const gateCount = slotNRays * slotNGates;
|
|
166
|
-
const gateDataCopy = new Uint8Array(slotNRays * slotNGates * 2);
|
|
167
|
-
const outView = new DataView(gateDataCopy.buffer);
|
|
168
|
-
|
|
169
|
-
if (bytesPerGate === 1) {
|
|
170
|
-
const raw = new Uint8Array(buffer, SLOT_HDR_BYTES, gateCount);
|
|
171
|
-
for (let ray = 0; ray < slotNRays; ray++) {
|
|
172
|
-
let prev = 0;
|
|
173
|
-
for (let g = 0; g < slotNGates; g++) {
|
|
174
|
-
const idx = ray * slotNGates + g;
|
|
175
|
-
const delta = raw[idx];
|
|
176
|
-
const val = (prev + delta) & 0xFF;
|
|
177
|
-
prev = val;
|
|
178
|
-
if (val === 0) {
|
|
179
|
-
outView.setInt16(idx * 2, -32768, false);
|
|
180
|
-
} else {
|
|
181
|
-
outView.setInt16(idx * 2, val, false);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
8
|
+
const response = decodeRadarSlotMessage(data);
|
|
9
|
+
if (response.gateData && response.rayBoundariesDeg) {
|
|
10
|
+
(self as unknown as Worker).postMessage(response, [
|
|
11
|
+
response.gateData.buffer as ArrayBuffer,
|
|
12
|
+
response.rayBoundariesDeg.buffer as ArrayBuffer,
|
|
13
|
+
]);
|
|
185
14
|
} else {
|
|
186
|
-
|
|
187
|
-
for (let ray = 0; ray < slotNRays; ray++) {
|
|
188
|
-
let prev = 0;
|
|
189
|
-
for (let g = 0; g < slotNGates; g++) {
|
|
190
|
-
const idx = ray * slotNGates + g;
|
|
191
|
-
const hi = rawBytes[idx * 2];
|
|
192
|
-
const lo = rawBytes[idx * 2 + 1];
|
|
193
|
-
const delta = (hi << 8 | lo) << 16 >> 16;
|
|
194
|
-
const val = (prev + delta) | 0;
|
|
195
|
-
prev = val;
|
|
196
|
-
outView.setInt16(idx * 2, val, false);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
15
|
+
self.postMessage(response);
|
|
199
16
|
}
|
|
200
|
-
|
|
201
|
-
const response: DecodeResponse = {
|
|
202
|
-
type: 'DECODE_RESULT',
|
|
203
|
-
requestId,
|
|
204
|
-
gateData: gateDataCopy,
|
|
205
|
-
stationLat: site.lat,
|
|
206
|
-
stationLon: site.lon,
|
|
207
|
-
firstGateKm,
|
|
208
|
-
gateWidthKm,
|
|
209
|
-
valueScale,
|
|
210
|
-
valueOffset,
|
|
211
|
-
rayBoundariesDeg,
|
|
212
|
-
nRays: slotNRays,
|
|
213
|
-
nGates: slotNGates,
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
// Transfer the buffers to the main thread to avoid copying overhead
|
|
217
|
-
(self as any).postMessage(response, [
|
|
218
|
-
gateDataCopy.buffer as ArrayBuffer,
|
|
219
|
-
rayBoundariesDeg.buffer as ArrayBuffer,
|
|
220
|
-
]);
|
|
221
17
|
} catch (error) {
|
|
222
18
|
self.postMessage({
|
|
223
|
-
type: 'DECODE_RESULT',
|
|
19
|
+
type: 'DECODE_RESULT',
|
|
20
|
+
requestId: data.requestId,
|
|
21
|
+
gateData: null,
|
|
224
22
|
error: error instanceof Error ? error.message : 'Unknown worker error',
|
|
225
23
|
});
|
|
226
24
|
}
|
|
227
|
-
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { decompress as decompressZstd } from 'fzstd';
|
|
2
|
+
import type { NexradSite } from './PreprocessedSweepParser.js';
|
|
3
|
+
|
|
4
|
+
export type DecodeSlotRequest = {
|
|
5
|
+
type: 'DECODE_SLOT';
|
|
6
|
+
requestId: number;
|
|
7
|
+
objectKey: string;
|
|
8
|
+
slotBuffer: ArrayBuffer;
|
|
9
|
+
nRays: number;
|
|
10
|
+
nGates: number;
|
|
11
|
+
firstGateKm: number;
|
|
12
|
+
gateWidthKm: number;
|
|
13
|
+
azimuthsBuffer: ArrayBuffer;
|
|
14
|
+
sites: NexradSite[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type DecodeSlotResponse = {
|
|
18
|
+
type: 'DECODE_RESULT';
|
|
19
|
+
requestId: number;
|
|
20
|
+
gateData: Uint8Array | null;
|
|
21
|
+
stationLat?: number;
|
|
22
|
+
stationLon?: number;
|
|
23
|
+
firstGateKm?: number;
|
|
24
|
+
gateWidthKm?: number;
|
|
25
|
+
valueScale?: number;
|
|
26
|
+
valueOffset?: number;
|
|
27
|
+
rayBoundariesDeg?: Float32Array;
|
|
28
|
+
nRays?: number;
|
|
29
|
+
nGates?: number;
|
|
30
|
+
error?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const SLOT_HDR_BYTES = 28;
|
|
34
|
+
|
|
35
|
+
function isZstd(bytes: Uint8Array): boolean {
|
|
36
|
+
return bytes.length >= 4 &&
|
|
37
|
+
bytes[0] === 0x28 && bytes[1] === 0xb5 && bytes[2] === 0x2f && bytes[3] === 0xfd;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseObjectKey(objectKey: string): { stationId: string } | null {
|
|
41
|
+
const parts = objectKey.split('_');
|
|
42
|
+
if (parts.length < 2) return null;
|
|
43
|
+
return { stationId: parts[0] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildRayBoundariesDeg(azimuths: Float32Array): Float32Array {
|
|
47
|
+
const nRays = azimuths.length;
|
|
48
|
+
const boundaries = new Float32Array(nRays + 1);
|
|
49
|
+
if (nRays === 0) return boundaries;
|
|
50
|
+
|
|
51
|
+
let totalSpacing = 0;
|
|
52
|
+
let validCount = 0;
|
|
53
|
+
for (let i = 1; i < nRays; i++) {
|
|
54
|
+
let diff = azimuths[i] - azimuths[i - 1];
|
|
55
|
+
while (diff < -180) diff += 360;
|
|
56
|
+
while (diff > 180) diff -= 360;
|
|
57
|
+
const absDiff = Math.abs(diff);
|
|
58
|
+
if (absDiff > 0 && absDiff < 10) {
|
|
59
|
+
totalSpacing += absDiff;
|
|
60
|
+
validCount++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let avgSpacing = (validCount > 0) ? (totalSpacing / validCount) : (360 / nRays);
|
|
65
|
+
if (avgSpacing <= 0 || isNaN(avgSpacing)) avgSpacing = 1.0;
|
|
66
|
+
|
|
67
|
+
boundaries[0] = azimuths[0] - (avgSpacing / 2);
|
|
68
|
+
for (let i = 1; i < nRays; i++) {
|
|
69
|
+
let diff = azimuths[i] - azimuths[i - 1];
|
|
70
|
+
while (diff < -180) diff += 360;
|
|
71
|
+
while (diff > 180) diff -= 360;
|
|
72
|
+
boundaries[i] = azimuths[i - 1] + (diff / 2);
|
|
73
|
+
}
|
|
74
|
+
boundaries[nRays] = azimuths[nRays - 1] + (avgSpacing / 2);
|
|
75
|
+
|
|
76
|
+
return boundaries;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Shared decode path for the dedicated worker and React Native (main-thread shim). */
|
|
80
|
+
export function decodeRadarSlotMessage(data: DecodeSlotRequest): DecodeSlotResponse {
|
|
81
|
+
const { requestId, objectKey, slotBuffer, nRays, nGates,
|
|
82
|
+
firstGateKm, gateWidthKm, azimuthsBuffer, sites } = data;
|
|
83
|
+
|
|
84
|
+
const compressed = new Uint8Array(slotBuffer);
|
|
85
|
+
if (!isZstd(compressed)) {
|
|
86
|
+
return { type: 'DECODE_RESULT', requestId, gateData: null,
|
|
87
|
+
error: `Slot for ${objectKey} is not valid zstd` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const decompressed = decompressZstd(compressed);
|
|
91
|
+
const buffer = decompressed.buffer.slice(
|
|
92
|
+
decompressed.byteOffset,
|
|
93
|
+
decompressed.byteOffset + decompressed.byteLength,
|
|
94
|
+
) as ArrayBuffer;
|
|
95
|
+
|
|
96
|
+
const view = new DataView(buffer);
|
|
97
|
+
const valueScale = view.getFloat32(0, false);
|
|
98
|
+
const valueOffset = view.getFloat32(4, false);
|
|
99
|
+
const present = view.getUint32(8, false);
|
|
100
|
+
|
|
101
|
+
if (present === 0) {
|
|
102
|
+
return { type: 'DECODE_RESULT', requestId, gateData: null,
|
|
103
|
+
error: `Slot for ${objectKey} marked not present` };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const slotNRays = view.getUint16(12, false);
|
|
107
|
+
const slotNGates = view.getUint16(14, false);
|
|
108
|
+
|
|
109
|
+
const gateBytes = buffer.byteLength - SLOT_HDR_BYTES;
|
|
110
|
+
const expectedGates = slotNRays * slotNGates;
|
|
111
|
+
const bytesPerGate = gateBytes / expectedGates;
|
|
112
|
+
|
|
113
|
+
if (bytesPerGate !== 1 && bytesPerGate !== 2 && bytesPerGate !== 4) {
|
|
114
|
+
return { type: 'DECODE_RESULT', requestId, gateData: null,
|
|
115
|
+
error: `Unexpected bytesPerGate=${bytesPerGate} for ${objectKey}` };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const keyParts = parseObjectKey(objectKey);
|
|
119
|
+
if (!keyParts) {
|
|
120
|
+
return { type: 'DECODE_RESULT', requestId, gateData: null,
|
|
121
|
+
error: `Unable to parse station from key: ${objectKey}` };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const site = sites.find(s => s.id === keyParts.stationId);
|
|
125
|
+
if (!site) {
|
|
126
|
+
return { type: 'DECODE_RESULT', requestId, gateData: null,
|
|
127
|
+
error: `Station "${keyParts.stationId}" not found` };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const azimuths = new Float32Array(slotNRays);
|
|
131
|
+
const azView = new DataView(azimuthsBuffer);
|
|
132
|
+
const headerNRays = azimuthsBuffer.byteLength / 4;
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < slotNRays; i++) {
|
|
135
|
+
if (i < headerNRays) {
|
|
136
|
+
azimuths[i] = azView.getFloat32(i * 4, false);
|
|
137
|
+
} else {
|
|
138
|
+
const prevAz = i > 0 ? azimuths[i - 1] : 0;
|
|
139
|
+
azimuths[i] = (prevAz + (360 / slotNRays)) % 360;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const rayBoundariesDeg = buildRayBoundariesDeg(azimuths);
|
|
144
|
+
|
|
145
|
+
const gateCount = slotNRays * slotNGates;
|
|
146
|
+
const gateDataCopy = new Uint8Array(slotNRays * slotNGates * 2);
|
|
147
|
+
const outView = new DataView(gateDataCopy.buffer);
|
|
148
|
+
|
|
149
|
+
if (bytesPerGate === 1) {
|
|
150
|
+
const raw = new Uint8Array(buffer, SLOT_HDR_BYTES, gateCount);
|
|
151
|
+
for (let ray = 0; ray < slotNRays; ray++) {
|
|
152
|
+
let prev = 0;
|
|
153
|
+
for (let g = 0; g < slotNGates; g++) {
|
|
154
|
+
const idx = ray * slotNGates + g;
|
|
155
|
+
const delta = raw[idx];
|
|
156
|
+
const val = (prev + delta) & 0xFF;
|
|
157
|
+
prev = val;
|
|
158
|
+
if (val === 0) {
|
|
159
|
+
outView.setInt16(idx * 2, -32768, false);
|
|
160
|
+
} else {
|
|
161
|
+
outView.setInt16(idx * 2, val, false);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
const rawBytes = new Uint8Array(buffer, SLOT_HDR_BYTES, gateCount * 2);
|
|
167
|
+
for (let ray = 0; ray < slotNRays; ray++) {
|
|
168
|
+
let prev = 0;
|
|
169
|
+
for (let g = 0; g < slotNGates; g++) {
|
|
170
|
+
const idx = ray * slotNGates + g;
|
|
171
|
+
const hi = rawBytes[idx * 2];
|
|
172
|
+
const lo = rawBytes[idx * 2 + 1];
|
|
173
|
+
const delta = (hi << 8 | lo) << 16 >> 16;
|
|
174
|
+
const val = (prev + delta) | 0;
|
|
175
|
+
prev = val;
|
|
176
|
+
outView.setInt16(idx * 2, val, false);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
type: 'DECODE_RESULT',
|
|
183
|
+
requestId,
|
|
184
|
+
gateData: gateDataCopy,
|
|
185
|
+
stationLat: site.lat,
|
|
186
|
+
stationLon: site.lon,
|
|
187
|
+
firstGateKm,
|
|
188
|
+
gateWidthKm,
|
|
189
|
+
valueScale,
|
|
190
|
+
valueOffset,
|
|
191
|
+
rayBoundariesDeg,
|
|
192
|
+
nRays: slotNRays,
|
|
193
|
+
nGates: slotNGates,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -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
|
|
package/src/nwsSdkConstants.js
CHANGED
|
@@ -326,6 +326,14 @@ function nwsLineDasharrayExprForMode(mode) {
|
|
|
326
326
|
return ['literal', lit];
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Literal-only dash pattern for one {@link NwsWatchesWarningsOverlay} line mode (`solid` | `dash` | `dot`).
|
|
331
|
+
* Use where Mapbox RN cannot apply data-driven `line-dasharray` (see {@link buildNwsLineDasharrayExpression}).
|
|
332
|
+
*/
|
|
333
|
+
export function buildNwsLineDasharrayLiteralExpression(mode) {
|
|
334
|
+
return nwsLineDasharrayExprForMode(mode);
|
|
335
|
+
}
|
|
336
|
+
|
|
329
337
|
function resolveNwsAlertLineStyleForEventDash(lineStyles, name, defaultMode) {
|
|
330
338
|
return resolveNwsAlertLineStyleForEvent(lineStyles, name)?.lineDash ?? defaultMode;
|
|
331
339
|
}
|