QGIS API Documentation  3.18.1-Zürich (202f1bf7e5)
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 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
78  mColormapTreeWidget->setColumnWidth( ColorColumn, Qgis::UI_SCALE_FACTOR * fontMetrics().width( 'X' ) * 6.6 );
79 #else
80  mColormapTreeWidget->setColumnWidth( ColorColumn, Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 6.6 );
81 #endif
82 
83  mColormapTreeWidget->setContextMenuPolicy( Qt::CustomContextMenu );
84  mColormapTreeWidget->setSelectionMode( QAbstractItemView::ExtendedSelection );
85  connect( mColormapTreeWidget, &QTreeView::customContextMenuRequested, this, [ = ]( QPoint ) { contextMenu->exec( QCursor::pos() ); } );
86 
87  QString defaultPalette = settings.value( QStringLiteral( "Raster/defaultPalette" ), "" ).toString();
88  btnColorRamp->setColorRampFromName( defaultPalette );
89 
90  mColorInterpolationComboBox->addItem( tr( "Discrete" ), QgsColorRampShader::Discrete );
91  mColorInterpolationComboBox->addItem( tr( "Linear" ), QgsColorRampShader::Interpolated );
92  mColorInterpolationComboBox->addItem( tr( "Exact" ), QgsColorRampShader::Exact );
93  mColorInterpolationComboBox->setCurrentIndex( mColorInterpolationComboBox->findData( QgsColorRampShader::Interpolated ) );
94 
95  mClassificationModeComboBox->addItem( tr( "Continuous" ), QgsColorRampShader::Continuous );
96  mClassificationModeComboBox->addItem( tr( "Equal Interval" ), QgsColorRampShader::EqualInterval );
97  // Quantile added only on demand
98  mClassificationModeComboBox->setCurrentIndex( mClassificationModeComboBox->findData( QgsColorRampShader::Continuous ) );
99 
100  mNumberOfEntriesSpinBox->setValue( 5 ); // some default
101 
102  mClassificationModeComboBox_currentIndexChanged( 0 );
103 
104  resetClassifyButton();
105 
106  connect( mClassificationModeComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &QgsColorRampShaderWidget::classify );
107  connect( mColorInterpolationComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &QgsColorRampShaderWidget::classify );
108  connect( mClassifyButton, &QPushButton::clicked, this, &QgsColorRampShaderWidget::classify );
109  connect( btnColorRamp, &QgsColorRampButton::colorRampChanged, this, &QgsColorRampShaderWidget::applyColorRamp );
110  connect( mNumberOfEntriesSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsColorRampShaderWidget::classify );
111  connect( mClipCheckBox, &QAbstractButton::toggled, this, &QgsColorRampShaderWidget::widgetChanged );
112  connect( mLabelPrecisionSpinBox, qgis::overload<int>::of( &QSpinBox::valueChanged ), this, [ = ]( int )
113  {
114  autoLabel();
115  } );
116 }
117 
119 {
120  Q_ASSERT( mClassificationModeComboBox->findData( QgsColorRampShader::Quantile < 0 ) );
121  mClassificationModeComboBox->addItem( tr( "Quantile" ), QgsColorRampShader::Quantile );
122 }
123 
125 {
126  mRasterDataProvider = dp;
127  mLoadFromBandButton->setVisible( bool( mRasterDataProvider ) ); // only for raster version
128 }
129 
131 {
132  mBand = band;
133  // Assume double by default
134  Qgis::DataType dataType { ( mRasterDataProvider &&mBand > 0 ) ? mRasterDataProvider->dataType( mBand ) : Qgis::DataType::Float64 };
135 
136  // Set the maximum number of digits in the precision spin box
137  const int maxDigits { QgsGuiUtils::significantDigits( dataType ) };
138  mLabelPrecisionSpinBox->setMaximum( maxDigits );
139  mValueDelegate->setDataType( dataType );
140 }
141 
143 {
144  mExtent = extent;
145 }
146 
148 {
149  QgsColorRampShader colorRampShader( mMin, mMax );
150  colorRampShader.setLabelPrecision( mLabelPrecisionSpinBox->value() );
151  colorRampShader.setColorRampType( static_cast< QgsColorRampShader::Type >( mColorInterpolationComboBox->currentData().toInt() ) );
152  colorRampShader.setClassificationMode( static_cast< QgsColorRampShader::ClassificationMode >( mClassificationModeComboBox->currentData().toInt() ) );
153  colorRampShader.setClip( mClipCheckBox->isChecked() );
154 
155  //iterate through mColormapTreeWidget and set colormap info of layer
156  QList<QgsColorRampShader::ColorRampItem> colorRampItems;
157  int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
158  QTreeWidgetItem *currentItem = nullptr;
159  for ( int i = 0; i < topLevelItemCount; ++i )
160  {
161  currentItem = mColormapTreeWidget->topLevelItem( i );
162  if ( !currentItem )
163  {
164  continue;
165  }
166  QgsColorRampShader::ColorRampItem newColorRampItem;
167  newColorRampItem.value = currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble();
168  newColorRampItem.color = currentItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
169  newColorRampItem.label = currentItem->text( LabelColumn );
170  colorRampItems.append( newColorRampItem );
171  }
172  // sort the shader items
173  std::sort( colorRampItems.begin(), colorRampItems.end() );
174  colorRampShader.setColorRampItemList( colorRampItems );
175 
176  if ( !btnColorRamp->isNull() )
177  {
178  colorRampShader.setSourceColorRamp( btnColorRamp->colorRamp() );
179  }
180 
181  colorRampShader.setLegendSettings( new QgsColorRampLegendNodeSettings( mLegendSettings ) );
182  return colorRampShader;
183 }
184 
185 void QgsColorRampShaderWidget::autoLabel()
186 {
187 
188  mColormapTreeWidget->sortItems( ValueColumn, Qt::AscendingOrder );
189 
190 #ifdef QGISDEBUG
191  dumpClasses();
192 #endif
193 
194  const QString unit = mUnitLineEdit->text();
195  int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
196 
197  QTreeWidgetItem *currentItem = nullptr;
198  for ( int i = 0; i < topLevelItemCount; ++i )
199  {
200  currentItem = mColormapTreeWidget->topLevelItem( i );
201  //If the item is null or does not have a pixel values set, skip
202  if ( !currentItem || currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString().isEmpty() )
203  {
204  continue;
205  }
206 
207  const QString lbl = createLabel( currentItem, i, unit );
208 
209  if ( currentItem->text( LabelColumn ).isEmpty() || currentItem->text( LabelColumn ) == lbl || currentItem->foreground( LabelColumn ).color() == QColor( Qt::gray ) )
210  {
211  currentItem->setText( LabelColumn, lbl );
212  currentItem->setForeground( LabelColumn, QBrush( QColor( Qt::gray ) ) );
213  }
214  }
215 
216 }
217 
218 void QgsColorRampShaderWidget::setUnitFromLabels()
219 {
220  QStringList allSuffixes;
221  QString label;
222  int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
223  QTreeWidgetItem *currentItem = nullptr;
224  for ( int i = 0; i < topLevelItemCount; ++i )
225  {
226  currentItem = mColormapTreeWidget->topLevelItem( i );
227  //If the item is null or does not have a pixel values set, skip
228  if ( !currentItem || currentItem->text( ValueColumn ).isEmpty() )
229  {
230  continue;
231  }
232 
233  label = createLabel( currentItem, i, QString() );
234 
235  if ( currentItem->text( LabelColumn ).startsWith( label ) )
236  {
237  allSuffixes.append( currentItem->text( LabelColumn ).mid( label.length() ) );
238  }
239  }
240  // find most common suffix
241  QStringList suffixes = QStringList( allSuffixes );
242  suffixes.removeDuplicates();
243  int max = 0;
244  QString unit;
245  for ( int i = 0; i < suffixes.count(); ++i )
246  {
247  int n = allSuffixes.count( suffixes[i] );
248  if ( n > max )
249  {
250  max = n;
251  unit = suffixes[i];
252  }
253  }
254  // Set this suffix as unit if at least used twice
255  if ( max >= 2 )
256  {
257  mUnitLineEdit->setText( unit );
258  }
259 }
260 
261 #ifdef QGISDEBUG
262 void QgsColorRampShaderWidget::dumpClasses()
263 {
264  for ( int row = 0; row < mColormapTreeWidget->model()->rowCount(); ++row )
265  {
266  const auto labelData { mColormapTreeWidget->model()->itemData( mColormapTreeWidget->model()->index( row, LabelColumn ) ) };
267  const auto valueData { mColormapTreeWidget->model()->itemData( mColormapTreeWidget->model()->index( row, ValueColumn ) ) };
268  QgsDebugMsgLevel( QStringLiteral( "Class %1 : %2 %3" ).arg( row )
269  .arg( labelData[ Qt::ItemDataRole::DisplayRole ].toString(),
270  valueData[ Qt::ItemDataRole::DisplayRole ].toString() ), 2 );
271  }
272 }
273 #endif
274 
275 void QgsColorRampShaderWidget::mAddEntryButton_clicked()
276 {
277  QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget );
278  newItem->setData( ValueColumn, Qt::ItemDataRole::DisplayRole, 0 );
279  newItem->setData( ColorColumn, Qt::ItemDataRole::EditRole, QColor( Qt::magenta ) );
280  newItem->setText( LabelColumn, QString() );
281  newItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable );
282  connect( newItem, &QgsTreeWidgetItemObject::itemEdited,
283  this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited );
284  autoLabel();
285 
287  updateColorRamp();
288  emit widgetChanged();
289 }
290 
291 void QgsColorRampShaderWidget::mDeleteEntryButton_clicked()
292 {
293  QList<QTreeWidgetItem *> itemList;
294  itemList = mColormapTreeWidget->selectedItems();
295  if ( itemList.isEmpty() )
296  {
297  return;
298  }
299 
300  const auto constItemList = itemList;
301  for ( QTreeWidgetItem *item : constItemList )
302  {
303  delete item;
304  }
305 
307  updateColorRamp();
308  emit widgetChanged();
309 }
310 
312 {
313  std::unique_ptr< QgsColorRamp > ramp( btnColorRamp->colorRamp() );
314  if ( !ramp || std::isnan( mMin ) || std::isnan( mMax ) )
315  {
316  return;
317  }
318 
319  std::unique_ptr< QgsColorRampShader > colorRampShader( new QgsColorRampShader(
320  mMin, mMax,
321  ramp.release(),
322  static_cast< QgsColorRampShader::Type >( mColorInterpolationComboBox->currentData().toInt() ),
323  static_cast< QgsColorRampShader::ClassificationMode >( mClassificationModeComboBox->currentData().toInt() ) )
324  );
325 
326  // only for Quantile we need band and provider and extent
327  colorRampShader->classifyColorRamp( mNumberOfEntriesSpinBox->value(),
328  mBand,
329  mExtent,
330  mRasterDataProvider );
331  colorRampShader->setClip( mClipCheckBox->isChecked() );
332 
333  mColormapTreeWidget->clear();
334 
335  const QList<QgsColorRampShader::ColorRampItem> colorRampItemList = colorRampShader->colorRampItemList();
336  QList<QgsColorRampShader::ColorRampItem>::const_iterator it = colorRampItemList.constBegin();
337  for ( ; it != colorRampItemList.end(); ++it )
338  {
339  QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget );
340  newItem->setData( ValueColumn, Qt::ItemDataRole::DisplayRole, it->value );
341  newItem->setData( ColorColumn, Qt::ItemDataRole::EditRole, it->color );
342  newItem->setText( LabelColumn, QString() ); // Labels will be populated in autoLabel()
343  newItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable );
344  connect( newItem, &QgsTreeWidgetItemObject::itemEdited,
345  this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited );
346  }
347 
348  mClipCheckBox->setChecked( colorRampShader->clip() );
349 
350  autoLabel();
351  emit widgetChanged();
352 }
353 
354 void QgsColorRampShaderWidget::mClassificationModeComboBox_currentIndexChanged( int index )
355 {
356  QgsColorRampShader::ClassificationMode mode = static_cast< QgsColorRampShader::ClassificationMode >( mClassificationModeComboBox->itemData( index ).toInt() );
357  mNumberOfEntriesSpinBox->setEnabled( mode != QgsColorRampShader::Continuous );
358  emit classificationModeChanged( mode );
359 }
360 
361 void QgsColorRampShaderWidget::updateColorRamp()
362 {
363  std::unique_ptr< QgsColorRamp > ramp( shader().createColorRamp() );
364  whileBlocking( btnColorRamp )->setColorRamp( ramp.get() );
365 }
366 
367 void QgsColorRampShaderWidget::applyColorRamp()
368 {
369  std::unique_ptr< QgsColorRamp > ramp( btnColorRamp->colorRamp() );
370  if ( !ramp )
371  {
372  return;
373  }
374 
375  if ( !btnColorRamp->colorRampName().isEmpty() )
376  {
377  // Remember last used color ramp
378  QgsSettings settings;
379  settings.setValue( QStringLiteral( "Raster/defaultPalette" ), btnColorRamp->colorRampName() );
380  }
381 
382  bool enableContinuous = ( ramp->count() > 0 );
383  mClassificationModeComboBox->setEnabled( enableContinuous );
384  if ( !enableContinuous )
385  {
386  mClassificationModeComboBox->setCurrentIndex( mClassificationModeComboBox->findData( QgsColorRampShader::EqualInterval ) );
387  }
388 
389  int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
390  if ( topLevelItemCount > 0 )
391  {
392  // We need to have valid min/max values here. If we haven't, load from colormap
393  double min, max;
394  if ( std::isnan( mMin ) || std::isnan( mMax ) )
395  {
396  colormapMinMax( min, max );
397  }
398  else
399  {
400  min = mMin;
401  max = mMax;
402  }
403 
404  // if the list values has been customized, maintain pre-existing values
405  QTreeWidgetItem *currentItem = nullptr;
406  for ( int i = 0; i < topLevelItemCount; ++i )
407  {
408  currentItem = mColormapTreeWidget->topLevelItem( i );
409  if ( !currentItem )
410  {
411  continue;
412  }
413 
414  double value = currentItem->data( ValueColumn, Qt::ItemDataRole::EditRole ).toDouble( );
415  double position = ( value - min ) / ( max - min );
416  whileBlocking( static_cast<QgsTreeWidgetItemObject *>( currentItem ) )->setData( ColorColumn, Qt::ItemDataRole::EditRole, ramp->color( position ) );
417  }
418 
419  emit widgetChanged();
420  }
421  else
422  {
423  classify();
424  }
425 }
426 
427 void QgsColorRampShaderWidget::populateColormapTreeWidget( const QList<QgsColorRampShader::ColorRampItem> &colorRampItems )
428 {
429  mColormapTreeWidget->clear();
430  QList<QgsColorRampShader::ColorRampItem>::const_iterator it = colorRampItems.constBegin();
431  int i = 0;
432  for ( ; it != colorRampItems.constEnd(); ++it )
433  {
434  QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget );
435  newItem->setData( ValueColumn, Qt::ItemDataRole::DisplayRole, it->value );
436  newItem->setData( ColorColumn, Qt::ItemDataRole::EditRole, it->color );
437  newItem->setText( LabelColumn, it->label );
438  newItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable );
439  connect( newItem, &QgsTreeWidgetItemObject::itemEdited,
440  this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited );
441  ++i;
442  }
443 
444 #ifdef QGISDEBUG
445  dumpClasses();
446 #endif
447 
448  setUnitFromLabels();
449 
450  // Now we have the suffix
451  const QString unit = mUnitLineEdit->text();
452  for ( i = 0; i < mColormapTreeWidget->topLevelItemCount(); i++ )
453  {
454  QgsTreeWidgetItemObject *currentItem { static_cast<QgsTreeWidgetItemObject *>( mColormapTreeWidget->topLevelItem( i ) ) };
455  QString lbl { createLabel( currentItem, i, unit )};
456  if ( currentItem->text( LabelColumn ).isEmpty() || currentItem->text( LabelColumn ) == lbl || currentItem->foreground( LabelColumn ).color() == QColor( Qt::gray ) )
457  {
458  currentItem->setText( LabelColumn, lbl );
459  currentItem->setForeground( LabelColumn, QBrush( QColor( Qt::gray ) ) );
460  }
461  }
462 
463 }
464 
465 void QgsColorRampShaderWidget::mLoadFromBandButton_clicked()
466 {
467  if ( !mRasterDataProvider )
468  return;
469 
470  QList<QgsColorRampShader::ColorRampItem> colorRampList = mRasterDataProvider->colorTable( mBand );
471  if ( !colorRampList.isEmpty() )
472  {
473  populateColormapTreeWidget( colorRampList );
474  mColorInterpolationComboBox->setCurrentIndex( mColorInterpolationComboBox->findData( QgsColorRampShader::Interpolated ) );
475  }
476  else
477  {
478  QMessageBox::warning( this, tr( "Load Color Map" ), tr( "The color map for band %1 has no entries." ).arg( mBand ) );
479  }
481  emit widgetChanged();
482 }
483 
484 void QgsColorRampShaderWidget::mLoadFromFileButton_clicked()
485 {
486  QgsSettings settings;
487  QString lastDir = settings.value( QStringLiteral( "lastColorMapDir" ), QDir::homePath() ).toString();
488  const QString fileName = QFileDialog::getOpenFileName( this, tr( "Load Color Map from File" ), lastDir, tr( "Textfile (*.txt)" ) );
489  if ( fileName.isEmpty() )
490  return;
491 
492  QList<QgsColorRampShader::ColorRampItem> colorRampItems;
494  QStringList errors;
495  if ( QgsRasterRendererUtils::parseColorMapFile( fileName, colorRampItems, type, errors ) )
496  {
497  //clear the current tree
498  mColormapTreeWidget->clear();
499 
500  mColorInterpolationComboBox->setCurrentIndex( mColorInterpolationComboBox->findData( type ) );
501 
502  populateColormapTreeWidget( colorRampItems );
503 
504  if ( !errors.empty() )
505  {
506  QMessageBox::warning( this, tr( "Load Color Map from File" ), tr( "The following lines contained errors\n\n" ) + errors.join( '\n' ) );
507  }
508  }
509  else
510  {
511  const QString error = tr( "An error occurred while reading the color map\n\n" ) + errors.join( '\n' );
512  QMessageBox::warning( this, tr( "Load Color Map from File" ), error );
513  }
514 
515  QFileInfo fileInfo( fileName );
516  settings.setValue( QStringLiteral( "lastColorMapDir" ), fileInfo.absoluteDir().absolutePath() );
517 
519  emit widgetChanged();
520 }
521 
522 void QgsColorRampShaderWidget::mExportToFileButton_clicked()
523 {
524  QgsSettings settings;
525  QString lastDir = settings.value( QStringLiteral( "lastColorMapDir" ), QDir::homePath() ).toString();
526  QString fileName = QFileDialog::getSaveFileName( this, tr( "Save Color Map as File" ), lastDir, tr( "Textfile (*.txt)" ) );
527  if ( fileName.isEmpty() )
528  return;
529 
530  fileName = QgsFileUtils::ensureFileNameHasExtension( fileName, QStringList() << QStringLiteral( "txt" ) );
531 
532  QList<QgsColorRampShader::ColorRampItem> colorRampItems;
533  int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
534  for ( int i = 0; i < topLevelItemCount; ++i )
535  {
536  QTreeWidgetItem *currentItem = mColormapTreeWidget->topLevelItem( i );
537  if ( !currentItem )
538  {
539  continue;
540  }
541 
543  item.value = currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble( );
544  item.color = currentItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
545  item.label = currentItem->text( LabelColumn );
546  colorRampItems << item;
547  }
548 
549  if ( !QgsRasterRendererUtils::saveColorMapFile( fileName, colorRampItems, static_cast< QgsColorRampShader::Type >( mColorInterpolationComboBox->currentData().toInt() ) ) )
550  {
551  QMessageBox::warning( this, tr( "Save Color Map as File" ), tr( "Write access denied. Adjust the file permissions and try again.\n\n" ) );
552  }
553 
554  QFileInfo fileInfo( fileName );
555  settings.setValue( QStringLiteral( "lastColorMapDir" ), fileInfo.absoluteDir().absolutePath() );
556 }
557 
558 void QgsColorRampShaderWidget::mColormapTreeWidget_itemDoubleClicked( QTreeWidgetItem *item, int column )
559 {
560  if ( !item )
561  {
562  return;
563  }
564 
565  if ( column == LabelColumn )
566  {
567  // Set text color to default black, which signifies a manually edited label
568  item->setForeground( LabelColumn, QBrush() );
569  }
570 }
571 
572 void QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited( QTreeWidgetItem *item, int column )
573 {
574  Q_UNUSED( item )
575 
576  switch ( column )
577  {
578  case ValueColumn:
579  {
580  autoLabel();
582  updateColorRamp();
583  emit widgetChanged();
584  break;
585  }
586 
587  case LabelColumn:
588  {
589  // call autoLabel to fill when empty or gray out when same as autoLabel
590  autoLabel();
591  emit widgetChanged();
592  break;
593  }
594 
595  case ColorColumn:
596  {
598  updateColorRamp();
599  emit widgetChanged();
600  break;
601  }
602  }
603 }
604 
606 {
607  // Those objects are connected to classify() the color ramp shader if they change, or call widget change
608  // need to block them to avoid to classify and to alter the color ramp, or to call duplicate widget change
609  whileBlocking( mClipCheckBox )->setChecked( colorRampShader.clip() );
610  whileBlocking( mColorInterpolationComboBox )->setCurrentIndex( mColorInterpolationComboBox->findData( colorRampShader.colorRampType() ) );
611  mColorInterpolationComboBox_currentIndexChanged( mColorInterpolationComboBox->currentIndex() );
612  whileBlocking( mClassificationModeComboBox )->setCurrentIndex( mClassificationModeComboBox->findData( colorRampShader.classificationMode() ) );
613  mClassificationModeComboBox_currentIndexChanged( mClassificationModeComboBox->currentIndex() );
614  whileBlocking( mNumberOfEntriesSpinBox )->setValue( colorRampShader.colorRampItemList().count() ); // some default
615 
616  if ( colorRampShader.sourceColorRamp() )
617  {
618  whileBlocking( btnColorRamp )->setColorRamp( colorRampShader.sourceColorRamp() );
619  }
620  else
621  {
622  QgsSettings settings;
623  QString defaultPalette = settings.value( QStringLiteral( "/Raster/defaultPalette" ), "Spectral" ).toString();
624  btnColorRamp->setColorRampFromName( defaultPalette );
625  }
626 
627  mLabelPrecisionSpinBox->setValue( colorRampShader.labelPrecision() );
628 
629  populateColormapTreeWidget( colorRampShader.colorRampItemList() );
630 
631  if ( colorRampShader.legendSettings() )
632  mLegendSettings = *colorRampShader.legendSettings();
633 
634  emit widgetChanged();
635 }
636 
637 void QgsColorRampShaderWidget::mColorInterpolationComboBox_currentIndexChanged( int index )
638 {
639  QgsColorRampShader::Type interpolation = static_cast< QgsColorRampShader::Type >( mColorInterpolationComboBox->itemData( index ).toInt() );
640 
641  mClipCheckBox->setEnabled( interpolation == QgsColorRampShader::Interpolated );
642 
643  QString valueLabel;
644  QString valueToolTip;
645  switch ( interpolation )
646  {
648  valueLabel = tr( "Value" );
649  valueToolTip = tr( "Value for color stop" );
650  mLegendSettingsButton->setEnabled( true );
651  break;
653  valueLabel = tr( "Value <=" );
654  valueToolTip = tr( "Maximum value for class" );
655  mLegendSettingsButton->setEnabled( false );
656  break;
658  valueLabel = tr( "Value =" );
659  valueToolTip = tr( "Value for color" );
660  mLegendSettingsButton->setEnabled( false );
661  break;
662  }
663 
664  QTreeWidgetItem *header = mColormapTreeWidget->headerItem();
665  header->setText( ValueColumn, valueLabel );
666  header->setToolTip( ValueColumn, valueToolTip );
667 
668  autoLabel();
669  emit widgetChanged();
670 }
671 
673 {
674  if ( !qgsDoubleNear( mMin, min ) || !qgsDoubleNear( mMax, max ) )
675  {
676  setMinimumMaximum( min, max );
677  classify();
678  }
679 }
680 
681 void QgsColorRampShaderWidget::setMinimumMaximum( double min, double max )
682 {
683  mMin = min;
684  mMax = max;
685  resetClassifyButton();
686 }
687 
689 {
690  return mMin;
691 }
692 
694 {
695  return mMax;
696 }
697 
698 bool QgsColorRampShaderWidget::colormapMinMax( double &min, double &max ) const
699 {
700  QTreeWidgetItem *item = mColormapTreeWidget->topLevelItem( 0 );
701  if ( !item )
702  {
703  return false;
704  }
705 
706  // If using discrete, the first and last items contain the upper and lower
707  // values of the first and last classes, we don't want these values but real min/max
708  if ( ! std::isnan( mMin ) && ! std::isnan( mMax ) && static_cast< QgsColorRampShader::Type >( mColorInterpolationComboBox->currentData().toInt() ) == QgsColorRampShader::Type::Discrete )
709  {
710  min = mMin;
711  max = mMax;
712  }
713  else
714  {
715  min = item->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble();
716  item = mColormapTreeWidget->topLevelItem( mColormapTreeWidget->topLevelItemCount() - 1 );
717  max = item->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble();
718  }
719  return true;
720 }
721 
723 {
724  double min = 0, max = 0;
725  if ( ! colormapMinMax( min, max ) )
726  {
727  return;
728  }
729 
730  if ( !qgsDoubleNear( mMin, min ) || !qgsDoubleNear( mMax, max ) )
731  {
732  mMin = min;
733  mMax = max;
734  emit minimumMaximumChangedFromTree( min, max );
735  }
736 }
737 
738 void QgsColorRampShaderWidget::resetClassifyButton()
739 {
740  mClassifyButton->setEnabled( true );
741  if ( std::isnan( mMin ) || std::isnan( mMax ) || mMin >= mMax )
742  {
743  mClassifyButton->setEnabled( false );
744  }
745 }
746 
747 QString QgsColorRampShaderWidget::createLabel( QTreeWidgetItem *currentItem, int row, const QString unit )
748 {
749  auto applyPrecision = [ = ]( const QString & value )
750  {
751  double val { value.toDouble( ) };
752  Qgis::DataType dataType { mRasterDataProvider ? mRasterDataProvider->dataType( mBand ) : Qgis::DataType::Float64 };
753  switch ( dataType )
754  {
755  case Qgis::DataType::Int16:
756  case Qgis::DataType::UInt16:
757  case Qgis::DataType::Int32:
758  case Qgis::DataType::UInt32:
759  case Qgis::DataType::Byte:
760  case Qgis::DataType::CInt16:
761  case Qgis::DataType::CInt32:
762  case Qgis::DataType::ARGB32:
763  case Qgis::DataType::ARGB32_Premultiplied:
764  {
765  return QLocale().toString( std::round( val ), 'f', 0 );
766  }
767  case Qgis::DataType::Float32:
768  case Qgis::DataType::CFloat32:
769  {
770  if ( mLabelPrecisionSpinBox->value() < 0 )
771  {
772  const double factor = std::pow( 10, - mLabelPrecisionSpinBox->value() );
773  val = static_cast<qlonglong>( val / factor ) * factor;
774  return QLocale().toString( val, 'f', 0 );
775  }
776  return QLocale().toString( val, 'f', mLabelPrecisionSpinBox->value() );
777  }
778  case Qgis::DataType::Float64:
779  case Qgis::DataType::CFloat64:
780  case Qgis::DataType::UnknownDataType:
781  {
782  if ( mLabelPrecisionSpinBox->value() < 0 )
783  {
784  const double factor = std::pow( 10, - mLabelPrecisionSpinBox->value() );
785  val = static_cast<qlonglong>( val / factor ) * factor;
786  return QLocale().toString( val, 'f', 0 );
787  }
788  return QLocale().toString( val, 'f', mLabelPrecisionSpinBox->value() );
789  }
790  }
791  return QString();
792  };
793 
794  QgsColorRampShader::Type interpolation = static_cast< QgsColorRampShader::Type >( mColorInterpolationComboBox->currentData().toInt() );
795  bool discrete = interpolation == QgsColorRampShader::Discrete;
796  QString lbl;
797 
798  if ( discrete )
799  {
800  if ( row == 0 )
801  {
802  lbl = "<= " + applyPrecision( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
803  }
804  else if ( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble( ) == std::numeric_limits<double>::infinity() )
805  {
806  lbl = "> " + applyPrecision( mColormapTreeWidget->topLevelItem( row - 1 )->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
807  }
808  else
809  {
810  lbl = applyPrecision( mColormapTreeWidget->topLevelItem( row - 1 )->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + " - " + applyPrecision( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
811  }
812  }
813  else
814  {
815  lbl = applyPrecision( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
816  }
817 
818  return lbl;
819 
820 }
821 
822 void QgsColorRampShaderWidget::changeColor()
823 {
824  QList<QTreeWidgetItem *> itemList;
825  itemList = mColormapTreeWidget->selectedItems();
826  if ( itemList.isEmpty() )
827  {
828  return;
829  }
830  QTreeWidgetItem *firstItem = itemList.first();
831 
832  QColor currentColor = firstItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
833  QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( qobject_cast< QWidget * >( parent() ) );
834  if ( panel && panel->dockMode() )
835  {
837  colorWidget->setPanelTitle( tr( "Select Color" ) );
838  colorWidget->setAllowOpacity( true );
839  connect( colorWidget, &QgsCompoundColorWidget::currentColorChanged, this, [ = ]( const QColor & newColor )
840  {
841  for ( QTreeWidgetItem *item : qgis::as_const( itemList ) )
842  {
843  item->setData( ColorColumn, Qt::ItemDataRole::EditRole, newColor );
844  }
845 
847  emit widgetChanged();
848  } );
849  panel->openPanel( colorWidget );
850  }
851  else
852  {
853  // modal dialog version... yuck
854  QColor newColor = QgsColorDialog::getColor( currentColor, this, QStringLiteral( "Change Color" ), true );
855  if ( newColor.isValid() )
856  {
857  for ( QTreeWidgetItem *item : qgis::as_const( itemList ) )
858  {
859  item->setData( ColorColumn, Qt::ItemDataRole::EditRole, newColor );
860  }
861 
863  emit widgetChanged();
864  }
865  }
866 }
867 
868 void QgsColorRampShaderWidget::changeOpacity()
869 {
870  QList<QTreeWidgetItem *> itemList;
871  itemList = mColormapTreeWidget->selectedItems();
872  if ( itemList.isEmpty() )
873  {
874  return;
875  }
876  QTreeWidgetItem *firstItem = itemList.first();
877 
878  bool ok;
879  double oldOpacity = firstItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>().alpha() / 255 * 100;
880  double opacity = QInputDialog::getDouble( this, tr( "Opacity" ), tr( "Change color opacity [%]" ), oldOpacity, 0.0, 100.0, 0, &ok );
881  if ( ok )
882  {
883  int newOpacity = static_cast<int>( opacity / 100 * 255 );
884  const auto constItemList = itemList;
885  for ( QTreeWidgetItem *item : constItemList )
886  {
887  QColor newColor = item->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
888  newColor.setAlpha( newOpacity );
889  item->setData( ColorColumn, Qt::ItemDataRole::EditRole, newColor );
890  }
891 
893  emit widgetChanged();
894  }
895 }
896 
897 void QgsColorRampShaderWidget::showLegendSettings()
898 {
899  QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( qobject_cast< QWidget * >( parent() ) );
900  if ( panel && panel->dockMode() )
901  {
903  legendPanel->setPanelTitle( tr( "Legend Settings" ) );
904  legendPanel->setSettings( mLegendSettings );
905  connect( legendPanel, &QgsColorRampLegendNodeWidget::widgetChanged, this, [ = ]
906  {
907  mLegendSettings = legendPanel->settings();
908  emit widgetChanged();
909  } );
910  panel->openPanel( legendPanel );
911  }
912  else
913  {
914  QgsColorRampLegendNodeDialog dialog( mLegendSettings, this );
915  dialog.setWindowTitle( tr( "Legend Settings" ) );
916  if ( dialog.exec() )
917  {
918  mLegendSettings = dialog.settings();
919  emit widgetChanged();
920  }
921  }
922 }
DataType
Raster data types.
Definition: qgis.h:102
static const double UI_SCALE_FACTOR
UI scaling factor.
Definition: qgis.h:183
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
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
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:316
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition: qgis.h:263
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39