From 29f0d479d0b71527283d8445ee84d830dbf2645a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 May 2023 11:48:27 +1000 Subject: [PATCH] Add an explicit "Add Layers" button to elevation profile dock This provides a user-friendly why of adding new layers to a plot - clicking it will show a filtered list of possible layers which can be added to the plot, but which currently aren't in the plot. (I.e it will include all raster layers from the project which aren't marked as having elevation data.) Selecting layers will cause them to automatically be marked as having elevation data and immediately added to the plot. --- .../auto_generated/qgselevationutils.sip.in | 16 +++ .../elevation/qgselevationprofilewidget.cpp | 125 ++++++++++++++++++ src/app/elevation/qgselevationprofilewidget.h | 23 ++++ src/core/qgselevationutils.cpp | 36 ++++- src/core/qgselevationutils.h | 16 +++ .../qgselevationprofilelayertreeview.cpp | 5 + .../qgselevationprofilelayertreeview.h | 7 + .../qgselevationprofileaddlayersdialogbase.ui | 102 ++++++++++++++ 8 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 src/ui/qgselevationprofileaddlayersdialogbase.ui diff --git a/python/core/auto_generated/qgselevationutils.sip.in b/python/core/auto_generated/qgselevationutils.sip.in index bf5086f3fc6..a5c2709a9e5 100644 --- a/python/core/auto_generated/qgselevationutils.sip.in +++ b/python/core/auto_generated/qgselevationutils.sip.in @@ -28,6 +28,22 @@ Calculates the elevation range for a ``project``. This method considers the elevation (or z) range available from layers contained within the project and returns the maximal combined elevation range of these layers. +%End + + static bool canEnableElevationForLayer( QgsMapLayer *layer ); +%Docstring +Returns ``True`` if elevation can be enabled for a map ``layer``. + +.. versionadded:: 3.32 +%End + + static bool enableElevationForLayer( QgsMapLayer *layer ); +%Docstring +Automatically enables elevation for a map ``layer``, using reasonable defaults. + +Returns ``True`` if the elevation was enabled successfully. + +.. versionadded:: 3.32 %End }; diff --git a/src/app/elevation/qgselevationprofilewidget.cpp b/src/app/elevation/qgselevationprofilewidget.cpp index 6566ceef498..efc8da93b58 100644 --- a/src/app/elevation/qgselevationprofilewidget.cpp +++ b/src/app/elevation/qgselevationprofilewidget.cpp @@ -20,6 +20,8 @@ #include "qgselevationprofilecanvas.h" #include "qgsdockablewidgethelper.h" #include "qgsmapcanvas.h" +#include "qgsmaplayerelevationproperties.h" +#include "qgsmaplayermodel.h" #include "qgsmaptoolprofilecurve.h" #include "qgsmaptoolprofilecurvefromfeature.h" #include "qgsrubberband.h" @@ -47,6 +49,8 @@ #include "qgselevationprofiletoolmeasure.h" #include "qgssettingsentryimpl.h" #include "qgssettingstree.h" +#include "qgsmaplayerproxymodel.h" +#include "qgselevationutils.h" #include @@ -61,6 +65,71 @@ const QgsSettingsEntryDouble *QgsElevationProfileWidget::settingTolerance = new const QgsSettingsEntryBool *QgsElevationProfileWidget::settingShowLayerTree = new QgsSettingsEntryBool( QStringLiteral( "show-layer-tree" ), QgsSettingsTree::sTreeElevationProfile, true, QStringLiteral( "Whether the layer tree should be shown for elevation profile plots" ) ); const QgsSettingsEntryBool *QgsElevationProfileWidget::settingLockAxis = new QgsSettingsEntryBool( QStringLiteral( "lock-axis-ratio" ), QgsSettingsTree::sTreeElevationProfile, false, QStringLiteral( "Whether the the distance and elevation axis scales are locked to each other" ) ); +// +// QgsElevationProfileLayersDialog +// + +QgsElevationProfileLayersDialog::QgsElevationProfileLayersDialog( QWidget *parent ) + : QDialog( parent ) +{ + setupUi( this ); + QgsGui::enableAutoGeometryRestore( this ); + + mFilterLineEdit->setShowClearButton( true ); + mFilterLineEdit->setShowSearchIcon( true ); + + mModel = new QgsMapLayerProxyModel( listMapLayers ); + listMapLayers->setModel( mModel ); + const QModelIndex firstLayer = mModel->index( 0, 0 ); + listMapLayers->selectionModel()->select( firstLayer, QItemSelectionModel::Select ); + + connect( listMapLayers, &QListView::doubleClicked, this, &QgsElevationProfileLayersDialog::accept ); + + connect( mFilterLineEdit, &QLineEdit::textChanged, mModel, &QgsMapLayerProxyModel::setFilterString ); + connect( mCheckBoxVisibleLayers, &QCheckBox::toggled, this, &QgsElevationProfileLayersDialog::filterVisible ); + + mFilterLineEdit->setFocus(); +} + +void QgsElevationProfileLayersDialog::setVisibleLayers( const QList &layers ) +{ + mVisibleLayers = layers; +} + +void QgsElevationProfileLayersDialog::setHiddenLayers( const QList &layers ) +{ + mModel->setExceptedLayerList( layers ); +} + +QList< QgsMapLayer *> QgsElevationProfileLayersDialog::selectedLayers() const +{ + QList< QgsMapLayer * > layers; + + const QModelIndexList selection = listMapLayers->selectionModel()->selectedIndexes(); + for ( const QModelIndex &index : selection ) + { + const QModelIndex sourceIndex = mModel->mapToSource( index ); + if ( !sourceIndex.isValid() ) + { + continue; + } + + QgsMapLayer *layer = mModel->sourceLayerModel()->layerFromIndex( sourceIndex ); + if ( layer ) + layers << layer; + } + return layers; +} + +void QgsElevationProfileLayersDialog::filterVisible( bool enabled ) +{ + if ( enabled ) + mModel->setLayerAllowlist( mVisibleLayers ); + else + mModel->setLayerAllowlist( QList< QgsMapLayer * >() ); +} + + QgsElevationProfileWidget::QgsElevationProfileWidget( const QString &name ) : QWidget( nullptr ) , mCanvasName( name ) @@ -103,6 +172,11 @@ QgsElevationProfileWidget::QgsElevationProfileWidget( const QString &name ) mCanvas->setTool( mIdentifyTool ); + QAction *addLayerAction = new QAction( tr( "Add Layers" ), this ); + addLayerAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionAddLayer.svg" ) ) ); + connect( addLayerAction, &QAction::triggered, this, &QgsElevationProfileWidget::addLayers ); + toolBar->addAction( addLayerAction ); + QAction *showLayerTree = new QAction( tr( "Show Layer Tree" ), this ); showLayerTree->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconLayerTree.svg" ) ) ); showLayerTree->setCheckable( true ); @@ -386,6 +460,53 @@ void QgsElevationProfileWidget::cancelJobs() mCanvas->cancelJobs(); } +void QgsElevationProfileWidget::addLayers() +{ + QgsElevationProfileLayersDialog addDialog( this ); + const QMap allMapLayers = QgsProject::instance()->mapLayers(); + + // The add layers dialog should only show layers which CAN have elevation, yet currently don't + // have it enabled. So collect layers which don't match this criteria now for filtering out. + QList< QgsMapLayer * > layersWhichAlreadyHaveElevationOrCannotHaveElevation; + for ( auto it = allMapLayers.constBegin(); it != allMapLayers.constEnd(); ++it ) + { + if ( !QgsElevationUtils::canEnableElevationForLayer( it.value() ) || it.value()->elevationProperties()->hasElevation() ) + { + layersWhichAlreadyHaveElevationOrCannotHaveElevation << it.value(); + continue; + } + } + addDialog.setHiddenLayers( layersWhichAlreadyHaveElevationOrCannotHaveElevation ); + + addDialog.setVisibleLayers( mMainCanvas->layers( true ) ); + + if ( addDialog.exec() == QDialog::Accepted ) + { + const QList layers = addDialog.selectedLayers(); + QList< QgsMapLayer * > updatedLayers; + if ( !layers.empty() ) + { + for ( QgsMapLayer *layer : layers ) + { + if ( QgsElevationUtils::enableElevationForLayer( layer ) ) + updatedLayers << layer; + } + + mLayerTreeView->proxyModel()->invalidate(); + for ( QgsMapLayer *layer : std::as_const( updatedLayers ) ) + { + if ( QgsLayerTreeLayer *node = mLayerTree->findLayer( layer ) ) + { + node->setItemVisibilityChecked( true ); + } + } + + updateCanvasLayers(); + scheduleUpdate(); + } + } +} + void QgsElevationProfileWidget::updateCanvasLayers() { QList layers; @@ -393,6 +514,10 @@ void QgsElevationProfileWidget::updateCanvasLayers() layers.reserve( layerOrder.size() ); for ( QgsMapLayer *layer : layerOrder ) { + // safety check. maybe elevation properties have been disabled externally. + if ( !layer->elevationProperties() || !layer->elevationProperties()->hasElevation() ) + continue; + if ( mLayerTree->findLayer( layer )->isVisible() ) layers << layer; } diff --git a/src/app/elevation/qgselevationprofilewidget.h b/src/app/elevation/qgselevationprofilewidget.h index 77df6db1a63..9f2b0392f9d 100644 --- a/src/app/elevation/qgselevationprofilewidget.h +++ b/src/app/elevation/qgselevationprofilewidget.h @@ -23,6 +23,7 @@ #include "qgsgeometry.h" #include "qobjectuniqueptr.h" #include "qgselevationprofilelayertreeview.h" +#include "ui_qgselevationprofileaddlayersdialogbase.h" #include #include @@ -50,6 +51,7 @@ class QLabel; class QgsProfilePoint; class QgsSettingsEntryDouble; class QgsSettingsEntryBool; +class QgsMapLayerProxyModel; class QgsAppElevationProfileLayerTreeView : public QgsElevationProfileLayerTreeView { @@ -63,6 +65,26 @@ class QgsAppElevationProfileLayerTreeView : public QgsElevationProfileLayerTreeV void contextMenuEvent( QContextMenuEvent *event ) override; }; +class QgsElevationProfileLayersDialog: public QDialog, private Ui::QgsElevationProfileAddLayersDialogBase +{ + Q_OBJECT + + public: + QgsElevationProfileLayersDialog( QWidget *parent = nullptr ); + void setVisibleLayers( const QList &layers ); + void setHiddenLayers( const QList &layers ); + QList< QgsMapLayer * > selectedLayers() const; + + private slots: + + void filterVisible( bool enabled ); + + private: + + QgsMapLayerProxyModel *mModel = nullptr; + QList< QgsMapLayer * > mVisibleLayers; +}; + class QgsElevationProfileWidget : public QWidget { Q_OBJECT @@ -93,6 +115,7 @@ class QgsElevationProfileWidget : public QWidget void toggleDockModeRequested( bool docked ); private slots: + void addLayers(); void updateCanvasLayers(); void onTotalPendingJobsCountChanged( int count ); void setProfileCurve( const QgsGeometry &curve, bool resetView ); diff --git a/src/core/qgselevationutils.cpp b/src/core/qgselevationutils.cpp index 7aa2b200064..0e44e712528 100644 --- a/src/core/qgselevationutils.cpp +++ b/src/core/qgselevationutils.cpp @@ -16,7 +16,7 @@ #include "qgselevationutils.h" #include "qgsproject.h" #include "qgsmaplayerelevationproperties.h" - +#include "qgsrasterlayerelevationproperties.h" QgsDoubleRange QgsElevationUtils::calculateZRangeForProject( QgsProject *project ) { @@ -54,3 +54,37 @@ QgsDoubleRange QgsElevationUtils::calculateZRangeForProject( QgsProject *project std::isnan( max ) ? std::numeric_limits< double >::max() : max ); } +bool QgsElevationUtils::canEnableElevationForLayer( QgsMapLayer *layer ) +{ + return static_cast< bool >( layer->elevationProperties() ); +} + +bool QgsElevationUtils::enableElevationForLayer( QgsMapLayer *layer ) +{ + switch ( layer->type() ) + { + case Qgis::LayerType::Raster: + { + if ( QgsRasterLayerElevationProperties *properties = qobject_cast( layer->elevationProperties() ) ) + { + properties->setEnabled( true ); + // This could potentially be made smarter, eg by checking the data type of bands. But that's likely overkill..! + properties->setBandNumber( 1 ); + return true; + } + break; + } + + // can't automatically enable elevation for these layer types + case Qgis::LayerType::Vector: + case Qgis::LayerType::Plugin: + case Qgis::LayerType::Mesh: + case Qgis::LayerType::VectorTile: + case Qgis::LayerType::Annotation: + case Qgis::LayerType::PointCloud: + case Qgis::LayerType::Group: + break; + } + return false; +} + diff --git a/src/core/qgselevationutils.h b/src/core/qgselevationutils.h index 13048de42d9..b6d01260e2c 100644 --- a/src/core/qgselevationutils.h +++ b/src/core/qgselevationutils.h @@ -40,6 +40,22 @@ class CORE_EXPORT QgsElevationUtils */ static QgsDoubleRange calculateZRangeForProject( QgsProject *project ); + /** + * Returns TRUE if elevation can be enabled for a map \a layer. + * + * \since QGIS 3.32 + */ + static bool canEnableElevationForLayer( QgsMapLayer *layer ); + + /** + * Automatically enables elevation for a map \a layer, using reasonable defaults. + * + * Returns TRUE if the elevation was enabled successfully. + * + * \since QGIS 3.32 + */ + static bool enableElevationForLayer( QgsMapLayer *layer ); + }; diff --git a/src/gui/elevation/qgselevationprofilelayertreeview.cpp b/src/gui/elevation/qgselevationprofilelayertreeview.cpp index 800ffd223d3..4b38ac26832 100644 --- a/src/gui/elevation/qgselevationprofilelayertreeview.cpp +++ b/src/gui/elevation/qgselevationprofilelayertreeview.cpp @@ -420,6 +420,11 @@ void QgsElevationProfileLayerTreeView::populateInitialLayers( QgsProject *projec } } +QgsElevationProfileLayerTreeProxyModel *QgsElevationProfileLayerTreeView::proxyModel() +{ + return mProxyModel; +} + void QgsElevationProfileLayerTreeView::resizeEvent( QResizeEvent *event ) { header()->setMinimumSectionSize( viewport()->width() ); diff --git a/src/gui/elevation/qgselevationprofilelayertreeview.h b/src/gui/elevation/qgselevationprofilelayertreeview.h index 63b1a0b12b6..33671b66eb0 100644 --- a/src/gui/elevation/qgselevationprofilelayertreeview.h +++ b/src/gui/elevation/qgselevationprofilelayertreeview.h @@ -129,6 +129,13 @@ class GUI_EXPORT QgsElevationProfileLayerTreeView : public QTreeView */ void populateInitialLayers( QgsProject *project ); + /** + * Returns the view's proxy model. + * + * \since QGIS 3.32 + */ + QgsElevationProfileLayerTreeProxyModel *proxyModel(); + protected: void resizeEvent( QResizeEvent *event ) override; diff --git a/src/ui/qgselevationprofileaddlayersdialogbase.ui b/src/ui/qgselevationprofileaddlayersdialogbase.ui new file mode 100644 index 00000000000..5ed9460025a --- /dev/null +++ b/src/ui/qgselevationprofileaddlayersdialogbase.ui @@ -0,0 +1,102 @@ + + + QgsElevationProfileAddLayersDialogBase + + + + 0 + 0 + 270 + 194 + + + + Add Layers to Elevation Profile + + + + 6 + + + + + QAbstractItemView::ExtendedSelection + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Search + + + + + + + If checked, only layers visible within the map will be listed + + + Show visible layers only + + + + + + + + QgsFilterLineEdit + QLineEdit +
qgsfilterlineedit.h
+
+
+ + mFilterLineEdit + listMapLayers + + + + + buttonBox + accepted() + QgsElevationProfileAddLayersDialogBase + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + QgsElevationProfileAddLayersDialogBase + reject() + + + 316 + 260 + + + 286 + 274 + + + + +