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