Skip to content

Commit

Permalink
Fix handling of relative STAC links (#59199)
Browse files Browse the repository at this point in the history
  • Loading branch information
uclaros committed Feb 7, 2025
1 parent 7758257 commit 4e8f781
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 15 deletions.
7 changes: 7 additions & 0 deletions src/core/stac/qgsstaccontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ void QgsStacController::handleStacObjectReply()
const QByteArray data = reply->readAll();
QgsStacParser parser;
parser.setData( data );
parser.setBaseUrl( reply->url() );

QgsStacObject *object = nullptr;
switch ( parser.type() )
Expand Down Expand Up @@ -143,6 +144,7 @@ void QgsStacController::handleItemCollectionReply()
const QByteArray data = reply->readAll();
QgsStacParser parser;
parser.setData( data );
parser.setBaseUrl( reply->url() );

QgsStacItemCollection *fc = parser.itemCollection();
mFetchedItemCollections.insert( requestId, fc );
Expand Down Expand Up @@ -177,6 +179,7 @@ QgsStacObject *QgsStacController::fetchStacObject( const QUrl &url, QString *err

QgsStacParser parser;
parser.setData( data );
parser.setBaseUrl( url );
QgsStacObject *object = nullptr;
switch ( parser.type() )
{
Expand Down Expand Up @@ -216,6 +219,7 @@ QgsStacItemCollection *QgsStacController::fetchItemCollection( const QUrl &url,

QgsStacParser parser;
parser.setData( data );
parser.setBaseUrl( url );
QgsStacItemCollection *ic = parser.itemCollection();

if ( error )
Expand Down Expand Up @@ -288,6 +292,7 @@ QgsStacCatalog *QgsStacController::openLocalCatalog( const QString &fileName ) c

QgsStacParser parser;
parser.setData( file.readAll() );
parser.setBaseUrl( fileName );
return parser.catalog();
}

Expand All @@ -304,6 +309,7 @@ QgsStacCollection *QgsStacController::openLocalCollection( const QString &fileNa

QgsStacParser parser;
parser.setData( file.readAll() );
parser.setBaseUrl( fileName );
return parser.collection();
}

Expand All @@ -319,6 +325,7 @@ QgsStacItem *QgsStacController::openLocalItem( const QString &fileName ) const

QgsStacParser parser;
parser.setData( file.readAll() );
parser.setBaseUrl( fileName );
return parser.item();
}

Expand Down
17 changes: 15 additions & 2 deletions src/core/stac/qgsstacparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ void QgsStacParser::setData( const QByteArray &data )
}
}

void QgsStacParser::setBaseUrl( const QUrl &url )
{
mBaseUrl = url;
}

