@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,810 @@
|
|
|
1
|
+
// src/nexrad/MapboxRadarLayer.ts
|
|
2
|
+
import { DEFAULT_COLORMAPS } from "@aguacerowx/javascript-sdk";
|
|
3
|
+
|
|
4
|
+
// src/nexrad/buildRadarRayGeometry.ts
|
|
5
|
+
function buildRadarRayGeometryBuffer(frame) {
|
|
6
|
+
const nRays = frame.nRays;
|
|
7
|
+
const nGates = frame.nGates;
|
|
8
|
+
if (nRays <= 0 || nGates <= 0) {
|
|
9
|
+
return { buffer: new Float32Array(0), anchorMercator: { x: 0, y: 0 } };
|
|
10
|
+
}
|
|
11
|
+
if (frame.rayBoundariesDeg.length < nRays + 1) {
|
|
12
|
+
return { buffer: new Float32Array(0), anchorMercator: { x: 0, y: 0 } };
|
|
13
|
+
}
|
|
14
|
+
const nearRangeM = frame.firstGateKm * 1e3;
|
|
15
|
+
const farRangeM = (frame.firstGateKm + frame.gateWidthKm * nGates) * 1e3;
|
|
16
|
+
const floatsPerVertex = 4;
|
|
17
|
+
const vertsPerGate = 6;
|
|
18
|
+
const out = new Float32Array(nRays * nGates * vertsPerGate * floatsPerVertex);
|
|
19
|
+
let write = 0;
|
|
20
|
+
const mercatorXFromLonDeg = (lonDeg) => (lonDeg + 180) / 360;
|
|
21
|
+
const mercatorYFromLatRad = (latRad) => (1 - Math.log(Math.tan(Math.PI * 0.25 + latRad * 0.5)) / Math.PI) * 0.5;
|
|
22
|
+
const anchorX = mercatorXFromLonDeg(frame.stationLon);
|
|
23
|
+
const anchorY = mercatorYFromLatRad(frame.stationLat * (Math.PI / 180));
|
|
24
|
+
const anchorMercator = { x: anchorX, y: anchorY };
|
|
25
|
+
const getDestination = (lat, lon, distanceM, bearingDeg) => {
|
|
26
|
+
const R = 6378137;
|
|
27
|
+
const dRad = distanceM / R;
|
|
28
|
+
const bRad = bearingDeg * (Math.PI / 180);
|
|
29
|
+
const lat1 = lat * (Math.PI / 180);
|
|
30
|
+
const lon1 = lon * (Math.PI / 180);
|
|
31
|
+
const lat2 = Math.asin(Math.sin(lat1) * Math.cos(dRad) + Math.cos(lat1) * Math.sin(dRad) * Math.cos(bRad));
|
|
32
|
+
const lon2 = lon1 + Math.atan2(
|
|
33
|
+
Math.sin(bRad) * Math.sin(dRad) * Math.cos(lat1),
|
|
34
|
+
Math.cos(dRad) - Math.sin(lat1) * Math.sin(lat2)
|
|
35
|
+
);
|
|
36
|
+
return {
|
|
37
|
+
x: mercatorXFromLonDeg(lon2 * (180 / Math.PI)) - anchorX,
|
|
38
|
+
y: mercatorYFromLatRad(lat2) - anchorY
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const writeVertex = (x, y, u, v) => {
|
|
42
|
+
out[write++] = x;
|
|
43
|
+
out[write++] = y;
|
|
44
|
+
out[write++] = u;
|
|
45
|
+
out[write++] = v;
|
|
46
|
+
};
|
|
47
|
+
for (let rayIdx = 0; rayIdx < nRays; rayIdx++) {
|
|
48
|
+
const az1 = frame.rayBoundariesDeg[rayIdx];
|
|
49
|
+
const az2 = frame.rayBoundariesDeg[rayIdx + 1];
|
|
50
|
+
const v0 = rayIdx / nRays;
|
|
51
|
+
const v1 = (rayIdx + 1) / nRays;
|
|
52
|
+
for (let g = 0; g < nGates; g++) {
|
|
53
|
+
const r1 = nearRangeM + (farRangeM - nearRangeM) * (g / nGates);
|
|
54
|
+
const r2 = nearRangeM + (farRangeM - nearRangeM) * ((g + 1) / nGates);
|
|
55
|
+
const u0 = g / nGates;
|
|
56
|
+
const u1 = (g + 1) / nGates;
|
|
57
|
+
const nearLeft = getDestination(frame.stationLat, frame.stationLon, r1, az1);
|
|
58
|
+
const nearRight = getDestination(frame.stationLat, frame.stationLon, r1, az2);
|
|
59
|
+
const farLeft = getDestination(frame.stationLat, frame.stationLon, r2, az1);
|
|
60
|
+
const farRight = getDestination(frame.stationLat, frame.stationLon, r2, az2);
|
|
61
|
+
writeVertex(nearLeft.x, nearLeft.y, u0, v0);
|
|
62
|
+
writeVertex(nearRight.x, nearRight.y, u0, v1);
|
|
63
|
+
writeVertex(farLeft.x, farLeft.y, u1, v0);
|
|
64
|
+
writeVertex(nearRight.x, nearRight.y, u0, v1);
|
|
65
|
+
writeVertex(farRight.x, farRight.y, u1, v1);
|
|
66
|
+
writeVertex(farLeft.x, farLeft.y, u1, v0);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { buffer: out, anchorMercator };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/nexrad/radarFrameGpuMatch.ts
|
|
73
|
+
function angularDistanceDeg(a, b) {
|
|
74
|
+
let d = Math.abs(a - b) % 360;
|
|
75
|
+
if (d > 180) d = 360 - d;
|
|
76
|
+
return d;
|
|
77
|
+
}
|
|
78
|
+
function canonicalBinsRadarFrame(frame) {
|
|
79
|
+
const nRays = frame.nRays;
|
|
80
|
+
const nGates = frame.nGates;
|
|
81
|
+
if (nRays <= 0 || nGates <= 0) return frame;
|
|
82
|
+
if (frame.rayBoundariesDeg.length < nRays + 1) return frame;
|
|
83
|
+
const degPerBin = 360 / nRays;
|
|
84
|
+
const bytesPerRay = nGates * 2;
|
|
85
|
+
const centers = new Float32Array(nRays);
|
|
86
|
+
for (let r = 0; r < nRays; r++) {
|
|
87
|
+
const lower = frame.rayBoundariesDeg[r];
|
|
88
|
+
const upper = frame.rayBoundariesDeg[r + 1];
|
|
89
|
+
const center = (lower + upper) * 0.5;
|
|
90
|
+
centers[r] = (center % 360 + 360) % 360;
|
|
91
|
+
}
|
|
92
|
+
const canonicalGateData = new Uint8Array(nRays * bytesPerRay);
|
|
93
|
+
for (let i = 0; i < canonicalGateData.length; i += 2) {
|
|
94
|
+
canonicalGateData[i] = 128;
|
|
95
|
+
}
|
|
96
|
+
const used = new Array(nRays).fill(false);
|
|
97
|
+
for (let bin = 0; bin < nRays; bin++) {
|
|
98
|
+
const targetDeg = ((bin + 0.5) * degPerBin % 360 + 360) % 360;
|
|
99
|
+
let bestR = -1;
|
|
100
|
+
let bestDist = Infinity;
|
|
101
|
+
for (let r = 0; r < nRays; r++) {
|
|
102
|
+
if (used[r]) continue;
|
|
103
|
+
const dist = angularDistanceDeg(centers[r], targetDeg);
|
|
104
|
+
if (dist < bestDist) {
|
|
105
|
+
bestDist = dist;
|
|
106
|
+
bestR = r;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (bestR >= 0) {
|
|
110
|
+
used[bestR] = true;
|
|
111
|
+
canonicalGateData.set(
|
|
112
|
+
frame.gateData.subarray(bestR * bytesPerRay, (bestR + 1) * bytesPerRay),
|
|
113
|
+
bin * bytesPerRay
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const canonicalBoundaries = new Float32Array(nRays + 1);
|
|
118
|
+
for (let i = 0; i <= nRays; i++) {
|
|
119
|
+
canonicalBoundaries[i] = i * degPerBin;
|
|
120
|
+
}
|
|
121
|
+
return { ...frame, gateData: canonicalGateData, rayBoundariesDeg: canonicalBoundaries };
|
|
122
|
+
}
|
|
123
|
+
function sortRadarFrameByAzimuth(frame) {
|
|
124
|
+
const nRays = frame.nRays;
|
|
125
|
+
const nGates = frame.nGates;
|
|
126
|
+
const bytesPerRay = nGates * 2;
|
|
127
|
+
const rayOrder = Array.from({ length: nRays }, (_, i) => i).sort(
|
|
128
|
+
(a, b) => frame.rayBoundariesDeg[a] - frame.rayBoundariesDeg[b]
|
|
129
|
+
);
|
|
130
|
+
const sortedGateData = new Uint8Array(nRays * bytesPerRay);
|
|
131
|
+
const sortedBoundaries = new Float32Array(nRays + 1);
|
|
132
|
+
for (let newIdx = 0; newIdx < nRays; newIdx++) {
|
|
133
|
+
const oldIdx = rayOrder[newIdx];
|
|
134
|
+
sortedGateData.set(
|
|
135
|
+
frame.gateData.subarray(oldIdx * bytesPerRay, (oldIdx + 1) * bytesPerRay),
|
|
136
|
+
newIdx * bytesPerRay
|
|
137
|
+
);
|
|
138
|
+
sortedBoundaries[newIdx] = frame.rayBoundariesDeg[oldIdx];
|
|
139
|
+
}
|
|
140
|
+
sortedBoundaries[nRays] = frame.rayBoundariesDeg[rayOrder[nRays - 1]] + (frame.rayBoundariesDeg[rayOrder[1]] - frame.rayBoundariesDeg[rayOrder[0]]);
|
|
141
|
+
return { ...frame, gateData: sortedGateData, rayBoundariesDeg: sortedBoundaries };
|
|
142
|
+
}
|
|
143
|
+
function prepareRadarFrameForGpuReadout(frame, options) {
|
|
144
|
+
const hasLayoutKey = options?.geometryLayoutKey != null && options.geometryLayoutKey !== "";
|
|
145
|
+
return hasLayoutKey ? canonicalBinsRadarFrame(frame) : sortRadarFrameByAzimuth(frame);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/nexrad/MapboxRadarLayer.ts
|
|
149
|
+
var GEOMETRY_LRU_MAX = 12;
|
|
150
|
+
var geometryLru = /* @__PURE__ */ new Map();
|
|
151
|
+
function geometryLruTouch(key) {
|
|
152
|
+
const e = geometryLru.get(key);
|
|
153
|
+
if (!e) return void 0;
|
|
154
|
+
geometryLru.delete(key);
|
|
155
|
+
geometryLru.set(key, e);
|
|
156
|
+
return e;
|
|
157
|
+
}
|
|
158
|
+
function geometryLruPut(key, entry) {
|
|
159
|
+
if (geometryLru.has(key)) geometryLru.delete(key);
|
|
160
|
+
geometryLru.set(key, entry);
|
|
161
|
+
while (geometryLru.size > GEOMETRY_LRU_MAX) {
|
|
162
|
+
const oldest = geometryLru.keys().next().value;
|
|
163
|
+
if (oldest === void 0) break;
|
|
164
|
+
geometryLru.delete(oldest);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function fingerprintRayBoundariesDeg(b) {
|
|
168
|
+
return fingerprintRayBoundariesDegQuantized(b, 1e3);
|
|
169
|
+
}
|
|
170
|
+
function fingerprintRayBoundariesDegQuantized(b, unitsPerDeg) {
|
|
171
|
+
const n = b.length;
|
|
172
|
+
if (n === 0) return "0";
|
|
173
|
+
let h = 2166136261;
|
|
174
|
+
for (let i = 0; i < n; i++) {
|
|
175
|
+
const v = Math.round(b[i] * unitsPerDeg);
|
|
176
|
+
h ^= v;
|
|
177
|
+
h = Math.imul(h, 16777619);
|
|
178
|
+
}
|
|
179
|
+
h ^= n * 73856093;
|
|
180
|
+
return (h >>> 0).toString(36);
|
|
181
|
+
}
|
|
182
|
+
function getRefc0DefaultColormap() {
|
|
183
|
+
const cm = DEFAULT_COLORMAPS?.refc_0?.units?.dBZ?.colormap;
|
|
184
|
+
return Array.isArray(cm) && cm.length >= 2 ? cm : null;
|
|
185
|
+
}
|
|
186
|
+
function buildRadarShaders(fragmentHighFloatSupported) {
|
|
187
|
+
const fragPrec = fragmentHighFloatSupported ? "highp" : "mediump";
|
|
188
|
+
const vertex = `
|
|
189
|
+
precision highp float;
|
|
190
|
+
uniform mat4 u_matrix;
|
|
191
|
+
attribute vec2 a_pos; // offset from anchor (small numbers)
|
|
192
|
+
attribute vec2 a_uv;
|
|
193
|
+
varying ${fragPrec} vec2 v_uv;
|
|
194
|
+
|
|
195
|
+
void main() {
|
|
196
|
+
v_uv = a_uv;
|
|
197
|
+
// u_matrix already has anchor baked in from CPU-side float64 math
|
|
198
|
+
gl_Position = u_matrix * vec4(a_pos, 0.0, 1.0);
|
|
199
|
+
}`;
|
|
200
|
+
const fragment = `
|
|
201
|
+
precision ${fragPrec} float;
|
|
202
|
+
varying ${fragPrec} vec2 v_uv;
|
|
203
|
+
|
|
204
|
+
uniform sampler2D u_gate_texture;
|
|
205
|
+
uniform sampler2D u_lut_texture;
|
|
206
|
+
uniform vec2 u_texture_size;
|
|
207
|
+
uniform vec2 u_value_scale_offset;
|
|
208
|
+
uniform vec2 u_lut_value_range;
|
|
209
|
+
uniform float u_discrete_integer_lut;
|
|
210
|
+
uniform float u_opacity;
|
|
211
|
+
uniform float u_gate_smooth_polar;
|
|
212
|
+
|
|
213
|
+
float decodeInt16(vec2 encoded) {
|
|
214
|
+
float hi = floor(encoded.x * 255.0 + 0.5);
|
|
215
|
+
float lo = floor(encoded.y * 255.0 + 0.5);
|
|
216
|
+
float raw = lo + hi * 256.0;
|
|
217
|
+
if (raw >= 32768.0) raw -= 65536.0;
|
|
218
|
+
return raw;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
float sampleGateRawBilinear(vec2 gxy) {
|
|
222
|
+
vec2 sz = u_texture_size;
|
|
223
|
+
|
|
224
|
+
// Both Range (X) and Azimuth (Y) cover the full domain.
|
|
225
|
+
// Scale by size and offset by 0.5 to target the exact texel centers.
|
|
226
|
+
float x_px = gxy.x * sz.x - 0.5;
|
|
227
|
+
float y_px = gxy.y * sz.y - 0.5;
|
|
228
|
+
|
|
229
|
+
vec2 i0 = floor(vec2(x_px, y_px));
|
|
230
|
+
vec2 f = vec2(x_px, y_px) - i0;
|
|
231
|
+
vec2 i1 = i0 + 1.0;
|
|
232
|
+
|
|
233
|
+
// Clamp Range (X) to valid texel indices
|
|
234
|
+
i0.x = clamp(i0.x, 0.0, sz.x - 1.0);
|
|
235
|
+
i1.x = clamp(i1.x, 0.0, sz.x - 1.0);
|
|
236
|
+
|
|
237
|
+
// Wrap Azimuth (Y) seamlessly (add sz.y to avoid negative mod bug on Windows/ANGLE)
|
|
238
|
+
i0.y = mod(i0.y + sz.y, sz.y);
|
|
239
|
+
i1.y = mod(i1.y + sz.y, sz.y);
|
|
240
|
+
|
|
241
|
+
// Calculate bilinear weights
|
|
242
|
+
float w00 = (1.0 - f.x) * (1.0 - f.y);
|
|
243
|
+
float w10 = f.x * (1.0 - f.y);
|
|
244
|
+
float w01 = (1.0 - f.x) * f.y;
|
|
245
|
+
float w11 = f.x * f.y;
|
|
246
|
+
|
|
247
|
+
vec2 invSz = 1.0 / sz;
|
|
248
|
+
vec4 p00 = texture2D(u_gate_texture, (vec2(i0.x, i0.y) + 0.5) * invSz);
|
|
249
|
+
vec4 p10 = texture2D(u_gate_texture, (vec2(i1.x, i0.y) + 0.5) * invSz);
|
|
250
|
+
vec4 p01 = texture2D(u_gate_texture, (vec2(i0.x, i1.y) + 0.5) * invSz);
|
|
251
|
+
vec4 p11 = texture2D(u_gate_texture, (vec2(i1.x, i1.y) + 0.5) * invSz);
|
|
252
|
+
|
|
253
|
+
float r00 = decodeInt16(vec2(p00.r, p00.a));
|
|
254
|
+
float r10 = decodeInt16(vec2(p10.r, p10.a));
|
|
255
|
+
float r01 = decodeInt16(vec2(p01.r, p01.a));
|
|
256
|
+
float r11 = decodeInt16(vec2(p11.r, p11.a));
|
|
257
|
+
|
|
258
|
+
float acc = 0.0;
|
|
259
|
+
float wsum = 0.0;
|
|
260
|
+
if (r00 > -32768.0) { acc += r00 * w00; wsum += w00; }
|
|
261
|
+
if (r10 > -32768.0) { acc += r10 * w10; wsum += w10; }
|
|
262
|
+
if (r01 > -32768.0) { acc += r01 * w01; wsum += w01; }
|
|
263
|
+
if (r11 > -32768.0) { acc += r11 * w11; wsum += w11; }
|
|
264
|
+
if (wsum < 1e-6) return -32768.0;
|
|
265
|
+
return acc / wsum;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
float sampleGateRawNearest(vec2 gxy) {
|
|
269
|
+
vec2 sz = u_texture_size;
|
|
270
|
+
vec2 px = gxy * sz;
|
|
271
|
+
vec2 i = floor(px);
|
|
272
|
+
|
|
273
|
+
// Clamp Range (X)
|
|
274
|
+
i.x = clamp(i.x, 0.0, sz.x - 1.0);
|
|
275
|
+
|
|
276
|
+
// Wrap Azimuth (Y) seamlessly (add sz.y to avoid negative mod bug on Windows/ANGLE)
|
|
277
|
+
i.y = mod(i.y + sz.y, sz.y);
|
|
278
|
+
|
|
279
|
+
vec4 p = texture2D(u_gate_texture, (i + 0.5) / sz);
|
|
280
|
+
return decodeInt16(vec2(p.r, p.a));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
void main() {
|
|
284
|
+
float raw;
|
|
285
|
+
if (u_gate_smooth_polar < 0.5) {
|
|
286
|
+
raw = sampleGateRawNearest(v_uv);
|
|
287
|
+
} else {
|
|
288
|
+
raw = sampleGateRawBilinear(v_uv);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (raw <= -32768.0) {
|
|
292
|
+
discard;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
float physical = raw * u_value_scale_offset.x + u_value_scale_offset.y;
|
|
296
|
+
|
|
297
|
+
if (physical < u_lut_value_range.x || physical > u_lut_value_range.y) discard;
|
|
298
|
+
|
|
299
|
+
float lutT;
|
|
300
|
+
if (u_discrete_integer_lut > 0.5) {
|
|
301
|
+
float n = u_lut_value_range.y - u_lut_value_range.x + 1.0;
|
|
302
|
+
lutT = (physical - u_lut_value_range.x + 0.5) / max(n, 0.0001);
|
|
303
|
+
} else {
|
|
304
|
+
lutT = (physical - u_lut_value_range.x) / max(u_lut_value_range.y - u_lut_value_range.x, 0.0001);
|
|
305
|
+
}
|
|
306
|
+
lutT = clamp(lutT, 0.0, 1.0);
|
|
307
|
+
vec4 color = texture2D(u_lut_texture, vec2(lutT, 0.5));
|
|
308
|
+
if (color.a <= 0.001) discard;
|
|
309
|
+
gl_FragColor = vec4(color.rgb, color.a * u_opacity);
|
|
310
|
+
}`;
|
|
311
|
+
return { vertex, fragment };
|
|
312
|
+
}
|
|
313
|
+
var MapboxRadarLayer = class _MapboxRadarLayer {
|
|
314
|
+
id;
|
|
315
|
+
type = "custom";
|
|
316
|
+
renderingMode = "2d";
|
|
317
|
+
static DEG_TO_RAD = Math.PI / 180;
|
|
318
|
+
static RAD_TO_DEG = 180 / Math.PI;
|
|
319
|
+
static EARTH_RADIUS_M = 6378137;
|
|
320
|
+
static LUT_SIZE = 256;
|
|
321
|
+
geometryBuffer = null;
|
|
322
|
+
gateTexture = null;
|
|
323
|
+
lutTexture = null;
|
|
324
|
+
activeVertCount = 0;
|
|
325
|
+
hasFrame = false;
|
|
326
|
+
pendingFrame = null;
|
|
327
|
+
pendingFrameUploadOptions = null;
|
|
328
|
+
/** Coalesce rapid setFrameData calls (same animation frame → one upload). */
|
|
329
|
+
pendingSetFrameRaf = null;
|
|
330
|
+
rafCoalesceFrame = null;
|
|
331
|
+
rafCoalesceOptions;
|
|
332
|
+
anchorMercator = { x: 0, y: 0 };
|
|
333
|
+
program = null;
|
|
334
|
+
gl = null;
|
|
335
|
+
uMatrixLocation = null;
|
|
336
|
+
uGateTextureLocation = null;
|
|
337
|
+
uLutTextureLocation = null;
|
|
338
|
+
uTextureSizeLocation = null;
|
|
339
|
+
uValueScaleOffsetLocation = null;
|
|
340
|
+
uLutValueRangeLocation = null;
|
|
341
|
+
uDiscreteIntegerLutLocation = null;
|
|
342
|
+
uOpacityLocation = null;
|
|
343
|
+
uGateSmoothPolarLocation = null;
|
|
344
|
+
aPosLocation = -1;
|
|
345
|
+
aUvLocation = -1;
|
|
346
|
+
activeTextureWidth = 0;
|
|
347
|
+
activeTextureHeight = 0;
|
|
348
|
+
activeValueScale = 1;
|
|
349
|
+
activeValueOffset = 0;
|
|
350
|
+
lutValueMin = 0;
|
|
351
|
+
lutValueMax = 80;
|
|
352
|
+
opacity = 1;
|
|
353
|
+
currentColormap = null;
|
|
354
|
+
interpolateColormap = true;
|
|
355
|
+
discreteIntegerLutActive = false;
|
|
356
|
+
gateSmoothing = false;
|
|
357
|
+
lastUploadedFrame = null;
|
|
358
|
+
map = null;
|
|
359
|
+
geometryCache = null;
|
|
360
|
+
buildGeometryCacheKey(frame, options) {
|
|
361
|
+
const base = `${frame.stationLat},${frame.stationLon},${frame.firstGateKm},${frame.gateWidthKm},${frame.nRays},${frame.nGates}`;
|
|
362
|
+
const layout = options?.geometryLayoutKey;
|
|
363
|
+
if (layout != null && layout !== "") {
|
|
364
|
+
return `${base},layout:${layout}`;
|
|
365
|
+
}
|
|
366
|
+
if (options?.geometryCacheKeysRayBoundaries === true) {
|
|
367
|
+
return `${base},rb:${fingerprintRayBoundariesDeg(frame.rayBoundariesDeg)}`;
|
|
368
|
+
}
|
|
369
|
+
return base;
|
|
370
|
+
}
|
|
371
|
+
constructor(id) {
|
|
372
|
+
this.id = id;
|
|
373
|
+
}
|
|
374
|
+
setValueRange(min, max) {
|
|
375
|
+
const safeMin = Number.isFinite(min) ? min : 0;
|
|
376
|
+
const safeMax = Number.isFinite(max) && max > safeMin ? max : 80;
|
|
377
|
+
if (this.lutValueMin === safeMin && this.lutValueMax === safeMax) return;
|
|
378
|
+
this.lutValueMin = safeMin;
|
|
379
|
+
this.lutValueMax = safeMax;
|
|
380
|
+
const gl = this.gl;
|
|
381
|
+
if (gl && this.lutTexture) {
|
|
382
|
+
const lut = this.buildLutTextureData();
|
|
383
|
+
gl.bindTexture(gl.TEXTURE_2D, this.lutTexture);
|
|
384
|
+
gl.texImage2D(
|
|
385
|
+
gl.TEXTURE_2D,
|
|
386
|
+
0,
|
|
387
|
+
gl.RGBA,
|
|
388
|
+
_MapboxRadarLayer.LUT_SIZE,
|
|
389
|
+
1,
|
|
390
|
+
0,
|
|
391
|
+
gl.RGBA,
|
|
392
|
+
gl.UNSIGNED_BYTE,
|
|
393
|
+
lut
|
|
394
|
+
);
|
|
395
|
+
this.applyLutTextureFilter(gl);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
setOpacity(opacity) {
|
|
399
|
+
const safe = Math.max(0, Math.min(1, Number.isFinite(opacity) ? opacity : 1));
|
|
400
|
+
if (this.opacity === safe) return;
|
|
401
|
+
this.opacity = safe;
|
|
402
|
+
}
|
|
403
|
+
setColormap(colormap) {
|
|
404
|
+
const effective = Array.isArray(colormap) && colormap.length >= 2 ? colormap : getRefc0DefaultColormap();
|
|
405
|
+
const effectiveStr = JSON.stringify(effective);
|
|
406
|
+
if (JSON.stringify(this.currentColormap) === effectiveStr) return;
|
|
407
|
+
this.currentColormap = effective;
|
|
408
|
+
const gl = this.gl;
|
|
409
|
+
if (gl && this.lutTexture) {
|
|
410
|
+
const lut = this.buildLutTextureData();
|
|
411
|
+
gl.bindTexture(gl.TEXTURE_2D, this.lutTexture);
|
|
412
|
+
gl.texImage2D(
|
|
413
|
+
gl.TEXTURE_2D,
|
|
414
|
+
0,
|
|
415
|
+
gl.RGBA,
|
|
416
|
+
_MapboxRadarLayer.LUT_SIZE,
|
|
417
|
+
1,
|
|
418
|
+
0,
|
|
419
|
+
gl.RGBA,
|
|
420
|
+
gl.UNSIGNED_BYTE,
|
|
421
|
+
lut
|
|
422
|
+
);
|
|
423
|
+
this.applyLutTextureFilter(gl);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
setGateSmoothing(smooth) {
|
|
427
|
+
if (this.gateSmoothing === smooth) return;
|
|
428
|
+
this.gateSmoothing = smooth;
|
|
429
|
+
}
|
|
430
|
+
setInterpolateColormap(interpolate) {
|
|
431
|
+
if (this.interpolateColormap === interpolate) return;
|
|
432
|
+
this.interpolateColormap = interpolate;
|
|
433
|
+
const gl = this.gl;
|
|
434
|
+
if (gl && this.lutTexture) {
|
|
435
|
+
const lut = this.buildLutTextureData();
|
|
436
|
+
gl.bindTexture(gl.TEXTURE_2D, this.lutTexture);
|
|
437
|
+
gl.texImage2D(
|
|
438
|
+
gl.TEXTURE_2D,
|
|
439
|
+
0,
|
|
440
|
+
gl.RGBA,
|
|
441
|
+
_MapboxRadarLayer.LUT_SIZE,
|
|
442
|
+
1,
|
|
443
|
+
0,
|
|
444
|
+
gl.RGBA,
|
|
445
|
+
gl.UNSIGNED_BYTE,
|
|
446
|
+
lut
|
|
447
|
+
);
|
|
448
|
+
this.applyLutTextureFilter(gl);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
onAdd(map, gl) {
|
|
452
|
+
this.map = map;
|
|
453
|
+
this.gl = gl;
|
|
454
|
+
const hiFloat = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT);
|
|
455
|
+
const fragmentHighFloatSupported = hiFloat != null && hiFloat.precision > 0;
|
|
456
|
+
const { vertex: vertexSource, fragment: fragmentSource } = buildRadarShaders(fragmentHighFloatSupported);
|
|
457
|
+
const vs = gl.createShader(gl.VERTEX_SHADER);
|
|
458
|
+
gl.shaderSource(vs, vertexSource);
|
|
459
|
+
gl.compileShader(vs);
|
|
460
|
+
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
|
|
461
|
+
console.error(`[MapboxRadarLayer] ${this.id} vertex shader error:`, gl.getShaderInfoLog(vs));
|
|
462
|
+
}
|
|
463
|
+
const fs = gl.createShader(gl.FRAGMENT_SHADER);
|
|
464
|
+
gl.shaderSource(fs, fragmentSource);
|
|
465
|
+
gl.compileShader(fs);
|
|
466
|
+
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
|
|
467
|
+
console.error(`[MapboxRadarLayer] ${this.id} fragment shader error:`, gl.getShaderInfoLog(fs));
|
|
468
|
+
}
|
|
469
|
+
this.program = gl.createProgram();
|
|
470
|
+
gl.attachShader(this.program, vs);
|
|
471
|
+
gl.attachShader(this.program, fs);
|
|
472
|
+
gl.linkProgram(this.program);
|
|
473
|
+
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
|
|
474
|
+
console.error(`[MapboxRadarLayer] ${this.id} shader link error:`, gl.getProgramInfoLog(this.program));
|
|
475
|
+
}
|
|
476
|
+
this.uMatrixLocation = gl.getUniformLocation(this.program, "u_matrix");
|
|
477
|
+
this.uGateTextureLocation = gl.getUniformLocation(this.program, "u_gate_texture");
|
|
478
|
+
this.uLutTextureLocation = gl.getUniformLocation(this.program, "u_lut_texture");
|
|
479
|
+
this.uTextureSizeLocation = gl.getUniformLocation(this.program, "u_texture_size");
|
|
480
|
+
this.uValueScaleOffsetLocation = gl.getUniformLocation(this.program, "u_value_scale_offset");
|
|
481
|
+
this.uLutValueRangeLocation = gl.getUniformLocation(this.program, "u_lut_value_range");
|
|
482
|
+
this.uDiscreteIntegerLutLocation = gl.getUniformLocation(this.program, "u_discrete_integer_lut");
|
|
483
|
+
this.uOpacityLocation = gl.getUniformLocation(this.program, "u_opacity");
|
|
484
|
+
this.uGateSmoothPolarLocation = gl.getUniformLocation(this.program, "u_gate_smooth_polar");
|
|
485
|
+
this.aUvLocation = gl.getAttribLocation(this.program, "a_uv");
|
|
486
|
+
this.aPosLocation = gl.getAttribLocation(this.program, "a_pos");
|
|
487
|
+
this.geometryBuffer = gl.createBuffer();
|
|
488
|
+
this.gateTexture = gl.createTexture();
|
|
489
|
+
this.lutTexture = gl.createTexture();
|
|
490
|
+
this.initializeLutTexture(gl);
|
|
491
|
+
if (this.pendingFrame) {
|
|
492
|
+
this.uploadFrame(gl, this.pendingFrame, this.pendingFrameUploadOptions ?? void 0);
|
|
493
|
+
this.pendingFrame = null;
|
|
494
|
+
this.pendingFrameUploadOptions = null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
onRemove(_map, _gl) {
|
|
498
|
+
this.map = null;
|
|
499
|
+
}
|
|
500
|
+
commitRadarGeometry(gl, cacheKey, buffer, anchorMercator) {
|
|
501
|
+
this.anchorMercator = anchorMercator;
|
|
502
|
+
this.geometryCache = {
|
|
503
|
+
key: cacheKey,
|
|
504
|
+
buffer,
|
|
505
|
+
anchor: anchorMercator
|
|
506
|
+
};
|
|
507
|
+
geometryLruPut(cacheKey, {
|
|
508
|
+
buffer,
|
|
509
|
+
anchor: { ...anchorMercator }
|
|
510
|
+
});
|
|
511
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.geometryBuffer);
|
|
512
|
+
gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW);
|
|
513
|
+
}
|
|
514
|
+
uploadGateTextureAndFinishFrame(gl, sortedFrame) {
|
|
515
|
+
if (!this.geometryCache) return;
|
|
516
|
+
this.activeVertCount = this.geometryCache.buffer.length / 4;
|
|
517
|
+
this.hasFrame = this.activeVertCount > 0;
|
|
518
|
+
this.activeTextureWidth = sortedFrame.nGates;
|
|
519
|
+
this.activeTextureHeight = sortedFrame.nRays;
|
|
520
|
+
this.activeValueScale = sortedFrame.valueScale;
|
|
521
|
+
this.activeValueOffset = sortedFrame.valueOffset;
|
|
522
|
+
gl.bindTexture(gl.TEXTURE_2D, this.gateTexture);
|
|
523
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
524
|
+
this.applyGateTextureFilter(gl);
|
|
525
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
526
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
527
|
+
gl.texImage2D(
|
|
528
|
+
gl.TEXTURE_2D,
|
|
529
|
+
0,
|
|
530
|
+
gl.LUMINANCE_ALPHA,
|
|
531
|
+
sortedFrame.nGates,
|
|
532
|
+
sortedFrame.nRays,
|
|
533
|
+
0,
|
|
534
|
+
gl.LUMINANCE_ALPHA,
|
|
535
|
+
gl.UNSIGNED_BYTE,
|
|
536
|
+
sortedFrame.gateData
|
|
537
|
+
);
|
|
538
|
+
this.map?.triggerRepaint();
|
|
539
|
+
}
|
|
540
|
+
preloadFrame(_frame) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
setFrameData(frame, options) {
|
|
544
|
+
const gl = this.gl;
|
|
545
|
+
if (!gl) {
|
|
546
|
+
this.pendingFrame = frame;
|
|
547
|
+
this.pendingFrameUploadOptions = options ?? null;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (options?.geometryCacheKeysRayBoundaries !== true) {
|
|
551
|
+
this.uploadFrame(gl, frame, options);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
this.rafCoalesceFrame = frame;
|
|
555
|
+
this.rafCoalesceOptions = options;
|
|
556
|
+
if (this.pendingSetFrameRaf != null) return;
|
|
557
|
+
this.pendingSetFrameRaf = requestAnimationFrame(() => {
|
|
558
|
+
this.pendingSetFrameRaf = null;
|
|
559
|
+
const gl2 = this.gl;
|
|
560
|
+
const f = this.rafCoalesceFrame;
|
|
561
|
+
const o = this.rafCoalesceOptions;
|
|
562
|
+
this.rafCoalesceFrame = null;
|
|
563
|
+
this.rafCoalesceOptions = void 0;
|
|
564
|
+
if (!gl2 || !f) return;
|
|
565
|
+
this.uploadFrame(gl2, f, o);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
evictAllFrames() {
|
|
569
|
+
if (this.pendingSetFrameRaf != null) {
|
|
570
|
+
cancelAnimationFrame(this.pendingSetFrameRaf);
|
|
571
|
+
this.pendingSetFrameRaf = null;
|
|
572
|
+
}
|
|
573
|
+
this.rafCoalesceFrame = null;
|
|
574
|
+
this.rafCoalesceOptions = void 0;
|
|
575
|
+
const gl = this.gl;
|
|
576
|
+
if (!gl) return;
|
|
577
|
+
if (this.geometryBuffer) {
|
|
578
|
+
gl.deleteBuffer(this.geometryBuffer);
|
|
579
|
+
this.geometryBuffer = null;
|
|
580
|
+
}
|
|
581
|
+
if (this.gateTexture) {
|
|
582
|
+
gl.deleteTexture(this.gateTexture);
|
|
583
|
+
this.gateTexture = null;
|
|
584
|
+
}
|
|
585
|
+
if (this.lutTexture) {
|
|
586
|
+
gl.deleteTexture(this.lutTexture);
|
|
587
|
+
this.lutTexture = null;
|
|
588
|
+
}
|
|
589
|
+
this.pendingFrame = null;
|
|
590
|
+
this.pendingFrameUploadOptions = null;
|
|
591
|
+
this.lastUploadedFrame = null;
|
|
592
|
+
this.activeVertCount = 0;
|
|
593
|
+
this.hasFrame = false;
|
|
594
|
+
this.activeTextureWidth = 0;
|
|
595
|
+
this.activeTextureHeight = 0;
|
|
596
|
+
this.geometryCache = null;
|
|
597
|
+
}
|
|
598
|
+
render(gl, matrix) {
|
|
599
|
+
if (!this.program || !this.geometryBuffer || !this.gateTexture || !this.lutTexture || !this.hasFrame) return;
|
|
600
|
+
if (this.activeVertCount === 0) return;
|
|
601
|
+
if (this.aPosLocation < 0 || this.aUvLocation < 0 || !this.uMatrixLocation) return;
|
|
602
|
+
if (this.uGateTextureLocation === null || this.uLutTextureLocation === null || this.uTextureSizeLocation === null || this.uValueScaleOffsetLocation === null || this.uLutValueRangeLocation === null || this.uDiscreteIntegerLutLocation === null || this.uOpacityLocation === null || this.uGateSmoothPolarLocation === null) return;
|
|
603
|
+
gl.useProgram(this.program);
|
|
604
|
+
const ax = this.anchorMercator.x;
|
|
605
|
+
const ay = this.anchorMercator.y;
|
|
606
|
+
const shiftedMatrix = new Float32Array(16);
|
|
607
|
+
for (let i = 0; i < 16; i++) shiftedMatrix[i] = matrix[i];
|
|
608
|
+
shiftedMatrix[12] = matrix[0] * ax + matrix[4] * ay + matrix[12];
|
|
609
|
+
shiftedMatrix[13] = matrix[1] * ax + matrix[5] * ay + matrix[13];
|
|
610
|
+
shiftedMatrix[14] = matrix[2] * ax + matrix[6] * ay + matrix[14];
|
|
611
|
+
shiftedMatrix[15] = matrix[3] * ax + matrix[7] * ay + matrix[15];
|
|
612
|
+
gl.uniformMatrix4fv(this.uMatrixLocation, false, shiftedMatrix);
|
|
613
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
614
|
+
gl.bindTexture(gl.TEXTURE_2D, this.gateTexture);
|
|
615
|
+
gl.uniform1i(this.uGateTextureLocation, 0);
|
|
616
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
617
|
+
gl.bindTexture(gl.TEXTURE_2D, this.lutTexture);
|
|
618
|
+
gl.uniform1i(this.uLutTextureLocation, 1);
|
|
619
|
+
gl.uniform2f(this.uTextureSizeLocation, this.activeTextureWidth, this.activeTextureHeight);
|
|
620
|
+
gl.uniform2f(this.uValueScaleOffsetLocation, this.activeValueScale, this.activeValueOffset);
|
|
621
|
+
gl.uniform2f(this.uLutValueRangeLocation, this.lutValueMin, this.lutValueMax);
|
|
622
|
+
gl.uniform1f(this.uDiscreteIntegerLutLocation, this.discreteIntegerLutActive ? 1 : 0);
|
|
623
|
+
gl.uniform1f(this.uOpacityLocation, this.opacity);
|
|
624
|
+
gl.uniform1f(this.uGateSmoothPolarLocation, this.gateSmoothing ? 1 : 0);
|
|
625
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.geometryBuffer);
|
|
626
|
+
gl.enableVertexAttribArray(this.aPosLocation);
|
|
627
|
+
gl.vertexAttribPointer(this.aPosLocation, 2, gl.FLOAT, false, 16, 0);
|
|
628
|
+
gl.enableVertexAttribArray(this.aUvLocation);
|
|
629
|
+
gl.vertexAttribPointer(this.aUvLocation, 2, gl.FLOAT, false, 16, 8);
|
|
630
|
+
gl.enable(gl.BLEND);
|
|
631
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
632
|
+
gl.drawArrays(gl.TRIANGLES, 0, this.activeVertCount);
|
|
633
|
+
gl.disableVertexAttribArray(this.aPosLocation);
|
|
634
|
+
gl.disableVertexAttribArray(this.aUvLocation);
|
|
635
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
636
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
637
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
638
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
639
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
640
|
+
}
|
|
641
|
+
uploadFrame(gl, frame, options) {
|
|
642
|
+
if (!this.geometryBuffer || !this.gateTexture) return;
|
|
643
|
+
const sortedFrame = prepareRadarFrameForGpuReadout(frame, options);
|
|
644
|
+
const cacheKey = this.buildGeometryCacheKey(sortedFrame, options);
|
|
645
|
+
let reused = false;
|
|
646
|
+
if (this.geometryCache?.key === cacheKey) {
|
|
647
|
+
this.anchorMercator = this.geometryCache.anchor;
|
|
648
|
+
reused = true;
|
|
649
|
+
}
|
|
650
|
+
if (!reused) {
|
|
651
|
+
const fromLru = geometryLruTouch(cacheKey);
|
|
652
|
+
if (fromLru) {
|
|
653
|
+
this.geometryCache = {
|
|
654
|
+
key: cacheKey,
|
|
655
|
+
buffer: fromLru.buffer,
|
|
656
|
+
anchor: fromLru.anchor
|
|
657
|
+
};
|
|
658
|
+
this.anchorMercator = fromLru.anchor;
|
|
659
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.geometryBuffer);
|
|
660
|
+
gl.bufferData(gl.ARRAY_BUFFER, fromLru.buffer, gl.STATIC_DRAW);
|
|
661
|
+
reused = true;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (reused) {
|
|
665
|
+
this.uploadGateTextureAndFinishFrame(gl, sortedFrame);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const built = buildRadarRayGeometryBuffer({
|
|
669
|
+
nRays: sortedFrame.nRays,
|
|
670
|
+
nGates: sortedFrame.nGates,
|
|
671
|
+
stationLat: sortedFrame.stationLat,
|
|
672
|
+
stationLon: sortedFrame.stationLon,
|
|
673
|
+
firstGateKm: sortedFrame.firstGateKm,
|
|
674
|
+
gateWidthKm: sortedFrame.gateWidthKm,
|
|
675
|
+
rayBoundariesDeg: sortedFrame.rayBoundariesDeg
|
|
676
|
+
});
|
|
677
|
+
this.commitRadarGeometry(gl, cacheKey, built.buffer, built.anchorMercator);
|
|
678
|
+
this.uploadGateTextureAndFinishFrame(gl, sortedFrame);
|
|
679
|
+
}
|
|
680
|
+
initializeLutTexture(gl) {
|
|
681
|
+
if (!this.lutTexture) return;
|
|
682
|
+
const lut = this.buildLutTextureData();
|
|
683
|
+
gl.bindTexture(gl.TEXTURE_2D, this.lutTexture);
|
|
684
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
685
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
686
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
687
|
+
gl.texImage2D(
|
|
688
|
+
gl.TEXTURE_2D,
|
|
689
|
+
0,
|
|
690
|
+
gl.RGBA,
|
|
691
|
+
_MapboxRadarLayer.LUT_SIZE,
|
|
692
|
+
1,
|
|
693
|
+
0,
|
|
694
|
+
gl.RGBA,
|
|
695
|
+
gl.UNSIGNED_BYTE,
|
|
696
|
+
lut
|
|
697
|
+
);
|
|
698
|
+
this.applyLutTextureFilter(gl);
|
|
699
|
+
}
|
|
700
|
+
applyGateTextureFilter(gl) {
|
|
701
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
702
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
703
|
+
}
|
|
704
|
+
applyLutTextureFilter(gl) {
|
|
705
|
+
const f = this.interpolateColormap ? gl.LINEAR : gl.NEAREST;
|
|
706
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, f);
|
|
707
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, f);
|
|
708
|
+
}
|
|
709
|
+
buildLutTextureData() {
|
|
710
|
+
const cm = this.currentColormap ?? getRefc0DefaultColormap();
|
|
711
|
+
const stops = [];
|
|
712
|
+
if (cm && cm.length >= 2) {
|
|
713
|
+
for (let i = 0; i < cm.length; i += 2) {
|
|
714
|
+
const value = Number(cm[i]);
|
|
715
|
+
const hex = String(cm[i + 1] ?? "");
|
|
716
|
+
const m = hex.match(/^#?([a-fA-F0-9]{8}|[a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/);
|
|
717
|
+
let r = 0, g = 0, b = 0, a = 255;
|
|
718
|
+
if (m) {
|
|
719
|
+
const s = m[1];
|
|
720
|
+
if (s.length === 8) {
|
|
721
|
+
r = parseInt(s.slice(0, 2), 16);
|
|
722
|
+
g = parseInt(s.slice(2, 4), 16);
|
|
723
|
+
b = parseInt(s.slice(4, 6), 16);
|
|
724
|
+
a = parseInt(s.slice(6, 8), 16);
|
|
725
|
+
} else if (s.length === 6) {
|
|
726
|
+
r = parseInt(s.slice(0, 2), 16);
|
|
727
|
+
g = parseInt(s.slice(2, 4), 16);
|
|
728
|
+
b = parseInt(s.slice(4, 6), 16);
|
|
729
|
+
} else {
|
|
730
|
+
r = parseInt(s[0] + s[0], 16);
|
|
731
|
+
g = parseInt(s[1] + s[1], 16);
|
|
732
|
+
b = parseInt(s[2] + s[2], 16);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
stops.push({ value, color: [r, g, b, a] });
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (stops.length === 0) {
|
|
739
|
+
this.discreteIntegerLutActive = false;
|
|
740
|
+
return new Uint8Array(_MapboxRadarLayer.LUT_SIZE * 4);
|
|
741
|
+
}
|
|
742
|
+
this.discreteIntegerLutActive = this.shouldUseDiscreteIntegerLut(stops);
|
|
743
|
+
const { interpolateColormap } = this;
|
|
744
|
+
const out = new Uint8Array(_MapboxRadarLayer.LUT_SIZE * 4);
|
|
745
|
+
if (this.discreteIntegerLutActive) {
|
|
746
|
+
const n = stops.length;
|
|
747
|
+
for (let i = 0; i < _MapboxRadarLayer.LUT_SIZE; i++) {
|
|
748
|
+
const u = (i + 0.5) / _MapboxRadarLayer.LUT_SIZE;
|
|
749
|
+
const bin = Math.min(Math.floor(u * n), n - 1);
|
|
750
|
+
const c = stops[bin].color;
|
|
751
|
+
const idx = i * 4;
|
|
752
|
+
out[idx] = c[0];
|
|
753
|
+
out[idx + 1] = c[1];
|
|
754
|
+
out[idx + 2] = c[2];
|
|
755
|
+
out[idx + 3] = c[3];
|
|
756
|
+
}
|
|
757
|
+
return out;
|
|
758
|
+
}
|
|
759
|
+
for (let i = 0; i < _MapboxRadarLayer.LUT_SIZE; i++) {
|
|
760
|
+
const value = this.lutValueMin + i / (_MapboxRadarLayer.LUT_SIZE - 1) * (this.lutValueMax - this.lutValueMin);
|
|
761
|
+
const color = this.sampleStops(stops, value, interpolateColormap);
|
|
762
|
+
const idx = i * 4;
|
|
763
|
+
out[idx] = color[0];
|
|
764
|
+
out[idx + 1] = color[1];
|
|
765
|
+
out[idx + 2] = color[2];
|
|
766
|
+
out[idx + 3] = color[3];
|
|
767
|
+
}
|
|
768
|
+
return out;
|
|
769
|
+
}
|
|
770
|
+
shouldUseDiscreteIntegerLut(stops) {
|
|
771
|
+
if (this.interpolateColormap || stops.length < 2) return false;
|
|
772
|
+
const lo = this.lutValueMin;
|
|
773
|
+
const hi = this.lutValueMax;
|
|
774
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) return false;
|
|
775
|
+
const nExp = Math.round(hi - lo + 1);
|
|
776
|
+
if (stops.length !== nExp) return false;
|
|
777
|
+
if (Math.abs(Math.round(lo) - lo) > 1e-3 || Math.abs(Math.round(hi) - hi) > 1e-3) return false;
|
|
778
|
+
for (let i = 0; i < stops.length; i++) {
|
|
779
|
+
if (Math.abs(stops[i].value - (lo + i)) > 1e-3) return false;
|
|
780
|
+
}
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
sampleStops(stops, value, interpolate) {
|
|
784
|
+
if (value <= stops[0].value) return stops[0].color;
|
|
785
|
+
if (value >= stops[stops.length - 1].value) return stops[stops.length - 1].color;
|
|
786
|
+
for (let i = 1; i < stops.length; i++) {
|
|
787
|
+
if (interpolate) {
|
|
788
|
+
if (value <= stops[i].value) {
|
|
789
|
+
const low = stops[i - 1];
|
|
790
|
+
const high = stops[i];
|
|
791
|
+
const t = (value - low.value) / Math.max(high.value - low.value, 1e-5);
|
|
792
|
+
return [
|
|
793
|
+
Math.round(low.color[0] + (high.color[0] - low.color[0]) * t),
|
|
794
|
+
Math.round(low.color[1] + (high.color[1] - low.color[1]) * t),
|
|
795
|
+
Math.round(low.color[2] + (high.color[2] - low.color[2]) * t),
|
|
796
|
+
Math.round(low.color[3] + (high.color[3] - low.color[3]) * t)
|
|
797
|
+
];
|
|
798
|
+
}
|
|
799
|
+
} else {
|
|
800
|
+
if (value < stops[i].value) {
|
|
801
|
+
return stops[i - 1].color;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return stops[stops.length - 1].color;
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
export {
|
|
809
|
+
MapboxRadarLayer
|
|
810
|
+
};
|