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