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