potree
Version:
WebGL point cloud viewer - WORK IN PROGRESS
1,708 lines (1,359 loc) • 74.4 kB
JavaScript
initSidebar = (viewer) => {
let createToolIcon = function (icon, title, callback) {
let element = $(`
<img src="${icon}"
style="width: 32px; height: 32px"
class="button-icon"
data-i18n="${title}" />
`);
element.click(callback);
return element;
};
function initToolbar () {
// ANGLE
let elToolbar = $('#tools');
elToolbar.append(createToolIcon(
Potree.resourcePath + '/icons/angle.png',
'[title]tt.angle_measurement',
function () {
$('#menu_measurements').next().slideDown();
viewer.measuringTool.startInsertion({
showDistances: false,
showAngles: true,
showArea: false,
closed: true,
maxMarkers: 3,
name: 'Angle'});
}
));
// POINT
elToolbar.append(createToolIcon(
Potree.resourcePath + '/icons/point.svg',
'[title]tt.point_measurement',
function () {
$('#menu_measurements').next().slideDown();
viewer.measuringTool.startInsertion({
showDistances: false,
showAngles: false,
showCoordinates: true,
showArea: false,
closed: true,
maxMarkers: 1,
name: 'Point'});
}
));
// DISTANCE
elToolbar.append(createToolIcon(
Potree.resourcePath + '/icons/distance.svg',
'[title]tt.distance_measurement',
function () {
$('#menu_measurements').next().slideDown();
viewer.measuringTool.startInsertion({
showDistances: true,
showArea: false,
closed: false,
name: 'Distance'});
}
));
// HEIGHT
elToolbar.append(createToolIcon(
Potree.resourcePath + '/icons/height.svg',
'[title]tt.height_measurement',
function () {
$('#menu_measurements').next().slideDown();
viewer.measuringTool.startInsertion({
showDistances: false,
showHeight: true,
showArea: false,
closed: false,
maxMarkers: 2,
name: 'Height'});
}
));
// AREA
elToolbar.append(createToolIcon(
Potree.resourcePath + '/icons/area.svg',
'[title]tt.area_measurement',
function () {
$('#menu_measurements').next().slideDown();
viewer.measuringTool.startInsertion({
showDistances: true,
showArea: true,
closed: true,
name: 'Area'});
}
));
// VOLUME
elToolbar.append(createToolIcon(
Potree.resourcePath + '/icons/volume.svg',
'[title]tt.volume_measurement',
function () { viewer.volumeTool.startInsertion(); }
));
// PROFILE
elToolbar.append(createToolIcon(
Potree.resourcePath + '/icons/profile.svg',
'[title]tt.height_profile',
function () {
$('#menu_measurements').next().slideDown(); ;
viewer.profileTool.startInsertion();
}
));
// CLIP VOLUME
elToolbar.append(createToolIcon(
Potree.resourcePath + '/icons/clip_volume.svg',
'[title]tt.clip_volume',
function () { viewer.volumeTool.startInsertion({clip: true}); }
));
// REMOVE ALL
elToolbar.append(createToolIcon(
Potree.resourcePath + '/icons/reset_tools.svg',
'[title]tt.remove_all_measurement',
function () {
viewer.scene.removeAllMeasurements();
}
));
}
function initClassificationList () {
let elClassificationList = $('#classificationList');
let addClassificationItem = function (code, name) {
let inputID = 'chkClassification_' + code;
let element = $(`
<li>
<label style="whitespace: nowrap">
<input id="${inputID}" type="checkbox" checked/>
<span>${name}</span>
</label>
</li>
`);
let elInput = element.find('input');
elInput.click(event => {
viewer.setClassificationVisibility(code, event.target.checked);
});
elClassificationList.append(element);
};
addClassificationItem(0, 'never classified');
addClassificationItem(1, 'unclassified');
addClassificationItem(2, 'ground');
addClassificationItem(3, 'low vegetation');
addClassificationItem(4, 'medium vegetation');
addClassificationItem(5, 'high vegetation');
addClassificationItem(6, 'building');
addClassificationItem(7, 'low point(noise)');
addClassificationItem(8, 'key-point');
addClassificationItem(9, 'water');
addClassificationItem(12, 'overlap');
}
function initAccordion () {
$('.accordion > h3').each(function () {
let header = $(this);
let content = $(this).next();
header.addClass('accordion-header ui-widget');
content.addClass('accordion-content ui-widget');
content.hide();
header.click(function () {
content.slideToggle();
});
});
// to close all, call
// $(".accordion > div").hide()
// to open the, for example, tool menu, call:
// $("#menu_tools").next().show()
}
function initAppearance () {
// $( "#optQuality" ).selectmenu();
// $("#optQuality").val(viewer.getQuality()).selectmenu("refresh")
// $("#optQuality").selectmenu({
// change: function(event, ui){
// viewer.setQuality(ui.item.value);
// }
// });
$('#sldPointBudget').slider({
value: viewer.getPointBudget(),
min: 100 * 1000,
max: 5 * 1000 * 1000,
step: 1000,
slide: function (event, ui) { viewer.setPointBudget(ui.value); }
});
$('#sldFOV').slider({
value: viewer.getFOV(),
min: 20,
max: 100,
step: 1,
slide: function (event, ui) { viewer.setFOV(ui.value); }
});
$('#sldEDLRadius').slider({
value: viewer.getEDLRadius(),
min: 1,
max: 4,
step: 0.01,
slide: function (event, ui) { viewer.setEDLRadius(ui.value); }
});
$('#sldEDLStrength').slider({
value: viewer.getEDLStrength(),
min: 0,
max: 5,
step: 0.01,
slide: function (event, ui) { viewer.setEDLStrength(ui.value); }
});
viewer.addEventListener('point_budget_changed', function (event) {
$('#lblPointBudget')[0].innerHTML = Potree.utils.addCommas(viewer.getPointBudget());
$('#sldPointBudget').slider({value: viewer.getPointBudget()});
});
viewer.addEventListener('fov_changed', function (event) {
$('#lblFOV')[0].innerHTML = parseInt(viewer.getFOV());
$('#sldFOV').slider({value: viewer.getFOV()});
});
// viewer.addEventListener("quality_changed", e => {
//
// let name = viewer.quality;
//
// $( "#optQuality" )
// .selectmenu()
// .val(name)
// .selectmenu("refresh");
// });
viewer.addEventListener('edl_radius_changed', function (event) {
$('#lblEDLRadius')[0].innerHTML = viewer.getEDLRadius().toFixed(1);
$('#sldEDLRadius').slider({value: viewer.getEDLRadius()});
});
viewer.addEventListener('edl_strength_changed', function (event) {
$('#lblEDLStrength')[0].innerHTML = viewer.getEDLStrength().toFixed(1);
$('#sldEDLStrength').slider({value: viewer.getEDLStrength()});
});
viewer.addEventListener('background_changed', function (event) {
$("input[name=background][value='" + viewer.getBackground() + "']").prop('checked', true);
});
$('#lblPointBudget')[0].innerHTML = Potree.utils.addCommas(viewer.getPointBudget());
$('#lblFOV')[0].innerHTML = parseInt(viewer.getFOV());
$('#lblEDLRadius')[0].innerHTML = viewer.getEDLRadius().toFixed(1);
$('#lblEDLStrength')[0].innerHTML = viewer.getEDLStrength().toFixed(1);
$('#chkEDLEnabled')[0].checked = viewer.getEDLEnabled();
$("input[name=background][value='" + viewer.getBackground() + "']").prop('checked', true);
}
function initNavigation () {
let elNavigation = $('#navigation');
let sldMoveSpeed = $('#sldMoveSpeed');
let lblMoveSpeed = $('#lblMoveSpeed');
elNavigation.append(createToolIcon(
Potree.resourcePath + '/icons/earth_controls_1.png',
'[title]tt.earth_control',
function () { viewer.setNavigationMode(Potree.EarthControls); }
));
elNavigation.append(createToolIcon(
Potree.resourcePath + '/icons/fps_controls.png',
'[title]tt.flight_control',
function () { viewer.setNavigationMode(Potree.FirstPersonControls); }
));
elNavigation.append(createToolIcon(
Potree.resourcePath + '/icons/orbit_controls.svg',
'[title]tt.orbit_control',
function () { viewer.setNavigationMode(Potree.OrbitControls); }
));
elNavigation.append(createToolIcon(
Potree.resourcePath + '/icons/focus.svg',
'[title]tt.focus_control',
function () { viewer.fitToScreen(); }
));
elNavigation.append(createToolIcon(
Potree.resourcePath + '/icons/topview.svg',
'[title]tt.top_view_control',
function () { viewer.setTopView(); }
));
elNavigation.append(createToolIcon(
Potree.resourcePath + '/icons/frontview.svg',
'[title]tt.front_view_control',
function () { viewer.setFrontView(); }
));
elNavigation.append(createToolIcon(
Potree.resourcePath + '/icons/leftview.svg',
'[title]tt.left_view_control',
function () { viewer.setLeftView(); }
));
let speedRange = new THREE.Vector2(1, 10 * 1000);
let toLinearSpeed = function (value) {
return Math.pow(value, 4) * speedRange.y + speedRange.x;
};
let toExpSpeed = function (value) {
return Math.pow((value - speedRange.x) / speedRange.y, 1 / 4);
};
sldMoveSpeed.slider({
value: toExpSpeed(viewer.getMoveSpeed()),
min: 0,
max: 1,
step: 0.01,
slide: function (event, ui) { viewer.setMoveSpeed(toLinearSpeed(ui.value)); }
});
viewer.addEventListener('move_speed_changed', function (event) {
lblMoveSpeed.html(viewer.getMoveSpeed().toFixed(1));
sldMoveSpeed.slider({value: toExpSpeed(viewer.getMoveSpeed())});
});
lblMoveSpeed.html(viewer.getMoveSpeed().toFixed(1));
}
function initAnnotationDetails () {
// annotation_details
let annotationPanel = $('#annotation_details');
let registeredEvents = [];
let rebuild = () => {
annotationPanel.empty();
for (let registeredEvent of registeredEvents) {
let {type, dispatcher, callback} = registeredEvent;
dispatcher.removeEventListener(type, callback);
}
registeredEvents = [];
let checked = viewer.getShowAnnotations() ? 'checked' : '';
let chkEnable = $(`
<li>
<label>
<input type="checkbox" id="chkShowAnnotations" ${checked}
onClick="viewer.setShowAnnotations(this.checked)"/>
<span data-i18n="annotations.show3D"></span>
</label>
<label>
<input type="checkbox" id="chkShowAnnotationsMap" ${checked}
onClick="viewer.mapView.getAnnotationsLayer().setVisible(this.checked)"/>
<span data-i18n="annotations.showMap"></span>
</label>
</li>
`);
annotationPanel.append(chkEnable);
// let stack = viewer.scene.annotations.children.reverse().map(
// a => ({annotation: a, container: annotationPanel}));
let stack = viewer.scene.annotations.children.map(
a => ({annotation: a, container: annotationPanel}));
while (stack.length > 0) {
let {annotation, container} = stack.shift();
// ► U+25BA \u25BA
// ▼ U+25BC \u25BC
let element = $(`
<div class="annotation-item" style="margin: 8px 20px">
<span class="annotation-main">
<span class="annotation-expand">\u25BA</span>
<span class="annotation-label">
${annotation.title}
</span>
</span>
</div>
`);
let elMain = element.find('.annotation-main');
let elExpand = element.find('.annotation-expand');
elExpand.css('display', annotation.children.length > 0 ? 'block' : 'none');
let actions = [];
{ // ACTIONS, INCLUDING GOTO LOCATION
if (annotation.hasView()) {
let action = new Potree.Action({
'icon': Potree.resourcePath + '/icons/target.svg',
'onclick': (e) => { annotation.moveHere(viewer.scene.camera); }
});
actions.push(action);
}
}
for (let action of annotation.actions) {
actions.push(action);
}
actions = actions.filter(
a => a.showIn === undefined || a.showIn.includes('sidebar'));
// FIRST ACTION
if (annotation.children.length === 0 && actions.length > 0) {
let action = actions[0];
let elIcon = $(`<img src="${action.icon}" class="annotation-icon">`);
if (action.tooltip) {
elIcon.attr('title', action.tooltip);
}
elMain.append(elIcon);
elMain.click(e => action.onclick({annotation: annotation}));
elMain.mouseover(e => elIcon.css('opacity', 1));
elMain.mouseout(e => elIcon.css('opacity', 0.5));
{
let iconChanged = e => {
elIcon.attr('src', e.icon);
};
action.addEventListener('icon_changed', iconChanged);
registeredEvents.push({
type: 'icon_changed',
dispatcher: action,
callback: iconChanged
});
}
actions.splice(0, 1);
}
// REMAINING ACTIONS
for (let action of actions) {
let elIcon = $(`<img src="${action.icon}" class="annotation-icon">`);
if (action.tooltip) {
elIcon.attr('title', action.tooltip);
}
elIcon.click(e => {
action.onclick({annotation: annotation});
return false;
});
elIcon.mouseover(e => elIcon.css('opacity', 1));
elIcon.mouseout(e => elIcon.css('opacity', 0.5));
{
let iconChanged = e => {
elIcon.attr('src', e.icon);
};
action.addEventListener('icon_changed', iconChanged);
registeredEvents.push({
type: 'icon_changed',
dispatcher: action,
callback: iconChanged
});
}
element.append(elIcon);
}
element.mouseover(e => annotation.setHighlighted(true));
element.mouseout(e => annotation.setHighlighted(false));
annotation.setHighlighted(false);
container.append(element);
if (annotation.children.length > 0) {
element.click(e => {
if (element.next().is(':visible')) {
elExpand.html('\u25BA');
} else {
elExpand.html('\u25BC');
}
element.next().toggle(100);
});
// let left = ((annotation.level()) * 20) + "px";
let left = '20px';
let childContainer = $(`<div style="margin: 0px; padding: 0px 0px 0px ${left}; display: none"></div>`);
for (let child of annotation.children) {
container.append(childContainer);
stack.push({annotation: child, container: childContainer});
}
}
};
annotationPanel.i18n();
};
let annotationsChanged = e => {
rebuild();
};
viewer.addEventListener('scene_changed', e => {
e.oldScene.annotations.removeEventListener('annotation_added', annotationsChanged);
e.scene.annotations.addEventListener('annotation_added', annotationsChanged);
rebuild();
});
viewer.scene.annotations.addEventListener('annotation_added', annotationsChanged);
rebuild();
}
function initMeasurementDetails () {
let trackedItems = new Map();
let removeIconPath = Potree.resourcePath + '/icons/remove.svg';
let mlist = $('#measurement_list');
let createCoordinatesTable = (measurement) => {
let table = $(`
<table class="measurement_value_table">
<tr>
<th>x</th>
<th>y</th>
<th>z</th>
</tr>
</table>
`);
for (let point of measurement.points) {
let position = point instanceof THREE.Vector3 ? point : point.position;
let x = Potree.utils.addCommas(position.x.toFixed(3));
let y = Potree.utils.addCommas(position.y.toFixed(3));
let z = Potree.utils.addCommas(position.z.toFixed(3));
let row = $(`
<tr>
<td><span>${x}</span></td>
<td><span>${y}</span></td>
<td><span>${z}</span></td>
</tr>
`);
table.append(row);
}
return table;
};
let createAttributesTable = (measurement) => {
let elTable = $('<table class="measurement_value_table"></table>');
let point = measurement.points[0];
if (point.color) {
let color = point.color;
let text = color.join(', ');
elTable.append($(`
<tr>
<td>rgb</td>
<td>${text}</td>
</tr>
`));
}
return elTable;
};
class MeasurePanel {
constructor (scene, measurement) {
this.scene = scene;
this.measurement = measurement;
this.icon = null;
this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;
this.id = this.constructor.counter;
let title = measurement.name;
this.elPanel = $(`
<span class="measurement_item">
<!-- HEADER -->
<div class="measurement_header" onclick="$(this).next().slideToggle(200)">
<span class="measurement_icon"><img src="" class="measurement_item_icon" /></span>
<span class="measurement_header_title">${title}</span>
</div>
<!-- DETAIL -->
<div class="measurement_content selectable" style="display: none">
</div>
</span>
`);
this.elContentContainer = this.elPanel.find('.measurement_content');
this.elIcon = this.elPanel.find('.measurement_item_icon');
this._update = () => { this.update(); };
}
destroy () {
}
update () {
}
};
class DistancePanel extends MeasurePanel {
constructor (scene, measurement) {
super(scene, measurement);
this.typename = 'Distance';
this.icon = Potree.resourcePath + '/icons/distance.svg';
this.elIcon.attr('src', this.icon);
this.elContent = $(`
<div>
<span class="coordinates_table_container"></span>
<br>
<table id="distances_table_${this.id}" class="measurement_value_table">
</table>
<!-- ACTIONS -->
<div style="display: flex; margin-top: 12px">
<span></span>
<span style="flex-grow: 1"></span>
<img class="measurement_action_remove" src="${removeIconPath}" style="width: 16px; height: 16px"/>
</div>
</div>
`);
this.elContentContainer.append(this.elContent);
let elRemove = this.elContent.find('.measurement_action_remove');
elRemove.click(() => { this.scene.removeMeasurement(measurement); });
this.measurement.addEventListener('marker_added', this._update);
this.measurement.addEventListener('marker_removed', this._update);
this.measurement.addEventListener('marker_moved', this._update);
this.update();
}
update () {
let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
elCoordiantesContainer.empty();
elCoordiantesContainer.append(createCoordinatesTable(this.measurement));
let positions = this.measurement.points.map(p => p.position);
let distances = [];
for (let i = 0; i < positions.length - 1; i++) {
let d = positions[i].distanceTo(positions[i + 1]);
distances.push(d.toFixed(3));
}
let totalDistance = this.measurement.getTotalDistance().toFixed(3);
let elDistanceTable = this.elContent.find(`#distances_table_${this.id}`);
elDistanceTable.empty();
for (let i = 0; i < distances.length; i++) {
let label = (i === 0) ? 'Distances: ' : '';
let distance = distances[i];
let elDistance = $(`
<tr>
<th>${label}</th>
<td style="width: 100%; padding-left: 10px">${distance}</td>
</tr>`);
elDistanceTable.append(elDistance);
}
let elTotal = $(`
<tr>
<th>Total: </td><td style="width: 100%; padding-left: 10px">${totalDistance}</th>
</tr>`);
elDistanceTable.append(elTotal);
// let elDistance = this.elContent.find(`#distance_${this.id}`);
// elDistance.html(totalDistance);
}
destroy () {
this.elPanel.remove();
this.measurement.removeEventListener('marker_added', this._update);
this.measurement.removeEventListener('marker_removed', this._update);
this.measurement.removeEventListener('marker_moved', this._update);
}
};
class PointPanel extends MeasurePanel {
constructor (scene, measurement) {
super(scene, measurement);
this.typename = 'Point';
this.icon = Potree.resourcePath + '/icons/point.svg';
this.elIcon.attr('src', this.icon);
this.elContent = $(`
<div>
<span class="coordinates_table_container"></span>
<br>
<span class="attributes_table_container"></span>
<!-- ACTIONS -->
<div style="display: flex; margin-top: 12px">
<span></span>
<span style="flex-grow: 1"></span>
<img class="measurement_action_remove" src="${removeIconPath}" style="width: 16px; height: 16px"/>
</div>
</div>
`);
this.elContentContainer.append(this.elContent);
let elRemove = this.elContent.find('.measurement_action_remove');
elRemove.click(() => { this.scene.removeMeasurement(measurement); });
this.measurement.addEventListener('marker_added', this._update);
this.measurement.addEventListener('marker_removed', this._update);
this.measurement.addEventListener('marker_moved', this._update);
this.update();
}
update () {
let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
elCoordiantesContainer.empty();
elCoordiantesContainer.append(createCoordinatesTable(this.measurement));
let elAttributesContainer = this.elContent.find('.attributes_table_container');
elAttributesContainer.empty();
elAttributesContainer.append(createAttributesTable(this.measurement));
}
destroy () {
this.elPanel.remove();
this.measurement.removeEventListener('marker_added', this._update);
this.measurement.removeEventListener('marker_removed', this._update);
this.measurement.removeEventListener('marker_moved', this._update);
}
};
class AreaPanel extends MeasurePanel {
constructor (scene, measurement) {
super(scene, measurement);
this.typename = 'Area';
this.icon = Potree.resourcePath + '/icons/area.svg';
this.elIcon.attr('src', this.icon);
this.elContent = $(`
<div>
<span class="coordinates_table_container"></span>
<br>
<span style="font-weight: bold">Area: </span>
<span id="measurement_area_${this.id}"></span>
<!-- ACTIONS -->
<div style="display: flex; margin-top: 12px">
<span></span>
<span style="flex-grow: 1"></span>
<img class="measurement_action_remove" src="${removeIconPath}" style="width: 16px; height: 16px"/>
</div>
</div>
`);
this.elContentContainer.append(this.elContent);
let elRemove = this.elContent.find('.measurement_action_remove');
elRemove.click(() => { this.scene.removeMeasurement(measurement); });
this.measurement.addEventListener('marker_added', this._update);
this.measurement.addEventListener('marker_removed', this._update);
this.measurement.addEventListener('marker_moved', this._update);
this.update();
}
update () {
let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
elCoordiantesContainer.empty();
elCoordiantesContainer.append(createCoordinatesTable(this.measurement));
let elArea = this.elContent.find(`#measurement_area_${this.id}`);
elArea.html(this.measurement.getArea().toFixed(3));
}
destroy () {
this.elPanel.remove();
this.measurement.removeEventListener('marker_added', this._update);
this.measurement.removeEventListener('marker_removed', this._update);
this.measurement.removeEventListener('marker_moved', this._update);
}
};
class AnglePanel extends MeasurePanel {
constructor (scene, measurement) {
super(scene, measurement);
this.typename = 'Angle';
this.icon = Potree.resourcePath + '/icons/angle.png';
this.elIcon.attr('src', this.icon);
this.elContent = $(`
<div>
<span class="coordinates_table_container"></span>
<br>
<table class="measurement_value_table">
<tr>
<th>\u03b1</th>
<th>\u03b2</th>
<th>\u03b3</th>
</tr>
<tr>
<td align="center" id="angle_cell_alpha_${this.id}" style="width: 33%"></td>
<td align="center" id="angle_cell_betta_${this.id}" style="width: 33%"></td>
<td align="center" id="angle_cell_gamma_${this.id}" style="width: 33%"></td>
</tr>
</table>
<!-- ACTIONS -->
<div style="display: flex; margin-top: 12px">
<span></span>
<span style="flex-grow: 1"></span>
<img class="measurement_action_remove" src="${removeIconPath}" style="width: 16px; height: 16px"/>
</div>
</div>
`);
this.elContentContainer.append(this.elContent);
let elRemove = this.elContent.find('.measurement_action_remove');
elRemove.click(() => { this.scene.removeMeasurement(measurement); });
this.measurement.addEventListener('marker_added', this._update);
this.measurement.addEventListener('marker_removed', this._update);
this.measurement.addEventListener('marker_moved', this._update);
this.update();
}
update () {
let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
elCoordiantesContainer.empty();
elCoordiantesContainer.append(createCoordinatesTable(this.measurement));
let angles = [];
for (let i = 0; i < this.measurement.points.length; i++) {
angles.push(this.measurement.getAngle(i) * (180.0 / Math.PI));
}
angles = angles.map(a => a.toFixed(1) + '\u00B0');
let elAlpha = this.elContent.find(`#angle_cell_alpha_${this.id}`);
let elBetta = this.elContent.find(`#angle_cell_betta_${this.id}`);
let elGamma = this.elContent.find(`#angle_cell_gamma_${this.id}`);
elAlpha.html(angles[0]);
elBetta.html(angles[1]);
elGamma.html(angles[2]);
}
destroy () {
this.elPanel.remove();
this.measurement.removeEventListener('marker_added', this._update);
this.measurement.removeEventListener('marker_removed', this._update);
this.measurement.removeEventListener('marker_moved', this._update);
}
};
class HeightPanel extends MeasurePanel {
constructor (scene, measurement) {
super(scene, measurement);
this.typename = 'Height';
this.icon = Potree.resourcePath + '/icons/height.svg';
this.elIcon.attr('src', this.icon);
this.elContent = $(`
<div>
<span class="coordinates_table_container"></span>
<br>
<span id="height_label_${this.id}">Height: </span><br>
<!-- ACTIONS -->
<div style="display: flex; margin-top: 12px">
<span></span>
<span style="flex-grow: 1"></span>
<img class="measurement_action_remove" src="${removeIconPath}" style="width: 16px; height: 16px"/>
</div>
</div>
`);
this.elContentContainer.append(this.elContent);
let elRemove = this.elContent.find('.measurement_action_remove');
elRemove.click(() => { this.scene.removeMeasurement(measurement); });
this.measurement.addEventListener('marker_added', this._update);
this.measurement.addEventListener('marker_removed', this._update);
this.measurement.addEventListener('marker_moved', this._update);
this.update();
}
update () {
let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
elCoordiantesContainer.empty();
elCoordiantesContainer.append(createCoordinatesTable(this.measurement));
{
let points = this.measurement.points;
let sorted = points.slice().sort((a, b) => a.position.z - b.position.z);
let lowPoint = sorted[0].position.clone();
let highPoint = sorted[sorted.length - 1].position.clone();
let min = lowPoint.z;
let max = highPoint.z;
let height = max - min;
height = height.toFixed(3);
this.elHeightLabel = this.elContent.find(`#height_label_${this.id}`);
this.elHeightLabel.html(`<b>Height:</b> ${height}`);
}
}
destroy () {
this.elPanel.remove();
this.measurement.removeEventListener('marker_added', this._update);
this.measurement.removeEventListener('marker_removed', this._update);
this.measurement.removeEventListener('marker_moved', this._update);
}
};
class ProfilePanel extends MeasurePanel {
constructor (scene, measurement) {
super(scene, measurement);
this.typename = 'Profile';
this.icon = Potree.resourcePath + '/icons/profile.svg';
this.elIcon.attr('src', this.icon);
let sliderID = 'sldProfileWidth_' + this.id;
this.elContent = $(`
<div>
<span class="coordinates_table_container"></span>
<br>
<span style="display:flex">
<span style="display:flex; align-items: center; padding-right: 10px">Width: </span>
<input id="${sliderID}" name="${sliderID}" value="5.06" style="flex-grow: 1; width:100%">
</span>
<br>
<input type="button" value="Prepare Download" id="download_profile_${this.id}"/>
<span id="download_profile_status_${this.id}"></span>
<br>
<input type="button" id="show_2d_profile_${this.id}" value="show 2d profile" style="width: 100%"/>
<!-- ACTIONS -->
<div style="display: flex; margin-top: 12px">
<span></span>
<span style="flex-grow: 1"></span>
<img class="measurement_action_remove" src="${removeIconPath}" style="width: 16px; height: 16px"/>
</div>
</div>
`);
this.elContentContainer.append(this.elContent);
this.elShow2DProfile = this.elContent.find(`#show_2d_profile_${this.id}`);
this.elShow2DProfile.click(() => {
viewer.profileWindow.show();
viewer.profileWindowController.setProfile(measurement);
// viewer.profileWindow.draw(measurement);
});
{ // download
this.elDownloadButton = this.elContent.find(`#download_profile_${this.id}`);
if (viewer.server) {
this.elDownloadButton.click(() => this.download());
} else {
this.elDownloadButton.hide();
}
}
{ // width spinner
let elWidthSlider = this.elContent.find(`#${sliderID}`);
elWidthSlider.spinner({
min: 0,
max: 10 * 1000 * 1000,
step: 0.01,
numberFormat: 'n',
start: () => {},
spin: (event, ui) => {
let value = elWidthSlider.spinner('value');
measurement.setWidth(value);
},
change: (event, ui) => {
let value = elWidthSlider.spinner('value');
measurement.setWidth(value);
},
stop: (event, ui) => {
let value = elWidthSlider.spinner('value');
measurement.setWidth(value);
},
incremental: (count) => {
let value = elWidthSlider.spinner('value');
let step = elWidthSlider.spinner('option', 'step');
let delta = value * 0.05;
let increments = Math.max(1, parseInt(delta / step));
return increments;
}
});
elWidthSlider.spinner('value', measurement.getWidth());
elWidthSlider.spinner('widget').css('width', '100%');
this.widthListener = (event) => {
let value = elWidthSlider.spinner('value');
if (value !== measurement.getWidth()) {
elWidthSlider.spinner('value', measurement.getWidth());
}
};
measurement.addEventListener('width_changed', this.widthListener);
}
let elRemove = this.elContent.find('.measurement_action_remove');
elRemove.click(() => { this.scene.removeProfile(measurement); });
this.measurement.addEventListener('marker_added', this._update);
this.measurement.addEventListener('marker_removed', this._update);
this.measurement.addEventListener('marker_moved', this._update);
this.update();
}
update () {
let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
elCoordiantesContainer.empty();
let coordinatesTable = createCoordinatesTable(this.measurement);
let cells = coordinatesTable.find('span');
cells.attr('contenteditable', 'true');
cells = cells.toArray();
for (let i = 0; i < cells.length; i++) {
let cell = cells[i];
let measure = this.measurement;
let updateCallback = this._update;
let assignValue = () => {
let text = Potree.utils.removeCommas($(cell).html());
let num = Number(text);
if (!isNaN(num)) {
$(cell).removeClass('invalid_value');
measure.removeEventListener('marker_moved', updateCallback);
let index = parseInt(i / 3);
let coordinateComponent = i % 3;
let position = measure.points[index].clone();
if (coordinateComponent === 0) {
position.x = num;
} else if (coordinateComponent === 1) {
position.y = num;
} else if (coordinateComponent === 2) {
position.z = num;
}
measure.setPosition(index, position);
measure.addEventListener('marker_moved', updateCallback);
} else {
$(cell).addClass('invalid_value');
}
};
$(cell).on('keypress', (e) => {
if (e.which === 13) {
assignValue();
return false;
}
});
$(cell).focusout(() => assignValue());
$(cell).on('input', function (e) {
let text = Potree.utils.removeCommas($(this).html());
let num = Number(text);
if (!isNaN(num)) {
$(this).removeClass('invalid_value');
} else {
$(this).addClass('invalid_value');
}
});
}
elCoordiantesContainer.append(coordinatesTable);
}
download () {
let profile = this.measurement;
let boxes = profile.getSegmentMatrices()
.map(m => m.elements.join(','))
.join(',');
let minLOD = 0;
let maxLOD = 100;
let pcs = [];
for (let pointcloud of this.scene.pointclouds) {
let urlIsAbsolute = new RegExp('^(?:[a-z]+:)?//', 'i').test(pointcloud.pcoGeometry.url);
let pc = '';
if (urlIsAbsolute) {
pc = pointcloud.pcoGeometry.url;
} else {
pc = `${window.location.href}/../${pointcloud.pcoGeometry.url}`;
}
pcs.push(pc);
}
let pc = pcs
.map(v => `pointcloud[]=${v}`)
.join('&');
let request = `${viewer.server}/start_extract_region_worker?minLOD=${minLOD}&maxLOD=${maxLOD}&box=${boxes}&${pc}`;
// console.log(request);
let elMessage = this.elContent.find(`#download_profile_status_${this.id}`);
elMessage.html('sending request...');
let workerID = null;
let start = new Date().getTime();
let observe = () => {
let request = `${viewer.server}/observe_status?workerID=${workerID}`;
let loaded = 0;
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.addEventListener('progress', e => {
let nowLoaded = e.loaded;
let response = xhr.responseText.substring(loaded, nowLoaded);
response = JSON.parse(response);
if (response.status === 'FINISHED') {
elMessage.html(`<br><a href="${viewer.server}/get_las?workerID=${workerID}">Download ready</a>`);
} else {
let current = new Date().getTime();
let duration = (current - start);
let seconds = parseInt(duration / 1000);
elMessage.html(`processing request... ${seconds}s`);
}
loaded = nowLoaded;
});
xhr.open('GET', request, true);
xhr.send(null);
};
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
// alert(xhr.responseText);
let res = JSON.parse(xhr.responseText);
console.log(res);
if (res.status === 'OK') {
workerID = res.workerID;
elMessage.html('request is being processed');
// checkUntilFinished();
observe();
} else if (res.status === 'ERROR_POINT_PROCESSED_ESTIMATE_TOO_LARGE') {
elMessage.html('Too many candidate points in selection.');
} else {
elMessage.html(`${res.status}`);
}
}
};
xhr.open('GET', request, true);
xhr.send(null);
}
destroy () {
this.elPanel.remove();
this.measurement.removeEventListener('marker_added', this._update);
this.measurement.removeEventListener('marker_removed', this._update);
this.measurement.removeEventListener('marker_moved', this._update);
this.measurement.removeEventListener('width_changed', this.widthListener);
}
};
class VolumePanel extends MeasurePanel {
constructor (scene, measurement) {
super(scene, measurement);
this.typename = 'Volume';
this.icon = Potree.resourcePath + '/icons/volume.svg';
this.elIcon.attr('src', this.icon);
this.values = {};
this.elContent = $(`
<div>
<div style="width: 100%;">
<div style="display:inline-flex; width: 100%; ">
<span class="input-grid-label">x</span>
<span class="input-grid-label">y</span>
<span class="input-grid-label">z</span>
</div>
<div style="display:inline-flex; width: 100%;">
<span class="input-grid-cell"><input type="text" id="volume_input_x_${measurement.id}"/></span>
<span class="input-grid-cell"><input type="text" id="volume_input_y_${measurement.id}"/></span>
<span class="input-grid-cell"><input type="text" id="volume_input_z_${measurement.id}"/></span>
</div>
</div>
<div style="width: 100%;">
<div style="display:inline-flex; width: 100%; ">
<span class="input-grid-label">length</span>
<span class="input-grid-label">width</span>
<span class="input-grid-label">height</span>
</div>
<div style="display:inline-flex; width: 100%;">
<span class="input-grid-cell"><input type="text" id="volume_input_length_${measurement.id}"/></span>
<span class="input-grid-cell"><input type="text" id="volume_input_width_${measurement.id}"/></span>
<span class="input-grid-cell"><input type="text" id="volume_input_height_${measurement.id}"/></span>
</div>
</div>
<div style="width: 100%;">
<div style="display:inline-flex; width: 100%; ">
<span class="input-grid-label">α</span>
<span class="input-grid-label">β</span>
<span class="input-grid-label">γ</span>
</div>
<div style="display:inline-flex; width: 100%;">
<span class="input-grid-cell"><input type="text" id="volume_input_alpha_${measurement.id}"/></span>
<span class="input-grid-cell"><input type="text" id="volume_input_beta_${measurement.id}"/></span>
<span class="input-grid-cell"><input type="text" id="volume_input_gamma_${measurement.id}"/></span>
</div>
</div>
<label><input type="checkbox" id="chkClip_${this.measurement.id}"/><span data-i18n="measurements.clip"></span></label>
<label><input type="checkbox" id="chkVisible_${this.measurement.id}"/><span data-i18n="measurements.show"></span></label>
<input type="button" value="Prepare Download" id="download_volume_${this.id}"/>
<span id="download_volume_status_${this.id}"></span>
<!-- ACTIONS -->
<div style="display: flex; margin-top: 12px">
<span></span>
<span style="flex-grow: 1"></span>
<img class="measurement_action_remove" src="${removeIconPath}" style="width: 16px; height: 16px"/>
</div>
</div>
`);
this.elContentContainer.append(this.elContent);
this.elClip = this.elContent.find(`#chkClip_${this.measurement.id}`);
this.elVisible = this.elContent.find(`#chkVisible_${this.measurement.id}`);
this.elClip.click(() => {
this.measurement.clip = this.elClip.is(':checked');
});
this.elVisible.click(() => {
this.measurement.visible = this.elVisible.is(':checked');
});
this.elClip.prop('checked', this.measurement.clip);
this.elVisible.prop('checked', this.measurement.visible);
this.elX = this.elContent.find(`#volume_input_x_${this.measurement.id}`);
this.elY = this.elContent.find(`#volume_input_y_${this.measurement.id}`);
this.elZ = this.elContent.find(`#volume_input_z_${this.measurement.id}`);
this.elLength = this.elContent.find(`#volume_input_length_${this.measurement.id}`);
this.elWidth = this.elContent.find(`#volume_input_width_${this.measurement.id}`);
this.elHeight = this.elContent.find(`#volume_input_height_${this.measurement.id}`);
this.elAlpha = this.elContent.find(`#volume_input_alpha_${this.measurement.id}`);
this.elBeta = this.elContent.find(`#volume_input_beta_${this.measurement.id}`);
this.elGamma = this.elContent.find(`#volume_input_gamma_${this.measurement.id}`);
this.elX.on('change', (e) => {
let val = this.elX.val();
if ($.isNumeric(val)) {
val = parseFloat(val);
this.measurement.position.x = val;
}
});
this.elY.on('change', (e) => {
let val = this.elY.val();
if ($.isNumeric(val)) {
val = parseFloat(val);
this.measurement.position.y = val;
}
});
this.elZ.on('change', (e) => {
let val = this.elZ.val();
if ($.isNumeric(val)) {
val = parseFloat(val);
this.measurement.position.z = val;
}
});
this.elLength.on('change', (e) => {
let val = this.elLength.val();
if ($.isNumeric(val)) {
val = parseFloat(val);
this.measurement.scale.x = val;
}
});
this.elWidth.on('change', (e) => {
let val = this.elWidth.val();
if ($.isNumeric(val)) {
val = parseFloat(val);
this.measurement.scale.y = val;
}
});
this.elHeight.on('change', (e) => {
let val = this.elHeight.val();
if ($.isNumeric(val)) {
val = parseFloat(val);
this.measurement.scale.z = val;
}
});
let toRadians = (d) => Math.PI * d / 180;
this.elAlpha.on('change', (e) => {
let val = this.elAlpha.val();
if ($.isNumeric(val)) {
val = parseFloat(val);
this.measurement.rotation.x = toRadians(val);
}
});
this.elBeta.on('change', (e) => {
let val = this.elBeta.val();
if ($.isNumeric(val)) {
val = parseFloat(val);
this.measurement.rotation.y = toRadians(val);
}
});
this.elGamma.on('change', (e) => {
let val = this.elGamma.val();
if ($.isNumeric(val)) {
val = parseFloat(val);
this.measurement.rotation.z = toRadians(val);
}
});
this.elDownloadButton = this.elContent.find(`#download_volume_${this.id}`);
if (viewer.server) {
this.elDownloadButton.click(() => this.download());
} else {
this.elDownloadButton.hide();
}
let elRemove = this.elContent.find('.measurement_action_remove');
elRemove.click(() => { this.scene.removeVolume(measurement); });
this.measurement.addEventListener('marker_added', this._update);
this.measurement.addEventListener('marker_removed', this._update);
this.measurement.addEventListener('marker_moved', this._update);
this.elContent.i18n();
this.update();
}
download () {
let volume = this.measurement;
let boxes = volume.matrixWorld.elements.join(',');
let minLOD = 0;
let maxLOD = 100;
let pcs = [];
for (let pointcloud of this.scene.pointclouds) {
let urlIsAbsolute = new RegExp('^(?:[a-z]+:)?//', 'i').test(pointcloud.pcoGeometry.url);
let pc = '';
if (urlIsAbsolute) {
pc = pointcloud.pcoGeometry.url;
} else {
pc = `${window.location.href}/../${pointcloud.pcoGeometry.url}`;
}
pcs.push(pc);
}
let pc = pcs
.map(v => `pointcloud[]=${v}`)
.join('&');
let request = `${viewer.server}/start_extract_region_worker?minLOD=${minLOD}&maxLOD=${maxLOD}&box=${boxes}&${pc}`;// &pointCloud=${pc}`;
// console.log(request);
let elMessage = this.elContent.find(`#download_volume_status_${this.id}`);
elMessage.html('sending request...');
let workerID = null;
let start = new Date().getTime();
let observe = () => {
let request = `${viewer.server}/observe_status?workerID=${workerID}`;
let loaded = 0;
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.addEventListener('progress', e => {
let nowLoaded = e.loaded;
let response = xhr.responseText.substring(loaded, nowLoaded);
response = JSON.parse(response);
if (response.status === 'FINISHED') {
elMessage.html(`<br><a href="${viewer.server}/get_las?workerID=${workerID}">Download ready</a>`);
} else {
let current = new Date().getTime();
let duration = (current - start);
let seconds = parseInt(duration / 1000);
elMessage.html(`processing request... ${seconds}s`);
}
loaded = nowLoaded;
});
xhr.open('GET', request, true);
xhr.send(null);
};
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
// alert(xhr.responseText);
let res = JSON.parse(xhr.responseText);
console.log(res);
if (res.status === 'OK') {
workerID = res.workerID;
elMessage.html('request is being processed');
// checkUntilFinished();
observe();
} else if (res.status === 'ERROR_POINT_PROCESSED_ESTIMATE_TOO_LARGE') {
elMessage.html('Too many candidate points in selection.');
} else {
elMessage.html(`${res.status}`);
}
}
};
xhr.open('GET', request, true);
xhr.send(null);
}
update () {
if (!this.destroyed) {
requestAnimationFrame(this._update);
}
if (!this.elContent.is(':visible')) {
return;
}
if (this.measurement.position.x !== this.values.x) {
this.elX.val(this.measurement.position.x.toFixed(3));
this.values.x = this.measurement.position.x;
}
if (this.measurement.position.y !== this.values.y) {
let elY = this.elContent.find(`#volume_input_y_${this.measurement.id}`);
elY.val(this.measurement.position.y.toFixed(3));
this.values.y = this.measurement.position.y;
}
if (this.measurement.position.z !== this.values.z) {
let elZ = this.elContent.find(`#volume_input_z_${this.measurement.id}`);
elZ.val(this.measurement.position.z.toFixed(3));
this.values.z = this.measurement.position.z;
}
if (this.measurement.scale.x !== this.values.length) {
this.elLength.val(this.measurement.scale.x.toFixed(3));
this.values.length = this.measurement.scale.x;
}
if (this.measurement.scale.y !== this.values.width) {
this.elWidth.val(this.measurement.scale.y.toFixed(3));
this.values.width = this.measurement.scale.y;
}
if (this.measurement.scale.z !== this.values.height) {
this.elHeight.val(this.measurement.scale.z.toFixed(3));
this.values.height = this.measurement.scale.z;
}
let toDegrees = (r) => 180 * r / Math.PI;
if (this.measurement.rotation.x !== this.values.alpha) {
this.elAlpha.val(toDegrees(this.measurement.rotation.x).toFixed(1));
this.values.alpha = this.measurement.rotation.x;
}
if (this.measurement.rotation.y !== this.values.beta) {
this.elBeta.val(toDegrees(this.measurement.rotation.y).toFixed(1));
this.values.beta = this.measurement.rotation.y;
}
if (this.measurement.rotation.z !== this.values.gamma) {
this.elGamma.val(toDegrees(this.measurement.rotation.z).toFixed(1));
this.values.gamma = this.measurement.rotation.z;
}
}
destroy () {
this.elPanel.remove();
this.measurement.removeEventListener('marker_added', this._update);
this.measurement.removeEventListener('marker_removed', this._update);
this.measurement.removeEventListener('marker_moved', this._update);
this.destroyed = true;
}
};
let TYPE = {
DISTANCE: {panel: DistancePanel},
AREA: {panel: AreaPanel},
POINT: {panel: PointPanel},
ANGLE: {panel: AnglePanel},
HEIGHT: {panel: HeightPanel},
PROFILE: {panel: ProfilePanel},
VOLUME: {panel: VolumePanel}
};
let getType = (measurement) => {
if (measurement instanceof Potree.Measure) {
if (measurement.showDistances && !measurement.showArea && !measurement.showAngles) {
return TYPE.DISTANCE;
} else if (measurement.showDistances && measurement.showArea && !measurement.showAngles) {
return TYPE.AREA;
} else if (measurement.maxMarkers === 1) {
return TYPE.POINT;
} else if (!measurement.showDistances && !measurement.showArea && measurement.showAngles) {
return TYPE.ANGLE;
} else if (measurement.showHeight) {
return TYPE.HEIGHT;
} else {
return TYPE.OTHER;
}
} else if (measurement instanceof Potree.Profile) {
return TYPE.PROFILE;
} else if (measurement instanceof Potree.Volume) {
return TYPE.VOLUME;
}
};
let trackMeasurement = (scene, measurement) => {
// TODO: Dead code?
// id++;
let type = getType(measurement);
const Panel = type.panel;
let panel = new Panel(scene, measurement);
mlist.append(panel.elPanel);
let track = {
scene: scene,
measurement: measurement,
panel: panel,
stopTracking: (e) => { panel.destroy(); }
};
trackedItems.set(measurement, track);
let onremove = (e) => {
let remove = () => {
panel.destroy();
scene.removeEventListener('measurement_removed', onremove);
scene.removeEventListener('profile_removed', onremove);
scene.removeEventListener('volume_removed', onremove);
};
if (e.measurement instanceof Potree.Measure && e.measurement === measurement) {
remove();
} else if (e.profile instanceof Potree.Profile && e.profile === measurement) {
remove();
} else if (e.volume instanceof Potree.Volume && e.volume === measurement) {
remove();
}
};
scene.addEventListener('measurement_removed', onremove);
scene.addEventListener('profile_removed', onremove);
scene.addEventListener('volume_removed', onremove);
};
let scenelistener = (e) => {
if (e.measurement) {
trackMeasurement(e.scene, e.measurement);
} else if (e.profile) {
trackMeasurement(e.scene, e.profile);
viewer.profileWindow.show();
viewer.profileWindowController.setProfile(e.profile);
} else if (e.volume) {
trackMeasurement(e.scene, e.volume);
}
};
let trackScene = (scene) => {
// $("#measurement_details").empty();
trackedItems.forEach(function (trackedItem, key, map) {
trackedItem.stopTracking();
});
let items = scene.measurements
.concat(scene.profiles)
.concat(scene.volumes);
for (let measurement of items) {
trackMeasurement(scene, measurement);
}
if (!scene.hasEventListener('measurement_added', scenelistener)) {
scene.add