Compare commits

...

12 Commits

Author SHA1 Message Date
Tom Christian
1e7e899d09
feat(#62838): PR feedback 2025-09-29 13:22:06 -07:00
Tom Christian
4409d0726d
feat(#62838): no explicit assignment
Co-authored-by: Matthias Kuhn <matthias@opengis.ch>
2025-09-29 13:22:06 -07:00
Tom Christian
0a5c968df2
feat(#62838): const ref for authcfg
Co-authored-by: Matthias Kuhn <matthias@opengis.ch>
2025-09-29 13:22:06 -07:00
Tom Christian
31b4cccc04
feat(#62838): cleaned up showDetails logic 2025-09-29 13:22:05 -07:00
Tom Christian
18e7b2c1c8
feat(#62838): added missing auth in asset uri call 2025-09-29 13:22:05 -07:00
Tom Christian
71867e5c67
fixed 'since' version comments 2025-09-29 13:22:05 -07:00
Tom Christian
bb3ed1ee31
feat(#62838): support single asset downloads 2025-09-29 13:22:05 -07:00
Tom Christian
29e33c0ae7
feat(#62838): asset context menu 2025-09-29 13:22:05 -07:00
Tom Christian
3aca600332
feat(#62838): list assets under items 2025-09-29 13:22:05 -07:00
Tom Christian
e396939ab4
feat(#62838): updated tests for Zarr as cloud optimized 2025-09-29 13:22:05 -07:00
Tom Christian
88d224f64c
feat(#62838): refactor duplicated logic 2025-09-29 13:22:05 -07:00
Tom Christian
337ec4c116
feat(#62838): recognise Zarr as cloud-optimised 2025-09-29 13:22:05 -07:00
18 changed files with 404 additions and 126 deletions

View File

@ -76,9 +76,32 @@ Returns the format name for cloud optimized formats
QgsMimeDataUtils::Uri uri() const;
%Docstring
Returns a uri for the asset if it is a cloud optimized file like COG or
COPC
COPC, empty auth configuration
.. versionadded:: 3.42
%End
QgsMimeDataUtils::Uri uri( const QString &authcfg ) const;
%Docstring
Returns a uri for the asset if it is a cloud optimized file like COG or
COPC
.. versionadded:: 4.0
%End
QString toHtml( const QString &assetId ) const;
%Docstring
Returns an HTML representation of the STAC Asset including its ID within
its container
.. versionadded:: 4.0
%End
bool isDownloadable() const;
%Docstring
Returns whether the asset can be downloaded
.. versionadded:: 4.0
%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

@ -76,9 +76,32 @@ Returns the format name for cloud optimized formats
QgsMimeDataUtils::Uri uri() const;
%Docstring
Returns a uri for the asset if it is a cloud optimized file like COG or
COPC
COPC, empty auth configuration
.. versionadded:: 3.42
%End
QgsMimeDataUtils::Uri uri( const QString &authcfg ) const;
%Docstring
Returns a uri for the asset if it is a cloud optimized file like COG or
COPC
.. versionadded:: 4.0
%End
QString toHtml( const QString &assetId ) const;
%Docstring
Returns an HTML representation of the STAC Asset including its ID within
its container
.. versionadded:: 4.0
%End
bool isDownloadable() const;
%Docstring
Returns whether the asset can be downloaded
.. versionadded:: 4.0
%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

@ -60,7 +60,8 @@ bool QgsStacAsset::isCloudOptimized() const
const QString format = formatName();
return format == QLatin1String( "COG" ) ||
format == QLatin1String( "COPC" ) ||
format == QLatin1String( "EPT" );
format == QLatin1String( "EPT" ) ||
format == QLatin1String( "Zarr" );
}
QString QgsStacAsset::formatName() const
@ -72,10 +73,18 @@ QString QgsStacAsset::formatName() const
return QStringLiteral( "COPC" );
else if ( mHref.endsWith( QLatin1String( "/ept.json" ) ) )
return QStringLiteral( "EPT" );
else if ( mMediaType == QLatin1String( "application/vnd+zarr" ) )
return QStringLiteral( "Zarr" );
return QString();
}
QgsMimeDataUtils::Uri QgsStacAsset::uri() const
{
return uri( QString() );
}
QgsMimeDataUtils::Uri QgsStacAsset::uri( const QString &authcfg ) const
{
QgsMimeDataUtils::Uri uri;
QUrl url( href() );
@ -87,6 +96,8 @@ QgsMimeDataUtils::Uri QgsStacAsset::uri() const
href().startsWith( QLatin1String( "ftp" ), Qt::CaseInsensitive ) )
{
uri.uri = QStringLiteral( "/vsicurl/%1" ).arg( href() );
if ( !authcfg.isEmpty() )
uri.uri.append( QStringLiteral( " authcfg='%1'" ).arg( authcfg ) );
}
else if ( href().startsWith( QLatin1String( "s3://" ), Qt::CaseInsensitive ) )
{
@ -102,12 +113,37 @@ QgsMimeDataUtils::Uri QgsStacAsset::uri() const
uri.layerType = QStringLiteral( "pointcloud" );
uri.providerKey = QStringLiteral( "copc" );
uri.uri = href();
if ( !authcfg.isEmpty() )
uri.uri.append( QStringLiteral( " authcfg='%1'" ).arg( authcfg ) );
}
else if ( formatName() == QLatin1String( "EPT" ) )
{
uri.layerType = QStringLiteral( "pointcloud" );
uri.providerKey = QStringLiteral( "ept" );
uri.uri = href();
if ( !authcfg.isEmpty() )
uri.uri.append( QStringLiteral( " authcfg='%1'" ).arg( authcfg ) );
}
else if ( formatName() == QLatin1String( "Zarr" ) )
{
uri.layerType = QStringLiteral( "raster" );
uri.providerKey = QStringLiteral( "gdal" );
if ( href().startsWith( QLatin1String( "http" ), Qt::CaseInsensitive ) ||
href().startsWith( QLatin1String( "ftp" ), Qt::CaseInsensitive ) )
{
uri.uri = QStringLiteral( "ZARR:\"/vsicurl/%1\"" ).arg( href() );
if ( !authcfg.isEmpty() )
uri.uri.append( QStringLiteral( " authcfg='%1'" ).arg( authcfg ) );
}
else if ( href().startsWith( QLatin1String( "s3://" ), Qt::CaseInsensitive ) )
{
// Remove the s3:// protocol prefix for compatibility with GDAL's /vsis3
uri.uri = QStringLiteral( "ZARR:\"/vsis3/%1\"" ).arg( href().mid( 5 ) );
}
else
{
uri.uri = href();
}
}
else
{
@ -118,3 +154,34 @@ QgsMimeDataUtils::Uri QgsStacAsset::uri() const
return uri;
}
QString QgsStacAsset::toHtml( const QString &assetId ) const
{
QString html = QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Asset" ) );
html += QStringLiteral( "<table class=\"list-view\">\n" );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "id" ), assetId );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "title" ), title() );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "description" ), description() );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td><a href=\"%2\">%2</a></td></tr>\n" ).arg( QStringLiteral( "url" ), href() );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "type" ), mediaType() );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "roles" ), roles().join( ',' ) );
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

