@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.
@@ -1,1388 +1,1388 @@
1
- import proj4 from 'proj4';
2
-
3
- const MERCATOR_SAFE_LIMIT = 89;
4
-
5
- /** Mapbox GL 3+ uses a WebGL2 context; LUMINANCE is not a valid sized internal format in WebGL2. */
6
- function isWebGL2Context(gl) {
7
- return typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext;
8
- }
9
-
10
- function hrdpsObliqueTransform(rotated_lon, rotated_lat) {
11
- const o_lat_p = 53.91148;
12
- const o_lon_p = 245.305142;
13
- const DEG_TO_RAD = Math.PI / 180.0;
14
- const RAD_TO_DEG = 180.0 / Math.PI;
15
- const o_lat_p_rad = o_lat_p * DEG_TO_RAD;
16
- const rot_lon_rad = rotated_lon * DEG_TO_RAD;
17
- const rot_lat_rad = rotated_lat * DEG_TO_RAD;
18
- const sin_rot_lat = Math.sin(rot_lat_rad);
19
- const cos_rot_lat = Math.cos(rot_lat_rad);
20
- const sin_rot_lon = Math.sin(rot_lon_rad);
21
- const cos_rot_lon = Math.cos(rot_lon_rad);
22
- const sin_o_lat_p = Math.sin(o_lat_p_rad);
23
- const cos_o_lat_p = Math.cos(o_lat_p_rad);
24
- const sin_lat = cos_o_lat_p * sin_rot_lat + sin_o_lat_p * cos_rot_lat * cos_rot_lon;
25
- let lat = Math.asin(sin_lat) * RAD_TO_DEG;
26
- const sin_lon_num = cos_rot_lat * sin_rot_lon;
27
- const sin_lon_den = -sin_o_lat_p * sin_rot_lat + cos_o_lat_p * cos_rot_lat * cos_rot_lon;
28
- let lon = Math.atan2(sin_lon_num, sin_lon_den) * RAD_TO_DEG + o_lon_p;
29
- if (lon > 180) lon -= 360;
30
- else if (lon < -180) lon += 360;
31
- return [lon, lat];
32
- }
33
-
34
- export class GridRenderLayer {
35
- /**
36
- * @param {string | { layerId?: string, id?: string }} idOrOpts - Mapbox custom layer id, or `{ layerId }` / `{ id }` (RN / prop passthrough parity).
37
- */
38
- constructor(idOrOpts) {
39
- let id;
40
- if (typeof idOrOpts === 'string') {
41
- id = idOrOpts;
42
- } else if (idOrOpts != null && typeof idOrOpts === 'object') {
43
- id = idOrOpts.layerId ?? idOrOpts.id;
44
- } else {
45
- id = idOrOpts;
46
- }
47
- this.id = id;
48
- this.type = 'custom';
49
- this.renderingMode = '2d';
50
- this.map = null;
51
- this.gl = null;
52
- this.program = null;
53
- this.opacity = 1;
54
- this.dataRange = [0, 1];
55
- this.vertexBuffer = null;
56
- this.indexBuffer = null;
57
- this.indexCount = 0;
58
- this.dataTexture = null;
59
- this.colormapTexture = null;
60
- this.encoding = null;
61
- this.textureWidth = 0;
62
- this.textureHeight = 0;
63
- this.cachedCorners = null;
64
- this.cachedGridDef = null;
65
- this.scaleType = 'linear';
66
- this.currentConversion = {
67
- type: 0
68
- };
69
- this.dataTextureArray = null; // NEW: Will hold all timesteps
70
- this.currentTimestep = 0; // NEW: Which layer to display
71
- this.timestepCount = 0;
72
- this.noSmoothing = false;
73
- this.currentVariable = '';
74
- this.u_is_ptype = null;
75
- this.isMRMS = false;
76
- }
77
-
78
- setIsMRMS(isMRMS) {
79
- this.isMRMS = isMRMS;
80
- }
81
-
82
- setVariable(variable) {
83
- this.currentVariable = variable;
84
- }
85
-
86
- setSmoothing(noSmoothing) {
87
- this.noSmoothing = noSmoothing;
88
-
89
- // Exit if the WebGL context or the data texture aren't available yet.
90
- if (!this.gl || !this.dataTexture) {
91
- return;
92
- }
93
-
94
- const gl = this.gl;
95
- // Determine the correct filtering mode based on the smoothing flag.
96
- const filter = noSmoothing ? gl.NEAREST : gl.LINEAR;
97
-
98
- // Bind the specific texture we want to modify.
99
- gl.bindTexture(gl.TEXTURE_2D, this.dataTexture);
100
-
101
- // Update the filtering parameters for how the texture is rendered when scaled down or up.
102
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
103
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
104
-
105
- // Unbind the texture as a good practice to avoid accidental changes.
106
- gl.bindTexture(gl.TEXTURE_2D, null);
107
-
108
- // The u_no_smoothing uniform is still passed to the shader to control
109
- // the alpha feathering logic at the edges of the data grid.
110
-
111
- // Trigger a map repaint to apply the visual change immediately.
112
- if (this.map) {
113
- this.map.triggerRepaint();
114
- }
115
- }
116
-
117
- /**
118
- * Safely get MercatorCoordinate from lng/lat with proper fallbacks
119
- * @param {number} lon - Longitude
120
- * @param {number} lat - Latitude
121
- * @returns {object} Mercator coordinate with x, y properties
122
- */
123
- getMercatorCoordinate(lon, lat) {
124
- // Try to use the map's transform MercatorCoordinate first
125
- if (this.map && this.map.transform && this.map.transform.MercatorCoordinate) {
126
- try {
127
- return this.map.transform.MercatorCoordinate.fromLngLat({ lon, lat });
128
- } catch (e) {
129
- // fall through to manual projection
130
- }
131
- }
132
-
133
- // Fallback to imported mapboxgl MercatorCoordinate
134
- if (mapboxgl && mapboxgl.MercatorCoordinate) {
135
- try {
136
- return mapboxgl.MercatorCoordinate.fromLngLat([lon, lat]);
137
- } catch (e) {
138
- // fall through to manual projection
139
- }
140
- }
141
-
142
- // Final fallback: manual calculation
143
- // This is the Web Mercator projection formula
144
- const x = (lon + 180) / 360;
145
- const latRad = lat * Math.PI / 180;
146
- const y = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2;
147
-
148
- return { x, y };
149
- }
150
-
151
- onAdd(map, gl) {
152
- this.map = map;
153
- this.gl = gl;
154
-
155
- const vertexSource = `
156
- attribute vec2 a_position;
157
- attribute vec2 a_texCoord;
158
- uniform mat4 u_matrix;
159
- varying vec2 v_texCoord;
160
- void main() {
161
- gl_Position = u_matrix * vec4(a_position, 0.0, 1.0);
162
- v_texCoord = a_texCoord;
163
- }`;
164
-
165
- const fragmentSource = `
166
- precision highp float;
167
- varying vec2 v_texCoord;
168
-
169
- uniform sampler2D u_data_texture;
170
- uniform sampler2D u_colormap_texture;
171
-
172
- uniform vec2 u_texture_size;
173
- uniform bool u_no_smoothing;
174
-
175
- uniform float u_scale;
176
- uniform float u_offset;
177
- uniform float u_missing_quantized;
178
- uniform int u_scale_type;
179
-
180
- uniform float u_opacity;
181
- uniform vec2 u_data_range;
182
- uniform int u_conversion_type;
183
-
184
- uniform bool u_is_ptype;
185
- uniform bool u_is_mrms;
186
-
187
- // --- HELPER FUNCTIONS ---
188
- float dequantize_val(float q_val) {
189
- if (abs(q_val - u_missing_quantized) < 0.5) {
190
- return -9999.0;
191
- }
192
- float intermediate = q_val * u_scale + u_offset;
193
- if (u_scale_type == 1) {
194
- return intermediate * abs(intermediate);
195
- }
196
- return intermediate;
197
- }
198
-
199
- float get_ptype_from_physical(float val) {
200
- if (val < 0.0) return -1.0;
201
-
202
- if (val < 100.0) return 0.0;
203
- if (val < 200.0) return 1.0;
204
- if (val < 300.0) return 2.0;
205
- if (val < 400.0) return 3.0;
206
- return -1.0;
207
- }
208
-
209
- float get_value(vec2 coord) {
210
- float value_0_to_255 = texture2D(u_data_texture, coord).r * 255.0;
211
- float val = value_0_to_255 - 128.0;
212
- if (abs(val - u_missing_quantized) < 0.5) {
213
- return 99999.0;
214
- }
215
- return val;
216
- }
217
-
218
- float convert_units(float raw_value) {
219
- if (u_conversion_type == 1) return raw_value;
220
- if (u_conversion_type == 2) return raw_value * 1.8 + 32.0;
221
- return raw_value;
222
- }
223
-
224
- void main() {
225
- float raw_value;
226
-
227
- if (u_no_smoothing) {
228
- float quantized_value = get_value(v_texCoord);
229
- if (quantized_value >= 99999.0) discard;
230
- raw_value = dequantize_val(quantized_value);
231
- } else {
232
- vec2 texel_size = 1.0 / u_texture_size;
233
- vec2 tex_coord_in_texels = v_texCoord * u_texture_size;
234
- vec2 bl_texel_index = floor(tex_coord_in_texels - 0.5);
235
- vec2 f = fract(tex_coord_in_texels - 0.5);
236
- vec2 v00_coord = (bl_texel_index + vec2(0.5, 0.5)) * texel_size;
237
- vec2 v10_coord = (bl_texel_index + vec2(1.5, 0.5)) * texel_size;
238
- vec2 v01_coord = (bl_texel_index + vec2(0.5, 1.5)) * texel_size;
239
- vec2 v11_coord = (bl_texel_index + vec2(1.5, 1.5)) * texel_size;
240
-
241
- if (u_is_ptype) {
242
- float v00 = dequantize_val(get_value(v00_coord));
243
- float v10 = dequantize_val(get_value(v10_coord));
244
- float v01 = dequantize_val(get_value(v01_coord));
245
- float v11 = dequantize_val(get_value(v11_coord));
246
-
247
- float p00 = get_ptype_from_physical(v00);
248
- float p10 = get_ptype_from_physical(v10);
249
- float p01 = get_ptype_from_physical(v01);
250
- float p11 = get_ptype_from_physical(v11);
251
-
252
- // Count unique valid precipitation types - no array initialization in GLSL ES 1.0
253
- bool has_type_0 = false;
254
- bool has_type_1 = false;
255
- bool has_type_2 = false;
256
- bool has_type_3 = false;
257
- int valid_count = 0;
258
-
259
- if (p00 >= 0.0) {
260
- int type = int(p00);
261
- if (type == 0) has_type_0 = true;
262
- else if (type == 1) has_type_1 = true;
263
- else if (type == 2) has_type_2 = true;
264
- else if (type == 3) has_type_3 = true;
265
- valid_count++;
266
- }
267
- if (p10 >= 0.0) {
268
- int type = int(p10);
269
- if (type == 0) has_type_0 = true;
270
- else if (type == 1) has_type_1 = true;
271
- else if (type == 2) has_type_2 = true;
272
- else if (type == 3) has_type_3 = true;
273
- valid_count++;
274
- }
275
- if (p01 >= 0.0) {
276
- int type = int(p01);
277
- if (type == 0) has_type_0 = true;
278
- else if (type == 1) has_type_1 = true;
279
- else if (type == 2) has_type_2 = true;
280
- else if (type == 3) has_type_3 = true;
281
- valid_count++;
282
- }
283
- if (p11 >= 0.0) {
284
- int type = int(p11);
285
- if (type == 0) has_type_0 = true;
286
- else if (type == 1) has_type_1 = true;
287
- else if (type == 2) has_type_2 = true;
288
- else if (type == 3) has_type_3 = true;
289
- valid_count++;
290
- }
291
-
292
- if (valid_count == 0) discard;
293
-
294
- // Count unique types
295
- int unique_types = 0;
296
- if (has_type_0) unique_types++;
297
- if (has_type_1) unique_types++;
298
- if (has_type_2) unique_types++;
299
- if (has_type_3) unique_types++;
300
-
301
- if (unique_types == 1) {
302
- // ALL SAME TYPE - smooth interpolation
303
- float total_value = 0.0;
304
- float total_weight = 0.0;
305
-
306
- if (p00 >= 0.0) {
307
- float weight = (1.0 - f.x) * (1.0 - f.y);
308
- total_value += v00 * weight;
309
- total_weight += weight;
310
- }
311
- if (p10 >= 0.0) {
312
- float weight = f.x * (1.0 - f.y);
313
- total_value += v10 * weight;
314
- total_weight += weight;
315
- }
316
- if (p01 >= 0.0) {
317
- float weight = (1.0 - f.x) * f.y;
318
- total_value += v01 * weight;
319
- total_weight += weight;
320
- }
321
- if (p11 >= 0.0) {
322
- float weight = f.x * f.y;
323
- total_value += v11 * weight;
324
- total_weight += weight;
325
- }
326
-
327
- if (total_weight <= 0.0) discard;
328
- raw_value = total_value / total_weight;
329
-
330
- } else {
331
- // MULTIPLE TYPES - weighted voting
332
- float type_weight_0 = 0.0;
333
- float type_weight_1 = 0.0;
334
- float type_weight_2 = 0.0;
335
- float type_weight_3 = 0.0;
336
-
337
- if (p00 >= 0.0) {
338
- float w = (1.0 - f.x) * (1.0 - f.y);
339
- int type = int(p00);
340
- if (type == 0) type_weight_0 += w;
341
- else if (type == 1) type_weight_1 += w;
342
- else if (type == 2) type_weight_2 += w;
343
- else if (type == 3) type_weight_3 += w;
344
- }
345
- if (p10 >= 0.0) {
346
- float w = f.x * (1.0 - f.y);
347
- int type = int(p10);
348
- if (type == 0) type_weight_0 += w;
349
- else if (type == 1) type_weight_1 += w;
350
- else if (type == 2) type_weight_2 += w;
351
- else if (type == 3) type_weight_3 += w;
352
- }
353
- if (p01 >= 0.0) {
354
- float w = (1.0 - f.x) * f.y;
355
- int type = int(p01);
356
- if (type == 0) type_weight_0 += w;
357
- else if (type == 1) type_weight_1 += w;
358
- else if (type == 2) type_weight_2 += w;
359
- else if (type == 3) type_weight_3 += w;
360
- }
361
- if (p11 >= 0.0) {
362
- float w = f.x * f.y;
363
- int type = int(p11);
364
- if (type == 0) type_weight_0 += w;
365
- else if (type == 1) type_weight_1 += w;
366
- else if (type == 2) type_weight_2 += w;
367
- else if (type == 3) type_weight_3 += w;
368
- }
369
-
370
- // Find winning type
371
- float max_type_weight = 0.0;
372
- float winning_type = -1.0;
373
- if (type_weight_0 > max_type_weight) { max_type_weight = type_weight_0; winning_type = 0.0; }
374
- if (type_weight_1 > max_type_weight) { max_type_weight = type_weight_1; winning_type = 1.0; }
375
- if (type_weight_2 > max_type_weight) { max_type_weight = type_weight_2; winning_type = 2.0; }
376
- if (type_weight_3 > max_type_weight) { max_type_weight = type_weight_3; winning_type = 3.0; }
377
-
378
- if (winning_type == -1.0) discard;
379
-
380
- // Interpolate only winning type
381
- float total_value = 0.0;
382
- float total_weight = 0.0;
383
-
384
- if (p00 == winning_type) {
385
- float weight = (1.0 - f.x) * (1.0 - f.y);
386
- total_value += v00 * weight;
387
- total_weight += weight;
388
- }
389
- if (p10 == winning_type) {
390
- float weight = f.x * (1.0 - f.y);
391
- total_value += v10 * weight;
392
- total_weight += weight;
393
- }
394
- if (p01 == winning_type) {
395
- float weight = (1.0 - f.x) * f.y;
396
- total_value += v01 * weight;
397
- total_weight += weight;
398
- }
399
- if (p11 == winning_type) {
400
- float weight = f.x * f.y;
401
- total_value += v11 * weight;
402
- total_weight += weight;
403
- }
404
-
405
- if (total_weight <= 0.0) discard;
406
- raw_value = total_value / total_weight;
407
- }
408
- } else {
409
- float v00 = get_value(v00_coord);
410
- float v10 = get_value(v10_coord);
411
- float v01 = get_value(v01_coord);
412
- float v11 = get_value(v11_coord);
413
- float total_value = 0.0;
414
- float total_weight = 0.0;
415
- if (v00 < 99999.0) { float w = (1.0-f.x)*(1.0-f.y); total_value+=v00*w; total_weight+=w; }
416
- if (v10 < 99999.0) { float w = f.x*(1.0-f.y); total_value+=v10*w; total_weight+=w; }
417
- if (v01 < 99999.0) { float w = (1.0-f.x)*f.y; total_value+=v01*w; total_weight+=w; }
418
- if (v11 < 99999.0) { float w = f.x*f.y; total_value+=v11*w; total_weight+=w; }
419
- if (total_weight <= 0.0) discard;
420
- raw_value = dequantize_val(total_value / total_weight);
421
- }
422
- }
423
-
424
- float converted_value = convert_units(raw_value);
425
-
426
- // Compare against u_data_range in the same unit space as the colormap (after unit conversion).
427
- if (converted_value < u_data_range.x) {
428
- discard;
429
- }
430
-
431
- float colormap_coord = clamp((converted_value - u_data_range.x) / (u_data_range.y - u_data_range.x), 0.0, 1.0);
432
-
433
- vec4 color = texture2D(u_colormap_texture, vec2(colormap_coord, 0.5));
434
-
435
- if (color.a < 0.1) {
436
- discard;
437
- }
438
-
439
- float final_alpha = color.a * u_opacity;
440
-
441
- gl_FragColor = vec4(color.rgb * final_alpha, final_alpha);
442
- }`;
443
-
444
- const vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, vertexSource); gl.compileShader(vertexShader);
445
- const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fragmentSource); gl.compileShader(fragmentShader);
446
-
447
- this.program = gl.createProgram(); gl.attachShader(this.program, vertexShader); gl.attachShader(this.program, fragmentShader); gl.linkProgram(this.program);
448
-
449
- this.a_position = gl.getAttribLocation(this.program, "a_position");
450
- this.a_texCoord = gl.getAttribLocation(this.program, "a_texCoord");
451
- this.u_matrix = gl.getUniformLocation(this.program, "u_matrix");
452
- this.u_data_texture = gl.getUniformLocation(this.program, "u_data_texture");
453
- this.u_colormap_texture = gl.getUniformLocation(this.program, "u_colormap_texture");
454
- this.u_opacity = gl.getUniformLocation(this.program, "u_opacity");
455
- this.u_data_range = gl.getUniformLocation(this.program, "u_data_range");
456
- this.u_scale = gl.getUniformLocation(this.program, "u_scale");
457
- this.u_offset = gl.getUniformLocation(this.program, "u_offset");
458
- this.u_missing_quantized = gl.getUniformLocation(this.program, "u_missing_quantized");
459
- this.u_texture_size = gl.getUniformLocation(this.program, "u_texture_size");
460
- this.u_conversion_type = gl.getUniformLocation(this.program, "u_conversion_type");
461
- this.u_scale_type = gl.getUniformLocation(this.program, "u_scale_type");
462
- this.u_no_smoothing = gl.getUniformLocation(this.program, "u_no_smoothing");
463
- this.u_is_ptype = gl.getUniformLocation(this.program, "u_is_ptype");
464
- this.u_is_mrms = gl.getUniformLocation(this.program, "u_is_mrms"); // NEW
465
-
466
- this.vertexBuffer = gl.createBuffer();
467
- this.indexBuffer = gl.createBuffer();
468
- this.dataTexture = gl.createTexture();
469
- this.colormapTexture = gl.createTexture();
470
-
471
- // Restore geometry if it exists from a previous initialization
472
- if (this.cachedCorners && this.cachedGridDef) {
473
- this.updateGeometry(this.cachedCorners, this.cachedGridDef);
474
- }
475
- }
476
-
477
- /**
478
- * Upload quantized grid bytes (Uint8, centered at 128) to the active 2D texture.
479
- * WebGL2: R8 + RED + UNSIGNED_BYTE (LUMINANCE is invalid). WebGL1: LUMINANCE.
480
- */
481
- _uploadQuantizedGridTexture(tex, nx, ny, dataArray) {
482
- const gl = this.gl;
483
- gl.bindTexture(gl.TEXTURE_2D, tex);
484
- gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
485
- if (isWebGL2Context(gl)) {
486
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, nx, ny, 0, gl.RED, gl.UNSIGNED_BYTE, dataArray);
487
- } else {
488
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, nx, ny, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, dataArray);
489
- }
490
- }
491
-
492
- /**
493
- * After updateDataTexture, register the same GPU texture under timeKey so the slider can
494
- * switch back without re-fetching (avoids duplicating VRAM).
495
- */
496
- registerCurrentDataTextureAsPreloaded(timeKey) {
497
- if (!this.gl || !this.dataTexture || this.encoding == null) {
498
- return;
499
- }
500
- if (!this.preloadedTextures) this.preloadedTextures = new Map();
501
- if (this.preloadedTextures.has(timeKey)) return;
502
- this.preloadedTextures.set(timeKey, {
503
- tex: this.dataTexture,
504
- encoding: this.encoding,
505
- nx: this.textureWidth,
506
- ny: this.textureHeight
507
- });
508
- }
509
-
510
- // Call this during preload to store a pre-uploaded GPU texture
511
- storePreloadedTexture(timeKey, data, encoding, nx, ny) {
512
- if (!this.gl) return;
513
- const gl = this.gl;
514
-
515
- const tex = gl.createTexture();
516
- gl.bindTexture(gl.TEXTURE_2D, tex);
517
-
518
- let dataArray = data instanceof Uint8Array ? data
519
- : data.buffer instanceof ArrayBuffer ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
520
- : new Uint8Array(data);
521
-
522
- this._uploadQuantizedGridTexture(tex, nx, ny, dataArray);
523
-
524
- const filter = this.noSmoothing ? gl.NEAREST : gl.LINEAR;
525
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
526
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
527
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
528
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
529
- gl.bindTexture(gl.TEXTURE_2D, null);
530
-
531
- if (!this.preloadedTextures) this.preloadedTextures = new Map();
532
- if (this.preloadedTextures.has(timeKey)) {
533
- const prev = this.preloadedTextures.get(timeKey);
534
- if (prev.tex && prev.tex !== this.dataTexture) {
535
- gl.deleteTexture(prev.tex);
536
- }
537
- }
538
- this.preloadedTextures.set(timeKey, { tex, encoding, nx, ny });
539
- }
540
-
541
- // O(1) frame switch — just swaps which texture is active
542
- switchToPreloadedTexture(timeKey) {
543
- if (!this.preloadedTextures || !this.preloadedTextures.has(timeKey)) return false;
544
- const { tex, encoding, nx, ny } = this.preloadedTextures.get(timeKey);
545
- this.dataTexture = tex;
546
- this.encoding = encoding;
547
- this.scaleType = encoding.scale_type || 'linear';
548
- this.textureWidth = nx;
549
- this.textureHeight = ny;
550
- return true;
551
- }
552
-
553
- updateGeometry(corners, gridDef) {
554
-
555
- // Cache the geometry
556
- this.cachedCorners = corners;
557
- this.cachedGridDef = gridDef;
558
- if (!this.gl || !this.vertexBuffer) return;
559
- const gl = this.gl;
560
-
561
- const subdivisions = 120;
562
-
563
- // Validate input corners
564
- const coordValues = [corners.lon_tl, corners.lat_tl, corners.lon_tr, corners.lat_tr,
565
- corners.lon_bl, corners.lat_bl, corners.lon_br, corners.lat_br];
566
-
567
- if (coordValues.some(coord => !isFinite(coord))) {
568
- return;
569
- }
570
-
571
- const gridType = gridDef.type;
572
-
573
- if (gridType === 'latlon') {
574
- const vertices = [];
575
- const indices = [];
576
-
577
- // Check if this is a flipped grid
578
- const isFlippedGrid = corners.lat_tl < corners.lat_bl;
579
-
580
- const isIconEUType = gridDef.grid_params &&
581
- gridDef.grid_params.lon_first === 336.5 &&
582
- Math.abs(gridDef.grid_params.lat_first) === 29.5;
583
-
584
- const isIconD2Type = gridDef.grid_params &&
585
- gridDef.grid_params.lon_first === 356.06 &&
586
- Math.abs(gridDef.grid_params.lat_first) === 43.18;
587
- // Detect grid type
588
- const isGFSType = gridDef.grid_params &&
589
- gridDef.grid_params.lon_first === 0.0 &&
590
- Math.abs(gridDef.grid_params.lat_first) === 90.0;
591
-
592
- const isECMWFType = gridDef.grid_params &&
593
- gridDef.grid_params.lon_first === 180.0 &&
594
- gridDef.grid_params.lat_first === 90.0;
595
-
596
- const isGEMType = gridDef.grid_params &&
597
- gridDef.grid_params.lon_first === 180.0 &&
598
- gridDef.grid_params.lat_first === -90.0 &&
599
- gridDef.grid_params.lon_last === 179.85;
600
-
601
- // Apply longitude shift for different models
602
- let adjustedCorners = corners;
603
- if (isGFSType) {
604
- adjustedCorners = {
605
- lon_tl: corners.lon_tl - 180,
606
- lat_tl: corners.lat_tl,
607
- lon_tr: corners.lon_tr - 180,
608
- lat_tr: corners.lat_tr,
609
- lon_bl: corners.lon_bl - 180,
610
- lat_bl: corners.lat_bl,
611
- lon_br: corners.lon_br - 180,
612
- lat_br: corners.lat_br
613
- };
614
-
615
- Object.keys(adjustedCorners).forEach(key => {
616
- if (key.startsWith('lon_') && adjustedCorners[key] < -180) {
617
- adjustedCorners[key] += 360;
618
- }
619
- });
620
- } else if (isECMWFType) {
621
- adjustedCorners = {
622
- lon_tl: corners.lon_tl >= 180 ? corners.lon_tl - 360 : corners.lon_tl,
623
- lat_tl: corners.lat_tl,
624
- lon_tr: corners.lon_tr >= 180 ? corners.lon_tr - 360 : corners.lon_tr,
625
- lat_tr: corners.lat_tr,
626
- lon_bl: corners.lon_bl >= 180 ? corners.lon_bl - 360 : corners.lon_bl,
627
- lat_bl: corners.lat_bl,
628
- lon_br: corners.lon_br >= 180 ? corners.lon_br - 360 : corners.lon_br,
629
- lat_br: corners.lat_br
630
- };
631
- } else if (isGEMType) {
632
- adjustedCorners = corners;
633
- }
634
-
635
- const lonSpan = Math.abs(adjustedCorners.lon_tr - adjustedCorners.lon_tl);
636
- const isGlobalGrid = lonSpan >= 359 || isGEMType;
637
-
638
- if (isGlobalGrid) {
639
- const totalWidth = subdivisions * 3;
640
-
641
- for (let row = 0; row <= subdivisions; row++) {
642
- for (let col = 0; col <= totalWidth; col++) {
643
- const tex_u = (col / subdivisions) - 1.0;
644
-
645
- let tex_v;
646
- if (isGEMType) {
647
- // FIX: Invert the V texture coordinate.
648
- // The GEM data starts at the South Pole, so the texture's V coordinate
649
- // needs to be flipped to match the geometry's top-to-bottom generation.
650
- tex_v = 1.0 - (row / subdivisions);
651
- } else {
652
- tex_v = isFlippedGrid ? (1.0 - row / subdivisions) : (row / subdivisions);
653
- }
654
-
655
-
656
- // Map to world longitude coordinates
657
- let worldLonStart, worldLonEnd;
658
-
659
- if (isGEMType) {
660
- // NEW: For GEM, don't shift the longitude range - use it as-is
661
- // GEM goes from 180° to 179.85°, which should map to -180° to 179.85°
662
- worldLonStart = adjustedCorners.lon_tl - 360; // This will be -180
663
- worldLonEnd = adjustedCorners.lon_tr + 360; // This will be ~539.85
664
- } else {
665
- worldLonStart = adjustedCorners.lon_tl - 360;
666
- worldLonEnd = adjustedCorners.lon_tr + 360;
667
- }
668
-
669
- const worldLonRange = worldLonEnd - worldLonStart;
670
- const worldLon = worldLonStart + (col / totalWidth) * worldLonRange;
671
-
672
- const t_lat = row / subdivisions;
673
- const worldLat = adjustedCorners.lat_tl + t_lat * (adjustedCorners.lat_bl - adjustedCorners.lat_tl);
674
-
675
- if (!isFinite(worldLon) || !isFinite(worldLat)) {
676
- continue;
677
- }
678
-
679
- const mercator = this.getMercatorCoordinate(worldLon, worldLat);
680
- if (!isFinite(mercator.x) || !isFinite(mercator.y)) {
681
- continue;
682
- }
683
-
684
- vertices.push(mercator.x, mercator.y, tex_u, tex_v);
685
- }
686
- }
687
-
688
- // Generate indices for the extended grid
689
- const verticesPerRow = totalWidth + 1;
690
- for (let row = 0; row < subdivisions; row++) {
691
- for (let col = 0; col < totalWidth; col++) {
692
- const topLeft = row * verticesPerRow + col;
693
- const topRight = topLeft + 1;
694
- const bottomLeft = (row + 1) * verticesPerRow + col;
695
- const bottomRight = bottomLeft + 1;
696
-
697
- // Make sure all vertices exist
698
- if (bottomRight < vertices.length / 4) {
699
- indices.push(topLeft, bottomLeft, topRight);
700
- indices.push(topRight, bottomLeft, bottomRight);
701
- }
702
- }
703
- }
704
-
705
- } else {
706
- // Regional grid handling
707
- const adjustedCoordValues = [adjustedCorners.lon_tl, adjustedCorners.lat_tl,
708
- adjustedCorners.lon_tr, adjustedCorners.lat_tr,
709
- adjustedCorners.lon_bl, adjustedCorners.lat_bl,
710
- adjustedCorners.lon_br, adjustedCorners.lat_br];
711
-
712
- if (adjustedCoordValues.some(coord => !isFinite(coord))) {
713
- return;
714
- }
715
-
716
- // Additional validation: check if latitudes are in valid range
717
- const latValues = [adjustedCorners.lat_tl, adjustedCorners.lat_tr, adjustedCorners.lat_bl, adjustedCorners.lat_br];
718
- if (latValues.some(lat => lat < -90 || lat > 90)) {
719
- return;
720
- }
721
-
722
- // Calculate longitude span to handle wrapping
723
- const lonSpan = Math.abs(adjustedCorners.lon_tr - adjustedCorners.lon_tl);
724
- const crossesDateline = lonSpan > 180;
725
-
726
- // NEW: Track texture coordinate ranges
727
- let minTexU = Infinity, maxTexU = -Infinity;
728
- let minTexV = Infinity, maxTexV = -Infinity;
729
-
730
- for (let row = 0; row <= subdivisions; row++) {
731
- for (let col = 0; col <= subdivisions; col++) {
732
- const t_x = col / subdivisions;
733
- const t_y = row / subdivisions;
734
-
735
- // UPDATED: Texture coordinate calculation with ICON model fix
736
- let tex_u = t_x;
737
- let tex_v;
738
-
739
- if (isFlippedGrid && !isIconD2Type && !isIconEUType) {
740
- tex_v = (1.0 - t_y);
741
- } else {
742
- tex_v = t_y;
743
- }
744
-
745
- // NEW: Track ranges
746
- minTexU = Math.min(minTexU, tex_u);
747
- maxTexU = Math.max(maxTexU, tex_u);
748
- minTexV = Math.min(minTexV, tex_v);
749
- maxTexV = Math.max(maxTexV, tex_v);
750
-
751
- // Calculate interpolated coordinates
752
- let lon, lat;
753
-
754
- if (crossesDateline) {
755
- // Handle dateline crossing with proper interpolation
756
- let lon_tl = adjustedCorners.lon_tl;
757
- let lon_tr = adjustedCorners.lon_tr;
758
- let lon_bl = adjustedCorners.lon_bl;
759
- let lon_br = adjustedCorners.lon_br;
760
-
761
- // Adjust for dateline crossing
762
- if (lon_tr < lon_tl) lon_tr += 360;
763
- if (lon_br < lon_bl) lon_br += 360;
764
-
765
- // Bilinear interpolation
766
- lon = (1 - t_y) * ((1 - t_x) * lon_tl + t_x * lon_tr) +
767
- t_y * ((1 - t_x) * lon_bl + t_x * lon_br);
768
-
769
- // Normalize back to [-180, 180]
770
- while (lon > 180) lon -= 360;
771
- while (lon < -180) lon += 360;
772
- } else {
773
- // Standard bilinear interpolation
774
- lon = (1 - t_y) * ((1 - t_x) * adjustedCorners.lon_tl + t_x * adjustedCorners.lon_tr) +
775
- t_y * ((1 - t_x) * adjustedCorners.lon_bl + t_x * adjustedCorners.lon_br);
776
- }
777
-
778
- lat = (1 - t_y) * ((1 - t_x) * adjustedCorners.lat_tl + t_x * adjustedCorners.lat_tr) +
779
- t_y * ((1 - t_x) * adjustedCorners.lat_bl + t_x * adjustedCorners.lat_br);
780
-
781
- lat = Math.max(-MERCATOR_SAFE_LIMIT, Math.min(MERCATOR_SAFE_LIMIT, lat));
782
-
783
- // Validate coordinates
784
- if (!isFinite(lon) || !isFinite(lat)) {
785
- continue;
786
- }
787
-
788
- // Normalize longitude to [-180, 180] range
789
- let normalizedLon = lon;
790
- while (normalizedLon > 180) normalizedLon -= 360;
791
- while (normalizedLon < -180) normalizedLon += 360;
792
-
793
- const mercator = this.getMercatorCoordinate(normalizedLon, lat);
794
-
795
- if (!isFinite(mercator.x) || !isFinite(mercator.y)) {
796
- continue;
797
- }
798
-
799
- vertices.push(mercator.x, mercator.y, tex_u, tex_v);
800
- }
801
- }
802
-
803
- // Generate indices (unchanged)
804
- for (let row = 0; row < subdivisions; row++) {
805
- for (let col = 0; col < subdivisions; col++) {
806
- const topLeft = row * (subdivisions + 1) + col;
807
- const topRight = topLeft + 1;
808
- const bottomLeft = (row + 1) * (subdivisions + 1) + col;
809
- const bottomRight = bottomLeft + 1;
810
-
811
- indices.push(topLeft, bottomLeft, topRight);
812
- indices.push(topRight, bottomLeft, bottomRight);
813
- }
814
- }
815
- }
816
-
817
- const vertexData = new Float32Array(vertices);
818
- gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
819
- gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
820
-
821
- if (!this.indexBuffer) {
822
- this.indexBuffer = gl.createBuffer();
823
- }
824
-
825
- const indexData = new Uint16Array(indices);
826
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
827
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
828
-
829
- this.indexCount = indices.length;
830
- this.vertexCount = vertices.length / 4;
831
- } else if (gridType === 'rotated_latlon') {
832
- const projParams = gridDef.proj_params;
833
- const vertices = [];
834
- const indices = [];
835
-
836
- // Get grid bounds in rotated coordinates
837
- const { lon_first, lat_first, dx_degrees, dy_degrees, nx, ny } = gridDef.grid_params;
838
- const rot_lon_min = lon_first;
839
- const rot_lat_max = lat_first;
840
- const rot_lon_max = lon_first + (nx - 1) * dx_degrees;
841
- const rot_lat_min = lat_first + (ny - 1) * dy_degrees;
842
-
843
-
844
- // Test corner transformations to verify positioning
845
- const corners = [
846
- [rot_lon_min, rot_lat_max], // Top-left
847
- [rot_lon_max, rot_lat_max], // Top-right
848
- [rot_lon_min, rot_lat_min], // Bottom-left
849
- [rot_lon_max, rot_lat_min] // Bottom-right
850
- ];
851
-
852
- // Use manual oblique transformation for HRDPS
853
- for (let row = 0; row <= subdivisions; row++) {
854
- for (let col = 0; col <= subdivisions; col++) {
855
- const t_x = col / subdivisions;
856
- const t_y = row / subdivisions;
857
-
858
- // Interpolate in rotated coordinate space
859
- const rot_lon = rot_lon_min + t_x * (rot_lon_max - rot_lon_min);
860
- const rot_lat = rot_lat_max + t_y * (rot_lat_min - rot_lat_max);
861
-
862
- try {
863
- // Use manual oblique transformation
864
- const [lon, lat] = hrdpsObliqueTransform(rot_lon, rot_lat);
865
-
866
- if (!isFinite(lon) || !isFinite(lat)) {
867
- continue;
868
- }
869
-
870
- const clampedLat = Math.max(-MERCATOR_SAFE_LIMIT, Math.min(MERCATOR_SAFE_LIMIT, lat));
871
-
872
- const mercator = this.getMercatorCoordinate(lon, clampedLat);
873
-
874
- if (!isFinite(mercator.x) || !isFinite(mercator.y)) {
875
- continue;
876
- }
877
-
878
- // Texture coordinates - corrected for HRDPS orientation
879
- const tex_u = t_x;
880
- const tex_v = 1.0 - t_y; // Flipped for correct orientation
881
-
882
- vertices.push(mercator.x, mercator.y, tex_u, tex_v);
883
-
884
- } catch (error) {
885
- continue;
886
- }
887
- }
888
- }
889
-
890
- // Generate indices (same for both HRDPS and other rotated models)
891
- for (let row = 0; row < subdivisions; row++) {
892
- for (let col = 0; col < subdivisions; col++) {
893
- const topLeft = row * (subdivisions + 1) + col;
894
- const topRight = topLeft + 1;
895
- const bottomLeft = (row + 1) * (subdivisions + 1) + col;
896
- const bottomRight = bottomLeft + 1;
897
-
898
- // Only add triangles if we have enough vertices
899
- if (bottomRight < vertices.length / 4) {
900
- indices.push(topLeft, bottomLeft, topRight);
901
- indices.push(topRight, bottomLeft, bottomRight);
902
- }
903
- }
904
- }
905
-
906
- const vertexData = new Float32Array(vertices);
907
- gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
908
- gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
909
-
910
- if (!this.indexBuffer) {
911
- this.indexBuffer = gl.createBuffer();
912
- }
913
-
914
- const indexData = new Uint16Array(indices);
915
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
916
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
917
-
918
- this.indexCount = indices.length;
919
- this.vertexCount = vertices.length / 4;
920
-
921
- } else if (gridType === 'polar_stereographic') {
922
- const projParams = gridDef.proj_params;
923
- let projectionString = `+proj=${projParams.proj}`;
924
- Object.keys(projParams).forEach(key => {
925
- if (key !== 'proj') {
926
- projectionString += ` +${key}=${projParams[key]}`;
927
- }
928
- });
929
- projectionString += ' +lat_0=90 +no_defs';
930
-
931
- const wgs84 = 'EPSG:4326';
932
-
933
- const { nx, ny, dx, dy, x_origin, y_origin } = gridDef.grid_params;
934
-
935
- const x_min = x_origin;
936
- const x_max = x_origin + (nx - 1) * dx;
937
- const y_max = y_origin;
938
- const y_min = y_origin + (ny - 1) * dy;
939
-
940
- const vertices = [];
941
- const indices = [];
942
- const vertexGrid = [];
943
-
944
- let validVertexCount = 0;
945
-
946
- // *** FIX: Declare lastLon outside the loops for continuous unwrapping ***
947
- let lastLon = NaN;
948
-
949
- // First pass: generate valid vertices
950
- for (let row = 0; row <= subdivisions; row++) {
951
- vertexGrid[row] = [];
952
- // The line "let lastLon = NaN;" has been removed from here.
953
-
954
- for (let col = 0; col <= subdivisions; col++) {
955
- const t_x = col / subdivisions;
956
- const t_y = row / subdivisions;
957
-
958
- const proj_x = x_min + t_x * (x_max - x_min);
959
- const proj_y = y_max + t_y * (y_min - y_max);
960
-
961
- try {
962
- const [lon_raw, lat] = proj4(projectionString, wgs84, [proj_x, proj_y]);
963
-
964
- if (!isFinite(lon_raw) || !isFinite(lat)) {
965
- vertexGrid[row][col] = null;
966
- // Do not reset lastLon; just skip this point
967
- continue;
968
- }
969
-
970
- if (lon_raw > 0) {
971
- vertexGrid[row][col] = null;
972
- continue;
973
- }
974
-
975
- let lon = lon_raw;
976
-
977
- // Unwrap longitude to create a continuous mesh.
978
- // This logic now works correctly across rows.
979
- if (!isNaN(lastLon)) {
980
- while (lon - lastLon > 180) { lon -= 360; }
981
- while (lastLon - lon > 180) { lon += 360; }
982
- }
983
- lastLon = lon; // Update lastLon for the next vertex
984
-
985
- const clampedLat = Math.max(-MERCATOR_SAFE_LIMIT, Math.min(MERCATOR_SAFE_LIMIT, lat));
986
-
987
- const mercator = this.getMercatorCoordinate(lon, clampedLat);
988
-
989
- if (!isFinite(mercator.x) || !isFinite(mercator.y)) {
990
- vertexGrid[row][col] = null;
991
- continue;
992
- }
993
-
994
- const tex_u = t_x;
995
- const tex_v = t_y;
996
-
997
- vertexGrid[row][col] = {
998
- mercator_x: mercator.x,
999
- mercator_y: mercator.y,
1000
- tex_u: tex_u,
1001
- tex_v: tex_v,
1002
- vertexIndex: validVertexCount
1003
- };
1004
-
1005
- vertices.push(mercator.x, mercator.y, tex_u, tex_v);
1006
- validVertexCount++;
1007
-
1008
- } catch (error) {
1009
- vertexGrid[row][col] = null;
1010
- continue;
1011
- }
1012
- }
1013
- }
1014
-
1015
- if (vertices.length === 0) {
1016
- return;
1017
- }
1018
-
1019
- // Second pass: generate indices (this logic remains the same)
1020
- for (let row = 0; row < subdivisions; row++) {
1021
- for (let col = 0; col < subdivisions; col++) {
1022
- const topLeft = vertexGrid[row][col];
1023
- const topRight = vertexGrid[row][col + 1];
1024
- const bottomLeft = vertexGrid[row + 1][col];
1025
- const bottomRight = vertexGrid[row + 1][col + 1];
1026
-
1027
- if (topLeft && topRight && bottomLeft && bottomRight) {
1028
- indices.push(
1029
- topLeft.vertexIndex,
1030
- bottomLeft.vertexIndex,
1031
- topRight.vertexIndex
1032
- );
1033
- indices.push(
1034
- topRight.vertexIndex,
1035
- bottomLeft.vertexIndex,
1036
- bottomRight.vertexIndex
1037
- );
1038
- }
1039
- }
1040
- }
1041
-
1042
- // (Final buffer data setup remains the same)
1043
- const vertexData = new Float32Array(vertices);
1044
- gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
1045
- gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
1046
-
1047
- if (!this.indexBuffer) {
1048
- this.indexBuffer = gl.createBuffer();
1049
- }
1050
-
1051
- const indexData = new Uint16Array(indices);
1052
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
1053
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
1054
-
1055
- this.indexCount = indices.length;
1056
- this.vertexCount = vertices.length / 4;
1057
-
1058
- } else if (gridType === 'lambert_conformal_conic') {
1059
- const projParams = gridDef.proj_params;
1060
- let projectionString = `+proj=${projParams.proj}`;
1061
- Object.keys(projParams).forEach(key => {
1062
- if (key !== 'proj') {
1063
- projectionString += ` +${key}=${projParams[key]}`;
1064
- }
1065
- });
1066
- projectionString += ' +no_defs';
1067
-
1068
- const wgs84 = 'EPSG:4326';
1069
-
1070
- const { nx, ny, dx, dy, x_origin, y_origin } = gridDef.grid_params;
1071
-
1072
- // Calculate grid bounds in projected coordinates
1073
- const x_min = x_origin;
1074
- const y_max = y_origin;
1075
- const x_max = x_origin + (nx - 1) * dx;
1076
- const y_min = y_origin + (ny - 1) * dy;
1077
-
1078
- const vertices = [];
1079
- const indices = [];
1080
-
1081
- // Track valid bounds
1082
- let validMinLon = Infinity, validMaxLon = -Infinity;
1083
- let validMinLat = Infinity, validMaxLat = -Infinity;
1084
- let validVertexCount = 0;
1085
-
1086
- // Create a sparse vertex array to handle gaps in valid projections
1087
- const vertexGrid = [];
1088
- const subdivisionsPlusOne = subdivisions + 1;
1089
-
1090
- // Initialize vertex grid
1091
- for (let row = 0; row <= subdivisions; row++) {
1092
- vertexGrid[row] = [];
1093
- }
1094
-
1095
- // First pass: generate valid vertices and store their grid positions
1096
- for (let row = 0; row <= subdivisions; row++) {
1097
- for (let col = 0; col <= subdivisions; col++) {
1098
- const t_x = col / subdivisions;
1099
- const t_y = row / subdivisions;
1100
-
1101
- const proj_x = x_min + t_x * (x_max - x_min);
1102
- const proj_y = y_max + t_y * (y_min - y_max);
1103
-
1104
- try {
1105
- const [lon, lat] = proj4(projectionString, wgs84, [proj_x, proj_y]);
1106
-
1107
- if (lon > 0) {
1108
- vertexGrid[row][col] = null;
1109
- continue;
1110
- }
1111
-
1112
- // Track valid bounds
1113
- validMinLon = Math.min(validMinLon, lon);
1114
- validMaxLon = Math.max(validMaxLon, lon);
1115
- validMinLat = Math.min(validMinLat, lat);
1116
- validMaxLat = Math.max(validMaxLat, lat);
1117
-
1118
- const mercator = this.getMercatorCoordinate(lon, lat);
1119
-
1120
- if (!isFinite(mercator.x) || !isFinite(mercator.y)) {
1121
- vertexGrid[row][col] = null;
1122
- continue;
1123
- }
1124
-
1125
- const tex_u = t_x;
1126
- const tex_v = t_y;
1127
-
1128
- // Store vertex info in grid
1129
- vertexGrid[row][col] = {
1130
- mercator_x: mercator.x,
1131
- mercator_y: mercator.y,
1132
- tex_u: tex_u,
1133
- tex_v: tex_v,
1134
- vertexIndex: validVertexCount
1135
- };
1136
-
1137
- vertices.push(mercator.x, mercator.y, tex_u, tex_v);
1138
- validVertexCount++;
1139
-
1140
- } catch (error) {
1141
- vertexGrid[row][col] = null;
1142
- continue;
1143
- }
1144
- }
1145
- }
1146
-
1147
- if (vertices.length === 0) {
1148
- return;
1149
- }
1150
-
1151
- // Second pass: generate indices only for quads where all 4 vertices are valid
1152
- for (let row = 0; row < subdivisions; row++) {
1153
- for (let col = 0; col < subdivisions; col++) {
1154
- const topLeft = vertexGrid[row][col];
1155
- const topRight = vertexGrid[row][col + 1];
1156
- const bottomLeft = vertexGrid[row + 1][col];
1157
- const bottomRight = vertexGrid[row + 1][col + 1];
1158
-
1159
- // Only create triangles if all 4 vertices are valid
1160
- if (topLeft && topRight && bottomLeft && bottomRight) {
1161
- // Create two triangles for the quad
1162
- indices.push(
1163
- topLeft.vertexIndex,
1164
- bottomLeft.vertexIndex,
1165
- topRight.vertexIndex
1166
- );
1167
- indices.push(
1168
- topRight.vertexIndex,
1169
- bottomLeft.vertexIndex,
1170
- bottomRight.vertexIndex
1171
- );
1172
- }
1173
- }
1174
- }
1175
-
1176
- // Check for potential dateline issues and log them
1177
- if (validMaxLon - validMinLon > 180) {
1178
- }
1179
-
1180
- const vertexData = new Float32Array(vertices);
1181
- gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
1182
- gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
1183
-
1184
- if (!this.indexBuffer) {
1185
- this.indexBuffer = gl.createBuffer();
1186
- }
1187
-
1188
- const indexData = new Uint16Array(indices);
1189
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
1190
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
1191
-
1192
- this.indexCount = indices.length;
1193
- this.vertexCount = vertices.length / 4;
1194
-
1195
- } else {
1196
- return;
1197
- }
1198
- }
1199
-
1200
- updateDataTexture(data, encoding, nx, ny) {
1201
- if (!this.gl || !data) {
1202
- return;
1203
- }
1204
-
1205
- const gl = this.gl;
1206
-
1207
- // Store encoding and dimensions
1208
- this.encoding = encoding;
1209
- this.scaleType = encoding.scale_type || 'linear';
1210
- this.textureWidth = nx;
1211
- this.textureHeight = ny;
1212
-
1213
- // DELETE any existing texture before creating a new one
1214
- if (this.dataTexture) {
1215
- gl.deleteTexture(this.dataTexture);
1216
- this.dataTexture = null;
1217
- }
1218
-
1219
- // Check if data length is 0 or insufficient
1220
- const expectedSize = nx * ny;
1221
-
1222
- if (data.length === 0 || data.length < expectedSize) {
1223
- return;
1224
- }
1225
-
1226
- // Ensure we have a proper Uint8Array
1227
- let dataArray;
1228
- if (data instanceof Uint8Array) {
1229
- dataArray = data;
1230
- } else if (data.buffer instanceof ArrayBuffer) {
1231
- dataArray = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1232
- } else {
1233
- dataArray = new Uint8Array(data);
1234
- }
1235
-
1236
- // Create fresh texture with actual data
1237
- this.dataTexture = gl.createTexture();
1238
- gl.bindTexture(gl.TEXTURE_2D, this.dataTexture);
1239
-
1240
- this._uploadQuantizedGridTexture(this.dataTexture, nx, ny, dataArray);
1241
-
1242
- // Set texture parameters
1243
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1244
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1245
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1246
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1247
- }
1248
-
1249
- updateColormapTexture(colormap) {
1250
- if (!this.gl) return;
1251
- const gl = this.gl;
1252
- const width = 256; const data = new Uint8Array(width * 4);
1253
- const stops = colormap.reduce((acc, _, i) => (i % 2 === 0 ? [...acc, { value: colormap[i], color: colormap[i + 1] }] : acc), []);
1254
- if (stops.length === 0) return;
1255
- const minVal = stops[0].value; const maxVal = stops[stops.length - 1].value;
1256
- const hexToRgb = hex => [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
1257
- for (let i = 0; i < width; i++) {
1258
- const val = minVal + (i / (width - 1)) * (maxVal - minVal);
1259
- let lower = stops[0], upper = stops[stops.length-1];
1260
- for (let j=0; j<stops.length - 1; j++) { if (val >= stops[j].value && val <= stops[j+1].value) { lower=stops[j]; upper=stops[j+1]; break; } }
1261
- const t = (val - lower.value) / (upper.value - lower.value || 1);
1262
- const rgb = hexToRgb(lower.color).map((c, idx) => c * (1 - t) + hexToRgb(upper.color)[idx] * t);
1263
- data.set(rgb, i * 4); data[i * 4 + 3] = 255;
1264
- }
1265
-
1266
- // Delete and recreate to avoid stale texture state
1267
- if (this.colormapTexture) {
1268
- gl.deleteTexture(this.colormapTexture);
1269
- }
1270
- this.colormapTexture = gl.createTexture();
1271
-
1272
- gl.activeTexture(gl.TEXTURE1); // <-- explicitly use TEXTURE1
1273
- gl.bindTexture(gl.TEXTURE_2D, this.colormapTexture);
1274
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
1275
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1276
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1277
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1278
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1279
- gl.bindTexture(gl.TEXTURE_2D, null);
1280
- gl.activeTexture(gl.TEXTURE0); // reset active unit
1281
- }
1282
-
1283
- updateStyle({ opacity, dataRange }) {
1284
- if (opacity !== undefined) this.opacity = opacity;
1285
- if (dataRange !== undefined) this.dataRange = dataRange;
1286
- }
1287
-
1288
- setUnitConversion(fromUnit, targetSystem) {
1289
- let conversionType = 0; // Default: no conversion
1290
-
1291
- // Clean the unit string by removing brackets and extra content
1292
- const cleanUnit = (fromUnit || '')
1293
- .toLowerCase()
1294
- .replace(/\[.*?\]/g, '') // Remove anything in brackets like [QPF]
1295
- .trim();
1296
-
1297
- // Check if it's a temperature unit (contains 'c' or 'f' as standalone or with degree symbol)
1298
- const isTempUnit = /\b[cf]\b|°[cf]|celsius|fahrenheit/.test(cleanUnit);
1299
-
1300
- if (isTempUnit) {
1301
- if (targetSystem === 'metric') conversionType = 1; // to C (no-op)
1302
- else if (targetSystem === 'imperial') conversionType = 2; // to F
1303
- }
1304
-
1305
- this.currentConversion.type = conversionType;
1306
- if (this.map) this.map.triggerRepaint();
1307
- }
1308
-
1309
- render(gl, matrix) {
1310
- if (!this.dataTexture || !this.encoding) {
1311
- return;
1312
- }
1313
- if (!this.colormapTexture) {
1314
- return;
1315
- }
1316
- if (!this.program || !this.vertexBuffer || !this.indexBuffer) {
1317
- return;
1318
- }
1319
- gl.useProgram(this.program);
1320
-
1321
- if (this.u_is_ptype) {
1322
- const isPtype = this.currentVariable === 'ptypeRefl' || this.currentVariable === 'ptypeRate';
1323
- gl.uniform1i(this.u_is_ptype, isPtype ? 1 : 0);
1324
- }
1325
-
1326
- if (this.u_is_mrms) {
1327
- gl.uniform1i(this.u_is_mrms, this.isMRMS ? 1 : 0); // NEW
1328
- }
1329
-
1330
- gl.uniformMatrix4fv(this.u_matrix, false, matrix);
1331
- gl.uniform1f(this.u_opacity, this.opacity);
1332
- gl.uniform2f(this.u_data_range, this.dataRange[0], this.dataRange[1]);
1333
- gl.uniform1f(this.u_scale, this.encoding.scale);
1334
- gl.uniform1f(this.u_offset, this.encoding.offset);
1335
- gl.uniform1f(this.u_missing_quantized, this.encoding.missing_quantized || 127);
1336
- gl.uniform2f(this.u_texture_size, this.textureWidth, this.textureHeight);
1337
- gl.uniform1i(this.u_scale_type, this.scaleType === 'sqrt' ? 1 : 0);
1338
- gl.uniform1i(this.u_conversion_type, this.currentConversion.type);
1339
- gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.dataTexture); gl.uniform1i(this.u_data_texture, 0);
1340
- gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, this.colormapTexture); gl.uniform1i(this.u_colormap_texture, 1);
1341
- gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
1342
- gl.enableVertexAttribArray(this.a_position); gl.vertexAttribPointer(this.a_position, 2, gl.FLOAT, false, 16, 0);
1343
- gl.enableVertexAttribArray(this.a_texCoord); gl.vertexAttribPointer(this.a_texCoord, 2, gl.FLOAT, false, 16, 8);
1344
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
1345
- gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1346
- gl.uniform1i(this.u_no_smoothing, this.noSmoothing);
1347
- gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_SHORT, 0);
1348
- }
1349
-
1350
- onRemove() {
1351
- if (!this.gl) return;
1352
- const gl = this.gl;
1353
-
1354
- if (this.program) {
1355
- gl.deleteProgram(this.program);
1356
- this.program = null;
1357
- }
1358
- if (this.vertexBuffer) {
1359
- gl.deleteBuffer(this.vertexBuffer);
1360
- this.vertexBuffer = null;
1361
- }
1362
- if (this.indexBuffer) {
1363
- gl.deleteBuffer(this.indexBuffer);
1364
- this.indexBuffer = null;
1365
- }
1366
-
1367
- const gridTextures = new Set();
1368
- if (this.dataTexture) gridTextures.add(this.dataTexture);
1369
- if (this.preloadedTextures) {
1370
- for (const { tex } of this.preloadedTextures.values()) {
1371
- if (tex) gridTextures.add(tex);
1372
- }
1373
- this.preloadedTextures.clear();
1374
- }
1375
- for (const tex of gridTextures) {
1376
- gl.deleteTexture(tex);
1377
- }
1378
- this.dataTexture = null;
1379
-
1380
- if (this.colormapTexture) {
1381
- gl.deleteTexture(this.colormapTexture);
1382
- this.colormapTexture = null;
1383
- }
1384
-
1385
- this.map = null;
1386
- this.gl = null;
1387
- }
1
+ import proj4 from 'proj4';
2
+
3
+ const MERCATOR_SAFE_LIMIT = 89;
4
+
5
+ /** Mapbox GL 3+ uses a WebGL2 context; LUMINANCE is not a valid sized internal format in WebGL2. */
6
+ function isWebGL2Context(gl) {
7
+ return typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext;
8
+ }
9
+
10
+ function hrdpsObliqueTransform(rotated_lon, rotated_lat) {
11
+ const o_lat_p = 53.91148;
12
+ const o_lon_p = 245.305142;
13
+ const DEG_TO_RAD = Math.PI / 180.0;
14
+ const RAD_TO_DEG = 180.0 / Math.PI;
15
+ const o_lat_p_rad = o_lat_p * DEG_TO_RAD;
16
+ const rot_lon_rad = rotated_lon * DEG_TO_RAD;
17
+ const rot_lat_rad = rotated_lat * DEG_TO_RAD;
18
+ const sin_rot_lat = Math.sin(rot_lat_rad);
19
+ const cos_rot_lat = Math.cos(rot_lat_rad);
20
+ const sin_rot_lon = Math.sin(rot_lon_rad);
21
+ const cos_rot_lon = Math.cos(rot_lon_rad);
22
+ const sin_o_lat_p = Math.sin(o_lat_p_rad);
23
+ const cos_o_lat_p = Math.cos(o_lat_p_rad);
24
+ const sin_lat = cos_o_lat_p * sin_rot_lat + sin_o_lat_p * cos_rot_lat * cos_rot_lon;
25
+ let lat = Math.asin(sin_lat) * RAD_TO_DEG;
26
+ const sin_lon_num = cos_rot_lat * sin_rot_lon;
27
+ const sin_lon_den = -sin_o_lat_p * sin_rot_lat + cos_o_lat_p * cos_rot_lat * cos_rot_lon;
28
+ let lon = Math.atan2(sin_lon_num, sin_lon_den) * RAD_TO_DEG + o_lon_p;
29
+ if (lon > 180) lon -= 360;
30
+ else if (lon < -180) lon += 360;
31
+ return [lon, lat];
32
+ }
33
+
34
+ export class GridRenderLayer {
35
+ /**
36
+ * @param {string | { layerId?: string, id?: string }} idOrOpts - Mapbox custom layer id, or `{ layerId }` / `{ id }` (RN / prop passthrough parity).
37
+ */
38
+ constructor(idOrOpts) {
39
+ let id;
40
+ if (typeof idOrOpts === 'string') {
41
+ id = idOrOpts;
42
+ } else if (idOrOpts != null && typeof idOrOpts === 'object') {
43
+ id = idOrOpts.layerId ?? idOrOpts.id;
44
+ } else {
45
+ id = idOrOpts;
46
+ }
47
+ this.id = id;
48
+ this.type = 'custom';
49
+ this.renderingMode = '2d';
50
+ this.map = null;
51
+ this.gl = null;
52
+ this.program = null;
53
+ this.opacity = 1;
54
+ this.dataRange = [0, 1];
55
+ this.vertexBuffer = null;
56
+ this.indexBuffer = null;
57
+ this.indexCount = 0;
58
+ this.dataTexture = null;
59
+ this.colormapTexture = null;
60
+ this.encoding = null;
61
+ this.textureWidth = 0;
62
+ this.textureHeight = 0;
63
+ this.cachedCorners = null;
64
+ this.cachedGridDef = null;
65
+ this.scaleType = 'linear';
66
+ this.currentConversion = {
67
+ type: 0
68
+ };
69
+ this.dataTextureArray = null; // NEW: Will hold all timesteps
70
+ this.currentTimestep = 0; // NEW: Which layer to display
71
+ this.timestepCount = 0;
72
+ this.noSmoothing = false;
73
+ this.currentVariable = '';
74
+ this.u_is_ptype = null;
75
+ this.isMRMS = false;
76
+ }
77
+
78
+ setIsMRMS(isMRMS) {
79
+ this.isMRMS = isMRMS;
80
+ }
81
+
82
+ setVariable(variable) {
83
+ this.currentVariable = variable;
84
+ }
85
+
86
+ setSmoothing(noSmoothing) {
87
+ this.noSmoothing = noSmoothing;
88
+
89
+ // Exit if the WebGL context or the data texture aren't available yet.
90
+ if (!this.gl || !this.dataTexture) {
91
+ return;
92
+ }
93
+
94
+ const gl = this.gl;
95
+ // Determine the correct filtering mode based on the smoothing flag.
96
+ const filter = noSmoothing ? gl.NEAREST : gl.LINEAR;
97
+
98
+ // Bind the specific texture we want to modify.
99
+ gl.bindTexture(gl.TEXTURE_2D, this.dataTexture);
100
+
101
+ // Update the filtering parameters for how the texture is rendered when scaled down or up.
102
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
103
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
104
+
105
+ // Unbind the texture as a good practice to avoid accidental changes.
106
+ gl.bindTexture(gl.TEXTURE_2D, null);
107
+
108
+ // The u_no_smoothing uniform is still passed to the shader to control
109
+ // the alpha feathering logic at the edges of the data grid.
110
+
111
+ // Trigger a map repaint to apply the visual change immediately.
112
+ if (this.map) {
113
+ this.map.triggerRepaint();
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Safely get MercatorCoordinate from lng/lat with proper fallbacks
119
+ * @param {number} lon - Longitude
120
+ * @param {number} lat - Latitude
121
+ * @returns {object} Mercator coordinate with x, y properties
122
+ */
123
+ getMercatorCoordinate(lon, lat) {
124
+ // Try to use the map's transform MercatorCoordinate first
125
+ if (this.map && this.map.transform && this.map.transform.MercatorCoordinate) {
126
+ try {
127
+ return this.map.transform.MercatorCoordinate.fromLngLat({ lon, lat });
128
+ } catch (e) {
129
+ // fall through to manual projection
130
+ }
131
+ }
132
+
133
+ // Fallback to imported mapboxgl MercatorCoordinate
134
+ if (mapboxgl && mapboxgl.MercatorCoordinate) {
135
+ try {
136
+ return mapboxgl.MercatorCoordinate.fromLngLat([lon, lat]);
137
+ } catch (e) {
138
+ // fall through to manual projection
139
+ }
140
+ }
141
+
142
+ // Final fallback: manual calculation
143
+ // This is the Web Mercator projection formula
144
+ const x = (lon + 180) / 360;
145
+ const latRad = lat * Math.PI / 180;
146
+ const y = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2;
147
+
148
+ return { x, y };
149
+ }
150
+
151
+ onAdd(map, gl) {
152
+ this.map = map;
153
+ this.gl = gl;
154
+
155
+ const vertexSource = `
156
+ attribute vec2 a_position;
157
+ attribute vec2 a_texCoord;
158
+ uniform mat4 u_matrix;
159
+ varying vec2 v_texCoord;
160
+ void main() {
161
+ gl_Position = u_matrix * vec4(a_position, 0.0, 1.0);
162
+ v_texCoord = a_texCoord;
163
+ }`;
164
+
165
+ const fragmentSource = `
166
+ precision highp float;
167
+ varying vec2 v_texCoord;
168
+
169
+ uniform sampler2D u_data_texture;
170
+ uniform sampler2D u_colormap_texture;
171
+
172
+ uniform vec2 u_texture_size;
173
+ uniform bool u_no_smoothing;
174
+
175
+ uniform float u_scale;
176
+ uniform float u_offset;
177
+ uniform float u_missing_quantized;
178
+ uniform int u_scale_type;
179
+
180
+ uniform float u_opacity;
181
+ uniform vec2 u_data_range;
182
+ uniform int u_conversion_type;
183
+
184
+ uniform bool u_is_ptype;
185
+ uniform bool u_is_mrms;
186
+
187
+ // --- HELPER FUNCTIONS ---
188
+ float dequantize_val(float q_val) {
189
+ if (abs(q_val - u_missing_quantized) < 0.5) {
190
+ return -9999.0;
191
+ }
192
+ float intermediate = q_val * u_scale + u_offset;
193
+ if (u_scale_type == 1) {
194
+ return intermediate * abs(intermediate);
195
+ }
196
+ return intermediate;
197
+ }
198
+
199
+ float get_ptype_from_physical(float val) {
200
+ if (val < 0.0) return -1.0;
201
+
202
+ if (val < 100.0) return 0.0;
203
+ if (val < 200.0) return 1.0;
204
+ if (val < 300.0) return 2.0;
205
+ if (val < 400.0) return 3.0;
206
+ return -1.0;
207
+ }
208
+
209
+ float get_value(vec2 coord) {
210
+ float value_0_to_255 = texture2D(u_data_texture, coord).r * 255.0;
211
+ float val = value_0_to_255 - 128.0;
212
+ if (abs(val - u_missing_quantized) < 0.5) {
213
+ return 99999.0;
214
+ }
215
+ return val;
216
+ }
217
+
218
+ float convert_units(float raw_value) {
219
+ if (u_conversion_type == 1) return raw_value;
220
+ if (u_conversion_type == 2) return raw_value * 1.8 + 32.0;
221
+ return raw_value;
222
+ }
223
+
224
+ void main() {
225
+ float raw_value;
226
+
227
+ if (u_no_smoothing) {
228
+ float quantized_value = get_value(v_texCoord);
229
+ if (quantized_value >= 99999.0) discard;
230
+ raw_value = dequantize_val(quantized_value);
231
+ } else {
232
+ vec2 texel_size = 1.0 / u_texture_size;
233
+ vec2 tex_coord_in_texels = v_texCoord * u_texture_size;
234
+ vec2 bl_texel_index = floor(tex_coord_in_texels - 0.5);
235
+ vec2 f = fract(tex_coord_in_texels - 0.5);
236
+ vec2 v00_coord = (bl_texel_index + vec2(0.5, 0.5)) * texel_size;
237
+ vec2 v10_coord = (bl_texel_index + vec2(1.5, 0.5)) * texel_size;
238
+ vec2 v01_coord = (bl_texel_index + vec2(0.5, 1.5)) * texel_size;
239
+ vec2 v11_coord = (bl_texel_index + vec2(1.5, 1.5)) * texel_size;
240
+
241
+ if (u_is_ptype) {
242
+ float v00 = dequantize_val(get_value(v00_coord));
243
+ float v10 = dequantize_val(get_value(v10_coord));
244
+ float v01 = dequantize_val(get_value(v01_coord));
245
+ float v11 = dequantize_val(get_value(v11_coord));
246
+
247
+ float p00 = get_ptype_from_physical(v00);
248
+ float p10 = get_ptype_from_physical(v10);
249
+ float p01 = get_ptype_from_physical(v01);
250
+ float p11 = get_ptype_from_physical(v11);
251
+
252
+ // Count unique valid precipitation types - no array initialization in GLSL ES 1.0
253
+ bool has_type_0 = false;
254
+ bool has_type_1 = false;
255
+ bool has_type_2 = false;
256
+ bool has_type_3 = false;
257
+ int valid_count = 0;
258
+
259
+ if (p00 >= 0.0) {
260
+ int type = int(p00);
261
+ if (type == 0) has_type_0 = true;
262
+ else if (type == 1) has_type_1 = true;
263
+ else if (type == 2) has_type_2 = true;
264
+ else if (type == 3) has_type_3 = true;
265
+ valid_count++;
266
+ }
267
+ if (p10 >= 0.0) {
268
+ int type = int(p10);
269
+ if (type == 0) has_type_0 = true;
270
+ else if (type == 1) has_type_1 = true;
271
+ else if (type == 2) has_type_2 = true;
272
+ else if (type == 3) has_type_3 = true;
273
+ valid_count++;
274
+ }
275
+ if (p01 >= 0.0) {
276
+ int type = int(p01);
277
+ if (type == 0) has_type_0 = true;
278
+ else if (type == 1) has_type_1 = true;
279
+ else if (type == 2) has_type_2 = true;
280
+ else if (type == 3) has_type_3 = true;
281
+ valid_count++;
282
+ }
283
+ if (p11 >= 0.0) {
284
+ int type = int(p11);
285
+ if (type == 0) has_type_0 = true;
286
+ else if (type == 1) has_type_1 = true;
287
+ else if (type == 2) has_type_2 = true;
288
+ else if (type == 3) has_type_3 = true;
289
+ valid_count++;
290
+ }
291
+
292
+ if (valid_count == 0) discard;
293
+
294
+ // Count unique types
295
+ int unique_types = 0;
296
+ if (has_type_0) unique_types++;
297
+ if (has_type_1) unique_types++;
298
+ if (has_type_2) unique_types++;
299
+ if (has_type_3) unique_types++;
300
+
301
+ if (unique_types == 1) {
302
+ // ALL SAME TYPE - smooth interpolation
303
+ float total_value = 0.0;
304
+ float total_weight = 0.0;
305
+
306
+ if (p00 >= 0.0) {
307
+ float weight = (1.0 - f.x) * (1.0 - f.y);
308
+ total_value += v00 * weight;
309
+ total_weight += weight;
310
+ }
311
+ if (p10 >= 0.0) {
312
+ float weight = f.x * (1.0 - f.y);
313
+ total_value += v10 * weight;
314
+ total_weight += weight;
315
+ }
316
+ if (p01 >= 0.0) {
317
+ float weight = (1.0 - f.x) * f.y;
318
+ total_value += v01 * weight;
319
+ total_weight += weight;
320
+ }
321
+ if (p11 >= 0.0) {
322
+ float weight = f.x * f.y;
323
+ total_value += v11 * weight;
324
+ total_weight += weight;
325
+ }
326
+
327
+ if (total_weight <= 0.0) discard;
328
+ raw_value = total_value / total_weight;
329
+
330
+ } else {
331
+ // MULTIPLE TYPES - weighted voting
332
+ float type_weight_0 = 0.0;
333
+ float type_weight_1 = 0.0;
334
+ float type_weight_2 = 0.0;
335
+ float type_weight_3 = 0.0;
336
+
337
+ if (p00 >= 0.0) {
338
+ float w = (1.0 - f.x) * (1.0 - f.y);
339
+ int type = int(p00);
340
+ if (type == 0) type_weight_0 += w;
341
+ else if (type == 1) type_weight_1 += w;
342
+ else if (type == 2) type_weight_2 += w;
343
+ else if (type == 3) type_weight_3 += w;
344
+ }
345
+ if (p10 >= 0.0) {
346
+ float w = f.x * (1.0 - f.y);
347
+ int type = int(p10);
348
+ if (type == 0) type_weight_0 += w;
349
+ else if (type == 1) type_weight_1 += w;
350
+ else if (type == 2) type_weight_2 += w;
351
+ else if (type == 3) type_weight_3 += w;
352
+ }
353
+ if (p01 >= 0.0) {
354
+ float w = (1.0 - f.x) * f.y;
355
+ int type = int(p01);
356
+ if (type == 0) type_weight_0 += w;
357
+ else if (type == 1) type_weight_1 += w;
358
+ else if (type == 2) type_weight_2 += w;
359
+ else if (type == 3) type_weight_3 += w;
360
+ }
361
+ if (p11 >= 0.0) {
362
+ float w = f.x * f.y;
363
+ int type = int(p11);
364
+ if (type == 0) type_weight_0 += w;
365
+ else if (type == 1) type_weight_1 += w;
366
+ else if (type == 2) type_weight_2 += w;
367
+ else if (type == 3) type_weight_3 += w;
368
+ }
369
+
370
+ // Find winning type
371
+ float max_type_weight = 0.0;
372
+ float winning_type = -1.0;
373
+ if (type_weight_0 > max_type_weight) { max_type_weight = type_weight_0; winning_type = 0.0; }
374
+ if (type_weight_1 > max_type_weight) { max_type_weight = type_weight_1; winning_type = 1.0; }
375
+ if (type_weight_2 > max_type_weight) { max_type_weight = type_weight_2; winning_type = 2.0; }
376
+ if (type_weight_3 > max_type_weight) { max_type_weight = type_weight_3; winning_type = 3.0; }
377
+
378
+ if (winning_type == -1.0) discard;
379
+
380
+ // Interpolate only winning type
381
+ float total_value = 0.0;
382
+ float total_weight = 0.0;
383
+
384
+ if (p00 == winning_type) {
385
+ float weight = (1.0 - f.x) * (1.0 - f.y);
386
+ total_value += v00 * weight;
387
+ total_weight += weight;
388
+ }
389
+ if (p10 == winning_type) {
390
+ float weight = f.x * (1.0 - f.y);
391
+ total_value += v10 * weight;
392
+ total_weight += weight;
393
+ }
394
+ if (p01 == winning_type) {
395
+ float weight = (1.0 - f.x) * f.y;
396
+ total_value += v01 * weight;
397
+ total_weight += weight;
398
+ }
399
+ if (p11 == winning_type) {
400
+ float weight = f.x * f.y;
401
+ total_value += v11 * weight;
402
+ total_weight += weight;
403
+ }
404
+
405
+ if (total_weight <= 0.0) discard;
406
+ raw_value = total_value / total_weight;
407
+ }
408
+ } else {
409
+ float v00 = get_value(v00_coord);
410
+ float v10 = get_value(v10_coord);
411
+ float v01 = get_value(v01_coord);
412
+ float v11 = get_value(v11_coord);
413
+ float total_value = 0.0;
414
+ float total_weight = 0.0;
415
+ if (v00 < 99999.0) { float w = (1.0-f.x)*(1.0-f.y); total_value+=v00*w; total_weight+=w; }
416
+ if (v10 < 99999.0) { float w = f.x*(1.0-f.y); total_value+=v10*w; total_weight+=w; }
417
+ if (v01 < 99999.0) { float w = (1.0-f.x)*f.y; total_value+=v01*w; total_weight+=w; }
418
+ if (v11 < 99999.0) { float w = f.x*f.y; total_value+=v11*w; total_weight+=w; }
419
+ if (total_weight <= 0.0) discard;
420
+ raw_value = dequantize_val(total_value / total_weight);
421
+ }
422
+ }
423
+
424
+ float converted_value = convert_units(raw_value);
425
+
426
+ // Compare against u_data_range in the same unit space as the colormap (after unit conversion).
427
+ if (converted_value < u_data_range.x) {
428
+ discard;
429
+ }
430
+
431
+ float colormap_coord = clamp((converted_value - u_data_range.x) / (u_data_range.y - u_data_range.x), 0.0, 1.0);
432
+
433
+ vec4 color = texture2D(u_colormap_texture, vec2(colormap_coord, 0.5));
434
+
435
+ if (color.a < 0.1) {
436
+ discard;
437
+ }
438
+
439
+ float final_alpha = color.a * u_opacity;
440
+
441
+ gl_FragColor = vec4(color.rgb * final_alpha, final_alpha);
442
+ }`;
443
+
444
+ const vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, vertexSource); gl.compileShader(vertexShader);
445
+ const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fragmentSource); gl.compileShader(fragmentShader);
446
+
447
+ this.program = gl.createProgram(); gl.attachShader(this.program, vertexShader); gl.attachShader(this.program, fragmentShader); gl.linkProgram(this.program);
448
+
449
+ this.a_position = gl.getAttribLocation(this.program, "a_position");
450
+ this.a_texCoord = gl.getAttribLocation(this.program, "a_texCoord");
451
+ this.u_matrix = gl.getUniformLocation(this.program, "u_matrix");
452
+ this.u_data_texture = gl.getUniformLocation(this.program, "u_data_texture");
453
+ this.u_colormap_texture = gl.getUniformLocation(this.program, "u_colormap_texture");
454
+ this.u_opacity = gl.getUniformLocation(this.program, "u_opacity");
455
+ this.u_data_range = gl.getUniformLocation(this.program, "u_data_range");
456
+ this.u_scale = gl.getUniformLocation(this.program, "u_scale");
457
+ this.u_offset = gl.getUniformLocation(this.program, "u_offset");
458
+ this.u_missing_quantized = gl.getUniformLocation(this.program, "u_missing_quantized");
459
+ this.u_texture_size = gl.getUniformLocation(this.program, "u_texture_size");
460
+ this.u_conversion_type = gl.getUniformLocation(this.program, "u_conversion_type");
461
+ this.u_scale_type = gl.getUniformLocation(this.program, "u_scale_type");
462
+ this.u_no_smoothing = gl.getUniformLocation(this.program, "u_no_smoothing");
463
+ this.u_is_ptype = gl.getUniformLocation(this.program, "u_is_ptype");
464
+ this.u_is_mrms = gl.getUniformLocation(this.program, "u_is_mrms"); // NEW
465
+
466
+ this.vertexBuffer = gl.createBuffer();
467
+ this.indexBuffer = gl.createBuffer();
468
+ this.dataTexture = gl.createTexture();
469
+ this.colormapTexture = gl.createTexture();
470
+
471
+ // Restore geometry if it exists from a previous initialization
472
+ if (this.cachedCorners && this.cachedGridDef) {
473
+ this.updateGeometry(this.cachedCorners, this.cachedGridDef);
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Upload quantized grid bytes (Uint8, centered at 128) to the active 2D texture.
479
+ * WebGL2: R8 + RED + UNSIGNED_BYTE (LUMINANCE is invalid). WebGL1: LUMINANCE.
480
+ */
481
+ _uploadQuantizedGridTexture(tex, nx, ny, dataArray) {
482
+ const gl = this.gl;
483
+ gl.bindTexture(gl.TEXTURE_2D, tex);
484
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
485
+ if (isWebGL2Context(gl)) {
486
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, nx, ny, 0, gl.RED, gl.UNSIGNED_BYTE, dataArray);
487
+ } else {
488
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, nx, ny, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, dataArray);
489
+ }
490
+ }
491
+
492
+ /**
493
+ * After updateDataTexture, register the same GPU texture under timeKey so the slider can
494
+ * switch back without re-fetching (avoids duplicating VRAM).
495
+ */
496
+ registerCurrentDataTextureAsPreloaded(timeKey) {
497
+ if (!this.gl || !this.dataTexture || this.encoding == null) {
498
+ return;
499
+ }
500
+ if (!this.preloadedTextures) this.preloadedTextures = new Map();
501
+ if (this.preloadedTextures.has(timeKey)) return;
502
+ this.preloadedTextures.set(timeKey, {
503
+ tex: this.dataTexture,
504
+ encoding: this.encoding,
505
+ nx: this.textureWidth,
506
+ ny: this.textureHeight
507
+ });
508
+ }
509
+
510
+ // Call this during preload to store a pre-uploaded GPU texture
511
+ storePreloadedTexture(timeKey, data, encoding, nx, ny) {
512
+ if (!this.gl) return;
513
+ const gl = this.gl;
514
+
515
+ const tex = gl.createTexture();
516
+ gl.bindTexture(gl.TEXTURE_2D, tex);
517
+
518
+ let dataArray = data instanceof Uint8Array ? data
519
+ : data.buffer instanceof ArrayBuffer ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
520
+ : new Uint8Array(data);
521
+
522
+ this._uploadQuantizedGridTexture(tex, nx, ny, dataArray);
523
+
524
+ const filter = this.noSmoothing ? gl.NEAREST : gl.LINEAR;
525
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
526
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
527
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
528
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
529
+ gl.bindTexture(gl.TEXTURE_2D, null);
530
+
531
+ if (!this.preloadedTextures) this.preloadedTextures = new Map();
532
+ if (this.preloadedTextures.has(timeKey)) {
533
+ const prev = this.preloadedTextures.get(timeKey);
534
+ if (prev.tex && prev.tex !== this.dataTexture) {
535
+ gl.deleteTexture(prev.tex);
536
+ }
537
+ }
538
+ this.preloadedTextures.set(timeKey, { tex, encoding, nx, ny });
539
+ }
540
+
541
+ // O(1) frame switch — just swaps which texture is active
542
+ switchToPreloadedTexture(timeKey) {
543
+ if (!this.preloadedTextures || !this.preloadedTextures.has(timeKey)) return false;
544
+ const { tex, encoding, nx, ny } = this.preloadedTextures.get(timeKey);
545
+ this.dataTexture = tex;
546
+ this.encoding = encoding;
547
+ this.scaleType = encoding.scale_type || 'linear';
548
+ this.textureWidth = nx;
549
+ this.textureHeight = ny;
550
+ return true;
551
+ }
552
+
553
+ updateGeometry(corners, gridDef) {
554
+
555
+ // Cache the geometry
556
+ this.cachedCorners = corners;
557
+ this.cachedGridDef = gridDef;
558
+ if (!this.gl || !this.vertexBuffer) return;
559
+ const gl = this.gl;
560
+
561
+ const subdivisions = 120;
562
+
563
+ // Validate input corners
564
+ const coordValues = [corners.lon_tl, corners.lat_tl, corners.lon_tr, corners.lat_tr,
565
+ corners.lon_bl, corners.lat_bl, corners.lon_br, corners.lat_br];
566
+
567
+ if (coordValues.some(coord => !isFinite(coord))) {
568
+ return;
569
+ }
570
+
571
+ const gridType = gridDef.type;
572
+
573
+ if (gridType === 'latlon') {
574
+ const vertices = [];
575
+ const indices = [];
576
+
577
+ // Check if this is a flipped grid
578
+ const isFlippedGrid = corners.lat_tl < corners.lat_bl;
579
+
580
+ const isIconEUType = gridDef.grid_params &&
581
+ gridDef.grid_params.lon_first === 336.5 &&
582
+ Math.abs(gridDef.grid_params.lat_first) === 29.5;
583
+
584
+ const isIconD2Type = gridDef.grid_params &&
585
+ gridDef.grid_params.lon_first === 356.06 &&
586
+ Math.abs(gridDef.grid_params.lat_first) === 43.18;
587
+ // Detect grid type
588
+ const isGFSType = gridDef.grid_params &&
589
+ gridDef.grid_params.lon_first === 0.0 &&
590
+ Math.abs(gridDef.grid_params.lat_first) === 90.0;
591
+
592
+ const isECMWFType = gridDef.grid_params &&
593
+ gridDef.grid_params.lon_first === 180.0 &&
594
+ gridDef.grid_params.lat_first === 90.0;
595
+
596
+ const isGEMType = gridDef.grid_params &&
597
+ gridDef.grid_params.lon_first === 180.0 &&
598
+ gridDef.grid_params.lat_first === -90.0 &&
599
+ gridDef.grid_params.lon_last === 179.85;
600
+
601
+ // Apply longitude shift for different models
602
+ let adjustedCorners = corners;
603
+ if (isGFSType) {
604
+ adjustedCorners = {
605
+ lon_tl: corners.lon_tl - 180,
606
+ lat_tl: corners.lat_tl,
607
+ lon_tr: corners.lon_tr - 180,
608
+ lat_tr: corners.lat_tr,
609
+ lon_bl: corners.lon_bl - 180,
610
+ lat_bl: corners.lat_bl,
611
+ lon_br: corners.lon_br - 180,
612
+ lat_br: corners.lat_br
613
+ };
614
+
615
+ Object.keys(adjustedCorners).forEach(key => {
616
+ if (key.startsWith('lon_') && adjustedCorners[key] < -180) {
617
+ adjustedCorners[key] += 360;
618
+ }
619
+ });
620
+ } else if (isECMWFType) {
621
+ adjustedCorners = {
622
+ lon_tl: corners.lon_tl >= 180 ? corners.lon_tl - 360 : corners.lon_tl,
623
+ lat_tl: corners.lat_tl,
624
+ lon_tr: corners.lon_tr >= 180 ? corners.lon_tr - 360 : corners.lon_tr,
625
+ lat_tr: corners.lat_tr,
626
+ lon_bl: corners.lon_bl >= 180 ? corners.lon_bl - 360 : corners.lon_bl,
627
+ lat_bl: corners.lat_bl,
628
+ lon_br: corners.lon_br >= 180 ? corners.lon_br - 360 : corners.lon_br,
629
+ lat_br: corners.lat_br
630
+ };
631
+ } else if (isGEMType) {
632
+ adjustedCorners = corners;
633
+ }
634
+
635
+ const lonSpan = Math.abs(adjustedCorners.lon_tr - adjustedCorners.lon_tl);
636
+ const isGlobalGrid = lonSpan >= 359 || isGEMType;
637
+
638
+ if (isGlobalGrid) {
639
+ const totalWidth = subdivisions * 3;
640
+
641
+ for (let row = 0; row <= subdivisions; row++) {
642
+ for (let col = 0; col <= totalWidth; col++) {
643
+ const tex_u = (col / subdivisions) - 1.0;
644
+
645
+ let tex_v;
646
+ if (isGEMType) {
647
+ // FIX: Invert the V texture coordinate.
648
+ // The GEM data starts at the South Pole, so the texture's V coordinate
649
+ // needs to be flipped to match the geometry's top-to-bottom generation.
650
+ tex_v = 1.0 - (row / subdivisions);
651
+ } else {
652
+ tex_v = isFlippedGrid ? (1.0 - row / subdivisions) : (row / subdivisions);
653
+ }
654
+
655
+
656
+ // Map to world longitude coordinates
657
+ let worldLonStart, worldLonEnd;
658
+
659
+ if (isGEMType) {
660
+ // NEW: For GEM, don't shift the longitude range - use it as-is
661
+ // GEM goes from 180° to 179.85°, which should map to -180° to 179.85°
662
+ worldLonStart = adjustedCorners.lon_tl - 360; // This will be -180
663
+ worldLonEnd = adjustedCorners.lon_tr + 360; // This will be ~539.85
664
+ } else {
665
+ worldLonStart = adjustedCorners.lon_tl - 360;
666
+ worldLonEnd = adjustedCorners.lon_tr + 360;
667
+ }
668
+
669
+ const worldLonRange = worldLonEnd - worldLonStart;
670
+ const worldLon = worldLonStart + (col / totalWidth) * worldLonRange;
671
+
672
+ const t_lat = row / subdivisions;
673
+ const worldLat = adjustedCorners.lat_tl + t_lat * (adjustedCorners.lat_bl - adjustedCorners.lat_tl);
674
+
675
+ if (!isFinite(worldLon) || !isFinite(worldLat)) {
676
+ continue;
677
+ }
678
+
679
+ const mercator = this.getMercatorCoordinate(worldLon, worldLat);
680
+ if (!isFinite(mercator.x) || !isFinite(mercator.y)) {
681
+ continue;
682
+ }
683
+
684
+ vertices.push(mercator.x, mercator.y, tex_u, tex_v);
685
+ }
686
+ }
687
+
688
+ // Generate indices for the extended grid
689
+ const verticesPerRow = totalWidth + 1;
690
+ for (let row = 0; row < subdivisions; row++) {
691
+ for (let col = 0; col < totalWidth; col++) {
692
+ const topLeft = row * verticesPerRow + col;
693
+ const topRight = topLeft + 1;
694
+ const bottomLeft = (row + 1) * verticesPerRow + col;
695
+ const bottomRight = bottomLeft + 1;
696
+
697
+ // Make sure all vertices exist
698
+ if (bottomRight < vertices.length / 4) {
699
+ indices.push(topLeft, bottomLeft, topRight);
700
+ indices.push(topRight, bottomLeft, bottomRight);
701
+ }
702
+ }
703
+ }
704
+
705
+ } else {
706
+ // Regional grid handling
707
+ const adjustedCoordValues = [adjustedCorners.lon_tl, adjustedCorners.lat_tl,
708
+ adjustedCorners.lon_tr, adjustedCorners.lat_tr,
709
+ adjustedCorners.lon_bl, adjustedCorners.lat_bl,
710
+ adjustedCorners.lon_br, adjustedCorners.lat_br];
711
+
712
+ if (adjustedCoordValues.some(coord => !isFinite(coord))) {
713
+ return;
714
+ }
715
+
716
+ // Additional validation: check if latitudes are in valid range
717
+ const latValues = [adjustedCorners.lat_tl, adjustedCorners.lat_tr, adjustedCorners.lat_bl, adjustedCorners.lat_br];
718
+ if (latValues.some(lat => lat < -90 || lat > 90)) {
719
+ return;
720
+ }
721
+
722
+ // Calculate longitude span to handle wrapping
723
+ const lonSpan = Math.abs(adjustedCorners.lon_tr - adjustedCorners.lon_tl);
724
+ const crossesDateline = lonSpan > 180;
725
+
726
+ // NEW: Track texture coordinate ranges
727
+ let minTexU = Infinity, maxTexU = -Infinity;
728
+ let minTexV = Infinity, maxTexV = -Infinity;
729
+
730
+ for (let row = 0; row <= subdivisions; row++) {
731
+ for (let col = 0; col <= subdivisions; col++) {
732
+ const t_x = col / subdivisions;
733
+ const t_y = row / subdivisions;
734
+
735
+ // UPDATED: Texture coordinate calculation with ICON model fix
736
+ let tex_u = t_x;
737
+ let tex_v;
738
+
739
+ if (isFlippedGrid && !isIconD2Type && !isIconEUType) {
740
+ tex_v = (1.0 - t_y);
741
+ } else {
742
+ tex_v = t_y;
743
+ }
744
+
745
+ // NEW: Track ranges
746
+ minTexU = Math.min(minTexU, tex_u);
747
+ maxTexU = Math.max(maxTexU, tex_u);
748
+ minTexV = Math.min(minTexV, tex_v);
749
+ maxTexV = Math.max(maxTexV, tex_v);
750
+
751
+ // Calculate interpolated coordinates
752
+ let lon, lat;
753
+
754
+ if (crossesDateline) {
755
+ // Handle dateline crossing with proper interpolation
756
+ let lon_tl = adjustedCorners.lon_tl;
757
+ let lon_tr = adjustedCorners.lon_tr;
758
+ let lon_bl = adjustedCorners.lon_bl;
759
+ let lon_br = adjustedCorners.lon_br;
760
+
761
+ // Adjust for dateline crossing
762
+ if (lon_tr < lon_tl) lon_tr += 360;
763
+ if (lon_br < lon_bl) lon_br += 360;
764
+
765
+ // Bilinear interpolation
766
+ lon = (1 - t_y) * ((1 - t_x) * lon_tl + t_x * lon_tr) +
767
+ t_y * ((1 - t_x) * lon_bl + t_x * lon_br);
768
+
769
+ // Normalize back to [-180, 180]
770
+ while (lon > 180) lon -= 360;
771
+ while (lon < -180) lon += 360;
772
+ } else {
773
+ // Standard bilinear interpolation
774
+ lon = (1 - t_y) * ((1 - t_x) * adjustedCorners.lon_tl + t_x * adjustedCorners.lon_tr) +
775
+ t_y * ((1 - t_x) * adjustedCorners.lon_bl + t_x * adjustedCorners.lon_br);
776
+ }
777
+
778
+ lat = (1 - t_y) * ((1 - t_x) * adjustedCorners.lat_tl + t_x * adjustedCorners.lat_tr) +
779
+ t_y * ((1 - t_x) * adjustedCorners.lat_bl + t_x * adjustedCorners.lat_br);
780
+
781
+ lat = Math.max(-MERCATOR_SAFE_LIMIT, Math.min(MERCATOR_SAFE_LIMIT, lat));
782
+
783
+ // Validate coordinates
784
+ if (!isFinite(lon) || !isFinite(lat)) {
785
+ continue;
786
+ }
787
+
788
+ // Normalize longitude to [-180, 180] range
789
+ let normalizedLon = lon;
790
+ while (normalizedLon > 180) normalizedLon -= 360;
791
+ while (normalizedLon < -180) normalizedLon += 360;
792
+
793
+ const mercator = this.getMercatorCoordinate(normalizedLon, lat);
794
+
795
+ if (!isFinite(mercator.x) || !isFinite(mercator.y)) {
796
+ continue;
797
+ }
798
+
799
+ vertices.push(mercator.x, mercator.y, tex_u, tex_v);
800
+ }
801
+ }
802
+
803
+ // Generate indices (unchanged)
804
+ for (let row = 0; row < subdivisions; row++) {
805
+ for (let col = 0; col < subdivisions; col++) {
806
+ const topLeft = row * (subdivisions + 1) + col;
807
+ const topRight = topLeft + 1;
808
+ const bottomLeft = (row + 1) * (subdivisions + 1) + col;
809
+ const bottomRight = bottomLeft + 1;
810
+
811
+ indices.push(topLeft, bottomLeft, topRight);
812
+ indices.push(topRight, bottomLeft, bottomRight);
813
+ }
814
+ }
815
+ }
816
+
817
+ const vertexData = new Float32Array(vertices);
818
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
819
+ gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
820
+
821
+ if (!this.indexBuffer) {
822
+ this.indexBuffer = gl.createBuffer();
823
+ }
824
+
825
+ const indexData = new Uint16Array(indices);
826
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
827
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
828
+
829
+ this.indexCount = indices.length;
830
+ this.vertexCount = vertices.length / 4;
831
+ } else if (gridType === 'rotated_latlon') {
832
+ const projParams = gridDef.proj_params;
833
+ const vertices = [];
834
+ const indices = [];
835
+
836
+ // Get grid bounds in rotated coordinates
837
+ const { lon_first, lat_first, dx_degrees, dy_degrees, nx, ny } = gridDef.grid_params;
838
+ const rot_lon_min = lon_first;
839
+ const rot_lat_max = lat_first;
840
+ const rot_lon_max = lon_first + (nx - 1) * dx_degrees;
841
+ const rot_lat_min = lat_first + (ny - 1) * dy_degrees;
842
+
843
+
844
+ // Test corner transformations to verify positioning
845
+ const corners = [
846
+ [rot_lon_min, rot_lat_max], // Top-left
847
+ [rot_lon_max, rot_lat_max], // Top-right
848
+ [rot_lon_min, rot_lat_min], // Bottom-left
849
+ [rot_lon_max, rot_lat_min] // Bottom-right
850
+ ];
851
+
852
+ // Use manual oblique transformation for HRDPS
853
+ for (let row = 0; row <= subdivisions; row++) {
854
+ for (let col = 0; col <= subdivisions; col++) {
855
+ const t_x = col / subdivisions;
856
+ const t_y = row / subdivisions;
857
+
858
+ // Interpolate in rotated coordinate space
859
+ const rot_lon = rot_lon_min + t_x * (rot_lon_max - rot_lon_min);
860
+ const rot_lat = rot_lat_max + t_y * (rot_lat_min - rot_lat_max);
861
+
862
+ try {
863
+ // Use manual oblique transformation
864
+ const [lon, lat] = hrdpsObliqueTransform(rot_lon, rot_lat);
865
+
866
+ if (!isFinite(lon) || !isFinite(lat)) {
867
+ continue;
868
+ }
869
+
870
+ const clampedLat = Math.max(-MERCATOR_SAFE_LIMIT, Math.min(MERCATOR_SAFE_LIMIT, lat));
871
+
872
+ const mercator = this.getMercatorCoordinate(lon, clampedLat);
873
+
874
+ if (!isFinite(mercator.x) || !isFinite(mercator.y)) {
875
+ continue;
876
+ }
877
+
878
+ // Texture coordinates - corrected for HRDPS orientation
879
+ const tex_u = t_x;
880
+ const tex_v = 1.0 - t_y; // Flipped for correct orientation
881
+
882
+ vertices.push(mercator.x, mercator.y, tex_u, tex_v);
883
+
884
+ } catch (error) {
885
+ continue;
886
+ }
887
+ }
888
+ }
889
+
890
+ // Generate indices (same for both HRDPS and other rotated models)
891
+ for (let row = 0; row < subdivisions; row++) {
892
+ for (let col = 0; col < subdivisions; col++) {
893
+ const topLeft = row * (subdivisions + 1) + col;
894
+ const topRight = topLeft + 1;
895
+ const bottomLeft = (row + 1) * (subdivisions + 1) + col;
896
+ const bottomRight = bottomLeft + 1;
897
+
898
+ // Only add triangles if we have enough vertices
899
+ if (bottomRight < vertices.length / 4) {
900
+ indices.push(topLeft, bottomLeft, topRight);
901
+ indices.push(topRight, bottomLeft, bottomRight);
902
+ }
903
+ }
904
+ }
905
+
906
+ const vertexData = new Float32Array(vertices);
907
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
908
+ gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
909
+
910
+ if (!this.indexBuffer) {
911
+ this.indexBuffer = gl.createBuffer();
912
+ }
913
+
914
+ const indexData = new Uint16Array(indices);
915
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
916
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
917
+
918
+ this.indexCount = indices.length;
919
+ this.vertexCount = vertices.length / 4;
920
+
921
+ } else if (gridType === 'polar_stereographic') {
922
+ const projParams = gridDef.proj_params;
923
+ let projectionString = `+proj=${projParams.proj}`;
924
+ Object.keys(projParams).forEach(key => {
925
+ if (key !== 'proj') {
926
+ projectionString += ` +${key}=${projParams[key]}`;
927
+ }
928
+ });
929
+ projectionString += ' +lat_0=90 +no_defs';
930
+
931
+ const wgs84 = 'EPSG:4326';
932
+
933
+ const { nx, ny, dx, dy, x_origin, y_origin } = gridDef.grid_params;
934
+
935
+ const x_min = x_origin;
936
+ const x_max = x_origin + (nx - 1) * dx;
937
+ const y_max = y_origin;
938
+ const y_min = y_origin + (ny - 1) * dy;
939
+
940
+ const vertices = [];
941
+ const indices = [];
942
+ const vertexGrid = [];
943
+
944
+ let validVertexCount = 0;
945
+
946
+ // *** FIX: Declare lastLon outside the loops for continuous unwrapping ***
947
+ let lastLon = NaN;
948
+
949
+ // First pass: generate valid vertices
950
+ for (let row = 0; row <= subdivisions; row++) {
951
+ vertexGrid[row] = [];
952
+ // The line "let lastLon = NaN;" has been removed from here.
953
+
954
+ for (let col = 0; col <= subdivisions; col++) {
955
+ const t_x = col / subdivisions;
956
+ const t_y = row / subdivisions;
957
+
958
+ const proj_x = x_min + t_x * (x_max - x_min);
959
+ const proj_y = y_max + t_y * (y_min - y_max);
960
+
961
+ try {
962
+ const [lon_raw, lat] = proj4(projectionString, wgs84, [proj_x, proj_y]);
963
+
964
+ if (!isFinite(lon_raw) || !isFinite(lat)) {
965
+ vertexGrid[row][col] = null;
966
+ // Do not reset lastLon; just skip this point
967
+ continue;
968
+ }
969
+
970
+ if (lon_raw > 0) {
971
+ vertexGrid[row][col] = null;
972
+ continue;
973
+ }
974
+
975
+ let lon = lon_raw;
976
+
977
+ // Unwrap longitude to create a continuous mesh.
978
+ // This logic now works correctly across rows.
979
+ if (!isNaN(lastLon)) {
980
+ while (lon - lastLon > 180) { lon -= 360; }
981
+ while (lastLon - lon > 180) { lon += 360; }
982
+ }
983
+ lastLon = lon; // Update lastLon for the next vertex
984
+
985
+ const clampedLat = Math.max(-MERCATOR_SAFE_LIMIT, Math.min(MERCATOR_SAFE_LIMIT, lat));
986
+
987
+ const mercator = this.getMercatorCoordinate(lon, clampedLat);
988
+
989
+ if (!isFinite(mercator.x) || !isFinite(mercator.y)) {
990
+ vertexGrid[row][col] = null;
991
+ continue;
992
+ }
993
+
994
+ const tex_u = t_x;
995
+ const tex_v = t_y;
996
+
997
+ vertexGrid[row][col] = {
998
+ mercator_x: mercator.x,
999
+ mercator_y: mercator.y,
1000
+ tex_u: tex_u,
1001
+ tex_v: tex_v,
1002
+ vertexIndex: validVertexCount
1003
+ };
1004
+
1005
+ vertices.push(mercator.x, mercator.y, tex_u, tex_v);
1006
+ validVertexCount++;
1007
+
1008
+ } catch (error) {
1009
+ vertexGrid[row][col] = null;
1010
+ continue;
1011
+ }
1012
+ }
1013
+ }
1014
+
1015
+ if (vertices.length === 0) {
1016
+ return;
1017
+ }
1018
+
1019
+ // Second pass: generate indices (this logic remains the same)
1020
+ for (let row = 0; row < subdivisions; row++) {
1021
+ for (let col = 0; col < subdivisions; col++) {
1022
+ const topLeft = vertexGrid[row][col];
1023
+ const topRight = vertexGrid[row][col + 1];
1024
+ const bottomLeft = vertexGrid[row + 1][col];
1025
+ const bottomRight = vertexGrid[row + 1][col + 1];
1026
+
1027
+ if (topLeft && topRight && bottomLeft && bottomRight) {
1028
+ indices.push(
1029
+ topLeft.vertexIndex,
1030
+ bottomLeft.vertexIndex,
1031
+ topRight.vertexIndex
1032
+ );
1033
+ indices.push(
1034
+ topRight.vertexIndex,
1035
+ bottomLeft.vertexIndex,
1036
+ bottomRight.vertexIndex
1037
+ );
1038
+ }
1039
+ }
1040
+ }
1041
+
1042
+ // (Final buffer data setup remains the same)
1043
+ const vertexData = new Float32Array(vertices);
1044
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
1045
+ gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
1046
+
1047
+ if (!this.indexBuffer) {
1048
+ this.indexBuffer = gl.createBuffer();
1049
+ }
1050
+
1051
+ const indexData = new Uint16Array(indices);
1052
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
1053
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
1054
+
1055
+ this.indexCount = indices.length;
1056
+ this.vertexCount = vertices.length / 4;
1057
+
1058
+ } else if (gridType === 'lambert_conformal_conic') {
1059
+ const projParams = gridDef.proj_params;
1060
+ let projectionString = `+proj=${projParams.proj}`;
1061
+ Object.keys(projParams).forEach(key => {
1062
+ if (key !== 'proj') {
1063
+ projectionString += ` +${key}=${projParams[key]}`;
1064
+ }
1065
+ });
1066
+ projectionString += ' +no_defs';
1067
+
1068
+ const wgs84 = 'EPSG:4326';
1069
+
1070
+ const { nx, ny, dx, dy, x_origin, y_origin } = gridDef.grid_params;
1071
+
1072
+ // Calculate grid bounds in projected coordinates
1073
+ const x_min = x_origin;
1074
+ const y_max = y_origin;
1075
+ const x_max = x_origin + (nx - 1) * dx;
1076
+ const y_min = y_origin + (ny - 1) * dy;
1077
+
1078
+ const vertices = [];
1079
+ const indices = [];
1080
+
1081
+ // Track valid bounds
1082
+ let validMinLon = Infinity, validMaxLon = -Infinity;
1083
+ let validMinLat = Infinity, validMaxLat = -Infinity;
1084
+ let validVertexCount = 0;
1085
+
1086
+ // Create a sparse vertex array to handle gaps in valid projections
1087
+ const vertexGrid = [];
1088
+ const subdivisionsPlusOne = subdivisions + 1;
1089
+
1090
+ // Initialize vertex grid
1091
+ for (let row = 0; row <= subdivisions; row++) {
1092
+ vertexGrid[row] = [];
1093
+ }
1094
+
1095
+ // First pass: generate valid vertices and store their grid positions
1096
+ for (let row = 0; row <= subdivisions; row++) {
1097
+ for (let col = 0; col <= subdivisions; col++) {
1098
+ const t_x = col / subdivisions;
1099
+ const t_y = row / subdivisions;
1100
+
1101
+ const proj_x = x_min + t_x * (x_max - x_min);
1102
+ const proj_y = y_max + t_y * (y_min - y_max);
1103
+
1104
+ try {
1105
+ const [lon, lat] = proj4(projectionString, wgs84, [proj_x, proj_y]);
1106
+
1107
+ if (lon > 0) {
1108
+ vertexGrid[row][col] = null;
1109
+ continue;
1110
+ }
1111
+
1112
+ // Track valid bounds
1113
+ validMinLon = Math.min(validMinLon, lon);
1114
+ validMaxLon = Math.max(validMaxLon, lon);
1115
+ validMinLat = Math.min(validMinLat, lat);
1116
+ validMaxLat = Math.max(validMaxLat, lat);
1117
+
1118
+ const mercator = this.getMercatorCoordinate(lon, lat);
1119
+
1120
+ if (!isFinite(mercator.x) || !isFinite(mercator.y)) {
1121
+ vertexGrid[row][col] = null;
1122
+ continue;
1123
+ }
1124
+
1125
+ const tex_u = t_x;
1126
+ const tex_v = t_y;
1127
+
1128
+ // Store vertex info in grid
1129
+ vertexGrid[row][col] = {
1130
+ mercator_x: mercator.x,
1131
+ mercator_y: mercator.y,
1132
+ tex_u: tex_u,
1133
+ tex_v: tex_v,
1134
+ vertexIndex: validVertexCount
1135
+ };
1136
+
1137
+ vertices.push(mercator.x, mercator.y, tex_u, tex_v);
1138
+ validVertexCount++;
1139
+
1140
+ } catch (error) {
1141
+ vertexGrid[row][col] = null;
1142
+ continue;
1143
+ }
1144
+ }
1145
+ }
1146
+
1147
+ if (vertices.length === 0) {
1148
+ return;
1149
+ }
1150
+
1151
+ // Second pass: generate indices only for quads where all 4 vertices are valid
1152
+ for (let row = 0; row < subdivisions; row++) {
1153
+ for (let col = 0; col < subdivisions; col++) {
1154
+ const topLeft = vertexGrid[row][col];
1155
+ const topRight = vertexGrid[row][col + 1];
1156
+ const bottomLeft = vertexGrid[row + 1][col];
1157
+ const bottomRight = vertexGrid[row + 1][col + 1];
1158
+
1159
+ // Only create triangles if all 4 vertices are valid
1160
+ if (topLeft && topRight && bottomLeft && bottomRight) {
1161
+ // Create two triangles for the quad
1162
+ indices.push(
1163
+ topLeft.vertexIndex,
1164
+ bottomLeft.vertexIndex,
1165
+ topRight.vertexIndex
1166
+ );
1167
+ indices.push(
1168
+ topRight.vertexIndex,
1169
+ bottomLeft.vertexIndex,
1170
+ bottomRight.vertexIndex
1171
+ );
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ // Check for potential dateline issues and log them
1177
+ if (validMaxLon - validMinLon > 180) {
1178
+ }
1179
+
1180
+ const vertexData = new Float32Array(vertices);
1181
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
1182
+ gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
1183
+
1184
+ if (!this.indexBuffer) {
1185
+ this.indexBuffer = gl.createBuffer();
1186
+ }
1187
+
1188
+ const indexData = new Uint16Array(indices);
1189
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
1190
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
1191
+
1192
+ this.indexCount = indices.length;
1193
+ this.vertexCount = vertices.length / 4;
1194
+
1195
+ } else {
1196
+ return;
1197
+ }
1198
+ }
1199
+
1200
+ updateDataTexture(data, encoding, nx, ny) {
1201
+ if (!this.gl || !data) {
1202
+ return;
1203
+ }
1204
+
1205
+ const gl = this.gl;
1206
+
1207
+ // Store encoding and dimensions
1208
+ this.encoding = encoding;
1209
+ this.scaleType = encoding.scale_type || 'linear';
1210
+ this.textureWidth = nx;
1211
+ this.textureHeight = ny;
1212
+
1213
+ // DELETE any existing texture before creating a new one
1214
+ if (this.dataTexture) {
1215
+ gl.deleteTexture(this.dataTexture);
1216
+ this.dataTexture = null;
1217
+ }
1218
+
1219
+ // Check if data length is 0 or insufficient
1220
+ const expectedSize = nx * ny;
1221
+
1222
+ if (data.length === 0 || data.length < expectedSize) {
1223
+ return;
1224
+ }
1225
+
1226
+ // Ensure we have a proper Uint8Array
1227
+ let dataArray;
1228
+ if (data instanceof Uint8Array) {
1229
+ dataArray = data;
1230
+ } else if (data.buffer instanceof ArrayBuffer) {
1231
+ dataArray = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1232
+ } else {
1233
+ dataArray = new Uint8Array(data);
1234
+ }
1235
+
1236
+ // Create fresh texture with actual data
1237
+ this.dataTexture = gl.createTexture();
1238
+ gl.bindTexture(gl.TEXTURE_2D, this.dataTexture);
1239
+
1240
+ this._uploadQuantizedGridTexture(this.dataTexture, nx, ny, dataArray);
1241
+
1242
+ // Set texture parameters
1243
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1244
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1245
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1246
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1247
+ }
1248
+
1249
+ updateColormapTexture(colormap) {
1250
+ if (!this.gl) return;
1251
+ const gl = this.gl;
1252
+ const width = 256; const data = new Uint8Array(width * 4);
1253
+ const stops = colormap.reduce((acc, _, i) => (i % 2 === 0 ? [...acc, { value: colormap[i], color: colormap[i + 1] }] : acc), []);
1254
+ if (stops.length === 0) return;
1255
+ const minVal = stops[0].value; const maxVal = stops[stops.length - 1].value;
1256
+ const hexToRgb = hex => [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
1257
+ for (let i = 0; i < width; i++) {
1258
+ const val = minVal + (i / (width - 1)) * (maxVal - minVal);
1259
+ let lower = stops[0], upper = stops[stops.length-1];
1260
+ for (let j=0; j<stops.length - 1; j++) { if (val >= stops[j].value && val <= stops[j+1].value) { lower=stops[j]; upper=stops[j+1]; break; } }
1261
+ const t = (val - lower.value) / (upper.value - lower.value || 1);
1262
+ const rgb = hexToRgb(lower.color).map((c, idx) => c * (1 - t) + hexToRgb(upper.color)[idx] * t);
1263
+ data.set(rgb, i * 4); data[i * 4 + 3] = 255;
1264
+ }
1265
+
1266
+ // Delete and recreate to avoid stale texture state
1267
+ if (this.colormapTexture) {
1268
+ gl.deleteTexture(this.colormapTexture);
1269
+ }
1270
+ this.colormapTexture = gl.createTexture();
1271
+
1272
+ gl.activeTexture(gl.TEXTURE1); // <-- explicitly use TEXTURE1
1273
+ gl.bindTexture(gl.TEXTURE_2D, this.colormapTexture);
1274
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
1275
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1276
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1277
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1278
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1279
+ gl.bindTexture(gl.TEXTURE_2D, null);
1280
+ gl.activeTexture(gl.TEXTURE0); // reset active unit
1281
+ }
1282
+
1283
+ updateStyle({ opacity, dataRange }) {
1284
+ if (opacity !== undefined) this.opacity = opacity;
1285
+ if (dataRange !== undefined) this.dataRange = dataRange;
1286
+ }
1287
+
1288
+ setUnitConversion(fromUnit, targetSystem) {
1289
+ let conversionType = 0; // Default: no conversion
1290
+
1291
+ // Clean the unit string by removing brackets and extra content
1292
+ const cleanUnit = (fromUnit || '')
1293
+ .toLowerCase()
1294
+ .replace(/\[.*?\]/g, '') // Remove anything in brackets like [QPF]
1295
+ .trim();
1296
+
1297
+ // Check if it's a temperature unit (contains 'c' or 'f' as standalone or with degree symbol)
1298
+ const isTempUnit = /\b[cf]\b|°[cf]|celsius|fahrenheit/.test(cleanUnit);
1299
+
1300
+ if (isTempUnit) {
1301
+ if (targetSystem === 'metric') conversionType = 1; // to C (no-op)
1302
+ else if (targetSystem === 'imperial') conversionType = 2; // to F
1303
+ }
1304
+
1305
+ this.currentConversion.type = conversionType;
1306
+ if (this.map) this.map.triggerRepaint();
1307
+ }
1308
+
1309
+ render(gl, matrix) {
1310
+ if (!this.dataTexture || !this.encoding) {
1311
+ return;
1312
+ }
1313
+ if (!this.colormapTexture) {
1314
+ return;
1315
+ }
1316
+ if (!this.program || !this.vertexBuffer || !this.indexBuffer) {
1317
+ return;
1318
+ }
1319
+ gl.useProgram(this.program);
1320
+
1321
+ if (this.u_is_ptype) {
1322
+ const isPtype = this.currentVariable === 'ptypeRefl' || this.currentVariable === 'ptypeRate';
1323
+ gl.uniform1i(this.u_is_ptype, isPtype ? 1 : 0);
1324
+ }
1325
+
1326
+ if (this.u_is_mrms) {
1327
+ gl.uniform1i(this.u_is_mrms, this.isMRMS ? 1 : 0); // NEW
1328
+ }
1329
+
1330
+ gl.uniformMatrix4fv(this.u_matrix, false, matrix);
1331
+ gl.uniform1f(this.u_opacity, this.opacity);
1332
+ gl.uniform2f(this.u_data_range, this.dataRange[0], this.dataRange[1]);
1333
+ gl.uniform1f(this.u_scale, this.encoding.scale);
1334
+ gl.uniform1f(this.u_offset, this.encoding.offset);
1335
+ gl.uniform1f(this.u_missing_quantized, this.encoding.missing_quantized || 127);
1336
+ gl.uniform2f(this.u_texture_size, this.textureWidth, this.textureHeight);
1337
+ gl.uniform1i(this.u_scale_type, this.scaleType === 'sqrt' ? 1 : 0);
1338
+ gl.uniform1i(this.u_conversion_type, this.currentConversion.type);
1339
+ gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.dataTexture); gl.uniform1i(this.u_data_texture, 0);
1340
+ gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, this.colormapTexture); gl.uniform1i(this.u_colormap_texture, 1);
1341
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
1342
+ gl.enableVertexAttribArray(this.a_position); gl.vertexAttribPointer(this.a_position, 2, gl.FLOAT, false, 16, 0);
1343
+ gl.enableVertexAttribArray(this.a_texCoord); gl.vertexAttribPointer(this.a_texCoord, 2, gl.FLOAT, false, 16, 8);
1344
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
1345
+ gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1346
+ gl.uniform1i(this.u_no_smoothing, this.noSmoothing);
1347
+ gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_SHORT, 0);
1348
+ }
1349
+
1350
+ onRemove() {
1351
+ if (!this.gl) return;
1352
+ const gl = this.gl;
1353
+
1354
+ if (this.program) {
1355
+ gl.deleteProgram(this.program);
1356
+ this.program = null;
1357
+ }
1358
+ if (this.vertexBuffer) {
1359
+ gl.deleteBuffer(this.vertexBuffer);
1360
+ this.vertexBuffer = null;
1361
+ }
1362
+ if (this.indexBuffer) {
1363
+ gl.deleteBuffer(this.indexBuffer);
1364
+ this.indexBuffer = null;
1365
+ }
1366
+
1367
+ const gridTextures = new Set();
1368
+ if (this.dataTexture) gridTextures.add(this.dataTexture);
1369
+ if (this.preloadedTextures) {
1370
+ for (const { tex } of this.preloadedTextures.values()) {
1371
+ if (tex) gridTextures.add(tex);
1372
+ }
1373
+ this.preloadedTextures.clear();
1374
+ }
1375
+ for (const tex of gridTextures) {
1376
+ gl.deleteTexture(tex);
1377
+ }
1378
+ this.dataTexture = null;
1379
+
1380
+ if (this.colormapTexture) {
1381
+ gl.deleteTexture(this.colormapTexture);
1382
+ this.colormapTexture = null;
1383
+ }
1384
+
1385
+ this.map = null;
1386
+ this.gl = null;
1387
+ }
1388
1388
  }