@aguacerowx/mapsgl 0.0.54 → 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 +3 -2
- package/src/NexradWeatherController.js +11 -1
- package/src/NwsWatchesWarningsOverlay.js +12 -8
- package/src/nexrad/loadNexradSites.ts +85 -7
- package/src/nexrad/nexradArchiveDiag.ts +26 -0
- 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 +2724 -37
- 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/nwsSdkConstants.js +8 -0
|
@@ -19,19 +19,119 @@ import {
|
|
|
19
19
|
import type { NexradSite } from './PreprocessedSweepParser.js';
|
|
20
20
|
import { archiveCache, setArchiveCache, type DecodedRadarFrame } from './nexradArchiveCache.js';
|
|
21
21
|
import { loadNexradSites } from './loadNexradSites.js';
|
|
22
|
+
export { setNexradSitesJsonUrl, setNexradSitesFetchAuth } from './loadNexradSites.js';
|
|
22
23
|
import { sampleNexradFrameAtLatLon } from './nexradCrossSectionSampleAtLatLon.js';
|
|
24
|
+
import { decodeRadarSlotMessage, type DecodeSlotRequest } from './radarDecodeSlot.js';
|
|
25
|
+
import { nexradArchiveDiag, redactApiKeyFromUrl } from './nexradArchiveDiag.js';
|
|
23
26
|
|
|
24
27
|
// seek-bzip pulls in node-bzip paths that use `new Buffer()` as a global; browsers have no Buffer.
|
|
25
28
|
if (typeof (globalThis as { Buffer?: typeof Buffer }).Buffer === 'undefined') {
|
|
26
29
|
(globalThis as { Buffer: typeof Buffer }).Buffer = Buffer;
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
/** RN/Hermes has no global {@link DOMException}; keep AbortError semantics for fetch abort paths. */
|
|
33
|
+
function createAbortError(message = 'Aborted'): Error {
|
|
34
|
+
const g = globalThis as typeof globalThis & { DOMException?: typeof DOMException };
|
|
35
|
+
if (typeof g.DOMException !== 'undefined') {
|
|
36
|
+
return new g.DOMException(message, 'AbortError');
|
|
37
|
+
}
|
|
38
|
+
const err = new Error(message);
|
|
39
|
+
err.name = 'AbortError';
|
|
40
|
+
return err;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isAbortError(err: unknown): boolean {
|
|
44
|
+
if (err == null || typeof err !== 'object') return false;
|
|
45
|
+
return (err as Error).name === 'AbortError';
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
let NEXRAD_ARCHIVE_API_KEY = '';
|
|
49
|
+
/** Same as {@link AguaceroCore} grid fetches: `x-app-identifier` on React Native when set. */
|
|
50
|
+
let NEXRAD_ARCHIVE_BUNDLE_ID = '';
|
|
51
|
+
|
|
30
52
|
export function setNexradArchiveApiKey(k: string) {
|
|
31
53
|
NEXRAD_ARCHIVE_API_KEY = k || '';
|
|
32
54
|
}
|
|
33
55
|
|
|
56
|
+
export function setNexradArchiveBundleId(bundleId: string) {
|
|
57
|
+
NEXRAD_ARCHIVE_BUNDLE_ID = bundleId || '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Match {@link AguaceroCore} `urlWithApiKeyParam` for the same CloudFront distribution. */
|
|
61
|
+
function cloudFrontUrlWithApiKeyQuery(baseUrl: string): string {
|
|
62
|
+
if (!NEXRAD_ARCHIVE_API_KEY) return baseUrl;
|
|
63
|
+
const sep = baseUrl.includes('?') ? '&' : '?';
|
|
64
|
+
return `${baseUrl}${sep}apiKey=${NEXRAD_ARCHIVE_API_KEY}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Level-II: match AguaceroCore grid `fetch` headers (x-api-key, x-app-identifier on RN when bundleId set) plus Range. */
|
|
68
|
+
function level2CloudFrontFetchHeaders(range: string | undefined): Record<string, string> {
|
|
69
|
+
const headers: Record<string, string> = {
|
|
70
|
+
'x-api-key': NEXRAD_ARCHIVE_API_KEY,
|
|
71
|
+
};
|
|
72
|
+
if (range !== undefined) {
|
|
73
|
+
headers['Range'] = range;
|
|
74
|
+
}
|
|
75
|
+
const g = globalThis as typeof globalThis & { navigator?: { product?: string } };
|
|
76
|
+
if (g.navigator?.product === 'ReactNative' && NEXRAD_ARCHIVE_BUNDLE_ID) {
|
|
77
|
+
headers['x-app-identifier'] = NEXRAD_ARCHIVE_BUNDLE_ID;
|
|
78
|
+
}
|
|
79
|
+
return headers;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type RadarDecodeWorkerResponse = {
|
|
83
|
+
type: 'DECODE_RESULT';
|
|
84
|
+
requestId: number;
|
|
85
|
+
gateData: Uint8Array | null;
|
|
86
|
+
nRays?: number;
|
|
87
|
+
nGates?: number;
|
|
88
|
+
stationLat?: number;
|
|
89
|
+
stationLon?: number;
|
|
90
|
+
firstGateKm?: number;
|
|
91
|
+
gateWidthKm?: number;
|
|
92
|
+
valueScale?: number;
|
|
93
|
+
valueOffset?: number;
|
|
94
|
+
rayBoundariesDeg?: Float32Array;
|
|
95
|
+
error?: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function shouldDecodeRadarOnMainThread(): boolean {
|
|
99
|
+
const g = globalThis as typeof globalThis & { navigator?: { product?: string } };
|
|
100
|
+
return typeof Worker === 'undefined' || g.navigator?.product === 'ReactNative';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Hermes/RN has no web Workers; mirror Worker.postMessage/onmessage on the JS thread. */
|
|
104
|
+
class MainThreadRadarDecodeWorker implements Pick<Worker, 'postMessage' | 'onmessage' | 'onerror' | 'terminate'> {
|
|
105
|
+
onmessage: ((ev: MessageEvent<RadarDecodeWorkerResponse>) => void) | null = null;
|
|
106
|
+
onerror: ((ev: ErrorEvent) => void) | null = null;
|
|
107
|
+
|
|
108
|
+
postMessage(data: unknown): void {
|
|
109
|
+
const msg = data as DecodeSlotRequest;
|
|
110
|
+
if (!msg || msg.type !== 'DECODE_SLOT') return;
|
|
111
|
+
queueMicrotask(() => {
|
|
112
|
+
try {
|
|
113
|
+
const response = decodeRadarSlotMessage(msg);
|
|
114
|
+
this.onmessage?.({ data: response } as MessageEvent<RadarDecodeWorkerResponse>);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
this.onmessage?.({
|
|
117
|
+
data: {
|
|
118
|
+
type: 'DECODE_RESULT',
|
|
119
|
+
requestId: msg.requestId,
|
|
120
|
+
gateData: null,
|
|
121
|
+
error: error instanceof Error ? error.message : 'Main-thread radar decode failed',
|
|
122
|
+
},
|
|
123
|
+
} as MessageEvent<RadarDecodeWorkerResponse>);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
terminate(): void {}
|
|
129
|
+
}
|
|
130
|
+
|
|
34
131
|
function createRadarDecodeWorker(): Worker {
|
|
132
|
+
if (shouldDecodeRadarOnMainThread()) {
|
|
133
|
+
return new MainThreadRadarDecodeWorker() as unknown as Worker;
|
|
134
|
+
}
|
|
35
135
|
return new Worker(new URL('./radarDecode.worker.bundled.js', import.meta.url), { type: 'module' });
|
|
36
136
|
}
|
|
37
137
|
|
|
@@ -115,22 +215,6 @@ const inflightFetchMeta = new Map<
|
|
|
115
215
|
>();
|
|
116
216
|
let radarFetchRequestSeq = 0;
|
|
117
217
|
|
|
118
|
-
type RadarDecodeWorkerResponse = {
|
|
119
|
-
type: 'DECODE_RESULT';
|
|
120
|
-
requestId: number;
|
|
121
|
-
gateData: Uint8Array | null;
|
|
122
|
-
nRays?: number;
|
|
123
|
-
nGates?: number;
|
|
124
|
-
stationLat?: number;
|
|
125
|
-
stationLon?: number;
|
|
126
|
-
firstGateKm?: number;
|
|
127
|
-
gateWidthKm?: number;
|
|
128
|
-
valueScale?: number;
|
|
129
|
-
valueOffset?: number;
|
|
130
|
-
rayBoundariesDeg?: Float32Array;
|
|
131
|
-
error?: string;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
218
|
let radarDecodeWorkers: Worker[] = [];
|
|
135
219
|
let radarDecodeRequestId = 0;
|
|
136
220
|
const radarDecodePending = new Map<number, {
|
|
@@ -1544,7 +1628,7 @@ function decodeSweepInWorker(
|
|
|
1544
1628
|
priority: 'display' | 'prefetch',
|
|
1545
1629
|
signal?: AbortSignal,
|
|
1546
1630
|
): Promise<DecodedRadarFrame | null> {
|
|
1547
|
-
if (signal?.aborted) return Promise.reject(
|
|
1631
|
+
if (signal?.aborted) return Promise.reject(createAbortError());
|
|
1548
1632
|
|
|
1549
1633
|
const { worker } = getDecodeWorkerForPriority(priority);
|
|
1550
1634
|
const requestId = ++radarDecodeRequestId;
|
|
@@ -1560,7 +1644,7 @@ function decodeSweepInWorker(
|
|
|
1560
1644
|
radarDecodePending.delete(requestId);
|
|
1561
1645
|
radarDecodeRequestMeta.delete(requestId);
|
|
1562
1646
|
signal?.removeEventListener('abort', onAbort);
|
|
1563
|
-
reject(
|
|
1647
|
+
reject(createAbortError());
|
|
1564
1648
|
};
|
|
1565
1649
|
|
|
1566
1650
|
radarDecodePending.set(requestId, {
|
|
@@ -1719,15 +1803,30 @@ export async function fetchAndParseArchive(
|
|
|
1719
1803
|
}
|
|
1720
1804
|
|
|
1721
1805
|
// ── Level-2 two-request range path ───────────────────────────────────────
|
|
1806
|
+
const level2Url = cloudFrontUrlWithApiKeyQuery(url);
|
|
1807
|
+
nexradArchiveDiag('level2.pipeline.start', {
|
|
1808
|
+
objectKey,
|
|
1809
|
+
radarVariable,
|
|
1810
|
+
groupId,
|
|
1811
|
+
apiKeyLen: NEXRAD_ARCHIVE_API_KEY.length,
|
|
1812
|
+
bundleIdLen: NEXRAD_ARCHIVE_BUNDLE_ID.length,
|
|
1813
|
+
urlNoSecret: redactApiKeyFromUrl(level2Url),
|
|
1814
|
+
});
|
|
1722
1815
|
// Request 1: fetch just the header + slot index to find byte offsets
|
|
1723
1816
|
const azBlockBytes = LEVEL2_AZ_BLOCK_BYTES;
|
|
1724
1817
|
const INDEX_FETCH_BYTES =
|
|
1725
1818
|
FILE_HDR_BYTES + azBlockBytes + LEVEL2_FILE_NYQUIST_BYTES + MAX_SLOTS * SLOT_INDEX_ENTRY;
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1819
|
+
nexradArchiveDiag('level2.index.fetch', {
|
|
1820
|
+
objectKey,
|
|
1821
|
+
byteRangeEnd: INDEX_FETCH_BYTES - 1,
|
|
1822
|
+
});
|
|
1823
|
+
const indexResp = await fetch(level2Url, {
|
|
1824
|
+
headers: level2CloudFrontFetchHeaders(`bytes=0-${INDEX_FETCH_BYTES - 1}`),
|
|
1825
|
+
});
|
|
1826
|
+
nexradArchiveDiag('level2.index.response', {
|
|
1827
|
+
objectKey,
|
|
1828
|
+
status: indexResp.status,
|
|
1829
|
+
ok: indexResp.ok,
|
|
1731
1830
|
});
|
|
1732
1831
|
if (!indexResp.ok && indexResp.status !== 206) {
|
|
1733
1832
|
throw new Error(`HTTP ${indexResp.status} fetching level2 index ${url}`);
|
|
@@ -1793,16 +1892,24 @@ export async function fetchAndParseArchive(
|
|
|
1793
1892
|
}
|
|
1794
1893
|
|
|
1795
1894
|
const slotRangeEnd = slotEndExclusive - 1;
|
|
1796
|
-
const slotHeaders
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1895
|
+
const slotHeaders = level2CloudFrontFetchHeaders(`bytes=${slot.offset}-${slotRangeEnd}`);
|
|
1896
|
+
nexradArchiveDiag('level2.slot.fetch', {
|
|
1897
|
+
objectKey,
|
|
1898
|
+
slotIdx,
|
|
1899
|
+
compressedSize: slot.compressedSize,
|
|
1900
|
+
offset: slot.offset,
|
|
1901
|
+
});
|
|
1902
|
+
let slotResp = await fetch(level2Url, { headers: slotHeaders });
|
|
1903
|
+
nexradArchiveDiag('level2.slot.response', {
|
|
1904
|
+
objectKey,
|
|
1905
|
+
status: slotResp.status,
|
|
1906
|
+
ok: slotResp.ok,
|
|
1907
|
+
});
|
|
1801
1908
|
let slotBuffer: ArrayBuffer;
|
|
1802
1909
|
if (slotResp.ok || slotResp.status === 206) {
|
|
1803
1910
|
slotBuffer = await slotResp.arrayBuffer();
|
|
1804
1911
|
} else if (slotResp.status === 416) {
|
|
1805
|
-
const fullResp = await fetch(
|
|
1912
|
+
const fullResp = await fetch(level2Url, { headers: level2CloudFrontFetchHeaders(undefined) });
|
|
1806
1913
|
if (!fullResp.ok) {
|
|
1807
1914
|
throw new Error(`HTTP ${fullResp.status} full fetch after 416 for level2 ${url}`);
|
|
1808
1915
|
}
|
|
@@ -1818,6 +1925,7 @@ export async function fetchAndParseArchive(
|
|
|
1818
1925
|
}
|
|
1819
1926
|
|
|
1820
1927
|
// ── Step 4: decode in worker ────────────────────────────────
|
|
1928
|
+
nexradArchiveDiag('level2.decode.start', { objectKey, radarVariable });
|
|
1821
1929
|
const sites = await loadNexradSites();
|
|
1822
1930
|
let decoded = await decodeSweepInWorker(
|
|
1823
1931
|
objectKey, slotBuffer, header, sites,
|
|
@@ -1836,11 +1944,22 @@ export async function fetchAndParseArchive(
|
|
|
1836
1944
|
);
|
|
1837
1945
|
}
|
|
1838
1946
|
setArchiveCache(cacheKey, decoded);
|
|
1947
|
+
nexradArchiveDiag('level2.decode.done', {
|
|
1948
|
+
objectKey,
|
|
1949
|
+
radarVariable,
|
|
1950
|
+
nRays: decoded.nRays ?? null,
|
|
1951
|
+
nGates: decoded.nGates ?? null,
|
|
1952
|
+
});
|
|
1839
1953
|
return decoded;
|
|
1840
1954
|
} catch (err) {
|
|
1841
|
-
if (err
|
|
1955
|
+
if (isAbortError(err)) {
|
|
1842
1956
|
return null;
|
|
1843
1957
|
}
|
|
1958
|
+
nexradArchiveDiag('fetchAndParseArchive.failed', {
|
|
1959
|
+
objectKey,
|
|
1960
|
+
radarSource,
|
|
1961
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1962
|
+
});
|
|
1844
1963
|
console.error(`[RadarLayer] fetchAndParseArchive failed for ${objectKey}:`, err);
|
|
1845
1964
|
return null;
|
|
1846
1965
|
} finally {
|
|
@@ -619,7 +619,7 @@ function decompress(dat, buf) {
|
|
|
619
619
|
return cct(bufs, ol);
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
-
// src/nexrad/
|
|
622
|
+
// src/nexrad/radarDecodeSlot.ts
|
|
623
623
|
var SLOT_HDR_BYTES = 28;
|
|
624
624
|
function isZstd(bytes) {
|
|
625
625
|
return bytes.length >= 4 && bytes[0] === 40 && bytes[1] === 181 && bytes[2] === 47 && bytes[3] === 253;
|
|
@@ -657,9 +657,7 @@ function buildRayBoundariesDeg(azimuths) {
|
|
|
657
657
|
boundaries[nRays] = azimuths[nRays - 1] + avgSpacing / 2;
|
|
658
658
|
return boundaries;
|
|
659
659
|
}
|
|
660
|
-
|
|
661
|
-
const data = event.data;
|
|
662
|
-
if (!data || data.type !== "DECODE_SLOT") return;
|
|
660
|
+
function decodeRadarSlotMessage(data) {
|
|
663
661
|
const {
|
|
664
662
|
requestId,
|
|
665
663
|
objectKey,
|
|
@@ -671,137 +669,143 @@ self.onmessage = (event) => {
|
|
|
671
669
|
azimuthsBuffer,
|
|
672
670
|
sites
|
|
673
671
|
} = data;
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
672
|
+
const compressed = new Uint8Array(slotBuffer);
|
|
673
|
+
if (!isZstd(compressed)) {
|
|
674
|
+
return {
|
|
675
|
+
type: "DECODE_RESULT",
|
|
676
|
+
requestId,
|
|
677
|
+
gateData: null,
|
|
678
|
+
error: `Slot for ${objectKey} is not valid zstd`
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const decompressed = decompress(compressed);
|
|
682
|
+
const buffer = decompressed.buffer.slice(
|
|
683
|
+
decompressed.byteOffset,
|
|
684
|
+
decompressed.byteOffset + decompressed.byteLength
|
|
685
|
+
);
|
|
686
|
+
const view = new DataView(buffer);
|
|
687
|
+
const valueScale = view.getFloat32(0, false);
|
|
688
|
+
const valueOffset = view.getFloat32(4, false);
|
|
689
|
+
const present = view.getUint32(8, false);
|
|
690
|
+
if (present === 0) {
|
|
691
|
+
return {
|
|
692
|
+
type: "DECODE_RESULT",
|
|
693
|
+
requestId,
|
|
694
|
+
gateData: null,
|
|
695
|
+
error: `Slot for ${objectKey} marked not present`
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
const slotNRays = view.getUint16(12, false);
|
|
699
|
+
const slotNGates = view.getUint16(14, false);
|
|
700
|
+
const gateBytes = buffer.byteLength - SLOT_HDR_BYTES;
|
|
701
|
+
const expectedGates = slotNRays * slotNGates;
|
|
702
|
+
const bytesPerGate = gateBytes / expectedGates;
|
|
703
|
+
if (bytesPerGate !== 1 && bytesPerGate !== 2 && bytesPerGate !== 4) {
|
|
704
|
+
return {
|
|
705
|
+
type: "DECODE_RESULT",
|
|
706
|
+
requestId,
|
|
707
|
+
gateData: null,
|
|
708
|
+
error: `Unexpected bytesPerGate=${bytesPerGate} for ${objectKey}`
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
const keyParts = parseObjectKey(objectKey);
|
|
712
|
+
if (!keyParts) {
|
|
713
|
+
return {
|
|
714
|
+
type: "DECODE_RESULT",
|
|
715
|
+
requestId,
|
|
716
|
+
gateData: null,
|
|
717
|
+
error: `Unable to parse station from key: ${objectKey}`
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
const site = sites.find((s) => s.id === keyParts.stationId);
|
|
721
|
+
if (!site) {
|
|
722
|
+
return {
|
|
723
|
+
type: "DECODE_RESULT",
|
|
724
|
+
requestId,
|
|
725
|
+
gateData: null,
|
|
726
|
+
error: `Station "${keyParts.stationId}" not found`
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
const azimuths = new Float32Array(slotNRays);
|
|
730
|
+
const azView = new DataView(azimuthsBuffer);
|
|
731
|
+
const headerNRays = azimuthsBuffer.byteLength / 4;
|
|
732
|
+
for (let i = 0; i < slotNRays; i++) {
|
|
733
|
+
if (i < headerNRays) {
|
|
734
|
+
azimuths[i] = azView.getFloat32(i * 4, false);
|
|
735
|
+
} else {
|
|
736
|
+
const prevAz = i > 0 ? azimuths[i - 1] : 0;
|
|
737
|
+
azimuths[i] = (prevAz + 360 / slotNRays) % 360;
|
|
736
738
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
739
|
+
}
|
|
740
|
+
const rayBoundariesDeg = buildRayBoundariesDeg(azimuths);
|
|
741
|
+
const gateCount = slotNRays * slotNGates;
|
|
742
|
+
const gateDataCopy = new Uint8Array(slotNRays * slotNGates * 2);
|
|
743
|
+
const outView = new DataView(gateDataCopy.buffer);
|
|
744
|
+
if (bytesPerGate === 1) {
|
|
745
|
+
const raw = new Uint8Array(buffer, SLOT_HDR_BYTES, gateCount);
|
|
746
|
+
for (let ray = 0; ray < slotNRays; ray++) {
|
|
747
|
+
let prev = 0;
|
|
748
|
+
for (let g = 0; g < slotNGates; g++) {
|
|
749
|
+
const idx = ray * slotNGates + g;
|
|
750
|
+
const delta = raw[idx];
|
|
751
|
+
const val = prev + delta & 255;
|
|
752
|
+
prev = val;
|
|
753
|
+
if (val === 0) {
|
|
754
|
+
outView.setInt16(idx * 2, -32768, false);
|
|
755
|
+
} else {
|
|
756
|
+
outView.setInt16(idx * 2, val, false);
|
|
757
|
+
}
|
|
746
758
|
}
|
|
747
759
|
}
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
prev = val;
|
|
761
|
-
if (val === 0) {
|
|
762
|
-
outView.setInt16(idx * 2, -32768, false);
|
|
763
|
-
} else {
|
|
764
|
-
outView.setInt16(idx * 2, val, false);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
760
|
+
} else {
|
|
761
|
+
const rawBytes = new Uint8Array(buffer, SLOT_HDR_BYTES, gateCount * 2);
|
|
762
|
+
for (let ray = 0; ray < slotNRays; ray++) {
|
|
763
|
+
let prev = 0;
|
|
764
|
+
for (let g = 0; g < slotNGates; g++) {
|
|
765
|
+
const idx = ray * slotNGates + g;
|
|
766
|
+
const hi = rawBytes[idx * 2];
|
|
767
|
+
const lo = rawBytes[idx * 2 + 1];
|
|
768
|
+
const delta = (hi << 8 | lo) << 16 >> 16;
|
|
769
|
+
const val = prev + delta | 0;
|
|
770
|
+
prev = val;
|
|
771
|
+
outView.setInt16(idx * 2, val, false);
|
|
767
772
|
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
type: "DECODE_RESULT",
|
|
777
|
+
requestId,
|
|
778
|
+
gateData: gateDataCopy,
|
|
779
|
+
stationLat: site.lat,
|
|
780
|
+
stationLon: site.lon,
|
|
781
|
+
firstGateKm,
|
|
782
|
+
gateWidthKm,
|
|
783
|
+
valueScale,
|
|
784
|
+
valueOffset,
|
|
785
|
+
rayBoundariesDeg,
|
|
786
|
+
nRays: slotNRays,
|
|
787
|
+
nGates: slotNGates
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/nexrad/radarDecode.worker.ts
|
|
792
|
+
self.onmessage = (event) => {
|
|
793
|
+
const data = event.data;
|
|
794
|
+
if (!data || data.type !== "DECODE_SLOT") return;
|
|
795
|
+
try {
|
|
796
|
+
const response = decodeRadarSlotMessage(data);
|
|
797
|
+
if (response.gateData && response.rayBoundariesDeg) {
|
|
798
|
+
self.postMessage(response, [
|
|
799
|
+
response.gateData.buffer,
|
|
800
|
+
response.rayBoundariesDeg.buffer
|
|
801
|
+
]);
|
|
768
802
|
} else {
|
|
769
|
-
|
|
770
|
-
for (let ray = 0; ray < slotNRays; ray++) {
|
|
771
|
-
let prev = 0;
|
|
772
|
-
for (let g = 0; g < slotNGates; g++) {
|
|
773
|
-
const idx = ray * slotNGates + g;
|
|
774
|
-
const hi = rawBytes[idx * 2];
|
|
775
|
-
const lo = rawBytes[idx * 2 + 1];
|
|
776
|
-
const delta = (hi << 8 | lo) << 16 >> 16;
|
|
777
|
-
const val = prev + delta | 0;
|
|
778
|
-
prev = val;
|
|
779
|
-
outView.setInt16(idx * 2, val, false);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
803
|
+
self.postMessage(response);
|
|
782
804
|
}
|
|
783
|
-
const response = {
|
|
784
|
-
type: "DECODE_RESULT",
|
|
785
|
-
requestId,
|
|
786
|
-
gateData: gateDataCopy,
|
|
787
|
-
stationLat: site.lat,
|
|
788
|
-
stationLon: site.lon,
|
|
789
|
-
firstGateKm,
|
|
790
|
-
gateWidthKm,
|
|
791
|
-
valueScale,
|
|
792
|
-
valueOffset,
|
|
793
|
-
rayBoundariesDeg,
|
|
794
|
-
nRays: slotNRays,
|
|
795
|
-
nGates: slotNGates
|
|
796
|
-
};
|
|
797
|
-
self.postMessage(response, [
|
|
798
|
-
gateDataCopy.buffer,
|
|
799
|
-
rayBoundariesDeg.buffer
|
|
800
|
-
]);
|
|
801
805
|
} catch (error) {
|
|
802
806
|
self.postMessage({
|
|
803
807
|
type: "DECODE_RESULT",
|
|
804
|
-
requestId,
|
|
808
|
+
requestId: data.requestId,
|
|
805
809
|
gateData: null,
|
|
806
810
|
error: error instanceof Error ? error.message : "Unknown worker error"
|
|
807
811
|
});
|