From 444434052bd5d1126dc127f3a0b4f9699a550779 Mon Sep 17 00:00:00 2001 From: uclaros Date: Wed, 27 Nov 2024 10:10:33 +0200 Subject: [PATCH 1/9] Allow downloading assets from the data source manager --- src/gui/stac/qgsstacdataitemguiprovider.cpp | 82 +---------------- src/gui/stac/qgsstacdownloadassetsdialog.cpp | 94 ++++++++++++++++++++ src/gui/stac/qgsstacdownloadassetsdialog.h | 7 ++ src/gui/stac/qgsstacsourceselect.cpp | 19 ++++ 4 files changed, 123 insertions(+), 79 deletions(-) diff --git a/src/gui/stac/qgsstacdataitemguiprovider.cpp b/src/gui/stac/qgsstacdataitemguiprovider.cpp index e093f501544e..8dc1648d711f 100644 --- a/src/gui/stac/qgsstacdataitemguiprovider.cpp +++ b/src/gui/stac/qgsstacdataitemguiprovider.cpp @@ -15,7 +15,6 @@ #include "qgsstacdataitemguiprovider.h" #include "moc_qgsstacdataitemguiprovider.cpp" -#include "qgsnetworkcontentfetcherregistry.h" #include "qgsstaccontroller.h" #include "qgsstacdataitems.h" #include "qgsstacconnection.h" @@ -25,7 +24,6 @@ #include "qgsstacitem.h" #include "qgsstacdownloadassetsdialog.h" #include "qgsstacobjectdetailsdialog.h" -#include "qgsapplication.h" ///@cond PRIVATE @@ -195,83 +193,9 @@ void QgsStacDataItemGuiProvider::downloadAssets( QgsDataItem *item, QgsDataItemG QgsStacDownloadAssetsDialog dialog; dialog.setStacItem( itemItem->stacItem() ); - - if ( dialog.exec() == QDialog::Accepted ) - { - const QString folder = dialog.selectedFolder(); - const QStringList urls = dialog.selectedUrls(); - for ( const QString &url : urls ) - { - QgsNetworkContentFetcherTask *fetcher = new QgsNetworkContentFetcherTask( url, - itemItem->stacController()->authCfg(), - QgsTask::CanCancel, - tr( "Downloading STAC asset" ) ); - - connect( fetcher, &QgsNetworkContentFetcherTask::errorOccurred, item, [context]( QNetworkReply::NetworkError, const QString & errorMsg ) - { - notify( tr( "Error downloading STAC asset" ), - errorMsg, - context, - Qgis::MessageLevel::Critical ); - } ); - - connect( fetcher, &QgsNetworkContentFetcherTask::fetched, item, [fetcher, folder, context] - { - QNetworkReply *reply = fetcher->reply(); - if ( !reply || reply->error() != QNetworkReply::NoError ) - { - // canceled or failed - return; - } - else - { - const QString fileName = fetcher->contentDispositionFilename().isEmpty() ? reply->url().fileName() : fetcher->contentDispositionFilename(); - QFileInfo fi( fileName ); - QFile file( QStringLiteral( "%1/%2" ).arg( folder, fileName ) ); - int i = 1; - while ( file.exists() ) - { - QString uniqueName = QStringLiteral( "%1/%2(%3)" ).arg( folder, fi.baseName() ).arg( i++ ); - if ( !fi.completeSuffix().isEmpty() ) - uniqueName.append( QStringLiteral( ".%1" ).arg( fi.completeSuffix() ) ); - file.setFileName( uniqueName ); - } - - bool failed = false; - if ( file.open( QIODevice::WriteOnly ) ) - { - const QByteArray data = reply->readAll(); - if ( file.write( data ) < 0 ) - failed = true; - - file.close(); - } - else - { - failed = true; - } - - if ( failed ) - { - notify( tr( "Error downloading STAC asset" ), - tr( "Could not write to file %1" ).arg( file.fileName() ), - context, - Qgis::MessageLevel::Critical ); - } - else - { - notify( tr( "STAC asset downloaded" ), - file.fileName(), - context, - Qgis::MessageLevel::Success ); - } - } - } ); - - QgsApplication::taskManager()->addTask( fetcher ); - } - } - + dialog.setMessageBar( context.messageBar() ); + dialog.setAuthCfg( itemItem->stacController()->authCfg() ); + dialog.exec(); } ///@endcond diff --git a/src/gui/stac/qgsstacdownloadassetsdialog.cpp b/src/gui/stac/qgsstacdownloadassetsdialog.cpp index 6aeca1a88877..30b61d63cdd4 100644 --- a/src/gui/stac/qgsstacdownloadassetsdialog.cpp +++ b/src/gui/stac/qgsstacdownloadassetsdialog.cpp @@ -16,8 +16,11 @@ #include "qgsstacdownloadassetsdialog.h" #include "moc_qgsstacdownloadassetsdialog.cpp" #include "qgsgui.h" +#include "qgsnetworkcontentfetchertask.h" #include "qgssettings.h" #include "qgsproject.h" +#include "qgsmessagebar.h" +#include "qgsapplication.h" #include #include @@ -51,6 +54,97 @@ QgsStacDownloadAssetsDialog::QgsStacDownloadAssetsDialog( QWidget *parent ) : this, &QgsStacDownloadAssetsDialog::showContextMenu ); } +void QgsStacDownloadAssetsDialog::accept() +{ + const QString folder = selectedFolder(); + const QStringList urls = selectedUrls(); + for ( const QString &url : urls ) + { + QgsNetworkContentFetcherTask *fetcher = new QgsNetworkContentFetcherTask( url, + mAuthCfg, + QgsTask::CanCancel, + tr( "Downloading STAC asset" ) ); + + connect( fetcher, &QgsNetworkContentFetcherTask::errorOccurred, fetcher, [bar = mMessageBar]( QNetworkReply::NetworkError, const QString & errorMsg ) + { + if ( bar ) + bar->pushMessage( + tr( "Error downloading STAC asset" ), + errorMsg, + Qgis::MessageLevel::Critical ); + } ); + + connect( fetcher, &QgsNetworkContentFetcherTask::fetched, fetcher, [fetcher, folder, bar = mMessageBar] + { + QNetworkReply *reply = fetcher->reply(); + if ( !reply || reply->error() != QNetworkReply::NoError ) + { + // canceled or failed + return; + } + else + { + const QString fileName = fetcher->contentDispositionFilename().isEmpty() ? reply->url().fileName() : fetcher->contentDispositionFilename(); + QFileInfo fi( fileName ); + QFile file( QStringLiteral( "%1/%2" ).arg( folder, fileName ) ); + int i = 1; + while ( file.exists() ) + { + QString uniqueName = QStringLiteral( "%1/%2(%3)" ).arg( folder, fi.baseName() ).arg( i++ ); + if ( !fi.completeSuffix().isEmpty() ) + uniqueName.append( QStringLiteral( ".%1" ).arg( fi.completeSuffix() ) ); + file.setFileName( uniqueName ); + } + + bool failed = false; + if ( file.open( QIODevice::WriteOnly ) ) + { + const QByteArray data = reply->readAll(); + if ( file.write( data ) < 0 ) + failed = true; + + file.close(); + } + else + { + failed = true; + } + + if ( failed ) + { + if ( bar ) + bar->pushMessage( + tr( "Error downloading STAC asset" ), + tr( "Could not write to file %1" ).arg( file.fileName() ), + Qgis::MessageLevel::Critical ); + } + else + { + if ( bar ) + bar->pushMessage( + tr( "STAC asset downloaded" ), + file.fileName(), + Qgis::MessageLevel::Success ); + } + } + } ); + + QgsApplication::taskManager()->addTask( fetcher ); + } + + QDialog::accept(); +} + +void QgsStacDownloadAssetsDialog::setAuthCfg( const QString &authCfg ) +{ + mAuthCfg = authCfg; +} + +void QgsStacDownloadAssetsDialog::setMessageBar( QgsMessageBar *bar ) +{ + mMessageBar = bar; +} + void QgsStacDownloadAssetsDialog::setStacItem( QgsStacItem *stacItem ) { if ( ! stacItem ) diff --git a/src/gui/stac/qgsstacdownloadassetsdialog.h b/src/gui/stac/qgsstacdownloadassetsdialog.h index 5324ec46105c..bca43d4d5a25 100644 --- a/src/gui/stac/qgsstacdownloadassetsdialog.h +++ b/src/gui/stac/qgsstacdownloadassetsdialog.h @@ -24,6 +24,7 @@ #include +class QgsMessageBar; class QgsStacDownloadAssetsDialog : public QDialog, private Ui::QgsStacDownloadAssetsDialog { @@ -32,6 +33,10 @@ class QgsStacDownloadAssetsDialog : public QDialog, private Ui::QgsStacDownloadA public: explicit QgsStacDownloadAssetsDialog( QWidget *parent = nullptr ); + void accept() override; + + void setAuthCfg( const QString &authCfg ); + void setMessageBar( QgsMessageBar *bar ); void setStacItem( QgsStacItem *stacItem ); QString selectedFolder(); QStringList selectedUrls(); @@ -44,6 +49,8 @@ class QgsStacDownloadAssetsDialog : public QDialog, private Ui::QgsStacDownloadA void deselectAll(); QMenu *mContextMenu = nullptr; + QString mAuthCfg; + QgsMessageBar *mMessageBar = nullptr; }; ///@endcond diff --git a/src/gui/stac/qgsstacsourceselect.cpp b/src/gui/stac/qgsstacsourceselect.cpp index 4c64de89a4d2..468d302f7ab2 100644 --- a/src/gui/stac/qgsstacsourceselect.cpp +++ b/src/gui/stac/qgsstacsourceselect.cpp @@ -15,6 +15,7 @@ #include "qgsstacsourceselect.h" #include "moc_qgsstacsourceselect.cpp" +#include "qgsdatasourcemanagerdialog.h" #include "qgsgui.h" #include "qgsmapcanvas.h" #include "qgsstaccontroller.h" @@ -473,6 +474,22 @@ void QgsStacSourceSelect::showItemsContextMenu( QPoint point ) QMenu *menu = new QMenu( this ); + QgsMessageBar *bar = nullptr; + QgsDataSourceManagerDialog *dsm = qobject_cast( window() ); + if ( dsm ) + bar = dsm->messageBar(); + + QAction *downloadAction = new QAction( tr( "Download Assets…" ), menu ); + connect( downloadAction, &QAction::triggered, this, [index, bar, authCfg = mStac->authCfg()] + { + QgsStacDownloadAssetsDialog dialog; + QgsStacItem *item = dynamic_cast( index.data( QgsStacItemListModel::Role::StacObject ).value() ); + dialog.setStacItem( item ); + dialog.setMessageBar( bar ); + dialog.setAuthCfg( authCfg ); + dialog.exec(); + } ); + QAction *detailsAction = new QAction( tr( "Details…" ), menu ); connect( detailsAction, &QAction::triggered, this, [this, index] { @@ -480,6 +497,8 @@ void QgsStacSourceSelect::showItemsContextMenu( QPoint point ) details.setStacObject( index.data( QgsStacItemListModel::Role::StacObject ).value() ); details.exec(); } ); + + menu->addAction( downloadAction ); menu->addAction( detailsAction ); menu->popup( mItemsView->mapToGlobal( point ) ); From 8b3c7f1efcd0c0b263c1c293346de8434c4fcd0f Mon Sep 17 00:00:00 2001 From: uclaros Date: Wed, 27 Nov 2024 16:13:55 +0200 Subject: [PATCH 2/9] Show results footprints and current item footprint --- src/gui/stac/qgsstacitemlistmodel.cpp | 9 ++++ src/gui/stac/qgsstacitemlistmodel.h | 2 + src/gui/stac/qgsstacsourceselect.cpp | 70 ++++++++++++++++++++++++++- src/gui/stac/qgsstacsourceselect.h | 11 ++++- src/ui/qgsstacsourceselectbase.ui | 37 ++++++++++++-- 5 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/gui/stac/qgsstacitemlistmodel.cpp b/src/gui/stac/qgsstacitemlistmodel.cpp index 1c149a9e6f04..17a81a49f973 100644 --- a/src/gui/stac/qgsstacitemlistmodel.cpp +++ b/src/gui/stac/qgsstacitemlistmodel.cpp @@ -105,6 +105,10 @@ QVariant QgsStacItemListModel::data( const QModelIndex &index, int role ) const } return QStringList( formats.cbegin(), formats.cend() ); } + case Role::Geometry: + { + return QVariant::fromValue( mItems.at( index.row() )->geometry() ); + } } return QVariant(); @@ -176,6 +180,11 @@ void QgsStacItemListModel::addItems( const QVector &items ) } } +QVector QgsStacItemListModel::items() const +{ + return mItems; +} + QgsStacItemDelegate::QgsStacItemDelegate( QObject *parent ) diff --git a/src/gui/stac/qgsstacitemlistmodel.h b/src/gui/stac/qgsstacitemlistmodel.h index 414821136f09..4a04737f8a0c 100644 --- a/src/gui/stac/qgsstacitemlistmodel.h +++ b/src/gui/stac/qgsstacitemlistmodel.h @@ -56,6 +56,8 @@ class QgsStacItemListModel : public QAbstractListModel void setCollections( const QVector< QgsStacCollection * > &collections ); //! Add items to the model. Takes ownership void addItems( const QVector< QgsStacItem * > &items ); + //! Returns all items in the model. Does not transfer ownership + QVector< QgsStacItem * > items() const; private: QVector< QgsStacItem * > mItems; diff --git a/src/gui/stac/qgsstacsourceselect.cpp b/src/gui/stac/qgsstacsourceselect.cpp index 468d302f7ab2..84baf3148106 100644 --- a/src/gui/stac/qgsstacsourceselect.cpp +++ b/src/gui/stac/qgsstacsourceselect.cpp @@ -79,7 +79,7 @@ QgsStacSourceSelect::QgsStacSourceSelect( QWidget *parent, Qt::WindowFlags fl, Q connect( mItemsView->selectionModel(), &QItemSelectionModel::currentChanged, this, &QgsStacSourceSelect::onCurrentItemChanged ); connect( mItemsView->verticalScrollBar(), &QScrollBar::valueChanged, this, &QgsStacSourceSelect::onItemsViewScroll ); - + connect( mFootprintsCheckBox, &QCheckBox::clicked, this, &QgsStacSourceSelect::showFootprints ); mParametersDialog = new QgsStacSearchParametersDialog( mapCanvas(), this ); mFiltersLabel->clear(); @@ -90,6 +90,24 @@ QgsStacSourceSelect::~QgsStacSourceSelect() delete mStac; } +void QgsStacSourceSelect::hideEvent( QHideEvent *e ) +{ + if ( !e->spontaneous() ) + { + showFootprints( false ); + } + QgsAbstractDataSourceWidget::hideEvent( e ); +} + +void QgsStacSourceSelect::showEvent( QShowEvent *e ) +{ + if ( !e->spontaneous() && mFootprintsCheckBox->isChecked() ) + { + showFootprints( true ); + } + QgsAbstractDataSourceWidget::showEvent( e ); +} + void QgsStacSourceSelect::addButtonClicked() { const QItemSelection selection = mItemsView->selectionModel()->selection(); @@ -141,6 +159,9 @@ void QgsStacSourceSelect::onCurrentItemChanged( const QModelIndex ¤t, cons { Q_UNUSED( previous ) + if ( mFootprintsCheckBox->isChecked() ) + highlightFootprint( current ); + const QVariant mediaTypes = current.data( QgsStacItemListModel::Role::MediaTypes ); emit enableButtons( !mediaTypes.toStringList().isEmpty() ); } @@ -157,6 +178,8 @@ void QgsStacSourceSelect::btnConnect_clicked() mSearchUrl.clear(); mNextPageUrl.clear(); mItemsModel->clear(); + qDeleteAll( mRubberBands ); + mRubberBands.clear(); mStatusLabel->setText( tr( "Connecting…" ) ); mStac->cancelPendingAsyncRequests(); mStac->fetchStacObjectAsync( connection.url ); @@ -337,6 +360,13 @@ void QgsStacSourceSelect::onItemCollectionRequestFinished( int requestId, QStrin const QVector< QgsStacItem *> items = col->takeItems(); mItemsModel->addItems( items ); + for ( QgsStacItem *i : items ) + { + QgsRubberBand *band = new QgsRubberBand( mapCanvas(), Qgis::GeometryType::Polygon ); + band->setToGeometry( i->geometry(), QgsCoordinateReferenceSystem::fromEpsgId( 4326 ) ); + mRubberBands.append( band ); + } + const int count = mItemsModel->rowCount(); if ( mNextPageUrl.isEmpty() ) { @@ -437,6 +467,8 @@ void QgsStacSourceSelect::openSearchParametersDialog() return; mItemsModel->clear(); + qDeleteAll( mRubberBands ); + mRubberBands.clear(); mItemsModel->setCollections( mParametersDialog->collections() ); mNextPageUrl.clear(); mStatusLabel->setText( tr( "Searching…" ) ); @@ -505,4 +537,40 @@ void QgsStacSourceSelect::showItemsContextMenu( QPoint point ) connect( menu, &QMenu::aboutToHide, menu, &QMenu::deleteLater ); } +void QgsStacSourceSelect::highlightFootprint( const QModelIndex &index ) +{ + QgsGeometry geom = index.data( QgsStacItemListModel::Role::Geometry ).value(); + if ( QgsMapCanvas *map = mapCanvas() ) + { + mCurrentItemBand.reset( new QgsRubberBand( map, Qgis::GeometryType::Polygon ) ); + mCurrentItemBand->setFillColor( QColor::fromRgb( 255, 0, 0, 128 ) ); + mCurrentItemBand->setToGeometry( geom, QgsCoordinateReferenceSystem::fromEpsgId( 4326 ) ); + } +} + +void QgsStacSourceSelect::showFootprints( bool enable ) +{ + if ( enable ) + { + const QVector items = mItemsModel->items(); + for ( QgsStacItem *i : items ) + { + QgsRubberBand *band = new QgsRubberBand( mapCanvas(), Qgis::GeometryType::Polygon ); + band->setToGeometry( i->geometry(), QgsCoordinateReferenceSystem::fromEpsgId( 4326 ) ); + mRubberBands.append( band ); + } + const QModelIndex index = mItemsView->selectionModel()->currentIndex(); + if ( index.isValid() ) + { + highlightFootprint( index ); + } + } + else + { + qDeleteAll( mRubberBands ); + mRubberBands.clear(); + mCurrentItemBand.reset(); + } +} + ///@endcond diff --git a/src/gui/stac/qgsstacsourceselect.h b/src/gui/stac/qgsstacsourceselect.h index 99d3f7736922..c0b97df5341f 100644 --- a/src/gui/stac/qgsstacsourceselect.h +++ b/src/gui/stac/qgsstacsourceselect.h @@ -30,6 +30,7 @@ class QgsStacSearchParametersDialog; class QgsStacItemListModel; class QgsStacController; +class QgsRubberBand; class GUI_EXPORT QgsStacSourceSelect : public QgsAbstractDataSourceWidget, private Ui::QgsStacSourceSelectBase @@ -42,6 +43,9 @@ class GUI_EXPORT QgsStacSourceSelect : public QgsAbstractDataSourceWidget, priva //! Destructor ~QgsStacSourceSelect() override; + void hideEvent( QHideEvent *e ) override; + void showEvent( QShowEvent *e ) override; + void addButtonClicked() override; private slots: @@ -79,7 +83,7 @@ class GUI_EXPORT QgsStacSourceSelect : public QgsAbstractDataSourceWidget, priva //! Called when double clicking a result item void onItemDoubleClicked( const QModelIndex &index ); - //! Enables Add Layers button based on current item + //! Enables Add Layers button based on current item, updates rubber bands void onCurrentItemChanged( const QModelIndex ¤t, const QModelIndex &previous ); private: @@ -93,6 +97,9 @@ class GUI_EXPORT QgsStacSourceSelect : public QgsAbstractDataSourceWidget, priva void showItemsContextMenu( QPoint point ); + void highlightFootprint( const QModelIndex &index ); + void showFootprints( bool enable ); + QString mCollectionsUrl; QString mSearchUrl; QUrl mNextPageUrl; @@ -100,6 +107,8 @@ class GUI_EXPORT QgsStacSourceSelect : public QgsAbstractDataSourceWidget, priva QgsStacController *mStac = nullptr; QgsStacItemListModel *mItemsModel = nullptr; QgsStacSearchParametersDialog *mParametersDialog = nullptr; + std::unique_ptr mCurrentItemBand; + QVector mRubberBands; }; ///@endcond diff --git a/src/ui/qgsstacsourceselectbase.ui b/src/ui/qgsstacsourceselectbase.ui index 382fe34a4e88..14cd61dd26e8 100644 --- a/src/ui/qgsstacsourceselectbase.ui +++ b/src/ui/qgsstacsourceselectbase.ui @@ -154,11 +154,38 @@ - - - - - + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Show Footprints + + + true + + + + From 0904ef84361cf1b838ba756fb30d153187b037d3 Mon Sep 17 00:00:00 2001 From: uclaros Date: Wed, 27 Nov 2024 22:58:55 +0200 Subject: [PATCH 3/9] Add zoom to and pan to STAC item menu actions --- src/gui/stac/qgsstacsourceselect.cpp | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/gui/stac/qgsstacsourceselect.cpp b/src/gui/stac/qgsstacsourceselect.cpp index 84baf3148106..2f1d796ab94c 100644 --- a/src/gui/stac/qgsstacsourceselect.cpp +++ b/src/gui/stac/qgsstacsourceselect.cpp @@ -511,6 +511,36 @@ void QgsStacSourceSelect::showItemsContextMenu( QPoint point ) if ( dsm ) bar = dsm->messageBar(); + QAction *zoomToAction = new QAction( tr( "Zoom to Item" ) ); + connect( zoomToAction, &QAction::triggered, this, [index, this] + { + QgsGeometry geom = index.data( QgsStacItemListModel::Role::Geometry ).value(); + if ( QgsMapCanvas *map = mapCanvas() ) + { + const QgsRectangle bbox = geom.boundingBox(); + const QgsCoordinateTransform ct( QgsCoordinateReferenceSystem::fromEpsgId( 4324 ), + map->mapSettings().destinationCrs(), + QgsProject::instance() ); + QgsRectangle extent = ct.transformBoundingBox( bbox ); + map->zoomToFeatureExtent( extent ); + } + } ); + + QAction *panToAction = new QAction( tr( "Pan to Item" ) ); + connect( panToAction, &QAction::triggered, this, [index, this] + { + QgsGeometry geom = index.data( QgsStacItemListModel::Role::Geometry ).value(); + if ( QgsMapCanvas *map = mapCanvas() ) + { + const QgsRectangle bbox = geom.boundingBox(); + const QgsCoordinateTransform ct( QgsCoordinateReferenceSystem::fromEpsgId( 4324 ), + map->mapSettings().destinationCrs(), + QgsProject::instance() ); + const QgsRectangle extent = ct.transformBoundingBox( bbox ); + map->setCenter( extent.center() ); + } + } ); + QAction *downloadAction = new QAction( tr( "Download Assets…" ), menu ); connect( downloadAction, &QAction::triggered, this, [index, bar, authCfg = mStac->authCfg()] { @@ -530,6 +560,8 @@ void QgsStacSourceSelect::showItemsContextMenu( QPoint point ) details.exec(); } ); + menu->addAction( zoomToAction ); + menu->addAction( panToAction ); menu->addAction( downloadAction ); menu->addAction( detailsAction ); From b464c3c5e45b5a8260fc2a677b9de2c8857705f2 Mon Sep 17 00:00:00 2001 From: uclaros Date: Wed, 27 Nov 2024 22:59:48 +0200 Subject: [PATCH 4/9] Show wait cursor while loading layers --- src/gui/stac/qgsstacsourceselect.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/stac/qgsstacsourceselect.cpp b/src/gui/stac/qgsstacsourceselect.cpp index 2f1d796ab94c..a27c4808282f 100644 --- a/src/gui/stac/qgsstacsourceselect.cpp +++ b/src/gui/stac/qgsstacsourceselect.cpp @@ -110,6 +110,7 @@ void QgsStacSourceSelect::showEvent( QShowEvent *e ) void QgsStacSourceSelect::addButtonClicked() { + QgsTemporaryCursorOverride cursorOverride( Qt::WaitCursor ); const QItemSelection selection = mItemsView->selectionModel()->selection(); const QModelIndexList selectedIndices = selection.indexes(); From 3ea70a24210500aa3ed525937398f3b1e03f6657 Mon Sep 17 00:00:00 2001 From: uclaros Date: Wed, 27 Nov 2024 23:51:38 +0200 Subject: [PATCH 5/9] Allow adding a single asset as a layer --- src/core/stac/qgsstacasset.cpp | 45 ++++++++++++++++++++++++ src/core/stac/qgsstacasset.h | 7 ++++ src/core/stac/qgsstacitem.cpp | 40 ++------------------- src/core/stac/qgsstacitem.h | 2 +- src/gui/stac/qgsstacsourceselect.cpp | 52 ++++++++++++++++++++-------- src/gui/stac/qgsstacsourceselect.h | 3 +- 6 files changed, 95 insertions(+), 54 deletions(-) diff --git a/src/core/stac/qgsstacasset.cpp b/src/core/stac/qgsstacasset.cpp index b2ba590dda5c..764dc823795f 100644 --- a/src/core/stac/qgsstacasset.cpp +++ b/src/core/stac/qgsstacasset.cpp @@ -15,6 +15,8 @@ #include "qgsstacasset.h" +#include + QgsStacAsset::QgsStacAsset( const QString &href, const QString &title, const QString &description, @@ -72,3 +74,46 @@ QString QgsStacAsset::formatName() const return QStringLiteral( "EPT" ); return QString(); } + +QgsMimeDataUtils::Uri QgsStacAsset::uri() const +{ + QgsMimeDataUtils::Uri uri; + QUrl url( href() ); + if ( url.isLocalFile() ) + { + uri.uri = href(); + } + else if ( formatName() == QLatin1String( "COG" ) ) + { + uri.layerType = QStringLiteral( "raster" ); + uri.providerKey = QStringLiteral( "gdal" ); + if ( href().startsWith( QLatin1String( "http" ), Qt::CaseInsensitive ) || + href().startsWith( QLatin1String( "ftp" ), Qt::CaseInsensitive ) ) + { + uri.uri = QStringLiteral( "/vsicurl/%1" ).arg( href() ); + } + else if ( href().startsWith( QLatin1String( "s3://" ), Qt::CaseInsensitive ) ) + { + uri.uri = QStringLiteral( "/vsis3/%1" ).arg( href().mid( 5 ) ); + } + else + { + uri.uri = href(); + } + } + else if ( formatName() == QLatin1String( "COPC" ) ) + { + uri.layerType = QStringLiteral( "pointcloud" ); + uri.providerKey = QStringLiteral( "copc" ); + uri.uri = href(); + } + else if ( formatName() == QLatin1String( "EPT" ) ) + { + uri.layerType = QStringLiteral( "pointcloud" ); + uri.providerKey = QStringLiteral( "ept" ); + uri.uri = href(); + } + uri.name = title().isEmpty() ? url.fileName() : title(); + + return uri; +} diff --git a/src/core/stac/qgsstacasset.h b/src/core/stac/qgsstacasset.h index c19717918fbd..ebc9954760f8 100644 --- a/src/core/stac/qgsstacasset.h +++ b/src/core/stac/qgsstacasset.h @@ -19,6 +19,7 @@ #define SIP_NO_FILE #include "qgis_core.h" +#include "qgsmimedatautils.h" #include #include @@ -73,6 +74,12 @@ class CORE_EXPORT QgsStacAsset */ QString formatName() const; + /** + * Returns a uri for the asset if it is a local or cloud optimized file like COG or COPC + * \since QGIS 3.42 + */ + QgsMimeDataUtils::Uri uri() const; + private: QString mHref; QString mTitle; diff --git a/src/core/stac/qgsstacitem.cpp b/src/core/stac/qgsstacitem.cpp index 010d19910587..aece5886dddb 100644 --- a/src/core/stac/qgsstacitem.cpp +++ b/src/core/stac/qgsstacitem.cpp @@ -191,50 +191,14 @@ QString QgsStacItem::description() const QgsMimeDataUtils::UriList QgsStacItem::uris() const { QgsMimeDataUtils::UriList uris; - for ( auto it = mAssets.constBegin(); it != mAssets.constEnd(); ++it ) + for ( const QgsStacAsset &asset : std::as_const( mAssets ) ) { - QgsMimeDataUtils::Uri uri; - QUrl url( it->href() ); - if ( url.isLocalFile() ) - { - uri.uri = it->href(); - } - else if ( it->formatName() == QLatin1String( "COG" ) ) - { - uri.layerType = QStringLiteral( "raster" ); - uri.providerKey = QStringLiteral( "gdal" ); - if ( it->href().startsWith( QLatin1String( "http" ), Qt::CaseInsensitive ) || - it->href().startsWith( QLatin1String( "ftp" ), Qt::CaseInsensitive ) ) - { - uri.uri = QStringLiteral( "/vsicurl/%1" ).arg( it->href() ); - } - else if ( it->href().startsWith( QLatin1String( "s3://" ), Qt::CaseInsensitive ) ) - { - uri.uri = QStringLiteral( "/vsis3/%1" ).arg( it->href().mid( 5 ) ); - } - else - { - uri.uri = it->href(); - } - } - else if ( it->formatName() == QLatin1String( "COPC" ) ) - { - uri.layerType = QStringLiteral( "pointcloud" ); - uri.providerKey = QStringLiteral( "copc" ); - uri.uri = it->href(); - } - else if ( it->formatName() == QLatin1String( "EPT" ) ) - { - uri.layerType = QStringLiteral( "pointcloud" ); - uri.providerKey = QStringLiteral( "ept" ); - uri.uri = it->href(); - } + QgsMimeDataUtils::Uri uri = asset.uri(); // skip assets with incompatible formats if ( uri.uri.isEmpty() ) continue; - uri.name = it->title().isEmpty() ? url.fileName() : it->title(); uris.append( uri ); } return uris; diff --git a/src/core/stac/qgsstacitem.h b/src/core/stac/qgsstacitem.h index efd73f9eca1a..e5bd7901373e 100644 --- a/src/core/stac/qgsstacitem.h +++ b/src/core/stac/qgsstacitem.h @@ -128,7 +128,7 @@ class CORE_EXPORT QgsStacItem : public QgsStacObject QString description() const; /** - * Returns a list of uris of all assets that have a cloud optimized format like COG or COPC + * Returns a list of uris of all assets that are local or have a cloud optimized format like COG or COPC * \since QGIS 3.42 */ QgsMimeDataUtils::UriList uris() const; diff --git a/src/gui/stac/qgsstacsourceselect.cpp b/src/gui/stac/qgsstacsourceselect.cpp index a27c4808282f..12d6835cbfdf 100644 --- a/src/gui/stac/qgsstacsourceselect.cpp +++ b/src/gui/stac/qgsstacsourceselect.cpp @@ -123,20 +123,7 @@ void QgsStacSourceSelect::addButtonClicked() for ( auto &uri : std::as_const( allUris ) ) { - if ( uri.layerType == QLatin1String( "raster" ) ) - { - Q_NOWARN_DEPRECATED_PUSH - emit addRasterLayer( uri.uri, uri.name, uri.providerKey ); - Q_NOWARN_DEPRECATED_POP - emit addLayer( Qgis::LayerType::Raster, uri.uri, uri.name, uri.providerKey ); - } - else if ( uri.layerType == QLatin1String( "pointcloud" ) ) - { - Q_NOWARN_DEPRECATED_PUSH - emit addPointCloudLayer( uri.uri, uri.name, uri.providerKey ); - Q_NOWARN_DEPRECATED_POP - emit addLayer( Qgis::LayerType::PointCloud, uri.uri, uri.name, uri.providerKey ); - } + loadUri( uri ); } } @@ -512,6 +499,23 @@ void QgsStacSourceSelect::showItemsContextMenu( QPoint point ) if ( dsm ) bar = dsm->messageBar(); + const QgsStacItem *item = dynamic_cast( index.data( QgsStacItemListModel::Role::StacObject ).value() ); + QMenu *assetsMenu = menu->addMenu( tr( "Add Layer" ) ); + const QMap assets = item->assets(); + for ( const QgsStacAsset &asset : assets ) + { + if ( asset.isCloudOptimized() ) + { + QAction *loadAssetAction = new QAction( asset.title(), assetsMenu ); + connect( loadAssetAction, &QAction::triggered, this, [this, &asset] + { + QgsTemporaryCursorOverride cursorOverride( Qt::WaitCursor ); + loadUri( asset.uri() ); + } ); + assetsMenu->addAction( loadAssetAction ); + } + } + QAction *zoomToAction = new QAction( tr( "Zoom to Item" ) ); connect( zoomToAction, &QAction::triggered, this, [index, this] { @@ -561,8 +565,11 @@ void QgsStacSourceSelect::showItemsContextMenu( QPoint point ) details.exec(); } ); + menu->addAction( zoomToAction ); menu->addAction( panToAction ); + if ( !assetsMenu->isEmpty() ) + menu->addMenu( assetsMenu ); menu->addAction( downloadAction ); menu->addAction( detailsAction ); @@ -606,4 +613,21 @@ void QgsStacSourceSelect::showFootprints( bool enable ) } } +void QgsStacSourceSelect::loadUri( const QgsMimeDataUtils::Uri &uri ) +{ + if ( uri.layerType == QLatin1String( "raster" ) ) + { + Q_NOWARN_DEPRECATED_PUSH + emit addRasterLayer( uri.uri, uri.name, uri.providerKey ); + Q_NOWARN_DEPRECATED_POP + emit addLayer( Qgis::LayerType::Raster, uri.uri, uri.name, uri.providerKey ); + } + else if ( uri.layerType == QLatin1String( "pointcloud" ) ) + { + Q_NOWARN_DEPRECATED_PUSH + emit addPointCloudLayer( uri.uri, uri.name, uri.providerKey ); + Q_NOWARN_DEPRECATED_POP + emit addLayer( Qgis::LayerType::PointCloud, uri.uri, uri.name, uri.providerKey ); + } +} ///@endcond diff --git a/src/gui/stac/qgsstacsourceselect.h b/src/gui/stac/qgsstacsourceselect.h index c0b97df5341f..13d7dca761fd 100644 --- a/src/gui/stac/qgsstacsourceselect.h +++ b/src/gui/stac/qgsstacsourceselect.h @@ -19,6 +19,7 @@ #include "ui_qgsstacsourceselectbase.h" #include "qgsabstractdatasourcewidget.h" #include "qgis_gui.h" +#include "qgsmimedatautils.h" #include #include @@ -32,7 +33,6 @@ class QgsStacItemListModel; class QgsStacController; class QgsRubberBand; - class GUI_EXPORT QgsStacSourceSelect : public QgsAbstractDataSourceWidget, private Ui::QgsStacSourceSelectBase { Q_OBJECT @@ -99,6 +99,7 @@ class GUI_EXPORT QgsStacSourceSelect : public QgsAbstractDataSourceWidget, priva void highlightFootprint( const QModelIndex &index ); void showFootprints( bool enable ); + void loadUri( const QgsMimeDataUtils::Uri &uri ); QString mCollectionsUrl; QString mSearchUrl; From 29f319d2f920061e6a5e9134f76c0cbd6445574d Mon Sep 17 00:00:00 2001 From: uclaros Date: Fri, 29 Nov 2024 20:20:26 +0200 Subject: [PATCH 6/9] Added asset uri tests --- tests/src/core/testqgsstac.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/src/core/testqgsstac.cpp b/tests/src/core/testqgsstac.cpp index 02d0d1a15b35..ac017c32df92 100644 --- a/tests/src/core/testqgsstac.cpp +++ b/tests/src/core/testqgsstac.cpp @@ -200,14 +200,27 @@ void TestQgsStac::testParseLocalItem() QVERIFY( asset.isCloudOptimized() ); QCOMPARE( asset.formatName(), QStringLiteral( "COG" ) ); + QgsMimeDataUtils::Uri uri = asset.uri(); + QCOMPARE( uri.uri, basePath + QStringLiteral( "20201211_223832_CS2_analytic.tif" ) ); + QCOMPARE( uri.name, QStringLiteral( "analytic" ) ); + QCOMPARE( uri.layerType, QStringLiteral( "raster" ) ); + asset = item->assets().value( QStringLiteral( "thumbnail" ), QgsStacAsset( {}, {}, {}, {}, {} ) ); QCOMPARE( asset.href(), QStringLiteral( "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg" ) ); QVERIFY( !asset.isCloudOptimized() ); + uri = asset.uri(); + QVERIFY( !uri.isValid() ); + QVERIFY( uri.uri.isEmpty() ); + QVERIFY( uri.name.isEmpty() ); // normal geotiff is not cloud optimized asset = item->assets().value( QStringLiteral( "udm" ), QgsStacAsset( {}, {}, {}, {}, {} ) ); QVERIFY( !asset.isCloudOptimized() ); QCOMPARE( asset.formatName(), QString() ); + uri = asset.uri(); + QVERIFY( !uri.isValid() ); + QVERIFY( uri.uri.isEmpty() ); + QVERIFY( uri.name.isEmpty() ); delete item; } From 4157d9ed9f86c5c5d45410630aa3d6e953045d49 Mon Sep 17 00:00:00 2001 From: uclaros Date: Mon, 2 Dec 2024 18:22:16 +0200 Subject: [PATCH 7/9] fix test --- src/core/stac/qgsstacasset.cpp | 11 ++++++----- src/core/stac/qgsstacasset.h | 2 +- src/core/stac/qgsstacitem.h | 2 +- tests/src/core/testqgsstac.cpp | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/core/stac/qgsstacasset.cpp b/src/core/stac/qgsstacasset.cpp index 764dc823795f..bf324f10984b 100644 --- a/src/core/stac/qgsstacasset.cpp +++ b/src/core/stac/qgsstacasset.cpp @@ -79,11 +79,7 @@ QgsMimeDataUtils::Uri QgsStacAsset::uri() const { QgsMimeDataUtils::Uri uri; QUrl url( href() ); - if ( url.isLocalFile() ) - { - uri.uri = href(); - } - else if ( formatName() == QLatin1String( "COG" ) ) + if( formatName() == QLatin1String( "COG" ) ) { uri.layerType = QStringLiteral( "raster" ); uri.providerKey = QStringLiteral( "gdal" ); @@ -113,6 +109,11 @@ QgsMimeDataUtils::Uri QgsStacAsset::uri() const uri.providerKey = QStringLiteral( "ept" ); uri.uri = href(); } + else + { + return {}; + } + uri.name = title().isEmpty() ? url.fileName() : title(); return uri; diff --git a/src/core/stac/qgsstacasset.h b/src/core/stac/qgsstacasset.h index ebc9954760f8..5cd616b8c557 100644 --- a/src/core/stac/qgsstacasset.h +++ b/src/core/stac/qgsstacasset.h @@ -75,7 +75,7 @@ class CORE_EXPORT QgsStacAsset QString formatName() const; /** - * Returns a uri for the asset if it is a local or cloud optimized file like COG or COPC + * Returns a uri for the asset if it is a cloud optimized file like COG or COPC * \since QGIS 3.42 */ QgsMimeDataUtils::Uri uri() const; diff --git a/src/core/stac/qgsstacitem.h b/src/core/stac/qgsstacitem.h index e5bd7901373e..efd73f9eca1a 100644 --- a/src/core/stac/qgsstacitem.h +++ b/src/core/stac/qgsstacitem.h @@ -128,7 +128,7 @@ class CORE_EXPORT QgsStacItem : public QgsStacObject QString description() const; /** - * Returns a list of uris of all assets that are local or have a cloud optimized format like COG or COPC + * Returns a list of uris of all assets that have a cloud optimized format like COG or COPC * \since QGIS 3.42 */ QgsMimeDataUtils::UriList uris() const; diff --git a/tests/src/core/testqgsstac.cpp b/tests/src/core/testqgsstac.cpp index ac017c32df92..a4cb32205dcf 100644 --- a/tests/src/core/testqgsstac.cpp +++ b/tests/src/core/testqgsstac.cpp @@ -202,7 +202,7 @@ void TestQgsStac::testParseLocalItem() QgsMimeDataUtils::Uri uri = asset.uri(); QCOMPARE( uri.uri, basePath + QStringLiteral( "20201211_223832_CS2_analytic.tif" ) ); - QCOMPARE( uri.name, QStringLiteral( "analytic" ) ); + QCOMPARE( uri.name, QStringLiteral( "4-Band Analytic" ) ); QCOMPARE( uri.layerType, QStringLiteral( "raster" ) ); asset = item->assets().value( QStringLiteral( "thumbnail" ), QgsStacAsset( {}, {}, {}, {}, {} ) ); From 7f5fc228cb2a029167414e0cb93958141dc69180 Mon Sep 17 00:00:00 2001 From: uclaros Date: Tue, 3 Dec 2024 10:11:08 +0200 Subject: [PATCH 8/9] Address review --- src/gui/stac/qgsstacsourceselect.cpp | 4 ++-- src/gui/stac/qgsstacsourceselect.h | 3 ++- src/ui/qgsstacsourceselectbase.ui | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/gui/stac/qgsstacsourceselect.cpp b/src/gui/stac/qgsstacsourceselect.cpp index 12d6835cbfdf..eda959f02212 100644 --- a/src/gui/stac/qgsstacsourceselect.cpp +++ b/src/gui/stac/qgsstacsourceselect.cpp @@ -516,7 +516,7 @@ void QgsStacSourceSelect::showItemsContextMenu( QPoint point ) } } - QAction *zoomToAction = new QAction( tr( "Zoom to Item" ) ); + QAction *zoomToAction = new QAction( tr( "Zoom to Item" ), menu ); connect( zoomToAction, &QAction::triggered, this, [index, this] { QgsGeometry geom = index.data( QgsStacItemListModel::Role::Geometry ).value(); @@ -531,7 +531,7 @@ void QgsStacSourceSelect::showItemsContextMenu( QPoint point ) } } ); - QAction *panToAction = new QAction( tr( "Pan to Item" ) ); + QAction *panToAction = new QAction( tr( "Pan to Item" ), menu ); connect( panToAction, &QAction::triggered, this, [index, this] { QgsGeometry geom = index.data( QgsStacItemListModel::Role::Geometry ).value(); diff --git a/src/gui/stac/qgsstacsourceselect.h b/src/gui/stac/qgsstacsourceselect.h index 13d7dca761fd..3bee8fa74f14 100644 --- a/src/gui/stac/qgsstacsourceselect.h +++ b/src/gui/stac/qgsstacsourceselect.h @@ -20,6 +20,7 @@ #include "qgsabstractdatasourcewidget.h" #include "qgis_gui.h" #include "qgsmimedatautils.h" +#include "qobjectuniqueptr.h" #include #include @@ -108,7 +109,7 @@ class GUI_EXPORT QgsStacSourceSelect : public QgsAbstractDataSourceWidget, priva QgsStacController *mStac = nullptr; QgsStacItemListModel *mItemsModel = nullptr; QgsStacSearchParametersDialog *mParametersDialog = nullptr; - std::unique_ptr mCurrentItemBand; + QObjectUniquePtr mCurrentItemBand; QVector mRubberBands; }; diff --git a/src/ui/qgsstacsourceselectbase.ui b/src/ui/qgsstacsourceselectbase.ui index 14cd61dd26e8..5e1e755f50e8 100644 --- a/src/ui/qgsstacsourceselectbase.ui +++ b/src/ui/qgsstacsourceselectbase.ui @@ -178,7 +178,7 @@ - Show Footprints + Show footprints true From 97784cb1a5045880d183c365be56e23019bb91aa Mon Sep 17 00:00:00 2001 From: uclaros Date: Tue, 3 Dec 2024 11:48:33 +0200 Subject: [PATCH 9/9] format mess --- src/core/stac/qgsstacasset.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/stac/qgsstacasset.cpp b/src/core/stac/qgsstacasset.cpp index bf324f10984b..7b98b379d356 100644 --- a/src/core/stac/qgsstacasset.cpp +++ b/src/core/stac/qgsstacasset.cpp @@ -79,7 +79,7 @@ QgsMimeDataUtils::Uri QgsStacAsset::uri() const { QgsMimeDataUtils::Uri uri; QUrl url( href() ); - if( formatName() == QLatin1String( "COG" ) ) + if ( formatName() == QLatin1String( "COG" ) ) { uri.layerType = QStringLiteral( "raster" ); uri.providerKey = QStringLiteral( "gdal" );