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.
- package/dist/{LidarLayerAdapter-kyyfQw05.js → LidarLayerAdapter-CgPF0Hvt.js} +2113 -14
- package/dist/LidarLayerAdapter-CgPF0Hvt.js.map +1 -0
- package/dist/{LidarLayerAdapter-eh59KEMT.cjs → LidarLayerAdapter-Ds5r98GR.cjs} +2102 -3
- package/dist/LidarLayerAdapter-Ds5r98GR.cjs.map +1 -0
- package/dist/index.cjs +6 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +15 -10
- package/dist/maplibre-gl-lidar.css +427 -0
- package/dist/react.cjs +1 -1
- package/dist/react.mjs +3 -3
- package/dist/types/index.d.ts +5 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/core/DeckOverlay.d.ts +1 -0
- package/dist/types/lib/core/DeckOverlay.d.ts.map +1 -1
- package/dist/types/lib/core/LidarControl.d.ts +92 -1
- package/dist/types/lib/core/LidarControl.d.ts.map +1 -1
- package/dist/types/lib/core/types.d.ts +134 -0
- package/dist/types/lib/core/types.d.ts.map +1 -1
- package/dist/types/lib/gui/CrossSectionPanel.d.ts +135 -0
- package/dist/types/lib/gui/CrossSectionPanel.d.ts.map +1 -0
- package/dist/types/lib/gui/ElevationProfileChart.d.ts +122 -0
- package/dist/types/lib/gui/ElevationProfileChart.d.ts.map +1 -0
- package/dist/types/lib/gui/MetadataPanel.d.ts +110 -0
- package/dist/types/lib/gui/MetadataPanel.d.ts.map +1 -0
- package/dist/types/lib/gui/PanelBuilder.d.ts +8 -0
- package/dist/types/lib/gui/PanelBuilder.d.ts.map +1 -1
- package/dist/types/lib/layers/PointCloudManager.d.ts +7 -0
- package/dist/types/lib/layers/PointCloudManager.d.ts.map +1 -1
- package/dist/types/lib/loaders/CopcStreamingLoader.d.ts +20 -0
- package/dist/types/lib/loaders/CopcStreamingLoader.d.ts.map +1 -1
- package/dist/types/lib/loaders/EptStreamingLoader.d.ts +6 -0
- package/dist/types/lib/loaders/EptStreamingLoader.d.ts.map +1 -1
- package/dist/types/lib/tools/CrossSectionTool.d.ts +106 -0
- package/dist/types/lib/tools/CrossSectionTool.d.ts.map +1 -0
- package/dist/types/lib/tools/ElevationProfileExtractor.d.ts +70 -0
- package/dist/types/lib/tools/ElevationProfileExtractor.d.ts.map +1 -0
- package/dist/types/lib/tools/index.d.ts +3 -0
- package/dist/types/lib/tools/index.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/LidarLayerAdapter-eh59KEMT.cjs.map +0 -1
- 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:
|
|
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">×</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
|
-
|
|
40983
|
-
|
|
40984
|
-
|
|
40985
|
-
|
|
40986
|
-
|
|
40987
|
-
|
|
40988
|
-
|
|
40989
|
-
|
|
40990
|
-
|
|
40991
|
-
|
|
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-
|
|
43092
|
+
//# sourceMappingURL=LidarLayerAdapter-CgPF0Hvt.js.map
|