QGIS API Documentation  3.21.0-Master (5b68dc587e)
qgscolorrampshaderwidget.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgscolorrampshaderwidget.cpp
3  ----------------------------
4  begin : Jun 2018
5  copyright : (C) 2018 by Peter Petrik
6  email : zilolv at gmail dot com
7  ***************************************************************************/
8 
9 /***************************************************************************
10  * *
11  * This program is free software; you can redistribute it and/or modify *
12  * it under the terms of the GNU General Public License as published by *
13  * the Free Software Foundation; either version 2 of the License, or *
14  * (at your option) any later version. *
15  * *
16  ***************************************************************************/
17 
18 #include "qgsrasterdataprovider.h"
19 
22 #include "qgsrasterlayer.h"
23 #include "qgsrasterdataprovider.h"
24 #include "qgsrastershader.h"
25 #include "qgsrasterminmaxwidget.h"
26 #include "qgstreewidgetitem.h"
27 #include "qgssettings.h"
28 #include "qgsstyle.h"
29 #include "qgscolorramp.h"
30 #include "qgscolorrampbutton.h"
31 #include "qgscolordialog.h"
32 #include "qgsrasterrendererutils.h"
33 #include "qgsfileutils.h"
34 #include "qgsguiutils.h"
35 #include "qgsdoublevalidator.h"
38 
39 #include <QCursor>
40 #include <QPushButton>
41 #include <QInputDialog>
42 #include <QFileDialog>
43 #include <QMenu>
44 #include <QMessageBox>
45 #include <QTextStream>
46 #include <QTreeView>
47 
48 
50  : QWidget( parent )
51 {
52  QgsSettings settings;
53 
54  setupUi( this );
55  mLoadFromBandButton->setVisible( false ); // only for raster version
56 
57  connect( mAddEntryButton, &QPushButton::clicked, this, &QgsColorRampShaderWidget::mAddEntryButton_clicked );
58  connect( mDeleteEntryButton, &QPushButton::clicked, this, &QgsColorRampShaderWidget::mDeleteEntryButton_clicked );
59  connect( mLoadFromBandButton, &QPushButton::clicked, this, &QgsColorRampShaderWidget::mLoadFromBandButton_clicked );
60  connect( mLoadFromFileButton, &QPushButton::clicked, this, &QgsColorRampShaderWidget::mLoadFromFileButton_clicked );
61  connect( mExportToFileButton, &QPushButton::clicked, this, &QgsColorRampShaderWidget::mExportToFileButton_clicked );
62  connect( mUnitLineEdit, &QLineEdit::textEdited, this, &QgsColorRampShaderWidget::mUnitLineEdit_textEdited );
63  connect( mColormapTreeWidget, &QTreeWidget::itemDoubleClicked, this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemDoubleClicked );
64  connect( mColorInterpolationComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &QgsColorRampShaderWidget::mColorInterpolationComboBox_currentIndexChanged );
65  connect( mClassificationModeComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &QgsColorRampShaderWidget::mClassificationModeComboBox_currentIndexChanged );
66 
67  connect( mLegendSettingsButton, &QPushButton::clicked, this, &QgsColorRampShaderWidget::showLegendSettings );
68 
69  contextMenu = new QMenu( tr( "Options" ), this );
70  contextMenu->addAction( tr( "Change Color…" ), this, SLOT( changeColor() ) );
71  contextMenu->addAction( tr( "Change Opacity…" ), this, SLOT( changeOpacity() ) );
72 
73  mColormapTreeWidget->setItemDelegateForColumn( ColorColumn, new QgsColorSwatchDelegate( this ) );
74  mValueDelegate = new QgsLocaleAwareNumericLineEditDelegate( Qgis::DataType::UnknownDataType, this );
75  mColormapTreeWidget->setItemDelegateForColumn( ValueColumn, mValueDelegate );
76 
77  mColormapTreeWidget->setColumnWidth( ColorColumn, Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 6.6 );
78 
79  mColormapTreeWidget->setContextMenuPolicy( Qt::CustomContextMenu );
80  mColormapTreeWidget->setSelectionMode( QAbstractItemView::ExtendedSelection );
81  connect( mColormapTreeWidget, &QTreeView::customContextMenuRequested, this, [ = ]( QPoint ) { contextMenu->exec( QCursor::pos() ); } );
82 
83  QString defaultPalette = settings.value( QStringLiteral( "Raster/defaultPalette" ), "" ).toString();
84  btnColorRamp->setColorRampFromName( defaultPalette );
85 
86  mColorInterpolationComboBox->addItem( tr( "Discrete" ), QgsColorRampShader::Discrete );
87  mColorInterpolationComboBox->addItem( tr( "Linear" ), QgsColorRampShader::Interpolated );
88  mColorInterpolationComboBox->addItem( tr( "Exact" ), QgsColorRampShader::Exact );
89  mColorInterpolationComboBox->setCurrentIndex( mColorInterpolationComboBox->findData( QgsColorRampShader::Interpolated ) );
90 
91  mClassificationModeComboBox->addItem( tr( "Continuous" ), QgsColorRampShader::Continuous );
92  mClassificationModeComboBox->addItem( tr( "Equal Interval" ), QgsColorRampShader::EqualInterval );
93  // Quantile added only on demand
94  mClassificationModeComboBox->setCurrentIndex( mClassificationModeComboBox->findData( QgsColorRampShader::Continuous ) );
95 
96  mNumberOfEntriesSpinBox->setValue( 5 ); // some default
97 
98  mClassificationModeComboBox_currentIndexChanged( 0 );
99 
100  resetClassifyButton();
101 
102  connect( mClassificationModeComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &QgsColorRampShaderWidget::classify );
103  connect( mColorInterpolationComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &QgsColorRampShaderWidget::classify );
104  connect( mClassifyButton, &QPushButton::clicked, this, &QgsColorRampShaderWidget::classify );
105  connect( btnColorRamp, &QgsColorRampButton::colorRampChanged, this, &QgsColorRampShaderWidget::applyColorRamp );
106  connect( mNumberOfEntriesSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsColorRampShaderWidget::classify );
107  connect( mClipCheckBox, &QAbstractButton::toggled, this, &QgsColorRampShaderWidget::widgetChanged );
108  connect( mLabelPrecisionSpinBox, qOverload<int>( &QSpinBox::valueChanged ), this, [ = ]( int )
109  {
110  autoLabel();
111  } );
112 }
113 
115 {
116  Q_ASSERT( mClassificationModeComboBox->findData( QgsColorRampShader::Quantile < 0 ) );
117  mClassificationModeComboBox->addItem( tr( "Quantile" ), QgsColorRampShader::Quantile );
118 }
119 
121 {
122  mRasterDataProvider = dp;
123  mLoadFromBandButton->setVisible( bool( mRasterDataProvider ) ); // only for raster version
124 }
125 
127 {
128  mBand = band;
129  // Assume double by default
130  Qgis::DataType dataType { ( mRasterDataProvider &&mBand > 0 ) ? mRasterDataProvider->dataType( mBand ) : Qgis::DataType::Float64 };
131 
132  // Set the maximum number of digits in the precision spin box
133  const int maxDigits { QgsGuiUtils::significantDigits( dataType ) };
134  mLabelPrecisionSpinBox->setMaximum( maxDigits );
135  mValueDelegate->setDataType( dataType );
136 }
137 
139 {
140  mExtent = extent;
141 }
142 
144 {
145  QgsColorRampShader colorRampShader( mMin, mMax );
146  colorRampShader.setLabelPrecision( mLabelPrecisionSpinBox->value() );
147  colorRampShader.setColorRampType( static_cast< QgsColorRampShader::Type >( mColorInterpolationComboBox->currentData().toInt() ) );
148  colorRampShader.setClassificationMode( static_cast< QgsColorRampShader::ClassificationMode >( mClassificationModeComboBox->currentData().toInt() ) );
149  colorRampShader.setClip( mClipCheckBox->isChecked() );
150 
151  //iterate through mColormapTreeWidget and set colormap info of layer
152  QList<QgsColorRampShader::ColorRampItem> colorRampItems;
153  int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
154  QTreeWidgetItem *currentItem = nullptr;
155  for ( int i = 0; i < topLevelItemCount; ++i )
156  {
157  currentItem = mColormapTreeWidget->topLevelItem( i );
158  if ( !currentItem )
159  {
160  continue;
161  }
162  QgsColorRampShader::ColorRampItem newColorRampItem;
163  newColorRampItem.value = currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble();
164  newColorRampItem.color = currentItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
165  newColorRampItem.label = currentItem->text( LabelColumn );
166  colorRampItems.append( newColorRampItem );
167  }
168  // sort the shader items
169  std::sort( colorRampItems.begin(), colorRampItems.end() );
170  colorRampShader.setColorRampItemList( colorRampItems );
171 
172  if ( !btnColorRamp->isNull() )
173  {
174  colorRampShader.setSourceColorRamp( btnColorRamp->colorRamp() );
175  }
176 
177  colorRampShader.setLegendSettings( new QgsColorRampLegendNodeSettings( mLegendSettings ) );
178  return colorRampShader;
179 }
180 
181 void QgsColorRampShaderWidget::autoLabel()
182 {
183 
184  mColormapTreeWidget->sortItems( ValueColumn, Qt::AscendingOrder );
185 
186 #ifdef QGISDEBUG
187  dumpClasses();
188 #endif
189 
190  const QString unit = mUnitLineEdit->text();
191  int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
192 
193  QTreeWidgetItem *currentItem = nullptr;
194  for ( int i = 0; i < topLevelItemCount; ++i )
195  {
196  currentItem = mColormapTreeWidget->topLevelItem( i );
197  //If the item is null or does not have a pixel values set, skip
198  if ( !currentItem || currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString().isEmpty() )
199  {
200  continue;
201  }
202 
203  const QString lbl = createLabel( currentItem, i, unit );
204 
205  if ( currentItem->text( LabelColumn ).isEmpty() || currentItem->text( LabelColumn ) == lbl || currentItem->foreground( LabelColumn ).color() == QColor( Qt::gray ) )
206  {
207  currentItem->setText( LabelColumn, lbl );
208  currentItem->setForeground( LabelColumn, QBrush( QColor( Qt::gray ) ) );
209  }
210  }
211 
212 }
213 
214 void QgsColorRampShaderWidget::setUnitFromLabels()
215 {
216  QStringList allSuffixes;
217  QString label;
218  int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
219  QTreeWidgetItem *currentItem = nullptr;
220  for ( int i = 0; i < topLevelItemCount; ++i )
221  {
222  currentItem = mColormapTreeWidget->topLevelItem( i );
223  //If the item is null or does not have a pixel values set, skip
224  if ( !currentItem || currentItem->text( ValueColumn ).isEmpty() )
225  {
226  continue;
227  }
228 
229  label = createLabel( currentItem, i, QString() );
230 
231  if ( currentItem->text( LabelColumn ).startsWith( label ) )
232  {
233  allSuffixes.append( currentItem->text( LabelColumn ).mid( label.length() ) );
234  }
235  }
236  // find most common suffix
237  QStringList suffixes = QStringList( allSuffixes );
238  suffixes.removeDuplicates();
239  int max = 0;
240  QString unit;
241  for ( int i = 0; i < suffixes.count(); ++i )
242  {
243  int n = allSuffixes.count( suffixes[i] );
244  if ( n > max )
245  {
246  max = n;
247  unit = suffixes[i];
248  }
249  }
250  // Set this suffix as unit if at least used twice
251  if ( max >= 2 )
252  {
253  mUnitLineEdit->setText( unit );
254  }
255 }
256 
257 #ifdef QGISDEBUG
258 void QgsColorRampShaderWidget::dumpClasses()
259 {
260  for ( int row = 0; row < mColormapTreeWidget->model()->rowCount(); ++row )
261  {
262  const auto labelData { mColormapTreeWidget->model()->itemData( mColormapTreeWidget->model()->index( row, LabelColumn ) ) };
263  const auto valueData { mColormapTreeWidget->model()->itemData( mColormapTreeWidget->model()->index( row, ValueColumn ) ) };
264  QgsDebugMsgLevel( QStringLiteral( "Class %1 : %2 %3" ).arg( row )
265  .arg( labelData[ Qt::ItemDataRole::DisplayRole ].toString(),
266  valueData[ Qt::ItemDataRole::DisplayRole ].toString() ), 2 );
267  }
268 }
269 #endif
270 
271 void QgsColorRampShaderWidget::mAddEntryButton_clicked()
272 {
273  QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget );
274  newItem->setData( ValueColumn, Qt::ItemDataRole::DisplayRole, 0 );
275  newItem->setData( ColorColumn, Qt::ItemDataRole::EditRole, QColor( Qt::magenta ) );
276  newItem->setText( LabelColumn, QString() );
277  newItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable );
278  connect( newItem, &QgsTreeWidgetItemObject::itemEdited,
279  this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited );
280  autoLabel();
281 
283  updateColorRamp();
284  emit widgetChanged();
285 }
286 
287 void QgsColorRampShaderWidget::mDeleteEntryButton_clicked()
288 {
289  QList<QTreeWidgetItem *> itemList;
290  itemList = mColormapTreeWidget->selectedItems();
291  if ( itemList.isEmpty() )
292  {
293  return;
294  }
295 
296  const auto constItemList = itemList;
297  for ( QTreeWidgetItem *item : constItemList )
298  {
299  delete item;
300  }
301 
303  updateColorRamp();
304  emit widgetChanged();
305 }
306 
308 {
309  std::unique_ptr< QgsColorRamp > ramp( btnColorRamp->colorRamp() );
310  if ( !ramp || std::isnan( mMin ) || std::isnan( mMax ) )
311  {
312  return;
313  }
314 
315  std::unique_ptr< QgsColorRampShader > colorRampShader( new QgsColorRampShader(
316  mMin, mMax,
317  ramp.release(),
318  static_cast< QgsColorRampShader::Type >( mColorInterpolationComboBox->currentData().toInt() ),
319  static_cast< QgsColorRampShader::ClassificationMode >( mClassificationModeComboBox->currentData().toInt() ) )
320  );
321 
322  // only for Quantile we need band and provider and extent
323  colorRampShader->classifyColorRamp( mNumberOfEntriesSpinBox->value(),
324  mBand,
325  mExtent,
326  mRasterDataProvider );
327  colorRampShader->setClip( mClipCheckBox->isChecked() );
328 
329  mColormapTreeWidget->clear();
330 
331  const QList<QgsColorRampShader::ColorRampItem> colorRampItemList = colorRampShader->colorRampItemList();
332  QList<QgsColorRampShader::ColorRampItem>::const_iterator it = colorRampItemList.constBegin();
333  for ( ; it != colorRampItemList.end(); ++it )
334  {
335  QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget );
336  newItem->setData( ValueColumn, Qt::ItemDataRole::DisplayRole, it->value );
337  newItem->setData( ColorColumn, Qt::ItemDataRole::EditRole, it->color );
338  newItem->setText( LabelColumn, QString() ); // Labels will be populated in autoLabel()
339  newItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable );
340  connect( newItem, &QgsTreeWidgetItemObject::itemEdited,
341  this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited );
342  }
343 
344  mClipCheckBox->setChecked( colorRampShader->clip() );
345 
346  autoLabel();
347  emit widgetChanged();
348 }
349 
350 void QgsColorRampShaderWidget::mClassificationModeComboBox_currentIndexChanged( int index )
351 {
352  QgsColorRampShader::ClassificationMode mode = static_cast< QgsColorRampShader::ClassificationMode >( mClassificationModeComboBox->itemData( index ).toInt() );
353  mNumberOfEntriesSpinBox->setEnabled( mode != QgsColorRampShader::Continuous );
354  emit classificationModeChanged( mode );
355 }
356 
357 void QgsColorRampShaderWidget::updateColorRamp()
358 {
359  std::unique_ptr< QgsColorRamp > ramp( shader().createColorRamp() );
360  whileBlocking( btnColorRamp )->setColorRamp( ramp.get() );
361 }
362 
363 void QgsColorRampShaderWidget::applyColorRamp()
364 {
365  std::unique_ptr< QgsColorRamp > ramp( btnColorRamp->colorRamp() );
366  if ( !ramp )
367  {
368  return;
369  }
370 
371  if ( !btnColorRamp->colorRampName().isEmpty() )
372  {
373  // Remember last used color ramp
374  QgsSettings settings;
375  settings.setValue( QStringLiteral( "Raster/defaultPalette" ), btnColorRamp->colorRampName() );
376  }
377 
378  bool enableContinuous = ( ramp->count() > 0 );
379  mClassificationModeComboBox->setEnabled( enableContinuous );
380  if ( !enableContinuous )
381  {
382  mClassificationModeComboBox->setCurrentIndex( mClassificationModeComboBox->findData( QgsColorRampShader::EqualInterval ) );
383  }
384 
385  int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
386  if ( topLevelItemCount > 0 )
387  {
388  // We need to have valid min/max values here. If we haven't, load from colormap
389  double min, max;
390  if ( std::isnan( mMin ) || std::isnan( mMax ) )
391  {
392  colormapMinMax( min, max );
393  }
394  else
395  {
396  min = mMin;
397  max = mMax;
398  }
399 
400  // if the list values has been customized, maintain pre-existing values
401  QTreeWidgetItem *currentItem = nullptr;
402  for ( int i = 0; i < topLevelItemCount; ++i )
403  {
404  currentItem = mColormapTreeWidget->topLevelItem( i );
405  if ( !currentItem )
406  {
407  continue;
408  }
409 
410  double value = currentItem->data( ValueColumn, Qt::ItemDataRole::EditRole ).toDouble( );
411  double position = ( value - min ) / ( max - min );
412  whileBlocking( static_cast<QgsTreeWidgetItemObject *>( currentItem ) )->setData( ColorColumn, Qt::ItemDataRole::EditRole, ramp->color( position ) );
413  }
414 
415  emit widgetChanged();
416  }
417  else
418  {
419  classify();
420  }
421 }
422 
423 void QgsColorRampShaderWidget::populateColormapTreeWidget( const QList<QgsColorRampShader::ColorRampItem> &colorRampItems )
424 {
425  mColormapTreeWidget->clear();
426  QList<QgsColorRampShader::ColorRampItem>::const_iterator it = colorRampItems.constBegin();
427  int i = 0;
428  for ( ; it != colorRampItems.constEnd(); ++it )
429  {
430  QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget );
431  newItem->setData( ValueColumn, Qt::ItemDataRole::DisplayRole, it->value );
432  newItem->setData( ColorColumn, Qt::ItemDataRole::EditRole, it->color );
433  newItem->setText( LabelColumn, it->label );
434  newItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable );
435  connect( newItem, &QgsTreeWidgetItemObject::itemEdited,
436  this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited );
437  ++i;
438  }
439 
440 #ifdef QGISDEBUG
441  dumpClasses();
442 #endif
443 
444  setUnitFromLabels();
445 
446  // Now we have the suffix
447  const QString unit = mUnitLineEdit->text();
448  for ( i = 0; i < mColormapTreeWidget->topLevelItemCount(); i++ )
449  {
450  QgsTreeWidgetItemObject *currentItem { static_cast<QgsTreeWidgetItemObject *>( mColormapTreeWidget->topLevelItem( i ) ) };
451  QString lbl { createLabel( currentItem, i, unit )};
452  if ( currentItem->text( LabelColumn ).isEmpty() || currentItem->text( LabelColumn ) == lbl || currentItem->foreground( LabelColumn ).color() == QColor( Qt::gray ) )
453  {
454  currentItem->setText( LabelColumn, lbl );
455  currentItem->setForeground( LabelColumn, QBrush( QColor( Qt::gray ) ) );
456  }
457  }
458 
459 }
460 
461 void QgsColorRampShaderWidget::mLoadFromBandButton_clicked()
462 {
463  if ( !mRasterDataProvider )
464  return;
465 
466  QList<QgsColorRampShader::ColorRampItem> colorRampList = mRasterDataProvider->colorTable( mBand );
467  if ( !colorRampList.isEmpty() )
468  {
469  populateColormapTreeWidget( colorRampList );
470  mColorInterpolationComboBox->setCurrentIndex( mColorInterpolationComboBox->findData( QgsColorRampShader::Interpolated ) );
471  }
472  else
473  {
474  QMessageBox::warning( this, tr( "Load Color Map" ), tr( "The color map for band %1 has no entries." ).arg( mBand ) );
475  }
477  emit widgetChanged();
478 }
479 
480 void QgsColorRampShaderWidget::mLoadFromFileButton_clicked()
481 {
482  QgsSettings settings;
483  QString lastDir = settings.value( QStringLiteral( "lastColorMapDir" ), QDir::homePath() ).toString();
484  const QString fileName = QFileDialog::getOpenFileName( this, tr( "Load Color Map from File" ), lastDir, tr( "Textfile (*.txt)" ) );
485  if ( fileName.isEmpty() )
486  return;
487 
488  QList<QgsColorRampShader::ColorRampItem> colorRampItems;
490  QStringList errors;
491  if ( QgsRasterRendererUtils::parseColorMapFile( fileName, colorRampItems, type, errors ) )
492  {
493  //clear the current tree
494  mColormapTreeWidget->clear();
495 
496  mColorInterpolationComboBox->setCurrentIndex( mColorInterpolationComboBox->findData( type ) );
497 
498  populateColormapTreeWidget( colorRampItems );
499 
500  if ( !errors.empty() )
501  {
502  QMessageBox::warning( this, tr( "Load Color Map from File" ), tr( "The following lines contained errors\n\n" ) + errors.join( '\n' ) );
503  }
504  }
505  else
506  {
507  const QString error = tr( "An error occurred while reading the color map\n\n" ) + errors.join( '\n' );
508  QMessageBox::warning( this, tr( "Load Color Map from File" ), error );
509  }
510 
511  QFileInfo fileInfo( fileName );
512  settings.setValue( QStringLiteral( "lastColorMapDir" ), fileInfo.absoluteDir().absolutePath() );
513 
515  updateColorRamp();
516  emit widgetChanged();
517 }
518 
519 void QgsColorRampShaderWidget::mExportToFileButton_clicked()
520 {
521  QgsSettings settings;
522  QString lastDir = settings.value( QStringLiteral( "lastColorMapDir" ), QDir::homePath() ).toString();
523  QString fileName = QFileDialog::getSaveFileName( this, tr( "Save Color Map as File" ), lastDir, tr( "Textfile (*.txt)" ) );
524  if ( fileName.isEmpty() )
525  return;
526 
527  fileName = QgsFileUtils::ensureFileNameHasExtension( fileName, QStringList() << QStringLiteral( "txt" ) );
528 
529  QList<QgsColorRampShader::ColorRampItem> colorRampItems;
530  int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
531  for ( int i = 0; i < topLevelItemCount; ++i )
532  {
533  QTreeWidgetItem *currentItem = mColormapTreeWidget->topLevelItem( i );
534  if ( !currentItem )
535  {
536  continue;
537  }
538 
540  item.value = currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble( );
541  item.color = currentItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
542  item.label = currentItem->text( LabelColumn );
543  colorRampItems << item;
544  }
545 
546  if ( !QgsRasterRendererUtils::saveColorMapFile( fileName, colorRampItems, static_cast< QgsColorRampShader::Type >( mColorInterpolationComboBox->currentData().toInt() ) ) )
547  {
548  QMessageBox::warning( this, tr( "Save Color Map as File" ), tr( "Write access denied. Adjust the file permissions and try again.\n\n" ) );
549  }
550 
551  QFileInfo fileInfo( fileName );
552  settings.setValue( QStringLiteral( "lastColorMapDir" ), fileInfo.absoluteDir().absolutePath() );
553 }
554 
555 void QgsColorRampShaderWidget::mColormapTreeWidget_itemDoubleClicked( QTreeWidgetItem *item, int column )
556 {
557  if ( !item )
558  {
559  return;
560  }
561 
562  if ( column == LabelColumn )
563  {
564  // Set text color to default black, which signifies a manually edited label
565  item->setForeground( LabelColumn, QBrush() );
566  }
567 }
568 
569 void QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited( QTreeWidgetItem *item, int column )
570 {
571  Q_UNUSED( item )
572 
573  switch ( column )
574  {
575  case ValueColumn:
576  {
577  autoLabel();
579  updateColorRamp();
580  emit widgetChanged();
581  break;
582  }
583 
584  case LabelColumn:
585  {
586  // call autoLabel to fill when empty or gray out when same as autoLabel
587  autoLabel();
588  emit widgetChanged();
589  break;
590  }
591 
592  case ColorColumn:
593  {
595  updateColorRamp();
596  emit widgetChanged();
597  break;
598  }
599  }
600 }
601 
603 {
604  // Those objects are connected to classify() the color ramp shader if they change, or call widget change
605  // need to block them to avoid to classify and to alter the color ramp, or to call duplicate widget change
606  whileBlocking( mClipCheckBox )->setChecked( colorRampShader.clip() );
607  whileBlocking( mColorInterpolationComboBox )->setCurrentIndex( mColorInterpolationComboBox->findData( colorRampShader.colorRampType() ) );
608  mColorInterpolationComboBox_currentIndexChanged( mColorInterpolationComboBox->currentIndex() );
609  whileBlocking( mClassificationModeComboBox )->setCurrentIndex( mClassificationModeComboBox->findData( colorRampShader.classificationMode() ) );
610  mClassificationModeComboBox_currentIndexChanged( mClassificationModeComboBox->currentIndex() );
611  whileBlocking( mNumberOfEntriesSpinBox )->setValue( colorRampShader.colorRampItemList().count() ); // some default
612 
613  if ( colorRampShader.sourceColorRamp() )
614  {
615  whileBlocking( btnColorRamp )->setColorRamp( colorRampShader.sourceColorRamp() );
616  }
617  else
618  {
619  QgsSettings settings;
620  QString defaultPalette = settings.value( QStringLiteral( "/Raster/defaultPalette" ), "Spectral" ).toString();
621  btnColorRamp->setColorRampFromName( defaultPalette );
622  }
623 
624  mLabelPrecisionSpinBox->setValue( colorRampShader.labelPrecision() );
625 
626  populateColormapTreeWidget( colorRampShader.colorRampItemList() );
627 
628  if ( colorRampShader.legendSettings() )
629  mLegendSettings = *colorRampShader.legendSettings();
630 
631  emit widgetChanged();
632 }
633 
634 void QgsColorRampShaderWidget::mColorInterpolationComboBox_currentIndexChanged( int index )
635 {
636  QgsColorRampShader::Type interpolation = static_cast< QgsColorRampShader::Type >( mColorInterpolationComboBox->itemData( index ).toInt() );
637 
638  mClipCheckBox->setEnabled( interpolation == QgsColorRampShader::Interpolated );
639 
640  QString valueLabel;
641  QString valueToolTip;
642  switch ( interpolation )
643  {
645  valueLabel = tr( "Value" );
646  valueToolTip = tr( "Value for color stop" );
647  mLegendSettingsButton->setEnabled( true );
648  break;
650  valueLabel = tr( "Value <=" );
651  valueToolTip = tr( "Maximum value for class" );
652  mLegendSettingsButton->setEnabled( false );
653  break;
655  valueLabel = tr( "Value =" );
656  valueToolTip = tr( "Value for color" );
657  mLegendSettingsButton->setEnabled( false );
658  break;
659  }
660 
661  QTreeWidgetItem *header = mColormapTreeWidget->headerItem();
662  header->setText( ValueColumn, valueLabel );
663  header->setToolTip( ValueColumn, valueToolTip );
664 
665  autoLabel();
666  emit widgetChanged();
667 }
668 
670 {
671  if ( !qgsDoubleNear( mMin, min ) || !qgsDoubleNear( mMax, max ) )
672  {
673  setMinimumMaximum( min, max );
674  classify();
675  }
676 }
677 
678 void QgsColorRampShaderWidget::setMinimumMaximum( double min, double max )
679 {
680  mMin = min;
681  mMax = max;
682  resetClassifyButton();
683 }
684 
686 {
687  return mMin;
688 }
689 
691 {
692  return mMax;
693 }
694 
695 bool QgsColorRampShaderWidget::colormapMinMax( double &min, double &max ) const
696 {
697  QTreeWidgetItem *item = mColormapTreeWidget->topLevelItem( 0 );
698  if ( !item )
699  {
700  return false;
701  }
702 
703  // If using discrete, the first and last items contain the upper and lower
704  // values of the first and last classes, we don't want these values but real min/max
705  if ( ! std::isnan( mMin ) && ! std::isnan( mMax ) && static_cast< QgsColorRampShader::Type >( mColorInterpolationComboBox->currentData().toInt() ) == QgsColorRampShader::Type::Discrete )
706  {
707  min = mMin;
708  max = mMax;
709  }
710  else
711  {
712  min = item->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble();
713  item = mColormapTreeWidget->topLevelItem( mColormapTreeWidget->topLevelItemCount() - 1 );
714  max = item->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble();
715  }
716  return true;
717 }
718 
720 {
721  double min = 0, max = 0;
722  if ( ! colormapMinMax( min, max ) )
723  {
724  return;
725  }
726 
727  if ( !qgsDoubleNear( mMin, min ) || !qgsDoubleNear( mMax, max ) )
728  {
729  mMin = min;
730  mMax = max;
731  emit minimumMaximumChangedFromTree( min, max );
732  }
733 }
734 
735 void QgsColorRampShaderWidget::resetClassifyButton()
736 {
737  mClassifyButton->setEnabled( true );
738  if ( std::isnan( mMin ) || std::isnan( mMax ) || mMin >= mMax )
739  {
740  mClassifyButton->setEnabled( false );
741  }
742 }
743 
744 QString QgsColorRampShaderWidget::createLabel( QTreeWidgetItem *currentItem, int row, const QString unit )
745 {
746  auto applyPrecision = [ = ]( const QString & value )
747  {
748  double val { value.toDouble( ) };
749  Qgis::DataType dataType { mRasterDataProvider ? mRasterDataProvider->dataType( mBand ) : Qgis::DataType::Float64 };
750  switch ( dataType )
751  {
761  {
762  return QLocale().toString( std::round( val ), 'f', 0 );
763  }
766  {
767  if ( mLabelPrecisionSpinBox->value() < 0 )
768  {
769  const double factor = std::pow( 10, - mLabelPrecisionSpinBox->value() );
770  val = static_cast<qlonglong>( val / factor ) * factor;
771  return QLocale().toString( val, 'f', 0 );
772  }
773  return QLocale().toString( val, 'f', mLabelPrecisionSpinBox->value() );
774  }
778  {
779  if ( mLabelPrecisionSpinBox->value() < 0 )
780  {
781  const double factor = std::pow( 10, - mLabelPrecisionSpinBox->value() );
782  val = static_cast<qlonglong>( val / factor ) * factor;
783  return QLocale().toString( val, 'f', 0 );
784  }
785  return QLocale().toString( val, 'f', mLabelPrecisionSpinBox->value() );
786  }
787  }
788  return QString();
789  };
790 
791  QgsColorRampShader::Type interpolation = static_cast< QgsColorRampShader::Type >( mColorInterpolationComboBox->currentData().toInt() );
792  bool discrete = interpolation == QgsColorRampShader::Discrete;
793  QString lbl;
794 
795  if ( discrete )
796  {
797  if ( row == 0 )
798  {
799  lbl = "<= " + applyPrecision( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
800  }
801  else if ( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble( ) == std::numeric_limits<double>::infinity() )
802  {
803  lbl = "> " + applyPrecision( mColormapTreeWidget->topLevelItem( row - 1 )->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
804  }
805  else
806  {
807  lbl = applyPrecision( mColormapTreeWidget->topLevelItem( row - 1 )->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + " - " + applyPrecision( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
808  }
809  }
810  else
811  {
812  lbl = applyPrecision( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
813  }
814 
815  return lbl;
816 
817 }
818 
819 void QgsColorRampShaderWidget::changeColor()
820 {
821  QList<QTreeWidgetItem *> itemList;
822  itemList = mColormapTreeWidget->selectedItems();
823  if ( itemList.isEmpty() )
824  {
825  return;
826  }
827  QTreeWidgetItem *firstItem = itemList.first();
828 
829  QColor currentColor = firstItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
830  QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( qobject_cast< QWidget * >( parent() ) );
831  if ( panel && panel->dockMode() )
832  {
834  colorWidget->setPanelTitle( tr( "Select Color" ) );
835  colorWidget->setAllowOpacity( true );
836  connect( colorWidget, &QgsCompoundColorWidget::currentColorChanged, this, [ = ]( const QColor & newColor )
837  {
838  for ( QTreeWidgetItem *item : std::as_const( itemList ) )
839  {
840  item->setData( ColorColumn, Qt::ItemDataRole::EditRole, newColor );
841  }
842 
844  emit widgetChanged();
845  } );
846  panel->openPanel( colorWidget );
847  }
848  else
849  {
850  // modal dialog version... yuck
851  QColor newColor = QgsColorDialog::getColor( currentColor, this, QStringLiteral( "Change Color" ), true );
852  if ( newColor.isValid() )
853  {
854  for ( QTreeWidgetItem *item : std::as_const( itemList ) )
855  {
856  item->setData( ColorColumn, Qt::ItemDataRole::EditRole, newColor );
857  }
858 
860  emit widgetChanged();
861  }
862  }
863 }
864 
865 void QgsColorRampShaderWidget::changeOpacity()
866 {
867  QList<QTreeWidgetItem *> itemList;
868  itemList = mColormapTreeWidget->selectedItems();
869  if ( itemList.isEmpty() )
870  {
871  return;
872  }
873  QTreeWidgetItem *firstItem = itemList.first();
874 
875  bool ok;
876  double oldOpacity = firstItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>().alpha() / 255 * 100;
877  double opacity = QInputDialog::getDouble( this, tr( "Opacity" ), tr( "Change color opacity [%]" ), oldOpacity, 0.0, 100.0, 0, &ok );
878  if ( ok )
879  {
880  int newOpacity = static_cast<int>( opacity / 100 * 255 );
881  const auto constItemList = itemList;
882  for ( QTreeWidgetItem *item : constItemList )
883  {
884  QColor newColor = item->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
885  newColor.setAlpha( newOpacity );
886  item->setData( ColorColumn, Qt::ItemDataRole::EditRole, newColor );
887  }
888 
890  emit widgetChanged();
891  }
892 }
893 
894 void QgsColorRampShaderWidget::showLegendSettings()
895 {
896  QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( qobject_cast< QWidget * >( parent() ) );
897  if ( panel && panel->dockMode() )
898  {
900  legendPanel->setPanelTitle( tr( "Legend Settings" ) );
901  legendPanel->setSettings( mLegendSettings );
902  connect( legendPanel, &QgsColorRampLegendNodeWidget::widgetChanged, this, [ = ]
903  {
904  mLegendSettings = legendPanel->settings();
905  emit widgetChanged();
906  } );
907  panel->openPanel( legendPanel );
908  }
909  else
910  {
911  QgsColorRampLegendNodeDialog dialog( mLegendSettings, this );
912  dialog.setWindowTitle( tr( "Legend Settings" ) );
913  if ( dialog.exec() )
914  {
915  mLegendSettings = dialog.settings();
916  emit widgetChanged();
917  }
918  }
919 }
DataType
Raster data types.
Definition: qgis.h:120
@ CInt32
Complex Int32.
@ Float32
Thirty two bit floating point (float)
@ CFloat64
Complex Float64.
@ Int16
Sixteen bit signed integer (qint16)
@ ARGB32_Premultiplied
Color, alpha, red, green, blue, 4 bytes the same as QImage::Format_ARGB32_Premultiplied.
@ UInt16
Sixteen bit unsigned integer (quint16)
@ Byte
Eight bit unsigned integer (quint8)
@ UnknownDataType
Unknown or unspecified type.
@ ARGB32
Color, alpha, red, green, blue, 4 bytes the same as QImage::Format_ARGB32.
@ Int32
Thirty two bit signed integer (qint32)
@ Float64
Sixty four bit floating point (double)
@ CFloat32
Complex Float32.
@ CInt16
Complex Int16.
@ UInt32
Thirty two bit unsigned integer (quint32)
static const double UI_SCALE_FACTOR
UI scaling factor.
Definition: qgis.h:1040
static QColor getColor(const QColor &initialColor, QWidget *parent, const QString &title=QString(), bool allowOpacity=false)
Returns a color selection from a color dialog.
void colorRampChanged()
Emitted whenever a new color ramp is set for the button.
A dialog for configuring a QgsColorRampLegendNode (QgsColorRampLegendNodeSettings).
Settings for a color ramp legend node.
A widget for properties relating to a QgsColorRampLegendNode (QgsColorRampLegendNodeSettings).
QgsColorRampLegendNodeSettings settings() const
Returns the legend node settings as defined by the widget.
void setSettings(const QgsColorRampLegendNodeSettings &settings)
Sets the settings to show in the widget.
void setExtent(const QgsRectangle &extent)
Sets extent, only when used for raster layer.
void setMinimumMaximumAndClassify(double minimum, double maximum)
Sets min max and classify color tree.
void initializeForUseWithRasterLayer()
Allows quantile classification mode for raster layers.
void populateColormapTreeWidget(const QList< QgsColorRampShader::ColorRampItem > &colorRampItems)
Populates color ramp tree from ramp items.
QgsColorRampShaderWidget(QWidget *parent=nullptr)
Creates new color ramp shader widget.
void classificationModeChanged(QgsColorRampShader::ClassificationMode mode)
Classification mode changed.
void minimumMaximumChangedFromTree(double minimum, double maximum)
Color ramp tree has changed.
void classify()
Executes the single band pseudo raster classification.
void widgetChanged()
Widget changed.
double minimum() const
Gets min value.
void setFromShader(const QgsColorRampShader &colorRampShader)
Sets widget state from the color ramp shader.
double maximum() const
Gets max value.
void setMinimumMaximum(double minimum, double maximum)
Sets min max.
QgsColorRampShader shader() const
Returns shared function used in the renderer.
void setRasterBand(int band)
Sets raster band, only when used for raster layer.
void setRasterDataProvider(QgsRasterDataProvider *dp)
Associates raster with the widget, only when used for raster layer.
void loadMinimumMaximumFromTree()
Loads min and max values from color ramp tree.
A ramp shader will color a raster pixel based on a list of values ranges in a ramp.
QList< QgsColorRampShader::ColorRampItem > colorRampItemList() const
Returns the custom colormap.
ClassificationMode classificationMode() const
Returns the classification mode.
const QgsColorRampLegendNodeSettings * legendSettings() const
Returns the color ramp shader legend settings.
Type colorRampType() const
Returns the color ramp type.
void setSourceColorRamp(QgsColorRamp *colorramp)
Set the source color ramp.
ClassificationMode
Classification modes used to create the color ramp shader.
@ EqualInterval
Uses equal interval.
@ Quantile
Uses quantile (i.e. equal pixel) count.
@ Continuous
Uses breaks from color palette.
void setClip(bool clip)
Sets whether the shader should not render values out of range.
bool clip() const
Returns whether the shader will clip values which are out of range.
QgsColorRamp * sourceColorRamp() const
Returns the source color ramp.
Type
Supported methods for color interpolation.
@ Interpolated
Interpolates the color between two class breaks linearly.
@ Discrete
Assigns the color of the higher class for every pixel between two class breaks.
@ Exact
Assigns the color of the exact matching value in the color ramp item list.
void setClassificationMode(ClassificationMode classificationMode)
Sets classification mode.
void setColorRampItemList(const QList< QgsColorRampShader::ColorRampItem > &list)
Sets a custom colormap.
void setColorRampType(QgsColorRampShader::Type colorRampType)
Sets the color ramp type.
void setLegendSettings(QgsColorRampLegendNodeSettings *settings)
Sets the color ramp shader legend settings.
A delegate for showing a color swatch in a list.
A custom QGIS widget for selecting a color, including options for selecting colors via hue wheel,...
@ LayoutVertical
Use a narrower, vertically stacked layout.
void currentColorChanged(const QColor &color)
Emitted when the dialog's color changes.
void setAllowOpacity(bool allowOpacity)
Sets whether opacity modification (transparency) is permitted for the color dialog.
static QString ensureFileNameHasExtension(const QString &fileName, const QStringList &extensions)
Ensures that a fileName ends with an extension from the provided list of extensions.
Base class for any widget that can be shown as a inline panel.
void openPanel(QgsPanelWidget *panel)
Open a panel or dialog depending on dock mode setting If dock mode is true this method will emit the ...
void widgetChanged()
Emitted when the widget state changes.
static QgsPanelWidget * findParentPanel(QWidget *widget)
Traces through the parents of a widget to find if it is contained within a QgsPanelWidget widget.
void setPanelTitle(const QString &panelTitle)
Set the title of the panel when shown in the interface.
bool dockMode()
Returns the dock mode state.
Base class for raster data providers.
virtual QList< QgsColorRampShader::ColorRampItem > colorTable(int bandNo) const
Qgis::DataType dataType(int bandNo) const override=0
Returns data type for the band specified by number.
static bool parseColorMapFile(const QString &path, QList< QgsColorRampShader::ColorRampItem > &items, QgsColorRampShader::Type &type, QStringList &errors)
Parses an exported color map file at the specified path and extracts the stored color ramp items and ...
static bool saveColorMapFile(const QString &path, const QList< QgsColorRampShader::ColorRampItem > &items, QgsColorRampShader::Type type)
Exports a list of color ramp items and ramp shader type to a color map file at the specified path.
void setLabelPrecision(int labelPrecision)
Sets label precision to labelPrecision.
int labelPrecision() const
Returns label precision.
A rectangle specified with double values.
Definition: qgsrectangle.h:42
Custom QgsTreeWidgetItem with extra signals when item is edited.
void itemEdited(QTreeWidgetItem *item, int column)
Emitted when the contents of the column in the specified item has been edited by the user.
void setData(int column, int role, const QVariant &value) override
Sets the value for the item's column and role to the given value.
int significantDigits(const Qgis::DataType rasterDataType)
Returns the maximum number of significant digits a for the given rasterDataType.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:1234
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition: qgis.h:1173
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39