@ -73,11 +73,29 @@ class CORE_EXPORT QgsStacAsset
QString formatName() const;
/**
* Returns a uri for the asset if it is a cloud optimized file like COG or COPC
* Returns a uri for the asset if it is a cloud optimized file like COG or COPC, empty auth configuration
* \since QGIS 3.42
*/
QgsMimeDataUtils::Uri uri() const;
/**
* Returns a uri for the asset if it is a cloud optimized file like COG or COPC
* \since QGIS 4.0
*/
QgsMimeDataUtils::Uri uri( const QString &authcfg ) const;
/**
* Returns an HTML representation of the STAC Asset including its ID within its container
* \since QGIS 4.0
*/
QString toHtml( const QString &assetId ) const;
/**
* Returns whether the asset can be downloaded
* \since QGIS 4.0
*/
bool isDownloadable() const;
private:
QString mHref;
QString mTitle;

View File

@ -27,6 +27,59 @@
constexpr int MAX_DISPLAYED_ITEMS = 20;
//
// QgsStacAssetItem
//
QgsStacAssetItem::QgsStacAssetItem( QgsDataItem *parent, const QString &name, const QgsStacAsset *asset )
: QgsDataItem( Qgis::BrowserItemType::Custom, parent, name, QString( "%1/%2" ).arg( parent->path(), name ), QStringLiteral( "special:Stac" ) ),
mStacAsset( asset ),
mName( name )
{
mIconName = QStringLiteral( "mActionPropertiesWidget.svg" );
updateToolTip();
setState( Qgis::BrowserItemState::Populated );
}
bool QgsStacAssetItem::hasDragEnabled() const
{
return mStacAsset->isCloudOptimized();
}
QgsMimeDataUtils::UriList QgsStacAssetItem::mimeUris() const
{
QgsStacItemItem *itemItem = qobject_cast<QgsStacItemItem *>( parent() );
const QString authcfg = itemItem->stacController()->authCfg();
QgsMimeDataUtils::Uri uri;
QUrl url( mStacAsset->href() );
if ( url.isLocalFile() )
{
uri.uri = mStacAsset->href();
}
else
{
uri = mStacAsset->uri( authcfg );
}
return { uri };
}
bool QgsStacAssetItem::equal( const QgsDataItem * )
{
return false;
}
void QgsStacAssetItem::updateToolTip()
{
QString title = mStacAsset->title();
if ( title.isNull() || title.isEmpty() )
{
title = mName;
}
mToolTip = QStringLiteral( "STAC Asset:\n%1\n%2" ).arg( title, mStacAsset->href() );
}
//
// QgsStacFetchMoreItem
//
@ -53,7 +106,6 @@ bool QgsStacFetchMoreItem::handleDoubleClick()
}
}
//
// QgsStacItemItem
//
@ -74,7 +126,15 @@ QVector<QgsDataItem *> QgsStacItemItem::createChildren()
if ( !mStacItem )
return { new QgsErrorItem( this, error, path() + QStringLiteral( "/error" ) ) };
return {};
QVector<QgsDataItem *> contents;
contents.reserve( mStacItem->assets().size() );
const QMap<QString, QgsStacAsset> assets = mStacItem->assets();
for ( auto it = assets.constBegin(); it != assets.constEnd(); ++it )
{
QgsStacAssetItem *assetItem = new QgsStacAssetItem( this, it.key(), &it.value() );
contents.append( assetItem );
}
return contents;
}
bool QgsStacItemItem::hasDragEnabled() const
@ -109,44 +169,10 @@ QgsMimeDataUtils::UriList QgsStacItemItem::mimeUris() const
{
uri.uri = it->href();
}
else if ( it->mediaType() == QLatin1String( "image/tiff; application=geotiff; profile=cloud-optimized" ) ||
it->mediaType() == QLatin1String( "image/vnd.stac.geotiff; cloud-optimized=true" ) )
else
{
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() );
if ( !authcfg.isEmpty() )
uri.uri.append( QStringLiteral( " authcfg='%1'" ).arg( authcfg ) );
}
else if ( it->href().startsWith( QLatin1String( "s3://" ), Qt::CaseInsensitive ) )
{
uri.uri = QStringLiteral( "/vsis3/%1" ).arg( it->href().mid( 5 ) );
}
else
{
uri.uri = it->href();
}
uri = it->uri( authcfg );
}
else if ( it->mediaType() == QLatin1String( "application/vnd.laszip+copc" ) )
{
uri.layerType = QStringLiteral( "pointcloud" );
uri.providerKey = QStringLiteral( "copc" );
uri.uri = it->href();
if ( !authcfg.isEmpty() )
uri.uri.append( QStringLiteral( " authcfg='%1'" ).arg( authcfg ) );
}
else if ( it->href().endsWith( QLatin1String( "/ept.json" ) ) )
{
uri.layerType = QStringLiteral( "pointcloud" );
uri.providerKey = QStringLiteral( "ept" );
uri.uri = it->href();
if ( !authcfg.isEmpty() )
uri.uri.append( QStringLiteral( " authcfg='%1'" ).arg( authcfg ) );
}
uri.name = it->title().isEmpty() ? url.fileName() : it->title();
uris.append( uri );
}
@ -210,7 +236,6 @@ void QgsStacItemItem::itemRequestFinished( int requestId, QString error )
mIconName = QStringLiteral( "/mIconDelete.svg" );
mName = error;
}
setState( Qgis::BrowserItemState::Populated );
}
@ -498,6 +523,8 @@ QVector< QgsDataItem * > QgsStacCatalogItem::createItems( const QVector<QgsStacI
QgsStacItemItem *i = new QgsStacItemItem( this, name, item->url() );
i->setStacItem( std::move( object ) );
// create any assets beneath the item, so that they can be individually drag-dropped as layers if compatible
i->populate( true );
i->setState( Qgis::BrowserItemState::Populated );
contents.append( i );
}

