QGIS API Documentation 3.99.0-Master (e9821da5c6b)
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( new QgsColorRampShader(
312 mMin, mMax,
313 ramp.release(),
314 mColorInterpolationComboBox->currentData().value<Qgis::ShaderInterpolationMethod>(),
315 mClassificationModeComboBox->currentData().value<Qgis::ShaderClassificationMethod>()
316 )
317 );
318
319 // only for Quantile we need band and provider and extent
320 colorRampShader->classifyColorRamp( mNumberOfEntriesSpinBox->value(), mBand, mExtent, mRasterDataProvider );
321 colorRampShader->setClip( mClipCheckBox->isChecked() );
322
323 mColormapTreeWidget->clear();
324
325 const QList<QgsColorRampShader::ColorRampItem> colorRampItemList = colorRampShader->colorRampItemList();
326 QList<QgsColorRampShader::ColorRampItem>::const_iterator it = colorRampItemList.constBegin();
327 for ( ; it != colorRampItemList.end(); ++it )
328 {
329 QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget );
330 newItem->setData( ValueColumn, Qt::ItemDataRole::DisplayRole, it->value );
331 newItem->setData( ColorColumn, Qt::ItemDataRole::EditRole, it->color );
332 newItem->setText( LabelColumn, QString() ); // Labels will be populated in autoLabel()
333 newItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable );
334 connect( newItem, &QgsTreeWidgetItemObject::itemEdited, this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited );
335 }
336
337 mClipCheckBox->setChecked( colorRampShader->clip() );
338
339 autoLabel();
340 emit widgetChanged();
341}
342
343void QgsColorRampShaderWidget::mClassificationModeComboBox_currentIndexChanged( int index )
344{
345 Qgis::ShaderClassificationMethod mode = mClassificationModeComboBox->itemData( index ).value<Qgis::ShaderClassificationMethod>();
346 mNumberOfEntriesSpinBox->setEnabled( mode != Qgis::ShaderClassificationMethod::Continuous );
347 emit classificationModeChanged( mode );
348}
349
350void QgsColorRampShaderWidget::updateColorRamp()
351{
352 std::unique_ptr<QgsColorRamp> ramp( shader().createColorRamp() );
353 whileBlocking( btnColorRamp )->setColorRamp( ramp.get() );
354}
355
356void QgsColorRampShaderWidget::applyColorRamp()
357{
358 std::unique_ptr<QgsColorRamp> ramp( btnColorRamp->colorRamp() );
359 if ( !ramp )
360 {
361 return;
362 }
363
364 if ( !btnColorRamp->colorRampName().isEmpty() )
365 {
366 // Remember last used color ramp
367 QgsSettings settings;
368 settings.setValue( u"Raster/defaultPalette"_s, btnColorRamp->colorRampName() );
369 }
370
371 bool enableContinuous = ( ramp->count() > 0 );
372 mClassificationModeComboBox->setEnabled( enableContinuous );
373 if ( !enableContinuous )
374 {
375 mClassificationModeComboBox->setCurrentIndex( mClassificationModeComboBox->findData( QVariant::fromValue( Qgis::ShaderClassificationMethod::EqualInterval ) ) );
376 }
377
378 int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
379 if ( topLevelItemCount > 0 )
380 {
381 // We need to have valid min/max values here. If we haven't, load from colormap
382 double min, max;
383 if ( std::isnan( mMin ) || std::isnan( mMax ) )
384 {
385 colormapMinMax( min, max );
386 }
387 else
388 {
389 min = mMin;
390 max = mMax;
391 }
392
393 // if the list values has been customized, maintain pre-existing values
394 QTreeWidgetItem *currentItem = nullptr;
395 for ( int i = 0; i < topLevelItemCount; ++i )
396 {
397 currentItem = mColormapTreeWidget->topLevelItem( i );
398 if ( !currentItem )
399 {
400 continue;
401 }
402
403 double value = currentItem->data( ValueColumn, Qt::ItemDataRole::EditRole ).toDouble();
404 double position = ( value - min ) / ( max - min );
405 whileBlocking( static_cast<QgsTreeWidgetItemObject *>( currentItem ) )->setData( ColorColumn, Qt::ItemDataRole::EditRole, ramp->color( position ) );
406 }
407
408 emit widgetChanged();
409 }
410 else
411 {
412 classify();
413 }
414}
415
416void QgsColorRampShaderWidget::populateColormapTreeWidget( const QList<QgsColorRampShader::ColorRampItem> &colorRampItems )
417{
418 mColormapTreeWidget->clear();
419 QList<QgsColorRampShader::ColorRampItem>::const_iterator it = colorRampItems.constBegin();
420 int i = 0;
421 for ( ; it != colorRampItems.constEnd(); ++it )
422 {
423 QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget );
424 newItem->setData( ValueColumn, Qt::ItemDataRole::DisplayRole, it->value );
425 newItem->setData( ColorColumn, Qt::ItemDataRole::EditRole, it->color );
426 newItem->setText( LabelColumn, it->label );
427 newItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable );
428 connect( newItem, &QgsTreeWidgetItemObject::itemEdited, this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited );
429 ++i;
430 }
431
432#ifdef QGISDEBUG
433 dumpClasses();
434#endif
435
436 setUnitFromLabels();
437
438 // Now we have the suffix
439 const QString unit = mUnitLineEdit->text();
440 for ( i = 0; i < mColormapTreeWidget->topLevelItemCount(); i++ )
441 {
442 QgsTreeWidgetItemObject *currentItem { static_cast<QgsTreeWidgetItemObject *>( mColormapTreeWidget->topLevelItem( i ) ) };
443 QString lbl { createLabel( currentItem, i, unit ) };
444 if ( currentItem->text( LabelColumn ).isEmpty() || currentItem->text( LabelColumn ) == lbl || currentItem->foreground( LabelColumn ).color() == QColor( Qt::gray ) )
445 {
446 currentItem->setText( LabelColumn, lbl );
447 currentItem->setForeground( LabelColumn, QBrush( QColor( Qt::gray ) ) );
448 }
449 }
450}
451
452void QgsColorRampShaderWidget::mLoadFromBandButton_clicked()
453{
454 if ( !mRasterDataProvider )
455 return;
456
457 QList<QgsColorRampShader::ColorRampItem> colorRampList = mRasterDataProvider->colorTable( mBand );
458 if ( !colorRampList.isEmpty() )
459 {
460 populateColormapTreeWidget( colorRampList );
461 mColorInterpolationComboBox->setCurrentIndex( mColorInterpolationComboBox->findData( QVariant::fromValue( Qgis::ShaderInterpolationMethod::Linear ) ) );
462 }
463 else
464 {
465 QMessageBox::warning( this, tr( "Load Color Map" ), tr( "The color map for band %1 has no entries." ).arg( mBand ) );
466 }
468 emit widgetChanged();
469}
470
471void QgsColorRampShaderWidget::mLoadFromFileButton_clicked()
472{
473 QgsSettings settings;
474 QString lastDir = settings.value( u"lastColorMapDir"_s, QDir::homePath() ).toString();
475 const QString fileName = QFileDialog::getOpenFileName( this, tr( "Load Color Map from File" ), lastDir, tr( "Textfile (*.txt)" ) );
476 if ( fileName.isEmpty() )
477 return;
478
479 QList<QgsColorRampShader::ColorRampItem> colorRampItems;
481 QStringList errors;
482 if ( QgsRasterRendererUtils::parseColorMapFile( fileName, colorRampItems, type, errors ) )
483 {
484 //clear the current tree
485 mColormapTreeWidget->clear();
486
487 mColorInterpolationComboBox->setCurrentIndex( mColorInterpolationComboBox->findData( QVariant::fromValue( type ) ) );
488
489 populateColormapTreeWidget( colorRampItems );
490
491 if ( !errors.empty() )
492 {
493 QMessageBox::warning( this, tr( "Load Color Map from File" ), tr( "The following lines contained errors\n\n" ) + errors.join( '\n' ) );
494 }
495 }
496 else
497 {
498 const QString error = tr( "An error occurred while reading the color map\n\n" ) + errors.join( '\n' );
499 QMessageBox::warning( this, tr( "Load Color Map from File" ), error );
500 }
501
502 QFileInfo fileInfo( fileName );
503 settings.setValue( u"lastColorMapDir"_s, fileInfo.absoluteDir().absolutePath() );
504
506 updateColorRamp();
507 emit widgetChanged();
508}
509
510void QgsColorRampShaderWidget::mExportToFileButton_clicked()
511{
512 QgsSettings settings;
513 QString lastDir = settings.value( u"lastColorMapDir"_s, QDir::homePath() ).toString();
514 QString fileName = QFileDialog::getSaveFileName( this, tr( "Save Color Map as File" ), lastDir, tr( "Textfile (*.txt)" ) );
515 if ( fileName.isEmpty() )
516 return;
517
518 fileName = QgsFileUtils::ensureFileNameHasExtension( fileName, QStringList() << u"txt"_s );
519
520 QList<QgsColorRampShader::ColorRampItem> colorRampItems;
521 int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
522 for ( int i = 0; i < topLevelItemCount; ++i )
523 {
524 QTreeWidgetItem *currentItem = mColormapTreeWidget->topLevelItem( i );
525 if ( !currentItem )
526 {
527 continue;
528 }
529
530 QgsColorRampShader::ColorRampItem item;
531 item.value = currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble();
532 item.color = currentItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
533 item.label = currentItem->text( LabelColumn );
534 colorRampItems << item;
535 }
536
537 if ( !QgsRasterRendererUtils::saveColorMapFile( fileName, colorRampItems, mColorInterpolationComboBox->currentData().value<Qgis::ShaderInterpolationMethod>() ) )
538 {
539 QMessageBox::warning( this, tr( "Save Color Map as File" ), tr( "Write access denied. Adjust the file permissions and try again.\n\n" ) );
540 }
541
542 QFileInfo fileInfo( fileName );
543 settings.setValue( u"lastColorMapDir"_s, fileInfo.absoluteDir().absolutePath() );
544}
545
546void QgsColorRampShaderWidget::mUnitLineEdit_textEdited( const QString & )
547{
548 autoLabel();
549
550 if ( !mBlockChanges )
551 emit widgetChanged();
552}
553
554void QgsColorRampShaderWidget::mColormapTreeWidget_itemDoubleClicked( QTreeWidgetItem *item, int column )
555{
556 if ( !item )
557 {
558 return;
559 }
560
561 if ( column == LabelColumn )
562 {
563 // Set text color to default black, which signifies a manually edited label
564 item->setForeground( LabelColumn, QBrush() );
565 }
566}
567
568void QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited( QTreeWidgetItem *item, int column )
569{
570 Q_UNUSED( item )
571
572 switch ( column )
573 {
574 case ValueColumn:
575 {
576 autoLabel();
578 updateColorRamp();
579 emit widgetChanged();
580 break;
581 }
582
583 case LabelColumn:
584 {
585 // call autoLabel to fill when empty or gray out when same as autoLabel
586 autoLabel();
587 emit widgetChanged();
588 break;
589 }
590
591 case ColorColumn:
592 {
594 updateColorRamp();
595 emit widgetChanged();
596 break;
597 }
598 }
599}
600
602{
603 mBlockChanges++;
604
605 // Those objects are connected to classify() the color ramp shader if they change, or call widget change
606 // need to block them to avoid to classify and to alter the color ramp, or to call duplicate widget change
607 whileBlocking( mClipCheckBox )->setChecked( colorRampShader.clip() );
608 whileBlocking( mColorInterpolationComboBox )->setCurrentIndex( mColorInterpolationComboBox->findData( QVariant::fromValue( colorRampShader.colorRampType() ) ) );
609 mColorInterpolationComboBox_currentIndexChanged( mColorInterpolationComboBox->currentIndex() );
610 whileBlocking( mClassificationModeComboBox )->setCurrentIndex( mClassificationModeComboBox->findData( QVariant::fromValue( colorRampShader.classificationMode() ) ) );
611 mClassificationModeComboBox_currentIndexChanged( mClassificationModeComboBox->currentIndex() );
612 whileBlocking( mNumberOfEntriesSpinBox )->setValue( colorRampShader.colorRampItemList().count() ); // some default
613
614 if ( colorRampShader.sourceColorRamp() )
615 {
616 whileBlocking( btnColorRamp )->setColorRamp( colorRampShader.sourceColorRamp() );
617 }
618 else
619 {
620 QgsSettings settings;
621 QString defaultPalette = settings.value( u"/Raster/defaultPalette"_s, "Spectral" ).toString();
622 btnColorRamp->setColorRampFromName( defaultPalette );
623 }
624
625 mLabelPrecisionSpinBox->setValue( colorRampShader.labelPrecision() );
626
628
629 if ( colorRampShader.legendSettings() )
630 mLegendSettings = *colorRampShader.legendSettings();
631
632 mBlockChanges--;
633 emit widgetChanged();
634}
635
636void QgsColorRampShaderWidget::mColorInterpolationComboBox_currentIndexChanged( int index )
637{
638 Qgis::ShaderInterpolationMethod interpolation = mColorInterpolationComboBox->itemData( index ).value<Qgis::ShaderInterpolationMethod>();
639
640 mClipCheckBox->setEnabled( interpolation == Qgis::ShaderInterpolationMethod::Linear );
641
642 QString valueLabel;
643 QString valueToolTip;
644 switch ( interpolation )
645 {
647 valueLabel = tr( "Value" );
648 valueToolTip = tr( "Value for color stop" );
649 mLegendSettingsButton->setEnabled( true );
650 break;
652 valueLabel = tr( "Value <=" );
653 valueToolTip = tr( "Maximum value for class" );
654 mLegendSettingsButton->setEnabled( false );
655 break;
657 valueLabel = tr( "Value =" );
658 valueToolTip = tr( "Value for color" );
659 mLegendSettingsButton->setEnabled( false );
660 break;
661 }
662
663 QTreeWidgetItem *header = mColormapTreeWidget->headerItem();
664 header->setText( ValueColumn, valueLabel );
665 header->setToolTip( ValueColumn, valueToolTip );
666
667 autoLabel();
668 emit widgetChanged();
669}
670
672{
673 if ( !qgsDoubleNear( mMin, min ) || !qgsDoubleNear( mMax, max ) )
674 {
675 setMinimumMaximum( min, max );
676 classify();
677 }
678}
679
681{
682 mMin = min;
683 mMax = max;
684 resetClassifyButton();
685}
686
688{
689 return mMin;
690}
691
693{
694 return mMax;
695}
696
697bool QgsColorRampShaderWidget::colormapMinMax( double &min, double &max ) const
698{
699 QTreeWidgetItem *item = mColormapTreeWidget->topLevelItem( 0 );
700 if ( !item )
701 {
702 return false;
703 }
704
705 // If using discrete, the first and last items contain the upper and lower
706 // values of the first and last classes, we don't want these values but real min/max
707 if ( !std::isnan( mMin ) && !std::isnan( mMax ) && mColorInterpolationComboBox->currentData().value<Qgis::ShaderInterpolationMethod>() == Qgis::ShaderInterpolationMethod::Discrete )
708 {
709 min = mMin;
710 max = mMax;
711 }
712 else
713 {
714 min = item->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble();
715 item = mColormapTreeWidget->topLevelItem( mColormapTreeWidget->topLevelItemCount() - 1 );
716 max = item->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble();
717 }
718 return true;
719}
720
722{
723 double min = 0, max = 0;
724 if ( !colormapMinMax( min, max ) )
725 {
726 return;
727 }
728
729 if ( !qgsDoubleNear( mMin, min ) || !qgsDoubleNear( mMax, max ) )
730 {
731 mMin = min;
732 mMax = max;
733 emit minimumMaximumChangedFromTree( min, max );
734 }
735}
736
737void QgsColorRampShaderWidget::resetClassifyButton()
738{
739 mClassifyButton->setEnabled( true );
740 if ( std::isnan( mMin ) || std::isnan( mMax ) || mMin >= mMax )
741 {
742 mClassifyButton->setEnabled( false );
743 }
744}
745
746QString QgsColorRampShaderWidget::createLabel( QTreeWidgetItem *currentItem, int row, const QString unit )
747{
748 auto applyPrecision = [this]( const QString &value ) {
749 double val { value.toDouble() };
750 Qgis::DataType dataType { mRasterDataProvider ? mRasterDataProvider->dataType( mBand ) : Qgis::DataType::Float64 };
751 switch ( dataType )
752 {
763 {
764 return QLocale().toString( std::round( val ), 'f', 0 );
765 }
768 {
769 if ( mLabelPrecisionSpinBox->value() < 0 )
770 {
771 const double factor = std::pow( 10, -mLabelPrecisionSpinBox->value() );
772 val = static_cast<qlonglong>( val / factor ) * factor;
773 return QLocale().toString( val, 'f', 0 );
774 }
775 return QLocale().toString( val, 'f', mLabelPrecisionSpinBox->value() );
776 }
780 {
781 if ( mLabelPrecisionSpinBox->value() < 0 )
782 {
783 const double factor = std::pow( 10, -mLabelPrecisionSpinBox->value() );
784 val = static_cast<qlonglong>( val / factor ) * factor;
785 return QLocale().toString( val, 'f', 0 );
786 }
787 return QLocale().toString( val, 'f', mLabelPrecisionSpinBox->value() );
788 }
789 }
790 return QString();
791 };
792
793 Qgis::ShaderInterpolationMethod interpolation = mColorInterpolationComboBox->currentData().value<Qgis::ShaderInterpolationMethod>();
794 bool discrete = interpolation == Qgis::ShaderInterpolationMethod::Discrete;
795 QString lbl;
796
797 if ( discrete )
798 {
799 if ( row == 0 )
800 {
801 lbl = "<= " + applyPrecision( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
802 }
803 else if ( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble() == std::numeric_limits<double>::infinity() )
804 {
805 lbl = "> " + applyPrecision( mColormapTreeWidget->topLevelItem( row - 1 )->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
806 }
807 else
808 {
809 lbl = applyPrecision( mColormapTreeWidget->topLevelItem( row - 1 )->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + " - " + applyPrecision( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
810 }
811 }
812 else
813 {
814 lbl = applyPrecision( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
815 }
816
817 return lbl;
818}
819
820void QgsColorRampShaderWidget::changeColor()
821{
822 QList<QTreeWidgetItem *> itemList;
823 itemList = mColormapTreeWidget->selectedItems();
824 if ( itemList.isEmpty() )
825 {
826 return;
827 }
828 QTreeWidgetItem *firstItem = itemList.first();
829
830 QColor currentColor = firstItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
831 QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( qobject_cast<QWidget *>( parent() ) );
832 if ( panel && panel->dockMode() )
833 {
834 QgsCompoundColorWidget *colorWidget = new QgsCompoundColorWidget( panel, currentColor, QgsCompoundColorWidget::LayoutVertical );
835 colorWidget->setPanelTitle( tr( "Select Color" ) );
836 colorWidget->setAllowOpacity( true );
837 connect( colorWidget, &QgsCompoundColorWidget::currentColorChanged, this, [this, itemList]( const QColor &newColor ) {
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, u"Change Color"_s, 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 {
899 QgsColorRampLegendNodeWidget *legendPanel = new QgsColorRampLegendNodeWidget();
900 legendPanel->setPanelTitle( tr( "Legend Settings" ) );
901 legendPanel->setSettings( mLegendSettings );
902 connect( legendPanel, &QgsColorRampLegendNodeWidget::widgetChanged, this, [this, legendPanel] {
903 mLegendSettings = legendPanel->settings();
904 emit widgetChanged();
905 } );
906 panel->openPanel( legendPanel );
907 }
908 else
909 {
910 QgsColorRampLegendNodeDialog dialog( mLegendSettings, this );
911 dialog.setWindowTitle( tr( "Legend Settings" ) );
912 if ( dialog.exec() )
913 {
914 mLegendSettings = dialog.settings();
915 emit widgetChanged();
916 }
917 }
918}
ShaderInterpolationMethod
Color ramp shader interpolation methods.
Definition qgis.h:1483
@ Exact
Assigns the color of the exact matching value in the color ramp item list.
Definition qgis.h:1486
@ Linear
Interpolates the color between two class breaks linearly.
Definition qgis.h:1484
@ Discrete
Assigns the color of the higher class for every pixel between two class breaks.
Definition qgis.h:1485
ShaderClassificationMethod
Color ramp shader classification methods.
Definition qgis.h:1498
@ Continuous
Uses breaks from color palette.
Definition qgis.h:1499
@ Quantile
Uses quantile (i.e. equal pixel) count.
Definition qgis.h:1501
@ EqualInterval
Uses equal interval.
Definition qgis.h:1500
DataType
Raster data types.
Definition qgis.h:379
@ CInt32
Complex Int32.
Definition qgis.h:390
@ Float32
Thirty two bit floating point (float).
Definition qgis.h:387
@ CFloat64
Complex Float64.
Definition qgis.h:392
@ Int16
Sixteen bit signed integer (qint16).
Definition qgis.h:384
@ ARGB32_Premultiplied
Color, alpha, red, green, blue, 4 bytes the same as QImage::Format_ARGB32_Premultiplied.
Definition qgis.h:394
@ Int8
Eight bit signed integer (qint8) (added in QGIS 3.30).
Definition qgis.h:382
@ UInt16
Sixteen bit unsigned integer (quint16).
Definition qgis.h:383
@ Byte
Eight bit unsigned integer (quint8).
Definition qgis.h:381
@ UnknownDataType
Unknown or unspecified type.
Definition qgis.h:380
@ ARGB32
Color, alpha, red, green, blue, 4 bytes the same as QImage::Format_ARGB32.
Definition qgis.h:393
@ Int32
Thirty two bit signed integer (qint32).
Definition qgis.h:386
@ Float64
Sixty four bit floating point (double).
Definition qgis.h:388
@ CFloat32
Complex Float32.
Definition qgis.h:391
@ CInt16
Complex Int16.
Definition qgis.h:389
@ UInt32
Thirty two bit unsigned integer (quint32).
Definition qgis.h:385
static const double UI_SCALE_FACTOR
UI scaling factor.
Definition qgis.h:6523
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:6924
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition qgis.h:6828
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:63