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