QgsStacObject::Type QgsStacParser::type() const
{
return mType;
Expand Down Expand Up @@ -390,7 +395,11 @@ QVector<QgsStacLink> QgsStacParser::parseLinks( const json &data )
links.reserve( static_cast<int>( data.size() ) );
for ( const auto &link : data )
{
const QgsStacLink l( QString::fromStdString( link.at( "href" ) ),
QUrl linkUrl( QString::fromStdString( link.at( "href" ) ) );
if ( linkUrl.isRelative() )
linkUrl = mBaseUrl.resolved( linkUrl );

const QgsStacLink l( linkUrl.toString(),
QString::fromStdString( link.at( "rel" ) ),
link.contains( "type" ) ? getString( link["type"] ) : QString(),
link.contains( "title" ) ? getString( link["title"] ) : QString() );
Expand All @@ -405,7 +414,11 @@ QMap<QString, QgsStacAsset> QgsStacParser::parseAssets( const json &data )
for ( const auto &asset : data.items() )
{
const json value = asset.value();
const QgsStacAsset a( QString::fromStdString( value.at( "href" ) ),
QUrl assetUrl( QString::fromStdString( value.at( "href" ) ) );
if ( assetUrl.isRelative() )
assetUrl = mBaseUrl.resolved( assetUrl );

const QgsStacAsset a( assetUrl.toString(),
value.contains( "title" ) ? getString( value["title"] ) : QString(),
value.contains( "description" ) ? getString( value["description"] ) : QString(),
value.contains( "type" ) ? getString( value["type"] ) : QString(),
Expand Down
13 changes: 10 additions & 3 deletions src/core/stac/qgsstacparser.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#define SIP_NO_FILE

#include <nlohmann/json.hpp>
#include <QUrl>

#include "qgsstacobject.h"
#include "qgsstacasset.h"
Expand Down Expand Up @@ -47,6 +48,12 @@ class QgsStacParser
//! Sets the JSON \data to be parsed
void setData( const QByteArray &data );

/**
* Sets the base \a url that will be used to resolve relative links.
* If not called, relative links will not be resolved to absolute links.
*/
void setBaseUrl( const QUrl &url );

/**
* Returns the parsed STAC Catalog
* If parsing failed, NULLPTR is returned
Expand Down Expand Up @@ -93,8 +100,8 @@ class QgsStacParser
QgsStacCatalog *parseCatalog( const nlohmann::json &data );
QgsStacCollection *parseCollection( const nlohmann::json &data );

static QVector< QgsStacLink > parseLinks( const nlohmann::json &data );
static QMap< QString, QgsStacAsset > parseAssets( const nlohmann::json &data );
QVector< QgsStacLink > parseLinks( const nlohmann::json &data );
QMap< QString, QgsStacAsset > parseAssets( const nlohmann::json &data );
static bool isSupportedStacVersion( const QString &version );
//! Returns a QString, treating null elements as empty strings
static QString getString( const nlohmann::json &data );
Expand All @@ -103,7 +110,7 @@ class QgsStacParser
QgsStacObject::Type mType = QgsStacObject::Type::Unknown;
std::unique_ptr<QgsStacObject> mObject;
QString mError;

QUrl mBaseUrl;
};


Expand Down
52 changes: 43 additions & 9 deletions tests/src/core/testqgsstac.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ void TestQgsStac::cleanupTestCase()

void TestQgsStac::testParseLocalCatalog()
{
const QString fileName = mDataDir + QStringLiteral( "catalog.json" );
const QUrl url( QStringLiteral( "file://%1%2" ).arg( mDataDir, QStringLiteral( "catalog.json" ) ) );
QgsStacController c;
QgsStacObject *obj = c.fetchStacObject( QStringLiteral( "file://%1" ).arg( fileName ) );
QgsStacObject *obj = c.fetchStacObject( url.toString() );
QVERIFY( obj );
QCOMPARE( obj->type(), QgsStacObject::Type::Catalog );
QgsStacCatalog *cat = dynamic_cast<QgsStacCatalog *>( obj );
Expand All @@ -85,17 +85,27 @@ void TestQgsStac::testParseLocalCatalog()
QCOMPARE( cat->stacVersion(), QLatin1String( "1.0.0" ) );
QCOMPARE( cat->title(), QLatin1String( "Example Catalog" ) );
QCOMPARE( cat->description(), QLatin1String( "This catalog is a simple demonstration of an example catalog that is used to organize a hierarchy of collections and their items." ) );
QCOMPARE( cat->links().size(), 6 );
QVERIFY( cat->stacExtensions().isEmpty() );

// check that relative links are correctly resolved into absolute links
const QVector<QgsStacLink> links = cat->links();
QCOMPARE( links.size(), 6 );
const QString basePath = url.adjusted( QUrl::RemoveFilename ).toString();
QCOMPARE( links.at( 0 ).href(), QStringLiteral( "%1catalog.json" ).arg( basePath ) );
QCOMPARE( links.at( 1 ).href(), QStringLiteral( "%1extensions-collection/collection.json" ).arg( basePath ) );
QCOMPARE( links.at( 2 ).href(), QStringLiteral( "%1collection-only/collection.json" ).arg( basePath ) );
QCOMPARE( links.at( 3 ).href(), QStringLiteral( "%1collection-only/collection-with-schemas.json" ).arg( basePath ) );
QCOMPARE( links.at( 4 ).href(), QStringLiteral( "%1collectionless-item.json" ).arg( basePath ) );
QCOMPARE( links.at( 5 ).href(), QStringLiteral( "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/catalog.json" ) );

delete cat;
}

void TestQgsStac::testParseLocalCollection()
{
const QString fileName = mDataDir + QStringLiteral( "collection.json" );
const QUrl url( QStringLiteral( "file://%1%2" ).arg( mDataDir, QStringLiteral( "collection.json" ) ) );
QgsStacController c;
QgsStacObject *obj = c.fetchStacObject( QStringLiteral( "file://%1" ).arg( fileName ) );
QgsStacObject *obj = c.fetchStacObject( url.toString() );
QVERIFY( obj );
QCOMPARE( obj->type(), QgsStacObject::Type::Collection );
QgsStacCollection *col = dynamic_cast<QgsStacCollection *>( obj );
Expand All @@ -105,7 +115,17 @@ void TestQgsStac::testParseLocalCollection()
QCOMPARE( col->stacVersion(), QLatin1String( "1.0.0" ) );
QCOMPARE( col->title(), QLatin1String( "Simple Example Collection" ) );
QCOMPARE( col->description(), QLatin1String( "A simple collection demonstrating core catalog fields with links to a couple of items" ) );
QCOMPARE( col->links().size(), 5 );

// check that relative links are correctly resolved into absolute links
const QVector<QgsStacLink> links = col->links();
QCOMPARE( links.size(), 5 );
const QString basePath = url.adjusted( QUrl::RemoveFilename ).toString();
QCOMPARE( links.at( 0 ).href(), QStringLiteral( "%1collection.json" ).arg( basePath ) );
QCOMPARE( links.at( 1 ).href(), QStringLiteral( "%1simple-item.json" ).arg( basePath ) );
QCOMPARE( links.at( 2 ).href(), QStringLiteral( "%1core-item.json" ).arg( basePath ) );
QCOMPARE( links.at( 3 ).href(), QStringLiteral( "%1extended-item.json" ).arg( basePath ) );
QCOMPARE( links.at( 4 ).href(), QStringLiteral( "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/collection.json" ) );

QCOMPARE( col->providers().size(), 1 );
QCOMPARE( col->stacExtensions().size(), 3 );
QCOMPARE( col->license(), QLatin1String( "CC-BY-4.0" ) );
Expand Down Expand Up @@ -133,19 +153,33 @@ void TestQgsStac::testParseLocalCollection()

void TestQgsStac::testParseLocalItem()
{
const QString fileName = mDataDir + QStringLiteral( "core-item.json" );
const QUrl url( QStringLiteral( "file://%1%2" ).arg( mDataDir, QStringLiteral( "core-item.json" ) ) );
QgsStacController c;
QgsStacObject *obj = c.fetchStacObject( QStringLiteral( "file://%1" ).arg( fileName ) );
QgsStacObject *obj = c.fetchStacObject( url.toString() );
QVERIFY( obj );
QCOMPARE( obj->type(), QgsStacObject::Type::Item );
QgsStacItem *item = dynamic_cast<QgsStacItem *>( obj );

QVERIFY( item );
QCOMPARE( item->id(), QLatin1String( "20201211_223832_CS2" ) );
QCOMPARE( item->stacVersion(), QLatin1String( "1.0.0" ) );
QCOMPARE( item->links().size(), 4 );
QCOMPARE( item->stacExtensions().size(), 0 );

// check that relative links are correctly resolved into absolute links
const QVector<QgsStacLink> links = item->links();
QCOMPARE( links.size(), 4 );
const QString basePath = url.adjusted( QUrl::RemoveFilename ).toString();
QCOMPARE( links.at( 0 ).href(), QStringLiteral( "%1collection.json" ).arg( basePath ) );
QCOMPARE( links.at( 1 ).href(), QStringLiteral( "%1collection.json" ).arg( basePath ) );
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 );
QgsStacAsset asset = item->assets().value( QStringLiteral( "analytic" ), QgsStacAsset( {}, {}, {}, {}, {} ) );
QCOMPARE( asset.href(), basePath + QStringLiteral( "20201211_223832_CS2_analytic.tif" ) );

asset = item->assets().value( QStringLiteral( "thumbnail" ), QgsStacAsset( {}, {}, {}, {}, {} ) );
QCOMPARE( asset.href(), QStringLiteral( "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg" ) );

delete item;
}
Expand Down
2 changes: 1 addition & 1 deletion tests/testdata/stac/core-item.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
],
"assets": {
"analytic": {
"href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif",
"href": "./20201211_223832_CS2_analytic.tif",
"type": "image/tiff; application=geotiff; profile=cloud-optimized",
"title": "4-Band Analytic",
"roles": [
Expand Down

0 comments on commit 4e8f781

Please sign in to comment.