feat(#62838): support single asset downloads

This commit is contained in:
Tom Christian 2025-09-17 11:06:24 -07:00
parent 8906518d27
commit 48f2d22a0a
No known key found for this signature in database
GPG Key ID: 0CC611E0CD80A370
13 changed files with 104 additions and 59 deletions

View File

@ -87,17 +87,17 @@ Returns a uri for the asset if it is a cloud optimized file like COG or
COPC
.. versionadded:: 3.42
%End
QString toHtml() const;
%Docstring
Returns an HTML representation of the STAC Asset without an ID
%End
QString toHtml( const QString &assetId ) const;
%Docstring
Returns an HTML representation of the STAC Asset including its ID within
its container
%End
bool isDownloadable() const;
%Docstring
Returns whether the asset can be downloaded
%End
};

View File

@ -82,14 +82,12 @@ Sets the item's additional metadata to ``properties``
QMap< QString, QgsStacAsset > assets() const;
%Docstring
Returns a dictionary of asset objects that can be downloaded, each with
a unique key.
Returns a dictionary of asset objects, each with a unique key.
%End
void setAssets( const QMap< QString, QgsStacAsset > &assets );
%Docstring
Sets the ``asset`` objects that can be downloaded, each with a unique
key.
Sets the ``asset`` objects, each with a unique key.
%End
QString collection() const;

View File

@ -87,17 +87,17 @@ Returns a uri for the asset if it is a cloud optimized file like COG or
COPC
.. versionadded:: 3.42
%End
QString toHtml() const;
%Docstring
Returns an HTML representation of the STAC Asset without an ID
%End
QString toHtml( const QString &assetId ) const;
%Docstring
Returns an HTML representation of the STAC Asset including its ID within
its container
%End
bool isDownloadable() const;
%Docstring
Returns whether the asset can be downloaded
%End
};

View File

@ -82,14 +82,12 @@ Sets the item's additional metadata to ``properties``
QMap< QString, QgsStacAsset > assets() const;
%Docstring
Returns a dictionary of asset objects that can be downloaded, each with
a unique key.
Returns a dictionary of asset objects, each with a unique key.
%End
void setAssets( const QMap< QString, QgsStacAsset > &assets );
%Docstring
Sets the ``asset`` objects that can be downloaded, each with a unique
key.
Sets the ``asset`` objects, each with a unique key.
%End
QString collection() const;

View File

@ -154,12 +154,6 @@ QgsMimeDataUtils::Uri QgsStacAsset::uri( QString authcfg ) const
return uri;
}
QString QgsStacAsset::toHtml() const
{
return toHtml( QString() );
}
QString QgsStacAsset::toHtml( const QString &assetId ) const
{
QString html = QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Asset" ) );
@ -173,3 +167,20 @@ QString QgsStacAsset::toHtml( const QString &assetId ) const
html += QStringLiteral( "</table><br/>\n" );
return html;
}
bool QgsStacAsset::isDownloadable() const
{
/*
* Directory-based data types like Zarr should not offer downloads.
* Download attempts might
* - fail with 4xx,
* - succeed but download an HTML directory listing response, or
* - something else that does not meet the user's needs.
*/
if ( formatName() == QLatin1String( "Zarr" ) )
{
return false;
}
return true;
}

View File

@ -84,16 +84,16 @@ class CORE_EXPORT QgsStacAsset
*/
QgsMimeDataUtils::Uri uri( QString authcfg ) const;
/**
* Returns an HTML representation of the STAC Asset without an ID
*/
QString toHtml() const;
/**
* Returns an HTML representation of the STAC Asset including its ID within its container
*/
QString toHtml( const QString &assetId ) const;
/**
* Returns whether the asset can be downloaded
*/
bool isDownloadable() const;
private:
QString mHref;
QString mTitle;

View File

@ -76,10 +76,10 @@ class CORE_EXPORT QgsStacItem : public QgsStacObject
//! Sets the item's additional metadata to \a properties
void setProperties( const QVariantMap &properties );
//! Returns a dictionary of asset objects that can be downloaded, each with a unique key.
//! Returns a dictionary of asset objects, each with a unique key.
QMap< QString, QgsStacAsset > assets() const;
//! Sets the \a asset objects that can be downloaded, each with a unique key.
//! Sets the \a asset objects, each with a unique key.
void setAssets( const QMap< QString, QgsStacAsset > &assets );
//! Returns the id of the STAC Collection this Item references to

