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