maplibre-gl-lidar 0.7.2 → 0.8.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 (41) hide show
  1. package/dist/{LidarLayerAdapter-kyyfQw05.js → LidarLayerAdapter-CgPF0Hvt.js} +2113 -14
  2. package/dist/LidarLayerAdapter-CgPF0Hvt.js.map +1 -0
  3. package/dist/{LidarLayerAdapter-eh59KEMT.cjs → LidarLayerAdapter-Ds5r98GR.cjs} +2102 -3
  4. package/dist/LidarLayerAdapter-Ds5r98GR.cjs.map +1 -0
  5. package/dist/index.cjs +6 -1
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.mjs +15 -10
  8. package/dist/maplibre-gl-lidar.css +427 -0
  9. package/dist/react.cjs +1 -1
  10. package/dist/react.mjs +3 -3
  11. package/dist/types/index.d.ts +5 -1
  12. package/dist/types/index.d.ts.map +1 -1
  13. package/dist/types/lib/core/DeckOverlay.d.ts +1 -0
  14. package/dist/types/lib/core/DeckOverlay.d.ts.map +1 -1
  15. package/dist/types/lib/core/LidarControl.d.ts +92 -1
  16. package/dist/types/lib/core/LidarControl.d.ts.map +1 -1
  17. package/dist/types/lib/core/types.d.ts +134 -0
  18. package/dist/types/lib/core/types.d.ts.map +1 -1
  19. package/dist/types/lib/gui/CrossSectionPanel.d.ts +135 -0
  20. package/dist/types/lib/gui/CrossSectionPanel.d.ts.map +1 -0
  21. package/dist/types/lib/gui/ElevationProfileChart.d.ts +122 -0
  22. package/dist/types/lib/gui/ElevationProfileChart.d.ts.map +1 -0
  23. package/dist/types/lib/gui/MetadataPanel.d.ts +110 -0
  24. package/dist/types/lib/gui/MetadataPanel.d.ts.map +1 -0
  25. package/dist/types/lib/gui/PanelBuilder.d.ts +8 -0
  26. package/dist/types/lib/gui/PanelBuilder.d.ts.map +1 -1
  27. package/dist/types/lib/layers/PointCloudManager.d.ts +7 -0
  28. package/dist/types/lib/layers/PointCloudManager.d.ts.map +1 -1
  29. package/dist/types/lib/loaders/CopcStreamingLoader.d.ts +20 -0
  30. package/dist/types/lib/loaders/CopcStreamingLoader.d.ts.map +1 -1
  31. package/dist/types/lib/loaders/EptStreamingLoader.d.ts +6 -0
  32. package/dist/types/lib/loaders/EptStreamingLoader.d.ts.map +1 -1
  33. package/dist/types/lib/tools/CrossSectionTool.d.ts +106 -0
  34. package/dist/types/lib/tools/CrossSectionTool.d.ts.map +1 -0
  35. package/dist/types/lib/tools/ElevationProfileExtractor.d.ts +70 -0
  36. package/dist/types/lib/tools/ElevationProfileExtractor.d.ts.map +1 -0
  37. package/dist/types/lib/tools/index.d.ts +3 -0
  38. package/dist/types/lib/tools/index.d.ts.map +1 -0
  39. package/package.json +2 -2
  40. package/dist/LidarLayerAdapter-eh59KEMT.cjs.map +0 -1
  41. package/dist/LidarLayerAdapter-kyyfQw05.js.map +0 -1
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import { MapboxOverlay } from "@deck.gl/mapbox";
5
- import { PointCloudLayer } from "@deck.gl/layers";
5
+ import { PointCloudLayer, GeoJsonLayer } from "@deck.gl/layers";
6
6
  import { COORDINATE_SYSTEM } from "@deck.gl/core";
