Skip to content

Commit 21f693e

Browse files
authored
[Feature] Data Source Manager STAC search initial implementation (qgis#59534)
* Data Source Manager STAC search initial implementation * Review fixes * More stac tests * Silence clang-tidy
1 parent f041077 commit 21f693e

26 files changed

+2342
-17
lines changed

src/app/qgisapp.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -3029,6 +3029,7 @@ void QgisApp::createActions()
30293029
connect( mActionAddPointCloudLayer, &QAction::triggered, this, [=] { dataSourceManager( QStringLiteral( "pointcloud" ) ); } );
30303030
connect( mActionAddGpsLayer, &QAction::triggered, this, [=] { dataSourceManager( QStringLiteral( "gpx" ) ); } );
30313031
connect( mActionAddWcsLayer, &QAction::triggered, this, [=] { dataSourceManager( QStringLiteral( "wcs" ) ); } );
3032+
connect( mActionAddStacLayer, &QAction::triggered, this, [=] { dataSourceManager( QStringLiteral( "stac" ) ); } );
30323033
#ifdef HAVE_SPATIALITE
30333034
connect( mActionAddWfsLayer, &QAction::triggered, this, [=] { dataSourceManager( QStringLiteral( "WFS" ) ); } );
30343035
#endif

src/core/stac/qgsstacasset.cpp

+20
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,23 @@ QStringList QgsStacAsset::roles() const
5252
{
5353
return mRoles;
5454
}
55+
56+
bool QgsStacAsset::isCloudOptimized() const
57+
{
58+
const QString format = formatName();
59+
return format == QLatin1String( "COG" ) ||
60+
format == QLatin1String( "COPC" ) ||
61+
format == QLatin1String( "EPT" );
62+
}
63+
64+
QString QgsStacAsset::formatName() const
65+
{
66+
if ( mMediaType == QLatin1String( "image/tiff; application=geotiff; profile=cloud-optimized" ) ||
67+
mMediaType == QLatin1String( "image/vnd.stac.geotiff; cloud-optimized=true" ) )
68+
return QStringLiteral( "COG" );
69+
else if ( mMediaType == QLatin1String( "application/vnd.laszip+copc" ) )
70+
return QStringLiteral( "COPC" );
71+
else if ( mHref.endsWith( QLatin1String( "/ept.json" ) ) )
72+
return QStringLiteral( "EPT" );
73+
return QString();
74+
}

src/core/stac/qgsstacasset.h

+12
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ class CORE_EXPORT QgsStacAsset
6161
*/
6262
QStringList roles() const;
6363

64+
/**
65+
* Returns whether the asset is in a cloud optimized format like COG or COPC
66+
* \since QGIS 3.42
67+
*/
68+
bool isCloudOptimized() const;
69+
70+
/**
71+
* Returns the format name for cloud optimized formats
72+
* \since QGIS 3.42
73+
*/
74+
QString formatName() const;
75+
6476
private:
6577
QString mHref;
6678
QString mTitle;

src/core/stac/qgsstaccontroller.cpp

+55-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,24 @@ int QgsStacController::fetchItemCollectionAsync( const QUrl &url )
5151
return reply->property( "requestId" ).toInt();
5252
}
5353

54+
int QgsStacController::fetchCollectionsAsync( const QUrl &url )
55+
{
56+
QNetworkReply *reply = fetchAsync( url );
57+
connect( reply, &QNetworkReply::finished, this, &QgsStacController::handleCollectionsReply );
58+
59+
return reply->property( "requestId" ).toInt();
60+
}
61+
62+
void QgsStacController::cancelPendingAsyncRequests()
63+
{
64+
for ( QNetworkReply *reply : std::as_const( mReplies ) )
65+
{
66+
reply->abort();
67+
reply->deleteLater();
68+
}
69+
mReplies.clear();
70+
}
71+
5472
QNetworkReply *QgsStacController::fetchAsync( const QUrl &url )
5573
{
5674
QNetworkRequest req( url );
@@ -102,6 +120,7 @@ void QgsStacController::handleStacObjectReply()
102120
parser.setData( data );
103121
parser.setBaseUrl( reply->url() );
104122

123+
QString error;
105124
QgsStacObject *object = nullptr;
106125
switch ( parser.type() )
107126
{
@@ -116,10 +135,11 @@ void QgsStacController::handleStacObjectReply()
116135
break;
117136
case QgsStacObject::Type::Unknown:
118137
object = nullptr;
138+
error = QStringLiteral( "Parsed STAC data is not a Catalog, Collection or Item" );
119139
break;
120140
}
121141
mFetchedStacObjects.insert( requestId, object );
122-
emit finishedStacObjectRequest( requestId, parser.error() );
142+
emit finishedStacObjectRequest( requestId, error.isEmpty() ? parser.error() : error );
123143
reply->deleteLater();
124144
mReplies.removeOne( reply );
125145
}
@@ -153,6 +173,35 @@ void QgsStacController::handleItemCollectionReply()
153173
mReplies.removeOne( reply );
154174
}
155175

