diff --git a/images/images.qrc b/images/images.qrc index 117010e8d713..38062a658d41 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -1009,6 +1009,7 @@ themes/default/mActionTextInsideRect.svg themes/default/mIconLabelingRules.svg themes/default/stacked-diagram.svg + themes/default/mIconStac.svg qgis_tips/symbol_levels.png diff --git a/images/themes/default/mIconStac.svg b/images/themes/default/mIconStac.svg new file mode 100644 index 000000000000..fbf4910a015d --- /dev/null +++ b/images/themes/default/mIconStac.svg @@ -0,0 +1,82 @@ + + diff --git a/python/PyQt6/gui/auto_additions/qgsmanageconnectionsdialog.py b/python/PyQt6/gui/auto_additions/qgsmanageconnectionsdialog.py index 87c00751fe0f..ef072a547dd5 100644 --- a/python/PyQt6/gui/auto_additions/qgsmanageconnectionsdialog.py +++ b/python/PyQt6/gui/auto_additions/qgsmanageconnectionsdialog.py @@ -15,3 +15,4 @@ QgsManageConnectionsDialog.TiledScene = QgsManageConnectionsDialog.Type.TiledScene QgsManageConnectionsDialog.SensorThings = QgsManageConnectionsDialog.Type.SensorThings QgsManageConnectionsDialog.CloudStorage = QgsManageConnectionsDialog.Type.CloudStorage +QgsManageConnectionsDialog.STAC = QgsManageConnectionsDialog.Type.STAC diff --git a/python/PyQt6/gui/auto_generated/qgsmanageconnectionsdialog.sip.in b/python/PyQt6/gui/auto_generated/qgsmanageconnectionsdialog.sip.in index 3b9431b74fa1..dda52f832009 100644 --- a/python/PyQt6/gui/auto_generated/qgsmanageconnectionsdialog.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsmanageconnectionsdialog.sip.in @@ -38,6 +38,7 @@ class QgsManageConnectionsDialog : QDialog TiledScene, SensorThings, CloudStorage, + STAC, }; QgsManageConnectionsDialog( QWidget *parent /TransferThis/ = 0, Mode mode = Export, Type type = WMS, const QString &fileName = QString() ); diff --git a/python/gui/auto_generated/qgsmanageconnectionsdialog.sip.in b/python/gui/auto_generated/qgsmanageconnectionsdialog.sip.in index b1248fdde6b6..9ae8f04e8034 100644 --- a/python/gui/auto_generated/qgsmanageconnectionsdialog.sip.in +++ b/python/gui/auto_generated/qgsmanageconnectionsdialog.sip.in @@ -38,6 +38,7 @@ class QgsManageConnectionsDialog : QDialog TiledScene, SensorThings, CloudStorage, + STAC, }; QgsManageConnectionsDialog( QWidget *parent /TransferThis/ = 0, Mode mode = Export, Type type = WMS, const QString &fileName = QString() ); diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index e584b303f47e..74716e65276b 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -233,6 +233,8 @@ #include "qgscustomlayerorderwidget.h" #include "qgsdataitemproviderregistry.h" #include "qgsdataitemguiproviderregistry.h" +#include "qgsstacdataitems.h" +#include "qgsstacdataitemguiprovider.h" #include "qgsdatasourceuri.h" #include "qgsdatumtransformdialog.h" #include "qgsdoublespinbox.h" @@ -1442,6 +1444,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipBadLayers QgsGui::dataItemGuiProviderRegistry()->addProvider( new QgsFieldDomainItemGuiProvider() ); QgsGui::dataItemGuiProviderRegistry()->addProvider( new QgsRelationshipItemGuiProvider() ); QgsGui::dataItemGuiProviderRegistry()->addProvider( new QgsDatabaseItemGuiProvider() ); + QgsGui::dataItemGuiProviderRegistry()->addProvider( new QgsStacDataItemGuiProvider() ); QShortcut *showBrowserDock = new QShortcut( QKeySequence( tr( "Ctrl+2" ) ), this ); connect( showBrowserDock, &QShortcut::activated, mBrowserWidget, &QgsDockWidget::toggleUserVisible ); @@ -1647,6 +1650,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipBadLayers #endif QgsApplication::dataItemProviderRegistry()->addProvider( new QgsProjectDataItemProvider() ); + QgsApplication::dataItemProviderRegistry()->addProvider( new QgsStacDataItemProvider() ); // now when all data item providers are registered, customize both browsers QgsCustomization::instance()->updateBrowserWidget( mBrowserWidget ); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index dc1ee15a7a2d..d0fab41a0f74 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -505,6 +505,11 @@ set(QGIS_GUI_SRCS settings/qgssettingstreemodel.cpp settings/qgssettingstreewidget.cpp + stac/qgsstacconnectiondialog.cpp + stac/qgsstacdataitemguiprovider.cpp + stac/qgsstacdownloadassetsdialog.cpp + stac/qgsstacobjectdetailsdialog.cpp + tableeditor/qgstableeditordialog.cpp tableeditor/qgstableeditorformattingwidget.cpp tableeditor/qgstableeditorwidget.cpp @@ -1560,6 +1565,11 @@ set(QGIS_GUI_HDRS settings/qgssettingstreemodel.h settings/qgssettingstreewidget.h + stac/qgsstacconnectiondialog.h + stac/qgsstacdataitemguiprovider.h + stac/qgsstacdownloadassetsdialog.h + stac/qgsstacobjectdetailsdialog.h + tableeditor/qgstableeditordialog.h tableeditor/qgstableeditorformattingwidget.h tableeditor/qgstableeditorwidget.h @@ -1720,6 +1730,7 @@ target_include_directories(qgis_gui PUBLIC ${CMAKE_SOURCE_DIR}/src/gui/raster ${CMAKE_SOURCE_DIR}/src/gui/sensor ${CMAKE_SOURCE_DIR}/src/gui/settings + ${CMAKE_SOURCE_DIR}/src/gui/stac ${CMAKE_SOURCE_DIR}/src/gui/tableeditor ${CMAKE_SOURCE_DIR}/src/gui/tiledscene ${CMAKE_SOURCE_DIR}/src/gui/vector diff --git a/src/gui/qgsmanageconnectionsdialog.cpp b/src/gui/qgsmanageconnectionsdialog.cpp index 33cefe551acc..98968d24dca8 100644 --- a/src/gui/qgsmanageconnectionsdialog.cpp +++ b/src/gui/qgsmanageconnectionsdialog.cpp @@ -30,6 +30,7 @@ #include "qgstiledsceneconnection.h" #include "qgssensorthingsconnection.h" #include "qgsgdalcloudconnection.h" +#include "qgsstacconnection.h" QgsManageConnectionsDialog::QgsManageConnectionsDialog( QWidget *parent, Mode mode, Type type, const QString &fileName ) : QDialog( parent ) @@ -159,6 +160,9 @@ void QgsManageConnectionsDialog::doExportImport() case CloudStorage: doc = saveCloudStorageConnections( items ); break; + case STAC: + doc = saveStacConnections( items ); + break; } QFile file( mFileName ); @@ -245,6 +249,9 @@ void QgsManageConnectionsDialog::doExportImport() case CloudStorage: loadCloudStorageConnections( doc, items ); break; + case STAC: + loadStacConnections( doc, items ); + break; } // clear connections list and close window listConnections->clear(); @@ -307,6 +314,9 @@ bool QgsManageConnectionsDialog::populateConnections() case CloudStorage: connections = QgsGdalCloudProviderConnection::sTreeConnectionCloud->items(); break; + case STAC: + connections = QgsStacConnection::sTreeConnectionStac->items(); + break; } for ( const QString &connection : std::as_const( connections ) ) { @@ -462,6 +472,14 @@ bool QgsManageConnectionsDialog::populateConnections() return false; } break; + case STAC: + if ( root.tagName() != QLatin1String( "qgsStacConnections" ) ) + { + QMessageBox::information( this, tr( "Loading Connections" ), + tr( "The file is not a STAC connections exchange file." ) ); + return false; + } + break; } QDomElement child = root.firstChildElement(); @@ -891,6 +909,32 @@ QDomDocument QgsManageConnectionsDialog::saveCloudStorageConnections( const QStr return doc; } +QDomDocument QgsManageConnectionsDialog::saveStacConnections( const QStringList &connections ) +{ + QDomDocument doc( QStringLiteral( "connections" ) ); + QDomElement root = doc.createElement( QStringLiteral( "qgsStacConnections" ) ); + root.setAttribute( QStringLiteral( "version" ), QStringLiteral( "1.0" ) ); + doc.appendChild( root ); + + for ( int i = 0; i < connections.count(); ++i ) + { + QDomElement el = doc.createElement( QStringLiteral( "stac" ) ); + + el.setAttribute( QStringLiteral( "name" ), connections[ i ] ); + el.setAttribute( QStringLiteral( "url" ), QgsStacConnection::settingsUrl->value( connections[ i ] ) ); + el.setAttribute( QStringLiteral( "authcfg" ), QgsStacConnection::settingsAuthcfg->value( connections[ i ] ) ); + el.setAttribute( QStringLiteral( "username" ), QgsStacConnection::settingsUsername->value( connections[ i ] ) ); + el.setAttribute( QStringLiteral( "password" ), QgsStacConnection::settingsPassword->value( connections[ i ] ) ); + + QgsHttpHeaders httpHeader( QgsStacConnection::settingsHeaders->value( connections[ i ] ) ); + httpHeader.updateDomElement( el ); + + root.appendChild( el ); + } + + return doc; +} + void QgsManageConnectionsDialog::loadOWSConnections( const QDomDocument &doc, const QStringList &items, const QString &service ) { const QDomElement root = doc.documentElement(); @@ -1974,6 +2018,89 @@ void QgsManageConnectionsDialog::loadCloudStorageConnections( const QDomDocument } } +void QgsManageConnectionsDialog::loadStacConnections( const QDomDocument &doc, const QStringList &items ) +{ + const QDomElement root = doc.documentElement(); + if ( root.tagName() != QLatin1String( "qgsStacConnections" ) ) + { + QMessageBox::information( this, tr( "Loading Connections" ), + tr( "The file is not a STAC connections exchange file." ) ); + return; + } + + QString connectionName; + QgsSettings settings; + settings.beginGroup( QStringLiteral( "/qgis/connections-stac" ) ); + QStringList keys = settings.childGroups(); + settings.endGroup(); + QDomElement child = root.firstChildElement(); + bool prompt = true; + bool overwrite = true; + + while ( !child.isNull() ) + { + connectionName = child.attribute( QStringLiteral( "name" ) ); + if ( !items.contains( connectionName ) ) + { + child = child.nextSiblingElement(); + continue; + } + + // check for duplicates + if ( keys.contains( connectionName ) && prompt ) + { + const int res = QMessageBox::warning( this, + tr( "Loading Connections" ), + tr( "Connection with name '%1' already exists. Overwrite?" ) + .arg( connectionName ), + QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll | QMessageBox::Cancel ); + + switch ( res ) + { + case QMessageBox::Cancel: + return; + case QMessageBox::No: + child = child.nextSiblingElement(); + continue; + case QMessageBox::Yes: + overwrite = true; + break; + case QMessageBox::YesToAll: + prompt = false; + overwrite = true; + break; + case QMessageBox::NoToAll: + prompt = false; + overwrite = false; + break; + } + } + + if ( keys.contains( connectionName ) ) + { + if ( !overwrite ) + { + child = child.nextSiblingElement(); + continue; + } + } + else + { + keys << connectionName; + } + + QgsStacConnection::settingsUrl->setValue( child.attribute( QStringLiteral( "url" ) ), connectionName ); + QgsStacConnection::settingsAuthcfg->setValue( child.attribute( QStringLiteral( "authcfg" ) ), connectionName ); + QgsStacConnection::settingsUsername->setValue( child.attribute( QStringLiteral( "username" ) ), connectionName ); + QgsStacConnection::settingsPassword->setValue( child.attribute( QStringLiteral( "password" ) ), connectionName ); + + QgsHttpHeaders httpHeader( child ); + QgsStacConnection::settingsHeaders->setValue( httpHeader.headers(), connectionName ); + + child = child.nextSiblingElement(); + } +} + void QgsManageConnectionsDialog::selectAll() { listConnections->selectAll(); diff --git a/src/gui/qgsmanageconnectionsdialog.h b/src/gui/qgsmanageconnectionsdialog.h index 74df71d8b7bc..3e1a4bfb1a6a 100644 --- a/src/gui/qgsmanageconnectionsdialog.h +++ b/src/gui/qgsmanageconnectionsdialog.h @@ -55,6 +55,7 @@ class GUI_EXPORT QgsManageConnectionsDialog : public QDialog, private Ui::QgsMan TiledScene, //!< Tiled scene connection \since QGIS 3.34 SensorThings, //!< SensorThings connections \since QGIS 3.36 CloudStorage, //!< Cloud storage connections \since QGIS 3.40 + STAC, //!< SpatioTemporal Asset Catalog connections \since QGIS 3.40 }; /** @@ -83,6 +84,7 @@ class GUI_EXPORT QgsManageConnectionsDialog : public QDialog, private Ui::QgsMan QDomDocument saveTiledSceneConnections( const QStringList &connections ); QDomDocument saveSensorThingsConnections( const QStringList &connections ); QDomDocument saveCloudStorageConnections( const QStringList &connections ); + QDomDocument saveStacConnections( const QStringList &connections ); void loadOWSConnections( const QDomDocument &doc, const QStringList &items, const QString &service ); void loadWfsConnections( const QDomDocument &doc, const QStringList &items ); @@ -96,6 +98,7 @@ class GUI_EXPORT QgsManageConnectionsDialog : public QDialog, private Ui::QgsMan void loadTiledSceneConnections( const QDomDocument &doc, const QStringList &items ); void loadSensorThingsConnections( const QDomDocument &doc, const QStringList &items ); void loadCloudStorageConnections( const QDomDocument &doc, const QStringList &items ); + void loadStacConnections( const QDomDocument &doc, const QStringList &items ); QString mFileName; Mode mDialogMode; diff --git a/src/gui/stac/qgsstacconnectiondialog.cpp b/src/gui/stac/qgsstacconnectiondialog.cpp new file mode 100644 index 000000000000..e963b0f7f6be --- /dev/null +++ b/src/gui/stac/qgsstacconnectiondialog.cpp @@ -0,0 +1,78 @@ +/*************************************************************************** + qgsstacconnectiondialog.h + --------------------- + begin : September 2024 + copyright : (C) 2024 by Stefanos Natsis + email : uclaros at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsstacconnectiondialog.h" +#include "qgsstacconnection.h" +#include "qgsgui.h" +#include +#include + +///@cond PRIVATE + +QgsStacConnectionDialog::QgsStacConnectionDialog( QWidget *parent ) + : QDialog( parent ) +{ + setupUi( this ); + QgsGui::enableAutoGeometryRestore( this ); + + buttonBox->button( QDialogButtonBox::Ok )->setDisabled( true ); + + connect( mEditName, &QLineEdit::textChanged, this, &QgsStacConnectionDialog::updateOkButtonState ); + connect( mEditUrl, &QLineEdit::textChanged, this, &QgsStacConnectionDialog::updateOkButtonState ); +} + +void QgsStacConnectionDialog::setConnection( const QString &name, const QString &uri ) +{ + mEditName->setText( name ); + + const QgsStacConnection::Data conn = QgsStacConnection::decodedUri( uri ); + mEditUrl->setText( conn.url ); + + mAuthSettings->setUsername( conn.username ); + mAuthSettings->setPassword( conn.password ); + mEditReferer->setText( conn.httpHeaders[QgsHttpHeaders::KEY_REFERER].toString() ); + mAuthSettings->setConfigId( conn.authCfg ); +} + +QString QgsStacConnectionDialog::connectionUri() const +{ + QgsStacConnection::Data conn; + conn.url = mEditUrl->text(); + + conn.username = mAuthSettings->username(); + conn.password = mAuthSettings->password(); + conn.httpHeaders[QgsHttpHeaders::KEY_REFERER] = mEditReferer->text(); + conn.authCfg = mAuthSettings->configId( ); + + return QgsStacConnection::encodedUri( conn ); +} + +QString QgsStacConnectionDialog::connectionName() const +{ + return mEditName->text(); +} + +void QgsStacConnectionDialog::updateOkButtonState() +{ + const bool enabled = !mEditName->text().isEmpty() && !mEditUrl->text().isEmpty(); + buttonBox->button( QDialogButtonBox::Ok )->setEnabled( enabled ); +} + +void QgsStacConnectionDialog::accept() +{ + QDialog::accept(); +} + +///@endcond diff --git a/src/gui/stac/qgsstacconnectiondialog.h b/src/gui/stac/qgsstacconnectiondialog.h new file mode 100644 index 000000000000..81fed692cfb6 --- /dev/null +++ b/src/gui/stac/qgsstacconnectiondialog.h @@ -0,0 +1,47 @@ +/*************************************************************************** + qgsstacconnectiondialog.h + --------------------- + begin : September 2024 + copyright : (C) 2024 by Stefanos Natsis + email : uclaros at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSSTACCONNECTIONDIALOG_H +#define QGSSTACCONNECTIONDIALOG_H + +///@cond PRIVATE +#define SIP_NO_FILE + +#include + +#include "ui_qgsstacconnectiondialog.h" + + +class QgsStacConnectionDialog : public QDialog, public Ui::QgsStacConnectionDialog +{ + Q_OBJECT + public: + explicit QgsStacConnectionDialog( QWidget *parent = nullptr ); + + void setConnection( const QString &name, const QString &uri ); + + QString connectionUri() const; + QString connectionName() const; + + void accept() override; + + private slots: + void updateOkButtonState(); + +}; + +///@endcond + +#endif // QGSSTACCONNECTIONDIALOG_H diff --git a/src/gui/stac/qgsstacdataitemguiprovider.cpp b/src/gui/stac/qgsstacdataitemguiprovider.cpp new file mode 100644 index 000000000000..7d709bee69cf --- /dev/null +++ b/src/gui/stac/qgsstacdataitemguiprovider.cpp @@ -0,0 +1,276 @@ +/*************************************************************************** + qgsstacdataitemguiprovider.cpp + -------------------------------------- + begin : September 2024 + copyright : (C) 2024 by Stefanos Natsis + email : uclaros at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsstacdataitemguiprovider.h" +#include "qgsnetworkcontentfetcherregistry.h" +#include "qgsstaccontroller.h" +#include "qgsstacdataitems.h" +#include "qgsstacconnection.h" +#include "qgsstacconnectiondialog.h" +#include "qgsmanageconnectionsdialog.h" +#include "qgsdataitemguiproviderutils.h" +#include "qgsstacitem.h" +#include "qgsstacdownloadassetsdialog.h" +#include "qgsstacobjectdetailsdialog.h" +#include "qgsapplication.h" + + +///@cond PRIVATE + +void QgsStacDataItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *menu, const QList &selection, QgsDataItemGuiContext context ) +{ + if ( QgsStacRootItem *rootItem = qobject_cast< QgsStacRootItem * >( item ) ) + { + QAction *actionNewConnection = new QAction( tr( "New STAC Connection…" ), menu ); + connect( actionNewConnection, &QAction::triggered, this, [rootItem] { newConnection( rootItem ); } ); + menu->addAction( actionNewConnection ); + + menu->addSeparator(); + + QAction *actionSave = new QAction( tr( "Save Connections…" ), menu ); + connect( actionSave, &QAction::triggered, this, [] { saveConnections(); } ); + menu->addAction( actionSave ); + + QAction *actionLoad = new QAction( tr( "Load Connections…" ), menu ); + connect( actionLoad, &QAction::triggered, this, [rootItem] { loadConnections( rootItem ); } ); + menu->addAction( actionLoad ); + } + + if ( QgsStacConnectionItem *connItem = qobject_cast< QgsStacConnectionItem * >( item ) ) + { + QAction *actionEdit = new QAction( tr( "Edit Connection…" ), menu ); + connect( actionEdit, &QAction::triggered, this, [connItem] { editConnection( connItem ); } ); + menu->addAction( actionEdit ); + + const QList< QgsStacConnectionItem * > stacConnectionItems = QgsDataItem::filteredItems( selection ); + QAction *actionDelete = new QAction( stacConnectionItems.size() > 1 ? tr( "Remove Connections…" ) : tr( "Remove Connection…" ), menu ); + connect( actionDelete, &QAction::triggered, this, [stacConnectionItems, context] + { + QgsDataItemGuiProviderUtils::deleteConnections( stacConnectionItems, []( const QString & connectionName ) + { + QgsStacConnection( QString() ).remove( connectionName ); + }, context ); + } ); + menu->addAction( actionDelete ); + } + + if ( QgsStacCatalogItem *catalogItem = qobject_cast< QgsStacCatalogItem * >( item ) ) + { + menu->addSeparator(); + + QAction *actionRefresh = new QAction( tr( "Refresh" ), menu ); + connect( actionRefresh, &QAction::triggered, this, [catalogItem] { catalogItem->refresh(); } ); + menu->addAction( actionRefresh ); + + if ( catalogItem->stacCatalog() ) + { + QAction *actionDetails = new QAction( tr( "Details" ), menu ); + connect( actionDetails, &QAction::triggered, this, [catalogItem] { showDetails( catalogItem ); } ); + menu->addAction( actionDetails ); + } + } + + if ( QgsStacItemItem *itemItem = qobject_cast< QgsStacItemItem * >( item ) ) + { + QAction *actionRefresh = new QAction( tr( "Refresh" ), menu ); + connect( actionRefresh, &QAction::triggered, this, [itemItem] { itemItem->refresh(); } ); + menu->addAction( actionRefresh ); + + if ( itemItem->stacItem() ) + { + menu->addSeparator(); + + QAction *actionDownload = new QAction( tr( "Download assets" ), menu ); + connect( actionDownload, &QAction::triggered, this, [itemItem, context] { downloadAssets( itemItem, context ); } ); + menu->addAction( actionDownload ); + + QAction *actionDetails = new QAction( tr( "Details" ), menu ); + connect( actionDetails, &QAction::triggered, this, [itemItem] { showDetails( itemItem ); } ); + menu->addAction( actionDetails ); + } + } +} + +void QgsStacDataItemGuiProvider::editConnection( QgsDataItem *item ) +{ + const QgsStacConnection::Data connection = QgsStacConnection::connection( item->name() ); + const QString uri = QgsStacConnection::encodedUri( connection ); + + QgsStacConnectionDialog dlg; + + dlg.setConnection( item->name(), uri ); + if ( !dlg.exec() ) + return; + + QgsStacConnection( QString() ).remove( item->name() ); + + QgsStacConnection::Data newConnection = QgsStacConnection::decodedUri( dlg.connectionUri() ); + + QgsStacConnection::addConnection( dlg.connectionName(), newConnection ); + + item->parent()->refreshConnections(); +} + +void QgsStacDataItemGuiProvider::refreshConnection( QgsDataItem *item ) +{ + item->refresh(); + // the parent should be updated + if ( item->parent() ) + item->parent()->refreshConnections(); +} + +void QgsStacDataItemGuiProvider::newConnection( QgsDataItem *item ) +{ + QgsStacConnectionDialog dlg; + if ( !dlg.exec() ) + return; + + QgsStacConnection::Data conn = QgsStacConnection::decodedUri( dlg.connectionUri() ); + + QgsStacConnection::addConnection( dlg.connectionName(), conn ); + + item->refreshConnections(); +} + +void QgsStacDataItemGuiProvider::saveConnections() +{ + QgsManageConnectionsDialog dlg( nullptr, QgsManageConnectionsDialog::Export, QgsManageConnectionsDialog::STAC ); + dlg.exec(); +} + +void QgsStacDataItemGuiProvider::loadConnections( QgsDataItem *item ) +{ + const QString fileName = QFileDialog::getOpenFileName( nullptr, tr( "Load Connections" ), QDir::homePath(), + tr( "XML files (*.xml *.XML)" ) ); + if ( fileName.isEmpty() ) + { + return; + } + + QgsManageConnectionsDialog dlg( nullptr, QgsManageConnectionsDialog::Import, QgsManageConnectionsDialog::STAC, fileName ); + if ( dlg.exec() == QDialog::Accepted ) + item->refreshConnections(); +} + +void QgsStacDataItemGuiProvider::showDetails( QgsDataItem *item ) +{ + QgsStacObject *obj = nullptr; + + if ( QgsStacItemItem *itemItem = qobject_cast< QgsStacItemItem * >( item ) ) + { + obj = itemItem->stacItem(); + } + else if ( QgsStacCatalogItem *catalogItem = qobject_cast< QgsStacCatalogItem * >( item ) ) + { + obj = catalogItem->stacCatalog(); + } + + if ( obj ) + { + QgsStacObjectDetailsDialog d; + d.setStacObject( obj ); + d.exec(); + } +} + +void QgsStacDataItemGuiProvider::downloadAssets( QgsDataItem *item, QgsDataItemGuiContext context ) +{ + QgsStacItemItem *itemItem = qobject_cast< QgsStacItemItem * >( item ); + + if ( ! itemItem ) + return; + + 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 ) + { + // canceled + 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 ); + } + } + +} + +///@endcond diff --git a/src/gui/stac/qgsstacdataitemguiprovider.h b/src/gui/stac/qgsstacdataitemguiprovider.h new file mode 100644 index 000000000000..15446158c3e6 --- /dev/null +++ b/src/gui/stac/qgsstacdataitemguiprovider.h @@ -0,0 +1,52 @@ +/*************************************************************************** + qgsstacdataitemguiprovider.h + -------------------------------------- + begin : September 2024 + copyright : (C) 2024 by Stefanos Natsis + email : uclaros at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSSTACDATAITEMGUIPROVIDER_H +#define QGSSTACDATAITEMGUIPROVIDER_H + +///@cond PRIVATE +#define SIP_NO_FILE + +#include "qgsdataitemguiprovider.h" +#include "qgis_gui.h" + + +class GUI_EXPORT QgsStacDataItemGuiProvider : public QObject, public QgsDataItemGuiProvider +{ + Q_OBJECT + public: + + QgsStacDataItemGuiProvider() = default; + + QString name() override { return QStringLiteral( "STAC" ); } + + void populateContextMenu( QgsDataItem *item, QMenu *menu, + const QList &selectedItems, + QgsDataItemGuiContext context ) override; + + private: + static void newConnection( QgsDataItem *item ); + static void editConnection( QgsDataItem *item ); + static void refreshConnection( QgsDataItem *item ); + static void saveConnections(); + static void loadConnections( QgsDataItem *item ); + + static void showDetails( QgsDataItem *item ); + static void downloadAssets( QgsDataItem *item, QgsDataItemGuiContext context ); +}; + +///@endcond + +#endif // QGSSTACDATAITEMGUIPROVIDER_H diff --git a/src/gui/stac/qgsstacdownloadassetsdialog.cpp b/src/gui/stac/qgsstacdownloadassetsdialog.cpp new file mode 100644 index 000000000000..0dab898f4210 --- /dev/null +++ b/src/gui/stac/qgsstacdownloadassetsdialog.cpp @@ -0,0 +1,106 @@ +/*************************************************************************** + qgsstacdownloadassetsdialog.cpp + --------------------- + begin : September 2024 + copyright : (C) 2024 by Stefanos Natsis + email : uclaros at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsstacdownloadassetsdialog.h" +#include "qgsgui.h" +#include "qgssettings.h" +#include "qgsproject.h" + +#include +#include + +///@cond PRIVATE + +QgsStacDownloadAssetsDialog::QgsStacDownloadAssetsDialog( QWidget *parent ) : + QDialog( parent ) +{ + setupUi( this ); + QgsGui::enableAutoGeometryRestore( this ); + mFileWidget->setStorageMode( QgsFileWidget::StorageMode::GetDirectory ); + + QString defPath = QDir::cleanPath( QFileInfo( QgsProject::instance()->absoluteFilePath() ).path() ); + defPath = QgsSettings().value( QStringLiteral( "UI/lastFileNameWidgetDir" ), defPath ).toString(); + if ( defPath.isEmpty() ) + defPath = QDir::homePath(); + mFileWidget->setFilePath( defPath ); + mFileWidget->lineEdit()->setReadOnly( true ); + + connect( mSelectAllButton, &QPushButton::clicked, this, &QgsStacDownloadAssetsDialog::selectAll ); + connect( mDeselectAllButton, &QPushButton::clicked, this, &QgsStacDownloadAssetsDialog::deselectAll ); +} + +void QgsStacDownloadAssetsDialog::setStacItem( QgsStacItem *stacItem ) +{ + if ( ! stacItem ) + return; + + const QMap< QString, QgsStacAsset > assets = stacItem->assets(); + for ( auto it = assets.constBegin(); it != assets.constEnd(); ++it ) + { + QTreeWidgetItem *item = new QTreeWidgetItem(); + item->setText( 0, it.key() ); + item->setToolTip( 0, it.key() ); + item->setCheckState( 0, Qt::Checked ); + item->setText( 1, it->title() ); + item->setToolTip( 1, it->title() ); + item->setText( 2, it->description() ); + item->setToolTip( 2, it->description() ); + item->setText( 3, it->roles().join( "," ) ); + item->setToolTip( 3, it->roles().join( "," ) ); + item->setText( 4, it->mediaType() ); + item->setToolTip( 4, it->mediaType() ); + item->setText( 5, it->href() ); + item->setToolTip( 5, it->href() ); + + mTreeWidget->addTopLevelItem( item ); + } +} + +QString QgsStacDownloadAssetsDialog::selectedFolder() +{ + return mFileWidget->filePath(); +} + +QStringList QgsStacDownloadAssetsDialog::selectedUrls() +{ + QStringList urls; + for ( int i = 0; i < mTreeWidget->topLevelItemCount(); ++i ) + { + QTreeWidgetItem *item = mTreeWidget->topLevelItem( i ); + if ( item->checkState( 0 ) == Qt::Checked ) + urls.append( item->text( 5 ) ); + } + return urls; +} + +void QgsStacDownloadAssetsDialog::selectAll() +{ + for ( int i = 0; i < mTreeWidget->topLevelItemCount(); ++i ) + { + QTreeWidgetItem *item = mTreeWidget->topLevelItem( i ); + item->setCheckState( 0, Qt::Checked ); + } +} + +void QgsStacDownloadAssetsDialog::deselectAll() +{ + for ( int i = 0; i < mTreeWidget->topLevelItemCount(); ++i ) + { + QTreeWidgetItem *item = mTreeWidget->topLevelItem( i ); + item->setCheckState( 0, Qt::Unchecked ); + } +} + +///@endcond diff --git a/src/gui/stac/qgsstacdownloadassetsdialog.h b/src/gui/stac/qgsstacdownloadassetsdialog.h new file mode 100644 index 000000000000..f6a69c53bfeb --- /dev/null +++ b/src/gui/stac/qgsstacdownloadassetsdialog.h @@ -0,0 +1,47 @@ +/*************************************************************************** + qgsstacdownloadassetsdialog.h + --------------------- + begin : September 2024 + copyright : (C) 2024 by Stefanos Natsis + email : uclaros at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSSTACDOWNLOADASSETSDIALOG_H +#define QGSSTACDOWNLOADASSETSDIALOG_H + +///@cond PRIVATE +#define SIP_NO_FILE + +#include "qgsstacitem.h" +#include "ui_qgsstacdownloadassetsdialog.h" + +#include + + +class QgsStacDownloadAssetsDialog : public QDialog, private Ui::QgsStacDownloadAssetsDialog +{ + Q_OBJECT + + public: + explicit QgsStacDownloadAssetsDialog( QWidget *parent = nullptr ); + + void setStacItem( QgsStacItem *stacItem ); + QString selectedFolder(); + QStringList selectedUrls(); + + private: + void selectAll(); + void deselectAll(); + +}; + +///@endcond + +#endif // QGSSTACDOWNLOADASSETSDIALOG_H diff --git a/src/gui/stac/qgsstacobjectdetailsdialog.cpp b/src/gui/stac/qgsstacobjectdetailsdialog.cpp new file mode 100644 index 000000000000..b38a075d1c5a --- /dev/null +++ b/src/gui/stac/qgsstacobjectdetailsdialog.cpp @@ -0,0 +1,58 @@ +/*************************************************************************** + qgsstacobjectdetailsdialog.cpp + --------------------- + begin : September 2024 + copyright : (C) 2024 by Stefanos Natsis + email : uclaros at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsstacobjectdetailsdialog.h" +#include "qgsgui.h" +#include "qgsapplication.h" +#include "qgsstacitem.h" + +///@cond PRIVATE + +QgsStacObjectDetailsDialog::QgsStacObjectDetailsDialog( QWidget *parent ) : + QDialog( parent ) +{ + setupUi( this ); + QgsGui::enableAutoGeometryRestore( this ); +} + +void QgsStacObjectDetailsDialog::setStacObject( QgsStacObject *stacObject ) +{ + if ( ! stacObject ) + return; + + QStringList thumbnails; + if ( QgsStacItem *item = dynamic_cast( stacObject ) ) + { + const QMap assets = item->assets(); + for ( auto it = assets.constBegin(); it != assets.constEnd(); ++it ) + { + if ( it->roles().contains( QLatin1String( "thumbnail" ) ) ) + { + thumbnails.append( QStringLiteral( "
" ).arg( it->href() ) ); + } + } + } + + const QString myStyle = QgsApplication::reportStyleSheet( QgsApplication::StyleSheetType::WebBrowser ); + // inject thumbnails + QString html = stacObject->toHtml().replace( QLatin1String( "" ), QStringLiteral( "\n%1" ).arg( thumbnails.join( QString() ) ) ); + // inject stylesheet + html = html.replace( QLatin1String( "" ), QStringLiteral( R"raw()raw" ) ).arg( myStyle ); + + mWebView->page()->setLinkDelegationPolicy( QWebPage::LinkDelegationPolicy::DelegateAllLinks ); + mWebView->setHtml( html ); +} + +///@endcond diff --git a/src/gui/stac/qgsstacobjectdetailsdialog.h b/src/gui/stac/qgsstacobjectdetailsdialog.h new file mode 100644 index 000000000000..9f831fac594a --- /dev/null +++ b/src/gui/stac/qgsstacobjectdetailsdialog.h @@ -0,0 +1,39 @@ +/*************************************************************************** + qgsstacobjectdetailsdialog.h + --------------------- + begin : September 2024 + copyright : (C) 2024 by Stefanos Natsis + email : uclaros at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSSTACOBJECTDETAILSDIALOG_H +#define QGSSTACOBJECTDETAILSDIALOG_H + +///@cond PRIVATE +#define SIP_NO_FILE + +#include "qgsstacobject.h" +#include "ui_qgsstacobjectdetailsdialog.h" + +#include + +class QgsStacObjectDetailsDialog : public QDialog, private Ui::QgsStacObjectDetailsDialog +{ + Q_OBJECT + + public: + explicit QgsStacObjectDetailsDialog( QWidget *parent = nullptr ); + + void setStacObject( QgsStacObject *stacObject ); +}; + +///@endcond + +#endif // QGSSTACOBJECTDETAILSDIALOG_H diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 7902222682b0..d89d204dc362 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -19,6 +19,7 @@ file(GLOB GEOREFERENCER_UIS "${CMAKE_CURRENT_SOURCE_DIR}/georeferencer/*.ui") file(GLOB ANNOTATION_UIS "${CMAKE_CURRENT_SOURCE_DIR}/annotations/*.ui") file(GLOB SENSOR_UIS "${CMAKE_CURRENT_SOURCE_DIR}/sensor/*.ui") file(GLOB TILED_SCENE_UIS "${CMAKE_CURRENT_SOURCE_DIR}/tiledscene/*.ui") +file(GLOB STAC_UIS "${CMAKE_CURRENT_SOURCE_DIR}/stac/*.ui") if (BUILD_WITH_QT6) QT6_WRAP_UI(QGIS_UIS_H @@ -42,6 +43,7 @@ if (BUILD_WITH_QT6) ${ANNOTATION_UIS} ${SENSOR_UIS} ${TILED_SCENE_UIS} + ${STAC_UIS} ) else() QT5_WRAP_UI(QGIS_UIS_H @@ -65,6 +67,7 @@ else() ${ANNOTATION_UIS} ${SENSOR_UIS} ${TILED_SCENE_UIS} + ${STAC_UIS} ) endif() diff --git a/src/ui/stac/qgsstacconnectiondialog.ui b/src/ui/stac/qgsstacconnectiondialog.ui new file mode 100644 index 000000000000..2838e15fa6af --- /dev/null +++ b/src/ui/stac/qgsstacconnectiondialog.ui @@ -0,0 +1,158 @@ + + + QgsStacConnectionDialog + + + + 0 + 0 + 659 + 442 + + + + STAC Connection + + + + + + Connection Details + + + + + + Referer + + + mEditReferer + + + + + + + Authentication + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + + + + + + Name of the new connection + + + + + + + Optional custom referer + + + + + + + URL + + + + + + + Name + + + + + + + URL of the connection + + + http://example.com/stac/ + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + QgsAuthSettingsWidget + QWidget +
qgsauthsettingswidget.h
+ 1 +
+
+ + mEditName + mEditUrl + mEditReferer + + + + + buttonBox + accepted() + QgsStacConnectionDialog + accept() + + + 224 + 381 + + + 157 + 274 + + + + + buttonBox + rejected() + QgsStacConnectionDialog + reject() + + + 292 + 387 + + + 286 + 274 + + + + +
diff --git a/src/ui/stac/qgsstacdownloadassetsdialog.ui b/src/ui/stac/qgsstacdownloadassetsdialog.ui new file mode 100644 index 000000000000..2f9323a48132 --- /dev/null +++ b/src/ui/stac/qgsstacdownloadassetsdialog.ui @@ -0,0 +1,159 @@ + + + QgsStacDownloadAssetsDialog + + + + 0 + 0 + 400 + 300 + + + + Download STAC assets + + + + + + Download location: + + + + + + + + + + false + + + true + + + 6 + + + true + + + + Id + + + + + Title + + + + + Description + + + + + Roles + + + + + Media Type + + + + + Url + + + + + + + + + + Select All + + + + + + + Deselect All + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + QgsFileWidget + QWidget +
qgsfilewidget.h
+ 1 +
+
+ + + + buttonBox + accepted() + QgsStacDownloadAssetsDialog + accept() + + + 199 + 278 + + + 199 + 149 + + + + + buttonBox + rejected() + QgsStacDownloadAssetsDialog + reject() + + + 199 + 278 + + + 199 + 149 + + + + +
diff --git a/src/ui/stac/qgsstacobjectdetailsdialog.ui b/src/ui/stac/qgsstacobjectdetailsdialog.ui new file mode 100644 index 000000000000..a0b7140b9822 --- /dev/null +++ b/src/ui/stac/qgsstacobjectdetailsdialog.ui @@ -0,0 +1,75 @@ + + + QgsStacObjectDetailsDialog + + + + 0 + 0 + 530 + 554 + + + + STAC Object Details + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + QgsWebView + QWidget +
qgswebview.h
+ 1 +
+
+ + + + buttonBox + accepted() + QgsStacObjectDetailsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + QgsStacObjectDetailsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +