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