@aguacerowx/mapsgl 0.0.57 → 0.0.58
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 +45 -45
- package/package.json +1 -1
- package/src/GridRenderLayer.js +1387 -1387
- package/src/MapManager.js +197 -197
- package/src/NexradSitesOverlay.js +148 -148
- package/src/NexradWeatherController.js +3 -0
- package/src/SatelliteShaderManager.js +1000 -1000
- package/src/WorkerPool.js +340 -340
- package/src/defaultBasisBaseUrl.js +11 -11
- package/src/nexrad/MapboxRadarLayer.ts +783 -783
- package/src/nexrad/PreprocessedSweepParser.ts +225 -225
- package/src/nexrad/buildRadarRayGeometry.ts +97 -97
- package/src/nexrad/level3StormRelative.ts +116 -116
- package/src/nexrad/loadNexradSites.ts +119 -119
- package/src/nexrad/nexradArchiveDiag.ts +26 -26
- package/src/nexrad/nexradCrossSectionSampleAtLatLon.ts +121 -121
- package/src/nexrad/nexradMapboxFrameOpts.ts +126 -126
- package/src/nexrad/nexradSitesDefault.json +1699 -1699
- package/src/nexrad/radarArchiveCore.bundled.js +10 -0
- package/src/nexrad/radarArchiveCore.ts +11 -0
- package/src/nexrad/radarDecode.worker.ts +25 -25
- package/src/nexrad/radarDecodeSlot.ts +195 -195
- package/src/nexrad/radarFrameGpuMatch.ts +111 -111
- package/src/satelliteDefaultColormaps.js +37 -37
- package/src/satelliteKtxWorker.js +232 -232
- package/src/satelliteShader.js +17 -17
- package/src/style-applicator.js +112 -112
- package/src/style-layer-map.js +26 -26
|
@@ -1,226 +1,226 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parser for the pre-processed sweep binary format produced by the NEXRAD Lambda.
|
|
3
|
-
*
|
|
4
|
-
* Binary layout (all little-endian — numpy default on x86):
|
|
5
|
-
* [0-3] int32 version = 1
|
|
6
|
-
* [4-7] float32 scale
|
|
7
|
-
* [8-11] float32 offset
|
|
8
|
-
* [12-15] float32 sweep_idx
|
|
9
|
-
* [16-19] float32 elev_angle (degrees)
|
|
10
|
-
* [20-23] float32 n_rays
|
|
11
|
-
* [24-27] float32 n_gates
|
|
12
|
-
* [28-31] float32 first_gate (km — range to center of first gate)
|
|
13
|
-
* [32-35] float32 gate_width (km — range sample interval)
|
|
14
|
-
* [36 ...] float32[n_rays] azimuths (degrees)
|
|
15
|
-
* [36+n_rays*4 ...] float32[n_rays] elevations (degrees, unused for 2-D rendering)
|
|
16
|
-
* [36+n_rays*8 ...] int16[n_rays * n_gates] gate values (-32768 = masked/NaN)
|
|
17
|
-
*
|
|
18
|
-
* Gate decoding: physical_value = raw_int16 * scale + offset (when raw != -32768)
|
|
19
|
-
* Masked gates are stored as -99 in the output dataMoments array to match the
|
|
20
|
-
* existing VertexDataGenerator convention.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const PREPROCESSED_VERSION = 1;
|
|
24
|
-
const HEADER_SIZE = 36; // 4 + 8 + 24 bytes
|
|
25
|
-
|
|
26
|
-
export interface NexradSite {
|
|
27
|
-
id: string;
|
|
28
|
-
lat: number;
|
|
29
|
-
lon: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Minimal moment interface consumed by VertexDataGenerator */
|
|
33
|
-
export interface SweepMoment {
|
|
34
|
-
dataMomentName: string;
|
|
35
|
-
azimuth: number;
|
|
36
|
-
/** Distance to center of first gate (km) */
|
|
37
|
-
dataMomentRange: number;
|
|
38
|
-
/** Gate width / range sample interval (km) */
|
|
39
|
-
dataMomentRangeSampleInterval: number;
|
|
40
|
-
/** Physical gate values; masked gates = -99 */
|
|
41
|
-
dataMoments: Float32Array;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface SweepRay {
|
|
45
|
-
dataMoments: SweepMoment[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Minimal archive interface consumed by VertexDataGenerator and MapboxRadarLayer */
|
|
49
|
-
export interface RadarArchive {
|
|
50
|
-
latitude: number;
|
|
51
|
-
longitude: number;
|
|
52
|
-
fieldName: string;
|
|
53
|
-
elevAngle: number;
|
|
54
|
-
nRays: number;
|
|
55
|
-
nGates: number;
|
|
56
|
-
volume: {
|
|
57
|
-
sweeps: Array<{ rays: SweepRay[] }>;
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Returns true if the buffer starts with the preprocessed sweep version header.
|
|
63
|
-
* Used in RadarLayer to dispatch to the correct parser.
|
|
64
|
-
*/
|
|
65
|
-
export function isPreprocessedFormat(buffer: ArrayBuffer): boolean {
|
|
66
|
-
if (buffer.byteLength < HEADER_SIZE) return false;
|
|
67
|
-
return new DataView(buffer).getInt32(0, true) === PREPROCESSED_VERSION;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function parsePreprocessedSweep(
|
|
71
|
-
buffer: ArrayBuffer,
|
|
72
|
-
stationId: string,
|
|
73
|
-
fieldName: string,
|
|
74
|
-
nexradSites: NexradSite[],
|
|
75
|
-
): RadarArchive | null {
|
|
76
|
-
const LE = true;
|
|
77
|
-
const view = new DataView(buffer);
|
|
78
|
-
|
|
79
|
-
const version = view.getInt32(0, LE);
|
|
80
|
-
if (version !== PREPROCESSED_VERSION) {
|
|
81
|
-
console.error(`[PreprocessedSweepParser] Unknown version ${version}, expected ${PREPROCESSED_VERSION}`);
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const scale = view.getFloat32(4, LE);
|
|
86
|
-
const offsetVal = view.getFloat32(8, LE);
|
|
87
|
-
// sweep_idx at bytes 12-15 (not needed for rendering)
|
|
88
|
-
const elevAngle = view.getFloat32(16, LE);
|
|
89
|
-
const nRays = Math.round(view.getFloat32(20, LE));
|
|
90
|
-
const nGates = Math.round(view.getFloat32(24, LE));
|
|
91
|
-
const firstGate = view.getFloat32(28, LE); // km
|
|
92
|
-
const gateWidth = view.getFloat32(32, LE); // km
|
|
93
|
-
|
|
94
|
-
const azOffset = HEADER_SIZE; // 36
|
|
95
|
-
const elOffset = azOffset + nRays * 4; // 36 + nRays*4
|
|
96
|
-
const gateOffset = elOffset + nRays * 4; // 36 + nRays*8
|
|
97
|
-
|
|
98
|
-
const expectedBytes = gateOffset + nRays * nGates * 2;
|
|
99
|
-
if (buffer.byteLength < expectedBytes) {
|
|
100
|
-
console.error(
|
|
101
|
-
`[PreprocessedSweepParser] Buffer too small: got ${buffer.byteLength} bytes, ` +
|
|
102
|
-
`need ${expectedBytes} (nRays=${nRays}, nGates=${nGates})`,
|
|
103
|
-
);
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const site = nexradSites.find(s => s.id === stationId);
|
|
108
|
-
if (!site) {
|
|
109
|
-
console.error(`[PreprocessedSweepParser] Station "${stationId}" not found in nexrad.json`);
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Slice into aligned copies for typed-array access
|
|
114
|
-
const azimuths = new Float32Array(buffer.slice(azOffset, azOffset + nRays * 4));
|
|
115
|
-
const gatesRaw = new Int16Array(buffer.slice(gateOffset, gateOffset + nRays * nGates * 2));
|
|
116
|
-
|
|
117
|
-
const rays: SweepRay[] = [];
|
|
118
|
-
for (let i = 0; i < nRays; i++) {
|
|
119
|
-
const physicalGates = new Float32Array(nGates);
|
|
120
|
-
const base = i * nGates;
|
|
121
|
-
for (let g = 0; g < nGates; g++) {
|
|
122
|
-
const raw = gatesRaw[base + g];
|
|
123
|
-
physicalGates[g] = raw === -32768 ? -99 : raw * scale + offsetVal;
|
|
124
|
-
}
|
|
125
|
-
rays.push({
|
|
126
|
-
dataMoments: [{
|
|
127
|
-
dataMomentName: fieldName,
|
|
128
|
-
azimuth: azimuths[i],
|
|
129
|
-
dataMomentRange: firstGate,
|
|
130
|
-
dataMomentRangeSampleInterval: gateWidth,
|
|
131
|
-
dataMoments: physicalGates,
|
|
132
|
-
}],
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
latitude: site.lat,
|
|
138
|
-
longitude: site.lon,
|
|
139
|
-
fieldName,
|
|
140
|
-
elevAngle,
|
|
141
|
-
nRays,
|
|
142
|
-
nGates,
|
|
143
|
-
volume: { sweeps: [{ rays }] },
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const SLOT_HDR_BYTES = 28;
|
|
148
|
-
|
|
149
|
-
export function parseSlotBuffer(
|
|
150
|
-
buffer: ArrayBuffer,
|
|
151
|
-
stationId: string,
|
|
152
|
-
fieldName: string,
|
|
153
|
-
nexradSites: NexradSite[],
|
|
154
|
-
// These come from the file header fetched separately
|
|
155
|
-
nRaysFile: number,
|
|
156
|
-
nGatesFile: number,
|
|
157
|
-
firstGateKm: number,
|
|
158
|
-
gateWidthKm: number,
|
|
159
|
-
azimuths: Float32Array,
|
|
160
|
-
): RadarArchive | null {
|
|
161
|
-
const BE = false; // DataView default is big-endian when false
|
|
162
|
-
const view = new DataView(buffer);
|
|
163
|
-
|
|
164
|
-
const scale = view.getFloat32(0, false); // big-endian
|
|
165
|
-
const offsetV = view.getFloat32(4, false);
|
|
166
|
-
const present = view.getUint32(8, false);
|
|
167
|
-
const nRays = view.getUint16(12, false);
|
|
168
|
-
const nGates = view.getUint16(14, false);
|
|
169
|
-
|
|
170
|
-
if (present === 0) {
|
|
171
|
-
console.warn(`[parseSlotBuffer] Slot for ${fieldName} marked not present`);
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const gateOffset = SLOT_HDR_BYTES;
|
|
176
|
-
const expectedBytes = gateOffset + nRays * nGates * 2;
|
|
177
|
-
if (buffer.byteLength < expectedBytes) {
|
|
178
|
-
console.error(
|
|
179
|
-
`[parseSlotBuffer] Buffer too small: got ${buffer.byteLength}, need ${expectedBytes}`
|
|
180
|
-
);
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const site = nexradSites.find(s => s.id === stationId);
|
|
185
|
-
if (!site) {
|
|
186
|
-
console.error(`[parseSlotBuffer] Station "${stationId}" not found`);
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Gate data is big-endian int16
|
|
191
|
-
const gateBytes = buffer.slice(gateOffset, gateOffset + nRays * nGates * 2);
|
|
192
|
-
const gatesRaw = new Int16Array(nRays * nGates);
|
|
193
|
-
const gateView = new DataView(gateBytes);
|
|
194
|
-
for (let i = 0; i < nRays * nGates; i++) {
|
|
195
|
-
gatesRaw[i] = gateView.getInt16(i * 2, false); // big-endian
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const rays: SweepRay[] = [];
|
|
199
|
-
for (let i = 0; i < nRays; i++) {
|
|
200
|
-
const physicalGates = new Float32Array(nGates);
|
|
201
|
-
const base = i * nGates;
|
|
202
|
-
for (let g = 0; g < nGates; g++) {
|
|
203
|
-
const raw = gatesRaw[base + g];
|
|
204
|
-
physicalGates[g] = raw === -32768 ? -99 : raw * scale + offsetV;
|
|
205
|
-
}
|
|
206
|
-
rays.push({
|
|
207
|
-
dataMoments: [{
|
|
208
|
-
dataMomentName: fieldName,
|
|
209
|
-
azimuth: azimuths[i],
|
|
210
|
-
dataMomentRange: firstGateKm,
|
|
211
|
-
dataMomentRangeSampleInterval: gateWidthKm,
|
|
212
|
-
dataMoments: physicalGates,
|
|
213
|
-
}],
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
latitude: site.lat,
|
|
219
|
-
longitude: site.lon,
|
|
220
|
-
fieldName,
|
|
221
|
-
elevAngle: 0, // not stored in slot, pass from file header if needed
|
|
222
|
-
nRays,
|
|
223
|
-
nGates,
|
|
224
|
-
volume: { sweeps: [{ rays }] },
|
|
225
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Parser for the pre-processed sweep binary format produced by the NEXRAD Lambda.
|
|
3
|
+
*
|
|
4
|
+
* Binary layout (all little-endian — numpy default on x86):
|
|
5
|
+
* [0-3] int32 version = 1
|
|
6
|
+
* [4-7] float32 scale
|
|
7
|
+
* [8-11] float32 offset
|
|
8
|
+
* [12-15] float32 sweep_idx
|
|
9
|
+
* [16-19] float32 elev_angle (degrees)
|
|
10
|
+
* [20-23] float32 n_rays
|
|
11
|
+
* [24-27] float32 n_gates
|
|
12
|
+
* [28-31] float32 first_gate (km — range to center of first gate)
|
|
13
|
+
* [32-35] float32 gate_width (km — range sample interval)
|
|
14
|
+
* [36 ...] float32[n_rays] azimuths (degrees)
|
|
15
|
+
* [36+n_rays*4 ...] float32[n_rays] elevations (degrees, unused for 2-D rendering)
|
|
16
|
+
* [36+n_rays*8 ...] int16[n_rays * n_gates] gate values (-32768 = masked/NaN)
|
|
17
|
+
*
|
|
18
|
+
* Gate decoding: physical_value = raw_int16 * scale + offset (when raw != -32768)
|
|
19
|
+
* Masked gates are stored as -99 in the output dataMoments array to match the
|
|
20
|
+
* existing VertexDataGenerator convention.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const PREPROCESSED_VERSION = 1;
|
|
24
|
+
const HEADER_SIZE = 36; // 4 + 8 + 24 bytes
|
|
25
|
+
|
|
26
|
+
export interface NexradSite {
|
|
27
|
+
id: string;
|
|
28
|
+
lat: number;
|
|
29
|
+
lon: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Minimal moment interface consumed by VertexDataGenerator */
|
|
33
|
+
export interface SweepMoment {
|
|
34
|
+
dataMomentName: string;
|
|
35
|
+
azimuth: number;
|
|
36
|
+
/** Distance to center of first gate (km) */
|
|
37
|
+
dataMomentRange: number;
|
|
38
|
+
/** Gate width / range sample interval (km) */
|
|
39
|
+
dataMomentRangeSampleInterval: number;
|
|
40
|
+
/** Physical gate values; masked gates = -99 */
|
|
41
|
+
dataMoments: Float32Array;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SweepRay {
|
|
45
|
+
dataMoments: SweepMoment[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Minimal archive interface consumed by VertexDataGenerator and MapboxRadarLayer */
|
|
49
|
+
export interface RadarArchive {
|
|
50
|
+
latitude: number;
|
|
51
|
+
longitude: number;
|
|
52
|
+
fieldName: string;
|
|
53
|
+
elevAngle: number;
|
|
54
|
+
nRays: number;
|
|
55
|
+
nGates: number;
|
|
56
|
+
volume: {
|
|
57
|
+
sweeps: Array<{ rays: SweepRay[] }>;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns true if the buffer starts with the preprocessed sweep version header.
|
|
63
|
+
* Used in RadarLayer to dispatch to the correct parser.
|
|
64
|
+
*/
|
|
65
|
+
export function isPreprocessedFormat(buffer: ArrayBuffer): boolean {
|
|
66
|
+
if (buffer.byteLength < HEADER_SIZE) return false;
|
|
67
|
+
return new DataView(buffer).getInt32(0, true) === PREPROCESSED_VERSION;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function parsePreprocessedSweep(
|
|
71
|
+
buffer: ArrayBuffer,
|
|
72
|
+
stationId: string,
|
|
73
|
+
fieldName: string,
|
|
74
|
+
nexradSites: NexradSite[],
|
|
75
|
+
): RadarArchive | null {
|
|
76
|
+
const LE = true;
|
|
77
|
+
const view = new DataView(buffer);
|
|
78
|
+
|
|
79
|
+
const version = view.getInt32(0, LE);
|
|
80
|
+
if (version !== PREPROCESSED_VERSION) {
|
|
81
|
+
console.error(`[PreprocessedSweepParser] Unknown version ${version}, expected ${PREPROCESSED_VERSION}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const scale = view.getFloat32(4, LE);
|
|
86
|
+
const offsetVal = view.getFloat32(8, LE);
|
|
87
|
+
// sweep_idx at bytes 12-15 (not needed for rendering)
|
|
88
|
+
const elevAngle = view.getFloat32(16, LE);
|
|
89
|
+
const nRays = Math.round(view.getFloat32(20, LE));
|
|
90
|
+
const nGates = Math.round(view.getFloat32(24, LE));
|
|
91
|
+
const firstGate = view.getFloat32(28, LE); // km
|
|
92
|
+
const gateWidth = view.getFloat32(32, LE); // km
|
|
93
|
+
|
|
94
|
+
const azOffset = HEADER_SIZE; // 36
|
|
95
|
+
const elOffset = azOffset + nRays * 4; // 36 + nRays*4
|
|
96
|
+
const gateOffset = elOffset + nRays * 4; // 36 + nRays*8
|
|
97
|
+
|
|
98
|
+
const expectedBytes = gateOffset + nRays * nGates * 2;
|
|
99
|
+
if (buffer.byteLength < expectedBytes) {
|
|
100
|
+
console.error(
|
|
101
|
+
`[PreprocessedSweepParser] Buffer too small: got ${buffer.byteLength} bytes, ` +
|
|
102
|
+
`need ${expectedBytes} (nRays=${nRays}, nGates=${nGates})`,
|
|
103
|
+
);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const site = nexradSites.find(s => s.id === stationId);
|
|
108
|
+
if (!site) {
|
|
109
|
+
console.error(`[PreprocessedSweepParser] Station "${stationId}" not found in nexrad.json`);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Slice into aligned copies for typed-array access
|
|
114
|
+
const azimuths = new Float32Array(buffer.slice(azOffset, azOffset + nRays * 4));
|
|
115
|
+
const gatesRaw = new Int16Array(buffer.slice(gateOffset, gateOffset + nRays * nGates * 2));
|
|
116
|
+
|
|
117
|
+
const rays: SweepRay[] = [];
|
|
118
|
+
for (let i = 0; i < nRays; i++) {
|
|
119
|
+
const physicalGates = new Float32Array(nGates);
|
|
120
|
+
const base = i * nGates;
|
|
121
|
+
for (let g = 0; g < nGates; g++) {
|
|
122
|
+
const raw = gatesRaw[base + g];
|
|
123
|
+
physicalGates[g] = raw === -32768 ? -99 : raw * scale + offsetVal;
|
|
124
|
+
}
|
|
125
|
+
rays.push({
|
|
126
|
+
dataMoments: [{
|
|
127
|
+
dataMomentName: fieldName,
|
|
128
|
+
azimuth: azimuths[i],
|
|
129
|
+
dataMomentRange: firstGate,
|
|
130
|
+
dataMomentRangeSampleInterval: gateWidth,
|
|
131
|
+
dataMoments: physicalGates,
|
|
132
|
+
}],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
latitude: site.lat,
|
|
138
|
+
longitude: site.lon,
|
|
139
|
+
fieldName,
|
|
140
|
+
elevAngle,
|
|
141
|
+
nRays,
|
|
142
|
+
nGates,
|
|
143
|
+
volume: { sweeps: [{ rays }] },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const SLOT_HDR_BYTES = 28;
|
|
148
|
+
|
|
149
|
+
export function parseSlotBuffer(
|
|
150
|
+
buffer: ArrayBuffer,
|
|
151
|
+
stationId: string,
|
|
152
|
+
fieldName: string,
|
|
153
|
+
nexradSites: NexradSite[],
|
|
154
|
+
// These come from the file header fetched separately
|
|
155
|
+
nRaysFile: number,
|
|
156
|
+
nGatesFile: number,
|
|
157
|
+
firstGateKm: number,
|
|
158
|
+
gateWidthKm: number,
|
|
159
|
+
azimuths: Float32Array,
|
|
160
|
+
): RadarArchive | null {
|
|
161
|
+
const BE = false; // DataView default is big-endian when false
|
|
162
|
+
const view = new DataView(buffer);
|
|
163
|
+
|
|
164
|
+
const scale = view.getFloat32(0, false); // big-endian
|
|
165
|
+
const offsetV = view.getFloat32(4, false);
|
|
166
|
+
const present = view.getUint32(8, false);
|
|
167
|
+
const nRays = view.getUint16(12, false);
|
|
168
|
+
const nGates = view.getUint16(14, false);
|
|
169
|
+
|
|
170
|
+
if (present === 0) {
|
|
171
|
+
console.warn(`[parseSlotBuffer] Slot for ${fieldName} marked not present`);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const gateOffset = SLOT_HDR_BYTES;
|
|
176
|
+
const expectedBytes = gateOffset + nRays * nGates * 2;
|
|
177
|
+
if (buffer.byteLength < expectedBytes) {
|
|
178
|
+
console.error(
|
|
179
|
+
`[parseSlotBuffer] Buffer too small: got ${buffer.byteLength}, need ${expectedBytes}`
|
|
180
|
+
);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const site = nexradSites.find(s => s.id === stationId);
|
|
185
|
+
if (!site) {
|
|
186
|
+
console.error(`[parseSlotBuffer] Station "${stationId}" not found`);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Gate data is big-endian int16
|
|
191
|
+
const gateBytes = buffer.slice(gateOffset, gateOffset + nRays * nGates * 2);
|
|
192
|
+
const gatesRaw = new Int16Array(nRays * nGates);
|
|
193
|
+
const gateView = new DataView(gateBytes);
|
|
194
|
+
for (let i = 0; i < nRays * nGates; i++) {
|
|
195
|
+
gatesRaw[i] = gateView.getInt16(i * 2, false); // big-endian
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const rays: SweepRay[] = [];
|
|
199
|
+
for (let i = 0; i < nRays; i++) {
|
|
200
|
+
const physicalGates = new Float32Array(nGates);
|
|
201
|
+
const base = i * nGates;
|
|
202
|
+
for (let g = 0; g < nGates; g++) {
|
|
203
|
+
const raw = gatesRaw[base + g];
|
|
204
|
+
physicalGates[g] = raw === -32768 ? -99 : raw * scale + offsetV;
|
|
205
|
+
}
|
|
206
|
+
rays.push({
|
|
207
|
+
dataMoments: [{
|
|
208
|
+
dataMomentName: fieldName,
|
|
209
|
+
azimuth: azimuths[i],
|
|
210
|
+
dataMomentRange: firstGateKm,
|
|
211
|
+
dataMomentRangeSampleInterval: gateWidthKm,
|
|
212
|
+
dataMoments: physicalGates,
|
|
213
|
+
}],
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
latitude: site.lat,
|
|
219
|
+
longitude: site.lon,
|
|
220
|
+
fieldName,
|
|
221
|
+
elevAngle: 0, // not stored in slot, pass from file header if needed
|
|
222
|
+
nRays,
|
|
223
|
+
nGates,
|
|
224
|
+
volume: { sweeps: [{ rays }] },
|
|
225
|
+
};
|
|
226
226
|
}
|
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CPU-heavy polar wedge mesh for {@link MapboxRadarLayer}. Extracted for Web Worker offload.
|
|
3
|
-
*/
|
|
4
|
-
export type RadarRayGeometryFrame = {
|
|
5
|
-
nRays: number;
|
|
6
|
-
nGates: number;
|
|
7
|
-
stationLat: number;
|
|
8
|
-
stationLon: number;
|
|
9
|
-
firstGateKm: number;
|
|
10
|
-
gateWidthKm: number;
|
|
11
|
-
rayBoundariesDeg: Float32Array;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export function buildRadarRayGeometryBuffer(frame: RadarRayGeometryFrame): {
|
|
15
|
-
buffer: Float32Array;
|
|
16
|
-
anchorMercator: { x: number; y: number };
|
|
17
|
-
} {
|
|
18
|
-
const nRays = frame.nRays;
|
|
19
|
-
const nGates = frame.nGates;
|
|
20
|
-
if (nRays <= 0 || nGates <= 0) {
|
|
21
|
-
return { buffer: new Float32Array(0), anchorMercator: { x: 0, y: 0 } };
|
|
22
|
-
}
|
|
23
|
-
if (frame.rayBoundariesDeg.length < nRays + 1) {
|
|
24
|
-
return { buffer: new Float32Array(0), anchorMercator: { x: 0, y: 0 } };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const nearRangeM = frame.firstGateKm * 1000;
|
|
28
|
-
const farRangeM = (frame.firstGateKm + frame.gateWidthKm * nGates) * 1000;
|
|
29
|
-
|
|
30
|
-
const floatsPerVertex = 4;
|
|
31
|
-
const vertsPerGate = 6;
|
|
32
|
-
const out = new Float32Array(nRays * nGates * vertsPerGate * floatsPerVertex);
|
|
33
|
-
let write = 0;
|
|
34
|
-
|
|
35
|
-
const mercatorXFromLonDeg = (lonDeg: number) => (lonDeg + 180) / 360;
|
|
36
|
-
const mercatorYFromLatRad = (latRad: number) =>
|
|
37
|
-
(1 - Math.log(Math.tan(Math.PI * 0.25 + latRad * 0.5)) / Math.PI) * 0.5;
|
|
38
|
-
|
|
39
|
-
const anchorX = mercatorXFromLonDeg(frame.stationLon);
|
|
40
|
-
const anchorY = mercatorYFromLatRad(frame.stationLat * (Math.PI / 180));
|
|
41
|
-
const anchorMercator = { x: anchorX, y: anchorY };
|
|
42
|
-
|
|
43
|
-
const getDestination = (lat: number, lon: number, distanceM: number, bearingDeg: number) => {
|
|
44
|
-
const R = 6378137;
|
|
45
|
-
const dRad = distanceM / R;
|
|
46
|
-
const bRad = bearingDeg * (Math.PI / 180);
|
|
47
|
-
const lat1 = lat * (Math.PI / 180);
|
|
48
|
-
const lon1 = lon * (Math.PI / 180);
|
|
49
|
-
const lat2 = Math.asin(Math.sin(lat1) * Math.cos(dRad) + Math.cos(lat1) * Math.sin(dRad) * Math.cos(bRad));
|
|
50
|
-
const lon2 =
|
|
51
|
-
lon1 +
|
|
52
|
-
Math.atan2(
|
|
53
|
-
Math.sin(bRad) * Math.sin(dRad) * Math.cos(lat1),
|
|
54
|
-
Math.cos(dRad) - Math.sin(lat1) * Math.sin(lat2),
|
|
55
|
-
);
|
|
56
|
-
return {
|
|
57
|
-
x: mercatorXFromLonDeg(lon2 * (180 / Math.PI)) - anchorX,
|
|
58
|
-
y: mercatorYFromLatRad(lat2) - anchorY,
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const writeVertex = (x: number, y: number, u: number, v: number) => {
|
|
63
|
-
out[write++] = x;
|
|
64
|
-
out[write++] = y;
|
|
65
|
-
out[write++] = u;
|
|
66
|
-
out[write++] = v;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
for (let rayIdx = 0; rayIdx < nRays; rayIdx++) {
|
|
70
|
-
const az1 = frame.rayBoundariesDeg[rayIdx]!;
|
|
71
|
-
const az2 = frame.rayBoundariesDeg[rayIdx + 1]!;
|
|
72
|
-
const v0 = rayIdx / nRays;
|
|
73
|
-
const v1 = (rayIdx + 1) / nRays;
|
|
74
|
-
|
|
75
|
-
for (let g = 0; g < nGates; g++) {
|
|
76
|
-
const r1 = nearRangeM + (farRangeM - nearRangeM) * (g / nGates);
|
|
77
|
-
const r2 = nearRangeM + (farRangeM - nearRangeM) * ((g + 1) / nGates);
|
|
78
|
-
const u0 = g / nGates;
|
|
79
|
-
const u1 = (g + 1) / nGates;
|
|
80
|
-
|
|
81
|
-
const nearLeft = getDestination(frame.stationLat, frame.stationLon, r1, az1);
|
|
82
|
-
const nearRight = getDestination(frame.stationLat, frame.stationLon, r1, az2);
|
|
83
|
-
const farLeft = getDestination(frame.stationLat, frame.stationLon, r2, az1);
|
|
84
|
-
const farRight = getDestination(frame.stationLat, frame.stationLon, r2, az2);
|
|
85
|
-
|
|
86
|
-
writeVertex(nearLeft.x, nearLeft.y, u0, v0);
|
|
87
|
-
writeVertex(nearRight.x, nearRight.y, u0, v1);
|
|
88
|
-
writeVertex(farLeft.x, farLeft.y, u1, v0);
|
|
89
|
-
|
|
90
|
-
writeVertex(nearRight.x, nearRight.y, u0, v1);
|
|
91
|
-
writeVertex(farRight.x, farRight.y, u1, v1);
|
|
92
|
-
writeVertex(farLeft.x, farLeft.y, u1, v0);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return { buffer: out, anchorMercator };
|
|
97
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* CPU-heavy polar wedge mesh for {@link MapboxRadarLayer}. Extracted for Web Worker offload.
|
|
3
|
+
*/
|
|
4
|
+
export type RadarRayGeometryFrame = {
|
|
5
|
+
nRays: number;
|
|
6
|
+
nGates: number;
|
|
7
|
+
stationLat: number;
|
|
8
|
+
stationLon: number;
|
|
9
|
+
firstGateKm: number;
|
|
10
|
+
gateWidthKm: number;
|
|
11
|
+
rayBoundariesDeg: Float32Array;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function buildRadarRayGeometryBuffer(frame: RadarRayGeometryFrame): {
|
|
15
|
+
buffer: Float32Array;
|
|
16
|
+
anchorMercator: { x: number; y: number };
|
|
17
|
+
} {
|
|
18
|
+
const nRays = frame.nRays;
|
|
19
|
+
const nGates = frame.nGates;
|
|
20
|
+
if (nRays <= 0 || nGates <= 0) {
|
|
21
|
+
return { buffer: new Float32Array(0), anchorMercator: { x: 0, y: 0 } };
|
|
22
|
+
}
|
|
23
|
+
if (frame.rayBoundariesDeg.length < nRays + 1) {
|
|
24
|
+
return { buffer: new Float32Array(0), anchorMercator: { x: 0, y: 0 } };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const nearRangeM = frame.firstGateKm * 1000;
|
|
28
|
+
const farRangeM = (frame.firstGateKm + frame.gateWidthKm * nGates) * 1000;
|
|
29
|
+
|
|
30
|
+
const floatsPerVertex = 4;
|
|
31
|
+
const vertsPerGate = 6;
|
|
32
|
+
const out = new Float32Array(nRays * nGates * vertsPerGate * floatsPerVertex);
|
|
33
|
+
let write = 0;
|
|
34
|
+
|
|
35
|
+
const mercatorXFromLonDeg = (lonDeg: number) => (lonDeg + 180) / 360;
|
|
36
|
+
const mercatorYFromLatRad = (latRad: number) =>
|
|
37
|
+
(1 - Math.log(Math.tan(Math.PI * 0.25 + latRad * 0.5)) / Math.PI) * 0.5;
|
|
38
|
+
|
|
39
|
+
const anchorX = mercatorXFromLonDeg(frame.stationLon);
|
|
40
|
+
const anchorY = mercatorYFromLatRad(frame.stationLat * (Math.PI / 180));
|
|
41
|
+
const anchorMercator = { x: anchorX, y: anchorY };
|
|
42
|
+
|
|
43
|
+
const getDestination = (lat: number, lon: number, distanceM: number, bearingDeg: number) => {
|
|
44
|
+
const R = 6378137;
|
|
45
|
+
const dRad = distanceM / R;
|
|
46
|
+
const bRad = bearingDeg * (Math.PI / 180);
|
|
47
|
+
const lat1 = lat * (Math.PI / 180);
|
|
48
|
+
const lon1 = lon * (Math.PI / 180);
|
|
49
|
+
const lat2 = Math.asin(Math.sin(lat1) * Math.cos(dRad) + Math.cos(lat1) * Math.sin(dRad) * Math.cos(bRad));
|
|
50
|
+
const lon2 =
|
|
51
|
+
lon1 +
|
|
52
|
+
Math.atan2(
|
|
53
|
+
Math.sin(bRad) * Math.sin(dRad) * Math.cos(lat1),
|
|
54
|
+
Math.cos(dRad) - Math.sin(lat1) * Math.sin(lat2),
|
|
55
|
+
);
|
|
56
|
+
return {
|
|
57
|
+
x: mercatorXFromLonDeg(lon2 * (180 / Math.PI)) - anchorX,
|
|
58
|
+
y: mercatorYFromLatRad(lat2) - anchorY,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const writeVertex = (x: number, y: number, u: number, v: number) => {
|
|
63
|
+
out[write++] = x;
|
|
64
|
+
out[write++] = y;
|
|
65
|
+
out[write++] = u;
|
|
66
|
+
out[write++] = v;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
for (let rayIdx = 0; rayIdx < nRays; rayIdx++) {
|
|
70
|
+
const az1 = frame.rayBoundariesDeg[rayIdx]!;
|
|
71
|
+
const az2 = frame.rayBoundariesDeg[rayIdx + 1]!;
|
|
72
|
+
const v0 = rayIdx / nRays;
|
|
73
|
+
const v1 = (rayIdx + 1) / nRays;
|
|
74
|
+
|
|
75
|
+
for (let g = 0; g < nGates; g++) {
|
|
76
|
+
const r1 = nearRangeM + (farRangeM - nearRangeM) * (g / nGates);
|
|
77
|
+
const r2 = nearRangeM + (farRangeM - nearRangeM) * ((g + 1) / nGates);
|
|
78
|
+
const u0 = g / nGates;
|
|
79
|
+
const u1 = (g + 1) / nGates;
|
|
80
|
+
|
|
81
|
+
const nearLeft = getDestination(frame.stationLat, frame.stationLon, r1, az1);
|
|
82
|
+
const nearRight = getDestination(frame.stationLat, frame.stationLon, r1, az2);
|
|
83
|
+
const farLeft = getDestination(frame.stationLat, frame.stationLon, r2, az1);
|
|
84
|
+
const farRight = getDestination(frame.stationLat, frame.stationLon, r2, az2);
|
|
85
|
+
|
|
86
|
+
writeVertex(nearLeft.x, nearLeft.y, u0, v0);
|
|
87
|
+
writeVertex(nearRight.x, nearRight.y, u0, v1);
|
|
88
|
+
writeVertex(farLeft.x, farLeft.y, u1, v0);
|
|
89
|
+
|
|
90
|
+
writeVertex(nearRight.x, nearRight.y, u0, v1);
|
|
91
|
+
writeVertex(farRight.x, farRight.y, u1, v1);
|
|
92
|
+
writeVertex(farLeft.x, farLeft.y, u1, v0);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { buffer: out, anchorMercator };
|
|
97
|
+
}
|