@aguacerowx/mapsgl 0.0.31 → 0.0.41
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/index.js +34 -2
- package/package.json +13 -3
- package/src/GridRenderLayer.js +105 -86
- package/src/MapManager.js +47 -15
- package/src/NexradSitesOverlay.js +148 -0
- package/src/NexradWeatherController.js +491 -0
- package/src/NwsWatchesWarningsOverlay.js +768 -0
- package/src/SatelliteShaderManager.js +999 -0
- package/src/WeatherLayerManager.js +800 -110
- package/src/WorkerPool.js +340 -0
- package/src/nexrad/MapboxRadarLayer.bundled.js +810 -0
- package/src/nexrad/MapboxRadarLayer.ts +784 -0
- package/src/nexrad/PreprocessedSweepParser.ts +226 -0
- package/src/nexrad/buildRadarRayGeometry.ts +97 -0
- package/src/nexrad/level3StormRelative.ts +116 -0
- package/src/nexrad/loadNexradSites.ts +41 -0
- package/src/nexrad/nexradArchiveCache.ts +64 -0
- package/src/nexrad/nexradCrossSectionSampleAtLatLon.ts +121 -0
- package/src/nexrad/nexradLevel3Products.ts +549 -0
- package/src/nexrad/nexradMapboxFrameOpts.js +106 -0
- package/src/nexrad/radarArchiveCore.bundled.js +4206 -0
- package/src/nexrad/radarArchiveCore.bundled.js.map +7 -0
- package/src/nexrad/radarArchiveCore.ts +1737 -0
- package/src/nexrad/radarDecode.worker.bundled.js +809 -0
- package/src/nexrad/radarDecode.worker.ts +227 -0
- package/src/nexrad/radarFrameGpuMatch.ts +111 -0
- package/src/nwsAlertsSupport.js +860 -0
- package/src/nwsEventColorsDefaults.js +133 -0
- package/src/nwsSdkConstants.js +360 -0
- package/src/nwsWarningCustomizationKey.gen.js +496 -0
- package/src/satelliteDefaultColormaps.js +37 -0
- package/src/satelliteKtxWorker.js +225 -0
- package/src/satelliteShader.js +17 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { decompress as decompressZstd } from 'fzstd';
|
|
2
|
+
import { type NexradSite } from './PreprocessedSweepParser.js';
|
|
3
|
+
|
|
4
|
+
type DecodeRequest =
|
|
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>) => {
|
|
86
|
+
const data = event.data;
|
|
87
|
+
if (!data || data.type !== 'DECODE_SLOT') return;
|
|
88
|
+
|
|
89
|
+
const { requestId, objectKey, slotBuffer, nRays, nGates,
|
|
90
|
+
firstGateKm, gateWidthKm, azimuthsBuffer, sites } = data;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const compressed = new Uint8Array(slotBuffer);
|
|
94
|
+
if (!isZstd(compressed)) {
|
|
95
|
+
self.postMessage({ type: 'DECODE_RESULT', requestId, gateData: null,
|
|
96
|
+
error: `Slot for ${objectKey} is not valid zstd` });
|
|
97
|
+
return;
|
|
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
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
const rawBytes = new Uint8Array(buffer, SLOT_HDR_BYTES, gateCount * 2);
|
|
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
|
+
}
|
|
199
|
+
}
|
|
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
|
+
} catch (error) {
|
|
222
|
+
self.postMessage({
|
|
223
|
+
type: 'DECODE_RESULT', requestId, gateData: null,
|
|
224
|
+
error: error instanceof Error ? error.message : 'Unknown worker error',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frame transforms applied in {@link MapboxRadarLayer} before GPU upload.
|
|
3
|
+
* Mouse readouts must use the **same** gate/ray ordering as the texture or samples drift from pixels.
|
|
4
|
+
*/
|
|
5
|
+
import type { DecodedRadarFrame } from './nexradArchiveCache.js';
|
|
6
|
+
|
|
7
|
+
/** Shortest distance between two headings on [0, 360), in degrees. */
|
|
8
|
+
function angularDistanceDeg(a: number, b: number): number {
|
|
9
|
+
let d = Math.abs(a - b) % 360;
|
|
10
|
+
if (d > 180) d = 360 - d;
|
|
11
|
+
return d;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* For Level-III layout products (KDP/N0H): remap incoming rays into fixed canonical azimuth bins
|
|
16
|
+
* (same as MapboxRadarLayer `sortFrameToCanonicalBins`).
|
|
17
|
+
*/
|
|
18
|
+
export function canonicalBinsRadarFrame(frame: DecodedRadarFrame): DecodedRadarFrame {
|
|
19
|
+
const nRays = frame.nRays;
|
|
20
|
+
const nGates = frame.nGates;
|
|
21
|
+
if (nRays <= 0 || nGates <= 0) return frame;
|
|
22
|
+
if (frame.rayBoundariesDeg.length < nRays + 1) return frame;
|
|
23
|
+
|
|
24
|
+
const degPerBin = 360 / nRays;
|
|
25
|
+
const bytesPerRay = nGates * 2;
|
|
26
|
+
|
|
27
|
+
const centers = new Float32Array(nRays);
|
|
28
|
+
for (let r = 0; r < nRays; r++) {
|
|
29
|
+
const lower = frame.rayBoundariesDeg[r]!;
|
|
30
|
+
const upper = frame.rayBoundariesDeg[r + 1]!;
|
|
31
|
+
const center = (lower + upper) * 0.5;
|
|
32
|
+
centers[r] = ((center % 360) + 360) % 360;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const canonicalGateData = new Uint8Array(nRays * bytesPerRay);
|
|
36
|
+
for (let i = 0; i < canonicalGateData.length; i += 2) {
|
|
37
|
+
canonicalGateData[i] = 128;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const used = new Array<boolean>(nRays).fill(false);
|
|
41
|
+
for (let bin = 0; bin < nRays; bin++) {
|
|
42
|
+
const targetDeg = ((((bin + 0.5) * degPerBin) % 360) + 360) % 360;
|
|
43
|
+
let bestR = -1;
|
|
44
|
+
let bestDist = Infinity;
|
|
45
|
+
for (let r = 0; r < nRays; r++) {
|
|
46
|
+
if (used[r]) continue;
|
|
47
|
+
const dist = angularDistanceDeg(centers[r], targetDeg);
|
|
48
|
+
if (dist < bestDist) {
|
|
49
|
+
bestDist = dist;
|
|
50
|
+
bestR = r;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (bestR >= 0) {
|
|
54
|
+
used[bestR] = true;
|
|
55
|
+
canonicalGateData.set(
|
|
56
|
+
frame.gateData.subarray(bestR * bytesPerRay, (bestR + 1) * bytesPerRay),
|
|
57
|
+
bin * bytesPerRay,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const canonicalBoundaries = new Float32Array(nRays + 1);
|
|
63
|
+
for (let i = 0; i <= nRays; i++) {
|
|
64
|
+
canonicalBoundaries[i] = i * degPerBin;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { ...frame, gateData: canonicalGateData, rayBoundariesDeg: canonicalBoundaries };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Sort rays by ascending start azimuth — same as MapboxRadarLayer `sortFrameByAzimuth`. */
|
|
71
|
+
export function sortRadarFrameByAzimuth(frame: DecodedRadarFrame): DecodedRadarFrame {
|
|
72
|
+
const nRays = frame.nRays;
|
|
73
|
+
const nGates = frame.nGates;
|
|
74
|
+
const bytesPerRay = nGates * 2;
|
|
75
|
+
|
|
76
|
+
const rayOrder = Array.from({ length: nRays }, (_, i) => i).sort(
|
|
77
|
+
(a, b) => frame.rayBoundariesDeg[a] - frame.rayBoundariesDeg[b],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const sortedGateData = new Uint8Array(nRays * bytesPerRay);
|
|
81
|
+
const sortedBoundaries = new Float32Array(nRays + 1);
|
|
82
|
+
|
|
83
|
+
for (let newIdx = 0; newIdx < nRays; newIdx++) {
|
|
84
|
+
const oldIdx = rayOrder[newIdx]!;
|
|
85
|
+
sortedGateData.set(
|
|
86
|
+
frame.gateData.subarray(oldIdx * bytesPerRay, (oldIdx + 1) * bytesPerRay),
|
|
87
|
+
newIdx * bytesPerRay,
|
|
88
|
+
);
|
|
89
|
+
sortedBoundaries[newIdx] = frame.rayBoundariesDeg[oldIdx]!;
|
|
90
|
+
}
|
|
91
|
+
sortedBoundaries[nRays] =
|
|
92
|
+
frame.rayBoundariesDeg[rayOrder[nRays - 1]!]! +
|
|
93
|
+
(frame.rayBoundariesDeg[rayOrder[1]!]! - frame.rayBoundariesDeg[rayOrder[0]!]!);
|
|
94
|
+
|
|
95
|
+
return { ...frame, gateData: sortedGateData, rayBoundariesDeg: sortedBoundaries };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type GpuReadoutFrameOptions = {
|
|
99
|
+
geometryLayoutKey?: string | null;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Match {@link MapboxRadarLayer} `uploadFrame` gate/ray ordering for CPU readout.
|
|
104
|
+
*/
|
|
105
|
+
export function prepareRadarFrameForGpuReadout(
|
|
106
|
+
frame: DecodedRadarFrame,
|
|
107
|
+
options?: GpuReadoutFrameOptions,
|
|
108
|
+
): DecodedRadarFrame {
|
|
109
|
+
const hasLayoutKey = options?.geometryLayoutKey != null && options.geometryLayoutKey !== '';
|
|
110
|
+
return hasLayoutKey ? canonicalBinsRadarFrame(frame) : sortRadarFrameByAzimuth(frame);
|
|
111
|
+
}
|