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