View File

@ -30,6 +30,29 @@ class QgsStacCollection;
///@cond PRIVATE
#define SIP_NO_FILE
/**
* \brief Item for STAC Asset within a collection or item.
* \since QGIS 4.0
*/
class CORE_EXPORT QgsStacAssetItem : public QgsDataItem
{
Q_OBJECT
public:
QgsStacAssetItem( QgsDataItem *parent, const QString &name, const QgsStacAsset *asset );
bool hasDragEnabled() const override;
QgsMimeDataUtils::UriList mimeUris() const override;
bool equal( const QgsDataItem *other ) override;
QVariant sortKey() const override { return QStringLiteral( "4 %1" ).arg( mName ); }
void updateToolTip();
const QgsStacAsset *stacAsset() { return mStacAsset; }
private:
const QgsStacAsset *mStacAsset;
const QString mName;
};
/**
* \brief Item to display that there are additional STAC items which are not loaded.
* \since QGIS 3.40

View File

@ -38,9 +38,7 @@ Qgis::StacObjectType QgsStacItem::type() const
QString QgsStacItem::toHtml() const
{
QString html = QStringLiteral( "<html><head></head>\n<body>\n" );
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Item" ) );
QString html = QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Item" ) );
html += QLatin1String( "<table class=\"list-view\">\n" );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "id" ), id() );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "stac_version" ), stacVersion() );
@ -95,18 +93,9 @@ QString QgsStacItem::toHtml() const
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Assets" ) );
for ( auto it = mAssets.constBegin(); it != mAssets.constEnd(); ++it )
{
html += QLatin1String( "<table class=\"list-view\">\n" );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "id" ), it.key() );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "title" ), it->title() );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "description" ), it->description() );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td><a href=\"%2\">%2</a></td></tr>\n" ).arg( QStringLiteral( "url" ), it->href() );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "type" ), it->mediaType() );
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "roles" ), it->roles().join( ',' ) );
html += QLatin1String( "</table><br/>\n" );
html += it->toHtml( it.key() );
}
}
html += QLatin1String( "\n</body>\n</html>\n" );
return html;
}

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,15 +89,41 @@ 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 ); } );
menu->addAction( actionDetails );
}
}
if ( QgsStacAssetItem *assetItem = qobject_cast<QgsStacAssetItem *>( item ) )
{
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 ); } );
menu->addAction( actionDetails );
}
}
void QgsStacDataItemGuiProvider::editConnection( QgsDataItem *item )
@ -162,37 +188,56 @@ void QgsStacDataItemGuiProvider::loadConnections( QgsDataItem *item )
void QgsStacDataItemGuiProvider::showDetails( QgsDataItem *item )
{
QgsStacObject *obj = nullptr;
QString authcfg;
if ( QgsStacItemItem *itemItem = qobject_cast<QgsStacItemItem *>( item ) )
QgsStacItemItem *itemItem = qobject_cast<QgsStacItemItem *>( item );
QgsStacCatalogItem *catalogItem = qobject_cast<QgsStacCatalogItem *>( item );
QgsStacAssetItem *assetItem = qobject_cast<QgsStacAssetItem *>( item );
if ( !( itemItem || catalogItem || assetItem ) )
{
obj = itemItem->stacItem();
authcfg = itemItem->stacController()->authCfg();
}
else if ( QgsStacCatalogItem *catalogItem = qobject_cast<QgsStacCatalogItem *>( item ) )
{
obj = catalogItem->stacCatalog();
return;
}
if ( obj )
QgsStacObjectDetailsDialog d;
if ( itemItem )
{
QgsStacObjectDetailsDialog d;
d.setAuthcfg( authcfg );
d.setStacObject( obj );
d.exec();
authcfg = itemItem->stacController()->authCfg();
d.setContentFromStacObject( itemItem->stacItem() );
}
else if ( catalogItem )
{
d.setContentFromStacObject( catalogItem->stacCatalog() );
}
else if ( assetItem )
{
QgsStacItemItem *itemItem = qobject_cast<QgsStacItemItem *>( assetItem->parent() );
authcfg = itemItem->stacController()->authCfg();
d.setContentFromStacAsset( assetItem->name(), assetItem->stacAsset() );
}
d.setAuthcfg( authcfg );
d.exec();
return;
}
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

@ -31,7 +31,8 @@ QgsStacObjectDetailsDialog::QgsStacObjectDetailsDialog( QWidget *parent )
QgsGui::enableAutoGeometryRestore( this );
}
void QgsStacObjectDetailsDialog::setStacObject( QgsStacObject *stacObject )
void QgsStacObjectDetailsDialog::setContentFromStacObject( QgsStacObject *stacObject )
{
if ( !stacObject )
return;
@ -42,28 +43,40 @@ void QgsStacObjectDetailsDialog::setStacObject( QgsStacObject *stacObject )
const QMap<QString, QgsStacAsset> assets = item->assets();
for ( auto it = assets.constBegin(); it != assets.constEnd(); ++it )
{
if ( it->roles().contains( QLatin1String( "thumbnail" ) ) )
if ( isThumbnailAsset( &it.value() ) )
{
QString uri = it->href();
if ( !mAuthcfg.isEmpty() )
{
QStringList connectionItems;
connectionItems << uri;
QgsApplication::authManager()->updateDataSourceUriItems( connectionItems, mAuthcfg );
uri = connectionItems.first();
}
thumbnails.append( QStringLiteral( "<img src=\"%1\" border=1><br>" ).arg( uri ) );
thumbnails.append( thumbnailHtmlContent( &it.value() ) );
}
}
}
const QString myStyle = QgsApplication::reportStyleSheet( QgsApplication::StyleSheetType::WebBrowser );
// inject thumbnails
QString html = stacObject->toHtml().replace( QLatin1String( "<head>" ), QStringLiteral( "<head>\n%1" ).arg( thumbnails.join( QString() ) ) );
// inject stylesheet
html = html.replace( QLatin1String( "<head>" ), QStringLiteral( R"raw(<head><style type="text/css">%1</style>)raw" ) ).arg( myStyle );
QString thumbnailHtml = thumbnails.join( QString() );
QString bodyHtml = stacObject->toHtml();
setContent( bodyHtml, thumbnailHtml );
}
void QgsStacObjectDetailsDialog::setContentFromStacAsset( const QString &assetId, const QgsStacAsset *stacAsset )
{
QString thumbnailHtml;
if ( isThumbnailAsset( stacAsset ) )
{
thumbnailHtml = thumbnailHtmlContent( stacAsset );
}
QString bodyHtml = stacAsset->toHtml( assetId );
setContent( bodyHtml, thumbnailHtml );
}
void QgsStacObjectDetailsDialog::setContent( QString bodyHtml, QString thumbnailHtml )
{
const QString myStyle = QgsApplication::reportStyleSheet( QgsApplication::StyleSheetType::WebBrowser );
QString html = QStringLiteral( "<html>\n<head>\n" );
html += QStringLiteral( "<style type=\"text/css\">%1</style>\n" ).arg( myStyle );
html += QStringLiteral( "%1\n" ).arg( thumbnailHtml );
html += QStringLiteral( "</head>\n<body>\n" );
html += QStringLiteral( "%1\n" ).arg( bodyHtml );
html += QLatin1String( "</body>\n</html>\n" );
mWebView->page()->setLinkDelegationPolicy( QWebPage::LinkDelegationPolicy::DelegateAllLinks );
connect( mWebView, &QgsWebView::linkClicked, this, []( const QUrl &url ) {
QDesktopServices::openUrl( url );
@ -76,4 +89,22 @@ void QgsStacObjectDetailsDialog::setAuthcfg( const QString &authcfg )
mAuthcfg = authcfg;
}
bool QgsStacObjectDetailsDialog::isThumbnailAsset( const QgsStacAsset *stacAsset )
{
return stacAsset->roles().contains( QLatin1String( "thumbnail" ) );
}
QString QgsStacObjectDetailsDialog::thumbnailHtmlContent( const QgsStacAsset *stacAsset )
{
QString uri = stacAsset->href();
if ( !mAuthcfg.isEmpty() )
{
QStringList connectionItems;
connectionItems << uri;
QgsApplication::authManager()->updateDataSourceUriItems( connectionItems, mAuthcfg );
uri = connectionItems.first();
}
return QStringLiteral( "<img src=\"%1\" border=1><br>" ).arg( uri );
}
///@endcond

View File

@ -19,6 +19,7 @@
///@cond PRIVATE
#define SIP_NO_FILE
#include "qgsstacasset.h"
#include "qgsstacobject.h"
#include "ui_qgsstacobjectdetailsdialog.h"
@ -31,12 +32,16 @@ class QgsStacObjectDetailsDialog : public QDialog, private Ui::QgsStacObjectDeta
public:
explicit QgsStacObjectDetailsDialog( QWidget *parent = nullptr );
void setStacObject( QgsStacObject *stacObject );
void setAuthcfg( const QString &authcfg );
void setContentFromStacObject( QgsStacObject *stacObject );
void setContentFromStacAsset( const QString &assetId, const QgsStacAsset *stacAsset );
private:
QString mAuthcfg;
void setContent( QString bodyHtml, QString thumbnailHtml );
bool isThumbnailAsset( const QgsStacAsset *stacAsset );
QString thumbnailHtmlContent( const QgsStacAsset *stacAsset );
};
///@endcond

View File

@ -142,7 +142,7 @@ void QgsStacSourceSelect::showItemDetails( const QModelIndex &index )
{
QgsStacObjectDetailsDialog details( this );
details.setAuthcfg( mStac->authCfg() );
details.setStacObject( index.data( QgsStacItemListModel::Role::StacObject ).value<QgsStacObject *>() );
details.setContentFromStacObject( index.data( QgsStacItemListModel::Role::StacObject ).value<QgsStacObject *>() );
details.exec();
}

View File

@ -172,11 +172,13 @@ void TestQgsStac::testParseLocalItem()
QCOMPARE( item->description(), QStringLiteral( "A sample STAC Item that includes examples of all common metadata" ) );
const QgsMimeDataUtils::UriList uris = item->uris();
QCOMPARE( uris.size(), 2 );
QCOMPARE( uris.first().uri, QStringLiteral( "file://%1%2" ).arg( mDataDir, QStringLiteral( "20201211_223832_CS2_analytic.tif" ) ) );
QCOMPARE( uris.first().name, QStringLiteral( "4-Band Analytic" ) );
QCOMPARE( uris.last().uri, QStringLiteral( "/vsicurl/https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif" ) );
QCOMPARE( uris.last().name, QStringLiteral( "3-Band Visual" ) );
QCOMPARE( uris.size(), 3 );
QCOMPARE( uris.at( 0 ).uri, QStringLiteral( "file://%1%2" ).arg( mDataDir, QStringLiteral( "20201211_223832_CS2_analytic.tif" ) ) );
QCOMPARE( uris.at( 0 ).name, QStringLiteral( "4-Band Analytic" ) );
QCOMPARE( uris.at( 1 ).uri, QStringLiteral( "/vsicurl/https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif" ) );
QCOMPARE( uris.at( 1 ).name, QStringLiteral( "3-Band Visual" ) );
QCOMPARE( uris.at( 2 ).uri, QStringLiteral( "ZARR:\"/vsicurl/https://objectstore.eodc.eu:2222/e05ab01a9d56408d82ac32d69a5aae2a:202505-s02msil2a/22/products/cpm_v256/S2B_MSIL2A_20250522T125039_N0511_R095_T26TML_20250522T133252.zarr\"" ) );
QCOMPARE( uris.at( 2 ).name, QStringLiteral( "Example Zarr Store" ) );
// check that relative links are correctly resolved into absolute links
const QVector<QgsStacLink> links = item->links();
@ -187,11 +189,12 @@ void TestQgsStac::testParseLocalItem()
QCOMPARE( links.at( 2 ).href(), QStringLiteral( "%1collection.json" ).arg( basePath ) );
QCOMPARE( links.at( 3 ).href(), QStringLiteral( "http://remotedata.io/catalog/20201211_223832_CS2/index.html" ) );
QCOMPARE( item->assets().size(), 6 );
QCOMPARE( item->assets().size(), 7 );
QgsStacAsset asset = item->assets().value( QStringLiteral( "analytic" ), QgsStacAsset( {}, {}, {}, {}, {} ) );
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" ) );
@ -205,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( {}, {}, {}, {}, {} ) );
@ -214,6 +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()

View File

@ -120,6 +120,15 @@
"ephemeris": {
"href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH",
"title": "Satellite Ephemeris Metadata"
},
"zarr-store": {
"href": "https://objectstore.eodc.eu:2222/e05ab01a9d56408d82ac32d69a5aae2a:202505-s02msil2a/22/products/cpm_v256/S2B_MSIL2A_20250522T125039_N0511_R095_T26TML_20250522T133252.zarr",
"title": "Example Zarr Store",
"type": "application/vnd+zarr",
"roles": [
"data",
"metadata"
]
}
}
}