QGIS API Documentation  3.20.0-Odense (decaadbb31)
qgspointcloudclassifiedrendererwidget.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgspointcloudclassifiedrendererwidget.cpp
3  ---------------------
4  begin : November 2020
5  copyright : (C) 2020 by Nyall Dawson
6  email : nyall dot dawson 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 #include "qgscontrastenhancement.h"
20 #include "qgspointcloudlayer.h"
22 #include "qgsdoublevalidator.h"
23 #include "qgsstyle.h"
24 #include "qgsguiutils.h"
25 #include "qgscompoundcolorwidget.h"
26 #include "qgscolordialog.h"
27 #include "qgsapplication.h"
28 #include "qgscolorschemeregistry.h"
29 
30 #include <QMimeData>
31 
33 
34 QgsPointCloudClassifiedRendererModel::QgsPointCloudClassifiedRendererModel( QObject *parent )
35  : QAbstractItemModel( parent )
36  , mMimeFormat( QStringLiteral( "application/x-qgspointcloudclassifiedrenderermodel" ) )
37 {
38 }
39 
40 void QgsPointCloudClassifiedRendererModel::setRendererCategories( const QgsPointCloudCategoryList &categories )
41 {
42  if ( !mCategories.empty() )
43  {
44  beginRemoveRows( QModelIndex(), 0, std::max( mCategories.size() - 1, 0 ) );
45  mCategories.clear();
46  endRemoveRows();
47  }
48  if ( categories.size() > 0 )
49  {
50  beginInsertRows( QModelIndex(), 0, categories.size() - 1 );
51  mCategories = categories;
52  endInsertRows();
53  }
54 }
55 
56 void QgsPointCloudClassifiedRendererModel::addCategory( const QgsPointCloudCategory &cat )
57 {
58  int idx = mCategories.size();
59  beginInsertRows( QModelIndex(), idx, idx );
60  mCategories.append( cat );
61  endInsertRows();
62 
63  emit categoriesChanged();
64 }
65 
66 QgsPointCloudCategory QgsPointCloudClassifiedRendererModel::category( const QModelIndex &index )
67 {
68  int row = index.row();
69  if ( row >= mCategories.size() )
70  {
71  return QgsPointCloudCategory();
72  }
73  return mCategories.at( row );
74 }
75 
76 Qt::ItemFlags QgsPointCloudClassifiedRendererModel::flags( const QModelIndex &index ) const
77 {
78  if ( !index.isValid() || mCategories.empty() )
79  {
80  return Qt::ItemIsDropEnabled;
81  }
82 
83  Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsUserCheckable;
84  if ( index.column() == 1 )
85  {
86  flags |= Qt::ItemIsEditable;
87  }
88  else if ( index.column() == 2 )
89  {
90  flags |= Qt::ItemIsEditable;
91  }
92  return flags;
93 }
94 
95 Qt::DropActions QgsPointCloudClassifiedRendererModel::supportedDropActions() const
96 {
97  return Qt::MoveAction;
98 }
99 
100 QVariant QgsPointCloudClassifiedRendererModel::data( const QModelIndex &index, int role ) const
101 {
102  if ( !index.isValid() || mCategories.empty() )
103  return QVariant();
104 
105  const QgsPointCloudCategory category = mCategories.value( index.row() );
106 
107  switch ( role )
108  {
109  case Qt::CheckStateRole:
110  {
111  if ( index.column() == 0 )
112  {
113  return category.renderState() ? Qt::Checked : Qt::Unchecked;
114  }
115  break;
116  }
117 
118  case Qt::DisplayRole:
119  case Qt::ToolTipRole:
120  {
121  switch ( index.column() )
122  {
123  case 1:
124  {
125  return QString::number( category.value() );
126  }
127  case 2:
128  return category.label();
129  }
130  break;
131  }
132 
133  case Qt::DecorationRole:
134  {
135  if ( index.column() == 0 )
136  {
137  const int iconSize = QgsGuiUtils::scaleIconSize( 16 );
138  QPixmap pix( iconSize, iconSize );
139  pix.fill( category.color() );
140  return QIcon( pix );
141  }
142  break;
143  }
144 
145  case Qt::TextAlignmentRole:
146  {
147  return ( index.column() == 0 ) ? Qt::AlignHCenter : Qt::AlignLeft;
148  }
149 
150  case Qt::EditRole:
151  {
152  switch ( index.column() )
153  {
154  case 1:
155  {
156  return QString::number( category.value() );
157  }
158 
159  case 2:
160  return category.label();
161  }
162  break;
163  }
164  }
165 
166  return QVariant();
167 }
168 
169 bool QgsPointCloudClassifiedRendererModel::setData( const QModelIndex &index, const QVariant &value, int role )
170 {
171  if ( !index.isValid() )
172  return false;
173 
174  if ( index.column() == 0 && role == Qt::CheckStateRole )
175  {
176  mCategories[ index.row() ].setRenderState( value == Qt::Checked );
177  emit dataChanged( index, index );
178  emit categoriesChanged();
179  return true;
180  }
181 
182  if ( role != Qt::EditRole )
183  return false;
184 
185  switch ( index.column() )
186  {
187  case 1: // value
188  {
189  const int val = value.toInt();
190  mCategories[ index.row() ].setValue( val );
191  break;
192  }
193  case 2: // label
194  {
195  mCategories[ index.row() ].setLabel( value.toString() );
196  break;
197  }
198  default:
199  return false;
200  }
201 
202  emit dataChanged( index, index );
203  emit categoriesChanged();
204  return true;
205 }
206 
207 QVariant QgsPointCloudClassifiedRendererModel::headerData( int section, Qt::Orientation orientation, int role ) const
208 {
209  if ( orientation == Qt::Horizontal && role == Qt::DisplayRole && section >= 0 && section < 3 )
210  {
211  QStringList lst;
212  lst << tr( "Color" ) << tr( "Value" ) << tr( "Legend" );
213  return lst.value( section );
214  }
215  return QVariant();
216 }
217 
218 int QgsPointCloudClassifiedRendererModel::rowCount( const QModelIndex &parent ) const
219 {
220  if ( parent.isValid() )
221  {
222  return 0;
223  }
224  return mCategories.size();
225 }
226 
227 int QgsPointCloudClassifiedRendererModel::columnCount( const QModelIndex &index ) const
228 {
229  Q_UNUSED( index )
230  return 3;
231 }
232 
233 QModelIndex QgsPointCloudClassifiedRendererModel::index( int row, int column, const QModelIndex &parent ) const
234 {
235  if ( hasIndex( row, column, parent ) )
236  {
237  return createIndex( row, column );
238  }
239  return QModelIndex();
240 }
241 
242 QModelIndex QgsPointCloudClassifiedRendererModel::parent( const QModelIndex &index ) const
243 {
244  Q_UNUSED( index )
245  return QModelIndex();
246 }
247 
248 QStringList QgsPointCloudClassifiedRendererModel::mimeTypes() const
249 {
250  QStringList types;
251  types << mMimeFormat;
252  return types;
253 }
254 
255 QMimeData *QgsPointCloudClassifiedRendererModel::mimeData( const QModelIndexList &indexes ) const
256 {
257  QMimeData *mimeData = new QMimeData();
258  QByteArray encodedData;
259 
260  QDataStream stream( &encodedData, QIODevice::WriteOnly );
261 
262  // Create list of rows
263  const auto constIndexes = indexes;
264  for ( const QModelIndex &index : constIndexes )
265  {
266  if ( !index.isValid() || index.column() != 0 )
267  continue;
268 
269  stream << index.row();
270  }
271  mimeData->setData( mMimeFormat, encodedData );
272  return mimeData;
273 }
274 
275 bool QgsPointCloudClassifiedRendererModel::dropMimeData( const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent )
276 {
277  Q_UNUSED( row )
278  Q_UNUSED( column )
279  if ( action != Qt::MoveAction )
280  return true;
281 
282  if ( !data->hasFormat( mMimeFormat ) )
283  return false;
284 
285  QByteArray encodedData = data->data( mMimeFormat );
286  QDataStream stream( &encodedData, QIODevice::ReadOnly );
287 
288  QVector<int> rows;
289  while ( !stream.atEnd() )
290  {
291  int r;
292  stream >> r;
293  rows.append( r );
294  }
295 
296  int to = parent.row();
297  // to is -1 if dragged outside items, i.e. below any item,
298  // then move to the last position
299  if ( to == -1 )
300  to = mCategories.size(); // out of rang ok, will be decreased
301  for ( int i = rows.size() - 1; i >= 0; i-- )
302  {
303  int t = to;
304  if ( rows[i] < t )
305  t--;
306 
307  if ( !( rows[i] < 0 || rows[i] >= mCategories.size() || t < 0 || t >= mCategories.size() ) )
308  {
309  mCategories.move( rows[i], t );
310  }
311 
312  // current moved under another, shift its index up
313  for ( int j = 0; j < i; j++ )
314  {
315  if ( to < rows[j] && rows[i] > rows[j] )
316  rows[j] += 1;
317  }
318  // removed under 'to' so the target shifted down
319  if ( rows[i] < to )
320  to--;
321  }
322  emit dataChanged( createIndex( 0, 0 ), createIndex( mCategories.size(), 0 ) );
323  emit categoriesChanged();
324  return false;
325 }
326 
327 void QgsPointCloudClassifiedRendererModel::deleteRows( QList<int> rows )
328 {
329  std::sort( rows.begin(), rows.end() ); // list might be unsorted, depending on how the user selected the rows
330  for ( int i = rows.size() - 1; i >= 0; i-- )
331  {
332  beginRemoveRows( QModelIndex(), rows[i], rows[i] );
333  mCategories.removeAt( rows[i] );
334  endRemoveRows();
335  }
336  emit categoriesChanged();
337 }
338 
339 void QgsPointCloudClassifiedRendererModel::removeAllRows()
340 {
341  beginRemoveRows( QModelIndex(), 0, mCategories.size() - 1 );
342  mCategories.clear();
343  endRemoveRows();
344  emit categoriesChanged();
345 }
346 
347 void QgsPointCloudClassifiedRendererModel::setCategoryColor( int row, const QColor &color )
348 {
349  mCategories[row].setColor( color );
350  emit dataChanged( createIndex( row, 0 ), createIndex( row, 0 ) );
351  emit categoriesChanged();
352 }
353 
354 // ------------------------------ View style --------------------------------
355 QgsPointCloudClassifiedRendererViewStyle::QgsPointCloudClassifiedRendererViewStyle( QWidget *parent )
356  : QgsProxyStyle( parent )
357 {}
358 
359 void QgsPointCloudClassifiedRendererViewStyle::drawPrimitive( PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget ) const
360 {
361  if ( element == QStyle::PE_IndicatorItemViewItemDrop && !option->rect.isNull() )
362  {
363  QStyleOption opt( *option );
364  opt.rect.setLeft( 0 );
365  // draw always as line above, because we move item to that index
366  opt.rect.setHeight( 0 );
367  if ( widget )
368  opt.rect.setRight( widget->width() );
369  QProxyStyle::drawPrimitive( element, &opt, painter, widget );
370  return;
371  }
372  QProxyStyle::drawPrimitive( element, option, painter, widget );
373 }
374 
375 
376 QgsPointCloudClassifiedRendererWidget::QgsPointCloudClassifiedRendererWidget( QgsPointCloudLayer *layer, QgsStyle *style )
377  : QgsPointCloudRendererWidget( layer, style )
378 {
379  setupUi( this );
380 
381  mAttributeComboBox->setAllowEmptyAttributeName( true );
383 
384  mModel = new QgsPointCloudClassifiedRendererModel( this );
385  mModel->setRendererCategories( QgsPointCloudClassifiedRenderer::defaultCategories() );
386 
387  if ( layer )
388  {
389  mAttributeComboBox->setLayer( layer );
390 
391  setFromRenderer( layer->renderer() );
392  }
393 
394  viewCategories->setModel( mModel );
395  viewCategories->resizeColumnToContents( 0 );
396  viewCategories->resizeColumnToContents( 1 );
397  viewCategories->resizeColumnToContents( 2 );
398 
399  viewCategories->setStyle( new QgsPointCloudClassifiedRendererViewStyle( viewCategories ) );
400 
401  connect( mAttributeComboBox, &QgsPointCloudAttributeComboBox::attributeChanged,
402  this, &QgsPointCloudClassifiedRendererWidget::emitWidgetChanged );
403  connect( mModel, &QgsPointCloudClassifiedRendererModel::categoriesChanged, this, &QgsPointCloudClassifiedRendererWidget::emitWidgetChanged );
404 
405  connect( viewCategories, &QAbstractItemView::doubleClicked, this, &QgsPointCloudClassifiedRendererWidget::categoriesDoubleClicked );
406  connect( btnAddCategories, &QAbstractButton::clicked, this, &QgsPointCloudClassifiedRendererWidget::addCategories );
407  connect( btnDeleteCategories, &QAbstractButton::clicked, this, &QgsPointCloudClassifiedRendererWidget::deleteCategories );
408  connect( btnDeleteAllCategories, &QAbstractButton::clicked, this, &QgsPointCloudClassifiedRendererWidget::deleteAllCategories );
409  connect( btnAddCategory, &QAbstractButton::clicked, this, &QgsPointCloudClassifiedRendererWidget::addCategory );
410 
411 }
412 
413 QgsPointCloudRendererWidget *QgsPointCloudClassifiedRendererWidget::create( QgsPointCloudLayer *layer, QgsStyle *style, QgsPointCloudRenderer * )
414 {
415  return new QgsPointCloudClassifiedRendererWidget( layer, style );
416 }
417 
418 QgsPointCloudRenderer *QgsPointCloudClassifiedRendererWidget::renderer()
419 {
420  if ( !mLayer )
421  {
422  return nullptr;
423  }
424 
425  std::unique_ptr< QgsPointCloudClassifiedRenderer > renderer = std::make_unique< QgsPointCloudClassifiedRenderer >();
426  renderer->setAttribute( mAttributeComboBox->currentAttribute() );
427  renderer->setCategories( mModel->categories() );
428 
429  return renderer.release();
430 }
431 
432 QgsPointCloudCategoryList QgsPointCloudClassifiedRendererWidget::categoriesList()
433 {
434  return mModel->categories();
435 }
436 
437 QString QgsPointCloudClassifiedRendererWidget::attribute()
438 {
439  return mAttributeComboBox->currentAttribute();
440 }
441 
442 void QgsPointCloudClassifiedRendererWidget::emitWidgetChanged()
443 {
444  if ( !mBlockChangedSignal )
445  emit widgetChanged();
446 }
447 
448 void QgsPointCloudClassifiedRendererWidget::categoriesDoubleClicked( const QModelIndex &idx )
449 {
450  if ( idx.isValid() && idx.column() == 0 )
451  changeCategorySymbol();
452 }
453 
454 void QgsPointCloudClassifiedRendererWidget::addCategories()
455 {
456  if ( !mLayer || !mLayer->dataProvider() )
457  return;
458 
459  const QVariantList providerCategories = mLayer->dataProvider()->metadataClasses( mAttributeComboBox->currentAttribute() );
460  QgsPointCloudCategoryList currentCategories = mModel->categories();
461 
462  for ( const QVariant &providerCategory : providerCategories )
463  {
464  const int newValue = providerCategory.toInt();
465  // does this category already exist?
466  bool found = false;
467  for ( const QgsPointCloudCategory &c : currentCategories )
468  {
469  if ( c.value() == newValue )
470  {
471  found = true;
472  break;
473  }
474  }
475 
476  if ( found )
477  continue;
478 
479  mModel->addCategory( QgsPointCloudCategory( newValue, QgsApplication::colorSchemeRegistry()->fetchRandomStyleColor(), QgsPointCloudDataProvider::translatedLasClassificationCodes().value( newValue, QString::number( newValue ) ) ) );
480  }
481 }
482 
483 void QgsPointCloudClassifiedRendererWidget::addCategory()
484 {
485  if ( !mModel )
486  return;
487 
488  QgsPointCloudCategory cat( mModel->categories().size(), QgsApplication::colorSchemeRegistry()->fetchRandomStyleColor(), QString(), true );
489  mModel->addCategory( cat );
490  emit widgetChanged();
491 }
492 
493 void QgsPointCloudClassifiedRendererWidget::deleteCategories()
494 {
495  QList<int> categoryIndexes = selectedCategories();
496  mModel->deleteRows( categoryIndexes );
497  emit widgetChanged();
498 }
499 
500 void QgsPointCloudClassifiedRendererWidget::deleteAllCategories()
501 {
502  mModel->removeAllRows();
503  emit widgetChanged();
504 }
505 
506 void QgsPointCloudClassifiedRendererWidget::setFromRenderer( const QgsPointCloudRenderer *r )
507 {
508  mBlockChangedSignal = true;
509  if ( const QgsPointCloudClassifiedRenderer *classifiedRenderer = dynamic_cast< const QgsPointCloudClassifiedRenderer *>( r ) )
510  {
511  mModel->setRendererCategories( classifiedRenderer->categories() );
512  mAttributeComboBox->setAttribute( classifiedRenderer->attribute() );
513  }
514  else
515  {
516  if ( mAttributeComboBox->findText( QStringLiteral( "Classification" ) ) > -1 )
517  {
518  mAttributeComboBox->setAttribute( QStringLiteral( "Classification" ) );
519  }
520  else
521  {
522  mAttributeComboBox->setCurrentIndex( mAttributeComboBox->count() > 1 ? 1 : 0 );
523  }
524  }
525  mBlockChangedSignal = false;
526 }
527 
528 void QgsPointCloudClassifiedRendererWidget::setFromCategories( QgsPointCloudCategoryList categories, const QString &attribute )
529 {
530  mBlockChangedSignal = false;
531  mModel->setRendererCategories( categories );
532  if ( !attribute.isEmpty() )
533  {
534  mAttributeComboBox->setAttribute( attribute );
535  }
536  else
537  {
538  if ( mAttributeComboBox->findText( QStringLiteral( "Classification" ) ) > -1 )
539  {
540  mAttributeComboBox->setAttribute( QStringLiteral( "Classification" ) );
541  }
542  else
543  {
544  mAttributeComboBox->setCurrentIndex( mAttributeComboBox->count() > 1 ? 1 : 0 );
545  }
546  }
547  mBlockChangedSignal = false;
548 }
549 
550 void QgsPointCloudClassifiedRendererWidget::changeCategorySymbol()
551 {
552  const int row = currentCategoryRow();
553  if ( row < 0 )
554  return;
555 
556  QgsPointCloudCategory category = mModel->categories().value( row );
557 
559  if ( panel && panel->dockMode() )
560  {
562  colorWidget->setPanelTitle( category.label() );
563  colorWidget->setAllowOpacity( true );
564  colorWidget->setPreviousColor( category.color() );
565 
566  connect( colorWidget, &QgsCompoundColorWidget::currentColorChanged, this, [ = ]( const QColor & newColor )
567  {
568  mModel->setCategoryColor( row, newColor );
569  } );
570  panel->openPanel( colorWidget );
571  }
572  else
573  {
574  const QColor newColor = QgsColorDialog::getColor( category.color(), this, category.label(), true );
575  if ( newColor.isValid() )
576  {
577  mModel->setCategoryColor( row, newColor );
578  }
579  }
580 }
581 
582 QList<int> QgsPointCloudClassifiedRendererWidget::selectedCategories()
583 {
584  QList<int> rows;
585  const QModelIndexList selectedRows = viewCategories->selectionModel()->selectedRows();
586  for ( const QModelIndex &r : selectedRows )
587  {
588  if ( r.isValid() )
589  {
590  rows.append( r.row() );
591  }
592  }
593  return rows;
594 }
595 
596 int QgsPointCloudClassifiedRendererWidget::currentCategoryRow()
597 {
598  QModelIndex idx = viewCategories->selectionModel()->currentIndex();
599  if ( !idx.isValid() )
600  return -1;
601  return idx.row();
602 }
603 
static QgsColorSchemeRegistry * colorSchemeRegistry()
Returns the application's color scheme registry, used for managing color schemes.
static QColor getColor(const QColor &initialColor, QWidget *parent, const QString &title=QString(), bool allowOpacity=false)
Returns a color selection from a color dialog.
QColor fetchRandomStyleColor() const
Returns a random color for use with a new symbol style (e.g.
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 setPreviousColor(const QColor &color)
Sets the color to show in an optional "previous color" section.
void setAllowOpacity(bool allowOpacity)
Sets whether opacity modification (transparency) is permitted for the color dialog.
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 ...
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.
void attributeChanged(const QString &name)
Emitted when the currently selected attribute changes.
Represents an individual category (class) from a QgsPointCloudClassifiedRenderer.
int value() const
Returns the value corresponding to this category.
bool renderState() const
Returns true if the category is currently enabled and should be rendered.
QColor color() const
Returns the color which will be used to render this category.
QString label() const
Returns the label for this category, which is used to represent the category within legends and the l...
Renders point clouds by a classification attribute.
static QgsPointCloudCategoryList defaultCategories()
Returns the default list of categories.
static QMap< int, QString > translatedLasClassificationCodes()
Returns the map of LAS classification code to translated string value, corresponding to the ASPRS Sta...
Represents a map layer supporting display of point clouds.
QgsPointCloudRenderer * renderer()
Returns the 2D renderer for the point cloud.
Base class for point cloud 2D renderer settings widgets.
Abstract base class for 2d point cloud renderers.
A QProxyStyle subclass which correctly sets the base style to match the QGIS application style,...
Definition: qgsproxystyle.h:31
QSize iconSize(bool dockableToolbar)
Returns the user-preferred size of a window's toolbar icons.
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly,...
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
QList< QgsPointCloudCategory > QgsPointCloudCategoryList