7
7
  class DeckOverlay {
8
8
  constructor(map) {
@@ -93,10 +93,18 @@ class DeckOverlay {
93
93
  }
94
94
  /**
95
95
  * Updates the overlay with current layers.
96
+ * Layers are sorted so that overlay layers (like cross-section) render on top.
96
97
  */
97
98
  _updateOverlay() {
99
+ const sortedLayers = Array.from(this._layers.entries()).sort(([idA], [idB]) => {
100
+ const isOverlayA = idA.includes("cross-section");
101
+ const isOverlayB = idB.includes("cross-section");
102
+ if (isOverlayA && !isOverlayB) return 1;
103
+ if (!isOverlayA && isOverlayB) return -1;
104
+ return 0;
105
+ }).map(([, layer]) => layer);
98
106
  this._overlay.setProps({
99
- layers: Array.from(this._layers.values())
107
+ layers: sortedLayers
100
108
  });
101
109
  this._map.triggerRepaint();
102
110
  }
@@ -34966,6 +34974,95 @@ class CopcStreamingLoader {
34966
34974
  isLoading() {
34967
34975
  return this._activeRequests > 0 || this._loadingQueue.length > 0;
34968
34976
  }
34977
+ /**
34978
+ * Gets the full COPC metadata for the loaded file.
34979
+ *
34980
+ * @returns COPC metadata or undefined if not initialized
34981
+ */
34982
+ getCopcMetadata() {
34983
+ var _a;
34984
+ if (!this._copc) return void 0;
34985
+ const { header: header2, info: info2 } = this._copc;
34986
+ const dimensions2 = [];
34987
+ if (header2.pointDataRecordFormat !== void 0) {
34988
+ const standardDims = [
34989
+ "X",
34990
+ "Y",
34991
+ "Z",
34992
+ "Intensity",
34993
+ "ReturnNumber",
34994
+ "NumberOfReturns",
34995
+ "ScanDirectionFlag",
34996
+ "EdgeOfFlightLine",
34997
+ "Classification",
34998
+ "ScanAngleRank",
34999
+ "UserData",
35000
+ "PointSourceId"
35001
+ ];
35002
+ for (const dimName of standardDims) {
35003
+ dimensions2.push({
35004
+ name: dimName,
35005
+ type: dimName === "X" || dimName === "Y" || dimName === "Z" ? "float" : dimName === "Intensity" ? "uint16" : "uint8",
35006
+ size: dimName === "X" || dimName === "Y" || dimName === "Z" ? 8 : dimName === "Intensity" ? 2 : 1,
35007
+ scale: dimName === "X" ? header2.scale[0] : dimName === "Y" ? header2.scale[1] : dimName === "Z" ? header2.scale[2] : void 0,
35008
+ offset: dimName === "X" ? header2.offset[0] : dimName === "Y" ? header2.offset[1] : dimName === "Z" ? header2.offset[2] : void 0
35009
+ });
35010
+ }
35011
+ const colorFormats = [2, 3, 5, 7, 8, 10];
35012
+ if (colorFormats.includes(header2.pointDataRecordFormat)) {
35013
+ dimensions2.push({ name: "Red", type: "uint16", size: 2 });
35014
+ dimensions2.push({ name: "Green", type: "uint16", size: 2 });
35015
+ dimensions2.push({ name: "Blue", type: "uint16", size: 2 });
35016
+ }
35017
+ }
35018
+ return {
35019
+ lasVersion: `${header2.majorVersion}.${header2.minorVersion}`,
35020
+ pointDataRecordFormat: header2.pointDataRecordFormat,
35021
+ generatingSoftware: header2.generatingSoftware || "Unknown",
35022
+ creationDate: header2.fileCreationYear ? {
35023
+ year: header2.fileCreationYear,
35024
+ dayOfYear: header2.fileCreationDayOfYear || 1
35025
+ } : void 0,
35026
+ scale: header2.scale,
35027
+ offset: header2.offset,
35028
+ nativeBounds: {
35029
+ min: header2.min,
35030
+ max: header2.max
35031
+ },
35032
+ copcInfo: {
35033
+ spacing: info2.spacing,
35034
+ rootHierarchyOffset: ((_a = info2.rootHierarchyPage) == null ? void 0 : _a.pageOffset) || 0,
35035
+ pointSpacing: this._calculateNominalSpacing(header2)
35036
+ },
35037
+ dimensions: dimensions2
35038
+ };
35039
+ }
35040
+ /**
35041
+ * Calculates the nominal point spacing from bounding box area.
35042
+ * Uses formula: sqrt(area / pointCount) * unitFactor
35043
+ *
35044
+ * @param header - COPC header with bounds and point count
35045
+ * @returns Estimated point spacing in meters
35046
+ */
35047
+ _calculateNominalSpacing(header2) {
35048
+ const width = header2.max[0] - header2.min[0];
35049
+ const height = header2.max[1] - header2.min[1];
35050
+ const area = width * height;
35051
+ if (area <= 0 || header2.pointCount <= 0) {
35052
+ return 0;
35053
+ }
35054
+ const spacingInSourceUnits = Math.sqrt(area / header2.pointCount);
35055
+ return spacingInSourceUnits * this._verticalUnitFactor;
35056
+ }
35057
+ /**
35058
+ * Gets the WKT coordinate reference system string.
35059
+ *
35060
+ * @returns WKT string or undefined if not available
35061
+ */
35062
+ getWkt() {
35063
+ var _a;
35064
+ return (_a = this._copc) == null ? void 0 : _a.wkt;
35065
+ }
34969
35066
  /**
34970
35067
  * Destroys the streaming loader and cleans up resources.
34971
35068
  */
@@ -36044,6 +36141,52 @@ class EptStreamingLoader {
36044
36141
  getMetadata() {
36045
36142
  return this._metadata;
36046
36143
  }
36144
+ /**
36145
+ * Gets the extended EPT metadata for the metadata panel.
36146
+ *
36147
+ * @returns Extended EPT metadata or undefined if not initialized
36148
+ */
36149
+ getExtendedMetadata() {
36150
+ if (!this._metadata) return void 0;
36151
+ const meta = this._metadata;
36152
+ const dimensions2 = [];
36153
+ if (meta.schema) {
36154
+ for (const dim of meta.schema) {
36155
+ dimensions2.push({
36156
+ name: dim.name,
36157
+ type: dim.type,
36158
+ size: dim.size,
36159
+ scale: dim.scale,
36160
+ offset: dim.offset
36161
+ });
36162
+ }
36163
+ }
36164
+ let pointSpacing;
36165
+ if (meta.bounds && meta.bounds.length >= 6 && this._totalPointsInFile > 0) {
36166
+ const width = meta.bounds[3] - meta.bounds[0];
36167
+ const height = meta.bounds[4] - meta.bounds[1];
36168
+ const area = width * height;
36169
+ if (area > 0) {
36170
+ const spacingInSourceUnits = Math.sqrt(area / this._totalPointsInFile);
36171
+ pointSpacing = spacingInSourceUnits * this._verticalUnitFactor;
36172
+ }
36173
+ }
36174
+ return {
36175
+ version: meta.version || "1.0",
36176
+ dataType: meta.dataType || "laszip",
36177
+ hierarchyType: meta.hierarchyType || "json",
36178
+ span: meta.span || 128,
36179
+ nativeBounds: meta.bounds || [],
36180
+ srs: meta.srs ? {
36181
+ authority: meta.srs.authority,
36182
+ horizontal: meta.srs.horizontal,
36183
+ vertical: meta.srs.vertical,
36184
+ wkt: meta.srs.wkt
36185
+ } : void 0,
36186
+ dimensions: dimensions2,
36187
+ pointSpacing
36188
+ };
36189
+ }
36047
36190
  /**
36048
36191
  * Destroys the streaming loader and cleans up resources.
36049
36192
  */
@@ -36980,6 +37123,60 @@ class PointCloudManager {
36980
37123
  getLastComputedBounds() {
36981
37124
  return this._lastComputedBounds;
36982
37125
  }
37126
+ /**
37127
+ * Gets merged point cloud data from all loaded point clouds.
37128
+ * Used for cross-section profile extraction.
37129
+ *
37130
+ * @returns Merged point cloud data or null if no data loaded
37131
+ */
37132
+ getMergedPointCloudData() {
37133
+ if (this._pointClouds.size === 0) return null;
37134
+ if (this._pointClouds.size === 1) {
37135
+ const [first] = this._pointClouds.values();
37136
+ return first.data;
37137
+ }
37138
+ let totalPoints = 0;
37139
+ for (const pc of this._pointClouds.values()) {
37140
+ totalPoints += pc.data.pointCount;
37141
+ }
37142
+ if (totalPoints === 0) return null;
37143
+ const [firstPc] = this._pointClouds.values();
37144
+ const originLng = firstPc.coordinateOrigin[0];
37145
+ const originLat = firstPc.coordinateOrigin[1];
37146
+ const positions = new Float32Array(totalPoints * 3);
37147
+ const intensities = new Float32Array(totalPoints);
37148
+ const classifications = new Uint8Array(totalPoints);
37149
+ let offset = 0;
37150
+ for (const pc of this._pointClouds.values()) {
37151
+ const data = pc.data;
37152
+ const count = data.pointCount;
37153
+ const dLng = pc.coordinateOrigin[0] - originLng;
37154
+ const dLat = pc.coordinateOrigin[1] - originLat;
37155
+ for (let i = 0; i < count; i++) {
37156
+ positions[(offset + i) * 3] = data.positions[i * 3] + dLng;
37157
+ positions[(offset + i) * 3 + 1] = data.positions[i * 3 + 1] + dLat;
37158
+ positions[(offset + i) * 3 + 2] = data.positions[i * 3 + 2];
37159
+ }
37160
+ if (data.intensities) {
37161
+ intensities.set(data.intensities.subarray(0, count), offset);
37162
+ }
37163
+ if (data.classifications) {
37164
+ classifications.set(data.classifications.subarray(0, count), offset);
37165
+ }
37166
+ offset += count;
37167
+ }
37168
+ return {
37169
+ positions,
37170
+ coordinateOrigin: [originLng, originLat, 0],
37171
+ intensities,
37172
+ classifications,
37173
+ pointCount: totalPoints,
37174
+ bounds: firstPc.data.bounds,
37175
+ hasRGB: firstPc.data.hasRGB,
37176
+ hasIntensity: true,
37177
+ hasClassification: true
37178
+ };
37179
+ }
36983
37180
  /**
36984
37181
  * Creates a deck.gl layer for a point cloud.
36985
37182
  * Chunks large point clouds into multiple layers to avoid WebGL buffer limits.
@@ -38330,6 +38527,10 @@ class PanelBuilder {
38330
38527
  content.appendChild(this._buildFileSection());
38331
38528
  content.appendChild(this._buildStylingSection());
38332
38529
  content.appendChild(this._buildPointCloudsList());
38530
+ const crossSectionPanel = this._buildCrossSectionSection();
38531
+ if (crossSectionPanel) {
38532
+ content.appendChild(crossSectionPanel);
38533
+ }
38333
38534
  content.appendChild(this._buildLoadingIndicator());
38334
38535
  content.appendChild(this._buildErrorMessage());
38335
38536
  return content;
@@ -38938,6 +39139,18 @@ class PanelBuilder {
38938
39139
  info2.appendChild(details);
38939
39140
  const actions = document.createElement("div");
38940
39141
  actions.className = "lidar-pointcloud-actions";
39142
+ if (this._callbacks.onShowMetadata) {
39143
+ const infoBtn = document.createElement("button");
39144
+ infoBtn.type = "button";
39145
+ infoBtn.className = "lidar-pointcloud-action info";
39146
+ infoBtn.textContent = "Info";
39147
+ infoBtn.title = "Show metadata";
39148
+ infoBtn.addEventListener("click", (e) => {
39149
+ e.stopPropagation();
39150
+ this._callbacks.onShowMetadata(pc.id);
39151
+ });
39152
+ actions.appendChild(infoBtn);
39153
+ }
38941
39154
  const zoomBtn = document.createElement("button");
38942
39155
  zoomBtn.type = "button";
38943
39156
  zoomBtn.className = "lidar-pointcloud-action";
@@ -38986,6 +39199,1658 @@ class PanelBuilder {
38986
39199
  this._errorMessage = error;
38987
39200
  return error;
38988
39201
  }
39202
+ /**
39203
+ * Builds the cross-section section if callback is provided.
39204
+ *
39205
+ * @returns Cross-section section or null if not available
39206
+ */
39207
+ _buildCrossSectionSection() {
39208
+ if (!this._callbacks.onCrossSectionPanel) return null;
39209
+ const panel = this._callbacks.onCrossSectionPanel();
39210
+ if (!panel) return null;
39211
+ const section = document.createElement("div");
39212
+ section.className = "lidar-control-section lidar-crosssection-section";
39213
+ const header2 = document.createElement("div");
39214
+ header2.className = "lidar-control-section-header lidar-section-collapsible";
39215
+ header2.innerHTML = '<span class="lidar-section-toggle">▶</span> Cross-Section';
39216
+ header2.style.cursor = "pointer";
39217
+ const body = document.createElement("div");
39218
+ body.className = "lidar-section-body";
39219
+ body.style.display = "none";
39220
+ body.appendChild(panel);
39221
+ header2.addEventListener("click", () => {
39222
+ const toggle = header2.querySelector(".lidar-section-toggle");
39223
+ if (body.style.display === "none") {
39224
+ body.style.display = "block";
39225
+ if (toggle) toggle.textContent = "▼";
39226
+ } else {
39227
+ body.style.display = "none";
39228
+ if (toggle) toggle.textContent = "▶";
39229
+ }
39230
+ });
39231
+ section.appendChild(header2);
39232
+ section.appendChild(body);
39233
+ return section;
39234
+ }
39235
+ }
39236
+ class MetadataPanel {
39237
+ /**
39238
+ * Creates a new MetadataPanel instance.
39239
+ *
39240
+ * @param options - Panel options
39241
+ */
39242
+ constructor(options) {
39243
+ __publicField(this, "_container", null);
39244
+ __publicField(this, "_backdrop", null);
39245
+ __publicField(this, "_options");
39246
+ __publicField(this, "_metadata", null);
39247
+ this._options = options;
39248
+ }
39249
+ /**
39250
+ * Shows the metadata panel with the given metadata.
39251
+ *
39252
+ * @param metadata - Full metadata to display
39253
+ */
39254
+ show(metadata) {
39255
+ this._metadata = metadata;
39256
+ this._render();
39257
+ }
39258
+ /**
39259
+ * Hides and destroys the metadata panel.
39260
+ */
39261
+ hide() {
39262
+ var _a, _b;
39263
+ (_a = this._backdrop) == null ? void 0 : _a.remove();
39264
+ (_b = this._container) == null ? void 0 : _b.remove();
39265
+ this._backdrop = null;
39266
+ this._container = null;
39267
+ }
39268
+ /**
39269
+ * Renders the metadata panel.
39270
+ */
39271
+ _render() {
39272
+ var _a;
39273
+ if (!this._metadata) return;
39274
+ this._backdrop = document.createElement("div");
39275
+ this._backdrop.className = "lidar-metadata-backdrop";
39276
+ this._backdrop.addEventListener("click", () => this._close());
39277
+ document.body.appendChild(this._backdrop);
39278
+ this._container = document.createElement("div");
39279
+ this._container.className = "lidar-metadata-panel";
39280
+ this._container.addEventListener("click", (e) => e.stopPropagation());
39281
+ const header2 = document.createElement("div");
39282
+ header2.className = "lidar-metadata-header";
39283
+ header2.innerHTML = `
39284
+ <span class="lidar-metadata-title">Point Cloud Metadata</span>
39285
+ <button type="button" class="lidar-metadata-close" title="Close">&times;</button>
39286
+ `;
39287
+ (_a = header2.querySelector(".lidar-metadata-close")) == null ? void 0 : _a.addEventListener("click", () => this._close());
39288
+ this._container.appendChild(header2);
39289
+ const content = document.createElement("div");
39290
+ content.className = "lidar-metadata-content";
39291
+ content.appendChild(this._buildBasicInfoSection());
39292
+ content.appendChild(this._buildBoundsSection());
39293
+ content.appendChild(this._buildCrsSection());
39294
+ content.appendChild(this._buildPointFormatSection());
39295
+ content.appendChild(this._buildDimensionsSection());
39296
+ this._container.appendChild(content);
39297
+ document.body.appendChild(this._container);
39298
+ }
39299
+ /**
39300
+ * Closes the panel and calls the onClose callback.
39301
+ */
39302
+ _close() {
39303
+ this.hide();
39304
+ this._options.onClose();
39305
+ }
39306
+ /**
39307
+ * Builds the basic info section.
39308
+ *
39309
+ * @returns Section element
39310
+ */
39311
+ _buildBasicInfoSection() {
39312
+ var _a;
39313
+ const section = this._createSection("Basic Info", true);
39314
+ const body = section.querySelector(".lidar-metadata-section-body");
39315
+ const meta = this._metadata;
39316
+ const basic = meta.basic;
39317
+ const rows = [
39318
+ { label: "Name", value: basic.name },
39319
+ { label: "Type", value: meta.type.toUpperCase() },
39320
+ { label: "Point Count", value: this._formatNumber(basic.pointCount) },
39321
+ { label: "Has RGB", value: basic.hasRGB ? "Yes" : "No" },
39322
+ { label: "Has Intensity", value: basic.hasIntensity ? "Yes" : "No" },
39323
+ { label: "Has Classification", value: basic.hasClassification ? "Yes" : "No" },
39324
+ { label: "Source", value: basic.source }
39325
+ ];
39326
+ if (meta.type === "copc" && meta.copc) {
39327
+ rows.push({ label: "LAS Version", value: meta.copc.lasVersion });
39328
+ rows.push({ label: "Point Format", value: String(meta.copc.pointDataRecordFormat) });
39329
+ rows.push({ label: "Generating Software", value: meta.copc.generatingSoftware });
39330
+ if (meta.copc.creationDate) {
39331
+ rows.push({
39332
+ label: "Creation Date",
39333
+ value: `${meta.copc.creationDate.year}, Day ${meta.copc.creationDate.dayOfYear}`
39334
+ });
39335
+ }
39336
+ if (((_a = meta.copc.copcInfo) == null ? void 0 : _a.pointSpacing) !== void 0 && meta.copc.copcInfo.pointSpacing > 0) {
39337
+ rows.push({ label: "Point Spacing (est.)", value: "~" + meta.copc.copcInfo.pointSpacing.toFixed(2) + " m" });
39338
+ }
39339
+ }
39340
+ if (meta.type === "ept" && meta.ept) {
39341
+ rows.push({ label: "EPT Version", value: meta.ept.version });
39342
+ rows.push({ label: "Data Type", value: meta.ept.dataType });
39343
+ if (meta.ept.pointSpacing !== void 0 && meta.ept.pointSpacing > 0) {
39344
+ rows.push({ label: "Point Spacing (est.)", value: "~" + meta.ept.pointSpacing.toFixed(2) + " m" });
39345
+ }
39346
+ }
39347
+ for (const row of rows) {
39348
+ body.appendChild(this._createRow(row.label, row.value));
39349
+ }
39350
+ return section;
39351
+ }
39352
+ /**
39353
+ * Builds the bounds section.
39354
+ *
39355
+ * @returns Section element
39356
+ */
39357
+ _buildBoundsSection() {
39358
+ var _a, _b;
39359
+ const section = this._createSection("Bounds", false);
39360
+ const body = section.querySelector(".lidar-metadata-section-body");
39361
+ const meta = this._metadata;
39362
+ const basic = meta.basic;
39363
+ body.appendChild(this._createSubheader("Geographic (WGS84)"));
39364
+ body.appendChild(this._createRow("Min X (Lng)", basic.bounds.minX.toFixed(6) + "°"));
39365
+ body.appendChild(this._createRow("Max X (Lng)", basic.bounds.maxX.toFixed(6) + "°"));
39366
+ body.appendChild(this._createRow("Min Y (Lat)", basic.bounds.minY.toFixed(6) + "°"));
39367
+ body.appendChild(this._createRow("Max Y (Lat)", basic.bounds.maxY.toFixed(6) + "°"));
39368
+ body.appendChild(this._createRow("Min Z", basic.bounds.minZ.toFixed(2) + " m"));
39369
+ body.appendChild(this._createRow("Max Z", basic.bounds.maxZ.toFixed(2) + " m"));
39370
+ if (meta.type === "copc" && ((_a = meta.copc) == null ? void 0 : _a.nativeBounds)) {
39371
+ body.appendChild(this._createSubheader("Native CRS"));
39372
+ const nb = meta.copc.nativeBounds;
39373
+ body.appendChild(this._createRow("Min X", nb.min[0].toFixed(3)));
39374
+ body.appendChild(this._createRow("Max X", nb.max[0].toFixed(3)));
39375
+ body.appendChild(this._createRow("Min Y", nb.min[1].toFixed(3)));
39376
+ body.appendChild(this._createRow("Max Y", nb.max[1].toFixed(3)));
39377
+ body.appendChild(this._createRow("Min Z", nb.min[2].toFixed(3)));
39378
+ body.appendChild(this._createRow("Max Z", nb.max[2].toFixed(3)));
39379
+ }
39380
+ if (meta.type === "ept" && ((_b = meta.ept) == null ? void 0 : _b.nativeBounds) && meta.ept.nativeBounds.length >= 6) {
39381
+ body.appendChild(this._createSubheader("Native CRS"));
39382
+ const nb = meta.ept.nativeBounds;
39383
+ body.appendChild(this._createRow("Min X", nb[0].toFixed(3)));
39384
+ body.appendChild(this._createRow("Max X", nb[3].toFixed(3)));
39385
+ body.appendChild(this._createRow("Min Y", nb[1].toFixed(3)));
39386
+ body.appendChild(this._createRow("Max Y", nb[4].toFixed(3)));
39387
+ body.appendChild(this._createRow("Min Z", nb[2].toFixed(3)));
39388
+ body.appendChild(this._createRow("Max Z", nb[5].toFixed(3)));
39389
+ }
39390
+ return section;
39391
+ }
39392
+ /**
39393
+ * Builds the CRS section.
39394
+ *
39395
+ * @returns Section element
39396
+ */
39397
+ _buildCrsSection() {
39398
+ var _a;
39399
+ const section = this._createSection("Coordinate Reference System", false);
39400
+ const body = section.querySelector(".lidar-metadata-section-body");
39401
+ const meta = this._metadata;
39402
+ const wkt2 = meta.basic.wkt;
39403
+ if (wkt2) {
39404
+ const wktContainer = document.createElement("div");
39405
+ wktContainer.className = "lidar-metadata-wkt";
39406
+ const copyBtn = document.createElement("button");
39407
+ copyBtn.type = "button";
39408
+ copyBtn.className = "lidar-metadata-copy-btn";
39409
+ copyBtn.textContent = "Copy WKT";
39410
+ copyBtn.addEventListener("click", () => {
39411
+ navigator.clipboard.writeText(wkt2).then(() => {
39412
+ copyBtn.textContent = "Copied!";
39413
+ setTimeout(() => {
39414
+ copyBtn.textContent = "Copy WKT";
39415
+ }, 2e3);
39416
+ });
39417
+ });
39418
+ wktContainer.appendChild(copyBtn);
39419
+ const wktCode = document.createElement("pre");
39420
+ wktCode.className = "lidar-metadata-code";
39421
+ wktCode.textContent = this._formatWkt(wkt2);
39422
+ wktContainer.appendChild(wktCode);
39423
+ body.appendChild(wktContainer);
39424
+ } else {
39425
+ body.appendChild(this._createRow("WKT", "Not available"));
39426
+ }
39427
+ if (meta.type === "ept" && ((_a = meta.ept) == null ? void 0 : _a.srs)) {
39428
+ const srs = meta.ept.srs;
39429
+ if (srs.authority) {
39430
+ body.appendChild(this._createRow("Authority", srs.authority));
39431
+ }
39432
+ if (srs.horizontal) {
39433
+ body.appendChild(this._createRow("Horizontal", srs.horizontal));
39434
+ }
39435
+ if (srs.vertical) {
39436
+ body.appendChild(this._createRow("Vertical", srs.vertical));
39437
+ }
39438
+ }
39439
+ if (meta.type === "copc" && meta.copc) {
39440
+ body.appendChild(this._createSubheader("Scale & Offset"));
39441
+ body.appendChild(this._createRow("Scale X", meta.copc.scale[0].toExponential(4)));
39442
+ body.appendChild(this._createRow("Scale Y", meta.copc.scale[1].toExponential(4)));
39443
+ body.appendChild(this._createRow("Scale Z", meta.copc.scale[2].toExponential(4)));
39444
+ body.appendChild(this._createRow("Offset X", meta.copc.offset[0].toFixed(3)));
39445
+ body.appendChild(this._createRow("Offset Y", meta.copc.offset[1].toFixed(3)));
39446
+ body.appendChild(this._createRow("Offset Z", meta.copc.offset[2].toFixed(3)));
39447
+ }
39448
+ return section;
39449
+ }
39450
+ /**
39451
+ * Builds the point format section.
39452
+ *
39453
+ * @returns Section element
39454
+ */
39455
+ _buildPointFormatSection() {
39456
+ const section = this._createSection("Point Format", false);
39457
+ const body = section.querySelector(".lidar-metadata-section-body");
39458
+ const meta = this._metadata;
39459
+ if (meta.type === "copc" && meta.copc) {
39460
+ body.appendChild(this._createRow("Point Data Record Format", String(meta.copc.pointDataRecordFormat)));
39461
+ body.appendChild(this._createRow("LAS Version", meta.copc.lasVersion));
39462
+ const format = meta.copc.pointDataRecordFormat;
39463
+ const formatDescriptions = {
39464
+ 0: "Core (XYZ, Intensity, Return, Classification)",
39465
+ 1: "Format 0 + GPS Time",
39466
+ 2: "Format 0 + RGB",
39467
+ 3: "Format 0 + GPS Time + RGB",
39468
+ 6: "Extended (14-bit Classification, NIR)",
39469
+ 7: "Format 6 + RGB",
39470
+ 8: "Format 6 + RGB + NIR"
39471
+ };
39472
+ if (formatDescriptions[format]) {
39473
+ body.appendChild(this._createRow("Description", formatDescriptions[format]));
39474
+ }
39475
+ }
39476
+ if (meta.type === "ept" && meta.ept) {
39477
+ body.appendChild(this._createRow("Data Encoding", meta.ept.dataType));
39478
+ }
39479
+ return section;
39480
+ }
39481
+ /**
39482
+ * Builds the dimensions section.
39483
+ *
39484
+ * @returns Section element
39485
+ */
39486
+ _buildDimensionsSection() {
39487
+ var _a, _b;
39488
+ const section = this._createSection("Dimensions", false);
39489
+ const body = section.querySelector(".lidar-metadata-section-body");
39490
+ const meta = this._metadata;
39491
+ let dimensions2 = [];
39492
+ if (meta.type === "copc" && ((_a = meta.copc) == null ? void 0 : _a.dimensions)) {
39493
+ dimensions2 = meta.copc.dimensions;
39494
+ } else if (meta.type === "ept" && ((_b = meta.ept) == null ? void 0 : _b.dimensions)) {
39495
+ dimensions2 = meta.ept.dimensions;
39496
+ }
39497
+ if (dimensions2.length > 0) {
39498
+ const table = document.createElement("table");
39499
+ table.className = "lidar-metadata-table";
39500
+ table.innerHTML = `
39501
+ <thead>
39502
+ <tr>
39503
+ <th>Name</th>
39504
+ <th>Type</th>
39505
+ <th>Size</th>
39506
+ </tr>
39507
+ </thead>
39508
+ <tbody></tbody>
39509
+ `;
39510
+ const tbody = table.querySelector("tbody");
39511
+ for (const dim of dimensions2) {
39512
+ const tr = document.createElement("tr");
39513
+ tr.innerHTML = `
39514
+ <td>${dim.name}</td>
39515
+ <td>${dim.type}</td>
39516
+ <td>${dim.size} bytes</td>
39517
+ `;
39518
+ tbody.appendChild(tr);
39519
+ }
39520
+ body.appendChild(table);
39521
+ } else {
39522
+ body.appendChild(this._createRow("Dimensions", "Not available"));
39523
+ }
39524
+ return section;
39525
+ }
39526
+ /**
39527
+ * Creates a collapsible section.
39528
+ *
39529
+ * @param title - Section title
39530
+ * @param expanded - Whether section starts expanded
39531
+ * @returns Section element
39532
+ */
39533
+ _createSection(title, expanded) {
39534
+ const section = document.createElement("div");
39535
+ section.className = "lidar-metadata-section";
39536
+ const header2 = document.createElement("div");
39537
+ header2.className = "lidar-metadata-section-header";
39538
+ header2.innerHTML = `
39539
+ <span class="lidar-metadata-section-toggle">${expanded ? "▼" : "▶"}</span>
39540
+ <span class="lidar-metadata-section-title">${title}</span>
39541
+ `;
39542
+ const body = document.createElement("div");
39543
+ body.className = "lidar-metadata-section-body";
39544
+ body.style.display = expanded ? "block" : "none";
39545
+ header2.addEventListener("click", () => {
39546
+ const toggle = header2.querySelector(".lidar-metadata-section-toggle");
39547
+ if (body.style.display === "none") {
39548
+ body.style.display = "block";
39549
+ toggle.textContent = "▼";
39550
+ } else {
39551
+ body.style.display = "none";
39552
+ toggle.textContent = "▶";
39553
+ }
39554
+ });
39555
+ section.appendChild(header2);
39556
+ section.appendChild(body);
39557
+ return section;
39558
+ }
39559
+ /**
39560
+ * Creates a key-value row.
39561
+ *
39562
+ * @param label - Row label
39563
+ * @param value - Row value
39564
+ * @returns Row element
39565
+ */
39566
+ _createRow(label, value) {
39567
+ const row = document.createElement("div");
39568
+ row.className = "lidar-metadata-row";
39569
+ row.innerHTML = `
39570
+ <span class="lidar-metadata-label">${label}:</span>
39571
+ <span class="lidar-metadata-value">${value}</span>
39572
+ `;
39573
+ return row;
39574
+ }
39575
+ /**
39576
+ * Creates a subheader element.
39577
+ *
39578
+ * @param title - Subheader title
39579
+ * @returns Subheader element
39580
+ */
39581
+ _createSubheader(title) {
39582
+ const subheader = document.createElement("div");
39583
+ subheader.className = "lidar-metadata-subheader";
39584
+ subheader.textContent = title;
39585
+ return subheader;
39586
+ }
39587
+ /**
39588
+ * Formats a number with commas.
39589
+ *
39590
+ * @param n - Number to format
39591
+ * @returns Formatted string
39592
+ */
39593
+ _formatNumber(n) {
39594
+ return n.toLocaleString();
39595
+ }
39596
+ /**
39597
+ * Formats a WKT string for display.
39598
+ *
39599
+ * @param wkt - WKT string
39600
+ * @returns Formatted WKT
39601
+ */
39602
+ _formatWkt(wkt2) {
39603
+ let formatted = "";
39604
+ let indent = 0;
39605
+ for (let i = 0; i < wkt2.length; i++) {
39606
+ const char = wkt2[i];
39607
+ if (char === "[") {
39608
+ formatted += "[\n" + " ".repeat(++indent);
39609
+ } else if (char === "]") {
39610
+ formatted += "\n" + " ".repeat(--indent) + "]";
39611
+ } else if (char === ",") {
39612
+ formatted += ",\n" + " ".repeat(indent);
39613
+ } else {
39614
+ formatted += char;
39615
+ }
39616
+ }
39617
+ return formatted;
39618
+ }
39619
+ }
39620
+ class ElevationProfileChart {
39621
+ /**
39622
+ * Creates a new ElevationProfileChart instance.
39623
+ *
39624
+ * @param options - Chart options
39625
+ */
39626
+ constructor(options) {
39627
+ __publicField(this, "_container");
39628
+ __publicField(this, "_canvas");
39629
+ __publicField(this, "_ctx");
39630
+ __publicField(this, "_tooltip");
39631
+ __publicField(this, "_options");
39632
+ __publicField(this, "_profile", null);
39633
+ __publicField(this, "_hoveredPointIndex", -1);
39634
+ // Chart dimensions (with margins)
39635
+ __publicField(this, "MARGIN", { top: 20, right: 20, bottom: 40, left: 60 });
39636
+ this._options = {
39637
+ width: (options == null ? void 0 : options.width) ?? 300,
39638
+ height: (options == null ? void 0 : options.height) ?? 150,
39639
+ colormap: (options == null ? void 0 : options.colormap) ?? "viridis",
39640
+ onPointHover: (options == null ? void 0 : options.onPointHover) ?? (() => {
39641
+ })
39642
+ };
39643
+ this._container = document.createElement("div");
39644
+ this._container.className = "lidar-profile-chart-container";
39645
+ this._canvas = document.createElement("canvas");
39646
+ this._canvas.className = "lidar-profile-chart";
39647
+ this._canvas.width = this._options.width;
39648
+ this._canvas.height = this._options.height;
39649
+ this._container.appendChild(this._canvas);
39650
+ this._ctx = this._canvas.getContext("2d");
39651
+ this._tooltip = document.createElement("div");
39652
+ this._tooltip.className = "lidar-profile-tooltip";
39653
+ this._tooltip.style.display = "none";
39654
+ this._container.appendChild(this._tooltip);
39655
+ this._canvas.addEventListener("mousemove", this._handleMouseMove.bind(this));
39656
+ this._canvas.addEventListener("mouseleave", this._handleMouseLeave.bind(this));
39657
+ }
39658
+ /**
39659
+ * Renders the chart container element.
39660
+ *
39661
+ * @returns Container element
39662
+ */
39663
+ render() {
39664
+ return this._container;
39665
+ }
39666
+ /**
39667
+ * Sets the elevation profile data and redraws the chart.
39668
+ *
39669
+ * @param profile - Elevation profile to display
39670
+ */
39671
+ setProfile(profile) {
39672
+ this._profile = profile;
39673
+ this._draw();
39674
+ }
39675
+ /**
39676
+ * Sets the colormap and redraws the chart.
39677
+ *
39678
+ * @param colormap - Colormap name
39679
+ */
39680
+ setColormap(colormap) {
39681
+ this._options.colormap = colormap;
39682
+ this._draw();
39683
+ }
39684
+ /**
39685
+ * Resizes the chart.
39686
+ *
39687
+ * @param width - New width
39688
+ * @param height - New height
39689
+ */
39690
+ resize(width, height) {
39691
+ const safeWidth = Math.max(100, Math.round(width));
39692
+ const safeHeight = Math.max(80, Math.round(height));
39693
+ this._options.width = safeWidth;
39694
+ this._options.height = safeHeight;
39695
+ this._canvas.width = safeWidth;
39696
+ this._canvas.height = safeHeight;
39697
+ const ctx = this._canvas.getContext("2d");
39698
+ if (ctx) {
39699
+ this._ctx = ctx;
39700
+ }
39701
+ this._draw();
39702
+ }
39703
+ /**
39704
+ * Draws the chart.
39705
+ */
39706
+ _draw() {
39707
+ const { width, height } = this._options;
39708
+ const ctx = this._ctx;
39709
+ ctx.clearRect(0, 0, width, height);
39710
+ ctx.fillStyle = "#f8f8f8";
39711
+ ctx.fillRect(0, 0, width, height);
39712
+ if (!this._profile || this._profile.points.length === 0) {
39713
+ this._drawNoData();
39714
+ return;
39715
+ }
39716
+ const plotWidth = width - this.MARGIN.left - this.MARGIN.right;
39717
+ const plotHeight = height - this.MARGIN.top - this.MARGIN.bottom;
39718
+ this._drawGrid(plotWidth, plotHeight);
39719
+ this._drawPoints(plotWidth, plotHeight);
39720
+ this._drawAxes(plotWidth, plotHeight);
39721
+ }
39722
+ /**
39723
+ * Draws "No data" message.
39724
+ */
39725
+ _drawNoData() {
39726
+ const { width, height } = this._options;
39727
+ const ctx = this._ctx;
39728
+ ctx.fillStyle = "#888";
39729
+ ctx.font = "12px sans-serif";
39730
+ ctx.textAlign = "center";
39731
+ ctx.textBaseline = "middle";
39732
+ ctx.fillText("Draw a cross-section line to view elevation profile", width / 2, height / 2);
39733
+ }
39734
+ /**
39735
+ * Draws grid lines.
39736
+ *
39737
+ * @param plotWidth - Plot area width
39738
+ * @param plotHeight - Plot area height
39739
+ */
39740
+ _drawGrid(plotWidth, plotHeight) {
39741
+ const ctx = this._ctx;
39742
+ const { left, top } = this.MARGIN;
39743
+ ctx.strokeStyle = "#ddd";
39744
+ ctx.lineWidth = 1;
39745
+ for (let i = 0; i <= 4; i++) {
39746
+ const y = top + plotHeight * i / 4;
39747
+ ctx.beginPath();
39748
+ ctx.moveTo(left, y);
39749
+ ctx.lineTo(left + plotWidth, y);
39750
+ ctx.stroke();
39751
+ }
39752
+ for (let i = 0; i <= 4; i++) {
39753
+ const x = left + plotWidth * i / 4;
39754
+ ctx.beginPath();
39755
+ ctx.moveTo(x, top);
39756
+ ctx.lineTo(x, top + plotHeight);
39757
+ ctx.stroke();
39758
+ }
39759
+ }
39760
+ /**
39761
+ * Draws the profile points.
39762
+ *
39763
+ * @param plotWidth - Plot area width
39764
+ * @param plotHeight - Plot area height
39765
+ */
39766
+ _drawPoints(plotWidth, plotHeight) {
39767
+ if (!this._profile) return;
39768
+ const ctx = this._ctx;
39769
+ const { left, top } = this.MARGIN;
39770
+ const { points, stats } = this._profile;
39771
+ const colormap = getColormap(this._options.colormap);
39772
+ const xScale = plotWidth / stats.totalDistance;
39773
+ const yRange = stats.maxElevation - stats.minElevation;
39774
+ const yScale = yRange > 0 ? plotHeight / yRange : 1;
39775
+ const pointRadius = Math.max(1, Math.min(3, 500 / points.length));
39776
+ for (let i = 0; i < points.length; i++) {
39777
+ const point = points[i];
39778
+ const x = left + point.distance * xScale;
39779
+ const y = top + plotHeight - (point.elevation - stats.minElevation) * yScale;
39780
+ const t = yRange > 0 ? (point.elevation - stats.minElevation) / yRange : 0.5;
39781
+ const colorIndex = Math.floor(t * (colormap.length - 1));
39782
+ const color = colormap[Math.max(0, Math.min(colormap.length - 1, colorIndex))];
39783
+ ctx.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
39784
+ ctx.beginPath();
39785
+ ctx.arc(x, y, pointRadius, 0, Math.PI * 2);
39786
+ ctx.fill();
39787
+ if (i === this._hoveredPointIndex) {
39788
+ ctx.strokeStyle = "#000";
39789
+ ctx.lineWidth = 2;
39790
+ ctx.beginPath();
39791
+ ctx.arc(x, y, pointRadius + 2, 0, Math.PI * 2);
39792
+ ctx.stroke();
39793
+ }
39794
+ }
39795
+ }
39796
+ /**
39797
+ * Draws the axes and labels.
39798
+ *
39799
+ * @param plotWidth - Plot area width
39800
+ * @param plotHeight - Plot area height
39801
+ */
39802
+ _drawAxes(plotWidth, plotHeight) {
39803
+ if (!this._profile) return;
39804
+ const ctx = this._ctx;
39805
+ const { left, top } = this.MARGIN;
39806
+ const { width, height } = this._options;
39807
+ const { stats } = this._profile;
39808
+ ctx.strokeStyle = "#333";
39809
+ ctx.lineWidth = 1;
39810
+ ctx.beginPath();
39811
+ ctx.moveTo(left, top);
39812
+ ctx.lineTo(left, top + plotHeight);
39813
+ ctx.stroke();
39814
+ ctx.beginPath();
39815
+ ctx.moveTo(left, top + plotHeight);
39816
+ ctx.lineTo(left + plotWidth, top + plotHeight);
39817
+ ctx.stroke();
39818
+ ctx.fillStyle = "#333";
39819
+ ctx.font = "10px sans-serif";
39820
+ ctx.textAlign = "center";
39821
+ ctx.textBaseline = "top";
39822
+ for (let i = 0; i <= 4; i++) {
39823
+ const x = left + plotWidth * i / 4;
39824
+ const dist = stats.totalDistance * i / 4;
39825
+ ctx.fillText(this._formatDistance(dist), x, top + plotHeight + 5);
39826
+ }
39827
+ ctx.textBaseline = "bottom";
39828
+ ctx.fillText("Distance (m)", width / 2, height - 6);
39829
+ ctx.textAlign = "right";
39830
+ ctx.textBaseline = "middle";
39831
+ const yRange = stats.maxElevation - stats.minElevation;
39832
+ for (let i = 0; i <= 4; i++) {
39833
+ const y = top + plotHeight - plotHeight * i / 4;
39834
+ const elev = stats.minElevation + yRange * i / 4;
39835
+ ctx.fillText(elev.toFixed(1), left - 5, y);
39836
+ }
39837
+ ctx.save();
39838
+ ctx.translate(12, height / 2);
39839
+ ctx.rotate(-Math.PI / 2);
39840
+ ctx.textAlign = "center";
39841
+ ctx.fillText("Elevation (m)", 0, 0);
39842
+ ctx.restore();
39843
+ }
39844
+ /**
39845
+ * Formats distance for display.
39846
+ *
39847
+ * @param meters - Distance in meters
39848
+ * @returns Formatted string
39849
+ */
39850
+ _formatDistance(meters) {
39851
+ if (meters >= 1e3) {
39852
+ return (meters / 1e3).toFixed(1) + "km";
39853
+ }
39854
+ return Math.round(meters) + "";
39855
+ }
39856
+ /**
39857
+ * Handles mouse move for hover interaction.
39858
+ *
39859
+ * @param event - Mouse event
39860
+ */
39861
+ _handleMouseMove(event) {
39862
+ if (!this._profile || this._profile.points.length === 0) {
39863
+ this._hideTooltip();
39864
+ return;
39865
+ }
39866
+ const rect = this._canvas.getBoundingClientRect();
39867
+ const mouseX = event.clientX - rect.left;
39868
+ const mouseY = event.clientY - rect.top;
39869
+ const { left, top } = this.MARGIN;
39870
+ const plotWidth = this._options.width - this.MARGIN.left - this.MARGIN.right;
39871
+ const plotHeight = this._options.height - this.MARGIN.top - this.MARGIN.bottom;
39872
+ const { points, stats } = this._profile;
39873
+ const xScale = plotWidth / stats.totalDistance;
39874
+ const yRange = stats.maxElevation - stats.minElevation;
39875
+ const yScale = yRange > 0 ? plotHeight / yRange : 1;
39876
+ let closestIndex = -1;
39877
+ let closestDist = Infinity;
39878
+ for (let i = 0; i < points.length; i++) {
39879
+ const point = points[i];
39880
+ const px = left + point.distance * xScale;
39881
+ const py = top + plotHeight - (point.elevation - stats.minElevation) * yScale;
39882
+ const dist = Math.sqrt((mouseX - px) ** 2 + (mouseY - py) ** 2);
39883
+ if (dist < closestDist && dist < 15) {
39884
+ closestDist = dist;
39885
+ closestIndex = i;
39886
+ }
39887
+ }
39888
+ if (closestIndex !== this._hoveredPointIndex) {
39889
+ this._hoveredPointIndex = closestIndex;
39890
+ this._draw();
39891
+ if (closestIndex >= 0) {
39892
+ const point = points[closestIndex];
39893
+ this._showTooltip(point, event.clientX, event.clientY);
39894
+ this._options.onPointHover(point, event.clientX, event.clientY);
39895
+ } else {
39896
+ this._hideTooltip();
39897
+ this._options.onPointHover(null, 0, 0);
39898
+ }
39899
+ }
39900
+ }
39901
+ /**
39902
+ * Handles mouse leave.
39903
+ */
39904
+ _handleMouseLeave() {
39905
+ this._hoveredPointIndex = -1;
39906
+ this._draw();
39907
+ this._hideTooltip();
39908
+ this._options.onPointHover(null, 0, 0);
39909
+ }
39910
+ /**
39911
+ * Shows the tooltip.
39912
+ *
39913
+ * @param point - Profile point
39914
+ * @param x - Screen X coordinate
39915
+ * @param y - Screen Y coordinate
39916
+ */
39917
+ _showTooltip(point, x, y) {
39918
+ const lines = [
39919
+ `Distance: ${point.distance.toFixed(1)} m`,
39920
+ `Elevation: ${point.elevation.toFixed(2)} m`,
39921
+ `Offset: ${point.offsetFromLine.toFixed(2)} m`
39922
+ ];
39923
+ if (point.classification !== void 0) {
39924
+ lines.push(`Class: ${point.classification}`);
39925
+ }
39926
+ this._tooltip.innerHTML = lines.join("<br>");
39927
+ this._tooltip.style.display = "block";
39928
+ const rect = this._container.getBoundingClientRect();
39929
+ this._tooltip.style.left = `${x - rect.left + 10}px`;
39930
+ this._tooltip.style.top = `${y - rect.top - 40}px`;
39931
+ }
39932
+ /**
39933
+ * Hides the tooltip.
39934
+ */
39935
+ _hideTooltip() {
39936
+ this._tooltip.style.display = "none";
39937
+ }
39938
+ /**
39939
+ * Destroys the chart and cleans up resources.
39940
+ */
39941
+ destroy() {
39942
+ this._canvas.removeEventListener("mousemove", this._handleMouseMove.bind(this));
39943
+ this._canvas.removeEventListener("mouseleave", this._handleMouseLeave.bind(this));
39944
+ this._container.remove();
39945
+ }
39946
+ }
39947
+ class CrossSectionPanel {
39948
+ /**
39949
+ * Creates a new CrossSectionPanel instance.
39950
+ *
39951
+ * @param callbacks - Panel callbacks
39952
+ * @param options - Panel options
39953
+ */
39954
+ constructor(callbacks, options) {
39955
+ __publicField(this, "_container");
39956
+ __publicField(this, "_callbacks");
39957
+ __publicField(this, "_options");
39958
+ // UI elements
39959
+ __publicField(this, "_drawButton");
39960
+ __publicField(this, "_clearButton");
39961
+ __publicField(this, "_downloadButton");
39962
+ __publicField(this, "_expandButton");
39963
+ __publicField(this, "_bufferSlider");
39964
+ __publicField(this, "_bufferValue");
39965
+ __publicField(this, "_statsContainer");
39966
+ __publicField(this, "_chartContainer");
39967
+ __publicField(this, "_chart");
39968
+ // Popup elements
39969
+ __publicField(this, "_popupBackdrop");
39970
+ __publicField(this, "_popupContainer");
39971
+ __publicField(this, "_popupChartContainer");
39972
+ __publicField(this, "_popupChart");
39973
+ __publicField(this, "_popupResizeObserver");
39974
+ __publicField(this, "_isDrawing", false);
39975
+ __publicField(this, "_profile", null);
39976
+ // Popup resize state
39977
+ __publicField(this, "_isResizing", false);
39978
+ __publicField(this, "_ignoreBackdropClick", false);
39979
+ __publicField(this, "_resizeStartX", 0);
39980
+ __publicField(this, "_resizeStartY", 0);
39981
+ __publicField(this, "_resizeStartWidth", 0);
39982
+ __publicField(this, "_resizeStartHeight", 0);
39983
+ __publicField(this, "_resizeObserver");
39984
+ // Bound handlers for cleanup
39985
+ __publicField(this, "_handlePopupResizeMouseMove");
39986
+ __publicField(this, "_handlePopupResizeMouseUp");
39987
+ this._callbacks = callbacks;
39988
+ this._options = {
39989
+ bufferDistance: (options == null ? void 0 : options.bufferDistance) ?? 10,
39990
+ colormap: (options == null ? void 0 : options.colormap) ?? "viridis",
39991
+ chartHeight: (options == null ? void 0 : options.chartHeight) ?? 180
39992
+ };
39993
+ this._container = document.createElement("div");
39994
+ this._container.className = "lidar-crosssection-panel";
39995
+ this._chart = new ElevationProfileChart({
39996
+ width: 320,
39997
+ height: this._options.chartHeight,
39998
+ colormap: this._options.colormap
39999
+ });
40000
+ this._handlePopupResizeMouseMove = this._onPopupResizeMouseMove.bind(this);
40001
+ this._handlePopupResizeMouseUp = this._onPopupResizeMouseUp.bind(this);
40002
+ this._build();
40003
+ this._setupResizeObserver();
40004
+ }
40005
+ /**
40006
+ * Renders the panel element.
40007
+ *
40008
+ * @returns Container element
40009
+ */
40010
+ render() {
40011
+ return this._container;
40012
+ }
40013
+ /**
40014
+ * Updates the elevation profile display.
40015
+ *
40016
+ * @param profile - Elevation profile data
40017
+ */
40018
+ setProfile(profile) {
40019
+ var _a;
40020
+ this._profile = profile;
40021
+ this._chart.setProfile(profile);
40022
+ (_a = this._popupChart) == null ? void 0 : _a.setProfile(profile);
40023
+ this._updateStats();
40024
+ const hasData = profile && profile.points.length > 0;
40025
+ if (this._downloadButton) {
40026
+ this._downloadButton.disabled = !hasData;
40027
+ }
40028
+ if (this._expandButton) {
40029
+ this._expandButton.disabled = !hasData;
40030
+ }
40031
+ }
40032
+ /**
40033
+ * Sets the drawing state.
40034
+ *
40035
+ * @param isDrawing - Whether drawing mode is active
40036
+ */
40037
+ setDrawing(isDrawing) {
40038
+ this._isDrawing = isDrawing;
40039
+ if (this._drawButton) {
40040
+ this._drawButton.textContent = isDrawing ? "Cancel" : "Draw Line";
40041
+ this._drawButton.classList.toggle("active", isDrawing);
40042
+ }
40043
+ }
40044
+ /**
40045
+ * Sets the colormap.
40046
+ *
40047
+ * @param colormap - Colormap name
40048
+ */
40049
+ setColormap(colormap) {
40050
+ var _a;
40051
+ this._options.colormap = colormap;
40052
+ this._chart.setColormap(colormap);
40053
+ (_a = this._popupChart) == null ? void 0 : _a.setColormap(colormap);
40054
+ }
40055
+ /**
40056
+ * Sets the buffer distance.
40057
+ *
40058
+ * @param meters - Buffer distance in meters
40059
+ */
40060
+ setBufferDistance(meters) {
40061
+ this._options.bufferDistance = meters;
40062
+ if (this._bufferSlider) {
40063
+ this._bufferSlider.value = String(meters);
40064
+ }
40065
+ if (this._bufferValue) {
40066
+ this._bufferValue.textContent = `${meters} m`;
40067
+ }
40068
+ }
40069
+ /**
40070
+ * Builds the panel UI.
40071
+ */
40072
+ _build() {
40073
+ const controls = document.createElement("div");
40074
+ controls.className = "lidar-crosssection-controls";
40075
+ this._drawButton = document.createElement("button");
40076
+ this._drawButton.type = "button";
40077
+ this._drawButton.className = "lidar-control-button lidar-crosssection-draw";
40078
+ this._drawButton.textContent = "Draw Line";
40079
+ this._drawButton.addEventListener("click", (e) => {
40080
+ e.stopPropagation();
40081
+ this._isDrawing = !this._isDrawing;
40082
+ this.setDrawing(this._isDrawing);
40083
+ this._callbacks.onDrawToggle(this._isDrawing);
40084
+ });
40085
+ controls.appendChild(this._drawButton);
40086
+ this._clearButton = document.createElement("button");
40087
+ this._clearButton.type = "button";
40088
+ this._clearButton.className = "lidar-control-button lidar-crosssection-clear";
40089
+ this._clearButton.textContent = "Clear";
40090
+ this._clearButton.addEventListener("click", (e) => {
40091
+ e.stopPropagation();
40092
+ this._callbacks.onClear();
40093
+ this.setDrawing(false);
40094
+ });
40095
+ controls.appendChild(this._clearButton);
40096
+ this._downloadButton = document.createElement("button");
40097
+ this._downloadButton.type = "button";
40098
+ this._downloadButton.className = "lidar-control-button secondary";
40099
+ this._downloadButton.textContent = "CSV";
40100
+ this._downloadButton.title = "Download profile data as CSV";
40101
+ this._downloadButton.disabled = true;
40102
+ this._downloadButton.addEventListener("click", (e) => {
40103
+ e.stopPropagation();
40104
+ this._downloadCSV();
40105
+ });
40106
+ controls.appendChild(this._downloadButton);
40107
+ this._expandButton = document.createElement("button");
40108
+ this._expandButton.type = "button";
40109
+ this._expandButton.className = "lidar-control-button secondary";
40110
+ this._expandButton.innerHTML = "⤢";
40111
+ this._expandButton.title = "Expand chart in popup";
40112
+ this._expandButton.disabled = true;
40113
+ this._expandButton.addEventListener("click", (e) => {
40114
+ e.stopPropagation();
40115
+ this._openPopup();
40116
+ });
40117
+ controls.appendChild(this._expandButton);
40118
+ this._container.appendChild(controls);
40119
+ const bufferGroup = document.createElement("div");
40120
+ bufferGroup.className = "lidar-control-group";
40121
+ const bufferLabel = document.createElement("label");
40122
+ bufferLabel.className = "lidar-control-label";
40123
+ bufferLabel.textContent = "Buffer Distance: ";
40124
+ this._bufferValue = document.createElement("span");
40125
+ this._bufferValue.textContent = `${this._options.bufferDistance} m`;
40126
+ bufferLabel.appendChild(this._bufferValue);
40127
+ bufferGroup.appendChild(bufferLabel);
40128
+ this._bufferSlider = document.createElement("input");
40129
+ this._bufferSlider.type = "range";
40130
+ this._bufferSlider.className = "lidar-control-slider";
40131
+ this._bufferSlider.min = "1";
40132
+ this._bufferSlider.max = "100";
40133
+ this._bufferSlider.step = "1";
40134
+ this._bufferSlider.value = String(this._options.bufferDistance);
40135
+ this._bufferSlider.addEventListener("input", (e) => {
40136
+ e.stopPropagation();
40137
+ const meters = parseInt(this._bufferSlider.value, 10);
40138
+ this._bufferValue.textContent = `${meters} m`;
40139
+ this._callbacks.onBufferDistanceChange(meters);
40140
+ });
40141
+ bufferGroup.appendChild(this._bufferSlider);
40142
+ this._container.appendChild(bufferGroup);
40143
+ this._chartContainer = document.createElement("div");
40144
+ this._chartContainer.className = "lidar-crosssection-chart";
40145
+ this._chartContainer.appendChild(this._chart.render());
40146
+ this._container.appendChild(this._chartContainer);
40147
+ this._statsContainer = document.createElement("div");
40148
+ this._statsContainer.className = "lidar-crosssection-stats";
40149
+ this._container.appendChild(this._statsContainer);
40150
+ this._updateStats();
40151
+ }
40152
+ /**
40153
+ * Updates the statistics display.
40154
+ */
40155
+ _updateStats() {
40156
+ if (!this._statsContainer) return;
40157
+ if (!this._profile || this._profile.points.length === 0) {
40158
+ this._statsContainer.innerHTML = '<div class="lidar-crosssection-stat">No profile data</div>';
40159
+ return;
40160
+ }
40161
+ const { stats } = this._profile;
40162
+ this._statsContainer.innerHTML = `
40163
+ <div class="lidar-crosssection-stat">
40164
+ <span class="lidar-crosssection-stat-label">Points:</span>
40165
+ <span class="lidar-crosssection-stat-value">${stats.pointCount.toLocaleString()}</span>
40166
+ </div>
40167
+ <div class="lidar-crosssection-stat">
40168
+ <span class="lidar-crosssection-stat-label">Distance:</span>
40169
+ <span class="lidar-crosssection-stat-value">${stats.totalDistance.toFixed(1)} m</span>
40170
+ </div>
40171
+ <div class="lidar-crosssection-stat">
40172
+ <span class="lidar-crosssection-stat-label">Elevation:</span>
40173
+ <span class="lidar-crosssection-stat-value">${stats.minElevation.toFixed(1)} - ${stats.maxElevation.toFixed(1)} m</span>
40174
+ </div>
40175
+ <div class="lidar-crosssection-stat">
40176
+ <span class="lidar-crosssection-stat-label">Mean:</span>
40177
+ <span class="lidar-crosssection-stat-value">${stats.meanElevation.toFixed(1)} m</span>
40178
+ </div>
40179
+ `;
40180
+ }
40181
+ /**
40182
+ * Sets up ResizeObserver for responsive chart width.
40183
+ */
40184
+ _setupResizeObserver() {
40185
+ this._resizeObserver = new ResizeObserver((entries) => {
40186
+ for (const entry of entries) {
40187
+ if (entry.target === this._container) {
40188
+ const containerWidth = entry.contentRect.width;
40189
+ const chartWidth = Math.max(200, containerWidth - 4);
40190
+ this._chart.resize(chartWidth, this._options.chartHeight);
40191
+ }
40192
+ }
40193
+ });
40194
+ this._resizeObserver.observe(this._container);
40195
+ }
40196
+ /**
40197
+ * Opens the popup with a larger, resizable chart.
40198
+ */
40199
+ _openPopup() {
40200
+ if (!this._profile) return;
40201
+ this._popupBackdrop = document.createElement("div");
40202
+ this._popupBackdrop.className = "lidar-chart-popup-backdrop";
40203
+ this._popupBackdrop.addEventListener("click", (e) => {
40204
+ if (this._ignoreBackdropClick) {
40205
+ this._ignoreBackdropClick = false;
40206
+ return;
40207
+ }
40208
+ if (e.target === this._popupBackdrop) {
40209
+ this._closePopup();
40210
+ }
40211
+ });
40212
+ this._popupContainer = document.createElement("div");
40213
+ this._popupContainer.className = "lidar-chart-popup";
40214
+ const header2 = document.createElement("div");
40215
+ header2.className = "lidar-chart-popup-header";
40216
+ const title = document.createElement("span");
40217
+ title.className = "lidar-chart-popup-title";
40218
+ title.textContent = "Cross-Section Elevation Profile";
40219
+ header2.appendChild(title);
40220
+ const closeBtn = document.createElement("button");
40221
+ closeBtn.className = "lidar-chart-popup-close";
40222
+ closeBtn.innerHTML = "×";
40223
+ closeBtn.title = "Close";
40224
+ closeBtn.addEventListener("click", () => this._closePopup());
40225
+ header2.appendChild(closeBtn);
40226
+ this._popupContainer.appendChild(header2);
40227
+ const popupChartContainer = document.createElement("div");
40228
+ popupChartContainer.className = "lidar-chart-popup-content";
40229
+ this._popupChartContainer = popupChartContainer;
40230
+ this._popupChart = new ElevationProfileChart({
40231
+ width: 700,
40232
+ height: 400,
40233
+ colormap: this._options.colormap
40234
+ });
40235
+ this._popupChart.setProfile(this._profile);
40236
+ popupChartContainer.appendChild(this._popupChart.render());
40237
+ this._popupContainer.appendChild(popupChartContainer);
40238
+ const popupStats = document.createElement("div");
40239
+ popupStats.className = "lidar-chart-popup-stats";
40240
+ const { stats } = this._profile;
40241
+ popupStats.innerHTML = `
40242
+ <span><strong>Points:</strong> ${stats.pointCount.toLocaleString()}</span>
40243
+ <span><strong>Distance:</strong> ${stats.totalDistance.toFixed(1)} m</span>
40244
+ <span><strong>Elevation:</strong> ${stats.minElevation.toFixed(1)} - ${stats.maxElevation.toFixed(1)} m</span>
40245
+ <span><strong>Mean:</strong> ${stats.meanElevation.toFixed(1)} m</span>
40246
+ `;
40247
+ this._popupContainer.appendChild(popupStats);
40248
+ const resizeHandle = document.createElement("div");
40249
+ resizeHandle.className = "lidar-chart-popup-resize";
40250
+ resizeHandle.title = "Drag to resize";
40251
+ resizeHandle.addEventListener("mousedown", (e) => {
40252
+ e.preventDefault();
40253
+ e.stopPropagation();
40254
+ this._startPopupResize(e);
40255
+ });
40256
+ this._popupContainer.appendChild(resizeHandle);
40257
+ this._popupBackdrop.appendChild(this._popupContainer);
40258
+ document.body.appendChild(this._popupBackdrop);
40259
+ document.body.style.overflow = "hidden";
40260
+ this._popupResizeObserver = new ResizeObserver(() => {
40261
+ this._syncPopupChartSize();
40262
+ });
40263
+ this._popupResizeObserver.observe(popupChartContainer);
40264
+ this._syncPopupChartSize();
40265
+ }
40266
+ /**
40267
+ * Closes the popup.
40268
+ */
40269
+ _closePopup() {
40270
+ if (this._popupBackdrop) {
40271
+ this._popupBackdrop.remove();
40272
+ this._popupBackdrop = void 0;
40273
+ }
40274
+ if (this._popupChart) {
40275
+ this._popupChart.destroy();
40276
+ this._popupChart = void 0;
40277
+ }
40278
+ if (this._popupResizeObserver) {
40279
+ this._popupResizeObserver.disconnect();
40280
+ this._popupResizeObserver = void 0;
40281
+ }
40282
+ this._popupContainer = void 0;
40283
+ this._popupChartContainer = void 0;
40284
+ this._ignoreBackdropClick = false;
40285
+ document.body.style.overflow = "";
40286
+ }
40287
+ /**
40288
+ * Starts popup resize operation.
40289
+ */
40290
+ _startPopupResize(e) {
40291
+ if (!this._popupContainer) return;
40292
+ this._isResizing = true;
40293
+ this._ignoreBackdropClick = true;
40294
+ this._resizeStartX = e.clientX;
40295
+ this._resizeStartY = e.clientY;
40296
+ const rect = this._popupContainer.getBoundingClientRect();
40297
+ this._resizeStartWidth = rect.width;
40298
+ this._resizeStartHeight = rect.height;
40299
+ document.addEventListener("mousemove", this._handlePopupResizeMouseMove);
40300
+ document.addEventListener("mouseup", this._handlePopupResizeMouseUp);
40301
+ document.body.style.cursor = "nwse-resize";
40302
+ document.body.style.userSelect = "none";
40303
+ }
40304
+ /**
40305
+ * Handles popup resize mouse move.
40306
+ */
40307
+ _onPopupResizeMouseMove(e) {
40308
+ if (!this._isResizing || !this._popupContainer) return;
40309
+ const deltaX = e.clientX - this._resizeStartX;
40310
+ const deltaY = e.clientY - this._resizeStartY;
40311
+ const newWidth = Math.max(400, Math.min(window.innerWidth - 40, this._resizeStartWidth + deltaX));
40312
+ const newHeight = Math.max(300, Math.min(window.innerHeight - 40, this._resizeStartHeight + deltaY));
40313
+ this._popupContainer.style.width = `${newWidth}px`;
40314
+ this._popupContainer.style.height = `${newHeight}px`;
40315
+ this._syncPopupChartSize();
40316
+ }
40317
+ /**
40318
+ * Handles popup resize mouse up.
40319
+ */
40320
+ _onPopupResizeMouseUp() {
40321
+ this._isResizing = false;
40322
+ document.removeEventListener("mousemove", this._handlePopupResizeMouseMove);
40323
+ document.removeEventListener("mouseup", this._handlePopupResizeMouseUp);
40324
+ document.body.style.cursor = "";
40325
+ document.body.style.userSelect = "";
40326
+ this._syncPopupChartSize();
40327
+ window.setTimeout(() => {
40328
+ this._ignoreBackdropClick = false;
40329
+ }, 0);
40330
+ if (this._popupChart && this._profile) {
40331
+ this._popupChart.setProfile(this._profile);
40332
+ }
40333
+ }
40334
+ _syncPopupChartSize() {
40335
+ if (!this._popupChart || !this._popupChartContainer) return;
40336
+ const rect = this._popupChartContainer.getBoundingClientRect();
40337
+ const styles = window.getComputedStyle(this._popupChartContainer);
40338
+ const paddingX = (Number.parseFloat(styles.paddingLeft) || 0) + (Number.parseFloat(styles.paddingRight) || 0);
40339
+ const paddingY = (Number.parseFloat(styles.paddingTop) || 0) + (Number.parseFloat(styles.paddingBottom) || 0);
40340
+ const chartWidth = Math.max(200, rect.width - paddingX);
40341
+ const chartHeight = Math.max(150, rect.height - paddingY);
40342
+ this._popupChart.resize(chartWidth, chartHeight);
40343
+ }
40344
+ /**
40345
+ * Downloads the profile data as CSV.
40346
+ */
40347
+ _downloadCSV() {
40348
+ if (!this._profile || this._profile.points.length === 0) return;
40349
+ const headers = ["distance", "elevation", "offsetFromLine", "longitude", "latitude", "intensity", "classification"];
40350
+ const rows = [headers.join(",")];
40351
+ for (const point of this._profile.points) {
40352
+ const row = [
40353
+ point.distance.toFixed(3),
40354
+ point.elevation.toFixed(3),
40355
+ point.offsetFromLine.toFixed(3),
40356
+ point.longitude.toFixed(8),
40357
+ point.latitude.toFixed(8),
40358
+ point.intensity !== void 0 ? point.intensity.toFixed(4) : "",
40359
+ point.classification !== void 0 ? String(point.classification) : ""
40360
+ ];
40361
+ rows.push(row.join(","));
40362
+ }
40363
+ const csvContent = rows.join("\n");
40364
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
40365
+ const url = URL.createObjectURL(blob);
40366
+ const link = document.createElement("a");
40367
+ link.href = url;
40368
+ link.download = `cross-section-profile-${Date.now()}.csv`;
40369
+ link.style.display = "none";
40370
+ document.body.appendChild(link);
40371
+ link.click();
40372
+ document.body.removeChild(link);
40373
+ URL.revokeObjectURL(url);
40374
+ }
40375
+ /**
40376
+ * Destroys the panel and cleans up resources.
40377
+ */
40378
+ destroy() {
40379
+ this._closePopup();
40380
+ if (this._resizeObserver) {
40381
+ this._resizeObserver.disconnect();
40382
+ this._resizeObserver = void 0;
40383
+ }
40384
+ if (this._isResizing) {
40385
+ document.removeEventListener("mousemove", this._handlePopupResizeMouseMove);
40386
+ document.removeEventListener("mouseup", this._handlePopupResizeMouseUp);
40387
+ document.body.style.cursor = "";
40388
+ document.body.style.userSelect = "";
40389
+ }
40390
+ this._chart.destroy();
40391
+ this._container.remove();
40392
+ }
40393
+ }
40394
+ class ElevationProfileExtractor {
40395
+ /**
40396
+ * Extracts points within the buffer distance of a cross-section line.
40397
+ *
40398
+ * @param line - The cross-section line definition
40399
+ * @param pointCloudData - The point cloud data to extract from
40400
+ * @param coordinateOrigin - The coordinate origin [lng, lat, z]
40401
+ * @returns Elevation profile with sorted points and statistics
40402
+ */
40403
+ static extract(line, pointCloudData, coordinateOrigin) {
40404
+ const points = [];
40405
+ const { positions, intensities, classifications, pointCount } = pointCloudData;
40406
+ const lineStart = line.start;
40407
+ const lineEnd = line.end;
40408
+ const totalDistance = this.haversineDistance(lineStart, lineEnd);
40409
+ const dx = lineEnd[0] - lineStart[0];
40410
+ const dy = lineEnd[1] - lineStart[1];
40411
+ const lineLengthSq = dx * dx + dy * dy;
40412
+ for (let i = 0; i < pointCount; i++) {
40413
+ const lng = positions[i * 3] + coordinateOrigin[0];
40414
+ const lat = positions[i * 3 + 1] + coordinateOrigin[1];
40415
+ const elevation = positions[i * 3 + 2];
40416
+ const { distance: distanceAlongLine, offset } = this.pointToLineDistance(
40417
+ [lng, lat],
40418
+ lineStart,
40419
+ lineEnd,
40420
+ lineLengthSq,
40421
+ totalDistance
40422
+ );
40423
+ if (offset <= line.bufferDistance && distanceAlongLine >= 0 && distanceAlongLine <= totalDistance) {
40424
+ const point = {
40425
+ distance: distanceAlongLine,
40426
+ elevation,
40427
+ offsetFromLine: offset,
40428
+ longitude: lng,
40429
+ latitude: lat,
40430
+ intensity: intensities ? intensities[i] : void 0,
40431
+ classification: classifications ? classifications[i] : void 0
40432
+ };
40433
+ points.push(point);
40434
+ }
40435
+ }
40436
+ points.sort((a, b) => a.distance - b.distance);
40437
+ const stats = this.calculateStats(points, totalDistance);
40438
+ return {
40439
+ line,
40440
+ points,
40441
+ stats
40442
+ };
40443
+ }
40444
+ /**
40445
+ * Calculates the Haversine distance between two WGS84 points.
40446
+ *
40447
+ * @param p1 - First point [lng, lat]
40448
+ * @param p2 - Second point [lng, lat]
40449
+ * @returns Distance in meters
40450
+ */
40451
+ static haversineDistance(p1, p2) {
40452
+ const lat1 = this.toRadians(p1[1]);
40453
+ const lat2 = this.toRadians(p2[1]);
40454
+ const dLat = this.toRadians(p2[1] - p1[1]);
40455
+ const dLng = this.toRadians(p2[0] - p1[0]);
40456
+ const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
40457
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
40458
+ return this.EARTH_RADIUS_M * c;
40459
+ }
40460
+ /**
40461
+ * Calculates the distance along a line and perpendicular offset from a point.
40462
+ *
40463
+ * @param point - The point [lng, lat]
40464
+ * @param lineStart - Line start [lng, lat]
40465
+ * @param lineEnd - Line end [lng, lat]
40466
+ * @param lineLengthSq - Squared length of line vector (for efficiency)
40467
+ * @param totalLineDistance - Total line distance in meters
40468
+ * @returns Distance along line (m) and perpendicular offset (m)
40469
+ */
40470
+ static pointToLineDistance(point, lineStart, lineEnd, lineLengthSq, totalLineDistance) {
40471
+ const dx = lineEnd[0] - lineStart[0];
40472
+ const dy = lineEnd[1] - lineStart[1];
40473
+ const px = point[0] - lineStart[0];
40474
+ const py = point[1] - lineStart[1];
40475
+ let t = (px * dx + py * dy) / lineLengthSq;
40476
+ t = Math.max(0, Math.min(1, t));
40477
+ const closestLng = lineStart[0] + t * dx;
40478
+ const closestLat = lineStart[1] + t * dy;
40479
+ const distanceAlongLine = t * totalLineDistance;
40480
+ const offset = this.haversineDistance(point, [closestLng, closestLat]);
40481
+ return { distance: distanceAlongLine, offset };
40482
+ }
40483
+ /**
40484
+ * Calculates profile statistics.
40485
+ *
40486
+ * @param points - Profile points
40487
+ * @param totalDistance - Total line distance
40488
+ * @returns Statistics object
40489
+ */
40490
+ static calculateStats(points, totalDistance) {
40491
+ if (points.length === 0) {
40492
+ return {
40493
+ minElevation: 0,
40494
+ maxElevation: 0,
40495
+ meanElevation: 0,
40496
+ totalDistance,
40497
+ pointCount: 0
40498
+ };
40499
+ }
40500
+ let minElevation = Infinity;
40501
+ let maxElevation = -Infinity;
40502
+ let sumElevation = 0;
40503
+ for (const point of points) {
40504
+ minElevation = Math.min(minElevation, point.elevation);
40505
+ maxElevation = Math.max(maxElevation, point.elevation);
40506
+ sumElevation += point.elevation;
40507
+ }
40508
+ return {
40509
+ minElevation,
40510
+ maxElevation,
40511
+ meanElevation: sumElevation / points.length,
40512
+ totalDistance,
40513
+ pointCount: points.length
40514
+ };
40515
+ }
40516
+ /**
40517
+ * Converts degrees to radians.
40518
+ *
40519
+ * @param degrees - Angle in degrees
40520
+ * @returns Angle in radians
40521
+ */
40522
+ static toRadians(degrees) {
40523
+ return degrees * (Math.PI / 180);
40524
+ }
40525
+ /**
40526
+ * Creates a buffer polygon around a line for visualization.
40527
+ * Returns GeoJSON coordinates for a polygon.
40528
+ *
40529
+ * @param line - The cross-section line
40530
+ * @param numPoints - Number of points for end caps
40531
+ * @returns GeoJSON polygon coordinates
40532
+ */
40533
+ static createBufferPolygon(line, numPoints = 8) {
40534
+ const [startLng, startLat] = line.start;
40535
+ const [endLng, endLat] = line.end;
40536
+ const bufferDegrees = this.metersToDegrees(line.bufferDistance, (startLat + endLat) / 2);
40537
+ const dx = endLng - startLng;
40538
+ const dy = endLat - startLat;
40539
+ const len = Math.sqrt(dx * dx + dy * dy);
40540
+ const perpX = -dy / len;
40541
+ const perpY = dx / len;
40542
+ const coords = [];
40543
+ coords.push([startLng + perpX * bufferDegrees, startLat + perpY * bufferDegrees]);
40544
+ coords.push([endLng + perpX * bufferDegrees, endLat + perpY * bufferDegrees]);
40545
+ for (let i = 0; i <= numPoints; i++) {
40546
+ const angle = Math.atan2(perpY, perpX) - Math.PI + Math.PI * i / numPoints;
40547
+ coords.push([
40548
+ endLng + Math.cos(angle) * bufferDegrees,
40549
+ endLat + Math.sin(angle) * bufferDegrees
40550
+ ]);
40551
+ }
40552
+ coords.push([endLng - perpX * bufferDegrees, endLat - perpY * bufferDegrees]);
40553
+ coords.push([startLng - perpX * bufferDegrees, startLat - perpY * bufferDegrees]);
40554
+ for (let i = 0; i <= numPoints; i++) {
40555
+ const angle = Math.atan2(perpY, perpX) + Math.PI * i / numPoints;
40556
+ coords.push([
40557
+ startLng + Math.cos(angle) * bufferDegrees,
40558
+ startLat + Math.sin(angle) * bufferDegrees
40559
+ ]);
40560
+ }
40561
+ coords.push(coords[0]);
40562
+ return coords;
40563
+ }
40564
+ /**
40565
+ * Converts meters to approximate degrees at a given latitude.
40566
+ *
40567
+ * @param meters - Distance in meters
40568
+ * @param latitude - Latitude for conversion
40569
+ * @returns Approximate degrees
40570
+ */
40571
+ static metersToDegrees(meters, latitude) {
40572
+ const latRadians = this.toRadians(latitude);
40573
+ return meters / (this.EARTH_RADIUS_M * Math.cos(latRadians) * (Math.PI / 180));
40574
+ }
40575
+ }
40576
+ __publicField(ElevationProfileExtractor, "EARTH_RADIUS_M", 6371e3);
40577
+ class CrossSectionTool {
40578
+ /**
40579
+ * Creates a new CrossSectionTool instance.
40580
+ *
40581
+ * @param map - MapLibre map instance
40582
+ * @param deckOverlay - DeckOverlay instance for rendering
40583
+ */
40584
+ constructor(map, deckOverlay) {
40585
+ __publicField(this, "_map");
40586
+ __publicField(this, "_deckOverlay");
40587
+ __publicField(this, "_enabled", false);
40588
+ __publicField(this, "_isDrawing", false);
40589
+ __publicField(this, "_startPoint", null);
40590
+ __publicField(this, "_endPoint", null);
40591
+ __publicField(this, "_bufferDistance", 10);
40592
+ // meters
40593
+ // deck.gl layer ID
40594
+ __publicField(this, "LAYER_ID", "lidar-cross-section-layer");
40595
+ // Event callbacks
40596
+ __publicField(this, "_onLineChange");
40597
+ // Bound event handlers
40598
+ __publicField(this, "_handleClickBound");
40599
+ __publicField(this, "_handleMouseMoveBound");
40600
+ this._map = map;
40601
+ this._deckOverlay = deckOverlay;
40602
+ this._handleClickBound = this._handleClick.bind(this);
40603
+ this._handleMouseMoveBound = this._handleMouseMove.bind(this);
40604
+ }
40605
+ /**
40606
+ * Enables cross-section drawing mode.
40607
+ */
40608
+ enable() {
40609
+ if (this._enabled) return;
40610
+ this._enabled = true;
40611
+ this._addEventListeners();
40612
+ this._map.getCanvas().style.cursor = "crosshair";
40613
+ }
40614
+ /**
40615
+ * Disables cross-section drawing mode.
40616
+ */
40617
+ disable() {
40618
+ if (!this._enabled) return;
40619
+ this._enabled = false;
40620
+ this._removeEventListeners();
40621
+ this._map.getCanvas().style.cursor = "";
40622
+ this._isDrawing = false;
40623
+ }
40624
+ /**
40625
+ * Checks if the tool is enabled.
40626
+ *
40627
+ * @returns True if enabled
40628
+ */
40629
+ isEnabled() {
40630
+ return this._enabled;
40631
+ }
40632
+ /**
40633
+ * Gets the current cross-section line.
40634
+ *
40635
+ * @returns Cross-section line or null if not defined
40636
+ */
40637
+ getLine() {
40638
+ if (!this._startPoint || !this._endPoint) return null;
40639
+ return {
40640
+ start: this._startPoint,
40641
+ end: this._endPoint,
40642
+ bufferDistance: this._bufferDistance
40643
+ };
40644
+ }
40645
+ /**
40646
+ * Sets the buffer distance.
40647
+ *
40648
+ * @param meters - Buffer distance in meters
40649
+ */
40650
+ setBufferDistance(meters) {
40651
+ this._bufferDistance = meters;
40652
+ this._updateVisualization();
40653
+ this._notifyLineChange();
40654
+ }
40655
+ /**
40656
+ * Gets the current buffer distance.
40657
+ *
40658
+ * @returns Buffer distance in meters
40659
+ */
40660
+ getBufferDistance() {
40661
+ return this._bufferDistance;
40662
+ }
40663
+ /**
40664
+ * Clears the current cross-section line.
40665
+ */
40666
+ clearLine() {
40667
+ this._startPoint = null;
40668
+ this._endPoint = null;
40669
+ this._isDrawing = false;
40670
+ this._removeDeckLayers();
40671
+ this._notifyLineChange();
40672
+ }
40673
+ /**
40674
+ * Sets the callback for line changes.
40675
+ *
40676
+ * @param callback - Callback function
40677
+ */
40678
+ setOnLineChange(callback) {
40679
+ this._onLineChange = callback;
40680
+ }
40681
+ /**
40682
+ * Destroys the tool and cleans up resources.
40683
+ */
40684
+ destroy() {
40685
+ this.disable();
40686
+ this._removeDeckLayers();
40687
+ }
40688
+ /**
40689
+ * Removes deck.gl layers.
40690
+ */
40691
+ _removeDeckLayers() {
40692
+ if (this._deckOverlay.hasLayer(this.LAYER_ID)) {
40693
+ this._deckOverlay.removeLayer(this.LAYER_ID);
40694
+ }
40695
+ }
40696
+ /**
40697
+ * Adds event listeners for drawing.
40698
+ */
40699
+ _addEventListeners() {
40700
+ this._map.on("click", this._handleClickBound);
40701
+ this._map.on("mousemove", this._handleMouseMoveBound);
40702
+ }
40703
+ /**
40704
+ * Removes event listeners.
40705
+ */
40706
+ _removeEventListeners() {
40707
+ this._map.off("click", this._handleClickBound);
40708
+ this._map.off("mousemove", this._handleMouseMoveBound);
40709
+ }
40710
+ /**
40711
+ * Handles map click events.
40712
+ *
40713
+ * @param e - Map mouse event
40714
+ */
40715
+ _handleClick(e) {
40716
+ const lngLat = e.lngLat;
40717
+ if (!this._isDrawing) {
40718
+ this._startPoint = [lngLat.lng, lngLat.lat];
40719
+ this._endPoint = null;
40720
+ this._isDrawing = true;
40721
+ } else {
40722
+ this._endPoint = [lngLat.lng, lngLat.lat];
40723
+ this._isDrawing = false;
40724
+ this._notifyLineChange();
40725
+ }
40726
+ this._updateVisualization();
40727
+ }
40728
+ /**
40729
+ * Handles mouse move events for preview.
40730
+ *
40731
+ * @param e - Map mouse event
40732
+ */
40733
+ _handleMouseMove(e) {
40734
+ if (!this._isDrawing || !this._startPoint) return;
40735
+ this._endPoint = [e.lngLat.lng, e.lngLat.lat];
40736
+ this._updateVisualization();
40737
+ }
40738
+ /**
40739
+ * Updates the visualization on the map using deck.gl.
40740
+ */
40741
+ _updateVisualization() {
40742
+ const features = [];
40743
+ if (this._startPoint) {
40744
+ features.push({
40745
+ type: "Feature",
40746
+ properties: { type: "point", position: "start" },
40747
+ geometry: {
40748
+ type: "Point",
40749
+ coordinates: this._startPoint
40750
+ }
40751
+ });
40752
+ }
40753
+ if (this._startPoint && this._endPoint) {
40754
+ features.push({
40755
+ type: "Feature",
40756
+ properties: { type: "point", position: "end" },
40757
+ geometry: {
40758
+ type: "Point",
40759
+ coordinates: this._endPoint
40760
+ }
40761
+ });
40762
+ features.push({
40763
+ type: "Feature",
40764
+ properties: { type: "line" },
40765
+ geometry: {
40766
+ type: "LineString",
40767
+ coordinates: [this._startPoint, this._endPoint]
40768
+ }
40769
+ });
40770
+ const bufferCoords = ElevationProfileExtractor.createBufferPolygon({
40771
+ start: this._startPoint,
40772
+ end: this._endPoint,
40773
+ bufferDistance: this._bufferDistance
40774
+ });
40775
+ features.push({
40776
+ type: "Feature",
40777
+ properties: { type: "buffer" },
40778
+ geometry: {
40779
+ type: "Polygon",
40780
+ coordinates: [bufferCoords]
40781
+ }
40782
+ });
40783
+ }
40784
+ const geojsonData = {
40785
+ type: "FeatureCollection",
40786
+ features
40787
+ };
40788
+ const layer = new GeoJsonLayer({
40789
+ id: this.LAYER_ID,
40790
+ data: geojsonData,
40791
+ pickable: false,
40792
+ stroked: true,
40793
+ filled: true,
40794
+ // Disable depth testing so this layer always renders on top
40795
+ parameters: {
40796
+ depthTest: false,
40797
+ depthMask: false
40798
+ },
40799
+ // Fill color for polygons (buffer) and points
40800
+ getFillColor: (f) => {
40801
+ var _a, _b;
40802
+ if (((_a = f.properties) == null ? void 0 : _a.type) === "point") {
40803
+ return [255, 51, 51, 255];
40804
+ }
40805
+ if (((_b = f.properties) == null ? void 0 : _b.type) === "buffer") {
40806
+ return [51, 136, 255, 50];
40807
+ }
40808
+ return [0, 0, 0, 0];
40809
+ },
40810
+ // Line color for lines and polygon borders
40811
+ getLineColor: (f) => {
40812
+ var _a, _b, _c;
40813
+ if (((_a = f.properties) == null ? void 0 : _a.type) === "line") {
40814
+ return [51, 136, 255, 255];
40815
+ }
40816
+ if (((_b = f.properties) == null ? void 0 : _b.type) === "buffer") {
40817
+ return [51, 136, 255, 100];
40818
+ }
40819
+ if (((_c = f.properties) == null ? void 0 : _c.type) === "point") {
40820
+ return [255, 255, 255, 255];
40821
+ }
40822
+ return [0, 0, 0, 0];
40823
+ },
40824
+ getLineWidth: (f) => {
40825
+ var _a, _b;
40826
+ if (((_a = f.properties) == null ? void 0 : _a.type) === "line") {
40827
+ return 3;
40828
+ }
40829
+ if (((_b = f.properties) == null ? void 0 : _b.type) === "point") {
40830
+ return 2;
40831
+ }
40832
+ return 1;
40833
+ },
40834
+ lineWidthUnits: "pixels",
40835
+ // Point styling
40836
+ pointType: "circle",
40837
+ getPointRadius: 8,
40838
+ pointRadiusUnits: "pixels"
40839
+ });
40840
+ if (this._deckOverlay.hasLayer(this.LAYER_ID)) {
40841
+ this._deckOverlay.updateLayer(this.LAYER_ID, layer);
40842
+ } else {
40843
+ this._deckOverlay.addLayer(this.LAYER_ID, layer);
40844
+ }
40845
+ }
40846
+ /**
40847
+ * Notifies listeners of line change.
40848
+ */
40849
+ _notifyLineChange() {
40850
+ if (this._onLineChange) {
40851
+ this._onLineChange(this.getLine());
40852
+ }
40853
+ }
38989
40854
  }
38990
40855
  const DEFAULT_OPTIONS = {
38991
40856
  collapsed: true,
@@ -39049,6 +40914,12 @@ class LidarControl {
39049
40914
  __publicField(this, "_viewportManagers", /* @__PURE__ */ new Map());
39050
40915
  __publicField(this, "_eptViewportRequestIds", /* @__PURE__ */ new Map());
39051
40916
  __publicField(this, "_eptLastViewport", /* @__PURE__ */ new Map());
40917
+ // Metadata and cross-section components
40918
+ __publicField(this, "_metadataPanel");
40919
+ __publicField(this, "_fullMetadata", /* @__PURE__ */ new Map());
40920
+ __publicField(this, "_crossSectionTool");
40921
+ __publicField(this, "_crossSectionPanel");
40922
+ __publicField(this, "_currentProfile", null);
39052
40923
  this._options = { ...DEFAULT_OPTIONS, ...options };
39053
40924
  const defaultColorRange = this._options.colorRange ?? {
39054
40925
  mode: "percentile",
@@ -40479,7 +42350,9 @@ class LidarControl {
40479
42350
  onClassificationToggle: (code, visible) => this._toggleClassification(code, visible),
40480
42351
  onClassificationShowAll: () => this._showAllClassifications(),
40481
42352
  onClassificationHideAll: () => this._hideAllClassifications(),
40482
- onTerrainChange: (enabled) => this.setTerrain(enabled)
42353
+ onTerrainChange: (enabled) => this.setTerrain(enabled),
42354
+ onShowMetadata: (id) => this.showMetadataPanel(id),
42355
+ onCrossSectionPanel: () => this.getCrossSectionPanel().render()
40483
42356
  },
40484
42357
  this._state
40485
42358
  );
@@ -40742,6 +42615,10 @@ class LidarControl {
40742
42615
  _setupEventListeners() {
40743
42616
  var _a;
40744
42617
  this._clickOutsideHandler = (e) => {
42618
+ var _a2, _b;
42619
+ if (((_a2 = this._crossSectionTool) == null ? void 0 : _a2.isEnabled()) || ((_b = this._crossSectionTool) == null ? void 0 : _b.getLine())) {
42620
+ return;
42621
+ }
40745
42622
  const target = e.target;
40746
42623
  if (this._container && this._panel && !this._container.contains(target) && !this._panel.contains(target)) {
40747
42624
  this.collapse();
@@ -40815,6 +42692,223 @@ class LidarControl {
40815
42692
  break;
40816
42693
  }
40817
42694
  }
42695
+ // ==================== Metadata Viewer API ====================
42696
+ /**
42697
+ * Gets the full metadata for a point cloud.
42698
+ *
42699
+ * @param id - Point cloud ID. If not provided, returns metadata for the active point cloud.
42700
+ * @returns Full metadata or undefined if not available
42701
+ */
42702
+ getFullMetadata(id) {
42703
+ const targetId = id ?? this._state.activePointCloudId;
42704
+ if (!targetId) return void 0;
42705
+ return this._fullMetadata.get(targetId);
42706
+ }
42707
+ /**
42708
+ * Shows the metadata panel for a point cloud.
42709
+ *
42710
+ * @param id - Point cloud ID. If not provided, shows metadata for the active point cloud.
42711
+ */
42712
+ showMetadataPanel(id) {
42713
+ const targetId = id ?? this._state.activePointCloudId;
42714
+ if (!targetId) return;
42715
+ let metadata = this._fullMetadata.get(targetId);
42716
+ if (!metadata) {
42717
+ metadata = this._buildFullMetadata(targetId);
42718
+ if (metadata) {
42719
+ this._fullMetadata.set(targetId, metadata);
42720
+ }
42721
+ }
42722
+ if (!metadata) return;
42723
+ if (!this._metadataPanel) {
42724
+ this._metadataPanel = new MetadataPanel({
42725
+ onClose: () => {
42726
+ }
42727
+ });
42728
+ }
42729
+ this._metadataPanel.show(metadata);
42730
+ }
42731
+ /**
42732
+ * Hides the metadata panel.
42733
+ */
42734
+ hideMetadataPanel() {
42735
+ var _a;
42736
+ (_a = this._metadataPanel) == null ? void 0 : _a.hide();
42737
+ }
42738
+ /**
42739
+ * Builds full metadata for a point cloud.
42740
+ *
42741
+ * @param id - Point cloud ID
42742
+ * @returns Full metadata or undefined
42743
+ */
42744
+ _buildFullMetadata(id) {
42745
+ const basic = this._state.pointClouds.find((pc) => pc.id === id);
42746
+ if (!basic) return void 0;
42747
+ const copcLoader = this._streamingLoaders.get(id);
42748
+ if (copcLoader) {
42749
+ const copcMeta = copcLoader.getCopcMetadata();
42750
+ return {
42751
+ type: "copc",
42752
+ copc: copcMeta,
42753
+ basic: { ...basic, wkt: copcLoader.getWkt() ?? basic.wkt }
42754
+ };
42755
+ }
42756
+ const eptLoader = this._eptStreamingLoaders.get(id);
42757
+ if (eptLoader) {
42758
+ const eptMeta = eptLoader.getExtendedMetadata();
42759
+ return {
42760
+ type: "ept",
42761
+ ept: eptMeta,
42762
+ basic
42763
+ };
42764
+ }
42765
+ return {
42766
+ type: "las",
42767
+ basic
42768
+ };
42769
+ }
42770
+ // ==================== Cross-Section API ====================
42771
+ /**
42772
+ * Enables cross-section drawing mode.
42773
+ */
42774
+ enableCrossSection() {
42775
+ var _a, _b;
42776
+ if (!this._map) return;
42777
+ if (!this._crossSectionTool && this._deckOverlay) {
42778
+ this._crossSectionTool = new CrossSectionTool(this._map, this._deckOverlay);
42779
+ this._crossSectionTool.setOnLineChange((line) => {
42780
+ this._handleCrossSectionLineChange(line);
42781
+ });
42782
+ }
42783
+ (_a = this._crossSectionTool) == null ? void 0 : _a.enable();
42784
+ (_b = this._crossSectionPanel) == null ? void 0 : _b.setDrawing(true);
42785
+ }
42786
+ /**
42787
+ * Disables cross-section drawing mode.
42788
+ */
42789
+ disableCrossSection() {
42790
+ var _a, _b;
42791
+ (_a = this._crossSectionTool) == null ? void 0 : _a.disable();
42792
+ (_b = this._crossSectionPanel) == null ? void 0 : _b.setDrawing(false);
42793
+ }
42794
+ /**
42795
+ * Checks if cross-section mode is enabled.
42796
+ *
42797
+ * @returns True if enabled
42798
+ */
42799
+ isCrossSectionEnabled() {
42800
+ var _a;
42801
+ return ((_a = this._crossSectionTool) == null ? void 0 : _a.isEnabled()) ?? false;
42802
+ }
42803
+ /**
42804
+ * Gets the current cross-section elevation profile.
42805
+ *
42806
+ * @returns Elevation profile or null if not available
42807
+ */
42808
+ getCrossSectionProfile() {
42809
+ return this._currentProfile;
42810
+ }
42811
+ /**
42812
+ * Sets the cross-section buffer distance.
42813
+ *
42814
+ * @param meters - Buffer distance in meters
42815
+ */
42816
+ setCrossSectionBufferDistance(meters) {
42817
+ var _a, _b, _c;
42818
+ (_a = this._crossSectionTool) == null ? void 0 : _a.setBufferDistance(meters);
42819
+ (_b = this._crossSectionPanel) == null ? void 0 : _b.setBufferDistance(meters);
42820
+ const line = (_c = this._crossSectionTool) == null ? void 0 : _c.getLine();
42821
+ if (line) {
42822
+ this._extractElevationProfile(line);
42823
+ }
42824
+ }
42825
+ /**
42826
+ * Gets the current cross-section buffer distance.
42827
+ *
42828
+ * @returns Buffer distance in meters
42829
+ */
42830
+ getCrossSectionBufferDistance() {
42831
+ var _a;
42832
+ return ((_a = this._crossSectionTool) == null ? void 0 : _a.getBufferDistance()) ?? 10;
42833
+ }
42834
+ /**
42835
+ * Clears the current cross-section.
42836
+ */
42837
+ clearCrossSection() {
42838
+ var _a, _b;
42839
+ (_a = this._crossSectionTool) == null ? void 0 : _a.clearLine();
42840
+ this._currentProfile = null;
42841
+ (_b = this._crossSectionPanel) == null ? void 0 : _b.setProfile(null);
42842
+ }
42843
+ /**
42844
+ * Gets the current cross-section line.
42845
+ *
42846
+ * @returns Cross-section line or null
42847
+ */
42848
+ getCrossSectionLine() {
42849
+ var _a;
42850
+ return ((_a = this._crossSectionTool) == null ? void 0 : _a.getLine()) ?? null;
42851
+ }
42852
+ /**
42853
+ * Handles cross-section line changes.
42854
+ *
42855
+ * @param line - New line or null if cleared
42856
+ */
42857
+ _handleCrossSectionLineChange(line) {
42858
+ var _a, _b, _c;
42859
+ if (line) {
42860
+ this._extractElevationProfile(line);
42861
+ (_a = this._crossSectionTool) == null ? void 0 : _a.disable();
42862
+ (_b = this._crossSectionPanel) == null ? void 0 : _b.setDrawing(false);
42863
+ } else {
42864
+ this._currentProfile = null;
42865
+ (_c = this._crossSectionPanel) == null ? void 0 : _c.setProfile(null);
42866
+ }
42867
+ }
42868
+ /**
42869
+ * Extracts elevation profile for the current cross-section line.
42870
+ *
42871
+ * @param line - Cross-section line
42872
+ */
42873
+ _extractElevationProfile(line) {
42874
+ var _a, _b, _c;
42875
+ const pointCloudData = (_a = this._pointCloudManager) == null ? void 0 : _a.getMergedPointCloudData();
42876
+ if (!pointCloudData || pointCloudData.pointCount === 0) {
42877
+ this._currentProfile = null;
42878
+ (_b = this._crossSectionPanel) == null ? void 0 : _b.setProfile(null);
42879
+ return;
42880
+ }
42881
+ this._currentProfile = ElevationProfileExtractor.extract(
42882
+ line,
42883
+ pointCloudData,
42884
+ pointCloudData.coordinateOrigin
42885
+ );
42886
+ (_c = this._crossSectionPanel) == null ? void 0 : _c.setProfile(this._currentProfile);
42887
+ }
42888
+ /**
42889
+ * Gets the cross-section panel for adding to the UI.
42890
+ * Creates the panel if it doesn't exist.
42891
+ *
42892
+ * @returns CrossSectionPanel instance
42893
+ */
42894
+ getCrossSectionPanel() {
42895
+ if (!this._crossSectionPanel) {
42896
+ this._crossSectionPanel = new CrossSectionPanel({
42897
+ onDrawToggle: (enabled) => {
42898
+ if (enabled) {
42899
+ this.enableCrossSection();
42900
+ } else {
42901
+ this.disableCrossSection();
42902
+ }
42903
+ },
42904
+ onClear: () => this.clearCrossSection(),
42905
+ onBufferDistanceChange: (meters) => this.setCrossSectionBufferDistance(meters)
42906
+ }, {
42907
+ colormap: this._state.colormap
42908
+ });
42909
+ }
42910
+ return this._crossSectionPanel;
42911
+ }
40818
42912
  }
40819
42913
  class LidarLayerAdapter {
40820
42914
  /**
@@ -40970,6 +43064,7 @@ export {
40970
43064
  DeckOverlay as D,
40971
43065
  EptStreamingLoader as E,
40972
43066
  LidarControl as L,
43067
+ MetadataPanel as M,
40973
43068
  PointCloudLoader as P,
40974
43069
  ViewportManager as V,
40975
43070
  PointCloudManager as a,
@@ -40979,15 +43074,19 @@ export {
40979
43074
  COLORMAP_LABELS as e,
40980
43075
  getColormap as f,
40981
43076
  getClassificationName as g,
40982
- LidarLayerAdapter as h,
40983
- clamp as i,
40984
- formatNumericValue as j,
40985
- generateId as k,
40986
- debounce as l,
40987
- classNames as m,
40988
- formatNumber as n,
40989
- formatBytes as o,
40990
- getFilename as p,
40991
- throttle as t
43077
+ CrossSectionTool as h,
43078
+ ElevationProfileExtractor as i,
43079
+ CrossSectionPanel as j,
43080
+ ElevationProfileChart as k,
43081
+ LidarLayerAdapter as l,
43082
+ clamp as m,
43083
+ formatNumericValue as n,
43084
+ generateId as o,
43085
+ debounce as p,
43086
+ classNames as q,
43087
+ formatNumber as r,
43088
+ formatBytes as s,
43089
+ throttle as t,
43090
+ getFilename as u
40992
43091
  };
40993
- //# sourceMappingURL=LidarLayerAdapter-kyyfQw05.js.map
43092
+ //# sourceMappingURL=LidarLayerAdapter-CgPF0Hvt.js.map