QGIS API Documentation 3.39.0-Master (47f7b3a4989)
Loading...
Searching...
No Matches
qgssensorthingsshareddata.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgssensorthingsshareddata.h
3 ----------------
4 begin : November 2023
5 copyright : (C) 2013 Nyall Dawson
6 email : nyall dot dawson 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
19#include "qgslogger.h"
20#include "qgsreadwritelocker.h"
24#include "qgsjsonutils.h"
25
26#include <QCryptographicHash>
27#include <QFile>
28#include <nlohmann/json.hpp>
29
31
32QgsSensorThingsSharedData::QgsSensorThingsSharedData( const QString &uri )
33{
34 const QVariantMap uriParts = QgsSensorThingsProviderMetadata().decodeUri( uri );
35
36 mEntityType = qgsEnumKeyToValue( uriParts.value( QStringLiteral( "entity" ) ).toString(), Qgis::SensorThingsEntity::Invalid );
37 const QVariantList expandTo = uriParts.value( QStringLiteral( "expandTo" ) ).toList();
38 QList< Qgis::SensorThingsEntity > expandedEntities;
39 for ( const QVariant &expansionVariant : expandTo )
40 {
41 const QgsSensorThingsExpansionDefinition expansion = expansionVariant.value< QgsSensorThingsExpansionDefinition >();
42 if ( expansion.isValid() )
43 {
44 mExpansions.append( expansion );
45 expandedEntities.append( expansion.childEntity() );
46 }
47
48 mExpandQueryString = QgsSensorThingsUtils::asQueryString( mEntityType, mExpansions );
49 }
50
51 mFields = QgsSensorThingsUtils::fieldsForExpandedEntityType( mEntityType, expandedEntities );
52
53 mGeometryField = QgsSensorThingsUtils::geometryFieldForEntityType( mEntityType );
54 // use initial value of maximum page size as default
55 mMaximumPageSize = uriParts.value( QStringLiteral( "pageSize" ), mMaximumPageSize ).toInt();
56 // will default to 0 if not specified, i.e. no limit
57 mFeatureLimit = uriParts.value( QStringLiteral( "featureLimit" ) ).toInt();
58 mFilterExtent = uriParts.value( QStringLiteral( "bounds" ) ).value< QgsRectangle >();
59 mSubsetString = uriParts.value( QStringLiteral( "sql" ) ).toString();
60
62 {
63 if ( uriParts.contains( QStringLiteral( "geometryType" ) ) )
64 {
65 const QString geometryType = uriParts.value( QStringLiteral( "geometryType" ) ).toString();
66 if ( geometryType.compare( QLatin1String( "point" ), Qt::CaseInsensitive ) == 0 )
67 {
68 mGeometryType = Qgis::WkbType::PointZ;
69 }
70 else if ( geometryType.compare( QLatin1String( "multipoint" ), Qt::CaseInsensitive ) == 0 )
71 {
72 mGeometryType = Qgis::WkbType::MultiPointZ;
73 }
74 else if ( geometryType.compare( QLatin1String( "line" ), Qt::CaseInsensitive ) == 0 )
75 {
76 mGeometryType = Qgis::WkbType::MultiLineStringZ;
77 }
78 else if ( geometryType.compare( QLatin1String( "polygon" ), Qt::CaseInsensitive ) == 0 )
79 {
80 mGeometryType = Qgis::WkbType::MultiPolygonZ;
81 }
82
83 if ( mGeometryType != Qgis::WkbType::NoGeometry )
84 {
85 // geometry is always GeoJSON spec (for now, at least), so CRS will always be WGS84
86 mSourceCRS = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) );
87 }
88 }
89 else
90 {
91 mGeometryType = Qgis::WkbType::NoGeometry;
92 }
93 }
94 else
95 {
96 mGeometryType = Qgis::WkbType::NoGeometry;
97 }
98
99 QgsDataSourceUri dsUri;
100 dsUri.setEncodedUri( uri );
101 mAuthCfg = dsUri.authConfigId();
102 mHeaders = dsUri.httpHeaders();
103
104 mRootUri = uriParts.value( QStringLiteral( "url" ) ).toString();
105}
106
107QUrl QgsSensorThingsSharedData::parseUrl( const QUrl &url, bool *isTestEndpoint )
108{
109 if ( isTestEndpoint )
110 *isTestEndpoint = false;
111
112 QUrl modifiedUrl( url );
113 if ( modifiedUrl.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
114 {
115 if ( isTestEndpoint )
116 *isTestEndpoint = true;
117
118 // Just for testing with local files instead of http:// resources
119 QString modifiedUrlString = modifiedUrl.toString();
120 // Qt5 does URL encoding from some reason (of the FILTER parameter for example)
121 modifiedUrlString = QUrl::fromPercentEncoding( modifiedUrlString.toUtf8() );
122 modifiedUrlString.replace( QLatin1String( "fake_qgis_http_endpoint/" ), QLatin1String( "fake_qgis_http_endpoint_" ) );
123 QgsDebugMsgLevel( QStringLiteral( "Get %1" ).arg( modifiedUrlString ), 2 );
124 modifiedUrlString = modifiedUrlString.mid( QStringLiteral( "http://" ).size() );
125 QString args = modifiedUrlString.indexOf( '?' ) >= 0 ? modifiedUrlString.mid( modifiedUrlString.indexOf( '?' ) ) : QString();
126 if ( modifiedUrlString.size() > 150 )
127 {
128 args = QCryptographicHash::hash( args.toUtf8(), QCryptographicHash::Md5 ).toHex();
129 }
130 else
131 {
132 args.replace( QLatin1String( "?" ), QLatin1String( "_" ) );
133 args.replace( QLatin1String( "&" ), QLatin1String( "_" ) );
134 args.replace( QLatin1String( "$" ), QLatin1String( "_" ) );
135 args.replace( QLatin1String( "<" ), QLatin1String( "_" ) );
136 args.replace( QLatin1String( ">" ), QLatin1String( "_" ) );
137 args.replace( QLatin1String( "'" ), QLatin1String( "_" ) );
138 args.replace( QLatin1String( "\"" ), QLatin1String( "_" ) );
139 args.replace( QLatin1String( " " ), QLatin1String( "_" ) );
140 args.replace( QLatin1String( ":" ), QLatin1String( "_" ) );
141 args.replace( QLatin1String( "/" ), QLatin1String( "_" ) );
142 args.replace( QLatin1String( "\n" ), QLatin1String( "_" ) );
143 }
144#ifdef Q_OS_WIN
145 // Passing "urls" like "http://c:/path" to QUrl 'eats' the : after c,
146 // so we must restore it
147 if ( modifiedUrlString[1] == '/' )
148 {
149 modifiedUrlString = modifiedUrlString[0] + ":/" + modifiedUrlString.mid( 2 );
150 }
151#endif
152 modifiedUrlString = modifiedUrlString.mid( 0, modifiedUrlString.indexOf( '?' ) ) + args;
153 QgsDebugMsgLevel( QStringLiteral( "Get %1 (after laundering)" ).arg( modifiedUrlString ), 2 );
154 modifiedUrl = QUrl::fromLocalFile( modifiedUrlString );
155 if ( !QFile::exists( modifiedUrlString ) )
156 {
157 QgsDebugError( QStringLiteral( "Local test file %1 for URL %2 does not exist!!!" ).arg( modifiedUrlString, url.toString() ) );
158 }
159 }
160
161 return modifiedUrl;
162}
163
164QgsRectangle QgsSensorThingsSharedData::extent() const
165{
166 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
167
168 // Since we can't retrieve the actual layer extent via SensorThings API, we use a pessimistic
169 // global extent until we've retrieved all the features from the layer
170 return hasCachedAllFeatures() ? mFetchedFeatureExtent
171 : ( !mFilterExtent.isNull() ? mFilterExtent : QgsRectangle( -180, -90, 180, 90 ) );
172}
173
174long long QgsSensorThingsSharedData::featureCount( QgsFeedback *feedback ) const
175{
176 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
177 if ( mFeatureCount >= 0 )
178 return mFeatureCount;
179
180 locker.changeMode( QgsReadWriteLocker::Write );
181 mError.clear();
182
183 // MISSING PART -- how to handle feature count when we are expanding features?
184 // This situation is not handled by the SensorThings standard at all, so we'll just have
185 // to return an unknown count whenever expansion is used
186 if ( !mExpansions.isEmpty() )
187 {
188 return static_cast< long long >( Qgis::FeatureCountState::UnknownCount );
189 }
190
191 // return no features, just the total count
192 QString countUri = QStringLiteral( "%1?$top=0&$count=true" ).arg( mEntityBaseUri );
193 const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType );
194 const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, mFilterExtent );
195 QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter, mSubsetString } );
196 if ( !filterString.isEmpty() )
197 filterString = QStringLiteral( "&$filter=" ) + filterString;
198 if ( !filterString.isEmpty() )
199 countUri += filterString;
200
201 const QUrl url = parseUrl( QUrl( countUri ) );
202
203 QNetworkRequest request( url );
204 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsSensorThingsSharedData" ) );
205 mHeaders.updateNetworkRequest( request );
206
207 QgsBlockingNetworkRequest networkRequest;
208 networkRequest.setAuthCfg( mAuthCfg );
209 const QgsBlockingNetworkRequest::ErrorCode error = networkRequest.get( request, false, feedback );
210
211 if ( feedback && feedback->isCanceled() )
212 return mFeatureCount;
213
214 // Handle network errors
216 {
217 QgsDebugError( QStringLiteral( "Network error: %1" ).arg( networkRequest.errorMessage() ) );
218 mError = networkRequest.errorMessage();
219 }
220 else
221 {
222 const QgsNetworkReplyContent content = networkRequest.reply();
223 try
224 {
225 auto rootContent = json::parse( content.content().toStdString() );
226 if ( !rootContent.contains( "@iot.count" ) )
227 {
228 mError = QObject::tr( "No '@iot.count' value in response" );
229 return mFeatureCount;
230 }
231
232 mFeatureCount = rootContent["@iot.count"].get<long long>();
233 if ( mFeatureLimit > 0 && mFeatureCount > mFeatureLimit )
234 mFeatureCount = mFeatureLimit;
235 }
236 catch ( const json::parse_error &ex )
237 {
238 mError = QObject::tr( "Error parsing response: %1" ).arg( ex.what() );
239 }
240 }
241
242 return mFeatureCount;
243}
244
245QString QgsSensorThingsSharedData::subsetString() const
246{
247 return mSubsetString;
248}
249
250bool QgsSensorThingsSharedData::hasCachedAllFeatures() const
251{
252 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
253 return mHasCachedAllFeatures
254 || ( mFeatureCount > 0 && mCachedFeatures.size() == mFeatureCount )
255 || ( mFeatureLimit > 0 && mRetrievedBaseFeatureCount >= mFeatureLimit );
256}
257
258bool QgsSensorThingsSharedData::getFeature( QgsFeatureId id, QgsFeature &f, QgsFeedback *feedback )
259{
260 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
261
262 // If cached, return cached feature
263 QMap<QgsFeatureId, QgsFeature>::const_iterator it = mCachedFeatures.constFind( id );
264 if ( it != mCachedFeatures.constEnd() )
265 {
266 f = it.value();
267 return true;
268 }
269
270 if ( hasCachedAllFeatures() )
271 return false; // all features are cached, and we didn't find a match
272
273 bool featureFetched = false;
274
275 if ( mNextPage.isEmpty() )
276 {
277 locker.changeMode( QgsReadWriteLocker::Write );
278
279 int thisPageSize = mMaximumPageSize;
280 if ( mFeatureLimit > 0 && ( mCachedFeatures.size() + thisPageSize ) > mFeatureLimit )
281 thisPageSize = mFeatureLimit - mCachedFeatures.size();
282
283 mNextPage = QStringLiteral( "%1?$top=%2&$count=false%3" ).arg( mEntityBaseUri ).arg( thisPageSize ).arg( !mExpandQueryString.isEmpty() ? ( QStringLiteral( "&" ) + mExpandQueryString ) : QString() );
284 const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType );
285 const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, mFilterExtent );
286 const QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter, mSubsetString } );
287 if ( !filterString.isEmpty() )
288 mNextPage += QStringLiteral( "&$filter=" ) + filterString;
289 }
290
291 locker.unlock();
292
293 processFeatureRequest( mNextPage, feedback, [id, &f, &featureFetched]( const QgsFeature & feature )
294 {
295 if ( feature.id() == id )
296 {
297 f = feature;
298 featureFetched = true;
299 // don't break here -- store all the features we retrieved in this page first!
300 }
301 }, [&featureFetched, this]
302 {
303 return !featureFetched && !hasCachedAllFeatures();
304 }, [this]
305 {
306 mNextPage.clear();
307 mHasCachedAllFeatures = true;
308 } );
309
310 return featureFetched;
311}
312
313QgsFeatureIds QgsSensorThingsSharedData::getFeatureIdsInExtent( const QgsRectangle &extent, QgsFeedback *feedback, const QString &thisPage, QString &nextPage, const QgsFeatureIds &alreadyFetchedIds )
314{
315 const QgsRectangle requestExtent = mFilterExtent.isNull() ? extent : extent.intersect( mFilterExtent );
316 const QgsGeometry extentGeom = QgsGeometry::fromRect( requestExtent );
317 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
318
319 if ( hasCachedAllFeatures() || mCachedExtent.contains( extentGeom ) )
320 {
321 // all features cached locally, rely on local spatial index
322 nextPage.clear();
323 return qgis::listToSet( mSpatialIndex.intersects( requestExtent ) );
324 }
325
326 const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType );
327 const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, requestExtent );
328 QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter, mSubsetString } );
329 if ( !filterString.isEmpty() )
330 filterString = QStringLiteral( "&$filter=" ) + filterString;
331 int thisPageSize = mMaximumPageSize;
332 QString queryUrl;
333 if ( !thisPage.isEmpty() )
334 {
335 queryUrl = thisPage;
336 const thread_local QRegularExpression topRe( QStringLiteral( "\\$top=\\d+" ) );
337 const QRegularExpressionMatch match = topRe.match( queryUrl );
338 if ( match.hasMatch() )
339 {
340 if ( mFeatureLimit > 0 && ( mCachedFeatures.size() + thisPageSize ) > mFeatureLimit )
341 thisPageSize = mFeatureLimit - mCachedFeatures.size();
342 queryUrl = queryUrl.left( match.capturedStart( 0 ) ) + QStringLiteral( "$top=%1" ).arg( thisPageSize ) + queryUrl.mid( match.capturedEnd( 0 ) );
343 }
344 }
345 else
346 {
347 queryUrl = QStringLiteral( "%1?$top=%2&$count=false%3%4" ).arg( mEntityBaseUri ).arg( thisPageSize ).arg( filterString, !mExpandQueryString.isEmpty() ? ( QStringLiteral( "&" ) + mExpandQueryString ) : QString() );
348 }
349
350 if ( thisPage.isEmpty() && mCachedExtent.intersects( extentGeom ) )
351 {
352 // we have SOME of the results from this extent cached. Let's return those first.
353 // This is slightly nicer from a rendering point of view, because panning the map won't see features
354 // previously visible disappear temporarily while we wait for them to be included in the service's result set...
355 nextPage = queryUrl;
356 return qgis::listToSet( mSpatialIndex.intersects( requestExtent ) );
357 }
358
359 locker.unlock();
360
361 QgsFeatureIds ids;
362
363 bool noMoreFeatures = false;
364 bool hasFirstPage = false;
365 const bool res = processFeatureRequest( queryUrl, feedback, [&ids, &alreadyFetchedIds]( const QgsFeature & feature )
366 {
367 if ( !alreadyFetchedIds.contains( feature.id() ) )
368 ids.insert( feature.id() );
369 }, [&hasFirstPage]
370 {
371 if ( !hasFirstPage )
372 {
373 hasFirstPage = true;
374 return true;
375 }
376
377 return false;
378 }, [&noMoreFeatures]
379 {
380 noMoreFeatures = true;
381 } );
382 if ( noMoreFeatures && res && ( !feedback || !feedback->isCanceled() ) )
383 {
384 locker.changeMode( QgsReadWriteLocker::Write );
385 mCachedExtent = QgsGeometry::unaryUnion( { mCachedExtent, extentGeom } );
386 }
387 nextPage = noMoreFeatures || !res ? QString() : queryUrl;
388
389 return ids;
390}
391
392void QgsSensorThingsSharedData::clearCache()
393{
394 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Write );
395
396 mFeatureCount = static_cast< long long >( Qgis::FeatureCountState::Uncounted );
397 mCachedFeatures.clear();
398 mIotIdToFeatureId.clear();
399 mSpatialIndex = QgsSpatialIndex();
400 mFetchedFeatureExtent = QgsRectangle();
401}
402
403bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFeedback *feedback, const std::function< void( const QgsFeature & ) > &fetchedFeatureCallback, const std::function<bool ()> &continueFetchingCallback, const std::function<void ()> &onNoMoreFeaturesCallback )
404{
405 // copy some members before we unlock the read/write locker
406
407 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
408 const QString authcfg = mAuthCfg;
409 const QgsHttpHeaders headers = mHeaders;
410 const QgsFields fields = mFields;
411 const QList< QgsSensorThingsExpansionDefinition > expansions = mExpansions;
412
413 while ( continueFetchingCallback() )
414 {
415 // don't lock while doing the fetch
416 locker.unlock();
417
418 // from: https://docs.ogc.org/is/18-088/18-088.html#nextLink
419 // "SensorThings clients SHALL treat the URL of the nextLink as opaque, and SHALL NOT append system query options to the URL of a next link"
420 //
421 // ie don't mess with this URL!!
422 const QUrl url = parseUrl( nextPage );
423
424 QNetworkRequest request( url );
425 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsSensorThingsSharedData" ) );
426 headers.updateNetworkRequest( request );
427
428 QgsBlockingNetworkRequest networkRequest;
429 networkRequest.setAuthCfg( authcfg );
430 const QgsBlockingNetworkRequest::ErrorCode error = networkRequest.get( request, false, feedback );
431 if ( feedback && feedback->isCanceled() )
432 {
433 return false;
434 }
435
437 {
438 QgsDebugError( QStringLiteral( "Network error: %1" ).arg( networkRequest.errorMessage() ) );
439 locker.changeMode( QgsReadWriteLocker::Write );
440 mError = networkRequest.errorMessage();
441 QgsDebugMsgLevel( QStringLiteral( "Query returned empty result" ), 2 );
442 return false;
443 }
444 else
445 {
446 const QgsNetworkReplyContent content = networkRequest.reply();
447 try
448 {
449 const auto rootContent = json::parse( content.content().toStdString() );
450 if ( !rootContent.contains( "value" ) )
451 {
452 locker.changeMode( QgsReadWriteLocker::Write );
453 mError = QObject::tr( "No 'value' in response" );
454 QgsDebugMsgLevel( QStringLiteral( "No 'value' in response" ), 2 );
455 return false;
456 }
457 else
458 {
459 // all good, got a batch of features
460 const auto &values = rootContent["value"];
461 if ( values.empty() )
462 {
463 locker.changeMode( QgsReadWriteLocker::Write );
464
465 onNoMoreFeaturesCallback();
466
467 return true;
468 }
469 else
470 {
471 locker.changeMode( QgsReadWriteLocker::Write );
472 for ( const auto &featureData : values )
473 {
474 auto getString = []( const basic_json<> &json, const char *tag ) -> QVariant
475 {
476 if ( !json.contains( tag ) )
477 return QVariant();
478
479 std::function< QString( const basic_json<> &obj, bool &ok ) > objToString;
480 objToString = [&objToString]( const basic_json<> &obj, bool & ok ) -> QString
481 {
482 ok = true;
483 if ( obj.is_number_integer() )
484 {
485 return QString::number( obj.get<int>() );
486 }
487 else if ( obj.is_number_unsigned() )
488 {
489 return QString::number( obj.get<unsigned>() );
490 }
491 else if ( obj.is_boolean() )
492 {
493 return QString::number( obj.get<bool>() );
494 }
495 else if ( obj.is_number_float() )
496 {
497 return QString::number( obj.get<double>() );
498 }
499 else if ( obj.is_array() )
500 {
501 QStringList results;
502 results.reserve( obj.size() );
503 for ( const auto &item : obj )
504 {
505 bool itemOk = false;
506 const QString itemString = objToString( item, itemOk );
507 if ( itemOk )
508 results.push_back( itemString );
509 }
510 return results.join( ',' );
511 }
512 else if ( obj.is_string() )
513 {
514 return QString::fromStdString( obj.get<std::string >() );
515 }
516
517 ok = false;
518 return QString();
519 };
520
521 const auto &jObj = json[tag];
522 bool ok = false;
523 const QString r = objToString( jObj, ok );
524 if ( ok )
525 return r;
526 return QVariant();
527 };
528
529 auto getDateTime = []( const basic_json<> &json, const char *tag ) -> QVariant
530 {
531 if ( !json.contains( tag ) )
532 return QVariant();
533
534 const auto &jObj = json[tag];
535 if ( jObj.is_string() )
536 {
537 const QString dateTimeString = QString::fromStdString( json[tag].get<std::string >() );
538 return QDateTime::fromString( dateTimeString, Qt::ISODateWithMs );
539 }
540
541 return QVariant();
542 };
543
544 auto getVariantMap = []( const basic_json<> &json, const char *tag ) -> QVariant
545 {
546 if ( !json.contains( tag ) )
547 return QVariant();
548
549 return QgsJsonUtils::jsonToVariant( json[tag] );
550 };
551
552 auto getVariantList = []( const basic_json<> &json, const char *tag ) -> QVariant
553 {
554 if ( !json.contains( tag ) )
555 return QVariant();
556
557 return QgsJsonUtils::jsonToVariant( json[tag] );
558 };
559
560 auto getStringList = []( const basic_json<> &json, const char *tag ) -> QVariant
561 {
562 if ( !json.contains( tag ) )
563 return QVariant();
564
565 const auto &jObj = json[tag];
566 if ( jObj.is_string() )
567 {
568 return QStringList{ QString::fromStdString( json[tag].get<std::string >() ) };
569 }
570 else if ( jObj.is_array() )
571 {
572 QStringList res;
573 for ( const auto &element : jObj )
574 {
575 if ( element.is_string() )
576 res.append( QString::fromStdString( element.get<std::string >() ) );
577 }
578 return res;
579 }
580
581 return QVariant();
582 };
583
584 auto getDateTimeRange = []( const basic_json<> &json, const char *tag ) -> std::pair< QVariant, QVariant >
585 {
586 if ( !json.contains( tag ) )
587 return { QVariant(), QVariant() };
588
589 const auto &jObj = json[tag];
590 if ( jObj.is_string() )
591 {
592 const QString rangeString = QString::fromStdString( json[tag].get<std::string >() );
593 const QStringList rangeParts = rangeString.split( '/' );
594 if ( rangeParts.size() == 2 )
595 {
596 return
597 {
598 QDateTime::fromString( rangeParts.at( 0 ), Qt::ISODateWithMs ),
599 QDateTime::fromString( rangeParts.at( 1 ), Qt::ISODateWithMs )
600 };
601 }
602 else
603 {
604 const QDateTime instant = QDateTime::fromString( rangeString, Qt::ISODateWithMs );
605 if ( instant.isValid() )
606 return { instant, instant };
607 }
608 }
609
610 return { QVariant(), QVariant() };
611 };
612
613 const QString iotId = getString( featureData, "@iot.id" ).toString();
614 if ( expansions.isEmpty() )
615 {
616 auto existingFeatureIdIt = mIotIdToFeatureId.constFind( iotId );
617 if ( existingFeatureIdIt != mIotIdToFeatureId.constEnd() )
618 {
619 // we've previously fetched and cached this feature, skip it
620 fetchedFeatureCallback( *mCachedFeatures.find( *existingFeatureIdIt ) );
621 continue;
622 }
623 }
624
625 QgsFeature feature( fields );
626
627 // Set geometry
628 if ( mGeometryType != Qgis::WkbType::NoGeometry )
629 {
630 if ( featureData.contains( mGeometryField.toLocal8Bit().constData() ) )
631 {
632 const auto &geometryPart = featureData[mGeometryField.toLocal8Bit().constData()];
633 if ( geometryPart.contains( "geometry" ) )
634 feature.setGeometry( QgsJsonUtils::geometryFromGeoJson( geometryPart["geometry"] ) );
635 else
636 feature.setGeometry( QgsJsonUtils::geometryFromGeoJson( geometryPart ) );
637 }
638 }
639
640 auto extendAttributes = [&getString, &getVariantMap, &getDateTimeRange, &getDateTime, &getStringList, &getVariantList]( Qgis::SensorThingsEntity entityType, const auto & entityData, QgsAttributes & attributes )
641 {
642 const QString iotId = getString( entityData, "@iot.id" ).toString();
643 const QString selfLink = getString( entityData, "@iot.selfLink" ).toString();
644
645 const QVariant properties = getVariantMap( entityData, "properties" );
646
647 // NOLINTBEGIN(bugprone-branch-clone)
648 switch ( entityType )
649 {
651 break;
652
654 attributes
655 << iotId
656 << selfLink
657 << getString( entityData, "name" )
658 << getString( entityData, "description" )
659 << properties;
660 break;
661
663 attributes
664 << iotId
665 << selfLink
666 << getString( entityData, "name" )
667 << getString( entityData, "description" )
668 << properties;
669 break;
670
672 attributes
673 << iotId
674 << selfLink
675 << getDateTime( entityData, "time" );
676 break;
677
679 {
680 std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( entityData, "phenomenonTime" );
681 std::pair< QVariant, QVariant > resultTime = getDateTimeRange( entityData, "resultTime" );
682 attributes
683 << iotId
684 << selfLink
685 << getString( entityData, "name" )
686 << getString( entityData, "description" )
687 << getVariantMap( entityData, "unitOfMeasurement" )
688 << getString( entityData, "observationType" )
689 << properties
690 << phenomenonTime.first
691 << phenomenonTime.second
692 << resultTime.first
693 << resultTime.second;
694 break;
695 }
696
698 attributes
699 << iotId
700 << selfLink
701 << getString( entityData, "name" )
702 << getString( entityData, "description" )
703 << getString( entityData, "metadata" )
704 << properties;
705 break;
706
708 attributes
709 << iotId
710 << selfLink
711 << getString( entityData, "name" )
712 << getString( entityData, "definition" )
713 << getString( entityData, "description" )
714 << properties;
715 break;
716
718 {
719 std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( entityData, "phenomenonTime" );
720 std::pair< QVariant, QVariant > validTime = getDateTimeRange( entityData, "validTime" );
721 attributes
722 << iotId
723 << selfLink
724 << phenomenonTime.first
725 << phenomenonTime.second
726 << getString( entityData, "result" ) // TODO -- result type handling!
727 << getDateTime( entityData, "resultTime" )
728 << getStringList( entityData, "resultQuality" )
729 << validTime.first
730 << validTime.second
731 << getVariantMap( entityData, "parameters" );
732 break;
733 }
734
736 attributes
737 << iotId
738 << selfLink
739 << getString( entityData, "name" )
740 << getString( entityData, "description" )
741 << properties;
742 break;
743
745 {
746 std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( entityData, "phenomenonTime" );
747 std::pair< QVariant, QVariant > resultTime = getDateTimeRange( entityData, "resultTime" );
748 attributes
749 << iotId
750 << selfLink
751 << getString( entityData, "name" )
752 << getString( entityData, "description" )
753 << getVariantList( entityData, "unitOfMeasurements" )
754 << getString( entityData, "observationType" )
755 << getStringList( entityData, "multiObservationDataTypes" )
756 << properties
757 << phenomenonTime.first
758 << phenomenonTime.second
759 << resultTime.first
760 << resultTime.second;
761 break;
762 }
763 }
764 // NOLINTEND(bugprone-branch-clone)
765 };
766
767 QgsAttributes attributes;
768 attributes.reserve( fields.size() );
769 extendAttributes( mEntityType, featureData, attributes );
770
771 auto processFeature = [this, &fetchedFeatureCallback]( QgsFeature & feature, const QString & rawFeatureId )
772 {
773 feature.setId( mNextFeatureId++ );
774
775 mCachedFeatures.insert( feature.id(), feature );
776 mIotIdToFeatureId.insert( rawFeatureId, feature.id() );
777 mSpatialIndex.addFeature( feature );
778 mFetchedFeatureExtent.combineExtentWith( feature.geometry().boundingBox() );
779
780 fetchedFeatureCallback( feature );
781 };
782
783 const QString baseFeatureId = getString( featureData, "@iot.id" ).toString();
784 if ( !expansions.empty() )
785 {
786 mRetrievedBaseFeatureCount++;
787
788 std::function< void( const nlohmann::json &, Qgis::SensorThingsEntity, const QList<QgsSensorThingsExpansionDefinition > &, const QString &, const QgsAttributes & ) > traverseExpansion;
789 traverseExpansion = [this, &feature, &getString, &traverseExpansion, &fetchedFeatureCallback, &extendAttributes, &processFeature]( const nlohmann::json & currentLevelData, Qgis::SensorThingsEntity parentEntityType, const QList<QgsSensorThingsExpansionDefinition > &expansionTargets, const QString & lowerLevelId, const QgsAttributes & lowerLevelAttributes )
790 {
791 const QgsSensorThingsExpansionDefinition currentExpansionTarget = expansionTargets.at( 0 );
792 const QList< QgsSensorThingsExpansionDefinition > remainingExpansionTargets = expansionTargets.mid( 1 );
793
794 bool ok = false;
795 const Qgis::RelationshipCardinality cardinality = QgsSensorThingsUtils::relationshipCardinality( parentEntityType, currentExpansionTarget.childEntity(), ok );
796 QString currentExpansionPropertyString;
797 switch ( cardinality )
798 {
801 currentExpansionPropertyString = qgsEnumValueToKey( currentExpansionTarget.childEntity() );
802 break;
803
806 currentExpansionPropertyString = QgsSensorThingsUtils::entityToSetString( currentExpansionTarget.childEntity() );
807 break;
808 }
809
810 if ( currentLevelData.contains( currentExpansionPropertyString.toLocal8Bit().constData() ) )
811 {
812 auto parseExpandedEntity = [lowerLevelAttributes, &feature, &processFeature, &lowerLevelId, &getString, &remainingExpansionTargets, &fetchedFeatureCallback, &extendAttributes, &traverseExpansion, &currentExpansionTarget, this]( const json & expandedEntityElement )
813 {
814 QgsAttributes expandedAttributes = lowerLevelAttributes;
815 const QString expandedEntityIotId = getString( expandedEntityElement, "@iot.id" ).toString();
816 const QString expandedFeatureId = lowerLevelId + '_' + expandedEntityIotId;
817
818 if ( remainingExpansionTargets.empty() )
819 {
820 auto existingFeatureIdIt = mIotIdToFeatureId.constFind( expandedFeatureId );
821 if ( existingFeatureIdIt != mIotIdToFeatureId.constEnd() )
822 {
823 // we've previously fetched and cached this feature, skip it
824 fetchedFeatureCallback( *mCachedFeatures.find( *existingFeatureIdIt ) );
825 return;
826 }
827 }
828
829 extendAttributes( currentExpansionTarget.childEntity(), expandedEntityElement, expandedAttributes );
830 if ( !remainingExpansionTargets.empty() )
831 {
832 // traverse deeper
833 traverseExpansion( expandedEntityElement, currentExpansionTarget.childEntity(), remainingExpansionTargets, expandedFeatureId, expandedAttributes );
834 }
835 else
836 {
837 feature.setAttributes( expandedAttributes );
838 processFeature( feature, expandedFeatureId );
839 }
840 };
841 const auto &expandedEntity = currentLevelData[currentExpansionPropertyString.toLocal8Bit().constData()];
842 if ( expandedEntity.is_array() )
843 {
844 for ( const auto &expandedEntityElement : expandedEntity )
845 {
846 parseExpandedEntity( expandedEntityElement );
847 }
848 // NOTE: What do we do when the expanded entity has a next link? Does this situation ever arise?
849 // The specification doesn't explicitly state whether pagination is supported for expansion, so we assume
850 // it's not possible.
851 }
852 else if ( expandedEntity.is_object() )
853 {
854 parseExpandedEntity( expandedEntity );
855 }
856 }
857 else
858 {
859 // No expansion for this parent feature.
860 // Maybe we should NULL out the attributes and return the parent feature? Right now we just
861 // skip it if there's no child features...
862 }
863 };
864
865 traverseExpansion( featureData, mEntityType, expansions, baseFeatureId, attributes );
866
867 if ( mFeatureLimit > 0 && mFeatureLimit <= mRetrievedBaseFeatureCount )
868 break;
869 }
870 else
871 {
872 feature.setAttributes( attributes );
873 processFeature( feature, baseFeatureId );
874 mRetrievedBaseFeatureCount++;
875 if ( mFeatureLimit > 0 && mFeatureLimit <= mRetrievedBaseFeatureCount )
876 break;
877 }
878 }
879 }
880 locker.unlock();
881
882 if ( rootContent.contains( "@iot.nextLink" ) && ( mFeatureLimit == 0 || mFeatureLimit > mCachedFeatures.size() ) )
883 {
884 nextPage = QString::fromStdString( rootContent["@iot.nextLink"].get<std::string>() );
885 }
886 else
887 {
888 onNoMoreFeaturesCallback();
889 }
890
891 // if target feature was added to cache, return it
892 if ( !continueFetchingCallback() )
893 {
894 return true;
895 }
896 }
897 }
898 catch ( const json::parse_error &ex )
899 {
900 locker.changeMode( QgsReadWriteLocker::Write );
901 mError = QObject::tr( "Error parsing response: %1" ).arg( ex.what() );
902 QgsDebugMsgLevel( QStringLiteral( "Error parsing response: %1" ).arg( ex.what() ), 2 );
903 return false;
904 }
905 }
906 }
907 return false;
908}
909
SensorThingsEntity
OGC SensorThings API entity types.
Definition qgis.h:5119
@ Sensor
A Sensor is an instrument that observes a property or phenomenon with the goal of producing an estima...
@ MultiDatastream
A MultiDatastream groups a collection of Observations and the Observations in a MultiDatastream have ...
@ ObservedProperty
An ObservedProperty specifies the phenomenon of an Observation.
@ Invalid
An invalid/unknown entity.
@ FeatureOfInterest
In the context of the Internet of Things, many Observations’ FeatureOfInterest can be the Location of...
@ Datastream
A Datastream groups a collection of Observations measuring the same ObservedProperty and produced by ...
@ Observation
An Observation is the act of measuring or otherwise determining the value of a property.
@ Location
A Location entity locates the Thing or the Things it associated with. A Thing’s Location entity is de...
@ Thing
A Thing is an object of the physical world (physical things) or the information world (virtual things...
@ HistoricalLocation
A Thing’s HistoricalLocation entity set provides the times of the current (i.e., last known) and prev...
RelationshipCardinality
Relationship cardinality.
Definition qgis.h:3828
@ ManyToMany
Many to many relationship.
@ ManyToOne
Many to one relationship.
@ OneToOne
One to one relationship.
@ OneToMany
One to many relationship.
@ MultiPointZ
MultiPointZ.
@ NoGeometry
No geometry.
@ PointZ
PointZ.
@ MultiLineStringZ
MultiLineStringZ.
@ MultiPolygonZ
MultiPolygonZ.
A vector of attributes.
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr)
Performs a "get" operation on the specified request.
void setAuthCfg(const QString &authCfg)
Sets the authentication config id which should be used during the request.
QString errorMessage() const
Returns the error message string, after a get(), post(), head() or put() request has been made.
@ NoError
No error was encountered.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get(), post(), head() or put() request has been mad...
This class represents a coordinate reference system (CRS).
Class for storing the component parts of a RDBMS data source URI (e.g.
void setEncodedUri(const QByteArray &uri)
Sets the complete encoded uri.
QgsHttpHeaders httpHeaders() const
Returns http headers.
QString authConfigId() const
Returns any associated authentication configuration ID stored in the URI.
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:58
QgsFeatureId id
Definition qgsfeature.h:66
void setId(QgsFeatureId id)
Sets the feature id for this feature.
QgsGeometry geometry
Definition qgsfeature.h:69
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:53
Container of fields for a vector layer.
Definition qgsfields.h:46
int size() const
Returns number of items.
A geometry is the spatial representation of a feature.
static QgsGeometry fromRect(const QgsRectangle &rect)
Creates a new geometry from a QgsRectangle.
static QgsGeometry unaryUnion(const QVector< QgsGeometry > &geometries, const QgsGeometryParameters &parameters=QgsGeometryParameters())
Compute the unary union on a list of geometries.
QgsRectangle boundingBox() const
Returns the bounding box of the geometry.
This class implements simple http header management.
bool updateNetworkRequest(QNetworkRequest &request) const
Updates a request by adding all the HTTP headers.
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.
Encapsulates a network reply within a container which is inexpensive to copy and safe to pass between...
QByteArray content() const
Returns the reply content.
The QgsReadWriteLocker class is a convenience class that simplifies locking and unlocking QReadWriteL...
@ Write
Lock for write.
A rectangle specified with double values.
bool isNull() const
Test if the rectangle is null (holding no spatial information).
QgsRectangle intersect(const QgsRectangle &rect) const
Returns the intersection with the given rectangle.
Encapsulates information about how relationships in a SensorThings API service should be expanded.
Qgis::SensorThingsEntity childEntity() const
Returns the target child entity which should be expanded.
bool isValid() const
Returns true if the definition is valid.
static QString entityToSetString(Qgis::SensorThingsEntity type)
Converts a SensorThings entity set to a SensorThings entity set string.
static QString asQueryString(Qgis::SensorThingsEntity baseType, const QList< QgsSensorThingsExpansionDefinition > &expansions)
Returns a list of expansions as a valid SensorThings API query string, eg "$expand=Locations($orderby...
static QString combineFilters(const QStringList &filters)
Combines a set of SensorThings API filter operators.
static QString filterForWkbType(Qgis::SensorThingsEntity entityType, Qgis::WkbType wkbType)
Returns a filter string which restricts results to those matching the specified entityType and wkbTyp...
static Qgis::RelationshipCardinality relationshipCardinality(Qgis::SensorThingsEntity baseType, Qgis::SensorThingsEntity relatedType, bool &valid)
Returns the cardinality of the relationship between a base entity type and a related entity type.
static bool entityTypeHasGeometry(Qgis::SensorThingsEntity type)
Returns true if the specified entity type can have geometry attached.
static QgsFields fieldsForExpandedEntityType(Qgis::SensorThingsEntity baseType, const QList< Qgis::SensorThingsEntity > &expandedTypes)
Returns the fields which correspond to a specified entity baseType, expanded using the specified list...
static QString geometryFieldForEntityType(Qgis::SensorThingsEntity type)
Returns the geometry field for a specified entity type.
static QString filterForExtent(const QString &geometryField, const QgsRectangle &extent)
Returns a filter string which restricts results to those within the specified extent.
A spatial index for QgsFeature objects.
@ Uncounted
Feature count not yet computed.
@ UnknownCount
Provider returned an unknown feature count.
T qgsEnumKeyToValue(const QString &key, const T &defaultValue, bool tryValueAsKey=true, bool *returnOk=nullptr)
Returns the value corresponding to the given key of an enum.
Definition qgis.h:5675
QString qgsEnumValueToKey(const T &value, bool *returnOk=nullptr)
Returns the value for the given key of an enum.
Definition qgis.h:5656
QSet< QgsFeatureId > QgsFeatureIds
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:39
#define QgsDebugError(str)
Definition qgslogger.h:38
#define QgsSetRequestInitiatorClass(request, _class)