maplibre-gl-lidar 0.6.1 → 0.7.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.
Files changed (45) hide show
  1. package/dist/{LidarLayerAdapter-BW2Hj7QW.cjs → LidarLayerAdapter-eh59KEMT.cjs} +1454 -169
  2. package/dist/LidarLayerAdapter-eh59KEMT.cjs.map +1 -0
  3. package/dist/{LidarLayerAdapter-BmGNLzm8.js → LidarLayerAdapter-kyyfQw05.js} +1463 -178
  4. package/dist/LidarLayerAdapter-kyyfQw05.js.map +1 -0
  5. package/dist/index.cjs +5 -1
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.mjs +14 -10
  8. package/dist/maplibre-gl-lidar.css +119 -0
  9. package/dist/react.cjs +8 -1
  10. package/dist/react.cjs.map +1 -1
  11. package/dist/react.mjs +10 -3
  12. package/dist/react.mjs.map +1 -1
  13. package/dist/types/index.d.ts +2 -1
  14. package/dist/types/index.d.ts.map +1 -1
  15. package/dist/types/lib/colorizers/ColorScheme.d.ts +75 -6
  16. package/dist/types/lib/colorizers/ColorScheme.d.ts.map +1 -1
  17. package/dist/types/lib/colorizers/Colormaps.d.ts +25 -0
  18. package/dist/types/lib/colorizers/Colormaps.d.ts.map +1 -0
  19. package/dist/types/lib/core/LidarControl.d.ts +43 -1
  20. package/dist/types/lib/core/LidarControl.d.ts.map +1 -1
  21. package/dist/types/lib/core/types.d.ts +46 -0
  22. package/dist/types/lib/core/types.d.ts.map +1 -1
  23. package/dist/types/lib/gui/Colorbar.d.ts +86 -0
  24. package/dist/types/lib/gui/Colorbar.d.ts.map +1 -0
  25. package/dist/types/lib/gui/DualRangeSlider.d.ts +9 -0
  26. package/dist/types/lib/gui/DualRangeSlider.d.ts.map +1 -1
  27. package/dist/types/lib/gui/PanelBuilder.d.ts +31 -5
  28. package/dist/types/lib/gui/PanelBuilder.d.ts.map +1 -1
  29. package/dist/types/lib/gui/PercentileRangeControl.d.ts +113 -0
  30. package/dist/types/lib/gui/PercentileRangeControl.d.ts.map +1 -0
  31. package/dist/types/lib/gui/index.d.ts +4 -0
  32. package/dist/types/lib/gui/index.d.ts.map +1 -1
  33. package/dist/types/lib/hooks/useLidarState.d.ts.map +1 -1
  34. package/dist/types/lib/layers/PointCloudManager.d.ts +22 -1
  35. package/dist/types/lib/layers/PointCloudManager.d.ts.map +1 -1
  36. package/dist/types/lib/layers/types.d.ts +11 -1
  37. package/dist/types/lib/layers/types.d.ts.map +1 -1
  38. package/dist/types/lib/loaders/CopcStreamingLoader.d.ts.map +1 -1
  39. package/dist/types/lib/loaders/EptStreamingLoader.d.ts +44 -0
  40. package/dist/types/lib/loaders/EptStreamingLoader.d.ts.map +1 -1
  41. package/dist/types/lib/loaders/streaming-types.d.ts +6 -0
  42. package/dist/types/lib/loaders/streaming-types.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/dist/LidarLayerAdapter-BW2Hj7QW.cjs.map +0 -1
  45. package/dist/LidarLayerAdapter-BmGNLzm8.js.map +0 -1
@@ -34221,6 +34221,7 @@ class PointCloudLoader {
34221
34221
  };
34222
34222
  }
34223
34223
  }
