@gisatcz/deckgl-geolib 2.1.2-dev.1 → 2.1.4-dev.0

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/dist/esm/index.js CHANGED
@@ -4513,6 +4513,41 @@ async function fromArrayBuffer(arrayBuffer, signal) {
4513
4513
  return GeoTIFF.fromSource(makeBufferSource(arrayBuffer), undefined, signal);
4514
4514
  }
4515
4515
 
4516
+ // types.ts
4517
+ const DefaultGeoImageOptions = {
4518
+ type: 'image',
4519
+ tesselator: 'martini',
4520
+ format: undefined,
4521
+ useHeatMap: true,
4522
+ useColorsBasedOnValues: false,
4523
+ useAutoRange: false,
4524
+ useDataForOpacity: false,
4525
+ useSingleColor: false,
4526
+ useColorClasses: false,
4527
+ blurredTexture: true,
4528
+ clipLow: null,
4529
+ clipHigh: null,
4530
+ multiplier: 1.0,
4531
+ color: [255, 0, 255, 255],
4532
+ colorScale: chroma.brewer.YlOrRd,
4533
+ colorScaleValueRange: [0, 255],
4534
+ colorsBasedOnValues: undefined,
4535
+ colorClasses: undefined,
4536
+ alpha: 100,
4537
+ useChannel: null,
4538
+ useChannelIndex: null,
4539
+ noDataValue: undefined,
4540
+ numOfChannels: undefined,
4541
+ nullColor: [0, 0, 0, 0],
4542
+ unidentifiedColor: [0, 0, 0, 0],
4543
+ clippedColor: [0, 0, 0, 0],
4544
+ terrainColor: [133, 133, 133, 255],
4545
+ terrainSkirtHeight: 100,
4546
+ // Default fallback for invalid/nodata elevations. Should be configured based on the dataset's actual range.
4547
+ terrainMinValue: 0,
4548
+ planarConfig: undefined,
4549
+ };
4550
+
4516
4551
  class Martini {
4517
4552
  constructor(gridSize = 257) {
4518
4553
  this.gridSize = gridSize;
@@ -4677,107 +4712,6 @@ class Tile {
4677
4712
  }
4678
4713
  }
4679
4714
 
4680
- // loaders.gl
4681
- // SPDX-License-Identifier: MIT
4682
- // Copyright (c) vis.gl contributors
4683
- /**
4684
- * Add skirt to existing mesh
4685
- * @param {object} attributes - POSITION and TEXCOOD_0 attributes data
4686
- * @param {any} triangles - indices array of the mesh geometry
4687
- * @param skirtHeight - height of the skirt geometry
4688
- * @param outsideIndices - edge indices from quantized mesh data
4689
- * @returns - geometry data with added skirt
4690
- */
4691
- function addSkirt(attributes, triangles, skirtHeight, outsideIndices) {
4692
- const outsideEdges = getOutsideEdgesFromTriangles(triangles);
4693
- // 2 new vertices for each outside edge
4694
- const newPosition = new attributes.POSITION.value.constructor(outsideEdges.length * 6);
4695
- const newTexcoord0 = new attributes.TEXCOORD_0.value.constructor(outsideEdges.length * 4);
4696
- // 2 new triangles for each outside edge
4697
- const newTriangles = new triangles.constructor(outsideEdges.length * 6);
4698
- for (let i = 0; i < outsideEdges.length; i++) {
4699
- const edge = outsideEdges[i];
4700
- updateAttributesForNewEdge({
4701
- edge,
4702
- edgeIndex: i,
4703
- attributes,
4704
- skirtHeight,
4705
- newPosition,
4706
- newTexcoord0,
4707
- newTriangles,
4708
- });
4709
- }
4710
- attributes.POSITION.value = concatenateTypedArrays(attributes.POSITION.value, newPosition);
4711
- attributes.TEXCOORD_0.value = concatenateTypedArrays(attributes.TEXCOORD_0.value, newTexcoord0);
4712
- const resultTriangles = triangles instanceof Array
4713
- ? triangles.concat(newTriangles)
4714
- : concatenateTypedArrays(triangles, newTriangles);
4715
- return {
4716
- attributes,
4717
- triangles: resultTriangles,
4718
- };
4719
- }
4720
- /**
4721
- * Get geometry edges that located on a border of the mesh
4722
- * @param {any} triangles - indices array of the mesh geometry
4723
- * @returns {number[][]} - outside edges data
4724
- */
4725
- function getOutsideEdgesFromTriangles(triangles) {
4726
- const edges = [];
4727
- for (let i = 0; i < triangles.length; i += 3) {
4728
- edges.push([triangles[i], triangles[i + 1]]);
4729
- edges.push([triangles[i + 1], triangles[i + 2]]);
4730
- edges.push([triangles[i + 2], triangles[i]]);
4731
- }
4732
- edges.sort((a, b) => Math.min(...a) - Math.min(...b) || Math.max(...a) - Math.max(...b));
4733
- const outsideEdges = [];
4734
- let index = 0;
4735
- while (index < edges.length) {
4736
- if (edges[index][0] === edges[index + 1]?.[1] && edges[index][1] === edges[index + 1]?.[0]) {
4737
- index += 2;
4738
- }
4739
- else {
4740
- outsideEdges.push(edges[index]);
4741
- index++;
4742
- }
4743
- }
4744
- return outsideEdges;
4745
- }
4746
- /**
4747
- * Get geometry edges that located on a border of the mesh
4748
- * @param {object} args
4749
- * @param {number[]} args.edge - edge indices in geometry
4750
- * @param {number} args.edgeIndex - edge index in outsideEdges array
4751
- * @param {object} args.attributes - POSITION and TEXCOORD_0 attributes
4752
- * @param {number} args.skirtHeight - height of the skirt geometry
4753
- * @param {TypedArray} args.newPosition - POSITION array for skirt data
4754
- * @param {TypedArray} args.newTexcoord0 - TEXCOORD_0 array for skirt data
4755
- * @param {TypedArray | Array} args.newTriangles - trinagle indices array for skirt data
4756
- * @returns {void}
4757
- */
4758
- function updateAttributesForNewEdge({ edge, edgeIndex, attributes, skirtHeight, newPosition, newTexcoord0, newTriangles, }) {
4759
- const positionsLength = attributes.POSITION.value.length;
4760
- const vertex1Offset = edgeIndex * 2;
4761
- const vertex2Offset = edgeIndex * 2 + 1;
4762
- // Define POSITION for new 1st vertex
4763
- newPosition.set(attributes.POSITION.value.subarray(edge[0] * 3, edge[0] * 3 + 3), vertex1Offset * 3);
4764
- newPosition[vertex1Offset * 3 + 2] = newPosition[vertex1Offset * 3 + 2] - skirtHeight; // put down elevation on the skirt height
4765
- // Define POSITION for new 2nd vertex
4766
- newPosition.set(attributes.POSITION.value.subarray(edge[1] * 3, edge[1] * 3 + 3), vertex2Offset * 3);
4767
- newPosition[vertex2Offset * 3 + 2] = newPosition[vertex2Offset * 3 + 2] - skirtHeight; // put down elevation on the skirt height
4768
- // Use same TEXCOORDS for skirt vertices
4769
- newTexcoord0.set(attributes.TEXCOORD_0.value.subarray(edge[0] * 2, edge[0] * 2 + 2), vertex1Offset * 2);
4770
- newTexcoord0.set(attributes.TEXCOORD_0.value.subarray(edge[1] * 2, edge[1] * 2 + 2), vertex2Offset * 2);
4771
- // Define new triangles
4772
- const triangle1Offset = edgeIndex * 2 * 3;
4773
- newTriangles[triangle1Offset] = edge[0];
4774
- newTriangles[triangle1Offset + 1] = positionsLength / 3 + vertex2Offset;
4775
- newTriangles[triangle1Offset + 2] = edge[1];
4776
- newTriangles[triangle1Offset + 3] = positionsLength / 3 + vertex2Offset;
4777
- newTriangles[triangle1Offset + 4] = edge[0];
4778
- newTriangles[triangle1Offset + 5] = positionsLength / 3 + vertex1Offset;
4779
- }
4780
-
4781
4715
  // loaders.gl
4782
4716
  // SPDX-License-Identifier: MIT
4783
4717
  // Copyright (c) vis.gl contributors
@@ -5188,141 +5122,130 @@ function inCircle(ax, ay, bx, by, cx, cy, px, py) {
5188
5122
  return dx * (ey * cp - bp * fy) - dy * (ex * cp - bp * fx) + ap * (ex * fy - ey * fx) < 0;
5189
5123
  }
5190
5124
 
5191
- // import { ExtentsLeftBottomRightTop } from '@deck.gl/core/utils/positions';
5192
- const DefaultGeoImageOptions = {
5193
- type: 'image',
5194
- tesselator: 'martini',
5195
- format: 'uint8',
5196
- useHeatMap: true,
5197
- useColorsBasedOnValues: false,
5198
- useAutoRange: false,
5199
- useDataForOpacity: false,
5200
- useSingleColor: false,
5201
- useColorClasses: false,
5202
- blurredTexture: true,
5203
- clipLow: null,
5204
- clipHigh: null,
5205
- multiplier: 1.0,
5206
- color: [255, 0, 255, 255],
5207
- colorScale: chroma.brewer.YlOrRd,
5208
- colorScaleValueRange: [0, 255],
5209
- colorsBasedOnValues: null,
5210
- colorClasses: null,
5211
- alpha: 100,
5212
- useChannel: null,
5213
- useChannelIndex: null,
5214
- noDataValue: undefined,
5215
- numOfChannels: undefined,
5216
- nullColor: [0, 0, 0, 0],
5217
- unidentifiedColor: [0, 0, 0, 0],
5218
- clippedColor: [0, 0, 0, 0],
5219
- terrainColor: [133, 133, 133, 255],
5220
- terrainSkirtHeight: 100,
5221
- terrainMinValue: 0,
5222
- planarConfig: undefined,
5223
- };
5224
- class GeoImage {
5225
- data;
5226
- scale = (num, inMin, inMax, outMin, outMax) => ((num - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
5227
- async setUrl(url) {
5228
- // TODO - not tested
5229
- const response = await fetch(url);
5230
- const arrayBuffer = await response.arrayBuffer();
5231
- const tiff = await fromArrayBuffer(arrayBuffer);
5232
- const data = await tiff.getImage(0);
5233
- this.data = data;
5125
+ // loaders.gl
5126
+ // SPDX-License-Identifier: MIT
5127
+ // Copyright (c) vis.gl contributors
5128
+ /**
5129
+ * Add skirt to existing mesh
5130
+ * @param {object} attributes - POSITION and TEXCOOD_0 attributes data
5131
+ * @param {any} triangles - indices array of the mesh geometry
5132
+ * @param skirtHeight - height of the skirt geometry
5133
+ * @param outsideIndices - edge indices from quantized mesh data
5134
+ * @returns - geometry data with added skirt
5135
+ */
5136
+ function addSkirt(attributes, triangles, skirtHeight, outsideIndices) {
5137
+ const outsideEdges = getOutsideEdgesFromTriangles(triangles);
5138
+ // 2 new vertices for each outside edge
5139
+ const newPosition = new attributes.POSITION.value.constructor(outsideEdges.length * 6);
5140
+ const newTexcoord0 = new attributes.TEXCOORD_0.value.constructor(outsideEdges.length * 4);
5141
+ // 2 new triangles for each outside edge
5142
+ const newTriangles = new triangles.constructor(outsideEdges.length * 6);
5143
+ for (let i = 0; i < outsideEdges.length; i++) {
5144
+ const edge = outsideEdges[i];
5145
+ updateAttributesForNewEdge({
5146
+ edge,
5147
+ edgeIndex: i,
5148
+ attributes,
5149
+ skirtHeight,
5150
+ newPosition,
5151
+ newTexcoord0,
5152
+ newTriangles,
5153
+ });
5234
5154
  }
5235
- async getMap(input, options, meshMaxError) {
5236
- const mergedOptions = { ...DefaultGeoImageOptions, ...options };
5237
- switch (mergedOptions.type) {
5238
- case 'image':
5239
- return this.getBitmap(input, mergedOptions);
5240
- case 'terrain':
5241
- return this.getHeightmap(input, mergedOptions, meshMaxError);
5242
- default:
5243
- return null;
5244
- }
5155
+ attributes.POSITION.value = concatenateTypedArrays(attributes.POSITION.value, newPosition);
5156
+ attributes.TEXCOORD_0.value = concatenateTypedArrays(attributes.TEXCOORD_0.value, newTexcoord0);
5157
+ const resultTriangles = triangles instanceof Array
5158
+ ? triangles.concat(newTriangles)
5159
+ : concatenateTypedArrays(triangles, newTriangles);
5160
+ return {
5161
+ attributes,
5162
+ triangles: resultTriangles,
5163
+ };
5164
+ }
5165
+ /**
5166
+ * Get geometry edges that located on a border of the mesh
5167
+ * @param {any} triangles - indices array of the mesh geometry
5168
+ * @returns {number[][]} - outside edges data
5169
+ */
5170
+ function getOutsideEdgesFromTriangles(triangles) {
5171
+ const edges = [];
5172
+ for (let i = 0; i < triangles.length; i += 3) {
5173
+ edges.push([triangles[i], triangles[i + 1]]);
5174
+ edges.push([triangles[i + 1], triangles[i + 2]]);
5175
+ edges.push([triangles[i + 2], triangles[i]]);
5245
5176
  }
5246
- // GetHeightmap uses only "useChannel" and "multiplier" options
5247
- async getHeightmap(input, options, meshMaxError) {
5248
- let rasters = [];
5249
- let width;
5250
- let height;
5251
- if (typeof (input) === 'string') {
5252
- // TODO not tested
5253
- // input is type of object
5254
- await this.setUrl(input);
5255
- rasters = (await this.data.readRasters());
5256
- width = this.data.getWidth();
5257
- height = this.data.getHeight();
5177
+ edges.sort((a, b) => Math.min(...a) - Math.min(...b) || Math.max(...a) - Math.max(...b));
5178
+ const outsideEdges = [];
5179
+ let index = 0;
5180
+ while (index < edges.length) {
5181
+ if (edges[index][0] === edges[index + 1]?.[1] && edges[index][1] === edges[index + 1]?.[0]) {
5182
+ index += 2;
5258
5183
  }
5259
5184
  else {
5260
- rasters = input.rasters;
5261
- width = input.width;
5262
- height = input.height;
5263
- }
5264
- const optionsLocal = { ...options };
5265
- let channel = rasters[0];
5266
- optionsLocal.useChannelIndex ??= optionsLocal.useChannel == null ? null : optionsLocal.useChannel - 1;
5267
- if (options.useChannelIndex != null) {
5268
- if (rasters[optionsLocal.useChannelIndex]) {
5269
- channel = rasters[optionsLocal.useChannelIndex];
5270
- }
5271
- }
5272
- const terrain = new Float32Array((width === 257 ? width : width + 1) * (height === 257 ? height : height + 1));
5273
- const numOfChannels = channel.length / (width * height);
5274
- let pixel = options.useChannelIndex === null ? 0 : options.useChannelIndex;
5275
- const isStitched = width === 257;
5276
- for (let y = 0; y < height; y++) {
5277
- for (let x = 0; x < width; x++) {
5278
- let elevationValue = (options.noDataValue && channel[pixel] === options.noDataValue) ? options.terrainMinValue : channel[pixel] * options.multiplier;
5279
- // Validate that the elevation value is within the valid range for Float32.
5280
- // Extreme values (like -1.79e308) can become -Infinity when cast, causing WebGL errors.
5281
- if (Number.isNaN(elevationValue) || elevationValue < -34e37 || elevationValue > 3.4e38) {
5282
- elevationValue = options.terrainMinValue;
5283
- }
5284
- // If stitched (257), fill linearly. If 256, fill with stride for padding.
5285
- const index = isStitched ? (y * width + x) : (y * (width + 1) + x);
5286
- terrain[index] = elevationValue;
5287
- pixel += numOfChannels;
5288
- }
5289
- }
5290
- if (!isStitched) {
5291
- // backfill bottom border
5292
- for (let i = (width + 1) * width, x = 0; x < width; x++, i++) {
5293
- terrain[i] = terrain[i - width - 1];
5294
- }
5295
- // backfill right border
5296
- for (let i = height, y = 0; y < height + 1; y++, i += height + 1) {
5297
- terrain[i] = terrain[i - 1];
5298
- }
5185
+ outsideEdges.push(edges[index]);
5186
+ index++;
5299
5187
  }
5300
- // getMesh
5188
+ }
5189
+ return outsideEdges;
5190
+ }
5191
+ /**
5192
+ * Get geometry edges that located on a border of the mesh
5193
+ * @param {object} args
5194
+ * @param {number[]} args.edge - edge indices in geometry
5195
+ * @param {number} args.edgeIndex - edge index in outsideEdges array
5196
+ * @param {object} args.attributes - POSITION and TEXCOORD_0 attributes
5197
+ * @param {number} args.skirtHeight - height of the skirt geometry
5198
+ * @param {TypedArray} args.newPosition - POSITION array for skirt data
5199
+ * @param {TypedArray} args.newTexcoord0 - TEXCOORD_0 array for skirt data
5200
+ * @param {TypedArray | Array} args.newTriangles - trinagle indices array for skirt data
5201
+ * @returns {void}
5202
+ */
5203
+ function updateAttributesForNewEdge({ edge, edgeIndex, attributes, skirtHeight, newPosition, newTexcoord0, newTriangles, }) {
5204
+ const positionsLength = attributes.POSITION.value.length;
5205
+ const vertex1Offset = edgeIndex * 2;
5206
+ const vertex2Offset = edgeIndex * 2 + 1;
5207
+ // Define POSITION for new 1st vertex
5208
+ newPosition.set(attributes.POSITION.value.subarray(edge[0] * 3, edge[0] * 3 + 3), vertex1Offset * 3);
5209
+ newPosition[vertex1Offset * 3 + 2] = newPosition[vertex1Offset * 3 + 2] - skirtHeight; // put down elevation on the skirt height
5210
+ // Define POSITION for new 2nd vertex
5211
+ newPosition.set(attributes.POSITION.value.subarray(edge[1] * 3, edge[1] * 3 + 3), vertex2Offset * 3);
5212
+ newPosition[vertex2Offset * 3 + 2] = newPosition[vertex2Offset * 3 + 2] - skirtHeight; // put down elevation on the skirt height
5213
+ // Use same TEXCOORDS for skirt vertices
5214
+ newTexcoord0.set(attributes.TEXCOORD_0.value.subarray(edge[0] * 2, edge[0] * 2 + 2), vertex1Offset * 2);
5215
+ newTexcoord0.set(attributes.TEXCOORD_0.value.subarray(edge[1] * 2, edge[1] * 2 + 2), vertex2Offset * 2);
5216
+ // Define new triangles
5217
+ const triangle1Offset = edgeIndex * 2 * 3;
5218
+ newTriangles[triangle1Offset] = edge[0];
5219
+ newTriangles[triangle1Offset + 1] = positionsLength / 3 + vertex2Offset;
5220
+ newTriangles[triangle1Offset + 2] = edge[1];
5221
+ newTriangles[triangle1Offset + 3] = positionsLength / 3 + vertex2Offset;
5222
+ newTriangles[triangle1Offset + 4] = edge[0];
5223
+ newTriangles[triangle1Offset + 5] = positionsLength / 3 + vertex1Offset;
5224
+ }
5225
+
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)
5301
5232
  const { terrainSkirtHeight } = options;
5302
5233
  let mesh;
5303
5234
  switch (options.tesselator) {
5304
5235
  case 'martini':
5305
- mesh = getMartiniTileMesh(meshMaxError, width, terrain);
5236
+ mesh = this.getMartiniTileMesh(meshMaxError, width, terrain);
5306
5237
  break;
5307
5238
  case 'delatin':
5308
- mesh = getDelatinTileMesh(meshMaxError, width, height, terrain);
5239
+ mesh = this.getDelatinTileMesh(meshMaxError, width, height, terrain);
5309
5240
  break;
5310
5241
  default:
5311
- if (width === height && !(height && (width - 1))) {
5312
- // fixme get terrain to separate method
5313
- // terrain = getTerrain(data, width, height, elevationDecoder, 'martini');
5314
- mesh = getMartiniTileMesh(meshMaxError, width, terrain);
5315
- }
5316
- else {
5317
- // fixme get terrain to separate method
5318
- // terrain = getTerrain(data, width, height, elevationDecoder, 'delatin');
5319
- mesh = getDelatinTileMesh(meshMaxError, width, height, terrain);
5320
- }
5242
+ // Intentional: default to Martini for any unspecified or unrecognized tesselator.
5243
+ mesh = this.getMartiniTileMesh(meshMaxError, width, terrain);
5321
5244
  break;
5322
5245
  }
5323
5246
  const { vertices } = mesh;
5324
5247
  let { triangles } = mesh;
5325
- let attributes = getMeshAttributes(vertices, terrain, width, height, input.bounds);
5248
+ let attributes = this.getMeshAttributes(vertices, terrain, width, height, input.bounds);
5326
5249
  // Compute bounding box before adding skirt so that z values are not skewed
5327
5250
  const boundingBox = getMeshBoundingBox(attributes);
5328
5251
  if (terrainSkirtHeight) {
@@ -5344,27 +5267,119 @@ class GeoImage {
5344
5267
  attributes,
5345
5268
  };
5346
5269
  }
5347
- async getBitmap(input, options) {
5270
+ /**
5271
+ * Decodes raw raster data into a Float32Array of elevation values.
5272
+ * Handles channel selection, value scaling, data type validation, and border stitching.
5273
+ */
5274
+ static computeTerrainData(input, options) {
5275
+ const { width, height, rasters } = input;
5348
5276
  const optionsLocal = { ...options };
5349
- let rasters = [];
5350
- let channels;
5351
- let width;
5352
- let height;
5353
- if (typeof (input) === 'string') {
5354
- // TODO not tested
5355
- // input is type of object
5356
- await this.setUrl(input);
5357
- rasters = (await this.data.readRasters());
5358
- channels = rasters.length;
5359
- width = this.data.getWidth();
5360
- height = this.data.getHeight();
5277
+ optionsLocal.useChannelIndex ??= optionsLocal.useChannel == null ? null : optionsLocal.useChannel - 1;
5278
+ // Detect if data is planar (multiple arrays) or interleaved (one array with multiple samples per pixel)
5279
+ const isPlanar = rasters.length > 1;
5280
+ const channel = isPlanar
5281
+ ? (rasters[optionsLocal.useChannelIndex ?? 0] ?? rasters[0])
5282
+ : rasters[0];
5283
+ const terrain = new Float32Array((width === 257 ? width : width + 1) * (height === 257 ? height : height + 1));
5284
+ const samplesPerPixel = isPlanar ? 1 : (channel.length / (width * height));
5285
+ // If planar, we already selected the correct array, so start at index 0.
5286
+ // If interleaved, start at the index of the desired channel.
5287
+ let pixel = isPlanar ? 0 : (optionsLocal.useChannelIndex ?? 0);
5288
+ const isStitched = width === 257;
5289
+ const fallbackValue = options.terrainMinValue ?? 0;
5290
+ for (let y = 0; y < height; y++) {
5291
+ for (let x = 0; x < width; x++) {
5292
+ const multiplier = options.multiplier ?? 1;
5293
+ let elevationValue = (options.noDataValue !== undefined &&
5294
+ options.noDataValue !== null &&
5295
+ channel[pixel] === options.noDataValue)
5296
+ ? fallbackValue
5297
+ : channel[pixel] * multiplier;
5298
+ // Validate that the elevation value is within the valid range for Float32.
5299
+ // Extreme values (like -1.79e308) can become -Infinity when cast, causing WebGL errors.
5300
+ if (Number.isNaN(elevationValue) || elevationValue < -34e37 || elevationValue > 3.4e38) {
5301
+ elevationValue = fallbackValue;
5302
+ }
5303
+ // If stitched (257), fill linearly. If 256, fill with stride for padding.
5304
+ const index = isStitched ? (y * width + x) : (y * (width + 1) + x);
5305
+ terrain[index] = elevationValue;
5306
+ pixel += samplesPerPixel;
5307
+ }
5361
5308
  }
5362
- else {
5363
- rasters = input.rasters;
5364
- channels = rasters.length;
5365
- width = input.width;
5366
- height = input.height;
5309
+ if (!isStitched) {
5310
+ // backfill bottom border
5311
+ for (let i = (width + 1) * width, x = 0; x < width; x++, i++) {
5312
+ terrain[i] = terrain[i - width - 1];
5313
+ }
5314
+ // backfill right border
5315
+ for (let i = height, y = 0; y < height + 1; y++, i += height + 1) {
5316
+ terrain[i] = terrain[i - 1];
5317
+ }
5367
5318
  }
5319
+ return terrain;
5320
+ }
5321
+ static getMartiniTileMesh(meshMaxError, width, terrain) {
5322
+ const gridSize = width === 257 ? 257 : width + 1;
5323
+ const martini = new Martini(gridSize);
5324
+ const tile = martini.createTile(terrain);
5325
+ const { vertices, triangles } = tile.getMesh(meshMaxError);
5326
+ return { vertices, triangles };
5327
+ }
5328
+ static getDelatinTileMesh(meshMaxError, width, height, terrain) {
5329
+ const widthPlus = width === 257 ? 257 : width + 1;
5330
+ const heightPlus = height === 257 ? 257 : height + 1;
5331
+ const tin = new Delatin(terrain, widthPlus, heightPlus);
5332
+ tin.run(meshMaxError);
5333
+ // @ts-expect-error: Delatin instance properties 'coords' and 'triangles' are not explicitly typed in the library port
5334
+ const { coords, triangles } = tin;
5335
+ const vertices = coords;
5336
+ return { vertices, triangles };
5337
+ }
5338
+ static getMeshAttributes(vertices, terrain, width, height, bounds) {
5339
+ const gridSize = width === 257 ? 257 : width + 1;
5340
+ const numOfVerticies = vertices.length / 2;
5341
+ // vec3. x, y in pixels, z in meters
5342
+ const positions = new Float32Array(numOfVerticies * 3);
5343
+ // vec2. 1 to 1 relationship with position. represents the uv on the texture image. 0,0 to 1,1.
5344
+ const texCoords = new Float32Array(numOfVerticies * 2);
5345
+ const [minX, minY, maxX, maxY] = bounds || [0, 0, width, height];
5346
+ // If stitched (257), the spatial extent covers 0..256 pixels, so we divide by 256.
5347
+ // If standard (256), the spatial extent covers 0..256 pixels (with backfill), so we divide by 256.
5348
+ const effectiveWidth = width === 257 ? width - 1 : width;
5349
+ const effectiveHeight = height === 257 ? height - 1 : height;
5350
+ const xScale = (maxX - minX) / effectiveWidth;
5351
+ const yScale = (maxY - minY) / effectiveHeight;
5352
+ for (let i = 0; i < numOfVerticies; i++) {
5353
+ const x = vertices[i * 2];
5354
+ const y = vertices[i * 2 + 1];
5355
+ const pixelIdx = y * gridSize + x;
5356
+ positions[3 * i] = x * xScale + minX;
5357
+ positions[3 * i + 1] = -y * yScale + maxY;
5358
+ positions[3 * i + 2] = terrain[pixelIdx];
5359
+ texCoords[2 * i] = x / effectiveWidth;
5360
+ texCoords[2 * i + 1] = y / effectiveHeight;
5361
+ }
5362
+ return {
5363
+ POSITION: { value: positions, size: 3 },
5364
+ TEXCOORD_0: { value: texCoords, size: 2 },
5365
+ // NORMAL: {}, - optional, but creates the high poly look with lighting
5366
+ };
5367
+ }
5368
+ }
5369
+
5370
+ // DataUtils.ts
5371
+ function scale(num, inMin, inMax, outMin, outMax) {
5372
+ if (inMax === inMin) {
5373
+ return outMin;
5374
+ }
5375
+ return ((num - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
5376
+ }
5377
+
5378
+ class BitmapGenerator {
5379
+ static async generate(input, options) {
5380
+ const optionsLocal = { ...options };
5381
+ const { rasters, width, height } = input;
5382
+ const channels = rasters.length;
5368
5383
  const canvas = document.createElement('canvas');
5369
5384
  canvas.width = width;
5370
5385
  canvas.height = height;
@@ -5375,21 +5390,14 @@ class GeoImage {
5375
5390
  let b;
5376
5391
  let a;
5377
5392
  const size = width * height * 4;
5378
- // const size = width * height;
5379
- // if (!options.noDataValue) {
5380
- // console.log('Missing noData value. Raster might be displayed incorrectly.');
5381
- // }
5382
- optionsLocal.unidentifiedColor = this.getColorFromChromaType(optionsLocal.unidentifiedColor);
5383
- optionsLocal.nullColor = this.getColorFromChromaType(optionsLocal.nullColor);
5384
- optionsLocal.clippedColor = this.getColorFromChromaType(optionsLocal.clippedColor);
5385
- optionsLocal.color = this.getColorFromChromaType(optionsLocal.color);
5386
- optionsLocal.useChannelIndex ??= options.useChannel === null ? null : options.useChannel - 1;
5387
- // console.log(rasters[0])
5388
- /* console.log("raster 0 length: " + rasters[0].length)
5389
- console.log("image width: " + width)
5390
- console.log("channels: " + channels)
5391
- console.log("format: " + rasters[0].length / (width * height))
5392
- */
5393
+ const alpha255 = Math.floor(optionsLocal.alpha * 2.55);
5394
+ optionsLocal.unidentifiedColor = this.getColorFromChromaType(optionsLocal.unidentifiedColor, alpha255);
5395
+ optionsLocal.nullColor = this.getColorFromChromaType(optionsLocal.nullColor, alpha255);
5396
+ optionsLocal.clippedColor = this.getColorFromChromaType(optionsLocal.clippedColor, alpha255);
5397
+ optionsLocal.color = this.getColorFromChromaType(optionsLocal.color, alpha255);
5398
+ optionsLocal.useChannelIndex ??= optionsLocal.useChannel == null ? null : optionsLocal.useChannel - 1;
5399
+ // Derive channel count from data if not provided
5400
+ const numAvailableChannels = optionsLocal.numOfChannels ?? (rasters.length === 1 ? rasters[0].length / (width * height) : rasters.length);
5393
5401
  if (optionsLocal.useChannelIndex == null) {
5394
5402
  if (channels === 1) {
5395
5403
  if (rasters[0].length / (width * height) === 1) {
@@ -5397,17 +5405,13 @@ class GeoImage {
5397
5405
  // AUTO RANGE
5398
5406
  if (optionsLocal.useAutoRange) {
5399
5407
  optionsLocal.colorScaleValueRange = this.getMinMax(channel, optionsLocal);
5400
- // console.log('data min: ' + optionsLocal.rangeMin + ', max: ' + optionsLocal.rangeMax);
5401
5408
  }
5402
5409
  // SINGLE CHANNEL
5403
5410
  const colorData = this.getColorValue(channel, optionsLocal, size);
5404
- colorData.forEach((value, index) => {
5405
- imageData.data[index] = value;
5406
- });
5411
+ imageData.data.set(colorData);
5407
5412
  }
5408
5413
  // RGB values in one channel
5409
5414
  if (rasters[0].length / (width * height) === 3) {
5410
- // console.log("geoImage: " + "RGB 1 array of length: " + rasters[0].length);
5411
5415
  let pixel = 0;
5412
5416
  for (let idx = 0; idx < size; idx += 4) {
5413
5417
  const rgbColor = [rasters[0][pixel], rasters[0][pixel + 1], rasters[0][pixel + 2]];
@@ -5419,7 +5423,6 @@ class GeoImage {
5419
5423
  }
5420
5424
  }
5421
5425
  if (rasters[0].length / (width * height) === 4) {
5422
- // console.log("geoImage: " + "RGBA 1 array");
5423
5426
  rasters[0].forEach((value, index) => {
5424
5427
  imageData.data[index] = value;
5425
5428
  });
@@ -5428,11 +5431,12 @@ class GeoImage {
5428
5431
  if (channels === 3) {
5429
5432
  // RGB
5430
5433
  let pixel = 0;
5434
+ const alphaConst = Math.floor(optionsLocal.alpha * 2.55);
5431
5435
  for (let i = 0; i < size; i += 4) {
5432
5436
  r = rasters[0][pixel];
5433
5437
  g = rasters[1][pixel];
5434
5438
  b = rasters[2][pixel];
5435
- a = Math.floor(optionsLocal.alpha * 2.55);
5439
+ a = alphaConst;
5436
5440
  imageData.data[i] = r;
5437
5441
  imageData.data[i + 1] = g;
5438
5442
  imageData.data[i + 2] = b;
@@ -5443,11 +5447,12 @@ class GeoImage {
5443
5447
  if (channels === 4) {
5444
5448
  // RGBA
5445
5449
  let pixel = 0;
5450
+ const alphaConst = Math.floor(optionsLocal.alpha * 2.55);
5446
5451
  for (let i = 0; i < size; i += 4) {
5447
5452
  r = rasters[0][pixel];
5448
5453
  g = rasters[1][pixel];
5449
5454
  b = rasters[2][pixel];
5450
- a = Math.floor(optionsLocal.alpha * 2.55);
5455
+ a = alphaConst;
5451
5456
  imageData.data[i] = r;
5452
5457
  imageData.data[i + 1] = g;
5453
5458
  imageData.data[i + 2] = b;
@@ -5456,21 +5461,16 @@ class GeoImage {
5456
5461
  }
5457
5462
  }
5458
5463
  }
5459
- else if (optionsLocal.useChannelIndex < optionsLocal.numOfChannels && optionsLocal.useChannelIndex >= 0) {
5460
- let channel = rasters[0];
5461
- if (rasters[optionsLocal.useChannelIndex]) {
5462
- channel = rasters[optionsLocal.useChannelIndex];
5463
- }
5464
+ else if (optionsLocal.useChannelIndex < numAvailableChannels && optionsLocal.useChannelIndex >= 0) {
5465
+ const isInterleaved = rasters.length === 1 && numAvailableChannels > 1;
5466
+ const channel = isInterleaved ? rasters[0] : (rasters[optionsLocal.useChannelIndex] ?? rasters[0]);
5467
+ const samplesPerPixel = isInterleaved ? numAvailableChannels : 1;
5464
5468
  // AUTO RANGE
5465
5469
  if (optionsLocal.useAutoRange) {
5466
- optionsLocal.colorScaleValueRange = this.getMinMax(channel, optionsLocal);
5467
- // console.log('data min: ' + optionsLocal.rangeMin + ', max: ' + optionsLocal.rangeMax);
5470
+ optionsLocal.colorScaleValueRange = this.getMinMax(channel, optionsLocal, samplesPerPixel);
5468
5471
  }
5469
- // const numOfChannels = channel.length / (width * height);
5470
- const colorData = this.getColorValue(channel, optionsLocal, size, optionsLocal.numOfChannels);
5471
- colorData.forEach((value, index) => {
5472
- imageData.data[index] = value;
5473
- });
5472
+ const colorData = this.getColorValue(channel, optionsLocal, size, samplesPerPixel);
5473
+ imageData.data.set(colorData);
5474
5474
  }
5475
5475
  else {
5476
5476
  // if user defined channel does not exist
@@ -5481,92 +5481,162 @@ class GeoImage {
5481
5481
  imageData.data[index] = value;
5482
5482
  });
5483
5483
  }
5484
- // console.timeEnd('bitmap-generated-in');
5484
+ // Optimization: Skip Canvas -> PNG encoding -> Base64 string
5485
+ // Return raw GPU-ready ImageBitmap directly
5486
+ // Note: createImageBitmap(imageData) is cleaner, but using the canvas ensures broad compatibility
5485
5487
  c.putImageData(imageData, 0, 0);
5486
- const imageUrl = canvas.toDataURL('image/png');
5487
- // console.log('Bitmap generated.');
5488
- return imageUrl;
5489
- }
5490
- getMinMax(array, options) {
5491
- let maxValue = options.maxValue ? options.maxValue : Number.MIN_VALUE;
5492
- let minValue = options.minValue ? options.minValue : Number.MAX_VALUE;
5493
- for (let idx = 0; idx < array.length; idx += 1) {
5488
+ return createImageBitmap(canvas);
5489
+ }
5490
+ static getMinMax(array, options, samplesPerPixel = 1) {
5491
+ let maxValue = -Infinity;
5492
+ let minValue = Infinity;
5493
+ let foundValid = false;
5494
+ let pixel = samplesPerPixel === 1 ? 0 : (options.useChannelIndex ?? 0);
5495
+ for (let idx = pixel; idx < array.length; idx += samplesPerPixel) {
5494
5496
  if (options.noDataValue === undefined || array[idx] !== options.noDataValue) {
5495
5497
  if (array[idx] > maxValue)
5496
5498
  maxValue = array[idx];
5497
5499
  if (array[idx] < minValue)
5498
5500
  minValue = array[idx];
5501
+ foundValid = true;
5499
5502
  }
5500
5503
  }
5504
+ if (!foundValid) {
5505
+ return options.colorScaleValueRange || [0, 255];
5506
+ }
5501
5507
  return [minValue, maxValue];
5502
5508
  }
5503
- getColorValue(dataArray, options, arrayLength, numOfChannels = 1) {
5504
- // const rgb = chroma.random().rgb(); // [R, G, B]
5505
- // const randomColor = [...rgb, 120];
5509
+ static getColorValue(dataArray, options, arrayLength, samplesPerPixel = 1) {
5506
5510
  const colorScale = chroma.scale(options.colorScale).domain(options.colorScaleValueRange);
5507
- // channel index is equal to channel number - 1
5508
- let pixel = options.useChannelIndex === null ? 0 : options.useChannelIndex;
5509
- const colorsArray = new Array(arrayLength);
5510
- // if useColorsBasedOnValues is true
5511
- const dataValues = options.colorsBasedOnValues ? options.colorsBasedOnValues.map(([first]) => first) : undefined;
5512
- const colorValues = options.colorsBasedOnValues ? options.colorsBasedOnValues.map(([, second]) => [...chroma(second).rgb(), Math.floor(options.alpha * 2.55)]) : undefined;
5513
- // if useClasses is true
5514
- const colorClasses = options.useColorClasses ? options.colorClasses.map(([color]) => [...chroma(color).rgb(), Math.floor(options.alpha * 2.55)]) : undefined;
5515
- const dataIntervals = options.useColorClasses ? options.colorClasses.map(([, interval]) => interval) : undefined;
5516
- const dataIntervalBounds = options.useColorClasses ? options.colorClasses.map(([, , bounds], index) => {
5511
+ let pixel = samplesPerPixel === 1 ? 0 : (options.useChannelIndex ?? 0);
5512
+ const colorsArray = new Uint8ClampedArray(arrayLength);
5513
+ const cbvInput = options.colorsBasedOnValues ?? [];
5514
+ const classesInput = options.colorClasses ?? [];
5515
+ const optUseColorsBasedOnValues = options.useColorsBasedOnValues && cbvInput.length > 0;
5516
+ const optUseColorClasses = options.useColorClasses && classesInput.length > 0;
5517
+ const dataValues = optUseColorsBasedOnValues ? cbvInput.map(([first]) => first) : [];
5518
+ const colorValues = optUseColorsBasedOnValues ? cbvInput.map(([, second]) => [...chroma(second).rgb(), Math.floor(options.alpha * 2.55)]) : [];
5519
+ const colorClasses = optUseColorClasses ? classesInput.map(([color]) => [...chroma(color).rgb(), Math.floor(options.alpha * 2.55)]) : [];
5520
+ const dataIntervals = optUseColorClasses ? classesInput.map(([, interval]) => interval) : [];
5521
+ const dataIntervalBounds = optUseColorClasses ? classesInput.map(([, , bounds], index) => {
5517
5522
  if (bounds !== undefined)
5518
5523
  return bounds;
5519
- if (index === options.colorClasses.length - 1)
5524
+ if (index === classesInput.length - 1)
5520
5525
  return [true, true];
5521
5526
  return [true, false];
5522
- }) : undefined;
5527
+ }) : [];
5528
+ // Pre-calculate Loop Variables to avoid object lookup in loop
5529
+ const optNoData = options.noDataValue;
5530
+ const optClipLow = options.clipLow;
5531
+ const optClipHigh = options.clipHigh;
5532
+ const optClippedColor = options.clippedColor;
5533
+ const optUseHeatMap = options.useHeatMap;
5534
+ const optUseSingleColor = options.useSingleColor;
5535
+ const optUseDataForOpacity = options.useDataForOpacity;
5536
+ const optColor = options.color;
5537
+ const optUnidentifiedColor = options.unidentifiedColor;
5538
+ const optNullColor = options.nullColor;
5539
+ const optAlpha = Math.floor(options.alpha * 2.55);
5540
+ const rangeMin = options.colorScaleValueRange[0];
5541
+ const rangeMax = options.colorScaleValueRange.slice(-1)[0];
5542
+ // LOOKUP TABLE OPTIMIZATION (for 8-bit data)
5543
+ // If the data is Uint8 (0-255), we can pre-calculate the result for every possible value.
5544
+ const is8Bit = dataArray instanceof Uint8Array || dataArray instanceof Uint8ClampedArray;
5545
+ // The LUT optimization is only applied for 8-bit data when `useDataForOpacity` is false.
5546
+ // `useDataForOpacity` is excluded because it requires the raw data value for
5547
+ // dynamic opacity scaling. All other visualization modes (HeatMap, Categorical,
5548
+ // Classes, Single Color) are pre-calculated into the LUT for maximum performance.
5549
+ if (is8Bit && !optUseDataForOpacity) {
5550
+ // Create LUT: 256 values * 4 channels (RGBA)
5551
+ const lut = new Uint8ClampedArray(256 * 4);
5552
+ for (let i = 0; i < 256; i++) {
5553
+ let r = optNullColor[0], g = optNullColor[1], b = optNullColor[2], a = optNullColor[3];
5554
+ // Logic mirroring the pixel loop
5555
+ if (optNoData === undefined || i !== optNoData) {
5556
+ if ((optClipLow != null && i <= optClipLow) || (optClipHigh != null && i >= optClipHigh)) {
5557
+ [r, g, b, a] = optClippedColor;
5558
+ }
5559
+ else {
5560
+ let c = [r, g, b, a];
5561
+ if (optUseHeatMap) {
5562
+ const rgb = colorScale(i).rgb();
5563
+ c = [rgb[0], rgb[1], rgb[2], optAlpha];
5564
+ }
5565
+ else if (optUseColorsBasedOnValues) {
5566
+ const index = dataValues.indexOf(i);
5567
+ c = (index > -1) ? colorValues[index] : optUnidentifiedColor;
5568
+ }
5569
+ else if (optUseColorClasses) {
5570
+ const index = this.findClassIndex(i, dataIntervals, dataIntervalBounds);
5571
+ c = (index > -1) ? colorClasses[index] : optUnidentifiedColor;
5572
+ }
5573
+ else if (optUseSingleColor) {
5574
+ c = optColor;
5575
+ }
5576
+ [r, g, b, a] = c;
5577
+ }
5578
+ }
5579
+ lut[i * 4] = r;
5580
+ lut[i * 4 + 1] = g;
5581
+ lut[i * 4 + 2] = b;
5582
+ lut[i * 4 + 3] = a;
5583
+ }
5584
+ // Fast Apply Loop
5585
+ let outIdx = 0;
5586
+ const numPixels = arrayLength / 4;
5587
+ for (let i = 0; i < numPixels; i++) {
5588
+ const val = dataArray[pixel];
5589
+ const lutIdx = Math.min(255, Math.max(0, val)) * 4;
5590
+ colorsArray[outIdx++] = lut[lutIdx];
5591
+ colorsArray[outIdx++] = lut[lutIdx + 1];
5592
+ colorsArray[outIdx++] = lut[lutIdx + 2];
5593
+ colorsArray[outIdx++] = lut[lutIdx + 3];
5594
+ pixel += samplesPerPixel;
5595
+ }
5596
+ return colorsArray;
5597
+ }
5598
+ // Standard Loop (Float or non-optimized)
5523
5599
  for (let i = 0; i < arrayLength; i += 4) {
5524
- let pixelColor = options.nullColor;
5525
- // let pixelColor = randomColor;
5526
- // FIXME
5527
- if ((!Number.isNaN(dataArray[pixel])) && (options.noDataValue === undefined || dataArray[pixel] !== options.noDataValue)) {
5528
- if ((options.clipLow != null && dataArray[pixel] <= options.clipLow)
5529
- || (options.clipHigh != null && dataArray[pixel] >= options.clipHigh)) {
5530
- pixelColor = options.clippedColor;
5600
+ let r = optNullColor[0], g = optNullColor[1], b = optNullColor[2], a = optNullColor[3];
5601
+ const val = dataArray[pixel];
5602
+ if ((!Number.isNaN(val)) && (optNoData === undefined || val !== optNoData)) {
5603
+ if ((optClipLow != null && val <= optClipLow) || (optClipHigh != null && val >= optClipHigh)) {
5604
+ [r, g, b, a] = optClippedColor;
5531
5605
  }
5532
5606
  else {
5533
- if (options.useHeatMap) {
5534
- // FIXME
5535
- pixelColor = [...colorScale(dataArray[pixel]).rgb(), Math.floor(options.alpha * 2.55)];
5607
+ let c;
5608
+ if (optUseHeatMap) {
5609
+ const rgb = colorScale(val).rgb();
5610
+ c = [rgb[0], rgb[1], rgb[2], optAlpha];
5536
5611
  }
5537
- if (options.useColorsBasedOnValues) {
5538
- const index = dataValues.indexOf(dataArray[pixel]);
5539
- if (index > -1) {
5540
- pixelColor = colorValues[index];
5541
- }
5542
- else
5543
- pixelColor = options.unidentifiedColor;
5612
+ else if (optUseColorsBasedOnValues) {
5613
+ const index = dataValues.indexOf(val);
5614
+ c = (index > -1) ? colorValues[index] : optUnidentifiedColor;
5544
5615
  }
5545
- if (options.useColorClasses) {
5546
- const index = this.findClassIndex(dataArray[pixel], dataIntervals, dataIntervalBounds);
5547
- if (index > -1) {
5548
- pixelColor = colorClasses[index];
5549
- }
5550
- else
5551
- pixelColor = options.unidentifiedColor;
5616
+ else if (optUseColorClasses) {
5617
+ const index = this.findClassIndex(val, dataIntervals, dataIntervalBounds);
5618
+ c = (index > -1) ? colorClasses[index] : optUnidentifiedColor;
5552
5619
  }
5553
- if (options.useSingleColor) {
5554
- // FIXME - Is this compatible with chroma.color?
5555
- pixelColor = options.color;
5620
+ else if (optUseSingleColor) {
5621
+ c = optColor;
5556
5622
  }
5557
- if (options.useDataForOpacity) {
5558
- pixelColor[3] = this.scale(dataArray[pixel], options.colorScaleValueRange[0], options.colorScaleValueRange.slice(-1)[0], 0, 255);
5623
+ if (c) {
5624
+ [r, g, b, a] = c;
5625
+ }
5626
+ if (optUseDataForOpacity) {
5627
+ a = scale(val, rangeMin, rangeMax, 0, 255);
5559
5628
  }
5560
5629
  }
5561
5630
  }
5562
- // FIXME
5563
- ([colorsArray[i], colorsArray[i + 1], colorsArray[i + 2], colorsArray[i + 3]] = pixelColor);
5564
- pixel += numOfChannels;
5631
+ colorsArray[i] = r;
5632
+ colorsArray[i + 1] = g;
5633
+ colorsArray[i + 2] = b;
5634
+ colorsArray[i + 3] = a;
5635
+ pixel += samplesPerPixel;
5565
5636
  }
5566
5637
  return colorsArray;
5567
5638
  }
5568
- findClassIndex(number, intervals, bounds) {
5569
- // returns index of the first class to which the number belongs
5639
+ static findClassIndex(number, intervals, bounds) {
5570
5640
  for (let idx = 0; idx < intervals.length; idx += 1) {
5571
5641
  const [min, max] = intervals[idx];
5572
5642
  const [includeEqualMin, includeEqualMax] = bounds[idx];
@@ -5577,89 +5647,89 @@ class GeoImage {
5577
5647
  }
5578
5648
  return -1;
5579
5649
  }
5580
- getDefaultColor(size, nullColor) {
5581
- const colorsArray = new Array(size);
5650
+ static getDefaultColor(size, nullColor) {
5651
+ const colorsArray = new Uint8ClampedArray(size);
5582
5652
  for (let i = 0; i < size; i += 4) {
5583
5653
  [colorsArray[i], colorsArray[i + 1], colorsArray[i + 2], colorsArray[i + 3]] = nullColor;
5584
5654
  }
5585
5655
  return colorsArray;
5586
5656
  }
5587
- getColorFromChromaType(colorDefinition) {
5657
+ static getColorFromChromaType(colorDefinition, alpha = 255) {
5588
5658
  if (!Array.isArray(colorDefinition) || colorDefinition.length !== 4) {
5589
- return [...chroma(colorDefinition).rgb(), 255];
5659
+ return [...chroma(colorDefinition).rgb(), alpha];
5590
5660
  }
5591
5661
  return colorDefinition;
5592
5662
  }
5593
- hasPixelsNoData(pixels, noDataValue) {
5663
+ static hasPixelsNoData(pixels, noDataValue) {
5594
5664
  return noDataValue !== undefined && pixels.every((pixel) => pixel === noDataValue);
5595
5665
  }
5596
5666
  }
5597
- //
5598
- //
5599
- //
5600
- /**
5601
- * Get Martini generated vertices and triangles
5602
- *
5603
- * @param {number} meshMaxError threshold for simplifying mesh
5604
- * @param {number} width width of the input data
5605
- * @param {number[] | Float32Array} terrain elevation data
5606
- * @returns {{vertices: Uint16Array, triangles: Uint32Array}} vertices and triangles data
5607
- */
5608
- function getMartiniTileMesh(meshMaxError, width, terrain) {
5609
- const gridSize = width === 257 ? 257 : width + 1;
5610
- const martini = new Martini(gridSize);
5611
- const tile = martini.createTile(terrain);
5612
- const { vertices, triangles } = tile.getMesh(meshMaxError);
5613
- return { vertices, triangles };
5614
- }
5615
- function getMeshAttributes(vertices, terrain, width, height, bounds) {
5616
- const gridSize = width === 257 ? 257 : width + 1;
5617
- const numOfVerticies = vertices.length / 2;
5618
- // vec3. x, y in pixels, z in meters
5619
- const positions = new Float32Array(numOfVerticies * 3);
5620
- // vec2. 1 to 1 relationship with position. represents the uv on the texture image. 0,0 to 1,1.
5621
- const texCoords = new Float32Array(numOfVerticies * 2);
5622
- const [minX, minY, maxX, maxY] = bounds || [0, 0, width, height];
5623
- // If stitched (257), the spatial extent covers 0..256 pixels, so we divide by 256.
5624
- // If standard (256), the spatial extent covers 0..256 pixels (with backfill), so we divide by 256.
5625
- const effectiveWidth = width === 257 ? width - 1 : width;
5626
- const effectiveHeight = height === 257 ? height - 1 : height;
5627
- const xScale = (maxX - minX) / effectiveWidth;
5628
- const yScale = (maxY - minY) / effectiveHeight;
5629
- for (let i = 0; i < numOfVerticies; i++) {
5630
- const x = vertices[i * 2];
5631
- const y = vertices[i * 2 + 1];
5632
- const pixelIdx = y * gridSize + x;
5633
- positions[3 * i + 0] = x * xScale + minX;
5634
- positions[3 * i + 1] = -y * yScale + maxY;
5635
- positions[3 * i + 2] = terrain[pixelIdx];
5636
- texCoords[2 * i + 0] = x / effectiveWidth;
5637
- texCoords[2 * i + 1] = y / effectiveHeight;
5667
+
5668
+ class GeoImage {
5669
+ data;
5670
+ async setUrl(url) {
5671
+ // TODO - not tested
5672
+ const response = await fetch(url);
5673
+ const arrayBuffer = await response.arrayBuffer();
5674
+ const tiff = await fromArrayBuffer(arrayBuffer);
5675
+ const data = await tiff.getImage(0);
5676
+ this.data = data;
5677
+ }
5678
+ async getMap(input, options, meshMaxError) {
5679
+ const mergedOptions = { ...DefaultGeoImageOptions, ...options };
5680
+ switch (mergedOptions.type) {
5681
+ case 'image':
5682
+ return this.getBitmap(input, mergedOptions);
5683
+ case 'terrain':
5684
+ return this.getHeightmap(input, mergedOptions, meshMaxError);
5685
+ default:
5686
+ return null;
5687
+ }
5688
+ }
5689
+ // GetHeightmap uses only "useChannel" and "multiplier" options
5690
+ async getHeightmap(input, options, meshMaxError) {
5691
+ let rasters = [];
5692
+ let width;
5693
+ let height;
5694
+ let bounds;
5695
+ if (typeof (input) === 'string') {
5696
+ // TODO not tested
5697
+ // input is type of object
5698
+ await this.setUrl(input);
5699
+ rasters = (await this.data.readRasters());
5700
+ width = this.data.getWidth();
5701
+ height = this.data.getHeight();
5702
+ bounds = this.data.getBoundingBox();
5703
+ }
5704
+ else {
5705
+ rasters = input.rasters;
5706
+ width = input.width;
5707
+ height = input.height;
5708
+ bounds = input.bounds;
5709
+ }
5710
+ // Delegate to TerrainGenerator
5711
+ return TerrainGenerator.generate({ width, height, rasters, bounds }, options, meshMaxError);
5712
+ }
5713
+ async getBitmap(input, options) {
5714
+ let rasters = [];
5715
+ let width;
5716
+ let height;
5717
+ if (typeof (input) === 'string') {
5718
+ // TODO not tested
5719
+ // input is type of object
5720
+ await this.setUrl(input);
5721
+ rasters = (await this.data.readRasters());
5722
+ width = this.data.getWidth();
5723
+ height = this.data.getHeight();
5724
+ }
5725
+ else {
5726
+ rasters = input.rasters;
5727
+ width = input.width;
5728
+ height = input.height;
5729
+ }
5730
+ // Delegate to BitmapGenerator
5731
+ return BitmapGenerator.generate({ width, height, rasters }, options);
5638
5732
  }
5639
- return {
5640
- POSITION: { value: positions, size: 3 },
5641
- TEXCOORD_0: { value: texCoords, size: 2 },
5642
- // NORMAL: {}, - optional, but creates the high poly look with lighting
5643
- };
5644
- }
5645
- /**
5646
- * Get Delatin generated vertices and triangles
5647
- *
5648
- * @param {number} meshMaxError threshold for simplifying mesh
5649
- * @param {number} width width of the input data array
5650
- * @param {number} height height of the input data array
5651
- * @param {number[] | Float32Array} terrain elevation data
5652
- * @returns {{vertices: number[], triangles: number[]}} vertices and triangles data
5653
- */
5654
- function getDelatinTileMesh(meshMaxError, width, height, terrain) {
5655
- const widthPlus = width === 257 ? 257 : width + 1;
5656
- const heightPlus = height === 257 ? 257 : height + 1;
5657
- const tin = new Delatin(terrain, widthPlus, heightPlus);
5658
- tin.run(meshMaxError);
5659
- // @ts-expect-error: Delatin instance properties 'coords' and 'triangles' are not explicitly typed in the library port
5660
- const { coords, triangles } = tin;
5661
- const vertices = coords;
5662
- return { vertices, triangles };
5663
5733
  }
5664
5734
 
5665
5735
  const EARTH_CIRCUMFERENCE = 2 * Math.PI * 6378137;
@@ -5684,19 +5754,31 @@ class CogTiles {
5684
5754
  }
5685
5755
  async initializeCog(url) {
5686
5756
  if (this.cog)
5687
- return; // Prevent re-initialization
5688
- this.cog = await fromUrl(url);
5689
- const image = await this.cog.getImage();
5690
- const fileDirectory = image.fileDirectory;
5691
- this.cogOrigin = image.getOrigin();
5692
- this.options.noDataValue ??= await this.getNoDataValue(image);
5693
- this.options.format ??= await this.getDataTypeFromTags(fileDirectory);
5694
- this.options.numOfChannels = fileDirectory.getValue('SamplesPerPixel');
5695
- this.options.planarConfig = fileDirectory.getValue('PlanarConfiguration');
5696
- [this.cogZoomLookup, this.cogResolutionLookup] = await this.buildCogZoomResolutionLookup(this.cog);
5697
- this.tileSize = image.getTileWidth();
5698
- this.zoomRange = this.calculateZoomRange(this.tileSize, image.getResolution()[0], await this.cog.getImageCount());
5699
- this.bounds = this.calculateBoundsAsLatLon(image.getBoundingBox());
5757
+ return; // Prevent re-initialization on the same instance
5758
+ try {
5759
+ this.cog = await fromUrl(url);
5760
+ const image = await this.cog.getImage();
5761
+ const fileDirectory = image.fileDirectory;
5762
+ this.cogOrigin = image.getOrigin();
5763
+ this.options.noDataValue ??= await this.getNoDataValue(image);
5764
+ this.options.format ??= await this.getDataTypeFromTags(fileDirectory);
5765
+ this.options.numOfChannels = fileDirectory.getValue('SamplesPerPixel');
5766
+ this.options.planarConfig = fileDirectory.getValue('PlanarConfiguration');
5767
+ [this.cogZoomLookup, this.cogResolutionLookup] = await this.buildCogZoomResolutionLookup(this.cog);
5768
+ this.tileSize = image.getTileWidth();
5769
+ // 1. Validation: Ensure the image is tiled
5770
+ if (!this.tileSize || !image.getTileHeight()) {
5771
+ throw new Error('GeoTIFF Error: The provided image is not tiled. '
5772
+ + 'Please use "rio cogeo create --web-optimized" to fix this.');
5773
+ }
5774
+ this.zoomRange = this.calculateZoomRange(this.tileSize, image.getResolution()[0], await this.cog.getImageCount());
5775
+ this.bounds = this.calculateBoundsAsLatLon(image.getBoundingBox());
5776
+ }
5777
+ catch (error) {
5778
+ /* eslint-disable no-console */
5779
+ console.error(`[CogTiles] Failed to initialize COG from ${url}:`, error);
5780
+ throw error;
5781
+ }
5700
5782
  }
5701
5783
  getZoomRange() {
5702
5784
  return this.zoomRange;
@@ -5802,13 +5884,6 @@ class CogTiles {
5802
5884
  async getTileFromImage(tileX, tileY, zoom, fetchSize) {
5803
5885
  const imageIndex = this.getImageIndexForZoomLevel(zoom);
5804
5886
  const targetImage = await this.cog.getImage(imageIndex);
5805
- // 1. Validation: Ensure the image is tiled
5806
- const tileWidth = targetImage.getTileWidth();
5807
- const tileHeight = targetImage.getTileHeight();
5808
- if (!tileWidth || !tileHeight) {
5809
- throw new Error('GeoTIFF Error: The provided image is not tiled. '
5810
- + 'Please use "rio cogeo create --web-optimized" to fix this.');
5811
- }
5812
5887
  // --- STEP 1: CALCULATE BOUNDS IN METERS ---
5813
5888
  // 2. Get COG Metadata (image = COG)
5814
5889
  const imageResolution = this.cogResolutionLookup[imageIndex];
@@ -5863,16 +5938,14 @@ class CogTiles {
5863
5938
  // If the tile is hanging off the edge, we need to manually reconstruct it.
5864
5939
  // We strictly compare against FETCH_SIZE because that is our target buffer dimension.
5865
5940
  if (missingLeft > 0 || missingTop > 0 || readWidth < FETCH_SIZE || readHeight < FETCH_SIZE) {
5866
- /// Initialize a temporary buffer for a single band (filled with NoData)
5867
- // We will reuse this buffer for each band to save memory allocations.
5868
- const tileBuffer = this.createTileBuffer(this.options.format, FETCH_SIZE);
5869
- tileBuffer.fill(this.options.noDataValue);
5941
+ const numChannels = this.options.numOfChannels || 1;
5942
+ // Initialize with a TypedArray of the full target size and correct data type
5943
+ const validImageData = this.createTileBuffer(this.options.format, FETCH_SIZE, numChannels);
5944
+ if (this.options.noDataValue !== undefined) {
5945
+ validImageData.fill(this.options.noDataValue);
5946
+ }
5870
5947
  // if the valid window is smaller than the tile size, it gets the image size width and height, thus validRasterData.width must be used as below
5871
5948
  const validRasterData = await targetImage.readRasters({ window });
5872
- // FOR MULTI-BAND - the result is one array with sequentially typed bands, firstly all data for the band 0, then for band 1
5873
- // I think this is less practical then the commented solution above, but I do it so it works with the code in GeoImage.ts in deck.gl-geoimage in function getColorValue.
5874
- const validImageData = Array(validRasterData.length * validRasterData[0].length);
5875
- validImageData.fill(this.options.noDataValue);
5876
5949
  // Place the valid pixel data into the tile buffer.
5877
5950
  for (let band = 0; band < validRasterData.length; band += 1) {
5878
5951
  // We must reset the buffer for each band, otherwise data from previous band persists in padding areas
@@ -5886,7 +5959,6 @@ class CogTiles {
5886
5959
  const srcRowOffset = row * validRasterData.width;
5887
5960
  for (let col = 0; col < readWidth; col += 1) {
5888
5961
  // Compute the destination position in the tile buffer.
5889
- // We shift by the number of missing pixels (if any) at the top/left.
5890
5962
  const destCol = missingLeft + col;
5891
5963
  // Bounds Check: Ensure we don't write outside the allocated buffer
5892
5964
  if (destRow < FETCH_SIZE && destCol < FETCH_SIZE) {
@@ -5899,7 +5971,7 @@ class CogTiles {
5899
5971
  }
5900
5972
  }
5901
5973
  for (let i = 0; i < tileBuffer.length; i += 1) {
5902
- validImageData[i * this.options.numOfChannels + band] = tileBuffer[i];
5974
+ validImageData[i * numChannels + band] = tileBuffer[i];
5903
5975
  }
5904
5976
  }
5905
5977
  return [validImageData];
@@ -6017,10 +6089,11 @@ class CogTiles {
6017
6089
  *
6018
6090
  * @param {string} dataType - A string specifying the data type (e.g., "Int32", "Float64", "UInt16", etc.).
6019
6091
  * @param {number} tileSize - The width/height of the square tile.
6020
- * @returns {TypedArray} A typed array buffer of length tileSize * tileSize.
6092
+ * @param {number} multiplier - Optional multiplier for interleaved buffers (e.g., numChannels).
6093
+ * @returns {TypedArray} A typed array buffer of length (tileSize * tileSize * multiplier).
6021
6094
  */
6022
- createTileBuffer(dataType, tileSize) {
6023
- const length = tileSize * tileSize;
6095
+ createTileBuffer(dataType, tileSize, multiplier = 1) {
6096
+ const length = tileSize * tileSize * multiplier;
6024
6097
  switch (dataType) {
6025
6098
  case 'UInt8':
6026
6099
  return new Uint8Array(length);
@@ -6135,12 +6208,10 @@ class CogBitmapLayer extends CompositeLayer {
6135
6208
  || props.bounds !== oldProps.bounds;
6136
6209
  if (!this.state.isTiled && shouldReload) ;
6137
6210
  // Update the useChannel option for bitmapCogTiles when cogBitmapOptions.useChannel changes.
6138
- // This ensures that the correct channel is used for rendering, but directly modifying the state
6139
- // object in this way is not ideal and may need refactoring in the future to follow a more
6140
- // declarative state management approach. Consider revisiting this if additional properties
6141
- // need to be synchronized or if the state structure changes.
6142
- if (props?.cogBitmapOptions?.useChannel && (props.cogBitmapOptions?.useChannel !== oldProps.cogBitmapOptions?.useChannel)) {
6211
+ if (props?.cogBitmapOptions?.useChannel !== oldProps.cogBitmapOptions?.useChannel) {
6143
6212
  this.state.bitmapCogTiles.options.useChannel = props.cogBitmapOptions.useChannel;
6213
+ // Trigger a refresh of the tiles
6214
+ this.state.bitmapCogTiles.options.useChannelIndex = null; // Clear cached index
6144
6215
  }
6145
6216
  if (props.workerUrl) {
6146
6217
  log.removed('workerUrl', 'loadOptions.terrain.workerUrl')();
@@ -6318,6 +6389,12 @@ class CogTerrainLayer extends CompositeLayer {
6318
6389
  || props.elevationDecoder !== oldProps.elevationDecoder
6319
6390
  || props.bounds !== oldProps.bounds;
6320
6391
  if (!this.state.isTiled && shouldReload) ;
6392
+ // Update the useChannel option for terrainCogTiles when terrainOptions.useChannel changes.
6393
+ if (props?.terrainOptions?.useChannel !== oldProps.terrainOptions?.useChannel) {
6394
+ this.state.terrainCogTiles.options.useChannel = props.terrainOptions.useChannel;
6395
+ // Trigger a refresh of the tiles
6396
+ this.state.terrainCogTiles.options.useChannelIndex = null; // Clear cached index
6397
+ }
6321
6398
  if (props.workerUrl) {
6322
6399
  log.removed('workerUrl', 'loadOptions.terrain.workerUrl')();
6323
6400
  }
@@ -6450,25 +6527,6 @@ class CogTerrainLayer extends CompositeLayer {
6450
6527
  refinementStrategy,
6451
6528
  });
6452
6529
  }
6453
- // if (!elevationData) {
6454
- // return null;
6455
- // }
6456
- // const SubLayerClass = this.getSubLayerClass('mesh', SimpleMeshLayer);
6457
- // return new SubLayerClass(
6458
- // this.getSubLayerProps({
6459
- // id: 'mesh',
6460
- // }),
6461
- // {
6462
- // data: DUMMY_DATA,
6463
- // mesh: this.state.terrain,
6464
- // texture,
6465
- // _instanced: false,
6466
- // getPosition: (d) => [0, 0, 0],
6467
- // getColor: color,
6468
- // material,
6469
- // wireframe,
6470
- // },
6471
- // );
6472
6530
  }
6473
6531
  }
6474
6532