QGIS API Documentation 4.3.0-Master (389d690bb77)
Loading...
Searching...
No Matches
qgsstacparser.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsstacparser.cpp
3 ---------------------
4 begin : August 2024
5 copyright : (C) 2024 by Stefanos Natsis
6 email : uclaros at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgsstacparser.h"
17
18#include "qgsjsonutils.h"
19#include "qgslogger.h"
20#include "qgsstaccatalog.h"
21#include "qgsstaccollection.h"
23#include "qgsstacitem.h"
25
26#include <QString>
27
28using namespace Qt::StringLiterals;
29
30void QgsStacParser::setData( const QByteArray &data )
31{
32 mError = QString();
34 try
35 {
36 mData = nlohmann::json::parse( data.data() );
37 if ( mData.contains( "stac_version" ) )
38 {
39 const QString ver( QString::fromStdString( mData.at( "stac_version" ) ) );
40 if ( !isSupportedStacVersion( ver ) )
41 {
42 mError = u"Unsupported STAC version: %1"_s.arg( ver );
43 return;
44 }
45 }
46 }
47 catch ( nlohmann::json::exception &ex )
48 {
49 mError = u"Error parsing JSON"_s;
50 QgsDebugError( u"Error parsing JSON : %1"_s.arg( ex.what() ) );
51 return;
52 }
53
54 try
55 {
56 if ( mData.at( "type" ) == "Catalog" )
57 {
59 }
60 else if ( mData.at( "type" ) == "Collection" )
61 {
63 }
64 else if ( mData.at( "type" ) == "Feature" )
65 {
67 }
68 }
69 catch ( nlohmann::json::exception &ex )
70 {
71 // might still be FeatureCollection or Collections Collection
72 }
73}
74
75void QgsStacParser::setBaseUrl( const QUrl &url )
76{
77 mBaseUrl = url;
78}
79
81{
82 return mType;
83}
84
85QString QgsStacParser::error() const
86{
87 return mError;
88}
89
90std::unique_ptr<QgsStacCatalog> QgsStacParser::catalog()
91{
92 return parseCatalog( mData );
93}
94
95std::unique_ptr<QgsStacCatalog> QgsStacParser::parseCatalog( const nlohmann::json &data )
96{
97 try
98 {
99 const QString ver( QString::fromStdString( data.at( "stac_version" ) ) );
100 if ( !isSupportedStacVersion( ver ) )
101 {
102 mError = u"Unsupported STAC version: %1"_s.arg( ver );
103 return nullptr;
104 }
105
106 const QString id( QString::fromStdString( data.at( "id" ) ) );
107 const QString description( getString( data.at( "description" ) ) );
108
109 QVector< QgsStacLink > links = parseLinks( data.at( "links" ) );
110
111 auto catalog = std::make_unique< QgsStacCatalog >( id, ver, description, links );
112
113 if ( data.contains( "title" ) )
114 catalog->setTitle( getString( data["title"] ) );
115
116 if ( data.contains( "conformsTo" ) )
117 {
118 for ( const auto &conformanceClass : data["conformsTo"] )
119 {
120 if ( conformanceClass.is_string() )
121 catalog->addConformanceClass( QString::fromStdString( conformanceClass ) );
122 }
123 }
124
125 if ( data.contains( "stac_extensions" ) )
126 {
127 QStringList extensions;
128 for ( const auto &extension : data["stac_extensions"] )
129 {
130 if ( extension.is_string() )
131 extensions.append( QString::fromStdString( extension ) );
132 }
133 catalog->setStacExtensions( extensions );
134 }
135
136 return catalog;
137 }
138 catch ( nlohmann::json::exception &ex )
139 {
140 mError = u"Error parsing Collection"_s;
141 QgsDebugError( u"Error parsing Collection: %1"_s.arg( ex.what() ) );
142 return nullptr;
143 }
144}
145
146std::unique_ptr<QgsStacCollection> QgsStacParser::collection()
147{
148 return parseCollection( mData );
149}
150
151std::unique_ptr<QgsStacCollection> QgsStacParser::parseCollection( const nlohmann::json &data )
152{
153 try
154 {
155 const QString ver( QString::fromStdString( data.at( "stac_version" ) ) );
156 if ( !isSupportedStacVersion( ver ) )
157 {
158 mError = u"Unsupported STAC version: %1"_s.arg( ver );
159 return nullptr;
160 }
161
162 const QString id( QString::fromStdString( data.at( "id" ) ) );
163 const QString description( getString( data.at( "description" ) ) );
164 const QString license( getString( data.at( "license" ) ) );
165
166 QgsStacExtent stacExtent;
167 int totalExtents = 0;
168 for ( const auto &e : data.at( "extent" ).at( "spatial" ).at( "bbox" ) )
169 {
170 QgsBox3D extent;
171 if ( e.size() == 4 )
172 {
173 extent = QgsBox3D( e[0].get<double>(), e[1].get<double>(), 0., e[2].get<double>(), e[3].get<double>(), 0. );
174 }
175 else if ( e.size() == 6 )
176 {
177 extent = QgsBox3D( e[0].get<double>(), e[1].get<double>(), e[2].get<double>(), e[3].get<double>(), e[4].get<double>(), e[5].get<double>() );
178 }
179 else
180 {
181 mError = u"Malformed STAC collection spatial extents"_s;
182 QgsDebugError( mError );
183 return nullptr;
184 }
185 if ( ++totalExtents == 1 )
186 stacExtent.setSpatialExtent( extent );
187 else
188 stacExtent.addDetailedSpatialExtent( extent );
189 }
190
191 totalExtents = 0;
192 for ( const auto &e : data.at( "extent" ).at( "temporal" ).at( "interval" ) )
193 {
194 if ( !e.is_array() || e.size() != 2 )
195 {
196 mError = u"Malformed STAC collection temporal extents"_s;
197 QgsDebugError( mError );
198 return nullptr;
199 }
200 const QDateTime start = QDateTime::fromString( getString( e[0] ), Qt::ISODateWithMs );
201 const QDateTime end = QDateTime::fromString( getString( e[1] ), Qt::ISODateWithMs );
202
203 if ( ++totalExtents == 1 )
204 stacExtent.setTemporalExtent( QgsDateTimeRange( start, end ) );
205 else
206 stacExtent.addDetailedTemporalExtent( QgsDateTimeRange( start, end ) );
207 }
208
209 QVector< QgsStacLink > links = parseLinks( data.at( "links" ) );
210
211 auto collection = std::make_unique< QgsStacCollection >( id, ver, description, links, license, stacExtent );
212
213 if ( data.contains( "title" ) )
214 collection->setTitle( getString( data["title"] ) );
215
216 if ( data.contains( "stac_extensions" ) )
217 {
218 QStringList extensions;
219 for ( const auto &extension : data["stac_extensions"] )
220 {
221 if ( extension.is_string() )
222 extensions.append( QString::fromStdString( extension ) );
223 }
224 collection->setStacExtensions( extensions );
225 }
226
227 if ( data.contains( "keywords" ) )
228 {
229 QStringList keywords;
230 for ( const auto &kw : data["keywords"] )
231 {
232 if ( kw.is_string() )
233 keywords.append( QString::fromStdString( kw ) );
234 }
235 collection->setKeywords( keywords );
236 }
237
238 if ( data.contains( "providers" ) )
239 {
240 QVector< QgsStacProvider > providers;
241 for ( const auto &p : data["providers"] )
242 {
243 if ( !p.contains( "name" ) || ( p.contains( "roles" ) && !p["roles"].is_array() ) )
244 {
245 QgsDebugError( u"Malformed STAC provider object"_s );
246 continue;
247 }
248
249 QStringList roles;
250 if ( p.contains( "roles" ) )
251 {
252 for ( const auto &role : p["roles"] )
253 {
254 if ( role.is_string() )
255 roles.append( QString::fromStdString( role ) );
256 }
257 }
258 const QgsStacProvider
259 provider( QString::fromStdString( p["name"] ), p.contains( "description" ) ? getString( p["description"] ) : QString(), roles, p.contains( "url" ) ? getString( p["url"] ) : QString() );
260
261
262 providers.append( provider );
263 }
264 collection->setProviders( providers );
265 }
266
267 if ( data.contains( "summaries" ) )
268 {
269 const QVariant summ = QgsJsonUtils::jsonToVariant( data["summaries"] );
270 collection->setSummaries( summ.toMap() );
271 }
272
273 if ( data.contains( "assets" ) )
274 {
275 QMap< QString, QgsStacAsset > assets = parseAssets( data["assets"] );
276 collection->setAssets( assets );
277 }
278
279 return collection;
280 }
281 catch ( nlohmann::json::exception &ex )
282 {
283 mError = u"Error parsing Collection"_s;
284 QgsDebugError( u"Error parsing Collection: %1"_s.arg( ex.what() ) );
285 return nullptr;
286 }
287}
288
289std::unique_ptr<QgsStacItem> QgsStacParser::item()
290{
291 return parseItem( mData );
292}
293
294std::unique_ptr<QgsStacItem> QgsStacParser::parseItem( const nlohmann::json &data )
295{
296 try
297 {
298 const QString ver = QString::fromStdString( data.at( "stac_version" ) );
299 if ( !isSupportedStacVersion( ver ) )
300 {
301 mError = u"Unsupported STAC version: %1"_s.arg( ver );
302 return nullptr;
303 }
304
305 const QString id = QString::fromStdString( data.at( "id" ) );
306 const QgsGeometry geom = QgsJsonUtils::geometryFromGeoJson( data.at( "geometry" ) );
307
308 QgsBox3D bbox;
309 if ( !geom.isNull() )
310 {
311 auto b = data.at( "bbox" );
312 if ( b.size() == 4 )
313 {
314 bbox.setXMinimum( b[0] );
315 bbox.setYMinimum( b[1] );
316 bbox.setXMaximum( b[2] );
317 bbox.setYMaximum( b[3] );
318 }
319 else if ( b.size() == 6 )
320 {
321 bbox.setXMinimum( b[0] );
322 bbox.setYMinimum( b[1] );
323 bbox.setZMinimum( b[2] );
324 bbox.setXMaximum( b[3] );
325 bbox.setYMaximum( b[4] );
326 bbox.setZMaximum( b[5] );
327 }
328 }
329
330 auto datetime = data.at( "properties" ).at( "datetime" );
331 if ( !datetime.is_string() || !QDateTime::fromString( QString::fromStdString( datetime ), Qt::ISODate ).isValid() )
332 {
333 auto s = data.at( "properties" ).at( "start_datetime" );
334 QString ss = QString::fromStdString( s );
335 const QDateTime start = QDateTime::fromString( ss, Qt::ISODate );
336
337 auto e = data.at( "properties" ).at( "end_datetime" );
338 const QString ee = QString::fromStdString( e );
339 const QDateTime end = QDateTime::fromString( ee, Qt::ISODate );
340 if ( start.isNull() || end.isNull() )
341 {
342 // invalid datetime
343 mError = u"Invalid STAC item temporal range"_s;
344 return nullptr;
345 }
346 }
347 const QVariantMap properties = QgsJsonUtils::jsonToVariant( data.at( "properties" ) ).toMap();
348
349 QVector< QgsStacLink > links = parseLinks( data.at( "links" ) );
350
351 QMap< QString, QgsStacAsset > assets = parseAssets( data.at( "assets" ) );
352
353 auto item = std::make_unique< QgsStacItem >( id, ver, geom, properties, links, assets, bbox );
354
355 if ( data.contains( "stac_extensions" ) )
356 {
357 QStringList extensions;
358 for ( const auto &extension : data["stac_extensions"] )
359 {
360 if ( extension.is_string() )
361 extensions.append( QString::fromStdString( extension ) );
362 }
363 item->setStacExtensions( extensions );
364 }
365
366 if ( data.contains( "collection" ) )
367 item->setCollection( getString( data["collection"] ) );
368
369 return item;
370 }
371 catch ( nlohmann::json::exception &ex )
372 {
373 mError = u"Error parsing Item"_s;
374 QgsDebugError( u"Error parsing Item: %1"_s.arg( ex.what() ) );
375 return nullptr;
376 }
377}
378
379QVector<QgsStacLink> QgsStacParser::parseLinks( const json &data )
380{
381 QVector< QgsStacLink > links;
382 links.reserve( static_cast<int>( data.size() ) );
383 for ( const auto &link : data )
384 {
385 QUrl linkUrl( QString::fromStdString( link.at( "href" ) ) );
386 if ( linkUrl.isRelative() )
387 linkUrl = mBaseUrl.resolved( linkUrl );
388
389 const QgsStacLink
390 l( linkUrl.toString(),
391 QString::fromStdString( link.at( "rel" ) ),
392 link.contains( "type" ) ? getString( link["type"] ) : QString(),
393 link.contains( "title" ) ? getString( link["title"] ) : QString() );
394 links.append( l );
395 }
396 return links;
397}
398
399QMap<QString, QgsStacAsset> QgsStacParser::parseAssets( const json &data )
400{
401 QMap< QString, QgsStacAsset > assets;
402 for ( const auto &asset : data.items() )
403 {
404 const json value = asset.value();
405 QUrl assetUrl( QString::fromStdString( value.at( "href" ) ) );
406 if ( assetUrl.isRelative() )
407 assetUrl = mBaseUrl.resolved( assetUrl );
408
409 const QgsStacAsset
410 a( assetUrl.toString(),
411 value.contains( "title" ) ? getString( value["title"] ) : QString(),
412 value.contains( "description" ) ? getString( value["description"] ) : QString(),
413 value.contains( "type" ) ? getString( value["type"] ) : QString(),
414 value.contains( "roles" ) ? QgsJsonUtils::jsonToVariant( value["roles"] ).toStringList() : QStringList() );
415 assets.insert( QString::fromStdString( asset.key() ), a );
416 }
417 return assets;
418}
419
420bool QgsStacParser::isSupportedStacVersion( const QString &version )
421{
422 const int maxMajor = 1;
423 const int minMajor = 1;
424 const int minMinor = 0;
425 const int minRelease = 0;
426
427 const thread_local QRegularExpression r( u"^(\\d+).(\\d+).(\\d+)(-.*)*"_s );
428 const QRegularExpressionMatch match = r.match( version );
429
430 const QStringList m = match.capturedTexts();
431
432 // m[0] is the whole matched string, so version 1.0.0 has a length of 4, 1.0.0-rc1 has a length of 5
433 if ( m.length() < 4 )
434 return false;
435
436 const int major = m[1].toInt();
437 const int minor = m[2].toInt();
438 const int release = m[3].toInt();
439
440 if ( major > maxMajor
441 || major < minMajor
442 || ( major == minMajor && minor < minMinor )
443 || ( major == minMajor && minor == minMinor && release < minRelease )
444 || ( major == minMajor && minor == minMinor && release == minRelease && m.length() == 5 ) )
445 {
446 return false;
447 }
448 return true;
449}
450
451QString QgsStacParser::getString( const nlohmann::json &data )
452{
453 return data.is_null() ? QString() : QString::fromStdString( data );
454}
455
456std::unique_ptr<QgsStacItemCollection> QgsStacParser::itemCollection()
457{
458 std::vector< std::unique_ptr<QgsStacItem> > items;
459 QVector< QgsStacLink > links;
460 int numberMatched = -1;
461 try
462 {
463 links = parseLinks( mData.at( "links" ) );
464
465 items.reserve( mData.at( "features" ).size() );
466 for ( auto &item : mData.at( "features" ) )
467 {
468 items.emplace_back( parseItem( item ) );
469 }
470
471 if ( mData.contains( "numberMatched" ) )
472 numberMatched = mData["numberMatched"].get<int>();
473 }
474 catch ( nlohmann::json::exception &ex )
475 {
476 mError = u"Error parsing ItemCollection"_s;
477 QgsDebugError( u"Error parsing ItemCollection: %1"_s.arg( ex.what() ) );
478 return nullptr;
479 }
480
481 QVector< QgsStacItem *> rawItems;
482 rawItems.reserve( static_cast<int>( items.size() ) );
483 for ( std::unique_ptr<QgsStacItem> &i : items )
484 {
485 if ( i )
486 rawItems.append( i.release() );
487 }
488
489 return std::make_unique< QgsStacItemCollection >( rawItems, links, numberMatched );
490}
491
493{
494 std::vector< std::unique_ptr<QgsStacCollection> > cols;
495 QVector< QgsStacLink > links;
496 int numberMatched = -1;
497
498 try
499 {
500 // some servers don't include links here, let's be more forgiving
501 if ( mData.contains( "links" ) )
502 links = parseLinks( mData.at( "links" ) );
503
504 cols.reserve( mData.at( "collections" ).size() );
505 for ( auto &col : mData.at( "collections" ) )
506 {
507 cols.emplace_back( parseCollection( col ) );
508 }
509
510 if ( mData.contains( "numberMatched" ) )
511 numberMatched = mData["numberMatched"].get<int>();
512 }
513 catch ( nlohmann::json::exception &ex )
514 {
515 mError = u"Error parsing ItemCollection"_s;
516 QgsDebugError( u"Error parsing ItemCollection: %1"_s.arg( ex.what() ) );
517 return nullptr;
518 }
519
520 QVector< QgsStacCollection * > rawCols;
521 rawCols.reserve( static_cast<int>( cols.size() ) );
522 for ( std::unique_ptr<QgsStacCollection> &c : cols )
523 {
524 if ( c )
525 rawCols.append( c.release() );
526 }
527
528 return new QgsStacCollectionList( rawCols, links, numberMatched );
529}
StacObjectType
Available types of stac objects.
Definition qgis.h:6885
@ Item
STAC item.
Definition qgis.h:6889
@ Unknown
Type is not known.
Definition qgis.h:6886
@ Catalog
STAC catalog.
Definition qgis.h:6887
@ Collection
STAC collection.
Definition qgis.h:6888
void setZMinimum(double z)
Sets the minimum z value.
Definition qgsbox3d.cpp:94
void setYMaximum(double y)
Sets the maximum y value.
Definition qgsbox3d.cpp:89
void setZMaximum(double z)
Sets the maximum z value.
Definition qgsbox3d.cpp:99
void setXMaximum(double x)
Sets the maximum x value.
Definition qgsbox3d.cpp:79
void setYMinimum(double y)
Sets the minimum y value.
Definition qgsbox3d.cpp:84
void setXMinimum(double x)
Sets the minimum x value.
Definition qgsbox3d.cpp:74
static QgsGeometry geometryFromGeoJson(const json &geometry)
Parses a GeoJSON "geometry" value to a QgsGeometry object.
static QVariant jsonToVariant(const json &value)
Converts a JSON value to a QVariant, in case of parsing error an invalid QVariant is returned.
Stores a list of STAC Collections, which is typically used to store the data returned by STAC API /co...
void addDetailedSpatialExtent(QgsBox3D extent)
Adds a more detailed spatial extent.
void addDetailedTemporalExtent(QgsDateTimeRange extent)
Adds a more detailed temporal extent.
void setSpatialExtent(QgsBox3D extent)
Sets the overall spatial extent to extent.
void setTemporalExtent(QgsDateTimeRange extent)
Sets the overall temporal extent to extent.
QString error() const
Returns the last parsing error.
QgsStacCollectionList * collections()
Returns the parsed STAC API Collections If parsing failed, nullptr is returned The caller takes owner...
void setBaseUrl(const QUrl &url)
Sets the base url that will be used to resolve relative links.
std::unique_ptr< QgsStacItem > item()
Returns the parsed STAC Item If parsing failed, nullptr is returned The caller takes ownership of the...
std::unique_ptr< QgsStacItemCollection > itemCollection()
Returns the parsed STAC API Item Collection If parsing failed, nullptr is returned The caller takes o...
std::unique_ptr< QgsStacCatalog > catalog()
Returns the parsed STAC Catalog If parsing failed, nullptr is returned The caller takes ownership of ...
Qgis::StacObjectType type() const
Returns the type of the parsed object.
std::unique_ptr< QgsStacCollection > collection()
Returns the parsed STAC Collection If parsing failed, nullptr is returned The caller takes ownership ...
void setData(const QByteArray &data)
Sets the JSON data to be parsed.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
#define QgsDebugError(str)
Definition qgslogger.h:71
QgsTemporalRange< QDateTime > QgsDateTimeRange
QgsRange which stores a range of date times.
Definition qgsrange.h:705