34224
+ proj4.defs("EPSG:2180", "+proj=tmerc +lat_0=0 +lon_0=19 +k=0.9993 +x_0=500000 +y_0=-5300000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs");
34224
34225
  function createBufferGetter(buffer) {
34225
34226
  const uint8 = new Uint8Array(buffer);
34226
34227
  return async (begin, end) => {
@@ -34332,12 +34333,40 @@ function getVerticalUnitConversionFactor$1(wkt2) {
34332
34333
  }
34333
34334
  return 1;
34334
34335
  }
34336
+ function clampLatLng$1(lng, lat, context = "") {
34337
+ let clampedLng = lng;
34338
+ let clampedLat = lat;
34339
+ let wasClamped = false;
34340
+ if (lat > 90) {
34341
+ clampedLat = 90;
34342
+ wasClamped = true;
34343
+ } else if (lat < -90) {
34344
+ clampedLat = -90;
34345
+ wasClamped = true;
34346
+ }
34347
+ if (lng > 180) {
34348
+ clampedLng = 180;
34349
+ wasClamped = true;
34350
+ } else if (lng < -180) {
34351
+ clampedLng = -180;
34352
+ wasClamped = true;
34353
+ }
34354
+ if (wasClamped) {
34355
+ console.warn(
34356
+ `COPC: Clamped transformed coordinates to valid WGS84 range${context ? ` (${context})` : ""}:`,
34357
+ `[${lng.toFixed(6)}, ${lat.toFixed(6)}] -> [${clampedLng.toFixed(6)}, ${clampedLat.toFixed(6)}]`
34358
+ );
34359
+ }
34360
+ return [clampedLng, clampedLat];
34361
+ }
34335
34362
  const DEFAULT_OPTIONS$2 = {
34336
34363
  pointBudget: 5e6,
34337
- maxConcurrentRequests: 4,
34338
- viewportDebounceMs: 150,
34364
+ maxConcurrentRequests: 8,
34365
+ viewportDebounceMs: 100,
34339
34366
  minDetailZoom: 10,
34340
- maxOctreeDepth: 20
34367
+ maxOctreeDepth: 20,
34368
+ maxSubtreesPerViewport: 60
34369
+ // Not used by COPC, but required by interface
34341
34370
  };
34342
34371
  class CopcStreamingLoader {
34343
34372
  /**
@@ -34439,10 +34468,30 @@ class CopcStreamingLoader {
34439
34468
  } catch (e) {
34440
34469
  console.warn("Failed to setup coordinate transformation:", e);
34441
34470
  }
34471
+ } else {
34472
+ const minX = header2.min[0];
34473
+ const minY = header2.min[1];
34474
+ const maxX = header2.max[0];
34475
+ const maxY = header2.max[1];
34476
+ let detectedEPSG = null;
34477
+ if (minX >= 1e5 && maxX <= 9e5 && minY >= 1e5 && maxY <= 8e5) {
34478
+ detectedEPSG = "EPSG:2180";
34479
+ }
34480
+ if (detectedEPSG) {
34481
+ try {
34482
+ const projConverter = proj4(detectedEPSG, "EPSG:4326");
34483
+ this._transformer = (coord) => projConverter.forward(coord);
34484
+ this._needsTransform = true;
34485
+ } catch (e) {
34486
+ console.warn(`Failed to setup coordinate transformation from ${detectedEPSG}:`, e);
34487
+ }
34488
+ }
34442
34489
  }
34443
34490
  if (this._needsTransform && this._transformer) {
34444
- const [minLng, minLat] = this._transformer([header2.min[0], header2.min[1]]);
34445
- const [maxLng, maxLat] = this._transformer([header2.max[0], header2.max[1]]);
34491
+ const [rawMinLng, rawMinLat] = this._transformer([header2.min[0], header2.min[1]]);
34492
+ const [rawMaxLng, rawMaxLat] = this._transformer([header2.max[0], header2.max[1]]);
34493
+ const [minLng, minLat] = clampLatLng$1(rawMinLng, rawMinLat, "header bounds min");
34494
+ const [maxLng, maxLat] = clampLatLng$1(rawMaxLng, rawMaxLat, "header bounds max");
34446
34495
  this._bounds = {
34447
34496
  minX: Math.min(minLng, maxLng),
34448
34497
  minY: Math.min(minLat, maxLat),
@@ -34532,8 +34581,10 @@ class CopcStreamingLoader {
34532
34581
  };
34533
34582
  let boundsWgs84 = bounds2;
34534
34583
  if (this._needsTransform && this._transformer) {
34535
- const [sw_lng, sw_lat] = this._transformer([minX, minY]);
34536
- const [ne_lng, ne_lat] = this._transformer([minX + nodeSize, minY + nodeSize]);
34584
+ const [rawSwLng, rawSwLat] = this._transformer([minX, minY]);
34585
+ const [rawNeLng, rawNeLat] = this._transformer([minX + nodeSize, minY + nodeSize]);
34586
+ const [sw_lng, sw_lat] = clampLatLng$1(rawSwLng, rawSwLat, "node bounds SW");
34587
+ const [ne_lng, ne_lat] = clampLatLng$1(rawNeLng, rawNeLat, "node bounds NE");
34537
34588
  boundsWgs84 = {
34538
34589
  minX: Math.min(sw_lng, ne_lng),
34539
34590
  minY: Math.min(sw_lat, ne_lat),
@@ -34765,7 +34816,8 @@ class CopcStreamingLoader {
34765
34816
  const y = yGetter(i);
34766
34817
  const z = zGetter(i);
34767
34818
  if (this._needsTransform && this._transformer) {
34768
- const [lng, lat] = this._transformer([x, y]);
34819
+ const [rawLng, rawLat] = this._transformer([x, y]);
34820
+ const [lng, lat] = clampLatLng$1(rawLng, rawLat, "");
34769
34821
  this._positions[pointIndex * 3] = lng - this._coordinateOrigin[0];
34770
34822
  this._positions[pointIndex * 3 + 1] = lat - this._coordinateOrigin[1];
34771
34823
  this._positions[pointIndex * 3 + 2] = z * this._verticalUnitFactor;
@@ -34986,6 +35038,32 @@ function createAttributeArray(type, length) {
34986
35038
  return new Float32Array(length);
34987
35039
  }
34988
35040
  }
35041
+ function clampLatLng(lng, lat, context = "") {
35042
+ let clampedLng = lng;
35043
+ let clampedLat = lat;
35044
+ let wasClamped = false;
35045
+ if (lat > 90) {
35046
+ clampedLat = 90;
35047
+ wasClamped = true;
35048
+ } else if (lat < -90) {
35049
+ clampedLat = -90;
35050
+ wasClamped = true;
35051
+ }
35052
+ if (lng > 180) {
35053
+ clampedLng = 180;
35054
+ wasClamped = true;
35055
+ } else if (lng < -180) {
35056
+ clampedLng = -180;
35057
+ wasClamped = true;
35058
+ }
35059
+ if (wasClamped && context) {
35060
+ console.warn(
35061
+ `EPT: Clamped transformed coordinates to valid WGS84 range${context ? ` (${context})` : ""}:`,
35062
+ `[${lng.toFixed(6)}, ${lat.toFixed(6)}] -> [${clampedLng.toFixed(6)}, ${clampedLat.toFixed(6)}]`
35063
+ );
35064
+ }
35065
+ return [clampedLng, clampedLat];
35066
+ }
34989
35067
  function extractProjcsFromWkt(wkt2) {
34990
35068
  if (wkt2.startsWith("COMPD_CS[")) {
34991
35069
  const projcsStart = wkt2.indexOf("PROJCS[");
@@ -35028,10 +35106,11 @@ function getVerticalUnitConversionFactor(wkt2) {
35028
35106
  }
35029
35107
  const DEFAULT_OPTIONS$1 = {
35030
35108
  pointBudget: 5e6,
35031
- maxConcurrentRequests: 4,
35032
- viewportDebounceMs: 150,
35109
+ maxConcurrentRequests: 8,
35110
+ viewportDebounceMs: 100,
35033
35111
  minDetailZoom: 10,
35034
- maxOctreeDepth: 20
35112
+ maxOctreeDepth: 20,
35113
+ maxSubtreesPerViewport: 60
35035
35114
  };
35036
35115
  class EptStreamingLoader {
35037
35116
  /**
@@ -35046,6 +35125,8 @@ class EptStreamingLoader {
35046
35125
  __publicField(this, "_metadata", null);
35047
35126
  // Hierarchy cache
35048
35127
  __publicField(this, "_hierarchyCache", /* @__PURE__ */ new Map());
35128
+ __publicField(this, "_hierarchyLoading", /* @__PURE__ */ new Set());
35129
+ __publicField(this, "_hierarchyFailures", /* @__PURE__ */ new Map());
35049
35130
  __publicField(this, "_subtreeRoots", /* @__PURE__ */ new Set());
35050
35131
  __publicField(this, "_rootHierarchyLoaded", false);
35051
35132
  // Node cache
@@ -35083,6 +35164,7 @@ class EptStreamingLoader {
35083
35164
  __publicField(this, "_pendingLayerUpdate", false);
35084
35165
  __publicField(this, "_updateBatchTimeout", null);
35085
35166
  __publicField(this, "_onPointsLoaded");
35167
+ __publicField(this, "_isResetting", false);
35086
35168
  this._baseUrl = eptUrl.endsWith("/ept.json") ? eptUrl.slice(0, -9) : eptUrl.replace(/\/$/, "");
35087
35169
  this._options = { ...DEFAULT_OPTIONS$1, ...options };
35088
35170
  }
@@ -35125,17 +35207,19 @@ class EptStreamingLoader {
35125
35207
  }
35126
35208
  const [minX, minY, minZ, maxX, maxY, maxZ] = this._metadata.boundsConforming;
35127
35209
  if (this._needsTransform && this._transformer) {
35128
- const [minLng, minLat] = this._transformer([minX, minY]);
35129
- const [maxLng, maxLat] = this._transformer([maxX, maxY]);
35130
- if (isNaN(minLng) || isNaN(minLat) || isNaN(maxLng) || isNaN(maxLat) || !isFinite(minLng) || !isFinite(minLat) || !isFinite(maxLng) || !isFinite(maxLat)) {
35210
+ const [rawMinLng, rawMinLat] = this._transformer([minX, minY]);
35211
+ const [rawMaxLng, rawMaxLat] = this._transformer([maxX, maxY]);
35212
+ if (isNaN(rawMinLng) || isNaN(rawMinLat) || isNaN(rawMaxLng) || isNaN(rawMaxLat) || !isFinite(rawMinLng) || !isFinite(rawMinLat) || !isFinite(rawMaxLng) || !isFinite(rawMaxLat)) {
35131
35213
  console.error("EPT coordinate transformation produced invalid bounds:", {
35132
35214
  input: { minX, minY, maxX, maxY },
35133
- output: { minLng, minLat, maxLng, maxLat }
35215
+ output: { rawMinLng, rawMinLat, rawMaxLng, rawMaxLat }
35134
35216
  });
35135
35217
  this._bounds = { minX, minY, minZ, maxX, maxY, maxZ };
35136
35218
  this._needsTransform = false;
35137
35219
  this._transformer = null;
35138
35220
  } else {
35221
+ const [minLng, minLat] = clampLatLng(rawMinLng, rawMinLat, "header bounds min");
35222
+ const [maxLng, maxLat] = clampLatLng(rawMaxLng, rawMaxLat, "header bounds max");
35139
35223
  this._bounds = {
35140
35224
  minX: Math.min(minLng, maxLng),
35141
35225
  minY: Math.min(minLat, maxLat),
@@ -35286,8 +35370,10 @@ class EptStreamingLoader {
35286
35370
  };
35287
35371
  let boundsWgs84 = bounds2;
35288
35372
  if (this._needsTransform && this._transformer) {
35289
- const [sw_lng, sw_lat] = this._transformer([minX, minY]);
35290
- const [ne_lng, ne_lat] = this._transformer([minX + nodeSize, minY + nodeSize]);
35373
+ const [rawSwLng, rawSwLat] = this._transformer([minX, minY]);
35374
+ const [rawNeLng, rawNeLat] = this._transformer([minX + nodeSize, minY + nodeSize]);
35375
+ const [sw_lng, sw_lat] = clampLatLng(rawSwLng, rawSwLat, "node bounds SW");
35376
+ const [ne_lng, ne_lat] = clampLatLng(rawNeLng, rawNeLat, "node bounds NE");
35291
35377
  boundsWgs84 = {
35292
35378
  minX: Math.min(sw_lng, ne_lng),
35293
35379
  minY: Math.min(sw_lat, ne_lat),
@@ -35338,22 +35424,26 @@ class EptStreamingLoader {
35338
35424
  * @param key - Hierarchy key (e.g., "0-0-0-0")
35339
35425
  */
35340
35426
  async _loadHierarchy(key) {
35341
- if (this._hierarchyCache.has(key)) return;
35427
+ if (this._hierarchyCache.has(key) || this._hierarchyLoading.has(key)) return;
35342
35428
  const url = `${this._baseUrl}/ept-hierarchy/${key}.json`;
35429
+ this._hierarchyLoading.add(key);
35343
35430
  try {
35344
35431
  const response = await fetch(url);
35345
35432
  if (!response.ok) {
35433
+ this._hierarchyFailures.set(key, Date.now());
35346
35434
  console.warn(`Failed to load hierarchy ${key}: ${response.status}`);
35347
35435
  return;
35348
35436
  }
35349
35437
  const hierarchy2 = await response.json();
35350
35438
  this._hierarchyCache.set(key, hierarchy2);
35439
+ this._hierarchyFailures.delete(key);
35351
35440
  for (const [nodeKey, value] of Object.entries(hierarchy2)) {
35352
35441
  const keyArray = this._parseNodeKey(nodeKey);
35353
35442
  const { bounds: bounds2, boundsWgs84 } = this._calculateNodeBounds(keyArray);
35443
+ const existingNode = this._nodeCache.get(nodeKey);
35354
35444
  if (value === -1) {
35355
35445
  this._subtreeRoots.add(nodeKey);
35356
- if (!this._nodeCache.has(nodeKey)) {
35446
+ if (!existingNode) {
35357
35447
  this._nodeCache.set(nodeKey, {
35358
35448
  key: nodeKey,
35359
35449
  keyArray,
@@ -35364,19 +35454,29 @@ class EptStreamingLoader {
35364
35454
  boundsWgs84
35365
35455
  });
35366
35456
  }
35367
- } else if (value > 0 && !this._nodeCache.has(nodeKey)) {
35368
- this._nodeCache.set(nodeKey, {
35369
- key: nodeKey,
35370
- keyArray,
35371
- state: "pending",
35372
- pointCount: value,
35373
- bounds: bounds2,
35374
- boundsWgs84
35375
- });
35457
+ } else if (value > 0) {
35458
+ if ((existingNode == null ? void 0 : existingNode.state) === "subtree") {
35459
+ existingNode.state = "pending";
35460
+ existingNode.pointCount = value;
35461
+ existingNode.bounds = bounds2;
35462
+ existingNode.boundsWgs84 = boundsWgs84;
35463
+ } else if (!existingNode) {
35464
+ this._nodeCache.set(nodeKey, {
35465
+ key: nodeKey,
35466
+ keyArray,
35467
+ state: "pending",
35468
+ pointCount: value,
35469
+ bounds: bounds2,
35470
+ boundsWgs84
35471
+ });
35472
+ }
35376
35473
  }
35377
35474
  }
35378
35475
  } catch (error) {
35476
+ this._hierarchyFailures.set(key, Date.now());
35379
35477
  console.warn(`Error loading hierarchy ${key}:`, error);
35478
+ } finally {
35479
+ this._hierarchyLoading.delete(key);
35380
35480
  }
35381
35481
  }
35382
35482
  /**
@@ -35401,22 +35501,31 @@ class EptStreamingLoader {
35401
35501
  }
35402
35502
  await this._ensureHierarchyLoaded();
35403
35503
  const targetDepth = viewport.targetDepth;
35404
- const subtreesToLoad = [];
35405
- for (const [, node] of this._nodeCache) {
35406
- const depth = node.keyArray[0];
35407
- if (depth > targetDepth + 2) continue;
35408
- if (node.state === "subtree" && this._boundsIntersectsViewport(node.boundsWgs84, viewport)) {
35409
- if (!this._hierarchyCache.has(node.key)) {
35410
- subtreesToLoad.push(node.key);
35411
- }
35412
- }
35413
- }
35414
- const maxSubtreesToLoad = 10;
35415
- for (const subtreeKey of subtreesToLoad.slice(0, maxSubtreesToLoad)) {
35416
- await this._loadHierarchy(subtreeKey);
35504
+ const maxSubtreesToLoad = Math.max(1, this._options.maxSubtreesPerViewport);
35505
+ const loadedSubtrees = /* @__PURE__ */ new Set();
35506
+ const maxPasses = 3;
35507
+ const now = Date.now();
35508
+ const hierarchyRetryCooldownMs = 5e3;
35509
+ for (let pass = 0; pass < maxPasses; pass++) {
35510
+ const subtreeCandidates = [];
35511
+ for (const [, node] of this._nodeCache) {
35512
+ const depth = node.keyArray[0];
35513
+ if (depth > targetDepth + 3) continue;
35514
+ const lastFailure = this._hierarchyFailures.get(node.key);
35515
+ if (node.state === "subtree" && !this._hierarchyCache.has(node.key) && !this._hierarchyLoading.has(node.key) && !loadedSubtrees.has(node.key) && (!lastFailure || now - lastFailure >= hierarchyRetryCooldownMs) && this._boundsIntersectsViewport(node.boundsWgs84, viewport)) {
35516
+ const priority = this._calculateNodePriority(node.boundsWgs84, viewport);
35517
+ subtreeCandidates.push({ key: node.key, priority });
35518
+ }
35519
+ }
35520
+ if (subtreeCandidates.length === 0) break;
35521
+ subtreeCandidates.sort((a, b) => a.priority - b.priority);
35522
+ const perPassLimit = Math.ceil(maxSubtreesToLoad / maxPasses);
35523
+ const subtreesToProcess = subtreeCandidates.slice(0, perPassLimit).map((s) => s.key);
35524
+ await Promise.all(subtreesToProcess.map((subtreeKey) => this._loadHierarchy(subtreeKey)));
35525
+ subtreesToProcess.forEach((key) => loadedSubtrees.add(key));
35526
+ if (loadedSubtrees.size >= maxSubtreesToLoad) break;
35417
35527
  }
35418
35528
  const nodesToLoad = [];
35419
- const now = Date.now();
35420
35529
  const retryCooldownMs = 5e3;
35421
35530
  for (const [, node] of this._nodeCache) {
35422
35531
  const depth = node.keyArray[0];
@@ -35425,7 +35534,7 @@ class EptStreamingLoader {
35425
35534
  if (node.lastFailedAt && now - node.lastFailedAt < retryCooldownMs) {
35426
35535
  continue;
35427
35536
  }
35428
- if (depth > targetDepth + 1) continue;
35537
+ if (depth > targetDepth + 2) continue;
35429
35538
  if (!this._boundsIntersectsViewport(node.boundsWgs84, viewport)) {
35430
35539
  continue;
35431
35540
  }
@@ -35571,7 +35680,8 @@ class EptStreamingLoader {
35571
35680
  const y = positions[i * 3 + 1];
35572
35681
  const z = positions[i * 3 + 2];
35573
35682
  if (this._needsTransform && this._transformer) {
35574
- const [lng, lat] = this._transformer([x, y]);
35683
+ const [rawLng, rawLat] = this._transformer([x, y]);
35684
+ const [lng, lat] = clampLatLng(rawLng, rawLat, "");
35575
35685
  this._positions[pointIndex * 3] = lng - this._coordinateOrigin[0];
35576
35686
  this._positions[pointIndex * 3 + 1] = lat - this._coordinateOrigin[1];
35577
35687
  this._positions[pointIndex * 3 + 2] = z * this._verticalUnitFactor;
@@ -35651,7 +35761,8 @@ class EptStreamingLoader {
35651
35761
  const y = yGetter(dataView, byteOffset);
35652
35762
  const z = zGetter(dataView, byteOffset);
35653
35763
  if (this._needsTransform && this._transformer) {
35654
- const [lng, lat] = this._transformer([x, y]);
35764
+ const [rawLng, rawLat] = this._transformer([x, y]);
35765
+ const [lng, lat] = clampLatLng(rawLng, rawLat, "");
35655
35766
  this._positions[pointIndex * 3] = lng - this._coordinateOrigin[0];
35656
35767
  this._positions[pointIndex * 3 + 1] = lat - this._coordinateOrigin[1];
35657
35768
  this._positions[pointIndex * 3 + 2] = z * this._verticalUnitFactor;
@@ -35744,6 +35855,27 @@ class EptStreamingLoader {
35744
35855
  wkt: (_c = (_b = this._metadata) == null ? void 0 : _b.srs) == null ? void 0 : _c.wkt
35745
35856
  };
35746
35857
  }
35858
+ /**
35859
+ * Checks whether there are subtree hierarchies still pending for the viewport.
35860
+ *
35861
+ * @param viewport - Current viewport information
35862
+ * @returns True if more subtree hierarchies should be loaded
35863
+ */
35864
+ hasPendingSubtrees(viewport) {
35865
+ if (!this._isInitialized) return false;
35866
+ const targetDepth = viewport.targetDepth;
35867
+ const now = Date.now();
35868
+ const hierarchyRetryCooldownMs = 5e3;
35869
+ for (const [, node] of this._nodeCache) {
35870
+ const depth = node.keyArray[0];
35871
+ if (depth > targetDepth + 3) continue;
35872
+ const lastFailure = this._hierarchyFailures.get(node.key);
35873
+ if (node.state === "subtree" && !this._hierarchyCache.has(node.key) && !this._hierarchyLoading.has(node.key) && (!lastFailure || now - lastFailure >= hierarchyRetryCooldownMs) && this._boundsIntersectsViewport(node.boundsWgs84, viewport)) {
35874
+ return true;
35875
+ }
35876
+ }
35877
+ return false;
35878
+ }
35747
35879
  /**
35748
35880
  * Gets the current streaming progress.
35749
35881
  */
@@ -35799,6 +35931,60 @@ class EptStreamingLoader {
35799
35931
  getLoadedPointCount() {
35800
35932
  return this._totalLoadedPoints;
35801
35933
  }
35934
+ /**
35935
+ * Gets the current point budget.
35936
+ */
35937
+ getPointBudget() {
35938
+ return this._options.pointBudget;
35939
+ }
35940
+ /**
35941
+ * Checks if any nodes intersecting the viewport have already been loaded.
35942
+ *
35943
+ * @param viewport - Current viewport information
35944
+ * @returns True if viewport has loaded coverage
35945
+ */
35946
+ hasLoadedNodesInViewport(viewport, minDepth = 0) {
35947
+ for (const [, node] of this._nodeCache) {
35948
+ if (node.state !== "loaded") continue;
35949
+ if (node.keyArray[0] < minDepth) continue;
35950
+ if (this._boundsIntersectsViewport(node.boundsWgs84, viewport)) {
35951
+ return true;
35952
+ }
35953
+ }
35954
+ return false;
35955
+ }
35956
+ /**
35957
+ * Estimates viewport coverage ratio by loaded nodes.
35958
+ * Returns a value from 0 to 1 representing how much of the viewport
35959
+ * is covered by loaded tiles.
35960
+ *
35961
+ * @param viewport - Current viewport information
35962
+ * @param minDepth - Minimum octree depth to consider
35963
+ * @returns Coverage ratio (0-1)
35964
+ */
35965
+ getViewportCoverageRatio(viewport, minDepth = 0) {
35966
+ const [west, south, east, north] = viewport.bounds;
35967
+ const viewportArea = (east - west) * (north - south);
35968
+ if (viewportArea <= 0) return 0;
35969
+ let coveredArea = 0;
35970
+ for (const [, node] of this._nodeCache) {
35971
+ if (node.state !== "loaded") continue;
35972
+ if (node.keyArray[0] < minDepth) continue;
35973
+ const nodeWest = node.boundsWgs84.minX;
35974
+ const nodeSouth = node.boundsWgs84.minY;
35975
+ const nodeEast = node.boundsWgs84.maxX;
35976
+ const nodeNorth = node.boundsWgs84.maxY;
35977
+ const intersectWest = Math.max(west, nodeWest);
35978
+ const intersectEast = Math.min(east, nodeEast);
35979
+ const intersectSouth = Math.max(south, nodeSouth);
35980
+ const intersectNorth = Math.min(north, nodeNorth);
35981
+ if (intersectWest < intersectEast && intersectSouth < intersectNorth) {
35982
+ const intersectArea = (intersectEast - intersectWest) * (intersectNorth - intersectSouth);
35983
+ coveredArea += intersectArea;
35984
+ }
35985
+ }
35986
+ return Math.min(1, coveredArea / viewportArea);
35987
+ }
35802
35988
  /**
35803
35989
  * Gets the total number of loaded nodes.
35804
35990
  */
@@ -35811,6 +35997,48 @@ class EptStreamingLoader {
35811
35997
  isLoading() {
35812
35998
  return this._activeRequests > 0 || this._loadingQueue.length > 0;
35813
35999
  }
36000
+ /**
36001
+ * Removes queued nodes that are outside the current viewport and re-sorts priorities.
36002
+ *
36003
+ * @param viewport - Current viewport information
36004
+ */
36005
+ pruneQueueForViewport(viewport) {
36006
+ if (this._loadingQueue.length === 0) return;
36007
+ this._loadingQueue = this._loadingQueue.filter(
36008
+ (node) => this._boundsIntersectsViewport(node.boundsWgs84, viewport)
36009
+ );
36010
+ for (const node of this._loadingQueue) {
36011
+ const distPriority = this._calculateNodePriority(node.boundsWgs84, viewport);
36012
+ const depth = node.keyArray[0];
36013
+ node.priority = distPriority - depth * 1e-4;
36014
+ }
36015
+ this._loadingQueue.sort((a, b) => (a.priority || Infinity) - (b.priority || Infinity));
36016
+ }
36017
+ /**
36018
+ * Resets loaded node data to allow loading a new area.
36019
+ * Keeps hierarchy cache intact but clears loaded points and node states.
36020
+ *
36021
+ * @returns True if reset occurred
36022
+ */
36023
+ resetLoadedData() {
36024
+ if (this._activeRequests > 0 || this._isResetting) return false;
36025
+ this._isResetting = true;
36026
+ this._loadingQueue = [];
36027
+ this._totalLoadedPoints = 0;
36028
+ this._totalLoadedNodes = 0;
36029
+ for (const [, node] of this._nodeCache) {
36030
+ if (node.state === "loaded" || node.state === "loading" || node.state === "error") {
36031
+ node.state = "pending";
36032
+ node.bufferStartIndex = void 0;
36033
+ node.error = void 0;
36034
+ node.retryCount = void 0;
36035
+ node.lastFailedAt = void 0;
36036
+ }
36037
+ }
36038
+ this._scheduleLayerUpdate();
36039
+ this._isResetting = false;
36040
+ return true;
36041
+ }
35814
36042
  /**
35815
36043
  * Gets the EPT metadata.
35816
36044
  */
@@ -35827,6 +36055,8 @@ class EptStreamingLoader {
35827
36055
  this._loadingQueue = [];
35828
36056
  this._nodeCache.clear();
35829
36057
  this._hierarchyCache.clear();
36058
+ this._hierarchyLoading.clear();
36059
+ this._hierarchyFailures.clear();
35830
36060
  this._subtreeRoots.clear();
35831
36061
  this._eventHandlers.clear();
35832
36062
  this._positions = null;
@@ -35924,37 +36154,180 @@ function computePercentileBounds(arr, lowerPercentile = 2, upperPercentile = 98)
35924
36154
  }
35925
36155
  return { min, max };
35926
36156
  }
35927
- const ELEVATION_RAMP = [
36157
+ const VIRIDIS = [
35928
36158
  [68, 1, 84],
35929
- // dark purple
35930
36159
  [72, 40, 120],
35931
- // purple
35932
36160
  [62, 74, 137],
35933
- // blue-purple
35934
36161
  [49, 104, 142],
35935
- // blue
35936
36162
  [38, 130, 142],
35937
- // teal-blue
35938
36163
  [31, 158, 137],
35939
- // teal
35940
36164
  [53, 183, 121],
35941
- // green-teal
35942
36165
  [109, 205, 89],
35943
- // green
35944
36166
  [180, 222, 44],
35945
- // yellow-green
35946
36167
  [253, 231, 37]
35947
- // yellow
35948
36168
  ];
35949
- const INTENSITY_RAMP = [
36169
+ const PLASMA = [
36170
+ [13, 8, 135],
36171
+ [75, 3, 161],
36172
+ [125, 3, 168],
36173
+ [168, 34, 150],
36174
+ [203, 70, 121],
36175
+ [229, 107, 93],
36176
+ [248, 148, 65],
36177
+ [253, 195, 40],
36178
+ [240, 249, 33],
36179
+ [240, 249, 33]
36180
+ ];
36181
+ const INFERNO = [
36182
+ [0, 0, 4],
36183
+ [40, 11, 84],
36184
+ [89, 13, 115],
36185
+ [137, 31, 107],
36186
+ [179, 55, 79],
36187
+ [213, 87, 49],
36188
+ [240, 130, 24],
36189
+ [253, 184, 43],
36190
+ [249, 251, 146],
36191
+ [252, 255, 164]
36192
+ ];
36193
+ const MAGMA = [
36194
+ [0, 0, 4],
36195
+ [28, 16, 68],
36196
+ [79, 18, 123],
36197
+ [129, 37, 129],
36198
+ [181, 54, 122],
36199
+ [229, 80, 100],
36200
+ [251, 135, 97],
36201
+ [254, 194, 135],
36202
+ [254, 247, 187],
36203
+ [252, 253, 191]
36204
+ ];
36205
+ const CIVIDIS = [
36206
+ [0, 32, 77],
36207
+ [0, 58, 103],
36208
+ [52, 77, 105],
36209
+ [87, 95, 108],
36210
+ [115, 113, 112],
36211
+ [143, 132, 108],
36212
+ [171, 152, 97],
36213
+ [200, 173, 79],
36214
+ [231, 196, 55],
36215
+ [253, 231, 37]
36216
+ ];
36217
+ const TURBO = [
36218
+ [48, 18, 59],
36219
+ [70, 107, 227],
36220
+ [40, 170, 225],
36221
+ [35, 221, 162],
36222
+ [122, 249, 85],
36223
+ [194, 241, 45],
36224
+ [241, 206, 51],
36225
+ [250, 144, 42],
36226
+ [229, 68, 25],
36227
+ [122, 4, 3]
36228
+ ];
36229
+ const JET = [
36230
+ [0, 0, 127],
36231
+ [0, 0, 255],
36232
+ [0, 127, 255],
36233
+ [0, 255, 255],
36234
+ [127, 255, 127],
36235
+ [255, 255, 0],
36236
+ [255, 127, 0],
36237
+ [255, 0, 0],
36238
+ [127, 0, 0],
36239
+ [127, 0, 0]
36240
+ ];
36241
+ const RAINBOW = [
36242
+ [150, 0, 90],
36243
+ [0, 0, 200],
36244
+ [0, 125, 255],
36245
+ [0, 200, 255],
36246
+ [0, 255, 125],
36247
+ [125, 255, 0],
36248
+ [255, 255, 0],
36249
+ [255, 125, 0],
36250
+ [255, 0, 0],
36251
+ [128, 0, 0]
36252
+ ];
36253
+ const TERRAIN = [
36254
+ [51, 51, 153],
36255
+ [51, 102, 153],
36256
+ [51, 153, 153],
36257
+ [102, 178, 102],
36258
+ [153, 204, 102],
36259
+ [204, 229, 102],
36260
+ [204, 204, 153],
36261
+ [178, 153, 102],
36262
+ [153, 102, 51],
36263
+ [255, 255, 255]
36264
+ ];
36265
+ const COOLWARM = [
36266
+ [59, 76, 192],
36267
+ [98, 130, 234],
36268
+ [141, 176, 254],
36269
+ [184, 208, 249],
36270
+ [221, 221, 221],
36271
+ [245, 196, 173],
36272
+ [244, 154, 123],
36273
+ [222, 96, 77],
36274
+ [180, 4, 38],
36275
+ [180, 4, 38]
36276
+ ];
36277
+ const GRAY = [
35950
36278
  [0, 0, 0],
35951
- // black
35952
- [64, 64, 64],
35953
- [128, 128, 128],
35954
- [192, 192, 192],
36279
+ [28, 28, 28],
36280
+ [57, 57, 57],
36281
+ [85, 85, 85],
36282
+ [113, 113, 113],
36283
+ [142, 142, 142],
36284
+ [170, 170, 170],
36285
+ [198, 198, 198],
36286
+ [227, 227, 227],
35955
36287
  [255, 255, 255]
35956
- // white
35957
36288
  ];
36289
+ const COLORMAPS = {
36290
+ viridis: VIRIDIS,
36291
+ plasma: PLASMA,
36292
+ inferno: INFERNO,
36293
+ magma: MAGMA,
36294
+ cividis: CIVIDIS,
36295
+ turbo: TURBO,
36296
+ jet: JET,
36297
+ rainbow: RAINBOW,
36298
+ terrain: TERRAIN,
36299
+ coolwarm: COOLWARM,
36300
+ gray: GRAY
36301
+ };
36302
+ const COLORMAP_NAMES = [
36303
+ "viridis",
36304
+ "plasma",
36305
+ "inferno",
36306
+ "magma",
36307
+ "cividis",
36308
+ "turbo",
36309
+ "jet",
36310
+ "rainbow",
36311
+ "terrain",
36312
+ "coolwarm",
36313
+ "gray"
36314
+ ];
36315
+ const COLORMAP_LABELS = {
36316
+ viridis: "Viridis",
36317
+ plasma: "Plasma",
36318
+ inferno: "Inferno",
36319
+ magma: "Magma",
36320
+ cividis: "Cividis",
36321
+ turbo: "Turbo",
36322
+ jet: "Jet",
36323
+ rainbow: "Rainbow",
36324
+ terrain: "Terrain",
36325
+ coolwarm: "Cool-Warm",
36326
+ gray: "Grayscale"
36327
+ };
36328
+ function getColormap(name) {
36329
+ return COLORMAPS[name] || COLORMAPS.viridis;
36330
+ }
35958
36331
  const CLASSIFICATION_COLORS = {
35959
36332
  0: [128, 128, 128],
35960
36333
  // Created, never classified
@@ -35996,6 +36369,10 @@ const CLASSIFICATION_COLORS = {
35996
36369
  // High Noise
35997
36370
  };
35998
36371
  class ColorSchemeProcessor {
36372
+ constructor() {
36373
+ /** Last computed color bounds (for colorbar display) */
36374
+ __publicField(this, "_lastComputedBounds");
36375
+ }
35999
36376
  /**
36000
36377
  * Generates a color array for the point cloud based on the color scheme.
36001
36378
  *
@@ -36005,100 +36382,150 @@ class ColorSchemeProcessor {
36005
36382
  * @returns Uint8Array of RGBA colors (length = pointCount * 4)
36006
36383
  */
36007
36384
  getColors(data, scheme, options = {}) {
36385
+ const result = this.getColorsWithBounds(data, scheme, options);
36386
+ return result.colors;
36387
+ }
36388
+ /**
36389
+ * Generates a color array and returns the computed bounds.
36390
+ *
36391
+ * @param data - Point cloud data
36392
+ * @param scheme - Color scheme to apply
36393
+ * @param options - Optional color generation options
36394
+ * @returns ColorResult containing colors and computed bounds
36395
+ */
36396
+ getColorsWithBounds(data, scheme, options = {}) {
36008
36397
  const colors = new Uint8Array(data.pointCount * 4);
36398
+ const colormap = options.colormap ?? "viridis";
36399
+ const colorRange = options.colorRange;
36009
36400
  const usePercentile = options.usePercentile ?? true;
36010
36401
  if (typeof scheme === "string") {
36011
36402
  switch (scheme) {
36012
36403
  case "elevation":
36013
- return this._colorByElevation(data, colors, usePercentile);
36404
+ return this._colorByElevation(data, colors, colormap, colorRange, usePercentile);
36014
36405
  case "intensity":
36015
- return this._colorByIntensity(data, colors, usePercentile);
36406
+ return this._colorByIntensity(data, colors, colormap, colorRange, usePercentile);
36016
36407
  case "classification":
36017
- return this._colorByClassification(data, colors, options.hiddenClassifications);
36408
+ return { colors: this._colorByClassification(data, colors, options.hiddenClassifications) };
36018
36409
  case "rgb":
36019
- return this._colorByRGB(data, colors);
36410
+ return { colors: this._colorByRGB(data, colors) };
36020
36411
  default:
36021
- return this._colorByElevation(data, colors, usePercentile);
36412
+ return this._colorByElevation(data, colors, colormap, colorRange, usePercentile);
36413
+ }
36414
+ } else {
36415
+ return { colors: this._colorByCustom(data, colors, scheme, colormap, colorRange, usePercentile) };
36416
+ }
36417
+ }
36418
+ /**
36419
+ * Gets the last computed color bounds (for colorbar display).
36420
+ *
36421
+ * @returns The last computed bounds or undefined
36422
+ */
36423
+ getLastComputedBounds() {
36424
+ return this._lastComputedBounds;
36425
+ }
36426
+ /**
36427
+ * Computes the color bounds based on the configuration.
36428
+ *
36429
+ * @param values - Array of values to compute bounds for
36430
+ * @param dataBounds - Data bounds (min/max)
36431
+ * @param colorRange - Color range configuration
36432
+ * @param usePercentile - Legacy percentile flag
36433
+ * @returns Computed min and max bounds
36434
+ */
36435
+ _computeBounds(values, dataBounds, colorRange, usePercentile) {
36436
+ if (colorRange) {
36437
+ if (colorRange.mode === "absolute") {
36438
+ return {
36439
+ min: colorRange.absoluteMin ?? dataBounds.min,
36440
+ max: colorRange.absoluteMax ?? dataBounds.max
36441
+ };
36442
+ } else {
36443
+ const pLow = colorRange.percentileLow ?? 2;
36444
+ const pHigh = colorRange.percentileHigh ?? 98;
36445
+ return computePercentileBounds(values, pLow, pHigh);
36022
36446
  }
36447
+ } else if (usePercentile) {
36448
+ return computePercentileBounds(values, 2, 98);
36023
36449
  } else {
36024
- return this._colorByCustom(data, colors, scheme);
36450
+ return dataBounds;
36025
36451
  }
36026
36452
  }
36027
36453
  /**
36028
- * Colors points by elevation using terrain-like ramp.
36454
+ * Colors points by elevation using the specified colormap.
36029
36455
  *
36030
36456
  * @param data - Point cloud data
36031
36457
  * @param colors - Output color array
36032
- * @param usePercentile - Whether to use percentile bounds (2-98%) for better color distribution
36458
+ * @param colormap - Colormap name to use
36459
+ * @param colorRange - Color range configuration
36460
+ * @param usePercentile - Legacy percentile flag
36461
+ * @returns ColorResult with colors and computed bounds
36033
36462
  */
36034
- _colorByElevation(data, colors, usePercentile) {
36463
+ _colorByElevation(data, colors, colormap, colorRange, usePercentile) {
36035
36464
  var _a, _b;
36036
36465
  if (!data.positions || data.positions.length === 0) {
36037
- return colors;
36038
- }
36039
- let minZ;
36040
- let maxZ;
36041
- if (usePercentile) {
36042
- const zValues = new Float32Array(data.pointCount);
36043
- for (let i = 0; i < data.pointCount; i++) {
36044
- zValues[i] = data.positions[i * 3 + 2] ?? 0;
36045
- }
36046
- const bounds2 = computePercentileBounds(zValues, 2, 98);
36047
- minZ = bounds2.min;
36048
- maxZ = bounds2.max;
36049
- } else {
36050
- minZ = ((_a = data.bounds) == null ? void 0 : _a.minZ) ?? 0;
36051
- maxZ = ((_b = data.bounds) == null ? void 0 : _b.maxZ) ?? 1;
36466
+ return { colors };
36052
36467
  }
36468
+ const zValues = new Float32Array(data.pointCount);
36469
+ for (let i = 0; i < data.pointCount; i++) {
36470
+ zValues[i] = data.positions[i * 3 + 2] ?? 0;
36471
+ }
36472
+ const dataBounds = {
36473
+ min: ((_a = data.bounds) == null ? void 0 : _a.minZ) ?? 0,
36474
+ max: ((_b = data.bounds) == null ? void 0 : _b.maxZ) ?? 1
36475
+ };
36476
+ const bounds2 = this._computeBounds(zValues, dataBounds, colorRange, usePercentile);
36477
+ this._lastComputedBounds = bounds2;
36478
+ const { min: minZ, max: maxZ } = bounds2;
36053
36479
  const range = maxZ - minZ || 1;
36480
+ const ramp = COLORMAPS[colormap] || COLORMAPS.viridis;
36054
36481
  for (let i = 0; i < data.pointCount; i++) {
36055
- const z = data.positions[i * 3 + 2] ?? 0;
36482
+ const z = zValues[i];
36056
36483
  const t = (z - minZ) / range;
36057
- const color = this._interpolateRamp(ELEVATION_RAMP, t);
36484
+ const color = this._interpolateRamp(ramp, t);
36058
36485
  colors[i * 4] = color[0];
36059
36486
  colors[i * 4 + 1] = color[1];
36060
36487
  colors[i * 4 + 2] = color[2];
36061
36488
  colors[i * 4 + 3] = 255;
36062
36489
  }
36063
- return colors;
36490
+ return { colors, bounds: bounds2 };
36064
36491
  }
36065
36492
  /**
36066
- * Colors points by intensity using grayscale ramp.
36493
+ * Colors points by intensity using the specified colormap.
36067
36494
  *
36068
36495
  * @param data - Point cloud data
36069
36496
  * @param colors - Output color array
36070
- * @param usePercentile - Whether to use percentile bounds (2-98%) for better color distribution
36497
+ * @param colormap - Colormap name to use
36498
+ * @param colorRange - Color range configuration
36499
+ * @param usePercentile - Legacy percentile flag
36500
+ * @returns ColorResult with colors and computed bounds
36071
36501
  */
36072
- _colorByIntensity(data, colors, usePercentile) {
36502
+ _colorByIntensity(data, colors, colormap, colorRange, usePercentile) {
36073
36503
  if (!data.hasIntensity || !data.intensities) {
36074
- return this._colorByElevation(data, colors, usePercentile);
36075
- }
36076
- let minI;
36077
- let maxI;
36078
- if (usePercentile) {
36079
- const bounds2 = computePercentileBounds(data.intensities, 2, 98);
36080
- minI = bounds2.min;
36081
- maxI = bounds2.max;
36082
- } else {
36083
- minI = Infinity;
36084
- maxI = -Infinity;
36085
- for (let i = 0; i < data.pointCount; i++) {
36086
- const intensity = data.intensities[i];
36087
- if (intensity < minI) minI = intensity;
36088
- if (intensity > maxI) maxI = intensity;
36089
- }
36504
+ return this._colorByElevation(data, colors, colormap, colorRange, usePercentile);
36090
36505
  }
36091
- const range = maxI - minI || 1;
36506
+ let minI = Infinity;
36507
+ let maxI = -Infinity;
36092
36508
  for (let i = 0; i < data.pointCount; i++) {
36093
36509
  const intensity = data.intensities[i];
36094
- const t = (intensity - minI) / range;
36095
- const color = this._interpolateRamp(INTENSITY_RAMP, t);
36510
+ if (intensity < minI) minI = intensity;
36511
+ if (intensity > maxI) maxI = intensity;
36512
+ }
36513
+ const dataBounds = { min: minI, max: maxI };
36514
+ const bounds2 = this._computeBounds(data.intensities, dataBounds, colorRange, usePercentile);
36515
+ this._lastComputedBounds = bounds2;
36516
+ const { min: minVal, max: maxVal } = bounds2;
36517
+ const range = maxVal - minVal || 1;
36518
+ const ramp = COLORMAPS[colormap] || COLORMAPS.gray;
36519
+ for (let i = 0; i < data.pointCount; i++) {
36520
+ const intensity = data.intensities[i];
36521
+ const t = (intensity - minVal) / range;
36522
+ const color = this._interpolateRamp(ramp, t);
36096
36523
  colors[i * 4] = color[0];
36097
36524
  colors[i * 4 + 1] = color[1];
36098
36525
  colors[i * 4 + 2] = color[2];
36099
36526
  colors[i * 4 + 3] = 255;
36100
36527
  }
36101
- return colors;
36528
+ return { colors, bounds: bounds2 };
36102
36529
  }
36103
36530
  /**
36104
36531
  * Colors points by classification using ASPRS standard colors.
@@ -36106,10 +36533,12 @@ class ColorSchemeProcessor {
36106
36533
  * @param data - Point cloud data
36107
36534
  * @param colors - Output color array
36108
36535
  * @param hiddenClassifications - Optional set of classification codes to hide (alpha=0)
36536
+ * @returns Color array
36109
36537
  */
36110
36538
  _colorByClassification(data, colors, hiddenClassifications) {
36111
36539
  if (!data.hasClassification || !data.classifications) {
36112
- return this._colorByElevation(data, colors, true);
36540
+ const result = this._colorByElevation(data, colors, "viridis", void 0, true);
36541
+ return result.colors;
36113
36542
  }
36114
36543
  for (let i = 0; i < data.pointCount; i++) {
36115
36544
  const cls = data.classifications[i];
@@ -36123,10 +36552,15 @@ class ColorSchemeProcessor {
36123
36552
  }
36124
36553
  /**
36125
36554
  * Uses embedded RGB colors from the point cloud.
36555
+ *
36556
+ * @param data - Point cloud data
36557
+ * @param colors - Output color array
36558
+ * @returns Color array
36126
36559
  */
36127
36560
  _colorByRGB(data, colors) {
36128
36561
  if (!data.hasRGB || !data.colors) {
36129
- return this._colorByElevation(data, colors, true);
36562
+ const result = this._colorByElevation(data, colors, "viridis", void 0, true);
36563
+ return result.colors;
36130
36564
  }
36131
36565
  for (let i = 0; i < data.pointCount; i++) {
36132
36566
  colors[i * 4] = data.colors[i * 4];
@@ -36138,12 +36572,25 @@ class ColorSchemeProcessor {
36138
36572
  }
36139
36573
  /**
36140
36574
  * Applies a custom color scheme configuration.
36575
+ *
36576
+ * @param data - Point cloud data
36577
+ * @param colors - Output color array
36578
+ * @param _config - Custom color scheme config
36579
+ * @param colormap - Colormap name to use
36580
+ * @param colorRange - Color range configuration
36581
+ * @param usePercentile - Legacy percentile flag
36582
+ * @returns Color array
36141
36583
  */
36142
- _colorByCustom(data, colors, _config) {
36143
- return this._colorByElevation(data, colors, true);
36584
+ _colorByCustom(data, colors, _config, colormap, colorRange, usePercentile) {
36585
+ const result = this._colorByElevation(data, colors, colormap, colorRange, usePercentile);
36586
+ return result.colors;
36144
36587
  }
36145
36588
  /**
36146
36589
  * Interpolates a color from a color ramp.
36590
+ *
36591
+ * @param ramp - Color ramp array
36592
+ * @param t - Interpolation parameter (0-1)
36593
+ * @returns Interpolated RGB color
36147
36594
  */
36148
36595
  _interpolateRamp(ramp, t) {
36149
36596
  if (!Number.isFinite(t)) {
@@ -36201,6 +36648,7 @@ class PointCloudManager {
36201
36648
  __publicField(this, "_pointClouds");
36202
36649
  __publicField(this, "_options");
36203
36650
  __publicField(this, "_colorProcessor");
36651
+ __publicField(this, "_lastComputedBounds");
36204
36652
  this._deckOverlay = deckOverlay;
36205
36653
  this._pointClouds = /* @__PURE__ */ new Map();
36206
36654
  this._colorProcessor = new ColorSchemeProcessor();
@@ -36209,6 +36657,8 @@ class PointCloudManager {
36209
36657
  opacity: options.opacity ?? 1,
36210
36658
  colorScheme: options.colorScheme ?? "elevation",
36211
36659
  usePercentile: options.usePercentile ?? true,
36660
+ colormap: options.colormap ?? "viridis",
36661
+ colorRange: options.colorRange,
36212
36662
  elevationRange: options.elevationRange ?? null,
36213
36663
  pickable: options.pickable ?? false,
36214
36664
  zOffset: options.zOffset ?? 0,
@@ -36230,15 +36680,20 @@ class PointCloudManager {
36230
36680
  * @param data - Point cloud data (positions are already offsets from coordinateOrigin)
36231
36681
  */
36232
36682
  addPointCloud(id, data) {
36233
- const colors = this._colorProcessor.getColors(data, this._options.colorScheme, {
36683
+ const result = this._colorProcessor.getColorsWithBounds(data, this._options.colorScheme, {
36234
36684
  usePercentile: this._options.usePercentile,
36685
+ colormap: this._options.colormap,
36686
+ colorRange: this._options.colorRange,
36235
36687
  hiddenClassifications: this._options.hiddenClassifications
36236
36688
  });
36689
+ if (result.bounds) {
36690
+ this._lastComputedBounds = result.bounds;
36691
+ }
36237
36692
  const coordinateOrigin = data.coordinateOrigin;
36238
36693
  this._pointClouds.set(id, {
36239
36694
  id,
36240
36695
  data,
36241
- colors,
36696
+ colors: result.colors,
36242
36697
  coordinateOrigin,
36243
36698
  visible: true,
36244
36699
  opacityOverride: null
@@ -36258,14 +36713,19 @@ class PointCloudManager {
36258
36713
  }
36259
36714
  const existing = this._pointClouds.get(id);
36260
36715
  if (existing) {
36261
- const colors = this._colorProcessor.getColors(data, this._options.colorScheme, {
36716
+ const result = this._colorProcessor.getColorsWithBounds(data, this._options.colorScheme, {
36262
36717
  usePercentile: this._options.usePercentile,
36718
+ colormap: this._options.colormap,
36719
+ colorRange: this._options.colorRange,
36263
36720
  hiddenClassifications: this._options.hiddenClassifications
36264
36721
  });
36722
+ if (result.bounds) {
36723
+ this._lastComputedBounds = result.bounds;
36724
+ }
36265
36725
  this._pointClouds.set(id, {
36266
36726
  id,
36267
36727
  data,
36268
- colors,
36728
+ colors: result.colors,
36269
36729
  coordinateOrigin: data.coordinateOrigin,
36270
36730
  visible: existing.visible,
36271
36731
  opacityOverride: existing.opacityOverride
@@ -36336,17 +36796,24 @@ class PointCloudManager {
36336
36796
  updateStyle(options) {
36337
36797
  const colorSchemeChanged = options.colorScheme !== void 0 && options.colorScheme !== this._options.colorScheme;
36338
36798
  const percentileChanged = options.usePercentile !== void 0 && options.usePercentile !== this._options.usePercentile;
36799
+ const colormapChanged = options.colormap !== void 0 && options.colormap !== this._options.colormap;
36800
+ const colorRangeChanged = options.colorRange !== void 0;
36339
36801
  const hiddenClassificationsChanged = options.hiddenClassifications !== void 0;
36340
36802
  this._options = { ...this._options, ...options };
36341
- if (colorSchemeChanged || percentileChanged || hiddenClassificationsChanged) {
36803
+ if (colorSchemeChanged || percentileChanged || colormapChanged || colorRangeChanged || hiddenClassificationsChanged) {
36342
36804
  for (const [id, pc] of this._pointClouds) {
36343
- const colors = this._colorProcessor.getColors(pc.data, this._options.colorScheme, {
36805
+ const result = this._colorProcessor.getColorsWithBounds(pc.data, this._options.colorScheme, {
36344
36806
  usePercentile: this._options.usePercentile,
36807
+ colormap: this._options.colormap,
36808
+ colorRange: this._options.colorRange,
36345
36809
  hiddenClassifications: this._options.hiddenClassifications
36346
36810
  });
36811
+ if (result.bounds) {
36812
+ this._lastComputedBounds = result.bounds;
36813
+ }
36347
36814
  this._pointClouds.set(id, {
36348
36815
  ...pc,
36349
- colors,
36816
+ colors: result.colors,
36350
36817
  coordinateOrigin: pc.coordinateOrigin,
36351
36818
  visible: pc.visible,
36352
36819
  opacityOverride: pc.opacityOverride
@@ -36393,6 +36860,22 @@ class PointCloudManager {
36393
36860
  setUsePercentile(usePercentile) {
36394
36861
  this.updateStyle({ usePercentile });
36395
36862
  }
36863
+ /**
36864
+ * Sets the colormap for elevation/intensity coloring.
36865
+ *
36866
+ * @param colormap - Colormap name
36867
+ */
36868
+ setColormap(colormap) {
36869
+ this.updateStyle({ colormap });
36870
+ }
36871
+ /**
36872
+ * Sets the color range configuration.
36873
+ *
36874
+ * @param colorRange - Color range configuration
36875
+ */
36876
+ setColorRange(colorRange) {
36877
+ this.updateStyle({ colorRange });
36878
+ }
36396
36879
  /**
36397
36880
  * Sets the elevation range filter.
36398
36881
  *
@@ -36491,6 +36974,13 @@ class PointCloudManager {
36491
36974
  getOptions() {
36492
36975
  return { ...this._options };
36493
36976
  }
36977
+ /**
36978
+ * Gets the last computed color bounds.
36979
+ * Used for displaying accurate colorbar min/max values.
36980
+ */
36981
+ getLastComputedBounds() {
36982
+ return this._lastComputedBounds;
36983
+ }
36494
36984
  /**
36495
36985
  * Creates a deck.gl layer for a point cloud.
36496
36986
  * Chunks large point clouds into multiple layers to avoid WebGL buffer limits.
@@ -36979,6 +37469,7 @@ class DualRangeSlider {
36979
37469
  __publicField(this, "_sliderLow");
36980
37470
  __publicField(this, "_sliderHigh");
36981
37471
  __publicField(this, "_valueDisplay");
37472
+ __publicField(this, "_rangeHighlight");
36982
37473
  this._options = options;
36983
37474
  }
36984
37475
  /**
@@ -37026,6 +37517,7 @@ class DualRangeSlider {
37026
37517
  background: #159895;
37027
37518
  border-radius: 2px;
37028
37519
  `;
37520
+ this._rangeHighlight = range;
37029
37521
  sliderContainer.appendChild(range);
37030
37522
  const sliderLow = document.createElement("input");
37031
37523
  sliderLow.type = "range";
@@ -37143,6 +37635,7 @@ class DualRangeSlider {
37143
37635
  if (this._valueDisplay) {
37144
37636
  this._valueDisplay.textContent = this._formatRange(low, high);
37145
37637
  }
37638
+ this._updateRangeHighlight();
37146
37639
  }
37147
37640
  /**
37148
37641
  * Updates the min/max bounds of the slider.
@@ -37158,6 +37651,33 @@ class DualRangeSlider {
37158
37651
  this._sliderHigh.min = String(min);
37159
37652
  this._sliderHigh.max = String(max);
37160
37653
  }
37654
+ this._updateRangeHighlight();
37655
+ }
37656
+ /**
37657
+ * Updates the step value of the slider.
37658
+ */
37659
+ setStep(step2) {
37660
+ this._options.step = step2;
37661
+ if (this._sliderLow) {
37662
+ this._sliderLow.step = String(step2);
37663
+ }
37664
+ if (this._sliderHigh) {
37665
+ this._sliderHigh.step = String(step2);
37666
+ }
37667
+ }
37668
+ /**
37669
+ * Updates the visual range highlight bar.
37670
+ */
37671
+ _updateRangeHighlight() {
37672
+ if (!this._rangeHighlight || !this._sliderLow || !this._sliderHigh) return;
37673
+ const low = parseFloat(this._sliderLow.value);
37674
+ const high = parseFloat(this._sliderHigh.value);
37675
+ const min = this._options.min;
37676
+ const max = this._options.max;
37677
+ const percentLow = (low - min) / (max - min) * 100;
37678
+ const percentHigh = (high - min) / (max - min) * 100;
37679
+ this._rangeHighlight.style.left = `${percentLow}%`;
37680
+ this._rangeHighlight.style.width = `${percentHigh - percentLow}%`;
37161
37681
  }
37162
37682
  /**
37163
37683
  * Gets the current range values.
@@ -37310,6 +37830,457 @@ class ClassificationLegend {
37310
37830
  return this._container;
37311
37831
  }
37312
37832
  }
37833
+ class Colorbar {
37834
+ /**
37835
+ * Creates a new Colorbar instance.
37836
+ *
37837
+ * @param options - Colorbar configuration options
37838
+ */
37839
+ constructor(options) {
37840
+ __publicField(this, "_options");
37841
+ __publicField(this, "_canvas");
37842
+ __publicField(this, "_minLabel");
37843
+ __publicField(this, "_maxLabel");
37844
+ this._options = { ...options };
37845
+ }
37846
+ /**
37847
+ * Renders the colorbar component.
37848
+ *
37849
+ * @returns The colorbar container element
37850
+ */
37851
+ render() {
37852
+ const container = document.createElement("div");
37853
+ container.className = "lidar-colorbar";
37854
+ if (this._options.label) {
37855
+ const label = document.createElement("div");
37856
+ label.className = "lidar-colorbar-label";
37857
+ label.textContent = this._options.label;
37858
+ container.appendChild(label);
37859
+ }
37860
+ const canvas = document.createElement("canvas");
37861
+ canvas.className = "lidar-colorbar-gradient";
37862
+ canvas.width = 200;
37863
+ canvas.height = 14;
37864
+ this._canvas = canvas;
37865
+ container.appendChild(canvas);
37866
+ const labelsContainer = document.createElement("div");
37867
+ labelsContainer.className = "lidar-colorbar-labels";
37868
+ const minLabel = document.createElement("span");
37869
+ minLabel.className = "lidar-colorbar-min";
37870
+ this._minLabel = minLabel;
37871
+ const maxLabel = document.createElement("span");
37872
+ maxLabel.className = "lidar-colorbar-max";
37873
+ this._maxLabel = maxLabel;
37874
+ labelsContainer.appendChild(minLabel);
37875
+ labelsContainer.appendChild(maxLabel);
37876
+ container.appendChild(labelsContainer);
37877
+ this._drawGradient();
37878
+ this._updateLabels();
37879
+ return container;
37880
+ }
37881
+ /**
37882
+ * Updates the colorbar with new options.
37883
+ *
37884
+ * @param options - Partial options to update
37885
+ */
37886
+ update(options) {
37887
+ if (options.colormap !== void 0) {
37888
+ this._options.colormap = options.colormap;
37889
+ }
37890
+ if (options.minValue !== void 0) {
37891
+ this._options.minValue = options.minValue;
37892
+ }
37893
+ if (options.maxValue !== void 0) {
37894
+ this._options.maxValue = options.maxValue;
37895
+ }
37896
+ if (options.label !== void 0) {
37897
+ this._options.label = options.label;
37898
+ }
37899
+ this._drawGradient();
37900
+ this._updateLabels();
37901
+ }
37902
+ /**
37903
+ * Sets the colormap.
37904
+ *
37905
+ * @param colormap - The colormap name
37906
+ */
37907
+ setColormap(colormap) {
37908
+ this._options.colormap = colormap;
37909
+ this._drawGradient();
37910
+ }
37911
+ /**
37912
+ * Sets the value range.
37913
+ *
37914
+ * @param min - Minimum value
37915
+ * @param max - Maximum value
37916
+ */
37917
+ setRange(min, max) {
37918
+ this._options.minValue = min;
37919
+ this._options.maxValue = max;
37920
+ this._updateLabels();
37921
+ }
37922
+ /**
37923
+ * Gets the current colormap.
37924
+ *
37925
+ * @returns The current colormap name
37926
+ */
37927
+ getColormap() {
37928
+ return this._options.colormap;
37929
+ }
37930
+ /**
37931
+ * Gets the current value range.
37932
+ *
37933
+ * @returns Object with min and max values
37934
+ */
37935
+ getRange() {
37936
+ return {
37937
+ min: this._options.minValue,
37938
+ max: this._options.maxValue
37939
+ };
37940
+ }
37941
+ /**
37942
+ * Draws the color gradient on the canvas.
37943
+ */
37944
+ _drawGradient() {
37945
+ if (!this._canvas) return;
37946
+ const ctx = this._canvas.getContext("2d");
37947
+ if (!ctx) return;
37948
+ const width = this._canvas.width;
37949
+ const height = this._canvas.height;
37950
+ const ramp = COLORMAPS[this._options.colormap] || COLORMAPS.viridis;
37951
+ const gradient = ctx.createLinearGradient(0, 0, width, 0);
37952
+ for (let i = 0; i < ramp.length; i++) {
37953
+ const t = i / (ramp.length - 1);
37954
+ const [r, g, b] = ramp[i];
37955
+ gradient.addColorStop(t, `rgb(${r}, ${g}, ${b})`);
37956
+ }
37957
+ ctx.fillStyle = gradient;
37958
+ ctx.fillRect(0, 0, width, height);
37959
+ }
37960
+ /**
37961
+ * Updates the min/max value labels.
37962
+ */
37963
+ _updateLabels() {
37964
+ if (this._minLabel) {
37965
+ this._minLabel.textContent = this._formatValue(this._options.minValue);
37966
+ }
37967
+ if (this._maxLabel) {
37968
+ this._maxLabel.textContent = this._formatValue(this._options.maxValue);
37969
+ }
37970
+ }
37971
+ /**
37972
+ * Formats a value for display.
37973
+ *
37974
+ * @param value - The value to format
37975
+ * @returns Formatted string
37976
+ */
37977
+ _formatValue(value) {
37978
+ if (!Number.isFinite(value)) {
37979
+ return "—";
37980
+ }
37981
+ const range = Math.abs(this._options.maxValue - this._options.minValue);
37982
+ if (range < 1) {
37983
+ return value.toFixed(3);
37984
+ } else if (range < 10) {
37985
+ return value.toFixed(2);
37986
+ } else if (range < 100) {
37987
+ return value.toFixed(1);
37988
+ } else {
37989
+ return value.toFixed(0);
37990
+ }
37991
+ }
37992
+ }
37993
+ const DEFAULT_PERCENTILE_LOW = 2;
37994
+ const DEFAULT_PERCENTILE_HIGH = 98;
37995
+ class PercentileRangeControl {
37996
+ /**
37997
+ * Creates a new PercentileRangeControl instance.
37998
+ *
37999
+ * @param options - Control configuration options
38000
+ */
38001
+ constructor(options) {
38002
+ __publicField(this, "_options");
38003
+ __publicField(this, "_percentileRadio");
38004
+ __publicField(this, "_absoluteRadio");
38005
+ __publicField(this, "_percentileSliderContainer");
38006
+ __publicField(this, "_absoluteSliderContainer");
38007
+ __publicField(this, "_percentileSlider");
38008
+ __publicField(this, "_absoluteSlider");
38009
+ __publicField(this, "_computedBounds");
38010
+ this._options = { ...options };
38011
+ this._computedBounds = options.computedBounds;
38012
+ }
38013
+ /**
38014
+ * Renders the control component.
38015
+ *
38016
+ * @returns The control container element
38017
+ */
38018
+ render() {
38019
+ const container = document.createElement("div");
38020
+ container.className = "lidar-color-range";
38021
+ const labelRow = document.createElement("div");
38022
+ labelRow.className = "lidar-color-range-header";
38023
+ const label = document.createElement("div");
38024
+ label.className = "lidar-control-label";
38025
+ label.textContent = "Color Range";
38026
+ labelRow.appendChild(label);
38027
+ const resetButton = document.createElement("button");
38028
+ resetButton.className = "lidar-range-reset-btn";
38029
+ resetButton.textContent = "Reset";
38030
+ resetButton.title = "Reset to default (2-98% percentile)";
38031
+ resetButton.addEventListener("click", () => this._onReset());
38032
+ labelRow.appendChild(resetButton);
38033
+ container.appendChild(labelRow);
38034
+ const modeContainer = document.createElement("div");
38035
+ modeContainer.className = "lidar-range-mode";
38036
+ const percentileLabel = document.createElement("label");
38037
+ const percentileRadio = document.createElement("input");
38038
+ percentileRadio.type = "radio";
38039
+ percentileRadio.name = "lidar-range-mode";
38040
+ percentileRadio.value = "percentile";
38041
+ percentileRadio.checked = this._options.config.mode === "percentile";
38042
+ this._percentileRadio = percentileRadio;
38043
+ percentileLabel.appendChild(percentileRadio);
38044
+ percentileLabel.appendChild(document.createTextNode(" Percentile"));
38045
+ modeContainer.appendChild(percentileLabel);
38046
+ const absoluteLabel = document.createElement("label");
38047
+ const absoluteRadio = document.createElement("input");
38048
+ absoluteRadio.type = "radio";
38049
+ absoluteRadio.name = "lidar-range-mode";
38050
+ absoluteRadio.value = "absolute";
38051
+ absoluteRadio.checked = this._options.config.mode === "absolute";
38052
+ this._absoluteRadio = absoluteRadio;
38053
+ absoluteLabel.appendChild(absoluteRadio);
38054
+ absoluteLabel.appendChild(document.createTextNode(" Absolute"));
38055
+ modeContainer.appendChild(absoluteLabel);
38056
+ container.appendChild(modeContainer);
38057
+ const percentileSliderContainer = document.createElement("div");
38058
+ percentileSliderContainer.style.display = this._options.config.mode === "percentile" ? "block" : "none";
38059
+ this._percentileSliderContainer = percentileSliderContainer;
38060
+ this._percentileSlider = new DualRangeSlider({
38061
+ label: "",
38062
+ min: 0,
38063
+ max: 100,
38064
+ step: 1,
38065
+ valueLow: this._options.config.percentileLow ?? DEFAULT_PERCENTILE_LOW,
38066
+ valueHigh: this._options.config.percentileHigh ?? DEFAULT_PERCENTILE_HIGH,
38067
+ onChange: (low, high) => this._onPercentileChange(low, high),
38068
+ formatValue: (v) => `${v.toFixed(0)}%`
38069
+ });
38070
+ percentileSliderContainer.appendChild(this._percentileSlider.render());
38071
+ container.appendChild(percentileSliderContainer);
38072
+ const absoluteSliderContainer = document.createElement("div");
38073
+ absoluteSliderContainer.style.display = this._options.config.mode === "absolute" ? "block" : "none";
38074
+ this._absoluteSliderContainer = absoluteSliderContainer;
38075
+ const dataBounds = this._options.dataBounds || { min: 0, max: 100 };
38076
+ const absMin = this._options.config.absoluteMin ?? dataBounds.min;
38077
+ const absMax = this._options.config.absoluteMax ?? dataBounds.max;
38078
+ this._absoluteSlider = new DualRangeSlider({
38079
+ label: "",
38080
+ min: dataBounds.min,
38081
+ max: dataBounds.max,
38082
+ step: this._getAbsoluteStep(dataBounds),
38083
+ valueLow: absMin,
38084
+ valueHigh: absMax,
38085
+ onChange: (low, high) => this._onAbsoluteChange(low, high),
38086
+ formatValue: (v) => this._formatAbsoluteValue(v)
38087
+ });
38088
+ absoluteSliderContainer.appendChild(this._absoluteSlider.render());
38089
+ container.appendChild(absoluteSliderContainer);
38090
+ percentileRadio.addEventListener("change", () => this._onModeChange());
38091
+ absoluteRadio.addEventListener("change", () => this._onModeChange());
38092
+ return container;
38093
+ }
38094
+ /**
38095
+ * Updates the control with new configuration.
38096
+ *
38097
+ * @param config - New color range configuration
38098
+ */
38099
+ setConfig(config) {
38100
+ this._options.config = { ...config };
38101
+ if (this._percentileRadio && this._absoluteRadio) {
38102
+ this._percentileRadio.checked = config.mode === "percentile";
38103
+ this._absoluteRadio.checked = config.mode === "absolute";
38104
+ }
38105
+ if (this._percentileSlider) {
38106
+ this._percentileSlider.setRange(
38107
+ config.percentileLow ?? DEFAULT_PERCENTILE_LOW,
38108
+ config.percentileHigh ?? DEFAULT_PERCENTILE_HIGH
38109
+ );
38110
+ }
38111
+ if (this._absoluteSlider) {
38112
+ const dataBounds = this._options.dataBounds || { min: 0, max: 100 };
38113
+ this._absoluteSlider.setRange(
38114
+ config.absoluteMin ?? dataBounds.min,
38115
+ config.absoluteMax ?? dataBounds.max
38116
+ );
38117
+ }
38118
+ this._updateSlidersVisibility();
38119
+ }
38120
+ /**
38121
+ * Updates the data bounds (for absolute mode reference).
38122
+ *
38123
+ * @param bounds - Data bounds
38124
+ */
38125
+ setDataBounds(bounds2) {
38126
+ this._options.dataBounds = bounds2;
38127
+ if (this._absoluteSlider) {
38128
+ this._absoluteSlider.setBounds(bounds2.min, bounds2.max);
38129
+ this._absoluteSlider.setStep(this._getAbsoluteStep(bounds2));
38130
+ if (this._options.config.absoluteMin === void 0) {
38131
+ this._options.config.absoluteMin = bounds2.min;
38132
+ }
38133
+ if (this._options.config.absoluteMax === void 0) {
38134
+ this._options.config.absoluteMax = bounds2.max;
38135
+ }
38136
+ const currentMin = this._options.config.absoluteMin ?? bounds2.min;
38137
+ const currentMax = this._options.config.absoluteMax ?? bounds2.max;
38138
+ const clampedMin = Math.max(bounds2.min, Math.min(bounds2.max, currentMin));
38139
+ const clampedMax = Math.max(bounds2.min, Math.min(bounds2.max, currentMax));
38140
+ this._absoluteSlider.setRange(clampedMin, clampedMax);
38141
+ }
38142
+ }
38143
+ /**
38144
+ * Sets the actual computed bounds from percentile calculation.
38145
+ * These bounds are used when switching from percentile to absolute mode.
38146
+ *
38147
+ * @param bounds - The actual computed bounds
38148
+ */
38149
+ setComputedBounds(bounds2) {
38150
+ this._computedBounds = bounds2;
38151
+ }
38152
+ /**
38153
+ * Gets the current configuration.
38154
+ *
38155
+ * @returns The current color range configuration
38156
+ */
38157
+ getConfig() {
38158
+ return { ...this._options.config };
38159
+ }
38160
+ /**
38161
+ * Handles percentile slider change.
38162
+ */
38163
+ _onPercentileChange(low, high) {
38164
+ this._options.config.percentileLow = low;
38165
+ this._options.config.percentileHigh = high;
38166
+ this._emitChange();
38167
+ }
38168
+ /**
38169
+ * Handles absolute slider change.
38170
+ */
38171
+ _onAbsoluteChange(low, high) {
38172
+ this._options.config.absoluteMin = low;
38173
+ this._options.config.absoluteMax = high;
38174
+ this._emitChange();
38175
+ }
38176
+ /**
38177
+ * Handles mode change (percentile/absolute toggle).
38178
+ * Syncs values when switching between modes using actual computed bounds.
38179
+ */
38180
+ _onModeChange() {
38181
+ var _a;
38182
+ const newMode = ((_a = this._percentileRadio) == null ? void 0 : _a.checked) ? "percentile" : "absolute";
38183
+ const oldMode = this._options.config.mode;
38184
+ if (newMode !== oldMode) {
38185
+ if (newMode === "absolute") {
38186
+ if (this._computedBounds) {
38187
+ this._options.config.absoluteMin = this._computedBounds.min;
38188
+ this._options.config.absoluteMax = this._computedBounds.max;
38189
+ } else if (this._options.dataBounds) {
38190
+ const { min: dataMin, max: dataMax } = this._options.dataBounds;
38191
+ const range = dataMax - dataMin;
38192
+ const pLow = this._options.config.percentileLow ?? DEFAULT_PERCENTILE_LOW;
38193
+ const pHigh = this._options.config.percentileHigh ?? DEFAULT_PERCENTILE_HIGH;
38194
+ this._options.config.absoluteMin = parseFloat((dataMin + range * (pLow / 100)).toFixed(2));
38195
+ this._options.config.absoluteMax = parseFloat((dataMin + range * (pHigh / 100)).toFixed(2));
38196
+ }
38197
+ if (this._absoluteSlider && this._options.config.absoluteMin !== void 0 && this._options.config.absoluteMax !== void 0) {
38198
+ this._absoluteSlider.setRange(this._options.config.absoluteMin, this._options.config.absoluteMax);
38199
+ }
38200
+ }
38201
+ }
38202
+ this._options.config.mode = newMode;
38203
+ this._updateSlidersVisibility();
38204
+ this._emitChange();
38205
+ }
38206
+ /**
38207
+ * Handles reset button click.
38208
+ * Resets to default percentile mode with 2-98% range.
38209
+ */
38210
+ _onReset() {
38211
+ this._options.config.mode = "percentile";
38212
+ this._options.config.percentileLow = DEFAULT_PERCENTILE_LOW;
38213
+ this._options.config.percentileHigh = DEFAULT_PERCENTILE_HIGH;
38214
+ if (this._options.dataBounds) {
38215
+ const { min: dataMin, max: dataMax } = this._options.dataBounds;
38216
+ const range = dataMax - dataMin;
38217
+ this._options.config.absoluteMin = parseFloat((dataMin + range * (DEFAULT_PERCENTILE_LOW / 100)).toFixed(2));
38218
+ this._options.config.absoluteMax = parseFloat((dataMin + range * (DEFAULT_PERCENTILE_HIGH / 100)).toFixed(2));
38219
+ }
38220
+ if (this._percentileRadio) {
38221
+ this._percentileRadio.checked = true;
38222
+ }
38223
+ if (this._absoluteRadio) {
38224
+ this._absoluteRadio.checked = false;
38225
+ }
38226
+ if (this._percentileSlider) {
38227
+ this._percentileSlider.setRange(DEFAULT_PERCENTILE_LOW, DEFAULT_PERCENTILE_HIGH);
38228
+ }
38229
+ if (this._absoluteSlider && this._options.config.absoluteMin !== void 0 && this._options.config.absoluteMax !== void 0) {
38230
+ this._absoluteSlider.setRange(this._options.config.absoluteMin, this._options.config.absoluteMax);
38231
+ }
38232
+ this._updateSlidersVisibility();
38233
+ this._emitChange();
38234
+ }
38235
+ /**
38236
+ * Updates the visibility of slider containers based on mode.
38237
+ */
38238
+ _updateSlidersVisibility() {
38239
+ if (this._percentileSliderContainer) {
38240
+ this._percentileSliderContainer.style.display = this._options.config.mode === "percentile" ? "block" : "none";
38241
+ }
38242
+ if (this._absoluteSliderContainer) {
38243
+ this._absoluteSliderContainer.style.display = this._options.config.mode === "absolute" ? "block" : "none";
38244
+ }
38245
+ }
38246
+ /**
38247
+ * Gets the appropriate step value for absolute slider based on data range.
38248
+ */
38249
+ _getAbsoluteStep(bounds2) {
38250
+ const range = bounds2.max - bounds2.min;
38251
+ if (range <= 1) {
38252
+ return 0.01;
38253
+ } else if (range <= 10) {
38254
+ return 0.1;
38255
+ } else if (range <= 100) {
38256
+ return 1;
38257
+ } else if (range <= 1e3) {
38258
+ return 1;
38259
+ } else {
38260
+ return Math.round(range / 100);
38261
+ }
38262
+ }
38263
+ /**
38264
+ * Formats absolute value for display based on the data range.
38265
+ */
38266
+ _formatAbsoluteValue(value) {
38267
+ const bounds2 = this._options.dataBounds || { min: 0, max: 100 };
38268
+ const range = bounds2.max - bounds2.min;
38269
+ if (range <= 1) {
38270
+ return value.toFixed(2);
38271
+ } else if (range <= 10) {
38272
+ return value.toFixed(1);
38273
+ } else {
38274
+ return value.toFixed(0);
38275
+ }
38276
+ }
38277
+ /**
38278
+ * Emits a change event with the current configuration.
38279
+ */
38280
+ _emitChange() {
38281
+ this._options.onChange({ ...this._options.config });
38282
+ }
38283
+ }
37313
38284
  class PanelBuilder {
37314
38285
  constructor(callbacks, initialState) {
37315
38286
  __publicField(this, "_callbacks");
@@ -37320,6 +38291,12 @@ class PanelBuilder {
37320
38291
  __publicField(this, "_urlInput");
37321
38292
  __publicField(this, "_loadButton");
37322
38293
  __publicField(this, "_colorSelect");
38294
+ __publicField(this, "_colormapSelect");
38295
+ __publicField(this, "_colormapGroup");
38296
+ __publicField(this, "_colorbar");
38297
+ __publicField(this, "_colorbarContainer");
38298
+ __publicField(this, "_colorRangeControl");
38299
+ __publicField(this, "_colorRangeContainer");
37323
38300
  __publicField(this, "_percentileCheckbox");
37324
38301
  __publicField(this, "_percentileGroup");
37325
38302
  __publicField(this, "_pointSizeSlider");
@@ -37395,6 +38372,25 @@ class PanelBuilder {
37395
38372
  this._colorSelect.value = state.colorScheme;
37396
38373
  this._updatePercentileVisibility(state.colorScheme);
37397
38374
  }
38375
+ if (this._colormapSelect && state.colormap) {
38376
+ this._colormapSelect.value = state.colormap;
38377
+ }
38378
+ if (this._colorbar) {
38379
+ if (state.colormap) {
38380
+ this._colorbar.setColormap(state.colormap);
38381
+ }
38382
+ if (state.computedColorBounds) {
38383
+ this._colorbar.setRange(state.computedColorBounds.min, state.computedColorBounds.max);
38384
+ }
38385
+ }
38386
+ if (this._colorRangeControl && state.colorRange) {
38387
+ this._colorRangeControl.setConfig(state.colorRange);
38388
+ const bounds2 = this._getDataBoundsForCurrentScheme();
38389
+ this._colorRangeControl.setDataBounds(bounds2);
38390
+ if (state.computedColorBounds) {
38391
+ this._colorRangeControl.setComputedBounds(state.computedColorBounds);
38392
+ }
38393
+ }
37398
38394
  if (this._percentileCheckbox) {
37399
38395
  this._percentileCheckbox.checked = state.usePercentile ?? true;
37400
38396
  }
@@ -37535,8 +38531,10 @@ class PanelBuilder {
37535
38531
  this._colorSelect = colorSelect;
37536
38532
  colorGroup.appendChild(colorSelect);
37537
38533
  section.appendChild(colorGroup);
38534
+ section.appendChild(this._buildColormapSelector());
38535
+ section.appendChild(this._buildColorbar());
37538
38536
  section.appendChild(this._buildClassificationLegend());
37539
- section.appendChild(this._buildPercentileCheckbox());
38537
+ section.appendChild(this._buildColorRangeControl());
37540
38538
  this._pointSizeSlider = new RangeSlider({
37541
38539
  label: "Point Size",
37542
38540
  min: 1,
@@ -37721,46 +38719,120 @@ class PanelBuilder {
37721
38719
  return { min: minZ, max: maxZ };
37722
38720
  }
37723
38721
  /**
37724
- * Builds the percentile checkbox control for elevation/intensity coloring.
38722
+ * Gets the intensity bounds.
38723
+ * Intensity values are normalized to 0-1 range during loading.
38724
+ */
38725
+ _getIntensityBounds() {
38726
+ return { min: 0, max: 1 };
38727
+ }
38728
+ /**
38729
+ * Gets the appropriate data bounds based on the current color scheme.
37725
38730
  */
37726
- _buildPercentileCheckbox() {
38731
+ _getDataBoundsForCurrentScheme() {
38732
+ const colorScheme = typeof this._state.colorScheme === "string" ? this._state.colorScheme : "elevation";
38733
+ if (colorScheme === "intensity") {
38734
+ return this._getIntensityBounds();
38735
+ }
38736
+ return this._getElevationBounds();
38737
+ }
38738
+ /**
38739
+ * Builds the colormap selector dropdown.
38740
+ */
38741
+ _buildColormapSelector() {
37727
38742
  const group = document.createElement("div");
37728
- group.className = "lidar-control-group";
37729
- this._percentileGroup = group;
37730
- const labelRow = document.createElement("div");
37731
- labelRow.className = "lidar-control-label-row";
37732
- labelRow.style.cursor = "pointer";
37733
- const checkbox = document.createElement("input");
37734
- checkbox.type = "checkbox";
37735
- checkbox.id = "lidar-percentile-checkbox";
37736
- checkbox.checked = this._state.usePercentile ?? true;
37737
- checkbox.style.marginRight = "6px";
37738
- this._percentileCheckbox = checkbox;
38743
+ group.className = "lidar-colormap-group";
38744
+ this._colormapGroup = group;
38745
+ const currentScheme = typeof this._state.colorScheme === "string" ? this._state.colorScheme : "elevation";
38746
+ const showColormap = currentScheme === "elevation" || currentScheme === "intensity";
38747
+ group.style.display = showColormap ? "block" : "none";
37739
38748
  const label = document.createElement("label");
37740
38749
  label.className = "lidar-control-label";
37741
- label.htmlFor = "lidar-percentile-checkbox";
37742
- label.style.display = "inline";
37743
- label.style.cursor = "pointer";
37744
- label.textContent = "Use percentile range (2-98%)";
37745
- label.title = "Clip outliers for better color distribution";
37746
- checkbox.addEventListener("change", () => {
37747
- this._callbacks.onUsePercentileChange(checkbox.checked);
38750
+ label.textContent = "Colormap";
38751
+ group.appendChild(label);
38752
+ const select = document.createElement("select");
38753
+ select.className = "lidar-colormap-select";
38754
+ for (const name of COLORMAP_NAMES) {
38755
+ const option = document.createElement("option");
38756
+ option.value = name;
38757
+ option.textContent = COLORMAP_LABELS[name];
38758
+ select.appendChild(option);
38759
+ }
38760
+ select.value = this._state.colormap || "viridis";
38761
+ this._colormapSelect = select;
38762
+ select.addEventListener("change", () => {
38763
+ const colormap = select.value;
38764
+ this._callbacks.onColormapChange(colormap);
37748
38765
  });
37749
- labelRow.appendChild(checkbox);
37750
- labelRow.appendChild(label);
37751
- group.appendChild(labelRow);
37752
- const currentScheme = typeof this._state.colorScheme === "string" ? this._state.colorScheme : "elevation";
37753
- this._updatePercentileVisibility(currentScheme);
38766
+ group.appendChild(select);
37754
38767
  return group;
37755
38768
  }
37756
38769
  /**
37757
- * Updates the visibility of the percentile checkbox and classification legend based on color scheme.
37758
- * Percentile shows for elevation and intensity. Legend shows for classification.
38770
+ * Builds the colorbar component.
38771
+ */
38772
+ _buildColorbar() {
38773
+ var _a, _b;
38774
+ const container = document.createElement("div");
38775
+ container.className = "lidar-control-group";
38776
+ this._colorbarContainer = container;
38777
+ const currentScheme = typeof this._state.colorScheme === "string" ? this._state.colorScheme : "elevation";
38778
+ const showColorbar = (currentScheme === "elevation" || currentScheme === "intensity") && this._state.showColorbar;
38779
+ container.style.display = showColorbar ? "block" : "none";
38780
+ this._colorbar = new Colorbar({
38781
+ colormap: this._state.colormap || "viridis",
38782
+ minValue: ((_a = this._state.computedColorBounds) == null ? void 0 : _a.min) ?? 0,
38783
+ maxValue: ((_b = this._state.computedColorBounds) == null ? void 0 : _b.max) ?? 100
38784
+ });
38785
+ container.appendChild(this._colorbar.render());
38786
+ return container;
38787
+ }
38788
+ /**
38789
+ * Builds the color range control (replaces percentile checkbox).
38790
+ */
38791
+ _buildColorRangeControl() {
38792
+ const container = document.createElement("div");
38793
+ container.className = "lidar-control-group";
38794
+ this._colorRangeContainer = container;
38795
+ const currentScheme = typeof this._state.colorScheme === "string" ? this._state.colorScheme : "elevation";
38796
+ const showControl = currentScheme === "elevation" || currentScheme === "intensity";
38797
+ container.style.display = showControl ? "block" : "none";
38798
+ const dataBounds = this._getDataBoundsForCurrentScheme();
38799
+ this._colorRangeControl = new PercentileRangeControl({
38800
+ config: this._state.colorRange || {
38801
+ mode: "percentile",
38802
+ percentileLow: 2,
38803
+ percentileHigh: 98
38804
+ },
38805
+ dataBounds,
38806
+ computedBounds: this._state.computedColorBounds,
38807
+ onChange: (config) => {
38808
+ this._callbacks.onColorRangeChange(config);
38809
+ }
38810
+ });
38811
+ container.appendChild(this._colorRangeControl.render());
38812
+ return container;
38813
+ }
38814
+ /**
38815
+ * Updates the visibility of color-related controls based on color scheme.
38816
+ * Shows colormap/colorbar/range for elevation and intensity.
38817
+ * Shows classification legend for classification.
37759
38818
  */
37760
38819
  _updatePercentileVisibility(colorScheme) {
38820
+ const showColorControls = colorScheme === "elevation" || colorScheme === "intensity";
38821
+ if (this._colormapGroup) {
38822
+ this._colormapGroup.style.display = showColorControls ? "block" : "none";
38823
+ }
38824
+ if (this._colorbarContainer) {
38825
+ this._colorbarContainer.style.display = showColorControls && this._state.showColorbar ? "block" : "none";
38826
+ }
38827
+ if (this._colorRangeContainer) {
38828
+ this._colorRangeContainer.style.display = showColorControls ? "block" : "none";
38829
+ }
38830
+ if (this._colorRangeControl && showColorControls) {
38831
+ const bounds2 = this._getDataBoundsForCurrentScheme();
38832
+ this._colorRangeControl.setDataBounds(bounds2);
38833
+ }
37761
38834
  if (this._percentileGroup) {
37762
- const showPercentile = colorScheme === "elevation" || colorScheme === "intensity";
37763
- this._percentileGroup.style.display = showPercentile ? "block" : "none";
38835
+ this._percentileGroup.style.display = "none";
37764
38836
  }
37765
38837
  if (this._classificationLegendContainer) {
37766
38838
  this._classificationLegendContainer.style.display = colorScheme === "classification" ? "block" : "none";
@@ -37927,6 +38999,9 @@ const DEFAULT_OPTIONS = {
37927
38999
  opacity: 1,
37928
39000
  colorScheme: "elevation",
37929
39001
  usePercentile: true,
39002
+ colormap: "viridis",
39003
+ colorRange: { mode: "percentile", percentileLow: 2, percentileHigh: 98 },
39004
+ showColorbar: true,
37930
39005
  pointBudget: 1e6,
37931
39006
  elevationRange: null,
37932
39007
  pickable: false,
@@ -37973,7 +39048,14 @@ class LidarControl {
37973
39048
  __publicField(this, "_streamingLoaders", /* @__PURE__ */ new Map());
37974
39049
  __publicField(this, "_eptStreamingLoaders", /* @__PURE__ */ new Map());
37975
39050
  __publicField(this, "_viewportManagers", /* @__PURE__ */ new Map());
39051
+ __publicField(this, "_eptViewportRequestIds", /* @__PURE__ */ new Map());
39052
+ __publicField(this, "_eptLastViewport", /* @__PURE__ */ new Map());
37976
39053
  this._options = { ...DEFAULT_OPTIONS, ...options };
39054
+ const defaultColorRange = this._options.colorRange ?? {
39055
+ mode: "percentile",
39056
+ percentileLow: 2,
39057
+ percentileHigh: 98
39058
+ };
37977
39059
  this._state = {
37978
39060
  collapsed: this._options.collapsed,
37979
39061
  panelWidth: this._options.panelWidth,
@@ -37983,6 +39065,9 @@ class LidarControl {
37983
39065
  pointSize: this._options.pointSize,
37984
39066
  opacity: this._options.opacity,
37985
39067
  colorScheme: this._options.colorScheme,
39068
+ colormap: this._options.colormap ?? "viridis",
39069
+ colorRange: defaultColorRange,
39070
+ showColorbar: this._options.showColorbar ?? true,
37986
39071
  usePercentile: this._options.usePercentile,
37987
39072
  elevationRange: this._options.elevationRange,
37988
39073
  pointBudget: this._options.pointBudget,
@@ -38178,7 +39263,7 @@ class LidarControl {
38178
39263
  * @returns Promise resolving to the point cloud info
38179
39264
  */
38180
39265
  async loadPointCloud(source, options) {
38181
- var _a, _b, _c;
39266
+ var _a, _b, _c, _d;
38182
39267
  const isEptUrl = typeof source === "string" && (source.endsWith("/ept.json") || source.includes("/ept.json?"));
38183
39268
  if (isEptUrl) {
38184
39269
  return this.loadPointCloudEptStreaming(source);
@@ -38253,6 +39338,8 @@ class LidarControl {
38253
39338
  zOffset: zOffset ?? this._state.zOffset,
38254
39339
  zOffsetEnabled
38255
39340
  });
39341
+ this._updateComputedColorBounds();
39342
+ (_c = this._panelBuilder) == null ? void 0 : _c.updateState(this._state);
38256
39343
  this._emitWithData("load", { pointCloud: info2 });
38257
39344
  if (this._options.autoZoom) {
38258
39345
  this.flyToPointCloud(id);
@@ -38266,7 +39353,7 @@ class LidarControl {
38266
39353
  console.warn(
38267
39354
  `CORS error detected for ${source}. Falling back to download mode...`
38268
39355
  );
38269
- (_c = this._panelBuilder) == null ? void 0 : _c.updateLoadingProgress(5, "CORS blocked - downloading file...");
39356
+ (_d = this._panelBuilder) == null ? void 0 : _d.updateLoadingProgress(5, "CORS blocked - downloading file...");
38270
39357
  return this._loadPointCloudFullDownload(source);
38271
39358
  }
38272
39359
  this.setState({
@@ -38324,7 +39411,7 @@ class LidarControl {
38324
39411
  * @returns Promise resolving to initial point cloud info
38325
39412
  */
38326
39413
  async loadPointCloudStreaming(source, options) {
38327
- var _a, _b, _c, _d;
39414
+ var _a, _b, _c, _d, _e;
38328
39415
  const id = generateId("pc-stream");
38329
39416
  let name;
38330
39417
  if (typeof source === "string") {
@@ -38429,12 +39516,18 @@ class LidarControl {
38429
39516
  pointClouds,
38430
39517
  activePointCloudId: id
38431
39518
  });
39519
+ this._updateComputedColorBounds();
39520
+ (_c = this._panelBuilder) == null ? void 0 : _c.updateState(this._state);
38432
39521
  viewportManager.start();
38433
39522
  if (this._options.autoZoom) {
38434
- (_c = this._map) == null ? void 0 : _c.fitBounds(
39523
+ const clampedMinY = Math.max(-90, Math.min(90, bounds2.minY));
39524
+ const clampedMaxY = Math.max(-90, Math.min(90, bounds2.maxY));
39525
+ const clampedMinX = Math.max(-180, Math.min(180, bounds2.minX));
39526
+ const clampedMaxX = Math.max(-180, Math.min(180, bounds2.maxX));
39527
+ (_d = this._map) == null ? void 0 : _d.fitBounds(
38435
39528
  [
38436
- [bounds2.minX, bounds2.minY],
38437
- [bounds2.maxX, bounds2.maxY]
39529
+ [clampedMinX, clampedMinY],
39530
+ [clampedMaxX, clampedMaxY]
38438
39531
  ],
38439
39532
  {
38440
39533
  padding: 50,
@@ -38470,7 +39563,7 @@ class LidarControl {
38470
39563
  streamingActive: hasActiveStreaming,
38471
39564
  error: null
38472
39565
  });
38473
- (_d = this._panelBuilder) == null ? void 0 : _d.updateLoadingProgress(5, "CORS blocked - downloading file...");
39566
+ (_e = this._panelBuilder) == null ? void 0 : _e.updateLoadingProgress(5, "CORS blocked - downloading file...");
38474
39567
  return this._loadPointCloudFullDownload(source);
38475
39568
  }
38476
39569
  this.setState({
@@ -38491,7 +39584,7 @@ class LidarControl {
38491
39584
  * @returns Promise resolving to initial point cloud info
38492
39585
  */
38493
39586
  async loadPointCloudEptStreaming(eptUrl, options) {
38494
- var _a, _b, _c, _d;
39587
+ var _a, _b, _c, _d, _e;
38495
39588
  const id = generateId("ept-stream");
38496
39589
  const name = getFilename(eptUrl.replace("/ept.json", ""));
38497
39590
  this.setState({ loading: true, error: null, streamingActive: true });
@@ -38589,12 +39682,18 @@ class LidarControl {
38589
39682
  pointClouds,
38590
39683
  activePointCloudId: id
38591
39684
  });
39685
+ this._updateComputedColorBounds();
39686
+ (_d = this._panelBuilder) == null ? void 0 : _d.updateState(this._state);
38592
39687
  viewportManager.start();
38593
39688
  if (this._options.autoZoom) {
38594
- (_d = this._map) == null ? void 0 : _d.fitBounds(
39689
+ const clampedMinY = Math.max(-90, Math.min(90, bounds2.minY));
39690
+ const clampedMaxY = Math.max(-90, Math.min(90, bounds2.maxY));
39691
+ const clampedMinX = Math.max(-180, Math.min(180, bounds2.minX));
39692
+ const clampedMaxX = Math.max(-180, Math.min(180, bounds2.maxX));
39693
+ (_e = this._map) == null ? void 0 : _e.fitBounds(
38595
39694
  [
38596
- [bounds2.minX, bounds2.minY],
38597
- [bounds2.maxX, bounds2.maxY]
39695
+ [clampedMinX, clampedMinY],
39696
+ [clampedMaxX, clampedMaxY]
38598
39697
  ],
38599
39698
  {
38600
39699
  padding: 50,
@@ -38614,6 +39713,8 @@ class LidarControl {
38614
39713
  eptLoader.destroy();
38615
39714
  this._eptStreamingLoaders.delete(id);
38616
39715
  }
39716
+ this._eptViewportRequestIds.delete(id);
39717
+ this._eptLastViewport.delete(id);
38617
39718
  const viewportManager = this._viewportManagers.get(id);
38618
39719
  if (viewportManager) {
38619
39720
  viewportManager.destroy();
@@ -38635,15 +39736,77 @@ class LidarControl {
38635
39736
  * @param viewport - Current viewport information
38636
39737
  * @param datasetId - ID of the EPT dataset
38637
39738
  */
38638
- async _handleViewportChangeForEptStreaming(viewport, datasetId) {
39739
+ _shouldResetEptForViewportChange(previous, current) {
39740
+ if (!previous) return false;
39741
+ const [prevWest, prevSouth, prevEast, prevNorth] = previous.bounds;
39742
+ const [curWest, curSouth, curEast, curNorth] = current.bounds;
39743
+ const intersects = !(curEast < prevWest || curWest > prevEast || curNorth < prevSouth || curSouth > prevNorth);
39744
+ if (!intersects) return true;
39745
+ const prevWidth = prevEast - prevWest;
39746
+ const prevHeight = prevNorth - prevSouth;
39747
+ const dx = current.center[0] - previous.center[0];
39748
+ const dy = current.center[1] - previous.center[1];
39749
+ const centerDistance = Math.sqrt(dx * dx + dy * dy);
39750
+ const threshold = Math.max(prevWidth, prevHeight) * 0.3;
39751
+ return centerDistance > threshold;
39752
+ }
39753
+ async _handleViewportChangeForEptStreaming(viewport, datasetId, requestId) {
38639
39754
  const eptLoader = this._eptStreamingLoaders.get(datasetId);
38640
39755
  if (!eptLoader) return;
38641
39756
  try {
38642
- const nodesToLoad = await eptLoader.selectNodesForViewport(viewport);
39757
+ const currentRequestId = requestId ?? (this._eptViewportRequestIds.get(datasetId) ?? 0) + 1;
39758
+ if (requestId === void 0) {
39759
+ this._eptViewportRequestIds.set(datasetId, currentRequestId);
39760
+ }
39761
+ if (this._eptViewportRequestIds.get(datasetId) !== currentRequestId) return;
39762
+ const previousViewport = this._eptLastViewport.get(datasetId);
39763
+ const shouldResetForMove = this._shouldResetEptForViewportChange(previousViewport, viewport);
39764
+ this._eptLastViewport.set(datasetId, viewport);
39765
+ eptLoader.pruneQueueForViewport(viewport);
39766
+ if (shouldResetForMove) {
39767
+ const resetSucceeded2 = eptLoader.resetLoadedData();
39768
+ if (!resetSucceeded2) {
39769
+ setTimeout(() => {
39770
+ this._handleViewportChangeForEptStreaming(viewport, datasetId, currentRequestId);
39771
+ }, 200);
39772
+ return;
39773
+ }
39774
+ }
39775
+ let nodesToLoad = await eptLoader.selectNodesForViewport(viewport);
39776
+ let resetSucceeded = false;
39777
+ const loadedPoints = eptLoader.getLoadedPointCount();
39778
+ const pointBudget = eptLoader.getPointBudget();
39779
+ const budgetReached = loadedPoints >= pointBudget * 0.8;
39780
+ const minDepthForCoverage = Math.max(0, viewport.targetDepth - 2);
39781
+ const coverageRatio = eptLoader.getViewportCoverageRatio(viewport, minDepthForCoverage);
39782
+ const needsCoverage = coverageRatio < 0.5;
39783
+ const hasPendingSubtrees = eptLoader.hasPendingSubtrees(viewport);
39784
+ const hasPendingWork = nodesToLoad.length > 0 || hasPendingSubtrees;
39785
+ if (budgetReached && needsCoverage && hasPendingWork) {
39786
+ resetSucceeded = eptLoader.resetLoadedData();
39787
+ if (resetSucceeded) {
39788
+ nodesToLoad = await eptLoader.selectNodesForViewport(viewport);
39789
+ }
39790
+ }
39791
+ if (!budgetReached && coverageRatio < 0.1 && nodesToLoad.length === 0) {
39792
+ nodesToLoad = await eptLoader.selectNodesForViewport(viewport);
39793
+ }
38643
39794
  for (const node of nodesToLoad) {
38644
39795
  eptLoader.queueNode(node);
38645
39796
  }
38646
39797
  await eptLoader.loadQueuedNodes();
39798
+ if (this._eptViewportRequestIds.get(datasetId) !== currentRequestId) return;
39799
+ if (budgetReached && needsCoverage && nodesToLoad.length > 0 && !resetSucceeded) {
39800
+ setTimeout(() => {
39801
+ this._handleViewportChangeForEptStreaming(viewport, datasetId, currentRequestId);
39802
+ }, 200);
39803
+ return;
39804
+ }
39805
+ if (hasPendingSubtrees) {
39806
+ setTimeout(() => {
39807
+ this._handleViewportChangeForEptStreaming(viewport, datasetId, currentRequestId);
39808
+ }, 100);
39809
+ }
38647
39810
  } catch (err) {
38648
39811
  console.warn("Failed to load EPT nodes for viewport:", err);
38649
39812
  }
@@ -38653,7 +39816,7 @@ class LidarControl {
38653
39816
  * Used as fallback when streaming fails due to CORS.
38654
39817
  */
38655
39818
  async _loadPointCloudFullDownload(url) {
38656
- var _a, _b, _c, _d, _e, _f, _g, _h;
39819
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
38657
39820
  const id = generateId("pc");
38658
39821
  const name = getFilename(url);
38659
39822
  try {
@@ -38740,6 +39903,8 @@ class LidarControl {
38740
39903
  zOffset: zOffset ?? this._state.zOffset,
38741
39904
  zOffsetEnabled
38742
39905
  });
39906
+ this._updateComputedColorBounds();
39907
+ (_i = this._panelBuilder) == null ? void 0 : _i.updateState(this._state);
38743
39908
  this._emitWithData("load", { pointCloud: info2 });
38744
39909
  if (this._options.autoZoom) {
38745
39910
  this.flyToPointCloud(id);
@@ -38836,6 +40001,8 @@ class LidarControl {
38836
40001
  eptLoader.destroy();
38837
40002
  }
38838
40003
  this._eptStreamingLoaders.clear();
40004
+ this._eptViewportRequestIds.clear();
40005
+ this._eptLastViewport.clear();
38839
40006
  for (const streamingId of streamingIds) {
38840
40007
  (_c = this._pointCloudManager) == null ? void 0 : _c.removePointCloud(streamingId);
38841
40008
  }
@@ -38904,16 +40071,84 @@ class LidarControl {
38904
40071
  }
38905
40072
  /**
38906
40073
  * Sets the color scheme.
40074
+ * Automatically switches colormap and resets color range when changing between elevation and intensity.
38907
40075
  *
38908
40076
  * @param scheme - Color scheme to apply
38909
40077
  */
38910
40078
  setColorScheme(scheme) {
38911
- var _a;
40079
+ var _a, _b, _c, _d, _e;
40080
+ const previousScheme = this._state.colorScheme;
38912
40081
  this._state.colorScheme = scheme;
38913
40082
  (_a = this._pointCloudManager) == null ? void 0 : _a.setColorScheme(scheme);
40083
+ if (typeof scheme === "string" && typeof previousScheme === "string") {
40084
+ const isNewElevationOrIntensity = scheme === "elevation" || scheme === "intensity";
40085
+ const wasElevationOrIntensity = previousScheme === "elevation" || previousScheme === "intensity";
40086
+ const switchedBetweenElevationAndIntensity = isNewElevationOrIntensity && wasElevationOrIntensity && scheme !== previousScheme;
40087
+ if (switchedBetweenElevationAndIntensity) {
40088
+ this._state.colorRange = {
40089
+ mode: "percentile",
40090
+ percentileLow: 2,
40091
+ percentileHigh: 98
40092
+ };
40093
+ (_b = this._pointCloudManager) == null ? void 0 : _b.setColorRange(this._state.colorRange);
40094
+ }
40095
+ if (scheme === "intensity" && previousScheme !== "intensity") {
40096
+ this._state.colormap = "gray";
40097
+ (_c = this._pointCloudManager) == null ? void 0 : _c.setColormap("gray");
40098
+ } else if (scheme === "elevation" && previousScheme === "intensity") {
40099
+ this._state.colormap = "viridis";
40100
+ (_d = this._pointCloudManager) == null ? void 0 : _d.setColormap("viridis");
40101
+ }
40102
+ }
40103
+ this._updateComputedColorBounds();
40104
+ (_e = this._panelBuilder) == null ? void 0 : _e.updateState(this._state);
40105
+ this._emit("stylechange");
40106
+ this._emit("statechange");
40107
+ }
40108
+ /**
40109
+ * Sets the colormap for elevation/intensity coloring.
40110
+ *
40111
+ * @param colormap - The colormap name (e.g., 'viridis', 'plasma', 'turbo')
40112
+ */
40113
+ setColormap(colormap) {
40114
+ var _a, _b;
40115
+ this._state.colormap = colormap;
40116
+ (_a = this._pointCloudManager) == null ? void 0 : _a.setColormap(colormap);
40117
+ this._updateComputedColorBounds();
40118
+ (_b = this._panelBuilder) == null ? void 0 : _b.updateState(this._state);
38914
40119
  this._emit("stylechange");
38915
40120
  this._emit("statechange");
38916
40121
  }
40122
+ /**
40123
+ * Gets the current colormap.
40124
+ *
40125
+ * @returns The current colormap name
40126
+ */
40127
+ getColormap() {
40128
+ return this._state.colormap;
40129
+ }
40130
+ /**
40131
+ * Sets the color range configuration.
40132
+ *
40133
+ * @param config - The color range configuration
40134
+ */
40135
+ setColorRange(config) {
40136
+ var _a, _b;
40137
+ this._state.colorRange = config;
40138
+ (_a = this._pointCloudManager) == null ? void 0 : _a.setColorRange(config);
40139
+ this._updateComputedColorBounds();
40140
+ (_b = this._panelBuilder) == null ? void 0 : _b.updateState(this._state);
40141
+ this._emit("stylechange");
40142
+ this._emit("statechange");
40143
+ }
40144
+ /**
40145
+ * Gets the current color range configuration.
40146
+ *
40147
+ * @returns The current color range configuration
40148
+ */
40149
+ getColorRange() {
40150
+ return this._state.colorRange;
40151
+ }
38917
40152
  /**
38918
40153
  * Sets whether to use percentile range for elevation/intensity coloring.
38919
40154
  *
@@ -39123,6 +40358,50 @@ class LidarControl {
39123
40358
  handlers.forEach((handler) => handler(eventData));
39124
40359
  }
39125
40360
  }
40361
+ /**
40362
+ * Updates the computed color bounds based on the current color scheme and range settings.
40363
+ * This is used to display accurate min/max values in the colorbar.
40364
+ */
40365
+ _updateComputedColorBounds() {
40366
+ var _a;
40367
+ if (this._state.pointClouds.length === 0) {
40368
+ this._state.computedColorBounds = void 0;
40369
+ return;
40370
+ }
40371
+ const actualBounds = (_a = this._pointCloudManager) == null ? void 0 : _a.getLastComputedBounds();
40372
+ if (actualBounds) {
40373
+ this._state.computedColorBounds = actualBounds;
40374
+ } else {
40375
+ const colorScheme = typeof this._state.colorScheme === "string" ? this._state.colorScheme : "elevation";
40376
+ if (colorScheme === "intensity") {
40377
+ this._state.computedColorBounds = this._getIntensityBounds();
40378
+ } else {
40379
+ this._state.computedColorBounds = this._getElevationBounds();
40380
+ }
40381
+ }
40382
+ }
40383
+ /**
40384
+ * Gets the elevation bounds from loaded point clouds.
40385
+ */
40386
+ _getElevationBounds() {
40387
+ if (this._state.pointClouds.length === 0) {
40388
+ return { min: 0, max: 100 };
40389
+ }
40390
+ let minZ = Infinity;
40391
+ let maxZ = -Infinity;
40392
+ for (const pc of this._state.pointClouds) {
40393
+ minZ = Math.min(minZ, pc.bounds.minZ);
40394
+ maxZ = Math.max(maxZ, pc.bounds.maxZ);
40395
+ }
40396
+ return { min: minZ, max: maxZ };
40397
+ }
40398
+ /**
40399
+ * Gets the intensity bounds from loaded point clouds.
40400
+ * Intensity values are typically normalized to 0-1 range.
40401
+ */
40402
+ _getIntensityBounds() {
40403
+ return { min: 0, max: 1 };
40404
+ }
39126
40405
  /**
39127
40406
  * Creates the main container element for the control.
39128
40407
  *
@@ -39183,6 +40462,8 @@ class LidarControl {
39183
40462
  onPointSizeChange: (size) => this.setPointSize(size),
39184
40463
  onOpacityChange: (opacity) => this.setOpacity(opacity),
39185
40464
  onColorSchemeChange: (scheme) => this.setColorScheme(scheme),
40465
+ onColormapChange: (colormap) => this.setColormap(colormap),
40466
+ onColorRangeChange: (config) => this.setColorRange(config),
39186
40467
  onUsePercentileChange: (usePercentile) => this.setUsePercentile(usePercentile),
39187
40468
  onElevationRangeChange: (range) => {
39188
40469
  if (range) {
@@ -39685,6 +40966,9 @@ class LidarLayerAdapter {
39685
40966
  this._changeCallbacks = [];
39686
40967
  }
39687
40968
  }
40969
+ exports.COLORMAPS = COLORMAPS;
40970
+ exports.COLORMAP_LABELS = COLORMAP_LABELS;
40971
+ exports.COLORMAP_NAMES = COLORMAP_NAMES;
39688
40972
  exports.ColorSchemeProcessor = ColorSchemeProcessor;
39689
40973
  exports.CopcStreamingLoader = CopcStreamingLoader;
39690
40974
  exports.DeckOverlay = DeckOverlay;
@@ -39702,6 +40986,7 @@ exports.formatNumber = formatNumber;
39702
40986
  exports.formatNumericValue = formatNumericValue;
39703
40987
  exports.generateId = generateId;
39704
40988
  exports.getClassificationName = getClassificationName;
40989
+ exports.getColormap = getColormap;
39705
40990
  exports.getFilename = getFilename;
39706
40991
  exports.throttle = throttle;
39707
- //# sourceMappingURL=LidarLayerAdapter-BW2Hj7QW.cjs.map
40992
+ //# sourceMappingURL=LidarLayerAdapter-eh59KEMT.cjs.map