@gisatcz/deckgl-geolib 2.3.1-dev.1 → 2.4.0-dev.1
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/README.md +35 -5
- package/dist/cjs/index.js +381 -169
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/index.min.js +2 -2
- package/dist/cjs/index.min.js.map +1 -1
- package/dist/esm/index.js +381 -169
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.min.js +2 -2
- package/dist/esm/index.min.js.map +1 -1
- package/dist/esm/types/core/CogTiles.d.ts +2 -2
- package/dist/esm/types/core/GeoImage.d.ts +2 -0
- package/dist/esm/types/core/lib/KernelGenerator.d.ts +31 -0
- package/dist/esm/types/core/lib/TerrainGenerator.d.ts +6 -1
- package/dist/esm/types/core/types.d.ts +19 -12
- package/dist/esm/types/layers/CogTerrainLayer.d.ts +6 -0
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -4515,37 +4515,46 @@ async function fromArrayBuffer(arrayBuffer, signal) {
|
|
|
4515
4515
|
|
|
4516
4516
|
// types.ts
|
|
4517
4517
|
const DefaultGeoImageOptions = {
|
|
4518
|
+
// --- Shared / Data ---
|
|
4518
4519
|
type: 'image',
|
|
4519
|
-
tesselator: 'martini',
|
|
4520
4520
|
format: undefined,
|
|
4521
|
+
useChannel: null,
|
|
4522
|
+
useChannelIndex: null,
|
|
4523
|
+
noDataValue: undefined,
|
|
4524
|
+
multiplier: 1.0,
|
|
4525
|
+
numOfChannels: undefined,
|
|
4526
|
+
planarConfig: undefined,
|
|
4527
|
+
// --- Mesh generation (terrain only) ---
|
|
4528
|
+
tesselator: 'martini',
|
|
4529
|
+
terrainColor: [133, 133, 133, 255],
|
|
4530
|
+
terrainSkirtHeight: 100,
|
|
4531
|
+
// Default fallback for invalid/nodata elevations. Should be configured based on the dataset's actual range.
|
|
4532
|
+
terrainMinValue: 0,
|
|
4533
|
+
// --- Texture / Visualization ---
|
|
4521
4534
|
useHeatMap: true,
|
|
4522
4535
|
useColorsBasedOnValues: false,
|
|
4536
|
+
useColorClasses: false,
|
|
4523
4537
|
useAutoRange: false,
|
|
4524
4538
|
useDataForOpacity: false,
|
|
4525
4539
|
useSingleColor: false,
|
|
4526
|
-
useColorClasses: false,
|
|
4527
4540
|
blurredTexture: true,
|
|
4528
4541
|
clipLow: null,
|
|
4529
4542
|
clipHigh: null,
|
|
4530
|
-
multiplier: 1.0,
|
|
4531
4543
|
color: [255, 0, 255, 255],
|
|
4532
4544
|
colorScale: chroma.brewer.YlOrRd,
|
|
4533
4545
|
colorScaleValueRange: [0, 255],
|
|
4534
4546
|
colorsBasedOnValues: undefined,
|
|
4535
4547
|
colorClasses: undefined,
|
|
4536
4548
|
alpha: 100,
|
|
4537
|
-
useChannel: null,
|
|
4538
|
-
useChannelIndex: null,
|
|
4539
|
-
noDataValue: undefined,
|
|
4540
|
-
numOfChannels: undefined,
|
|
4541
4549
|
nullColor: [0, 0, 0, 0],
|
|
4542
4550
|
unidentifiedColor: [0, 0, 0, 0],
|
|
4543
4551
|
clippedColor: [0, 0, 0, 0],
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4552
|
+
// --- Kernel-specific (terrain only) ---
|
|
4553
|
+
useSlope: false,
|
|
4554
|
+
useHillshade: false,
|
|
4555
|
+
hillshadeAzimuth: 315,
|
|
4556
|
+
hillshadeAltitude: 45,
|
|
4557
|
+
zFactor: 1,
|
|
4549
4558
|
};
|
|
4550
4559
|
|
|
4551
4560
|
class Martini {
|
|
@@ -5223,158 +5232,6 @@ function updateAttributesForNewEdge({ edge, edgeIndex, attributes, skirtHeight,
|
|
|
5223
5232
|
newTriangles[triangle1Offset + 5] = positionsLength / 3 + vertex1Offset;
|
|
5224
5233
|
}
|
|
5225
5234
|
|
|
5226
|
-
class TerrainGenerator {
|
|
5227
|
-
static generate(input, options, meshMaxError) {
|
|
5228
|
-
const { width, height } = input;
|
|
5229
|
-
// 1. Compute Terrain Data (Extract Elevation)
|
|
5230
|
-
const terrain = this.computeTerrainData(input, options);
|
|
5231
|
-
// 2. Tesselate (Generate Mesh)
|
|
5232
|
-
const { terrainSkirtHeight } = options;
|
|
5233
|
-
let mesh;
|
|
5234
|
-
switch (options.tesselator) {
|
|
5235
|
-
case 'martini':
|
|
5236
|
-
mesh = this.getMartiniTileMesh(meshMaxError, width, terrain);
|
|
5237
|
-
break;
|
|
5238
|
-
case 'delatin':
|
|
5239
|
-
mesh = this.getDelatinTileMesh(meshMaxError, width, height, terrain);
|
|
5240
|
-
break;
|
|
5241
|
-
default:
|
|
5242
|
-
// Intentional: default to Martini for any unspecified or unrecognized tesselator.
|
|
5243
|
-
mesh = this.getMartiniTileMesh(meshMaxError, width, terrain);
|
|
5244
|
-
break;
|
|
5245
|
-
}
|
|
5246
|
-
const { vertices } = mesh;
|
|
5247
|
-
let { triangles } = mesh;
|
|
5248
|
-
let attributes = this.getMeshAttributes(vertices, terrain, width, height, input.bounds);
|
|
5249
|
-
// Compute bounding box before adding skirt so that z values are not skewed
|
|
5250
|
-
const boundingBox = getMeshBoundingBox(attributes);
|
|
5251
|
-
if (terrainSkirtHeight) {
|
|
5252
|
-
const { attributes: newAttributes, triangles: newTriangles } = addSkirt(attributes, triangles, terrainSkirtHeight);
|
|
5253
|
-
attributes = newAttributes;
|
|
5254
|
-
triangles = newTriangles;
|
|
5255
|
-
}
|
|
5256
|
-
const map = {
|
|
5257
|
-
// Data return by this loader implementation
|
|
5258
|
-
loaderData: {
|
|
5259
|
-
header: {},
|
|
5260
|
-
},
|
|
5261
|
-
header: {
|
|
5262
|
-
vertexCount: triangles.length,
|
|
5263
|
-
boundingBox,
|
|
5264
|
-
},
|
|
5265
|
-
mode: 4, // TRIANGLES
|
|
5266
|
-
indices: { value: Uint32Array.from(triangles), size: 1 },
|
|
5267
|
-
attributes,
|
|
5268
|
-
};
|
|
5269
|
-
const gridWidth = width === 257 ? 257 : width + 1;
|
|
5270
|
-
const gridHeight = height === 257 ? 257 : height + 1;
|
|
5271
|
-
return {
|
|
5272
|
-
map,
|
|
5273
|
-
raw: terrain,
|
|
5274
|
-
width: gridWidth,
|
|
5275
|
-
height: gridHeight
|
|
5276
|
-
};
|
|
5277
|
-
}
|
|
5278
|
-
/**
|
|
5279
|
-
* Decodes raw raster data into a Float32Array of elevation values.
|
|
5280
|
-
* Handles channel selection, value scaling, data type validation, and border stitching.
|
|
5281
|
-
*/
|
|
5282
|
-
static computeTerrainData(input, options) {
|
|
5283
|
-
const { width, height, rasters } = input;
|
|
5284
|
-
const optionsLocal = { ...options };
|
|
5285
|
-
optionsLocal.useChannelIndex ??= optionsLocal.useChannel == null ? null : optionsLocal.useChannel - 1;
|
|
5286
|
-
// Detect if data is planar (multiple arrays) or interleaved (one array with multiple samples per pixel)
|
|
5287
|
-
const isPlanar = rasters.length > 1;
|
|
5288
|
-
const channel = isPlanar
|
|
5289
|
-
? (rasters[optionsLocal.useChannelIndex ?? 0] ?? rasters[0])
|
|
5290
|
-
: rasters[0];
|
|
5291
|
-
const terrain = new Float32Array((width === 257 ? width : width + 1) * (height === 257 ? height : height + 1));
|
|
5292
|
-
const samplesPerPixel = isPlanar ? 1 : (channel.length / (width * height));
|
|
5293
|
-
// If planar, we already selected the correct array, so start at index 0.
|
|
5294
|
-
// If interleaved, start at the index of the desired channel.
|
|
5295
|
-
let pixel = isPlanar ? 0 : (optionsLocal.useChannelIndex ?? 0);
|
|
5296
|
-
const isStitched = width === 257;
|
|
5297
|
-
const fallbackValue = options.terrainMinValue ?? 0;
|
|
5298
|
-
for (let y = 0; y < height; y++) {
|
|
5299
|
-
for (let x = 0; x < width; x++) {
|
|
5300
|
-
const multiplier = options.multiplier ?? 1;
|
|
5301
|
-
let elevationValue = (options.noDataValue !== undefined &&
|
|
5302
|
-
options.noDataValue !== null &&
|
|
5303
|
-
channel[pixel] === options.noDataValue)
|
|
5304
|
-
? fallbackValue
|
|
5305
|
-
: channel[pixel] * multiplier;
|
|
5306
|
-
// Validate that the elevation value is within the valid range for Float32.
|
|
5307
|
-
// Extreme values (like -1.79e308) can become -Infinity when cast, causing WebGL errors.
|
|
5308
|
-
if (Number.isNaN(elevationValue) || elevationValue < -34e37 || elevationValue > 3.4e38) {
|
|
5309
|
-
elevationValue = fallbackValue;
|
|
5310
|
-
}
|
|
5311
|
-
// If stitched (257), fill linearly. If 256, fill with stride for padding.
|
|
5312
|
-
const index = isStitched ? (y * width + x) : (y * (width + 1) + x);
|
|
5313
|
-
terrain[index] = elevationValue;
|
|
5314
|
-
pixel += samplesPerPixel;
|
|
5315
|
-
}
|
|
5316
|
-
}
|
|
5317
|
-
if (!isStitched) {
|
|
5318
|
-
// backfill bottom border
|
|
5319
|
-
for (let i = (width + 1) * width, x = 0; x < width; x++, i++) {
|
|
5320
|
-
terrain[i] = terrain[i - width - 1];
|
|
5321
|
-
}
|
|
5322
|
-
// backfill right border
|
|
5323
|
-
for (let i = height, y = 0; y < height + 1; y++, i += height + 1) {
|
|
5324
|
-
terrain[i] = terrain[i - 1];
|
|
5325
|
-
}
|
|
5326
|
-
}
|
|
5327
|
-
return terrain;
|
|
5328
|
-
}
|
|
5329
|
-
static getMartiniTileMesh(meshMaxError, width, terrain) {
|
|
5330
|
-
const gridSize = width === 257 ? 257 : width + 1;
|
|
5331
|
-
const martini = new Martini(gridSize);
|
|
5332
|
-
const tile = martini.createTile(terrain);
|
|
5333
|
-
const { vertices, triangles } = tile.getMesh(meshMaxError);
|
|
5334
|
-
return { vertices, triangles };
|
|
5335
|
-
}
|
|
5336
|
-
static getDelatinTileMesh(meshMaxError, width, height, terrain) {
|
|
5337
|
-
const widthPlus = width === 257 ? 257 : width + 1;
|
|
5338
|
-
const heightPlus = height === 257 ? 257 : height + 1;
|
|
5339
|
-
const tin = new Delatin(terrain, widthPlus, heightPlus);
|
|
5340
|
-
tin.run(meshMaxError);
|
|
5341
|
-
// @ts-expect-error: Delatin instance properties 'coords' and 'triangles' are not explicitly typed in the library port
|
|
5342
|
-
const { coords, triangles } = tin;
|
|
5343
|
-
const vertices = coords;
|
|
5344
|
-
return { vertices, triangles };
|
|
5345
|
-
}
|
|
5346
|
-
static getMeshAttributes(vertices, terrain, width, height, bounds) {
|
|
5347
|
-
const gridSize = width === 257 ? 257 : width + 1;
|
|
5348
|
-
const numOfVerticies = vertices.length / 2;
|
|
5349
|
-
// vec3. x, y in pixels, z in meters
|
|
5350
|
-
const positions = new Float32Array(numOfVerticies * 3);
|
|
5351
|
-
// vec2. 1 to 1 relationship with position. represents the uv on the texture image. 0,0 to 1,1.
|
|
5352
|
-
const texCoords = new Float32Array(numOfVerticies * 2);
|
|
5353
|
-
const [minX, minY, maxX, maxY] = bounds || [0, 0, width, height];
|
|
5354
|
-
// If stitched (257), the spatial extent covers 0..256 pixels, so we divide by 256.
|
|
5355
|
-
// If standard (256), the spatial extent covers 0..256 pixels (with backfill), so we divide by 256.
|
|
5356
|
-
const effectiveWidth = width === 257 ? width - 1 : width;
|
|
5357
|
-
const effectiveHeight = height === 257 ? height - 1 : height;
|
|
5358
|
-
const xScale = (maxX - minX) / effectiveWidth;
|
|
5359
|
-
const yScale = (maxY - minY) / effectiveHeight;
|
|
5360
|
-
for (let i = 0; i < numOfVerticies; i++) {
|
|
5361
|
-
const x = vertices[i * 2];
|
|
5362
|
-
const y = vertices[i * 2 + 1];
|
|
5363
|
-
const pixelIdx = y * gridSize + x;
|
|
5364
|
-
positions[3 * i] = x * xScale + minX;
|
|
5365
|
-
positions[3 * i + 1] = -y * yScale + maxY;
|
|
5366
|
-
positions[3 * i + 2] = terrain[pixelIdx];
|
|
5367
|
-
texCoords[2 * i] = x / effectiveWidth;
|
|
5368
|
-
texCoords[2 * i + 1] = y / effectiveHeight;
|
|
5369
|
-
}
|
|
5370
|
-
return {
|
|
5371
|
-
POSITION: { value: positions, size: 3 },
|
|
5372
|
-
TEXCOORD_0: { value: texCoords, size: 2 },
|
|
5373
|
-
// NORMAL: {}, - optional, but creates the high poly look with lighting
|
|
5374
|
-
};
|
|
5375
|
-
}
|
|
5376
|
-
}
|
|
5377
|
-
|
|
5378
5235
|
// DataUtils.ts
|
|
5379
5236
|
function scale(num, inMin, inMax, outMin, outMax) {
|
|
5380
5237
|
if (inMax === inMin) {
|
|
@@ -5682,6 +5539,342 @@ class BitmapGenerator {
|
|
|
5682
5539
|
}
|
|
5683
5540
|
}
|
|
5684
5541
|
|
|
5542
|
+
/**
|
|
5543
|
+
* KernelGenerator — 3×3 neighborhood kernel calculations on elevation rasters.
|
|
5544
|
+
*
|
|
5545
|
+
* Input contract: a Float32Array of 258×258 elevation values (row-major).
|
|
5546
|
+
* Edge pixels (row/col 0 and 257) are used only as kernel neighbors and do
|
|
5547
|
+
* not appear in the output.
|
|
5548
|
+
* Output: Float32Array of 256×256 computed values.
|
|
5549
|
+
*/
|
|
5550
|
+
class KernelGenerator {
|
|
5551
|
+
/**
|
|
5552
|
+
* Calculates slope (0–90 degrees) for each pixel using Horn's method.
|
|
5553
|
+
*
|
|
5554
|
+
* @param src Float32Array of 258×258 elevation values (row-major)
|
|
5555
|
+
* @param cellSize Cell size in meters per pixel
|
|
5556
|
+
* @param zFactor Vertical exaggeration factor (default 1)
|
|
5557
|
+
* @param noDataValue Elevation value treated as noData; output is NaN for those pixels
|
|
5558
|
+
*/
|
|
5559
|
+
static calculateSlope(src, cellSize, zFactor = 1, noDataValue) {
|
|
5560
|
+
const OUT = 256;
|
|
5561
|
+
const IN = 258;
|
|
5562
|
+
const out = new Float32Array(OUT * OUT);
|
|
5563
|
+
for (let r = 0; r < OUT; r++) {
|
|
5564
|
+
for (let c = 0; c < OUT; c++) {
|
|
5565
|
+
// 3×3 neighborhood in the 258×258 input, centered at (r+1, c+1)
|
|
5566
|
+
const base = r * IN + c;
|
|
5567
|
+
const z5 = src[base + IN + 1]; // center pixel
|
|
5568
|
+
if (noDataValue !== undefined && z5 === noDataValue) {
|
|
5569
|
+
out[r * OUT + c] = NaN;
|
|
5570
|
+
continue;
|
|
5571
|
+
}
|
|
5572
|
+
const z1 = src[base]; // nw
|
|
5573
|
+
const z2 = src[base + 1]; // n
|
|
5574
|
+
const z3 = src[base + 2]; // ne
|
|
5575
|
+
const z4 = src[base + IN]; // w
|
|
5576
|
+
const z6 = src[base + IN + 2]; // e
|
|
5577
|
+
const z7 = src[base + 2 * IN]; // sw
|
|
5578
|
+
const z8 = src[base + 2 * IN + 1]; // s
|
|
5579
|
+
const z9 = src[base + 2 * IN + 2]; // se
|
|
5580
|
+
const dzdx = ((z3 + 2 * z6 + z9) - (z1 + 2 * z4 + z7)) / (8 * cellSize);
|
|
5581
|
+
const dzdy = ((z7 + 2 * z8 + z9) - (z1 + 2 * z2 + z3)) / (8 * cellSize);
|
|
5582
|
+
const slopeRad = Math.atan(zFactor * Math.sqrt(dzdx * dzdx + dzdy * dzdy));
|
|
5583
|
+
out[r * OUT + c] = slopeRad * (180 / Math.PI);
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
return out;
|
|
5587
|
+
}
|
|
5588
|
+
/**
|
|
5589
|
+
* Calculates hillshade (0–255 grayscale) for each pixel.
|
|
5590
|
+
* Follows the ESRI hillshade algorithm convention.
|
|
5591
|
+
*
|
|
5592
|
+
* @param src Float32Array of 258×258 elevation values (row-major)
|
|
5593
|
+
* @param azimuth Sun azimuth in degrees (default 315 = NW)
|
|
5594
|
+
* @param altitude Sun altitude above horizon in degrees (default 45)
|
|
5595
|
+
* @param cellSize Cell size in meters per pixel
|
|
5596
|
+
* @param zFactor Vertical exaggeration factor (default 1)
|
|
5597
|
+
* @param noDataValue Elevation value treated as noData; output is NaN for those pixels
|
|
5598
|
+
*/
|
|
5599
|
+
static calculateHillshade(src, cellSize, azimuth = 315, altitude = 45, zFactor = 1, noDataValue) {
|
|
5600
|
+
const OUT = 256;
|
|
5601
|
+
const IN = 258;
|
|
5602
|
+
const out = new Float32Array(OUT * OUT);
|
|
5603
|
+
const zenithRad = (90 - altitude) * (Math.PI / 180);
|
|
5604
|
+
let azimuthMath = 360 - azimuth + 90;
|
|
5605
|
+
if (azimuthMath >= 360)
|
|
5606
|
+
azimuthMath -= 360;
|
|
5607
|
+
const azimuthRad = azimuthMath * (Math.PI / 180);
|
|
5608
|
+
for (let r = 0; r < OUT; r++) {
|
|
5609
|
+
for (let c = 0; c < OUT; c++) {
|
|
5610
|
+
const base = r * IN + c;
|
|
5611
|
+
const z5 = src[base + IN + 1]; // center pixel
|
|
5612
|
+
if (noDataValue !== undefined && z5 === noDataValue) {
|
|
5613
|
+
out[r * OUT + c] = NaN;
|
|
5614
|
+
continue;
|
|
5615
|
+
}
|
|
5616
|
+
const z1 = src[base]; // nw
|
|
5617
|
+
const z2 = src[base + 1]; // n
|
|
5618
|
+
const z3 = src[base + 2]; // ne
|
|
5619
|
+
const z4 = src[base + IN]; // w
|
|
5620
|
+
const z6 = src[base + IN + 2]; // e
|
|
5621
|
+
const z7 = src[base + 2 * IN]; // sw
|
|
5622
|
+
const z8 = src[base + 2 * IN + 1]; // s
|
|
5623
|
+
const z9 = src[base + 2 * IN + 2]; // se
|
|
5624
|
+
const dzdx = ((z3 + 2 * z6 + z9) - (z1 + 2 * z4 + z7)) / (8 * cellSize);
|
|
5625
|
+
// dzdy: north minus south (geographic convention — top rows minus bottom rows in raster)
|
|
5626
|
+
const dzdy = ((z1 + 2 * z2 + z3) - (z7 + 2 * z8 + z9)) / (8 * cellSize);
|
|
5627
|
+
const slopeRad = Math.atan(zFactor * Math.sqrt(dzdx * dzdx + dzdy * dzdy));
|
|
5628
|
+
const aspectRad = Math.atan2(dzdy, -dzdx);
|
|
5629
|
+
const hillshade = 255 * (Math.cos(zenithRad) * Math.cos(slopeRad) +
|
|
5630
|
+
Math.sin(zenithRad) * Math.sin(slopeRad) * Math.cos(azimuthRad - aspectRad));
|
|
5631
|
+
out[r * OUT + c] = Math.max(0, Math.min(255, hillshade));
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
return out;
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
|
|
5638
|
+
class TerrainGenerator {
|
|
5639
|
+
static async generate(input, options, meshMaxError) {
|
|
5640
|
+
const { width, height } = input;
|
|
5641
|
+
const isKernel = width === 258;
|
|
5642
|
+
// 1. Compute Terrain Data (Extract Elevation)
|
|
5643
|
+
const terrain = this.computeTerrainData(input, options);
|
|
5644
|
+
// For kernel tiles, the mesh uses the inner 257×257 sub-grid (rows 1–257, cols 1–257)
|
|
5645
|
+
// so that row 0 / col 0 (kernel padding) is dropped while the bottom/right stitching
|
|
5646
|
+
// overlap is preserved.
|
|
5647
|
+
const meshTerrain = isKernel ? this.extractMeshRaster(terrain) : terrain;
|
|
5648
|
+
const meshWidth = isKernel ? 257 : width;
|
|
5649
|
+
const meshHeight = isKernel ? 257 : height;
|
|
5650
|
+
// 2. Tesselate (Generate Mesh)
|
|
5651
|
+
const { terrainSkirtHeight } = options;
|
|
5652
|
+
let mesh;
|
|
5653
|
+
switch (options.tesselator) {
|
|
5654
|
+
case 'martini':
|
|
5655
|
+
mesh = this.getMartiniTileMesh(meshMaxError, meshWidth, meshTerrain);
|
|
5656
|
+
break;
|
|
5657
|
+
case 'delatin':
|
|
5658
|
+
mesh = this.getDelatinTileMesh(meshMaxError, meshWidth, meshHeight, meshTerrain);
|
|
5659
|
+
break;
|
|
5660
|
+
default:
|
|
5661
|
+
// Intentional: default to Martini for any unspecified or unrecognized tesselator.
|
|
5662
|
+
mesh = this.getMartiniTileMesh(meshMaxError, meshWidth, meshTerrain);
|
|
5663
|
+
break;
|
|
5664
|
+
}
|
|
5665
|
+
const { vertices } = mesh;
|
|
5666
|
+
let { triangles } = mesh;
|
|
5667
|
+
let attributes = this.getMeshAttributes(vertices, meshTerrain, meshWidth, meshHeight, input.bounds);
|
|
5668
|
+
// Compute bounding box before adding skirt so that z values are not skewed
|
|
5669
|
+
const boundingBox = getMeshBoundingBox(attributes);
|
|
5670
|
+
if (terrainSkirtHeight) {
|
|
5671
|
+
const { attributes: newAttributes, triangles: newTriangles } = addSkirt(attributes, triangles, terrainSkirtHeight);
|
|
5672
|
+
attributes = newAttributes;
|
|
5673
|
+
triangles = newTriangles;
|
|
5674
|
+
}
|
|
5675
|
+
const map = {
|
|
5676
|
+
// Data return by this loader implementation
|
|
5677
|
+
loaderData: {
|
|
5678
|
+
header: {},
|
|
5679
|
+
},
|
|
5680
|
+
header: {
|
|
5681
|
+
vertexCount: triangles.length,
|
|
5682
|
+
boundingBox,
|
|
5683
|
+
},
|
|
5684
|
+
mode: 4, // TRIANGLES
|
|
5685
|
+
indices: { value: Uint32Array.from(triangles), size: 1 },
|
|
5686
|
+
attributes,
|
|
5687
|
+
};
|
|
5688
|
+
// For kernel tiles, raw holds the 257×257 mesh elevation (same as non-kernel).
|
|
5689
|
+
// gridWidth/gridHeight reflect the mesh dimensions.
|
|
5690
|
+
const gridWidth = meshWidth === 257 ? 257 : meshWidth + 1;
|
|
5691
|
+
const gridHeight = meshHeight === 257 ? 257 : meshHeight + 1;
|
|
5692
|
+
const tileResult = {
|
|
5693
|
+
map,
|
|
5694
|
+
raw: meshTerrain,
|
|
5695
|
+
width: gridWidth,
|
|
5696
|
+
height: gridHeight,
|
|
5697
|
+
};
|
|
5698
|
+
// 3. Kernel path: compute slope or hillshade, store as rawDerived, generate texture
|
|
5699
|
+
if (isKernel && (options.useSlope || options.useHillshade)) {
|
|
5700
|
+
// Use pre-computed geographic cellSize (meters/pixel) from tile indices.
|
|
5701
|
+
// Falls back to bounds-derived estimate if not provided.
|
|
5702
|
+
const cellSize = input.cellSizeMeters ?? ((input.bounds[2] - input.bounds[0]) / 256);
|
|
5703
|
+
const zFactor = options.zFactor ?? 1;
|
|
5704
|
+
if (options.useSlope && options.useHillshade) {
|
|
5705
|
+
console.warn('[TerrainGenerator] useSlope and useHillshade are mutually exclusive; useSlope takes precedence.');
|
|
5706
|
+
}
|
|
5707
|
+
// Build a separate raster for kernel computation that preserves noData samples.
|
|
5708
|
+
const kernelTerrain = new Float32Array(terrain.length);
|
|
5709
|
+
const sourceRaster = input.rasters[0];
|
|
5710
|
+
const noData = options.noDataValue;
|
|
5711
|
+
if (noData !== undefined &&
|
|
5712
|
+
noData !== null &&
|
|
5713
|
+
sourceRaster &&
|
|
5714
|
+
sourceRaster.length === terrain.length) {
|
|
5715
|
+
for (let i = 0; i < terrain.length; i++) {
|
|
5716
|
+
// If the source raster marks this sample as noData, keep it as noData for the kernel.
|
|
5717
|
+
// Otherwise, use the processed terrain elevation value.
|
|
5718
|
+
// eslint-disable-next-line eqeqeq
|
|
5719
|
+
kernelTerrain[i] = sourceRaster[i] == noData ? noData : terrain[i];
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
else {
|
|
5723
|
+
// Fallback: no usable noData metadata or mismatched lengths; mirror existing behavior.
|
|
5724
|
+
kernelTerrain.set(terrain);
|
|
5725
|
+
}
|
|
5726
|
+
let kernelOutput;
|
|
5727
|
+
if (options.useSlope) {
|
|
5728
|
+
kernelOutput = KernelGenerator.calculateSlope(kernelTerrain, cellSize, zFactor, options.noDataValue);
|
|
5729
|
+
}
|
|
5730
|
+
else {
|
|
5731
|
+
kernelOutput = KernelGenerator.calculateHillshade(kernelTerrain, cellSize, options.hillshadeAzimuth ?? 315, options.hillshadeAltitude ?? 45, zFactor, options.noDataValue);
|
|
5732
|
+
}
|
|
5733
|
+
tileResult.rawDerived = kernelOutput;
|
|
5734
|
+
if (this.hasVisualizationOptions(options)) {
|
|
5735
|
+
const bitmapResult = await BitmapGenerator.generate({ width: 256, height: 256, rasters: [kernelOutput] }, { ...options, type: 'image' });
|
|
5736
|
+
tileResult.texture = bitmapResult.map;
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
else if (this.hasVisualizationOptions(options)) {
|
|
5740
|
+
// 4. Non-kernel path: crop 257→256, generate texture from elevation
|
|
5741
|
+
const cropped = this.cropRaster(meshTerrain, gridWidth, gridHeight, 256, 256);
|
|
5742
|
+
const bitmapResult = await BitmapGenerator.generate({ width: 256, height: 256, rasters: [cropped] }, { ...options, type: 'image' });
|
|
5743
|
+
tileResult.texture = bitmapResult.map;
|
|
5744
|
+
}
|
|
5745
|
+
return tileResult;
|
|
5746
|
+
}
|
|
5747
|
+
/** Extracts rows 1–257, cols 1–257 from a 258×258 terrain array → 257×257 for mesh generation. */
|
|
5748
|
+
static extractMeshRaster(terrain258) {
|
|
5749
|
+
const MESH = 257;
|
|
5750
|
+
const IN = 258;
|
|
5751
|
+
const out = new Float32Array(MESH * MESH);
|
|
5752
|
+
for (let r = 0; r < MESH; r++) {
|
|
5753
|
+
for (let c = 0; c < MESH; c++) {
|
|
5754
|
+
out[r * MESH + c] = terrain258[(r + 1) * IN + (c + 1)];
|
|
5755
|
+
}
|
|
5756
|
+
}
|
|
5757
|
+
return out;
|
|
5758
|
+
}
|
|
5759
|
+
static hasVisualizationOptions(options) {
|
|
5760
|
+
return !!(options.useHeatMap ||
|
|
5761
|
+
options.useSingleColor ||
|
|
5762
|
+
options.useColorsBasedOnValues ||
|
|
5763
|
+
options.useColorClasses);
|
|
5764
|
+
}
|
|
5765
|
+
static cropRaster(src, srcWidth, _srcHeight, dstWidth, dstHeight) {
|
|
5766
|
+
const out = new Float32Array(dstWidth * dstHeight);
|
|
5767
|
+
for (let y = 0; y < dstHeight; y++) {
|
|
5768
|
+
for (let x = 0; x < dstWidth; x++) {
|
|
5769
|
+
out[y * dstWidth + x] = src[y * srcWidth + x];
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5772
|
+
return out;
|
|
5773
|
+
}
|
|
5774
|
+
/**
|
|
5775
|
+
* Decodes raw raster data into a Float32Array of elevation values.
|
|
5776
|
+
* Handles channel selection, value scaling, data type validation, and border stitching.
|
|
5777
|
+
*/
|
|
5778
|
+
static computeTerrainData(input, options) {
|
|
5779
|
+
const { width, height, rasters } = input;
|
|
5780
|
+
const optionsLocal = { ...options };
|
|
5781
|
+
optionsLocal.useChannelIndex ??= optionsLocal.useChannel == null ? null : optionsLocal.useChannel - 1;
|
|
5782
|
+
// Detect if data is planar (multiple arrays) or interleaved (one array with multiple samples per pixel)
|
|
5783
|
+
const isPlanar = rasters.length > 1;
|
|
5784
|
+
const channel = isPlanar
|
|
5785
|
+
? (rasters[optionsLocal.useChannelIndex ?? 0] ?? rasters[0])
|
|
5786
|
+
: rasters[0];
|
|
5787
|
+
const isKernel = width === 258;
|
|
5788
|
+
const isStitched = width === 257;
|
|
5789
|
+
// Kernel: 258×258 flat array. Stitched: 257×257. Default: (width+1)×(height+1) with backfill.
|
|
5790
|
+
const outWidth = isKernel ? 258 : (isStitched ? 257 : width + 1);
|
|
5791
|
+
const outHeight = isKernel ? 258 : (isStitched ? 257 : height + 1);
|
|
5792
|
+
const terrain = new Float32Array(outWidth * outHeight);
|
|
5793
|
+
const samplesPerPixel = isPlanar ? 1 : (channel.length / (width * height));
|
|
5794
|
+
// If planar, we already selected the correct array, so start at index 0.
|
|
5795
|
+
// If interleaved, start at the index of the desired channel.
|
|
5796
|
+
let pixel = isPlanar ? 0 : (optionsLocal.useChannelIndex ?? 0);
|
|
5797
|
+
const fallbackValue = options.terrainMinValue ?? 0;
|
|
5798
|
+
for (let y = 0; y < height; y++) {
|
|
5799
|
+
for (let x = 0; x < width; x++) {
|
|
5800
|
+
const multiplier = options.multiplier ?? 1;
|
|
5801
|
+
let elevationValue = (options.noDataValue !== undefined &&
|
|
5802
|
+
options.noDataValue !== null &&
|
|
5803
|
+
channel[pixel] === options.noDataValue)
|
|
5804
|
+
? fallbackValue
|
|
5805
|
+
: channel[pixel] * multiplier;
|
|
5806
|
+
// Validate that the elevation value is within the valid range for Float32.
|
|
5807
|
+
// Extreme values (like -1.79e308) can become -Infinity when cast, causing WebGL errors.
|
|
5808
|
+
if (Number.isNaN(elevationValue) || elevationValue < -34e37 || elevationValue > 3.4e38) {
|
|
5809
|
+
elevationValue = fallbackValue;
|
|
5810
|
+
}
|
|
5811
|
+
// Kernel/Stitched: fill linearly. Default (256): fill with stride for padding.
|
|
5812
|
+
const index = (isKernel || isStitched) ? (y * width + x) : (y * (width + 1) + x);
|
|
5813
|
+
terrain[index] = elevationValue;
|
|
5814
|
+
pixel += samplesPerPixel;
|
|
5815
|
+
}
|
|
5816
|
+
}
|
|
5817
|
+
if (!isKernel && !isStitched) {
|
|
5818
|
+
// backfill bottom border
|
|
5819
|
+
for (let i = (width + 1) * width, x = 0; x < width; x++, i++) {
|
|
5820
|
+
terrain[i] = terrain[i - width - 1];
|
|
5821
|
+
}
|
|
5822
|
+
// backfill right border
|
|
5823
|
+
for (let i = height, y = 0; y < height + 1; y++, i += height + 1) {
|
|
5824
|
+
terrain[i] = terrain[i - 1];
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
return terrain;
|
|
5828
|
+
}
|
|
5829
|
+
static getMartiniTileMesh(meshMaxError, width, terrain) {
|
|
5830
|
+
const gridSize = width === 257 ? 257 : width + 1;
|
|
5831
|
+
const martini = new Martini(gridSize);
|
|
5832
|
+
const tile = martini.createTile(terrain);
|
|
5833
|
+
const { vertices, triangles } = tile.getMesh(meshMaxError);
|
|
5834
|
+
return { vertices, triangles };
|
|
5835
|
+
}
|
|
5836
|
+
static getDelatinTileMesh(meshMaxError, width, height, terrain) {
|
|
5837
|
+
const widthPlus = width === 257 ? 257 : width + 1;
|
|
5838
|
+
const heightPlus = height === 257 ? 257 : height + 1;
|
|
5839
|
+
const tin = new Delatin(terrain, widthPlus, heightPlus);
|
|
5840
|
+
tin.run(meshMaxError);
|
|
5841
|
+
// @ts-expect-error: Delatin instance properties 'coords' and 'triangles' are not explicitly typed in the library port
|
|
5842
|
+
const { coords, triangles } = tin;
|
|
5843
|
+
const vertices = coords;
|
|
5844
|
+
return { vertices, triangles };
|
|
5845
|
+
}
|
|
5846
|
+
static getMeshAttributes(vertices, terrain, width, height, bounds) {
|
|
5847
|
+
const gridSize = width === 257 ? 257 : width + 1;
|
|
5848
|
+
const numOfVerticies = vertices.length / 2;
|
|
5849
|
+
// vec3. x, y in pixels, z in meters
|
|
5850
|
+
const positions = new Float32Array(numOfVerticies * 3);
|
|
5851
|
+
// vec2. 1 to 1 relationship with position. represents the uv on the texture image. 0,0 to 1,1.
|
|
5852
|
+
const texCoords = new Float32Array(numOfVerticies * 2);
|
|
5853
|
+
const [minX, minY, maxX, maxY] = bounds || [0, 0, width, height];
|
|
5854
|
+
// If stitched (257), the spatial extent covers 0..256 pixels, so we divide by 256.
|
|
5855
|
+
// If standard (256), the spatial extent covers 0..256 pixels (with backfill), so we divide by 256.
|
|
5856
|
+
const effectiveWidth = width === 257 ? width - 1 : width;
|
|
5857
|
+
const effectiveHeight = height === 257 ? height - 1 : height;
|
|
5858
|
+
const xScale = (maxX - minX) / effectiveWidth;
|
|
5859
|
+
const yScale = (maxY - minY) / effectiveHeight;
|
|
5860
|
+
for (let i = 0; i < numOfVerticies; i++) {
|
|
5861
|
+
const x = vertices[i * 2];
|
|
5862
|
+
const y = vertices[i * 2 + 1];
|
|
5863
|
+
const pixelIdx = y * gridSize + x;
|
|
5864
|
+
positions[3 * i] = x * xScale + minX;
|
|
5865
|
+
positions[3 * i + 1] = -y * yScale + maxY;
|
|
5866
|
+
positions[3 * i + 2] = terrain[pixelIdx];
|
|
5867
|
+
texCoords[2 * i] = x / effectiveWidth;
|
|
5868
|
+
texCoords[2 * i + 1] = y / effectiveHeight;
|
|
5869
|
+
}
|
|
5870
|
+
return {
|
|
5871
|
+
POSITION: { value: positions, size: 3 },
|
|
5872
|
+
TEXCOORD_0: { value: texCoords, size: 2 },
|
|
5873
|
+
// NORMAL: {}, - optional, but creates the high poly look with lighting
|
|
5874
|
+
};
|
|
5875
|
+
}
|
|
5876
|
+
}
|
|
5877
|
+
|
|
5685
5878
|
class GeoImage {
|
|
5686
5879
|
data;
|
|
5687
5880
|
async setUrl(url) {
|
|
@@ -5709,6 +5902,7 @@ class GeoImage {
|
|
|
5709
5902
|
let width;
|
|
5710
5903
|
let height;
|
|
5711
5904
|
let bounds;
|
|
5905
|
+
let cellSizeMeters;
|
|
5712
5906
|
if (typeof (input) === 'string') {
|
|
5713
5907
|
// TODO not tested
|
|
5714
5908
|
// input is type of object
|
|
@@ -5723,9 +5917,10 @@ class GeoImage {
|
|
|
5723
5917
|
width = input.width;
|
|
5724
5918
|
height = input.height;
|
|
5725
5919
|
bounds = input.bounds;
|
|
5920
|
+
cellSizeMeters = input.cellSizeMeters;
|
|
5726
5921
|
}
|
|
5727
5922
|
// Delegate to TerrainGenerator
|
|
5728
|
-
return TerrainGenerator.generate({ width, height, rasters, bounds }, options, meshMaxError);
|
|
5923
|
+
return await TerrainGenerator.generate({ width, height, rasters, bounds, cellSizeMeters }, options, meshMaxError);
|
|
5729
5924
|
}
|
|
5730
5925
|
async getBitmap(input, options) {
|
|
5731
5926
|
let rasters = [];
|
|
@@ -6017,14 +6212,21 @@ class CogTiles {
|
|
|
6017
6212
|
async getTile(x, y, z, bounds, meshMaxError) {
|
|
6018
6213
|
let requiredSize = this.tileSize; // Default 256 for image/bitmap
|
|
6019
6214
|
if (this.options.type === 'terrain') {
|
|
6020
|
-
|
|
6215
|
+
const isKernel = this.options.useSlope || this.options.useHillshade;
|
|
6216
|
+
requiredSize = this.tileSize + (isKernel ? 2 : 1); // 258 for kernel (3×3 border), 257 for normal stitching
|
|
6021
6217
|
}
|
|
6022
6218
|
const tileData = await this.getTileFromImage(x, y, z, requiredSize);
|
|
6219
|
+
// Compute true ground cell size in meters from tile indices.
|
|
6220
|
+
// Tile y in slippy-map convention → center latitude → Web Mercator distortion correction.
|
|
6221
|
+
const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 0.5) / Math.pow(2, z))));
|
|
6222
|
+
const tileWidthMeters = (EARTH_CIRCUMFERENCE / Math.pow(2, z)) * Math.cos(latRad);
|
|
6223
|
+
const cellSizeMeters = tileWidthMeters / this.tileSize;
|
|
6023
6224
|
return this.geo.getMap({
|
|
6024
6225
|
rasters: [tileData[0]],
|
|
6025
6226
|
width: requiredSize,
|
|
6026
6227
|
height: requiredSize,
|
|
6027
6228
|
bounds,
|
|
6229
|
+
cellSizeMeters,
|
|
6028
6230
|
}, this.options, meshMaxError);
|
|
6029
6231
|
}
|
|
6030
6232
|
/**
|
|
@@ -6418,6 +6620,12 @@ class CogTerrainLayer extends CompositeLayer {
|
|
|
6418
6620
|
// Trigger a refresh of the tiles
|
|
6419
6621
|
this.state.terrainCogTiles.options.useChannelIndex = null; // Clear cached index
|
|
6420
6622
|
}
|
|
6623
|
+
// When the external cogTiles instance is swapped (e.g. mode switch), update state so
|
|
6624
|
+
// renderLayers picks up the new reference and the TileLayer updateTrigger fires a refetch
|
|
6625
|
+
// while keeping old tile content visible until new tiles are ready.
|
|
6626
|
+
if (props.cogTiles && props.cogTiles !== oldProps.cogTiles) {
|
|
6627
|
+
this.setState({ terrainCogTiles: props.cogTiles });
|
|
6628
|
+
}
|
|
6421
6629
|
if (props.workerUrl) {
|
|
6422
6630
|
log.removed('workerUrl', 'loadOptions.terrain.workerUrl')();
|
|
6423
6631
|
}
|
|
@@ -6475,15 +6683,16 @@ class CogTerrainLayer extends CompositeLayer {
|
|
|
6475
6683
|
}
|
|
6476
6684
|
// const [mesh, texture] = data;
|
|
6477
6685
|
const [meshResult] = data;
|
|
6478
|
-
|
|
6686
|
+
const tileTexture = (!this.props.disableTexture && meshResult?.texture) ? meshResult.texture : null;
|
|
6687
|
+
return new SubLayerClass({ ...props, tileSize: props.tileSize }, {
|
|
6479
6688
|
data: DUMMY_DATA,
|
|
6480
6689
|
mesh: meshResult?.map,
|
|
6481
|
-
|
|
6690
|
+
texture: tileTexture,
|
|
6482
6691
|
_instanced: false,
|
|
6483
6692
|
pickable: props.pickable,
|
|
6484
6693
|
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
|
|
6485
6694
|
// getPosition: (d) => [0, 0, 0],
|
|
6486
|
-
getColor: color,
|
|
6695
|
+
getColor: tileTexture ? [255, 255, 255] : color,
|
|
6487
6696
|
wireframe,
|
|
6488
6697
|
material,
|
|
6489
6698
|
});
|
|
@@ -6532,6 +6741,9 @@ class CogTerrainLayer extends CompositeLayer {
|
|
|
6532
6741
|
// texture: urlTemplateToUpdateTrigger(texture),
|
|
6533
6742
|
meshMaxError,
|
|
6534
6743
|
elevationDecoder,
|
|
6744
|
+
// When cogTiles instance is swapped (e.g. mode switch), refetch tiles.
|
|
6745
|
+
// deck.gl keeps old tile content visible until new tiles are ready.
|
|
6746
|
+
terrainCogTiles: this.state.terrainCogTiles,
|
|
6535
6747
|
},
|
|
6536
6748
|
},
|
|
6537
6749
|
onViewportLoad: this.onViewportLoad.bind(this),
|