176+
void QgsStacController::handleCollectionsReply()
177+
{
178+
QNetworkReply *reply = qobject_cast<QNetworkReply *>( QObject::sender() );
179+
if ( !reply )
180+
return;
181+
182+
const int requestId = reply->property( "requestId" ).toInt();
183+
QgsDebugMsgLevel( QStringLiteral( "Finished STAC request with id %1" ).arg( requestId ), 2 );
184+
185+
if ( reply->error() != QNetworkReply::NoError )
186+
{
187+
emit finishedCollectionsRequest( requestId, reply->errorString() );
188+
reply->deleteLater();
189+
mReplies.removeOne( reply );
190+
return;
191+
}
192+
193+
const QByteArray data = reply->readAll();
194+
QgsStacParser parser;
195+
parser.setData( data );
196+
parser.setBaseUrl( reply->url() );
197+
198+
QgsStacCollections *cols = parser.collections();
199+
mFetchedCollections.insert( requestId, cols );
200+
emit finishedCollectionsRequest( requestId, parser.error() );
201+
reply->deleteLater();
202+
mReplies.removeOne( reply );
203+
}
204+
156205
QgsStacObject *QgsStacController::takeStacObject( int requestId )
157206
{
158207
return mFetchedStacObjects.take( requestId );
@@ -163,6 +212,11 @@ QgsStacItemCollection *QgsStacController::takeItemCollection( int requestId )
163212
return mFetchedItemCollections.take( requestId );
164213
}
165214

215+
QgsStacCollections *QgsStacController::takeCollections( int requestId )
216+
{
217+
return mFetchedCollections.take( requestId );
218+
}
219+
166220
QgsStacObject *QgsStacController::fetchStacObject( const QUrl &url, QString *error )
167221
{
168222
QgsNetworkReplyContent content = fetchBlocking( url );

src/core/stac/qgsstaccontroller.h

+39
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ class CORE_EXPORT QgsStacController : public QObject
107107
*/
108108
int fetchItemCollectionAsync( const QUrl &url );
109109

110+
/**
111+
* Initiates an asynchronous request for a Collections collection using the \a url
112+
* and returns an associated request id.
113+
* When the request is completed, the finishedCollectionsRequest() signal is fired
114+
* and the collections can be accessed with takeCollections()
115+
* \since QGIS 3.42
116+
*/
117+
int fetchCollectionsAsync( const QUrl &url );
118+
119+
/**
120+
* Cancels all pending async requests
121+
* \since QGIS 3.42
122+
*/
123+
void cancelPendingAsyncRequests();
124+
110125
/**
111126
* Returns the STAC object fetched with the specified \a requestId.
112127
* It should be used after the finishedStacObjectRequest signal is fired to get the fetched STAC object.
@@ -127,6 +142,17 @@ class CORE_EXPORT QgsStacController : public QObject
127142
*/
128143
QgsStacItemCollection *takeItemCollection( int requestId );
129144

145+
/**
146+
* Returns the collections collection fetched with the specified \a requestId
147+
* It should be used after the finishedCollectionsRequest signal is fired to get the fetched STAC collections.
148+
* Returns NULLPTR if the requestId was not found, request was canceled, request failed or parsing the STAC object failed.
149+
* The caller takes ownership of the returned collections
150+
* \see fetchCollectionsAsync
151+
* \see finishedCollectionsRequest
152+
* \since QGIS 3.42
153+
*/
154+
QgsStacCollections *takeCollections( int requestId );
155+
130156
/**
131157
* Returns the authentication config id which will be used during the request.
132158
* \see setAuthCfg()
@@ -161,9 +187,21 @@ class CORE_EXPORT QgsStacController : public QObject
161187
*/
162188
void finishedItemCollectionRequest( int id, QString errorMessage );
163189

190+
/**
191+
* This signal is fired when an async request initiated with fetchCollectionsAsync is finished.
192+
* The parsed STAC collections collection can be retrieved using takeCollections
193+
* \param id The requestId attribute of the finished request
194+
* \param errorMessage Reason the request or parsing of the STAC collections may have failed
195+
* \see fetchCollectionsAsync
196+
* \see takeCollections
197+
* \since QGIS 3.42
198+
*/
199+
void finishedCollectionsRequest( int id, QString errorMessage );
200+
164201
private slots:
165202
void handleStacObjectReply();
166203
void handleItemCollectionReply();
204+
void handleCollectionsReply();
167205

168206
private:
169207
QNetworkReply *fetchAsync( const QUrl &url );
@@ -173,6 +211,7 @@ class CORE_EXPORT QgsStacController : public QObject
173211
QgsHttpHeaders mHeaders;
174212
QMap< int, QgsStacObject *> mFetchedStacObjects;
175213
QMap< int, QgsStacItemCollection *> mFetchedItemCollections;
214+
QMap< int, QgsStacCollections *> mFetchedCollections;
176215
QVector<QNetworkReply *> mReplies;
177216
QString mError;
178217
};

