@aguacerowx/mapsgl 0.0.58 → 0.0.59
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 +31 -31
- package/src/GridRenderLayer.js +1387 -1387
- package/src/MapManager.js +197 -197
- package/src/NexradSitesOverlay.js +148 -148
- package/src/NexradWeatherController.js +523 -523
- 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 +4 -2
- package/src/nexrad/radarArchiveCore.ts +1987 -1985
- 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,25 +1,25 @@
|
|
|
1
|
-
import { decodeRadarSlotMessage, type DecodeSlotRequest } from './radarDecodeSlot.js';
|
|
2
|
-
|
|
3
|
-
self.onmessage = (event: MessageEvent<DecodeSlotRequest>) => {
|
|
4
|
-
const data = event.data;
|
|
5
|
-
if (!data || data.type !== 'DECODE_SLOT') return;
|
|
6
|
-
|
|
7
|
-
try {
|
|
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
|
-
]);
|
|
14
|
-
} else {
|
|
15
|
-
self.postMessage(response);
|
|
16
|
-
}
|
|
17
|
-
} catch (error) {
|
|
18
|
-
self.postMessage({
|
|
19
|
-
type: 'DECODE_RESULT',
|
|
20
|
-
requestId: data.requestId,
|
|
21
|
-
gateData: null,
|
|
22
|
-
error: error instanceof Error ? error.message : 'Unknown worker error',
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
};
|
|
1
|
+
import { decodeRadarSlotMessage, type DecodeSlotRequest } from './radarDecodeSlot.js';
|
|
2
|
+
|
|
3
|
+
self.onmessage = (event: MessageEvent<DecodeSlotRequest>) => {
|
|
4
|
+
const data = event.data;
|
|
5
|
+
if (!data || data.type !== 'DECODE_SLOT') return;
|
|
6
|
+
|
|
7
|
+
try {
|
|
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
|
+
]);
|
|
14
|
+
} else {
|
|
15
|
+
self.postMessage(response);
|
|
16
|
+
}
|
|
17
|
+
} catch (error) {
|
|
18
|
+
self.postMessage({
|
|
19
|
+
type: 'DECODE_RESULT',
|
|
20
|
+
requestId: data.requestId,
|
|
21
|
+
gateData: null,
|
|
22
|
+
error: error instanceof Error ? error.message : 'Unknown worker error',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -1,195 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,111 +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
|
-
}
|
|
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
|
+
}
|