View File

@ -89,9 +89,21 @@ void QgsStacDataItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *
{
menu->addSeparator();
QAction *actionDownload = new QAction( tr( "Download Assets…" ), menu );
connect( actionDownload, &QAction::triggered, this, [itemItem, context] { downloadAssets( itemItem, context ); } );
menu->addAction( actionDownload );
int downloadableAssets = 0;
const QMap<QString, QgsStacAsset> assets = itemItem->stacItem()->assets();
for ( auto it = assets.constBegin(); it != assets.constEnd(); ++it )
{
if ( it.value().isDownloadable() )
{
downloadableAssets += 1;
}
}
if ( downloadableAssets > 0 )
{
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 ); } );
@ -101,9 +113,12 @@ void QgsStacDataItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *
if ( QgsStacAssetItem *assetItem = qobject_cast<QgsStacAssetItem *>( item ) )
{
QAction *actionDownload = new QAction( tr( "Download Asset…" ), menu );
connect( actionDownload, &QAction::triggered, this, [assetItem, context] { downloadAssets( assetItem, context ); } );
menu->addAction( actionDownload );
if ( assetItem->stacAsset()->isDownloadable() )
{
QAction *actionDownload = new QAction( tr( "Download Asset…" ), menu );
connect( actionDownload, &QAction::triggered, this, [assetItem, context] { downloadAssets( assetItem, context ); } );
menu->addAction( actionDownload );
}
QAction *actionDetails = new QAction( tr( "Details…" ), menu );
connect( actionDetails, &QAction::triggered, this, [assetItem] { showDetails( assetItem ); } );
@ -199,7 +214,7 @@ void QgsStacDataItemGuiProvider::showDetails( QgsDataItem *item )
QgsStacObjectDetailsDialog d;
QgsStacItemItem *itemItem = qobject_cast<QgsStacItemItem *>( assetItem->parent() );
d.setAuthcfg( itemItem->stacController()->authCfg() );
d.setContentFromStacAsset( assetItem->stacAsset() );
d.setContentFromStacAsset( assetItem->name(), assetItem->stacAsset() );
d.exec();
return;
}
@ -208,12 +223,21 @@ void QgsStacDataItemGuiProvider::showDetails( QgsDataItem *item )
void QgsStacDataItemGuiProvider::downloadAssets( QgsDataItem *item, QgsDataItemGuiContext context )
{
QgsStacItemItem *itemItem = qobject_cast<QgsStacItemItem *>( item );
QgsStacAssetItem *assetItem = qobject_cast<QgsStacAssetItem *>( item );
if ( !itemItem )
if ( !( itemItem || assetItem ) )
return;
QgsStacDownloadAssetsDialog dialog;
dialog.setStacItem( itemItem->stacItem() );
if ( itemItem )
{
dialog.setStacItem( itemItem->stacItem() );
}
else if ( assetItem )
{
itemItem = qobject_cast<QgsStacItemItem *>( assetItem->parent() );
dialog.addStacAsset( assetItem->name(), assetItem->stacAsset() );
}
dialog.setMessageBar( context.messageBar() );
dialog.setAuthCfg( itemItem->stacController()->authCfg() );
dialog.exec();

View File

@ -150,25 +150,34 @@ void QgsStacDownloadAssetsDialog::setStacItem( QgsStacItem *stacItem )
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 );
if ( it.value().isDownloadable() )
{
addStacAsset( it.key(), &it.value() );
}
}
}
void QgsStacDownloadAssetsDialog::addStacAsset( const QString &assetId, const QgsStacAsset *stacAsset )
{
QTreeWidgetItem *item = new QTreeWidgetItem();
item->setText( 0, assetId );
item->setToolTip( 0, assetId );
item->setCheckState( 0, Qt::Checked );
item->setText( 1, stacAsset->title() );
item->setToolTip( 1, stacAsset->title() );
item->setText( 2, stacAsset->description() );
item->setToolTip( 2, stacAsset->description() );
item->setText( 3, stacAsset->roles().join( "," ) );
item->setToolTip( 3, stacAsset->roles().join( "," ) );
item->setText( 4, stacAsset->mediaType() );
item->setToolTip( 4, stacAsset->mediaType() );
item->setText( 5, stacAsset->href() );
item->setToolTip( 5, stacAsset->href() );
mTreeWidget->addTopLevelItem( item );
}
QString QgsStacDownloadAssetsDialog::selectedFolder()
{
return mFileWidget->filePath();

View File

@ -38,6 +38,7 @@ class QgsStacDownloadAssetsDialog : public QDialog, private Ui::QgsStacDownloadA
void setAuthCfg( const QString &authCfg );
void setMessageBar( QgsMessageBar *bar );
void setStacItem( QgsStacItem *stacItem );
void addStacAsset( const QString &assetId, const QgsStacAsset *stacAsset );
QString selectedFolder();
QStringList selectedUrls();

View File

@ -56,14 +56,14 @@ void QgsStacObjectDetailsDialog::setContentFromStacObject( QgsStacObject *stacOb
}
void QgsStacObjectDetailsDialog::setContentFromStacAsset( const QgsStacAsset *stacAsset )
void QgsStacObjectDetailsDialog::setContentFromStacAsset( const QString &assetId, const QgsStacAsset *stacAsset )
{
QString thumbnailHtml = QString( "" );
if ( isThumbnailAsset( stacAsset ) )
{
thumbnailHtml = thumbnailHtmlContent( stacAsset );
}
QString bodyHtml = stacAsset->toHtml();
QString bodyHtml = stacAsset->toHtml( assetId );
setContent( bodyHtml, thumbnailHtml );
}

View File

@ -35,7 +35,7 @@ class QgsStacObjectDetailsDialog : public QDialog, private Ui::QgsStacObjectDeta
void setAuthcfg( const QString &authcfg );
void setContentFromStacObject( QgsStacObject *stacObject );
void setContentFromStacAsset( const QgsStacAsset *stacAsset );
void setContentFromStacAsset( const QString &assetId, const QgsStacAsset *stacAsset );
private:
QString mAuthcfg;

View File

@ -194,6 +194,7 @@ void TestQgsStac::testParseLocalItem()
QCOMPARE( asset.href(), basePath + QStringLiteral( "20201211_223832_CS2_analytic.tif" ) );
QVERIFY( asset.isCloudOptimized() );
QCOMPARE( asset.formatName(), QStringLiteral( "COG" ) );
QVERIFY( asset.isDownloadable() );
QgsMimeDataUtils::Uri uri = asset.uri();
QCOMPARE( uri.uri, basePath + QStringLiteral( "20201211_223832_CS2_analytic.tif" ) );
@ -207,6 +208,7 @@ void TestQgsStac::testParseLocalItem()
QVERIFY( !uri.isValid() );
QVERIFY( uri.uri.isEmpty() );
QVERIFY( uri.name.isEmpty() );
QVERIFY( asset.isDownloadable() );
// normal geotiff is not cloud optimized
asset = item->assets().value( QStringLiteral( "udm" ), QgsStacAsset( {}, {}, {}, {}, {} ) );
@ -216,12 +218,14 @@ void TestQgsStac::testParseLocalItem()
QVERIFY( !uri.isValid() );
QVERIFY( uri.uri.isEmpty() );
QVERIFY( uri.name.isEmpty() );
QVERIFY( asset.isDownloadable() );
// Zarr recognised as cloud optimized
asset = item->assets().value( QStringLiteral( "zarr-store" ), QgsStacAsset( {}, {}, {}, {}, {} ) );
QVERIFY( asset.isCloudOptimized() );
QCOMPARE( asset.formatName(), QStringLiteral( "Zarr" ) );
QCOMPARE( asset.uri().layerType, QStringLiteral( "raster" ) );
QVERIFY( !asset.isDownloadable() );
}
void TestQgsStac::testParseLocalItemCollection()