src/core/stac/qgsstacdataitems.cpp

+2-5
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,7 @@ bool QgsStacItemItem::hasDragEnabled() const
8787
const QMap<QString, QgsStacAsset> assets = mStacItem->assets();
8888
for ( auto it = assets.constBegin(); it != assets.constEnd(); ++it )
8989
{
90-
if ( it->mediaType() == QLatin1String( "image/tiff; application=geotiff; profile=cloud-optimized" ) ||
91-
it->mediaType() == QLatin1String( "image/vnd.stac.geotiff; cloud-optimized=true" ) ||
92-
it->mediaType() == QLatin1String( "application/vnd.laszip+copc" ) ||
93-
it->href().endsWith( QLatin1String( "/ept.json" ) ) )
90+
if ( it->isCloudOptimized() )
9491
return true;
9592
}
9693
return false;
@@ -147,7 +144,7 @@ QgsMimeDataUtils::UriList QgsStacItemItem::mimeUris() const
147144
uris.append( uri );
148145
}
149146

150-
return { uris };
147+
return uris;
151148
}
152149

153150
bool QgsStacItemItem::equal( const QgsDataItem * )

src/core/stac/qgsstacitem.cpp

+69-7
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ QString QgsStacItem::toHtml() const
4040
{
4141
QString html = QStringLiteral( "<html><head></head>\n<body>\n" );
4242

43-
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Item") );
43+
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Item" ) );
4444
html += QLatin1String( "<table class=\"list-view\">\n" );
4545
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "id" ), id() );
4646
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "stac_version" ), stacVersion() );
@@ -49,7 +49,7 @@ QString QgsStacItem::toHtml() const
4949

