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