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