5050
if ( !mStacExtensions.isEmpty() )
5151
{
52-
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Extensions") );
52+
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Extensions" ) );
5353
html += QLatin1String( "<ul>\n" );
5454
for ( const QString &extension : mStacExtensions )
5555
{
@@ -58,19 +58,19 @@ QString QgsStacItem::toHtml() const
5858
html += QLatin1String( "</ul>\n" );
5959
}
6060

61-
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Geometry") );
61+
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Geometry" ) );
6262
html += QLatin1String( "<table class=\"list-view\">\n" );
6363
html += QStringLiteral( "<tr><td class=\"highlight\">%1</td><td>%2</td></tr>\n" ).arg( QStringLiteral( "WKT" ), mGeometry.asWkt() );
6464
html += QLatin1String( "</table>\n" );
6565

66-
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Bounding Box") );
66+
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Bounding Box" ) );
6767
html += QLatin1String( "<ul>\n" );
6868
html += QStringLiteral( "<li>%1</li>\n" ).arg( mBbox.is2d() ? mBbox.toRectangle().toString() : mBbox.toString() );
6969
html += QLatin1String( "</ul>\n" );
7070

7171
if ( ! mProperties.isEmpty() )
7272
{
73-
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Properties") );
73+
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Properties" ) );
7474
html += QLatin1String( "<table class=\"list-view\">\n" );
7575
for ( auto it = mProperties.constBegin(); it != mProperties.constEnd(); ++it )
7676
{
@@ -79,7 +79,7 @@ QString QgsStacItem::toHtml() const
7979
html += QLatin1String( "</table><br/>\n" );
8080
}
8181

82-
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Links") );
82+
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Links" ) );
8383
for ( const QgsStacLink &link : mLinks )
8484
{
8585
html += QLatin1String( "<table class=\"list-view\">\n" );
@@ -92,7 +92,7 @@ QString QgsStacItem::toHtml() const
9292

9393
if ( ! mAssets.isEmpty() )
9494
{
95-
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Assets") );
95+
html += QStringLiteral( "<h1>%1</h1>\n<hr>\n" ).arg( QLatin1String( "Assets" ) );
9696
for ( auto it = mAssets.constBegin(); it != mAssets.constEnd(); ++it )
9797
{
9898
html += QLatin1String( "<table class=\"list-view\">\n" );
@@ -177,3 +177,65 @@ QgsDateTimeRange QgsStacItem::dateTimeRange() const
177177
const QDateTime end = QDateTime::fromString( mProperties.value( QStringLiteral( "end_datetime" ), QStringLiteral( "null" ) ).toString() );
178178
return QgsDateTimeRange( start, end );
179179
}
180+
181+
QString QgsStacItem::title() const
182+
{
183+
return mProperties.value( QStringLiteral( "title" ) ).toString();
184+
}
185+
186+
QString QgsStacItem::description() const
187+
{
188+
return mProperties.value( QStringLiteral( "description" ) ).toString();
189+
}
190+
191+
QgsMimeDataUtils::UriList QgsStacItem::uris() const
192+
{
193+
QgsMimeDataUtils::UriList uris;
194+
for ( auto it = mAssets.constBegin(); it != mAssets.constEnd(); ++it )
195+
{
196+
QgsMimeDataUtils::Uri uri;
197+
QUrl url( it->href() );
198+
if ( url.isLocalFile() )
199+
{
200+
uri.uri = it->href();
201+
}
202+
else if ( it->formatName() == QLatin1String( "COG" ) )
203+
{
204+
uri.layerType = QStringLiteral( "raster" );
205+
uri.providerKey = QStringLiteral( "gdal" );
206+
if ( it->href().startsWith( QLatin1String( "http" ), Qt::CaseInsensitive ) ||
207+
it->href().startsWith( QLatin1String( "ftp" ), Qt::CaseInsensitive ) )
208+
{
209+
uri.uri = QStringLiteral( "/vsicurl/%1" ).arg( it->href() );
210+
}
211+
else if ( it->href().startsWith( QLatin1String( "s3://" ), Qt::CaseInsensitive ) )
212+
{
213+
uri.uri = QStringLiteral( "/vsis3/%1" ).arg( it->href().mid( 5 ) );
214+
}
215+
else
216+
{
217+
uri.uri = it->href();
218+
}
219+
}
220+
else if ( it->formatName() == QLatin1String( "COPC" ) )
221+
{
222+
uri.layerType = QStringLiteral( "pointcloud" );
223+
uri.providerKey = QStringLiteral( "copc" );
224+
uri.uri = it->href();
225+
}
226+
else if ( it->formatName() == QLatin1String( "EPT" ) )
227+
{
228+
uri.layerType = QStringLiteral( "pointcloud" );
229+
uri.providerKey = QStringLiteral( "ept" );
230+
uri.uri = it->href();
231+
}
232+
233+
// skip assets with incompatible formats
234+
if ( uri.uri.isEmpty() )
235+
continue;
236+
237+
uri.name = it->title().isEmpty() ? url.fileName() : it->title();
238+
uris.append( uri );
239+
}
240+
return uris;
241+
}

src/core/stac/qgsstacitem.h

+19
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "qgsstacasset.h"
2525
#include "qgsgeometry.h"
2626
#include "qgsbox3d.h"
27+
#include "qgsmimedatautils.h"
2728

2829
/**
2930
* \ingroup core
@@ -113,6 +114,24 @@ class CORE_EXPORT QgsStacItem : public QgsStacObject
113114
*/
114115
QgsDateTimeRange dateTimeRange() const;
115116

117+
/**
118+
* Returns an optional human readable title describing the Item.
119+
* \since QGIS 3.42
120+
*/
121+
QString title() const;
122+
123+
/**
124+
* Returns a Detailed multi-line description to fully explain the Item.
125+
* CommonMark 0.29 syntax may be used for rich text representation.
126+
* \since QGIS 3.42
127+
*/
128+
QString description() const;
129+
130+
/**
131+
* Returns a list of uris of all assets that have a cloud optimized format like COG or COPC
132+
* \since QGIS 3.42
133+
*/
134+
QgsMimeDataUtils::UriList uris() const;
116135

117136
private:
118137
QgsGeometry mGeometry;

0 commit comments

Comments
 (0)