ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
LasSaveDialog.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #include "LasSaveDialog.h"
9 
10 #include "LasDetails.h"
12 
13 // Qt
14 #include <QLayoutItem>
15 #include <QStringListModel>
16 
17 // Qt5/Qt6 Compatibility
18 #include <QtCompat.h>
19 // CORE
20 #include <CVLog.h>
21 // DB
22 #include <ecvPointCloud.h>
23 #include <ecvScalarField.h>
24 
25 static constexpr int ExtraScalarFieldsTabIndex = 2;
26 
28 class MappingLabel : public QWidget
29 {
30  public:
31  explicit MappingLabel(QWidget* parent = nullptr)
32  : QWidget(parent)
33  , m_nameLabel(new QLabel)
34  , m_statusLabel(new QLabel)
35  {
36  auto* layout = new QHBoxLayout;
37  layout->addWidget(m_nameLabel);
38  layout->addWidget(m_statusLabel);
39  setLayout(layout);
40  }
41 
42  void setName(const QString& name)
43  {
44  m_nameLabel->setText(name);
45  }
46 
47  QString name() const
48  {
49  return m_nameLabel->text();
50  }
51 
52  void setWarning(const QString& message)
53  {
54  m_statusLabel->setPixmap(QApplication::style()->standardPixmap(QStyle::SP_MessageBoxWarning));
55  m_statusLabel->setToolTip(message);
56  }
57 
58  bool hasWarning() const
59  {
60  return !m_statusLabel->toolTip().isEmpty();
61  }
62 
63  void clearWarning()
64  {
65  m_statusLabel->setPixmap(QPixmap());
66  m_statusLabel->setToolTip(QString());
67  }
68 
69  private:
70  QLabel* m_nameLabel;
71  QLabel* m_statusLabel;
72 };
73 
75  : QDialog(parent)
76  , m_cloud(cloud)
77  , m_scalarFieldsNamesModel(new QStringListModel)
78  , m_extraFieldsDataTypesModel(new QStringListModel)
79  , m_optimalScale(std::numeric_limits<double>::quiet_NaN(),
80  std::numeric_limits<double>::quiet_NaN(),
81  std::numeric_limits<double>::quiet_NaN())
82  , m_originalScale(std::numeric_limits<double>::quiet_NaN(),
83  std::numeric_limits<double>::quiet_NaN(),
84  std::numeric_limits<double>::quiet_NaN())
85 {
86  setupUi(this);
87 
88  // retrieve the list of SFs (must be done first)
89  QStringList cloudScalarFieldsNames;
90  cloudScalarFieldsNames << QString();
91  if (m_cloud)
92  {
93  for (unsigned i = 0; i < m_cloud->getNumberOfScalarFields(); ++i)
94  {
95  if (strcmp(m_cloud->getScalarFieldName(i), "Default") != 0)
96  {
97  cloudScalarFieldsNames << m_cloud->getScalarFieldName(i);
98  }
99  }
100  }
101  else
102  {
103  assert(false);
104  }
105  m_scalarFieldsNamesModel->setStringList(cloudScalarFieldsNames);
106 
107  bestScaleRadioButton->setChecked(false);
108  originalScaleRadioButton->setEnabled(false);
109  customScaleRadioButton->setEnabled(true);
110  customScaleRadioButton->setChecked(true);
111 
112  for (const char* versionStr : LasDetails::AvailableVersions())
113  {
114  versionComboBox->addItem(versionStr);
115  }
116  versionComboBox->setCurrentIndex(0);
117 
118  connect(versionComboBox,
119  (void(QComboBox::*)(const QString&))(&QComboBox::currentIndexChanged),
120  this,
122 
123  connect(pointFormatComboBox,
124  (void(QComboBox::*)(int))(&QComboBox::currentIndexChanged),
125  this,
127 
128  handleSelectedVersionChange(versionComboBox->currentText()); // will call handleSelectedPointFormatChange
129 
130  QStringList extraFieldsDataTypeNames{"uint8",
131  "uint16",
132  "uint32",
133  "uint64",
134  "int8",
135  "int16",
136  "int32",
137  "int64",
138  "float32",
139  "float64"};
140  m_extraFieldsDataTypesModel->setStringList(extraFieldsDataTypeNames);
141 
142  connect(addExtraScalarFieldButton, &QPushButton::clicked, this, &LasSaveDialog::addExtraScalarFieldCard);
143 }
144 
148 {
149  pointFormatComboBox->blockSignals(true);
150  pointFormatComboBox->clear();
151 
152  const std::vector<unsigned>* pointFormats = LasDetails::PointFormatsAvailableForVersion(qPrintable(version));
153  if (pointFormats)
154  {
155  for (unsigned fmt : *pointFormats)
156  {
157  pointFormatComboBox->addItem(QString::number(fmt));
158  }
159  pointFormatComboBox->setCurrentIndex(0);
160 
161  // We have to force the call here so that the point format combo-box is always updated
163  }
164 
165  pointFormatComboBox->blockSignals(false);
166 }
167 
171 {
172  if (!m_cloud)
173  {
174  assert(false);
175  return;
176  }
177  if (index < 0)
178  {
179  return;
180  }
181  QObject* senderObject = sender();
182  size_t senderIndex = std::distance(m_scalarFieldMapping.begin(),
183  std::find_if(m_scalarFieldMapping.begin(),
184  m_scalarFieldMapping.end(),
185  [senderObject](const std::pair<MappingLabel*, QComboBox*>& pair)
186  { return pair.second == senderObject; }));
187 
188  if (qobject_cast<QComboBox*>(senderObject)->itemText(index).isEmpty())
189  {
190  m_scalarFieldMapping[senderIndex].first->clearWarning();
191  return;
192  }
193  const QString scalarFieldName = m_scalarFieldMapping[senderIndex].first->name();
194  const QString ccScalarFieldName = m_scalarFieldMapping[senderIndex].second->currentText();
195  int sfIdx = m_cloud->getScalarFieldIndexByName(ccScalarFieldName.toStdString().c_str());
196  if (sfIdx == -1)
197  {
198  assert(false);
199  return;
200  }
201  const cloudViewer::ScalarField* scalarField = m_cloud->getScalarField(sfIdx);
202  if (!scalarField)
203  {
204  assert(false);
205  return;
206  }
207 
208  // TODO support QString equality
209  LasScalarField::Id scalarFieldId = LasScalarField::IdFromName(scalarFieldName.toStdString().c_str(), selectedPointFormat());
210  LasScalarField::Range range = LasScalarField::ValueRange(scalarFieldId);
211 
212  if (scalarField->getMin() < range.min || scalarField->getMax() > range.max)
213  {
214  m_scalarFieldMapping[senderIndex].first->setWarning("Some values are out of range and will be truncated");
215  }
216  else
217  {
218  m_scalarFieldMapping[senderIndex].first->clearWarning();
219  }
220 
221  size_t numWarnings = std::count_if(m_scalarFieldMapping.begin(),
222  m_scalarFieldMapping.end(),
223  [](const std::pair<MappingLabel*, QComboBox*>& pair)
224  { return pair.first->hasWarning(); });
225 
226  if (numWarnings > 0)
227  {
228  tabWidget->setTabIcon(1, QApplication::style()->standardPixmap(QStyle::SP_MessageBoxWarning));
229  }
230  else
231  {
232  tabWidget->setTabIcon(1, {});
233  }
234 }
235 
238 {
239  if (!m_cloud)
240  {
241  assert(false);
242  return;
243  }
244  if (index < 0)
245  {
246  return;
247  }
248 
249  const std::vector<unsigned>* pointFormats = LasDetails::PointFormatsAvailableForVersion(versionComboBox->currentText());
250  if (!pointFormats)
251  {
252  Q_ASSERT_X(false, __func__, "No point format available for the selected version");
253  return;
254  }
255 
256  // Hide all the rows in our form
257  for (int i = 0; i < scalarFieldFormLayout->rowCount(); ++i)
258  {
259  auto* label = scalarFieldFormLayout->itemAt(i, QFormLayout::LabelRole);
260  if (label != nullptr && label->widget() != nullptr)
261  {
262  label->widget()->hide();
263  }
264 
265  auto* field = scalarFieldFormLayout->itemAt(i, QFormLayout::FieldRole);
266  if (field != nullptr && field->widget() != nullptr)
267  {
268  field->widget()->hide();
269  }
270  }
271 
272  unsigned selectedPointFormat = pointFormats->at(index);
273  std::vector<LasScalarField> lasScalarFields = LasScalarField::ForPointFormat(selectedPointFormat);
274 
275  int numDeltaFields = scalarFieldFormLayout->rowCount() - static_cast<int>(lasScalarFields.size());
276  if (numDeltaFields < 0)
277  {
278  // We have fewer rows in our form, create new ones
279  for (int i = numDeltaFields; i < 0; ++i)
280  {
281  auto* box = new QComboBox(this);
282  box->setModel(m_scalarFieldsNamesModel);
283  connect(box,
284  qOverload<int>(&QComboBox::currentIndexChanged),
285  this,
287 
288  auto* widget = new MappingLabel(this);
289  scalarFieldFormLayout->addRow(widget, box);
290  m_scalarFieldMapping.emplace_back(widget, box);
291  }
292  }
293 
294  assert(lasScalarFields.size() <= scalarFieldFormLayout->rowCount());
295 
296  QStringList cloudScalarFieldsNames = m_scalarFieldsNamesModel->stringList();
297  for (size_t i = 0; i < lasScalarFields.size(); ++i)
298  {
299  const LasScalarField& field = lasScalarFields[i];
300  m_scalarFieldMapping[i].first->setName(field.name());
301  m_scalarFieldMapping[i].first->clearWarning();
302  m_scalarFieldMapping[i].second->setCurrentIndex(cloudScalarFieldsNames.indexOf(field.name()));
303 
304  auto* label = scalarFieldFormLayout->itemAt(static_cast<int>(i), QFormLayout::LabelRole);
305  if (label != nullptr && label->widget() != nullptr)
306  {
307  label->widget()->show();
308  }
309 
310  auto* fieldWidget = scalarFieldFormLayout->itemAt(static_cast<int>(i), QFormLayout::FieldRole);
311  if (fieldWidget != nullptr && fieldWidget->widget() != nullptr)
312  {
313  fieldWidget->widget()->show();
314  }
315  }
316 
318  {
319  specialScalarFieldFrame->hide();
320  waveformCheckBox->setCheckState(Qt::Unchecked);
321  rgbCheckBox->setCheckState(Qt::Unchecked);
322  }
323  else
324  {
325  specialScalarFieldFrame->show();
327  {
328  rgbCheckBox->show();
329  rgbCheckBox->setEnabled(m_cloud->hasColors());
330  rgbCheckBox->setChecked(m_cloud->hasColors());
331  }
332  else
333  {
334  rgbCheckBox->hide();
335  }
336 
338  {
339  waveformCheckBox->show();
340  waveformCheckBox->setEnabled(m_cloud->hasFWF());
341  waveformCheckBox->setChecked(m_cloud->hasFWF());
342  }
343  else
344  {
345  waveformCheckBox->hide();
346  }
347  }
348 }
349 
350 LasExtraScalarFieldCard* LasSaveDialog::createCard() const
351 {
352  auto* card = new LasExtraScalarFieldCard;
353  card->typeComboBox->setModel(m_extraFieldsDataTypesModel);
354  card->firstScalarFieldComboBox->setModel(m_scalarFieldsNamesModel);
355  card->secondScalarFieldComboBox->setModel(m_scalarFieldsNamesModel);
356  card->thirdScalarFieldComboBox->setModel(m_scalarFieldsNamesModel);
357  connect(card->removeButton, &QPushButton::clicked, card, &QWidget::hide);
358 
359  return card;
360 }
361 
363 {
364 #ifdef CC_CORE_LIB_USES_DOUBLE
365  const char* defaultType = "float64";
366 #else
367  const char* defaultType = "float32";
368 #endif
369  int defaultTypeIndex = m_extraFieldsDataTypesModel->stringList().indexOf(defaultType);
370  defaultTypeIndex = std::max(defaultTypeIndex, 0);
371 
372  int esfCount = extraScalarFieldsLayout->count();
373 
374  // firt, look if a card has already been created, but is currently hidden
375  for (int i = 0; i < esfCount; ++i)
376  {
377  QLayoutItem* item = extraScalarFieldsLayout->itemAt(i);
378  QWidget* widget = item->widget();
379  if (widget && widget->isHidden())
380  {
381  // remove and insert it again at the end
382  extraScalarFieldsLayout->removeItem(item);
383  extraScalarFieldsLayout->insertItem(extraScalarFieldsLayout->count(), item);
384  auto* card = qobject_cast<LasExtraScalarFieldCard*>(widget);
385  if (!card)
386  {
387  assert(false);
388  continue;
389  }
390 
391  card->reset();
392  widget->show();
393  return;
394  }
395  }
396 
397  // else we'll create a new card
398  auto* card = createCard();
399  extraScalarFieldsLayout->insertWidget(esfCount, card);
400 }
401 
403 {
404  const QString versionStr = QString("1.%1").arg(versionAndFmt.minorVersion);
405 
406  int versionIndex = versionComboBox->findText(versionStr);
407  if (versionIndex >= 0)
408  {
409  QString fmtStr = QString::number(versionAndFmt.pointFormat);
410  versionComboBox->setCurrentIndex(versionIndex);
411  int pointFormatIndex = pointFormatComboBox->findText(fmtStr);
412  if (pointFormatIndex >= 0)
413  {
414  pointFormatComboBox->setCurrentIndex(pointFormatIndex);
415  }
416  }
417 }
418 
419 void LasSaveDialog::setOptimalScale(const CCVector3d& scale, bool autoCheck /*=false*/)
420 {
421  m_optimalScale = scale;
422 
423  bestScaleLabel->setText(QString("(%1, %2, %3)").arg(scale.x).arg(scale.y).arg(scale.z));
424 
425  bestScaleRadioButton->setEnabled(true);
426  if (autoCheck)
427  {
428  bestScaleRadioButton->setChecked(true);
429  }
430 }
431 
432 void LasSaveDialog::setOriginalScale(const CCVector3d& scale, bool canUseScale, bool autoCheck /*=true*/)
433 {
434  m_originalScale = scale;
435 
436  originalScaleLabel->setText(QString("(%1, %2, %3)").arg(scale.x).arg(scale.y).arg(scale.z));
437 
438  originalScaleRadioButton->setEnabled(canUseScale);
439  if (!canUseScale)
440  {
441  labelOriginal->setText(QObject::tr("Original scale is too small for this cloud ")); // add two whitespaces to avoid issues with italic characters justification
442  labelOriginal->setStyleSheet("color: red;");
443  originalScaleRadioButton->setChecked(false);
444  }
445  else if (autoCheck)
446  {
447  originalScaleRadioButton->setChecked(true);
448  }
449 }
450 
451 void LasSaveDialog::setExtraScalarFields(const std::vector<LasExtraScalarField>& extraScalarFields)
452 {
453  if (extraScalarFields.empty())
454  {
455  return;
456  }
457 
458  for (const LasExtraScalarField& field : extraScalarFields)
459  {
460  auto* card = createCard();
461  extraScalarFieldsLayout->insertWidget(extraScalarFieldsLayout->count(), card);
462  card->fillFrom(field);
463  }
464 }
465 
467 {
468  return static_cast<uint8_t>(std::min(pointFormatComboBox->currentText().toUInt(), 255u));
469 }
470 
471 void LasSaveDialog::selectedVersion(uint8_t& versionMajor, uint8_t& versionMinor) const
472 {
473  versionMajor = 1;
474  versionMinor = 0;
475 
476  const QString versionString = versionComboBox->currentText();
477  auto tokens = qtCompatSplitRefChar(versionString, '.');
478  if (tokens.size() == 2)
479  {
480  versionMajor = static_cast<uint8_t>(std::min(tokens[0].toUInt(), 255u));
481  versionMinor = static_cast<uint8_t>(std::min(tokens[1].toUInt(), 255u));
482  }
483  else
484  {
485  assert(false);
486  }
487 }
488 
490 {
491  return rgbCheckBox->isChecked();
492 }
493 
495 {
496  return waveformCheckBox->isChecked();
497 }
498 
500 {
501  if (bestScaleRadioButton->isChecked())
502  {
503  assert(std::isfinite(m_optimalScale.x) && std::isfinite(m_optimalScale.y) && std::isfinite(m_optimalScale.z));
504  return m_optimalScale;
505  }
506  else if (originalScaleRadioButton->isChecked())
507  {
508  assert(std::isfinite(m_originalScale.x) && std::isfinite(m_originalScale.y) && std::isfinite(m_originalScale.z));
509  return m_originalScale;
510  }
511  else if (customScaleRadioButton->isChecked())
512  {
513  double customScale = customScaleDoubleSpinBox->value();
514  return {customScale, customScale, customScale};
515  }
516 
517  assert(false);
518  return {};
519 }
520 
521 std::vector<LasScalarField> LasSaveDialog::fieldsToSave() const
522 {
523  if (!m_cloud)
524  {
525  assert(false);
526  return {};
527  }
528 
529  unsigned pointFormat = selectedPointFormat();
530 
531  std::vector<LasScalarField> fields;
532  fields.reserve(scalarFieldFormLayout->rowCount());
533 
534  for (const auto& item : m_scalarFieldMapping)
535  {
536  if (item.second->currentIndex() > 0)
537  {
538  int sfIdx = m_cloud->getScalarFieldIndexByName(qPrintable(item.second->currentText()));
539  if (sfIdx < 0)
540  {
541  assert(false);
542  continue;
543  }
544 
545  ccScalarField* sf = static_cast<ccScalarField*>(m_cloud->getScalarField(sfIdx));
546 
547  const std::string name = item.first->name().toStdString();
548  fields.emplace_back(LasScalarField::IdFromName(name.c_str(), pointFormat), sf);
549  }
550  }
551 
552  fields.shrink_to_fit();
553 
554  return fields;
555 }
556 
557 std::vector<LasExtraScalarField> LasSaveDialog::extraFieldsToSave() const
558 {
559  if (!m_cloud)
560  {
561  assert(false);
562  return {};
563  }
564 
565  if (extraScalarFieldsLayout->count() == 0)
566  {
567  return {};
568  }
569  int esfCount = extraScalarFieldsLayout->count();
570 
571  std::vector<LasExtraScalarField> extraScalarFields;
572  extraScalarFields.reserve(esfCount);
573 
574  for (int i = 0; i < esfCount; ++i)
575  {
576  QLayoutItem* item = extraScalarFieldsLayout->itemAt(i);
577  QWidget* widget = item->widget();
578  if (!widget || widget->isHidden())
579  {
580  continue;
581  }
582  auto* card = qobject_cast<LasExtraScalarFieldCard*>(widget);
583  if (!card)
584  {
585  continue;
586  }
587 
588  LasExtraScalarField field;
589  if (!card->fillField(field, *m_cloud))
590  {
591  CVLog::Error("failed to convert scalar field info to something writable");
592  continue;
593  }
594  extraScalarFields.push_back(field);
595  }
596 
597  extraScalarFields.shrink_to_fit();
598 
599  return extraScalarFields;
600 }
std::string version
std::vector< PCLPointField > fields
std::string name
static constexpr int ExtraScalarFieldsTabIndex
QtCompatStringRefList qtCompatSplitRefChar(const QString &str, QChar sep)
Definition: QtCompat.h:430
static bool Error(const char *format,...)
Display an error dialog with formatted message.
Definition: CVLog.cpp:143
This serves the same purpose as LasScalarField but for extra bytes.
void setExtraScalarFields(const std::vector< LasExtraScalarField > &extraScalarFields)
Set the extra LAS scalar fields saved from the original file.
void handleComboBoxChange(int index)
void setOriginalScale(const CCVector3d &scale, bool canUseScale, bool autoCheck=true)
void handleSelectedPointFormatChange(int index)
When the user changes the point format, we need to update the scalar field form.
bool shouldSaveWaveform() const
Returns whether the user wants to save the Waveforms.
LasSaveDialog(ccPointCloud *cloud, QWidget *parent=nullptr)
void selectedVersion(uint8_t &versionMajor, uint8_t &versionMinor) const
Returns the version currently selected.
bool shouldSaveRGB() const
Returns whether the user wants to save RGB.
std::vector< LasScalarField > fieldsToSave() const
void addExtraScalarFieldCard()
uint8_t selectedPointFormat() const
Returns the point format currently selected.
CCVector3d chosenScale() const
Returns the currently selected scale.
std::vector< LasExtraScalarField > extraFieldsToSave() const
void setVersionAndPointFormat(const LasDetails::LasVersion versionAndFmt)
void handleSelectedVersionChange(const QString &)
void setOptimalScale(const CCVector3d &scale, bool autoCheck=false)
Set scale that would offer the user the best precision.
Widget to map a predefined scalar field 'role' with a particular scalar field (combo box)
MappingLabel(QWidget *parent=nullptr)
void setName(const QString &name)
void clearWarning()
void setWarning(const QString &message)
bool hasWarning() const
QString name() const
Type y
Definition: CVGeom.h:137
Type x
Definition: CVGeom.h:137
Type z
Definition: CVGeom.h:137
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
bool hasFWF() const
Returns whether the cloud has associated Full WaveForm data.
bool hasColors() const override
Returns whether colors are enabled or not.
A scalar field associated to display-related parameters.
int getScalarFieldIndexByName(const char *name) const
Returns the index of a scalar field represented by its name.
ScalarField * getScalarField(int index) const
Returns a pointer to a specific scalar field.
unsigned getNumberOfScalarFields() const
Returns the number of associated (and active) scalar fields.
const char * getScalarFieldName(int index) const
Returns the name of a specific scalar field.
A simple scalar field (to be associated to a point cloud)
Definition: ScalarField.h:25
ScalarType getMin() const
Returns the minimum value.
Definition: ScalarField.h:72
ScalarType getMax() const
Returns the maximum value.
Definition: ScalarField.h:74
int min(int a, int b)
Definition: cutil_math.h:53
int max(int a, int b)
Definition: cutil_math.h:48
const std::array< const char *, 3 > & AvailableVersions()
Definition: LasDetails.cpp:177
bool HasRGB(unsigned pointFormatId)
Returns whether the point format supports RGB.
Definition: LasDetails.h:132
bool HasWaveform(unsigned pointFormatId)
Returns whether the point format supports Waveforms.
Definition: LasDetails.h:143
const std::vector< unsigned > * PointFormatsAvailableForVersion(QString version)
Definition: LasDetails.cpp:155
Definition: Eigen.h:85
See SelectBestVersion
Definition: LasDetails.h:178
static std::vector< LasScalarField > ForPointFormat(unsigned pointFormatId)
const char * name() const
static LasScalarField::Range ValueRange(LasScalarField::Id id)
Returns the range of value the given field (ID) supports.
static LasScalarField::Id IdFromName(const char *name, unsigned targetPointFormat)