QGIS API Documentation 3.41.0-Master (af5edcb665c)
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 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
211void QgsColorRampShaderWidget::setUnitFromLabels()
212{
213 QStringList allSuffixes;
214 QString label;
215 int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
216 QTreeWidgetItem *currentItem = nullptr;
217 for ( int i = 0; i < topLevelItemCount; ++i )
218 {
219 currentItem = mColormapTreeWidget->topLevelItem( i );
220 //If the item is null or does not have a pixel values set, skip
221 if ( !currentItem || currentItem->text( ValueColumn ).isEmpty() )
222 {
223 continue;
224 }
225
226 label = createLabel( currentItem, i, QString() );
227
228 if ( currentItem->text( LabelColumn ).startsWith( label ) )
229 {
230 allSuffixes.append( currentItem->text( LabelColumn ).mid( label.length() ) );
231 }
232 }
233 // find most common suffix
234 QStringList suffixes = QStringList( allSuffixes );
235 suffixes.removeDuplicates();
236 int max = 0;
237 QString unit;
238 for ( int i = 0; i < suffixes.count(); ++i )
239 {
240 int n = allSuffixes.count( suffixes[i] );
241 if ( n > max )
242 {
243 max = n;
244 unit = suffixes[i];
245 }
246 }
247 // Set this suffix as unit if at least used twice
248 if ( max >= 2 )
249 {
250 mUnitLineEdit->setText( unit );
251 }
252}
253
254#ifdef QGISDEBUG
255void QgsColorRampShaderWidget::dumpClasses()
256{
257 for ( int row = 0; row < mColormapTreeWidget->model()->rowCount(); ++row )
258 {
259 const auto labelData { mColormapTreeWidget->model()->itemData( mColormapTreeWidget->model()->index( row, LabelColumn ) ) };
260 const auto valueData { mColormapTreeWidget->model()->itemData( mColormapTreeWidget->model()->index( row, ValueColumn ) ) };
261 QgsDebugMsgLevel( QStringLiteral( "Class %1 : %2 %3" ).arg( row ).arg( labelData[Qt::ItemDataRole::DisplayRole].toString(), valueData[Qt::ItemDataRole::DisplayRole].toString() ), 2 );
262 }
263}
264#endif
265
266void QgsColorRampShaderWidget::mAddEntryButton_clicked()
267{
268 QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget );
269 newItem->setData( ValueColumn, Qt::ItemDataRole::DisplayRole, 0 );
270 newItem->setData( ColorColumn, Qt::ItemDataRole::EditRole, QColor( Qt::magenta ) );
271 newItem->setText( LabelColumn, QString() );
272 newItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable );
273 connect( newItem, &QgsTreeWidgetItemObject::itemEdited, this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited );
274 autoLabel();
275
277 updateColorRamp();
278 emit widgetChanged();
279}
280
281void QgsColorRampShaderWidget::mDeleteEntryButton_clicked()
282{
283 QList<QTreeWidgetItem *> itemList;
284 itemList = mColormapTreeWidget->selectedItems();
285 if ( itemList.isEmpty() )
286 {
287 return;
288 }
289
290 const auto constItemList = itemList;
291 for ( QTreeWidgetItem *item : constItemList )
292 {
293 delete item;
294 }
295
297 updateColorRamp();
298 emit widgetChanged();
299}
300
302{
303 std::unique_ptr<QgsColorRamp> ramp( btnColorRamp->colorRamp() );
304 if ( !ramp || std::isnan( mMin ) || std::isnan( mMax ) )
305 {
306 return;
307 }
308
309 std::unique_ptr<QgsColorRampShader> colorRampShader( new QgsColorRampShader(
310 mMin, mMax,
311 ramp.release(),
312 mColorInterpolationComboBox->currentData().value<Qgis::ShaderInterpolationMethod>(),
313 mClassificationModeComboBox->currentData().value<Qgis::ShaderClassificationMethod>()
314 )
315 );
316
317 // only for Quantile we need band and provider and extent
318 colorRampShader->classifyColorRamp( mNumberOfEntriesSpinBox->value(), mBand, mExtent, mRasterDataProvider );
319 colorRampShader->setClip( mClipCheckBox->isChecked() );
320
321 mColormapTreeWidget->clear();
322
323 const QList<QgsColorRampShader::ColorRampItem> colorRampItemList = colorRampShader->colorRampItemList();
324 QList<QgsColorRampShader::ColorRampItem>::const_iterator it = colorRampItemList.constBegin();
325 for ( ; it != colorRampItemList.end(); ++it )
326 {
327 QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget );
328 newItem->setData( ValueColumn, Qt::ItemDataRole::DisplayRole, it->value );
329 newItem->setData( ColorColumn, Qt::ItemDataRole::EditRole, it->color );
330 newItem->setText( LabelColumn, QString() ); // Labels will be populated in autoLabel()
331 newItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable );
332 connect( newItem, &QgsTreeWidgetItemObject::itemEdited, this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited );
333 }
334
335 mClipCheckBox->setChecked( colorRampShader->clip() );
336
337 autoLabel();
338 emit widgetChanged();
339}
340
341void QgsColorRampShaderWidget::mClassificationModeComboBox_currentIndexChanged( int index )
342{
343 Qgis::ShaderClassificationMethod mode = mClassificationModeComboBox->itemData( index ).value<Qgis::ShaderClassificationMethod>();
344 mNumberOfEntriesSpinBox->setEnabled( mode != Qgis::ShaderClassificationMethod::Continuous );
345 emit classificationModeChanged( mode );
346}
347
348void QgsColorRampShaderWidget::updateColorRamp()
349{
350 std::unique_ptr<QgsColorRamp> ramp( shader().createColorRamp() );
351 whileBlocking( btnColorRamp )->setColorRamp( ramp.get() );
352}
353
354void QgsColorRampShaderWidget::applyColorRamp()
355{
356 std::unique_ptr<QgsColorRamp> ramp( btnColorRamp->colorRamp() );
357 if ( !ramp )
358 {
359 return;
360 }
361
362 if ( !btnColorRamp->colorRampName().isEmpty() )
363 {
364 // Remember last used color ramp
365 QgsSettings settings;
366 settings.setValue( QStringLiteral( "Raster/defaultPalette" ), btnColorRamp->colorRampName() );
367 }
368
369 bool enableContinuous = ( ramp->count() > 0 );
370 mClassificationModeComboBox->setEnabled( enableContinuous );
371 if ( !enableContinuous )
372 {
373 mClassificationModeComboBox->setCurrentIndex( mClassificationModeComboBox->findData( QVariant::fromValue( Qgis::ShaderClassificationMethod::EqualInterval ) ) );
374 }
375
376 int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
377 if ( topLevelItemCount > 0 )
378 {
379 // We need to have valid min/max values here. If we haven't, load from colormap
380 double min, max;
381 if ( std::isnan( mMin ) || std::isnan( mMax ) )
382 {
383 colormapMinMax( min, max );
384 }
385 else
386 {
387 min = mMin;
388 max = mMax;
389 }
390
391 // if the list values has been customized, maintain pre-existing values
392 QTreeWidgetItem *currentItem = nullptr;
393 for ( int i = 0; i < topLevelItemCount; ++i )
394 {
395 currentItem = mColormapTreeWidget->topLevelItem( i );
396 if ( !currentItem )
397 {
398 continue;
399 }
400
401 double value = currentItem->data( ValueColumn, Qt::ItemDataRole::EditRole ).toDouble();
402 double position = ( value - min ) / ( max - min );
403 whileBlocking( static_cast<QgsTreeWidgetItemObject *>( currentItem ) )->setData( ColorColumn, Qt::ItemDataRole::EditRole, ramp->color( position ) );
404 }
405
406 emit widgetChanged();
407 }
408 else
409 {
410 classify();
411 }
412}
413
414void QgsColorRampShaderWidget::populateColormapTreeWidget( const QList<QgsColorRampShader::ColorRampItem> &colorRampItems )
415{
416 mColormapTreeWidget->clear();
417 QList<QgsColorRampShader::ColorRampItem>::const_iterator it = colorRampItems.constBegin();
418 int i = 0;
419 for ( ; it != colorRampItems.constEnd(); ++it )
420 {
421 QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget );
422 newItem->setData( ValueColumn, Qt::ItemDataRole::DisplayRole, it->value );
423 newItem->setData( ColorColumn, Qt::ItemDataRole::EditRole, it->color );
424 newItem->setText( LabelColumn, it->label );
425 newItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable );
426 connect( newItem, &QgsTreeWidgetItemObject::itemEdited, this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited );
427 ++i;
428 }
429
430#ifdef QGISDEBUG
431 dumpClasses();
432#endif
433
434 setUnitFromLabels();
435
436 // Now we have the suffix
437 const QString unit = mUnitLineEdit->text();
438 for ( i = 0; i < mColormapTreeWidget->topLevelItemCount(); i++ )
439 {
440 QgsTreeWidgetItemObject *currentItem { static_cast<QgsTreeWidgetItemObject *>( mColormapTreeWidget->topLevelItem( i ) ) };
441 QString lbl { createLabel( currentItem, i, unit ) };
442 if ( currentItem->text( LabelColumn ).isEmpty() || currentItem->text( LabelColumn ) == lbl || currentItem->foreground( LabelColumn ).color() == QColor( Qt::gray ) )
443 {
444 currentItem->setText( LabelColumn, lbl );
445 currentItem->setForeground( LabelColumn, QBrush( QColor( Qt::gray ) ) );
446 }
447 }
448}
449
450void QgsColorRampShaderWidget::mLoadFromBandButton_clicked()
451{
452 if ( !mRasterDataProvider )
453 return;
454
455 QList<QgsColorRampShader::ColorRampItem> colorRampList = mRasterDataProvider->colorTable( mBand );
456 if ( !colorRampList.isEmpty() )
457 {
458 populateColormapTreeWidget( colorRampList );
459 mColorInterpolationComboBox->setCurrentIndex( mColorInterpolationComboBox->findData( QVariant::fromValue( Qgis::ShaderInterpolationMethod::Linear ) ) );
460 }
461 else
462 {
463 QMessageBox::warning( this, tr( "Load Color Map" ), tr( "The color map for band %1 has no entries." ).arg( mBand ) );
464 }
466 emit widgetChanged();
467}
468
469void QgsColorRampShaderWidget::mLoadFromFileButton_clicked()
470{
471 QgsSettings settings;
472 QString lastDir = settings.value( QStringLiteral( "lastColorMapDir" ), QDir::homePath() ).toString();
473 const QString fileName = QFileDialog::getOpenFileName( this, tr( "Load Color Map from File" ), lastDir, tr( "Textfile (*.txt)" ) );
474 if ( fileName.isEmpty() )
475 return;
476
477 QList<QgsColorRampShader::ColorRampItem> colorRampItems;
479 QStringList errors;
480 if ( QgsRasterRendererUtils::parseColorMapFile( fileName, colorRampItems, type, errors ) )
481 {
482 //clear the current tree
483 mColormapTreeWidget->clear();
484
485 mColorInterpolationComboBox->setCurrentIndex( mColorInterpolationComboBox->findData( QVariant::fromValue( type ) ) );
486
487 populateColormapTreeWidget( colorRampItems );
488
489 if ( !errors.empty() )
490 {
491 QMessageBox::warning( this, tr( "Load Color Map from File" ), tr( "The following lines contained errors\n\n" ) + errors.join( '\n' ) );
492 }
493 }
494 else
495 {
496 const QString error = tr( "An error occurred while reading the color map\n\n" ) + errors.join( '\n' );
497 QMessageBox::warning( this, tr( "Load Color Map from File" ), error );
498 }
499
500 QFileInfo fileInfo( fileName );
501 settings.setValue( QStringLiteral( "lastColorMapDir" ), fileInfo.absoluteDir().absolutePath() );
502
504 updateColorRamp();
505 emit widgetChanged();
506}
507
508void QgsColorRampShaderWidget::mExportToFileButton_clicked()
509{
510 QgsSettings settings;
511 QString lastDir = settings.value( QStringLiteral( "lastColorMapDir" ), QDir::homePath() ).toString();
512 QString fileName = QFileDialog::getSaveFileName( this, tr( "Save Color Map as File" ), lastDir, tr( "Textfile (*.txt)" ) );
513 if ( fileName.isEmpty() )
514 return;
515
516 fileName = QgsFileUtils::ensureFileNameHasExtension( fileName, QStringList() << QStringLiteral( "txt" ) );
517
518 QList<QgsColorRampShader::ColorRampItem> colorRampItems;
519 int topLevelItemCount = mColormapTreeWidget->topLevelItemCount();
520 for ( int i = 0; i < topLevelItemCount; ++i )
521 {
522 QTreeWidgetItem *currentItem = mColormapTreeWidget->topLevelItem( i );
523 if ( !currentItem )
524 {
525 continue;
526 }
527
529 item.value = currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble();
530 item.color = currentItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
531 item.label = currentItem->text( LabelColumn );
532 colorRampItems << item;
533 }
534
535 if ( !QgsRasterRendererUtils::saveColorMapFile( fileName, colorRampItems, mColorInterpolationComboBox->currentData().value<Qgis::ShaderInterpolationMethod>() ) )
536 {
537 QMessageBox::warning( this, tr( "Save Color Map as File" ), tr( "Write access denied. Adjust the file permissions and try again.\n\n" ) );
538 }
539
540 QFileInfo fileInfo( fileName );
541 settings.setValue( QStringLiteral( "lastColorMapDir" ), fileInfo.absoluteDir().absolutePath() );
542}
543
544void QgsColorRampShaderWidget::mUnitLineEdit_textEdited( const QString & )
545{
546 autoLabel();
547
548 if ( !mBlockChanges )
549 emit widgetChanged();
550}
551
552void QgsColorRampShaderWidget::mColormapTreeWidget_itemDoubleClicked( QTreeWidgetItem *item, int column )
553{
554 if ( !item )
555 {
556 return;
557 }
558
559 if ( column == LabelColumn )
560 {
561 // Set text color to default black, which signifies a manually edited label
562 item->setForeground( LabelColumn, QBrush() );
563 }
564}
565
566void QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited( QTreeWidgetItem *item, int column )
567{
568 Q_UNUSED( item )
569
570 switch ( column )
571 {
572 case ValueColumn:
573 {
574 autoLabel();
576 updateColorRamp();
577 emit widgetChanged();
578 break;
579 }
580
581 case LabelColumn:
582 {
583 // call autoLabel to fill when empty or gray out when same as autoLabel
584 autoLabel();
585 emit widgetChanged();
586 break;
587 }
588
589 case ColorColumn:
590 {
592 updateColorRamp();
593 emit widgetChanged();
594 break;
595 }
596 }
597}
598
600{
601 mBlockChanges++;
602
603 // Those objects are connected to classify() the color ramp shader if they change, or call widget change
604 // need to block them to avoid to classify and to alter the color ramp, or to call duplicate widget change
605 whileBlocking( mClipCheckBox )->setChecked( colorRampShader.clip() );
606 whileBlocking( mColorInterpolationComboBox )->setCurrentIndex( mColorInterpolationComboBox->findData( QVariant::fromValue( colorRampShader.colorRampType() ) ) );
607 mColorInterpolationComboBox_currentIndexChanged( mColorInterpolationComboBox->currentIndex() );
608 whileBlocking( mClassificationModeComboBox )->setCurrentIndex( mClassificationModeComboBox->findData( QVariant::fromValue( colorRampShader.classificationMode() ) ) );
609 mClassificationModeComboBox_currentIndexChanged( mClassificationModeComboBox->currentIndex() );
610 whileBlocking( mNumberOfEntriesSpinBox )->setValue( colorRampShader.colorRampItemList().count() ); // some default
611
612 if ( colorRampShader.sourceColorRamp() )
613 {
614 whileBlocking( btnColorRamp )->setColorRamp( colorRampShader.sourceColorRamp() );
615 }
616 else
617 {
618 QgsSettings settings;
619 QString defaultPalette = settings.value( QStringLiteral( "/Raster/defaultPalette" ), "Spectral" ).toString();
620 btnColorRamp->setColorRampFromName( defaultPalette );
621 }
622
623 mLabelPrecisionSpinBox->setValue( colorRampShader.labelPrecision() );
624
626
627 if ( colorRampShader.legendSettings() )
628 mLegendSettings = *colorRampShader.legendSettings();
629
630 mBlockChanges--;
631 emit widgetChanged();
632}
633
634void QgsColorRampShaderWidget::mColorInterpolationComboBox_currentIndexChanged( int index )
635{
636 Qgis::ShaderInterpolationMethod interpolation = mColorInterpolationComboBox->itemData( index ).value<Qgis::ShaderInterpolationMethod>();
637
638 mClipCheckBox->setEnabled( interpolation == Qgis::ShaderInterpolationMethod::Linear );
639
640 QString valueLabel;
641 QString valueToolTip;
642 switch ( interpolation )
643 {
645 valueLabel = tr( "Value" );
646 valueToolTip = tr( "Value for color stop" );
647 mLegendSettingsButton->setEnabled( true );
648 break;
650 valueLabel = tr( "Value <=" );
651 valueToolTip = tr( "Maximum value for class" );
652 mLegendSettingsButton->setEnabled( false );
653 break;
655 valueLabel = tr( "Value =" );
656 valueToolTip = tr( "Value for color" );
657 mLegendSettingsButton->setEnabled( false );
658 break;
659 }
660
661 QTreeWidgetItem *header = mColormapTreeWidget->headerItem();
662 header->setText( ValueColumn, valueLabel );
663 header->setToolTip( ValueColumn, valueToolTip );
664
665 autoLabel();
666 emit widgetChanged();
667}
668
670{
671 if ( !qgsDoubleNear( mMin, min ) || !qgsDoubleNear( mMax, max ) )
672 {
673 setMinimumMaximum( min, max );
674 classify();
675 }
676}
677
679{
680 mMin = min;
681 mMax = max;
682 resetClassifyButton();
683}
684
686{
687 return mMin;
688}
689
691{
692 return mMax;
693}
694
695bool QgsColorRampShaderWidget::colormapMinMax( double &min, double &max ) const
696{
697 QTreeWidgetItem *item = mColormapTreeWidget->topLevelItem( 0 );
698 if ( !item )
699 {
700 return false;
701 }
702
703 // If using discrete, the first and last items contain the upper and lower
704 // values of the first and last classes, we don't want these values but real min/max
705 if ( !std::isnan( mMin ) && !std::isnan( mMax ) && mColorInterpolationComboBox->currentData().value<Qgis::ShaderInterpolationMethod>() == Qgis::ShaderInterpolationMethod::Discrete )
706 {
707 min = mMin;
708 max = mMax;
709 }
710 else
711 {
712 min = item->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble();
713 item = mColormapTreeWidget->topLevelItem( mColormapTreeWidget->topLevelItemCount() - 1 );
714 max = item->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble();
715 }
716 return true;
717}
718
720{
721 double min = 0, max = 0;
722 if ( !colormapMinMax( min, max ) )
723 {
724 return;
725 }
726
727 if ( !qgsDoubleNear( mMin, min ) || !qgsDoubleNear( mMax, max ) )
728 {
729 mMin = min;
730 mMax = max;
731 emit minimumMaximumChangedFromTree( min, max );
732 }
733}
734
735void QgsColorRampShaderWidget::resetClassifyButton()
736{
737 mClassifyButton->setEnabled( true );
738 if ( std::isnan( mMin ) || std::isnan( mMax ) || mMin >= mMax )
739 {
740 mClassifyButton->setEnabled( false );
741 }
742}
743
744QString QgsColorRampShaderWidget::createLabel( QTreeWidgetItem *currentItem, int row, const QString unit )
745{
746 auto applyPrecision = [=]( const QString &value ) {
747 double val { value.toDouble() };
748 Qgis::DataType dataType { mRasterDataProvider ? mRasterDataProvider->dataType( mBand ) : Qgis::DataType::Float64 };
749 switch ( dataType )
750 {
761 {
762 return QLocale().toString( std::round( val ), 'f', 0 );
763 }
766 {
767 if ( mLabelPrecisionSpinBox->value() < 0 )
768 {
769 const double factor = std::pow( 10, -mLabelPrecisionSpinBox->value() );
770 val = static_cast<qlonglong>( val / factor ) * factor;
771 return QLocale().toString( val, 'f', 0 );
772 }
773 return QLocale().toString( val, 'f', mLabelPrecisionSpinBox->value() );
774 }
778 {
779 if ( mLabelPrecisionSpinBox->value() < 0 )
780 {
781 const double factor = std::pow( 10, -mLabelPrecisionSpinBox->value() );
782 val = static_cast<qlonglong>( val / factor ) * factor;
783 return QLocale().toString( val, 'f', 0 );
784 }
785 return QLocale().toString( val, 'f', mLabelPrecisionSpinBox->value() );
786 }
787 }
788 return QString();
789 };
790
791 Qgis::ShaderInterpolationMethod interpolation = mColorInterpolationComboBox->currentData().value<Qgis::ShaderInterpolationMethod>();
792 bool discrete = interpolation == Qgis::ShaderInterpolationMethod::Discrete;
793 QString lbl;
794
795 if ( discrete )
796 {
797 if ( row == 0 )
798 {
799 lbl = "<= " + applyPrecision( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
800 }
801 else if ( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toDouble() == std::numeric_limits<double>::infinity() )
802 {
803 lbl = "> " + applyPrecision( mColormapTreeWidget->topLevelItem( row - 1 )->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
804 }
805 else
806 {
807 lbl = applyPrecision( mColormapTreeWidget->topLevelItem( row - 1 )->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + " - " + applyPrecision( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
808 }
809 }
810 else
811 {
812 lbl = applyPrecision( currentItem->data( ValueColumn, Qt::ItemDataRole::DisplayRole ).toString() ) + unit;
813 }
814
815 return lbl;
816}
817
818void QgsColorRampShaderWidget::changeColor()
819{
820 QList<QTreeWidgetItem *> itemList;
821 itemList = mColormapTreeWidget->selectedItems();
822 if ( itemList.isEmpty() )
823 {
824 return;
825 }
826 QTreeWidgetItem *firstItem = itemList.first();
827
828 QColor currentColor = firstItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
829 QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( qobject_cast<QWidget *>( parent() ) );
830 if ( panel && panel->dockMode() )
831 {
833 colorWidget->setPanelTitle( tr( "Select Color" ) );
834 colorWidget->setAllowOpacity( true );
835 connect( colorWidget, &QgsCompoundColorWidget::currentColorChanged, this, [=]( const QColor &newColor ) {
836 for ( QTreeWidgetItem *item : std::as_const( itemList ) )
837 {
838 item->setData( ColorColumn, Qt::ItemDataRole::EditRole, newColor );
839 }
840
842 emit widgetChanged();
843 } );
844 panel->openPanel( colorWidget );
845 }
846 else
847 {
848 // modal dialog version... yuck
849 QColor newColor = QgsColorDialog::getColor( currentColor, this, QStringLiteral( "Change Color" ), true );
850 if ( newColor.isValid() )
851 {
852 for ( QTreeWidgetItem *item : std::as_const( itemList ) )
853 {
854 item->setData( ColorColumn, Qt::ItemDataRole::EditRole, newColor );
855 }
856
858 emit widgetChanged();
859 }
860 }
861}
862
863void QgsColorRampShaderWidget::changeOpacity()
864{
865 QList<QTreeWidgetItem *> itemList;
866 itemList = mColormapTreeWidget->selectedItems();
867 if ( itemList.isEmpty() )
868 {
869 return;
870 }
871 QTreeWidgetItem *firstItem = itemList.first();
872
873 bool ok;
874 double oldOpacity = firstItem->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>().alpha() / 255 * 100;
875 double opacity = QInputDialog::getDouble( this, tr( "Opacity" ), tr( "Change color opacity [%]" ), oldOpacity, 0.0, 100.0, 0, &ok );
876 if ( ok )
877 {
878 int newOpacity = static_cast<int>( opacity / 100 * 255 );
879 const auto constItemList = itemList;
880 for ( QTreeWidgetItem *item : constItemList )
881 {
882 QColor newColor = item->data( ColorColumn, Qt::ItemDataRole::EditRole ).value<QColor>();
883 newColor.setAlpha( newOpacity );
884 item->setData( ColorColumn, Qt::ItemDataRole::EditRole, newColor );
885 }
886
888 emit widgetChanged();
889 }
890}
891
892void QgsColorRampShaderWidget::showLegendSettings()
893{
894 QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( qobject_cast<QWidget *>( parent() ) );
895 if ( panel && panel->dockMode() )
896 {
898 legendPanel->setPanelTitle( tr( "Legend Settings" ) );
899 legendPanel->setSettings( mLegendSettings );
900 connect( legendPanel, &QgsColorRampLegendNodeWidget::widgetChanged, this, [=] {
901 mLegendSettings = legendPanel->settings();
902 emit widgetChanged();
903 } );
904 panel->openPanel( legendPanel );
905 }
906 else
907 {
908 QgsColorRampLegendNodeDialog dialog( mLegendSettings, this );
909 dialog.setWindowTitle( tr( "Legend Settings" ) );
910 if ( dialog.exec() )
911 {
912 mLegendSettings = dialog.settings();
913 emit widgetChanged();
914 }
915 }
916}
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:5775
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:6066
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition qgis.h:5970
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:39