@gisatcz/deckgl-geolib 2.5.0-dev.1 → 2.5.0-dev.3

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
@@ -4872,11 +4872,12 @@ const DefaultGeoImageOptions = {
4872
4872
  useChannelIndex: null,
4873
4873
  noDataValue: undefined,
4874
4874
  multiplier: 1.0,
4875
+ verticalExaggeration: 1.0,
4875
4876
  numOfChannels: undefined,
4876
4877
  planarConfig: undefined,
4877
4878
  // --- Mesh generation (terrain only) ---
4878
4879
  tesselator: 'martini',
4879
- terrainColor: [133, 133, 133, 255],
4880
+ terrainColor: [200, 200, 200, 255],
4880
4881
  terrainSkirtHeight: 100,
4881
4882
  // Default fallback for invalid/nodata elevations. Should be configured based on the dataset's actual range.
4882
4883
  terrainMinValue: 0,
@@ -5546,25 +5547,31 @@ function addSkirt(attributes, triangles, skirtHeight, outsideIndices) {
5546
5547
  * @returns {number[][]} - outside edges data
5547
5548
  */
5548
5549
  function getOutsideEdgesFromTriangles(triangles) {
5549
- const edges = [];
5550
- for (let i = 0; i < triangles.length; i += 3) {
5551
- edges.push([triangles[i], triangles[i + 1]]);
5552
- edges.push([triangles[i + 1], triangles[i + 2]]);
5553
- edges.push([triangles[i + 2], triangles[i]]);
5554
- }
5555
- edges.sort((a, b) => Math.min(...a) - Math.min(...b) || Math.max(...a) - Math.max(...b));
5556
- const outsideEdges = [];
5557
- let index = 0;
5558
- while (index < edges.length) {
5559
- if (edges[index][0] === edges[index + 1]?.[1] && edges[index][1] === edges[index + 1]?.[0]) {
5560
- index += 2;
5550
+ // Use integer keys instead of strings: min * 70000 + max is collision-free
5551
+ // for any grid 257×257 (66,049 vertices < 70,000)
5552
+ const edgeMap = new Map();
5553
+ const processEdge = (a, b) => {
5554
+ const min = Math.min(a, b);
5555
+ const max = Math.max(a, b);
5556
+ // Integer key: no string allocation per edge
5557
+ const key = min * 70000 + max;
5558
+ if (edgeMap.has(key)) {
5559
+ edgeMap.delete(key); // Interior edge, remove
5561
5560
  }
5562
5561
  else {
5563
- outsideEdges.push(edges[index]);
5564
- index++;
5562
+ edgeMap.set(key, [a, b]);
5565
5563
  }
5564
+ };
5565
+ for (let i = 0; i < triangles.length; i += 3) {
5566
+ const v0 = triangles[i];
5567
+ const v1 = triangles[i + 1];
5568
+ const v2 = triangles[i + 2];
5569
+ // Process each edge inline — no temporary array allocation per triangle
5570
+ processEdge(v0, v1);
5571
+ processEdge(v1, v2);
5572
+ processEdge(v2, v0);
5566
5573
  }
5567
- return outsideEdges;
5574
+ return Array.from(edgeMap.values());
5568
5575
  }
5569
5576
  /**
5570
5577
  * Get geometry edges that located on a border of the mesh
@@ -5615,6 +5622,22 @@ class BitmapGenerator {
5615
5622
  * Key: colorScale config + range, Value: pre-computed RGBA LUT
5616
5623
  */
5617
5624
  static _swissColorLUTCache = new Map();
5625
+ /**
5626
+ * Cache for 8-bit (256-entry) color LUTs.
5627
+ * Shared process-wide across all tiles and datasets when options are fixed (i.e. !useAutoRange).
5628
+ * Key: serialised coloring options, Value: pre-computed 256×RGBA LUT
5629
+ */
5630
+ static _8bitLUTCache = new Map();
5631
+ /**
5632
+ * Cache for float/16-bit (1024-entry) heatmap LUTs.
5633
+ * Shared process-wide across all tiles and datasets when !useAutoRange.
5634
+ * Key: serialised coloring options + range, Value: pre-computed 1024×RGBA LUT
5635
+ */
5636
+ static _floatLUTCache = new Map();
5637
+ /** Build a cache key that captures all options affecting LUT colour output. */
5638
+ static getLUTCacheKey(options, rangeMin, rangeMax, optAlpha) {
5639
+ return `${rangeMin}_${rangeMax}_${optAlpha}_${JSON.stringify(options.colorScale)}_${options.useSingleColor}_${JSON.stringify(options.color)}_${options.useColorClasses}_${JSON.stringify(options.colorClasses)}_${options.useColorsBasedOnValues}_${JSON.stringify(options.colorsBasedOnValues)}_${options.useHeatMap}_${options.clipLow ?? ''}_${options.clipHigh ?? ''}_${JSON.stringify(options.clippedColor)}_${JSON.stringify(options.nullColor)}_${JSON.stringify(options.unidentifiedColor)}`;
5640
+ }
5618
5641
  /**
5619
5642
  * Main entry point: Generates an ImageBitmap from raw raster data.
5620
5643
  */
@@ -5795,17 +5818,25 @@ class BitmapGenerator {
5795
5818
  return colorsArray;
5796
5819
  }
5797
5820
  // 2. 8-BIT COMPREHENSIVE LUT
5798
- // Single-band 8-bit (grayscale or indexed): use LUT for fast mapping
5821
+ // Single-band 8-bit (grayscale or indexed): use LUT for fast mapping.
5822
+ // The LUT covers all 256 possible values and is fixed for a given set of coloring options,
5823
+ // so cache it across tiles (skip cache only when useAutoRange recomputes the range per tile).
5799
5824
  if (is8Bit && !options.useDataForOpacity) {
5800
- const lut = new Uint8ClampedArray(256 * 4);
5801
- for (let i = 0; i < 256; i++) {
5802
- if ((options.clipLow != null && i <= options.clipLow) ||
5803
- (options.clipHigh != null && i >= options.clipHigh)) {
5804
- lut.set(options.clippedColor, i * 4);
5805
- }
5806
- else {
5807
- lut.set(this.calculateSingleColor(i, colorScale, options, optAlpha), i * 4);
5825
+ const cacheKey = !options.useAutoRange ? this.getLUTCacheKey(options, rangeMin, rangeMax, optAlpha) : null;
5826
+ let lut = cacheKey ? (this._8bitLUTCache.get(cacheKey) ?? null) : null;
5827
+ if (!lut) {
5828
+ lut = new Uint8ClampedArray(256 * 4);
5829
+ for (let i = 0; i < 256; i++) {
5830
+ if ((options.clipLow != null && i <= options.clipLow) ||
5831
+ (options.clipHigh != null && i >= options.clipHigh)) {
5832
+ lut.set(options.clippedColor, i * 4);
5833
+ }
5834
+ else {
5835
+ lut.set(this.calculateSingleColor(i, colorScale, options, optAlpha), i * 4);
5836
+ }
5808
5837
  }
5838
+ if (cacheKey)
5839
+ this._8bitLUTCache.set(cacheKey, lut);
5809
5840
  }
5810
5841
  for (let i = 0, sampleIndex = (options.useChannelIndex ?? 0); i < arrayLength; i += 4, sampleIndex += samplesPerPixel) {
5811
5842
  const lutIdx = primaryBuffer[sampleIndex] * 4;
@@ -5817,23 +5848,32 @@ class BitmapGenerator {
5817
5848
  return colorsArray;
5818
5849
  }
5819
5850
  // 3. FLOAT / 16-BIT LUT (HEATMAP ONLY)
5820
- if (isFloatOrWide && options.useHeatMap && !options.useDataForOpacity) {
5851
+ // Guard: only activate when heatmap is the highest-priority active mode.
5852
+ // If a more specific mode (useSingleColor, useColorClasses, useColorsBasedOnValues) is set,
5853
+ // fall through to the general loop so calculateSingleColor can honour the priority chain.
5854
+ if (isFloatOrWide && options.useHeatMap && !options.useSingleColor && !options.useColorClasses && !options.useColorsBasedOnValues && !options.useDataForOpacity) {
5821
5855
  const LUT_SIZE = 1024;
5822
- const lut = new Uint8ClampedArray(LUT_SIZE * 4);
5823
5856
  const rangeSpan = (rangeMax - rangeMin) || 1;
5824
- for (let i = 0; i < LUT_SIZE; i++) {
5825
- const domainVal = rangeMin + (i / (LUT_SIZE - 1)) * rangeSpan;
5826
- if ((options.clipLow != null && domainVal <= options.clipLow) ||
5827
- (options.clipHigh != null && domainVal >= options.clipHigh)) {
5828
- lut.set(options.clippedColor, i * 4);
5829
- }
5830
- else {
5831
- const rgb = colorScale(domainVal).rgb();
5832
- lut[i * 4] = rgb[0];
5833
- lut[i * 4 + 1] = rgb[1];
5834
- lut[i * 4 + 2] = rgb[2];
5835
- lut[i * 4 + 3] = optAlpha;
5857
+ const cacheKey = !options.useAutoRange ? this.getLUTCacheKey(options, rangeMin, rangeMax, optAlpha) : null;
5858
+ let lut = cacheKey ? (this._floatLUTCache.get(cacheKey) ?? null) : null;
5859
+ if (!lut) {
5860
+ lut = new Uint8ClampedArray(LUT_SIZE * 4);
5861
+ for (let i = 0; i < LUT_SIZE; i++) {
5862
+ const domainVal = rangeMin + (i / (LUT_SIZE - 1)) * rangeSpan;
5863
+ if ((options.clipLow != null && domainVal <= options.clipLow) ||
5864
+ (options.clipHigh != null && domainVal >= options.clipHigh)) {
5865
+ lut.set(options.clippedColor, i * 4);
5866
+ }
5867
+ else {
5868
+ const rgb = colorScale(domainVal).rgb();
5869
+ lut[i * 4] = rgb[0];
5870
+ lut[i * 4 + 1] = rgb[1];
5871
+ lut[i * 4 + 2] = rgb[2];
5872
+ lut[i * 4 + 3] = optAlpha;
5873
+ }
5836
5874
  }
5875
+ if (cacheKey)
5876
+ this._floatLUTCache.set(cacheKey, lut);
5837
5877
  }
5838
5878
  for (let i = 0, sampleIndex = (options.useChannelIndex ?? 0); i < arrayLength; i += 4, sampleIndex += samplesPerPixel) {
5839
5879
  const val = primaryBuffer[sampleIndex];
@@ -6249,7 +6289,7 @@ class TerrainGenerator {
6249
6289
  const meshWidth = isKernel ? 257 : width;
6250
6290
  const meshHeight = isKernel ? 257 : height;
6251
6291
  // 2. Tesselate (Generate Mesh)
6252
- const { terrainSkirtHeight } = options;
6292
+ const { terrainSkirtHeight, verticalExaggeration = 1.0 } = options;
6253
6293
  let mesh;
6254
6294
  switch (options.tesselator) {
6255
6295
  case 'martini':
@@ -6265,13 +6305,17 @@ class TerrainGenerator {
6265
6305
  }
6266
6306
  const { vertices } = mesh;
6267
6307
  let { triangles } = mesh;
6268
- let attributes = this.getMeshAttributes(vertices, meshTerrain, meshWidth, meshHeight, input.bounds);
6308
+ let attributes = this.getMeshAttributes(vertices, meshTerrain, meshWidth, meshHeight, input.bounds, verticalExaggeration);
6269
6309
  // Compute bounding box before adding skirt so that z values are not skewed
6270
6310
  const boundingBox = getMeshBoundingBox(attributes);
6271
6311
  if (terrainSkirtHeight) {
6272
- const { attributes: newAttributes, triangles: newTriangles } = addSkirt(attributes, triangles, terrainSkirtHeight);
6273
- attributes = newAttributes;
6274
- triangles = newTriangles;
6312
+ const scaledSkirtHeight = terrainSkirtHeight * verticalExaggeration;
6313
+ // Skip skirt generation if scaled height is zero (e.g., verticalExaggeration = 0)
6314
+ if (scaledSkirtHeight > 0) {
6315
+ const { attributes: newAttributes, triangles: newTriangles } = addSkirt(attributes, triangles, scaledSkirtHeight);
6316
+ attributes = newAttributes;
6317
+ triangles = newTriangles;
6318
+ }
6275
6319
  }
6276
6320
  const map = {
6277
6321
  // Data return by this loader implementation
@@ -6467,10 +6511,10 @@ class TerrainGenerator {
6467
6511
  const vertices = coords;
6468
6512
  return { vertices, triangles };
6469
6513
  }
6470
- static getMeshAttributes(vertices, terrain, width, height, bounds) {
6514
+ static getMeshAttributes(vertices, terrain, width, height, bounds, verticalExaggeration = 1.0) {
6471
6515
  const gridSize = width === 257 ? 257 : width + 1;
6472
6516
  const numOfVerticies = vertices.length / 2;
6473
- // vec3. x, y in pixels, z in meters
6517
+ // vec3. x, y in pixels, z in meters (scaled by verticalExaggeration)
6474
6518
  const positions = new Float32Array(numOfVerticies * 3);
6475
6519
  // vec2. 1 to 1 relationship with position. represents the uv on the texture image. 0,0 to 1,1.
6476
6520
  const texCoords = new Float32Array(numOfVerticies * 2);
@@ -6487,7 +6531,7 @@ class TerrainGenerator {
6487
6531
  const pixelIdx = y * gridSize + x;
6488
6532
  positions[3 * i] = x * xScale + minX;
6489
6533
  positions[3 * i + 1] = -y * yScale + maxY;
6490
- positions[3 * i + 2] = terrain[pixelIdx];
6534
+ positions[3 * i + 2] = terrain[pixelIdx] * verticalExaggeration;
6491
6535
  texCoords[2 * i] = x / effectiveWidth;
6492
6536
  texCoords[2 * i + 1] = y / effectiveHeight;
6493
6537
  }
@@ -6510,7 +6554,7 @@ class GeoImage {
6510
6554
  this.data = data;
6511
6555
  }
6512
6556
  async getMap(input, options, meshMaxError) {
6513
- const mergedOptions = { ...DefaultGeoImageOptions, ...options };
6557
+ const mergedOptions = GeoImage.resolveVisualizationMode({ ...DefaultGeoImageOptions, ...options }, options);
6514
6558
  switch (mergedOptions.type) {
6515
6559
  case 'image':
6516
6560
  return this.getBitmap(input, mergedOptions);
@@ -6520,6 +6564,56 @@ class GeoImage {
6520
6564
  return null;
6521
6565
  }
6522
6566
  }
6567
+ /**
6568
+ * Resolves the active visualization (coloring) mode after merging user options with defaults.
6569
+ *
6570
+ * Solves three key issues:
6571
+ *
6572
+ * 1. **Mutual exclusivity**: `DefaultGeoImageOptions` sets `useHeatMap: true`. If a user
6573
+ * explicitly enables a coloring mode without explicitly setting `useHeatMap: false`,
6574
+ * enforce that only the explicitly-enabled modes are active (all others forced to false).
6575
+ * This prevents the default `useHeatMap: true` from interfering with user-chosen modes.
6576
+ *
6577
+ * 2. **Bitmap default**: for `type === 'image'` with no user-specified coloring mode,
6578
+ * keep `useHeatMap: true` from defaults. This provides sensible data-driven visualization.
6579
+ *
6580
+ * 3. **Terrain default**: for `type === 'terrain'` with no user-specified coloring mode and
6581
+ * no kernel-texture mode (`useSwissRelief` / `useSlope` / `useHillshade`), enable
6582
+ * `useSingleColor` with `color = terrainColor`. This renders the mesh in the documented
6583
+ * default colour (grey) without a data-driven texture overlay. When a kernel mode IS
6584
+ * present but no coloring mode is specified, keep `useHeatMap: true` so the kernel output
6585
+ * is still colourised.
6586
+ */
6587
+ static resolveVisualizationMode(mergedOptions, userOptions) {
6588
+ const coloringModes = ['useSingleColor', 'useColorClasses', 'useColorsBasedOnValues', 'useHeatMap'];
6589
+ const userExplicitColoringModes = coloringModes.filter(m => userOptions[m] === true);
6590
+ const resolved = { ...mergedOptions };
6591
+ if (userExplicitColoringModes.length > 0) {
6592
+ // Enforce mutual exclusivity: disable all coloring modes, then enable only those
6593
+ // explicitly set by the user. This prevents the default useHeatMap from interfering.
6594
+ for (const mode of coloringModes) {
6595
+ resolved[mode] = false;
6596
+ }
6597
+ for (const mode of userExplicitColoringModes) {
6598
+ resolved[mode] = true;
6599
+ }
6600
+ }
6601
+ else if (mergedOptions.type === 'terrain') {
6602
+ // Terrain with no explicit coloring mode.
6603
+ const hasKernelMode = userOptions.useSwissRelief || userOptions.useSlope || userOptions.useHillshade;
6604
+ if (!hasKernelMode) {
6605
+ // No kernel mode: enable useSingleColor with terrainColor as the default.
6606
+ // This renders the mesh in the documented colour without a data-driven texture.
6607
+ resolved.useHeatMap = false;
6608
+ resolved.useSingleColor = true;
6609
+ resolved.color = mergedOptions.terrainColor;
6610
+ }
6611
+ // When a kernel mode is present without an explicit coloring mode, keep
6612
+ // useHeatMap: true from defaults so the kernel output is colourised.
6613
+ }
6614
+ // For 'image' with no explicit coloring mode: keep useHeatMap: true from DefaultGeoImageOptions.
6615
+ return resolved;
6616
+ }
6523
6617
  // GetHeightmap uses only "useChannel" and "multiplier" options
6524
6618
  async getHeightmap(input, options, meshMaxError) {
6525
6619
  let rasters = [];