@aguacerowx/mapsgl 0.0.57 → 0.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +45 -45
- package/package.json +1 -1
- package/src/GridRenderLayer.js +1387 -1387
- package/src/MapManager.js +197 -197
- package/src/NexradSitesOverlay.js +148 -148
- package/src/NexradWeatherController.js +3 -0
- package/src/SatelliteShaderManager.js +1000 -1000
- package/src/WorkerPool.js +340 -340
- package/src/defaultBasisBaseUrl.js +11 -11
- package/src/nexrad/MapboxRadarLayer.ts +783 -783
- package/src/nexrad/PreprocessedSweepParser.ts +225 -225
- package/src/nexrad/buildRadarRayGeometry.ts +97 -97
- package/src/nexrad/level3StormRelative.ts +116 -116
- package/src/nexrad/loadNexradSites.ts +119 -119
- package/src/nexrad/nexradArchiveDiag.ts +26 -26
- package/src/nexrad/nexradCrossSectionSampleAtLatLon.ts +121 -121
- package/src/nexrad/nexradMapboxFrameOpts.ts +126 -126
- package/src/nexrad/nexradSitesDefault.json +1699 -1699
- package/src/nexrad/radarArchiveCore.bundled.js +10 -0
- package/src/nexrad/radarArchiveCore.ts +11 -0
- package/src/nexrad/radarDecode.worker.ts +25 -25
- package/src/nexrad/radarDecodeSlot.ts +195 -195
- package/src/nexrad/radarFrameGpuMatch.ts +111 -111
- package/src/satelliteDefaultColormaps.js +37 -37
- package/src/satelliteKtxWorker.js +232 -232
- package/src/satelliteShader.js +17 -17
- package/src/style-applicator.js +112 -112
- package/src/style-layer-map.js +26 -26
package/src/GridRenderLayer.js
CHANGED
|
@@ -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
|
}
|