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