ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
ecvColorScale.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 "ecvColorScale.h"
9 
10 // Qt
11 #include <QUuid>
12 #include <QXmlStreamReader>
13 #include <QXmlStreamWriter>
14 
15 // Qt5/Qt6 Compatibility
16 #include <QtCompat.h>
17 
18 // cloudViewer
19 #include <CVGeom.h>
20 #include <CVLog.h>
21 
22 // Local
23 #include "ecvObject.h"
24 
25 static const QString s_xmlACloudViewer("ACloudViewer");
26 static const QString s_xmlCloudCompare("CloudCompare");
27 static const QString s_xmlColorScaleTitle("ColorScale");
28 static const QString s_xmlColorScaleProperties("Properties");
29 static const QString s_xmlColorScaleData("Data");
30 constexpr int s_xmlColorScaleVer = 1;
31 
34 }
35 
37  const QString& uuid /*=QString()*/)
38  : m_name(name),
39  m_uuid(uuid),
40  m_updated(false),
41  m_relative(true),
42  m_locked(false),
43  m_absoluteMinValue(0.0),
44  m_absoluteRange(1.0) {
45  if (m_uuid.isNull()) generateNewUuid();
46 }
47 
49  m_uuid = QUuid::createUuid().toString();
50 }
51 
53  const QString& uuid /*=QString()*/) const {
54  ccColorScale::Shared newCS(new ccColorScale(m_name, uuid));
55  try {
56  newCS->m_relative = m_relative;
57  newCS->m_locked = m_locked;
58  newCS->m_absoluteMinValue = m_absoluteMinValue;
59  newCS->m_absoluteRange = m_absoluteRange;
60  newCS->m_steps = m_steps;
61  newCS->m_customLabels = m_customLabels;
62  newCS->update();
63  } catch (const std::bad_alloc&) {
65  QStringLiteral("Not enough memory to copy the color scale"));
66  return ccColorScale::Shared(nullptr);
67  }
68 
69  return newCS;
70 }
71 
73 
75  bool autoUpdate /*=true*/) {
76  if (m_locked) {
77  CVLog::Warning(QString("[ccColorScale::insert] Scale '%1' is locked!")
78  .arg(m_name));
79  return;
80  }
81 
82  m_steps.push_back(step);
83 
84  m_updated = false;
85 
86  if (autoUpdate && m_steps.size() >= static_cast<int>(MIN_STEPS)) {
87  update();
88  }
89 }
90 
92  if (m_locked) {
93  CVLog::Warning(QString("[ccColorScale::clear] Scale '%1' is locked!")
94  .arg(m_name));
95  return;
96  }
97 
98  m_steps.clear();
99 
100  m_updated = false;
101 }
102 
103 void ccColorScale::remove(int index, bool autoUpdate /*=true*/) {
104  if (m_locked) {
105  CVLog::Warning(QString("[ccColorScale::remove] Scale '%1' is locked!")
106  .arg(m_name));
107  return;
108  }
109 
110  m_steps.removeAt(index);
111  m_updated = false;
112 
113  if (autoUpdate) update();
114 }
115 
117  std::sort(m_steps.begin(), m_steps.end(), ccColorScaleElement::IsSmaller);
118 }
119 
121  m_updated = false;
122 
123  if (m_steps.size() >= static_cast<int>(MIN_STEPS)) {
124  sort();
125 
126  const unsigned stepCount = static_cast<unsigned>(m_steps.size());
127  assert(stepCount >= 2);
128  assert(m_steps.front().getRelativePos() == 0.0);
129  assert(m_steps.back().getRelativePos() == 1.0);
130  if (m_steps.front().getRelativePos() != 0.0 ||
131  m_steps.back().getRelativePos() != 1.0) {
132  CVLog::Warning(QString("[ccColorScale] Scale '%1' is invalid! "
133  "(boundaries are not [0.0-1.0]")
134  .arg(getName()));
135  } else {
136  unsigned j = 0; // current interval
137  for (unsigned i = 0; i < MAX_STEPS; ++i) {
138  const double relativePos =
139  static_cast<double>(i) / (MAX_STEPS - 1);
140 
141  // forward to the right intervale
142  while (j + 2 < stepCount &&
143  m_steps[j + 1].getRelativePos() < relativePos)
144  ++j;
145 
146  // linear interpolation
147  const CCVector3d colBefore(m_steps[j].getColor().redF(),
148  m_steps[j].getColor().greenF(),
149  m_steps[j].getColor().blueF());
150  const CCVector3d colNext(m_steps[j + 1].getColor().redF(),
151  m_steps[j + 1].getColor().greenF(),
152  m_steps[j + 1].getColor().blueF());
153 
154  // interpolation coef
155  const double alpha =
156  (relativePos - m_steps[j].getRelativePos()) /
157  (m_steps[j + 1].getRelativePos() -
158  m_steps[j].getRelativePos());
159 
160  const CCVector3d interpCol =
161  colBefore + (colNext - colBefore) * alpha;
162 
164  static_cast<ColorCompType>(interpCol.x * ecvColor::MAX),
165  static_cast<ColorCompType>(interpCol.y * ecvColor::MAX),
166  static_cast<ColorCompType>(interpCol.z * ecvColor::MAX),
167  ecvColor::MAX);
168  }
169 
170  m_updated = true;
171  }
172  } else {
173  CVLog::Warning(QString("[ccColorScale] Scale '%1' is invalid! (not "
174  "enough elements)")
175  .arg(getName()));
176  }
177 
178  if (!m_updated) {
179  // I saw an invalid scale and I want it painted black ;)
180  for (unsigned i = 0; i < MAX_STEPS; ++i)
182  }
183 }
184 
185 bool ccColorScale::toFile(QFile& out, short dataVersion) const {
186  if (dataVersion < 27) {
187  assert(false);
188  return false;
189  }
190 
191  QDataStream outStream(&out);
192 
193  // name (dataVersion>=27)
194  outStream << m_name;
195 
196  // UUID (dataVersion>=27)
197  outStream << m_uuid;
198 
199  // relative state (dataVersion>=27)
200  if (out.write((const char*)&m_relative, sizeof(bool)) < 0)
201  return WriteError();
202 
203  // Absolute min value (dataVersion>=27)
204  if (out.write((const char*)&m_absoluteMinValue, sizeof(double)) < 0)
205  return WriteError();
206  // Absolute range (dataVersion>=27)
207  if (out.write((const char*)&m_absoluteRange, sizeof(double)) < 0)
208  return WriteError();
209 
210  // locked state (dataVersion>=27)
211  if (out.write((const char*)&m_locked, sizeof(bool)) < 0)
212  return WriteError();
213 
214  // steps list (dataVersion>=27)
215  {
216  // steps count
217  uint32_t stepCount = static_cast<uint32_t>(m_steps.size());
218  if (out.write((const char*)&stepCount, 4) < 0) return WriteError();
219 
220  // write each step
221  for (uint32_t i = 0; i < stepCount; ++i) {
222  outStream << m_steps[i].getRelativePos();
223  outStream << m_steps[i].getColor();
224  }
225  }
226 
227  // custom labels (dataVersion>=40)
228  if (dataVersion >= 40) {
229  // custom label count
230  uint32_t labelCount = static_cast<uint32_t>(m_customLabels.size());
231  if (out.write((const char*)&labelCount, 4) < 0) return WriteError();
232 
233  // write each custom label
234  for (LabelSet::const_iterator it = m_customLabels.begin();
235  it != m_customLabels.end(); ++it) {
236  outStream << it->value;
237  if (dataVersion >= 54) {
238  outStream << it->text;
239  }
240  }
241  }
242 
243  return true;
244 }
245 
247  short minVersion = 27;
248  if (!m_customLabels.empty()) {
249  minVersion = 40;
250  // Check if any custom label has text
251  for (const auto& label : m_customLabels) {
252  if (!label.text.isEmpty()) {
253  minVersion = 54;
254  break;
255  }
256  }
257  }
258  return minVersion;
259 }
260 
261 bool ccColorScale::fromFile(QFile& in,
262  short dataVersion,
263  int flags,
264  LoadedIDMap& oldToNewIDMap) {
265  if (dataVersion < 27) // structure appeared at version 27!
266  return false;
267 
268  QDataStream inStream(&in);
269 
270  // name (dataVersion>=27)
271  inStream >> m_name;
272 
273  // UUID (dataVersion>=27)
274  inStream >> m_uuid;
275 
276  // relative state (dataVersion>=27)
277  if (in.read((char*)&m_relative, sizeof(bool)) < 0) return ReadError();
278 
279  // Absolute min value (dataVersion>=27)
280  if (in.read((char*)&m_absoluteMinValue, sizeof(double)) < 0)
281  return ReadError();
282  // Absolute range (dataVersion>=27)
283  if (in.read((char*)&m_absoluteRange, sizeof(double)) < 0)
284  return ReadError();
285 
286  // locked state (dataVersion>=27)
287  if (in.read((char*)&m_locked, sizeof(bool)) < 0) return ReadError();
288 
289  // steps list (dataVersion>=27)
290  {
291  // steps count
292  uint32_t stepCount = 0;
293  if (in.read((char*)&stepCount, 4) < 0) return ReadError();
294 
295  // read each step
296  m_steps.clear();
297  for (uint32_t i = 0; i < stepCount; ++i) {
298  double relativePos = 0.0;
299  QColor color(Qt::white);
300  inStream >> relativePos;
301  inStream >> color;
302 
303  m_steps.push_back(ccColorScaleElement(relativePos, color));
304  }
305 
306  update();
307  }
308 
309  // custom labels (dataVersion>=40)
310  if (dataVersion >= 40) {
311  // custom label count
312  uint32_t labelCount = 0;
313  if (in.read((char*)&labelCount, 4) < 0) return ReadError();
314 
315  try {
316  for (uint32_t i = 0; i < labelCount; ++i) {
317  double label = 0.0;
318  QString text;
319 
320  inStream >> label;
321  if (dataVersion >= 54) {
322  inStream >> text;
323  }
324 
325  m_customLabels.insert({label, text});
326  }
327  } catch (const std::bad_alloc&) {
328  // not enough memory
329  return MemoryError();
330  }
331  }
332 
333  return true;
334 }
335 
336 void ccColorScale::setAbsolute(double minVal, double maxVal) {
337  assert(maxVal >= minVal);
338 
339  m_relative = false;
340 
341  m_absoluteMinValue = minVal;
342  m_absoluteRange = maxVal - minVal;
343 
344  // as 'm_absoluteRange' is used for division, we make sure it is not left to
345  // 0!
346  m_absoluteRange = std::max(m_absoluteRange, 1e-12);
347 }
348 
349 void ccColorScale::getAbsoluteBoundaries(double& minVal, double& maxVal) const {
350  minVal = m_absoluteMinValue;
352 }
353 
354 bool ccColorScale::saveAsXML(QString filename) const {
355  QFile file(filename);
356  if (!file.open(QFile::WriteOnly | QFile::Text)) {
357  CVLog::Error(
358  QString("Failed to open file '%1' for writing!").arg(filename));
359  return false;
360  }
361 
362  // write content
363  QXmlStreamWriter stream(&file);
364  stream.setAutoFormatting(true);
365  stream.writeStartDocument();
366  {
367  stream.writeStartElement(s_xmlACloudViewer); // CLOUDVIEWER
368  {
369  stream.writeStartElement(s_xmlColorScaleTitle); // ColorScale
370  {
371  // file version
372  stream.writeAttribute("version",
373  QString::number(s_xmlColorScaleVer));
374 
375  // Properties
376  stream.writeStartElement(s_xmlColorScaleProperties);
377  {
378  stream.writeTextElement(QStringLiteral("name"), getName());
379  stream.writeTextElement(QStringLiteral("uuid"), getUuid());
380  stream.writeTextElement(QStringLiteral("absolute"),
381  isRelative() ? QStringLiteral("0")
382  : QStringLiteral("1"));
383  if (!isRelative()) {
384  stream.writeTextElement(
385  QStringLiteral("minValue"),
386  QString::number(m_absoluteMinValue, 'g', 12));
387  stream.writeTextElement(
388  QStringLiteral("range"),
389  QString::number(m_absoluteRange, 'g', 12));
390  }
391  }
392  stream.writeEndElement(); // Properties
393 
394  // Data
395  stream.writeStartElement(s_xmlColorScaleData);
396  {
397  // write each step
398  {
399  for (QList<ccColorScaleElement>::const_iterator it =
400  m_steps.begin();
401  it != m_steps.end(); ++it) {
402  stream.writeStartElement(QStringLiteral("step"));
403  {
404  const ccColorScaleElement& elem = *it;
405  const QColor& color = elem.getColor();
406  double relativePos = elem.getRelativePos();
407 
408  stream.writeAttribute(
409  QStringLiteral("r"),
410  QString::number(color.red()));
411  stream.writeAttribute(
412  QStringLiteral("g"),
413  QString::number(color.green()));
414  stream.writeAttribute(
415  QStringLiteral("b"),
416  QString::number(color.blue()));
417  stream.writeAttribute(
418  QStringLiteral("pos"),
419  QString::number(relativePos, 'g', 12));
420  }
421  stream.writeEndElement(); // step
422  }
423  }
424 
425  // write custom labels as well (if any)
426  {
427  for (LabelSet::const_iterator it =
428  m_customLabels.begin();
429  it != m_customLabels.end(); ++it) {
430  {
431  stream.writeStartElement(
432  QStringLiteral("label"));
433  {
434  stream.writeAttribute(
435  QStringLiteral("val"),
436  QString::number(it->value, 'g',
437  12));
438  if (!it->text.isEmpty()) {
439  stream.writeAttribute(
440  QStringLiteral("text"),
441  it->text);
442  }
443  }
444  stream.writeEndElement(); // label
445  }
446  }
447  }
448  }
449  stream.writeEndElement(); // Data
450  }
451  stream.writeEndElement(); // ColorScale
452  }
453  stream.writeEndElement(); // CLOUDVIEWER
454  }
455  stream.writeEndDocument();
456 
457  return true;
458 }
459 
461  QFile file(filename);
462  if (!file.open(QFile::ReadOnly | QFile::Text)) {
463  CVLog::Error(
464  QString("Failed to open file '%1' for reading!").arg(filename));
465  return Shared(nullptr);
466  }
467 
468  Shared scale(0);
469 
470  // read content
471  QXmlStreamReader stream(&file);
472  bool error = true;
473  while (true) // fake loop for easy break
474  {
475  // expected: CLOUDVIEWER or
476  if (!stream.readNextStartElement() ||
477  (stream.name() != s_xmlACloudViewer &&
478  stream.name() != s_xmlCloudCompare)) {
479  break;
480  }
481 
482  // expected: ColorScale
483  if (!stream.readNextStartElement() ||
484  stream.name() != s_xmlColorScaleTitle) {
485  break;
486  }
487 
488  // read version number
489  QXmlStreamAttributes attributes = stream.attributes();
490  if (attributes.size() == 0 ||
491  attributes[0].name() != QStringLiteral("version")) {
492  break;
493  }
494  bool ok = false;
495  int version = attributes[0].value().toString().toInt(&ok);
496  if (!ok || version > s_xmlColorScaleVer) {
497  if (ok)
498  CVLog::Warning(QString("[ccColorScale::LoadFromXML] Unhandled "
499  "version: %1")
500  .arg(version));
501  break;
502  }
503 
504  // expected: Properties
505  if (!stream.readNextStartElement() ||
506  stream.name() != s_xmlColorScaleProperties) {
507  break;
508  }
509 
510  // we can now create the scale structure
511  scale = Shared(new ccColorScale("temp"));
512 
513  // read elements
514  int missingItems = 3;
515  while (!stream.atEnd() && missingItems > 0) {
516  if (!stream.readNextStartElement()) {
517  break;
518  }
519  QtCompatStringRef itemName = stream.name();
520  QString itemValue = stream.readElementText();
521  CVLog::Print(QString("[XML] Item '%1': '%2'")
522  .arg(itemName.toString(), itemValue));
523 
524  if (itemName == QStringLiteral("name")) {
525  scale->setName(itemValue);
526  --missingItems;
527  } else if (itemName == QStringLiteral("uuid")) {
528  scale->setUuid(itemValue);
529  --missingItems;
530  } else if (itemName == QStringLiteral("absolute")) {
531  if (itemValue == QStringLiteral("1")) {
532  scale->setAbsolute(
533  0,
534  1); // the true values will be updated afterwards
535  missingItems += 2; // we need the minValue and range items!
536  }
537  --missingItems;
538  } else if (itemName == QStringLiteral("minValue")) {
539  scale->m_absoluteMinValue = itemValue.toDouble(&ok);
540  if (!ok) break;
541  --missingItems;
542  } else if (itemName == QStringLiteral("range")) {
543  scale->m_absoluteRange = itemValue.toDouble(&ok);
544  if (!ok) break;
545  --missingItems;
546  }
547  }
548 
549  if (missingItems > 0) {
551  QString("[ccColorScale::LoadFromXML] Missing properties!"));
552  break;
553  }
554  stream.skipCurrentElement();
555 
556  // expected: Data
557  if (!stream.readNextStartElement() ||
558  stream.name() != s_xmlColorScaleData) {
559  CVLog::Warning(QString("[ccColorScale::LoadFromXML] Unexpected "
560  "element: %1")
561  .arg(stream.name().toString()));
562  break;
563  }
564 
565  // read data
566  bool dataError = false;
567  try {
568  while (!stream.atEnd()) {
569  if (!stream.readNextStartElement()) break;
570  if (stream.name() == QStringLiteral("step")) {
571  QXmlStreamAttributes attributes = stream.attributes();
572  int attributeCount = attributes.size();
573  if (attributeCount < 4) {
574  dataError = true;
575  break;
576  }
577  QColor rgb;
578  double pos = 0;
579  for (int i = 0; i < attributes.size(); ++i) {
580  QString name =
581  attributes[i].name().toString().toUpper();
582  QString value = attributes[i].value().toString();
583  if (name == QStringLiteral("R"))
584  rgb.setRed(value.toInt());
585  else if (name == QStringLiteral("G"))
586  rgb.setGreen(value.toInt());
587  else if (name == QStringLiteral("B"))
588  rgb.setBlue(value.toInt());
589  else if (name == QStringLiteral("POS"))
590  pos = value.toDouble();
591  else
592  --attributeCount;
593  }
594 
595  if (attributeCount < 4) {
597  QString("[ccColorScale::LoadFromXML] Missing "
598  "data attributes!"));
599  dataError = true;
600  break;
601  }
602  stream.skipCurrentElement();
603 
604  scale->insert(ccColorScaleElement(pos, rgb), false);
605  } else if (stream.name() == QStringLiteral("label")) {
606  QXmlStreamAttributes attributes = stream.attributes();
607  int attributeCount = attributes.size();
608  if (attributeCount < 1) {
609  dataError = true;
610  break;
611  }
612 
613  double value = std::numeric_limits<double>::quiet_NaN();
614  QString text;
615  for (int i = 0; i < attributes.size(); ++i) {
616  QString name =
617  attributes[i].name().toString().toUpper();
618  if (name == QStringLiteral("VAL")) {
619  QString valueStr = attributes[i].value().toString();
620  bool ok = false;
621  value = valueStr.toDouble(&ok);
622  if (!ok) {
624  QStringLiteral(
625  "[ccColorScale::LoadFromXML] "
626  "Invalid value:") +
627  valueStr);
628  value = std::numeric_limits<
629  double>::quiet_NaN();
630  dataError = true;
631  }
632  } else if (name == QStringLiteral("TEXT")) {
633  text = attributes[i].value().toString();
634  }
635  }
636 
637  if (std::isfinite(value)) {
638  // we have a valid label
639  scale->m_customLabels.insert({value, text});
640  }
641 
642  stream.skipCurrentElement();
643  }
644  }
645  } catch (const std::bad_alloc&) {
647  QString("[ccColorScale::LoadFromXML] Not enough memory!"));
648  dataError = true;
649  }
650  scale->update();
651 
652  // end
653  error = dataError;
654  break;
655  }
656 
657  if (error) {
658  scale.clear();
659  CVLog::Error(QString("An error occurred while reading file '%1'")
660  .arg(filename));
661  }
662 
663  return scale;
664 }
std::string filename
std::string version
std::string name
math::float4 color
QStringView QtCompatStringRef
Definition: QtCompat.h:227
static bool Warning(const char *format,...)
Prints out a formatted warning message in console.
Definition: CVLog.cpp:133
static bool Print(const char *format,...)
Prints out a formatted message in console.
Definition: CVLog.cpp:113
static bool Error(const char *format,...)
Display an error dialog with formatted message.
Definition: CVLog.cpp:143
Type y
Definition: CVGeom.h:137
Type x
Definition: CVGeom.h:137
Type z
Definition: CVGeom.h:137
Color scale element: one value + one color.
Definition: ecvColorScale.h:22
const QColor & getColor() const
Returns color.
Definition: ecvColorScale.h:43
double getRelativePos() const
Returns step position (relative to scale boundaries)
Definition: ecvColorScale.h:38
static bool IsSmaller(const ccColorScaleElement &e1, const ccColorScaleElement &e2)
Comparison operator between two color scale elements.
Definition: ecvColorScale.h:46
bool saveAsXML(QString filename) const
Saves this color scale as an XML file.
ccColorScaleElement & step(int index)
Access to a given step.
QString getUuid() const
Returns unique ID.
void setAbsolute(double minVal, double maxVal)
Sets scale as absolute.
void update()
Updates internal representation.
static const unsigned MIN_STEPS
Minimum number of steps.
Definition: ecvColorScale.h:99
QList< ccColorScaleElement > m_steps
Elements.
const QString & getName() const
Returns name.
bool m_relative
Whether scale is relative or not.
bool toFile(QFile &out, short dataVersion) const override
Saves data to binary stream.
bool isRelative() const
Returns whether scale is relative or absoute.
ccColorScale(const QString &name, const QString &uuid=QString())
Default constructor.
bool m_locked
Whether scale is locked or not.
ecvColor::Rgb m_rgbaScale[MAX_STEPS]
Internal representation (RGB)
void remove(int index, bool autoUpdate=true)
Deletes a given step.
bool fromFile(QFile &in, short dataVersion, int flags, LoadedIDMap &oldToNewIDMap) override
Loads data from binary stream.
static Shared LoadFromXML(QString filename)
Loads a color scale from an XML file.
short minimumFileVersion() const override
Returns the minimum file version required to save this instance.
void getAbsoluteBoundaries(double &minVal, double &maxVal) const
Get absolute scale boundaries.
void insert(const ccColorScaleElement &step, bool autoUpdate=true)
Adds a step.
void generateNewUuid()
Generates a new unique ID.
double m_absoluteMinValue
'Absolute' minimum value
double m_absoluteRange
'Absolute' range
int stepCount() const
Returns the current number of steps.
ccColorScale::Shared copy(const QString &uuid=QString()) const
Creates a copy of this color scale (with a specified unique id)
virtual ~ccColorScale()
Destructor.
QSharedPointer< ccColorScale > Shared
Shared pointer type.
Definition: ecvColorScale.h:74
QString m_uuid
Unique ID.
QString m_name
Name.
void clear()
Clears all steps.
static ccColorScale::Shared Create(const QString &name)
Creates a new color scale (with auto-generated unique id)
void sort()
Sort elements.
LabelSet m_customLabels
List of custom labels.
bool m_updated
Internal representation validity.
static const unsigned MAX_STEPS
Maximum number of steps (internal representation)
QMultiMap< unsigned, unsigned > LoadedIDMap
Map of loaded unique IDs (old ID --> new ID)
static bool ReadError()
Sends a custom error message (read error) and returns 'false'.
static bool WriteError()
Sends a custom error message (write error) and returns 'false'.
static bool MemoryError()
Sends a custom error message (not enough memory) and returns 'false'.
static const QString s_xmlColorScaleData("Data")
static const QString s_xmlColorScaleTitle("ColorScale")
static const QString s_xmlACloudViewer("ACloudViewer")
static const QString s_xmlCloudCompare("CloudCompare")
constexpr int s_xmlColorScaleVer
static const QString s_xmlColorScaleProperties("Properties")
unsigned char ColorCompType
Default color components type (R,G and B)
Definition: ecvColorTypes.h:29
const double * e
normal_z rgb
constexpr Rgb black(0, 0, 0)
constexpr Rgb white(MAX, MAX, MAX)
constexpr ColorCompType MAX
Max value of a single color component (default type)
Definition: ecvColorTypes.h:34
RgbaTpl< ColorCompType > Rgba
4 components, default type