QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
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
19
22#include "qgsrasterlayer.h"
24#include "qgsrastershader.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"
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
181void 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
214void 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
258void 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
271void 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
287void 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
350void 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
357void QgsColorRampShaderWidget::updateColorRamp()
358{
359 std::unique_ptr< QgsColorRamp > ramp( shader().createColorRamp() );
360 whileBlocking( btnColorRamp )->setColorRamp( ramp.get() );
361}
362
363void 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
423void 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
461void 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
480void 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
519void 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
555void 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
569void 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
627
628 if ( colorRampShader.legendSettings() )
629 mLegendSettings = *colorRampShader.legendSettings();
630
631 emit widgetChanged();
632}
633
634void 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
679{
680 mMin = min;
681 mMax = max;
682 resetClassifyButton();
683}
684
686{
687 return mMin;
688}
689
691{
692 return mMax;
693}
694
695bool 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
735void 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
744QString 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
819void 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
865void 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
894void 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:129
@ 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:2304
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.
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.
QList< QgsColorRampShader::ColorRampItem > colorRampItemList() const
Returns the custom colormap.
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.
Qgis::DataType dataType(int bandNo) const override=0
Returns data type for the band specified by number.
virtual QList< QgsColorRampShader::ColorRampItem > colorTable(int bandNo) const
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:2527
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition: qgis.h:2453
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39