QGIS API Documentation 3.99.0-Master (09f76ad7019)
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
175 : ( !mFilterExtent.isNull() ? mFilterExtent : QgsRectangle( -180, -90, 180, 90 ) );
176}
177
178long long QgsSensorThingsSharedData::featureCount( QgsFeedback *feedback ) const
179{
180 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
181 if ( mFeatureCount >= 0 )
182 return mFeatureCount;
183
184 locker.changeMode( QgsReadWriteLocker::Write );
185 mError.clear();
186
187 // MISSING PART -- how to handle feature count when we are expanding features?
188 // This situation is not handled by the SensorThings standard at all, so we'll just have
189 // to return an unknown count whenever expansion is used
190 if ( !mExpansions.isEmpty() )
191 {
192 return static_cast< long long >( Qgis::FeatureCountState::UnknownCount );
193 }
194
195 // return no features, just the total count
196 QString countUri = u"%1?$top=0&$count=true"_s.arg( mEntityBaseUri );
197 const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType );
198 const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, mFilterExtent );
199 QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter, mSubsetString } );
200 if ( !filterString.isEmpty() )
201 filterString = u"&$filter="_s + filterString;
202 if ( !filterString.isEmpty() )
203 countUri += filterString;
204
205 const QUrl url = parseUrl( QUrl( countUri ) );
206
207 QNetworkRequest request( url );
208 QgsSetRequestInitiatorClass( request, u"QgsSensorThingsSharedData"_s );
209 mHeaders.updateNetworkRequest( request );
210
211 QgsBlockingNetworkRequest networkRequest;
212 networkRequest.setAuthCfg( mAuthCfg );
213 const QgsBlockingNetworkRequest::ErrorCode error = networkRequest.get( request, false, feedback );
214
215 if ( feedback && feedback->isCanceled() )
216 return mFeatureCount;
217
218 // Handle network errors
220 {
221 QgsDebugError( u"Network error: %1"_s.arg( networkRequest.errorMessage() ) );
222 mError = networkRequest.errorMessage();
223 }
224 else
225 {
226 const QgsNetworkReplyContent content = networkRequest.reply();
227 try
228 {
229 auto rootContent = json::parse( content.content().toStdString() );
230 if ( !rootContent.contains( "@iot.count" ) )
231 {
232 mError = QObject::tr( "No '@iot.count' value in response" );
233 return mFeatureCount;
234 }
235
236 mFeatureCount = rootContent["@iot.count"].get<long long>();
237 if ( mFeatureLimit > 0 && mFeatureCount > mFeatureLimit )
238 mFeatureCount = mFeatureLimit;
239 }
240 catch ( const json::parse_error &ex )
241 {
242 mError = QObject::tr( "Error parsing response: %1" ).arg( ex.what() );
243 }
244 }
245
246 return mFeatureCount;
247}
248
249QString QgsSensorThingsSharedData::subsetString() const
250{
251 return mSubsetString;
252}
253
254bool QgsSensorThingsSharedData::hasCachedAllFeatures() const
255{
256 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
257 return mHasCachedAllFeatures
258 || ( mFeatureCount > 0 && mCachedFeatures.size() == mFeatureCount )
259 || ( 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 int thisPageSize = mMaximumPageSize;
284 if ( mFeatureLimit > 0 && ( mCachedFeatures.size() + thisPageSize ) > mFeatureLimit )
285 thisPageSize = 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( mNextPage, feedback, [id, &f, &featureFetched]( const QgsFeature & feature )
298 {
299 if ( feature.id() == id )
300 {
301 f = feature;
302 featureFetched = true;
303 // don't break here -- store all the features we retrieved in this page first!
304 }
305 }, [&featureFetched, this]
306 {
307 return !featureFetched && !hasCachedAllFeatures();
308 }, [this]
309 {
310 mNextPage.clear();
311 mHasCachedAllFeatures = true;
312 } );
313
314 return featureFetched;
315}
316
317QgsFeatureIds QgsSensorThingsSharedData::getFeatureIdsInExtent( const QgsRectangle &extent, QgsFeedback *feedback, const QString &thisPage, QString &nextPage, const QgsFeatureIds &alreadyFetchedIds )
318{
319 const QgsRectangle requestExtent = mFilterExtent.isNull() ? extent : extent.intersect( mFilterExtent );
320 const QgsGeometry extentGeom = QgsGeometry::fromRect( requestExtent );
321 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
322
323 if ( hasCachedAllFeatures() || mCachedExtent.contains( extentGeom ) )
324 {
325 // all features cached locally, rely on local spatial index
326 nextPage.clear();
327 return qgis::listToSet( mSpatialIndex.intersects( requestExtent ) );
328 }
329
330 const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType );
331 const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, requestExtent );
332 QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter, mSubsetString } );
333 if ( !filterString.isEmpty() )
334 filterString = u"&$filter="_s + filterString;
335 int thisPageSize = mMaximumPageSize;
336 QString queryUrl;
337 if ( !thisPage.isEmpty() )
338 {
339 queryUrl = thisPage;
340 const thread_local QRegularExpression topRe( u"\\$top=\\d+"_s );
341 const QRegularExpressionMatch match = topRe.match( queryUrl );
342 if ( match.hasMatch() )
343 {
344 if ( mFeatureLimit > 0 && ( mCachedFeatures.size() + thisPageSize ) > mFeatureLimit )
345 thisPageSize = mFeatureLimit - mCachedFeatures.size();
346 queryUrl = queryUrl.left( match.capturedStart( 0 ) ) + u"$top=%1"_s.arg( thisPageSize ) + queryUrl.mid( match.capturedEnd( 0 ) );
347 }
348 }
349 else
350 {
351 queryUrl = u"%1?$top=%2&$count=false%3%4"_s.arg( mEntityBaseUri ).arg( thisPageSize ).arg( filterString, !mExpandQueryString.isEmpty() ? ( u"&"_s + mExpandQueryString ) : QString() );
352 }
353
354 if ( thisPage.isEmpty() && mCachedExtent.intersects( extentGeom ) )
355 {
356 // we have SOME of the results from this extent cached. Let's return those first.
357 // This is slightly nicer from a rendering point of view, because panning the map won't see features
358 // previously visible disappear temporarily while we wait for them to be included in the service's result set...
359 nextPage = queryUrl;
360 return qgis::listToSet( mSpatialIndex.intersects( requestExtent ) );
361 }
362
363 locker.unlock();
364
365 QgsFeatureIds ids;
366
367 bool noMoreFeatures = false;
368 bool hasFirstPage = false;
369 const bool res = processFeatureRequest( queryUrl, feedback, [&ids, &alreadyFetchedIds]( const QgsFeature & feature )
370 {
371 if ( !alreadyFetchedIds.contains( feature.id() ) )
372 ids.insert( feature.id() );
373 }, [&hasFirstPage]
374 {
375 if ( !hasFirstPage )
376 {
377 hasFirstPage = true;
378 return true;
379 }
380
381 return false;
382 }, [&noMoreFeatures]
383 {
384 noMoreFeatures = true;
385 } );
386 if ( noMoreFeatures && res && ( !feedback || !feedback->isCanceled() ) )
387 {
388 locker.changeMode( QgsReadWriteLocker::Write );
389 mCachedExtent = QgsGeometry::unaryUnion( { mCachedExtent, extentGeom } );
390 }
391 nextPage = noMoreFeatures || !res ? QString() : queryUrl;
392
393 return ids;
394}
395
396void QgsSensorThingsSharedData::clearCache()
397{
398 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Write );
399
400 mFeatureCount = static_cast< long long >( Qgis::FeatureCountState::Uncounted );
401 mCachedFeatures.clear();
402 mIotIdToFeatureId.clear();
403 mSpatialIndex = QgsSpatialIndex();
404 mFetchedFeatureExtent = QgsRectangle();
405}
406
407bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFeedback *feedback, const std::function< void( const QgsFeature & ) > &fetchedFeatureCallback, const std::function<bool ()> &continueFetchingCallback, const std::function<void ()> &onNoMoreFeaturesCallback )
408{
409 // copy some members before we unlock the read/write locker
410
411 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
412 const QString authcfg = mAuthCfg;
413 const QgsHttpHeaders headers = mHeaders;
414 const QgsFields fields = mFields;
415 const QList< QgsSensorThingsExpansionDefinition > expansions = mExpansions;
416
417 while ( continueFetchingCallback() )
418 {
419 // don't lock while doing the fetch
420 locker.unlock();
421
422 // from: https://docs.ogc.org/is/18-088/18-088.html#nextLink
423 // "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"
424 //
425 // ie don't mess with this URL!!
426 const QUrl url = parseUrl( nextPage );
427
428 QNetworkRequest request( url );
429 QgsSetRequestInitiatorClass( request, u"QgsSensorThingsSharedData"_s );
430 headers.updateNetworkRequest( request );
431
432 QgsBlockingNetworkRequest networkRequest;
433 networkRequest.setAuthCfg( authcfg );
434 const QgsBlockingNetworkRequest::ErrorCode error = networkRequest.get( request, false, feedback );
435 if ( feedback && feedback->isCanceled() )
436 {
437 return false;
438 }
439
441 {
442 QgsDebugError( u"Network error: %1"_s.arg( networkRequest.errorMessage() ) );
443 locker.changeMode( QgsReadWriteLocker::Write );
444 mError = networkRequest.errorMessage();
445 QgsDebugMsgLevel( u"Query returned empty result"_s, 2 );
446 return false;
447 }
448 else
449 {
450 const QgsNetworkReplyContent content = networkRequest.reply();
451 try
452 {
453 const auto rootContent = json::parse( content.content().toStdString() );
454 if ( !rootContent.contains( "value" ) )
455 {
456 locker.changeMode( QgsReadWriteLocker::Write );
457 mError = QObject::tr( "No 'value' in response" );
458 QgsDebugMsgLevel( u"No 'value' in response"_s, 2 );
459 return false;
460 }
461 else
462 {
463 // all good, got a batch of features
464 const auto &values = rootContent["value"];
465 if ( values.empty() )
466 {
467 locker.changeMode( QgsReadWriteLocker::Write );
468
469 onNoMoreFeaturesCallback();
470
471 return true;
472 }
473 else
474 {
475 locker.changeMode( QgsReadWriteLocker::Write );
476 for ( const auto &featureData : values )
477 {
478 auto getString = []( const basic_json<> &json, const char *tag ) -> QVariant
479 {
480 if ( !json.contains( tag ) )
481 return QVariant();
482
483 std::function< QString( const basic_json<> &obj, bool &ok ) > objToString;
484 objToString = [&objToString]( const basic_json<> &obj, bool & ok ) -> QString
485 {
486 ok = true;
487 if ( obj.is_number_integer() )
488 {
489 return QString::number( obj.get<int>() );
490 }
491 else if ( obj.is_number_unsigned() )
492 {
493 return QString::number( obj.get<unsigned>() );
494 }
495 else if ( obj.is_boolean() )
496 {
497 return QString::number( obj.get<bool>() );
498 }
499 else if ( obj.is_number_float() )
500 {
501 return QString::number( obj.get<double>() );
502 }
503 else if ( obj.is_array() )
504 {
505 QStringList results;
506 results.reserve( obj.size() );
507 for ( const auto &item : obj )
508 {
509 bool itemOk = false;
510 const QString itemString = objToString( item, itemOk );
511 if ( itemOk )
512 results.push_back( itemString );
513 }
514 return results.join( ',' );
515 }
516 else if ( obj.is_string() )
517 {
518 return QString::fromStdString( obj.get<std::string >() );
519 }
520
521 ok = false;
522 return QString();
523 };
524
525 const auto &jObj = json[tag];
526 bool ok = false;
527 const QString r = objToString( jObj, ok );
528 if ( ok )
529 return r;
530 return QVariant();
531 };
532
533 auto getDateTime = []( const basic_json<> &json, const char *tag ) -> QVariant
534 {
535 if ( !json.contains( tag ) )
536 return QVariant();
537
538 const auto &jObj = json[tag];
539 if ( jObj.is_string() )
540 {
541 const QString dateTimeString = QString::fromStdString( json[tag].get<std::string >() );
542 return QDateTime::fromString( dateTimeString, Qt::ISODateWithMs );
543 }
544
545 return QVariant();
546 };
547
548 auto getVariantMap = []( const basic_json<> &json, const char *tag ) -> QVariant
549 {
550 if ( !json.contains( tag ) )
551 return QVariant();
552
553 return QgsJsonUtils::jsonToVariant( json[tag] );
554 };
555
556 auto getVariantList = []( const basic_json<> &json, const char *tag ) -> QVariant
557 {
558 if ( !json.contains( tag ) )
559 return QVariant();
560
561 return QgsJsonUtils::jsonToVariant( json[tag] );
562 };
563
564 auto getStringList = []( const basic_json<> &json, const char *tag ) -> QVariant
565 {
566 if ( !json.contains( tag ) )
567 return QVariant();
568
569 const auto &jObj = json[tag];
570 if ( jObj.is_string() )
571 {
572 return QStringList{ QString::fromStdString( json[tag].get<std::string >() ) };
573 }
574 else if ( jObj.is_array() )
575 {
576 QStringList res;
577 for ( const auto &element : jObj )
578 {
579 if ( element.is_string() )
580 res.append( QString::fromStdString( element.get<std::string >() ) );
581 }
582 return res;
583 }
584
585 return QVariant();
586 };
587
588 auto getDateTimeRange = []( const basic_json<> &json, const char *tag ) -> std::pair< QVariant, QVariant >
589 {
590 if ( !json.contains( tag ) )
591 return { QVariant(), QVariant() };
592
593 const auto &jObj = json[tag];
594 if ( jObj.is_string() )
595 {
596 const QString rangeString = QString::fromStdString( json[tag].get<std::string >() );
597 const QStringList rangeParts = rangeString.split( '/' );
598 if ( rangeParts.size() == 2 )
599 {
600 return
601 {
602 QDateTime::fromString( rangeParts.at( 0 ), Qt::ISODateWithMs ),
603 QDateTime::fromString( rangeParts.at( 1 ), Qt::ISODateWithMs )
604 };
605 }
606 else
607 {
608 const QDateTime instant = QDateTime::fromString( rangeString, Qt::ISODateWithMs );
609 if ( instant.isValid() )
610 return { instant, instant };
611 }
612 }
613
614 return { QVariant(), QVariant() };
615 };
616
617 const QString iotId = getString( featureData, "@iot.id" ).toString();
618 if ( expansions.isEmpty() )
619 {
620 auto existingFeatureIdIt = mIotIdToFeatureId.constFind( iotId );
621 if ( existingFeatureIdIt != mIotIdToFeatureId.constEnd() )
622 {
623 // we've previously fetched and cached this feature, skip it
624 fetchedFeatureCallback( *mCachedFeatures.find( *existingFeatureIdIt ) );
625 continue;
626 }
627 }
628
629 QgsFeature feature( fields );
630
631 // Set geometry
632 if ( mGeometryType != Qgis::WkbType::NoGeometry )
633 {
634 if ( featureData.contains( mGeometryField.toLocal8Bit().constData() ) )
635 {
636 const auto &geometryPart = featureData[mGeometryField.toLocal8Bit().constData()];
637 if ( geometryPart.contains( "geometry" ) )
638 feature.setGeometry( QgsJsonUtils::geometryFromGeoJson( geometryPart["geometry"] ) );
639 else
640 feature.setGeometry( QgsJsonUtils::geometryFromGeoJson( geometryPart ) );
641 }
642 }
643
644 auto extendAttributes = [&getString, &getVariantMap, &getDateTimeRange, &getDateTime, &getStringList, &getVariantList]( Qgis::SensorThingsEntity entityType, const auto & entityData, QgsAttributes & attributes )
645 {
646 const QString iotId = getString( entityData, "@iot.id" ).toString();
647 const QString selfLink = getString( entityData, "@iot.selfLink" ).toString();
648
649 const QVariant properties = getVariantMap( entityData, "properties" );
650
651 // NOLINTBEGIN(bugprone-branch-clone)
652 switch ( entityType )
653 {
655 break;
656
658 attributes
659 << iotId
660 << selfLink
661 << getString( entityData, "name" )
662 << getString( entityData, "description" )
663 << properties;
664 break;
665
667 attributes
668 << iotId
669 << selfLink
670 << getString( entityData, "name" )
671 << getString( entityData, "description" )
672 << properties;
673 break;
674
676 attributes
677 << iotId
678 << selfLink
679 << getDateTime( entityData, "time" );
680 break;
681
683 {
684 std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( entityData, "phenomenonTime" );
685 std::pair< QVariant, QVariant > resultTime = getDateTimeRange( entityData, "resultTime" );
686 attributes
687 << iotId
688 << selfLink
689 << getString( entityData, "name" )
690 << getString( entityData, "description" )
691 << getVariantMap( entityData, "unitOfMeasurement" )
692 << getString( entityData, "observationType" )
693 << properties
694 << phenomenonTime.first
695 << phenomenonTime.second
696 << resultTime.first
697 << resultTime.second;
698 break;
699 }
700
702 attributes
703 << iotId
704 << selfLink
705 << getString( entityData, "name" )
706 << getString( entityData, "description" )
707 << getString( entityData, "metadata" )
708 << properties;
709 break;
710
712 attributes
713 << iotId
714 << selfLink
715 << getString( entityData, "name" )
716 << getString( entityData, "definition" )
717 << getString( entityData, "description" )
718 << properties;
719 break;
720
722 {
723 std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( entityData, "phenomenonTime" );
724 std::pair< QVariant, QVariant > validTime = getDateTimeRange( entityData, "validTime" );
725 attributes
726 << iotId
727 << selfLink
728 << phenomenonTime.first
729 << phenomenonTime.second
730 << getString( entityData, "result" ) // TODO -- result type handling!
731 << getDateTime( entityData, "resultTime" )
732 << getStringList( entityData, "resultQuality" )
733 << validTime.first
734 << validTime.second
735 << getVariantMap( entityData, "parameters" );
736 break;
737 }
738
740 attributes
741 << iotId
742 << selfLink
743 << getString( entityData, "name" )
744 << getString( entityData, "description" )
745 << properties;
746 break;
747
749 {
750 std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( entityData, "phenomenonTime" );
751 std::pair< QVariant, QVariant > resultTime = getDateTimeRange( entityData, "resultTime" );
752 attributes
753 << iotId
754 << selfLink
755 << getString( entityData, "name" )
756 << getString( entityData, "description" )
757 << getVariantList( entityData, "unitOfMeasurements" )
758 << getString( entityData, "observationType" )
759 << getStringList( entityData, "multiObservationDataTypes" )
760 << properties
761 << phenomenonTime.first
762 << phenomenonTime.second
763 << resultTime.first
764 << resultTime.second;
765 break;
766 }
767 }
768 // NOLINTEND(bugprone-branch-clone)
769 };
770
771 QgsAttributes attributes;
772 attributes.reserve( fields.size() );
773 extendAttributes( mEntityType, featureData, attributes );
774
775 auto processFeature = [this, &fetchedFeatureCallback]( QgsFeature & feature, const QString & rawFeatureId )
776 {
777 feature.setId( mNextFeatureId++ );
778
779 mCachedFeatures.insert( feature.id(), feature );
780 mIotIdToFeatureId.insert( rawFeatureId, feature.id() );
781 mSpatialIndex.addFeature( feature );
782 mFetchedFeatureExtent.combineExtentWith( feature.geometry().boundingBox() );
783
784 fetchedFeatureCallback( feature );
785 };
786
787 const QString baseFeatureId = getString( featureData, "@iot.id" ).toString();
788 if ( !expansions.empty() )
789 {
790 mRetrievedBaseFeatureCount++;
791
792 std::function< void( const nlohmann::json &, Qgis::SensorThingsEntity, const QList<QgsSensorThingsExpansionDefinition > &, const QString &, const QgsAttributes & ) > traverseExpansion;
793 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 )
794 {
795 const QgsSensorThingsExpansionDefinition currentExpansionTarget = expansionTargets.at( 0 );
796 const QList< QgsSensorThingsExpansionDefinition > remainingExpansionTargets = expansionTargets.mid( 1 );
797
798 bool ok = false;
799 const Qgis::RelationshipCardinality cardinality = QgsSensorThingsUtils::relationshipCardinality( parentEntityType, currentExpansionTarget.childEntity(), ok );
800 QString currentExpansionPropertyString;
801 switch ( cardinality )
802 {
805 currentExpansionPropertyString = qgsEnumValueToKey( currentExpansionTarget.childEntity() );
806 break;
807
810 currentExpansionPropertyString = QgsSensorThingsUtils::entityToSetString( currentExpansionTarget.childEntity() );
811 break;
812 }
813
814 if ( currentLevelData.contains( currentExpansionPropertyString.toLocal8Bit().constData() ) )
815 {
816 auto parseExpandedEntity = [lowerLevelAttributes, &feature, &processFeature, &lowerLevelId, &getString, &remainingExpansionTargets, &fetchedFeatureCallback, &extendAttributes, &traverseExpansion, &currentExpansionTarget, this]( const json & expandedEntityElement )
817 {
818 QgsAttributes expandedAttributes = lowerLevelAttributes;
819 const QString expandedEntityIotId = getString( expandedEntityElement, "@iot.id" ).toString();
820 const QString expandedFeatureId = lowerLevelId + '_' + expandedEntityIotId;
821
822 if ( remainingExpansionTargets.empty() )
823 {
824 auto existingFeatureIdIt = mIotIdToFeatureId.constFind( expandedFeatureId );
825 if ( existingFeatureIdIt != mIotIdToFeatureId.constEnd() )
826 {
827 // we've previously fetched and cached this feature, skip it
828 fetchedFeatureCallback( *mCachedFeatures.find( *existingFeatureIdIt ) );
829 return;
830 }
831 }
832
833 extendAttributes( currentExpansionTarget.childEntity(), expandedEntityElement, expandedAttributes );
834 if ( !remainingExpansionTargets.empty() )
835 {
836 // traverse deeper
837 traverseExpansion( expandedEntityElement, currentExpansionTarget.childEntity(), remainingExpansionTargets, expandedFeatureId, expandedAttributes );
838 }
839 else
840 {
841 feature.setAttributes( expandedAttributes );
842 processFeature( feature, expandedFeatureId );
843 }
844 };
845 const auto &expandedEntity = currentLevelData[currentExpansionPropertyString.toLocal8Bit().constData()];
846 if ( expandedEntity.is_array() )
847 {
848 for ( const auto &expandedEntityElement : expandedEntity )
849 {
850 parseExpandedEntity( expandedEntityElement );
851 }
852 // NOTE: What do we do when the expanded entity has a next link? Does this situation ever arise?
853 // The specification doesn't explicitly state whether pagination is supported for expansion, so we assume
854 // it's not possible.
855 }
856 else if ( expandedEntity.is_object() )
857 {
858 parseExpandedEntity( expandedEntity );
859 }
860 }
861 else
862 {
863 // No expansion for this parent feature.
864 // Maybe we should NULL out the attributes and return the parent feature? Right now we just
865 // skip it if there's no child features...
866 }
867 };
868
869 traverseExpansion( featureData, mEntityType, expansions, baseFeatureId, attributes );
870
871 if ( mFeatureLimit > 0 && mFeatureLimit <= mRetrievedBaseFeatureCount )
872 break;
873 }
874 else
875 {
876 feature.setAttributes( attributes );
877 processFeature( feature, baseFeatureId );
878 mRetrievedBaseFeatureCount++;
879 if ( mFeatureLimit > 0 && mFeatureLimit <= mRetrievedBaseFeatureCount )
880 break;
881 }
882 }
883 }
884 locker.unlock();
885
886 if ( rootContent.contains( "@iot.nextLink" ) && ( mFeatureLimit == 0 || mFeatureLimit > mCachedFeatures.size() ) )
887 {
888 nextPage = QString::fromStdString( rootContent["@iot.nextLink"].get<std::string>() );
889 }
890 else
891 {
892 onNoMoreFeaturesCallback();
893 }
894
895 // if target feature was added to cache, return it
896 if ( !continueFetchingCallback() )
897 {
898 return true;
899 }
900 }
901 }
902 catch ( const json::parse_error &ex )
903 {
904 locker.changeMode( QgsReadWriteLocker::Write );
905 mError = QObject::tr( "Error parsing response: %1" ).arg( ex.what() );
906 QgsDebugMsgLevel( u"Error parsing response: %1"_s.arg( ex.what() ), 2 );
907 return false;
908 }
909 }
910 }
911 return false;
912}
913
SensorThingsEntity
OGC SensorThings API entity types.
Definition qgis.h:6267
@ Sensor
A Sensor is an instrument that observes a property or phenomenon with the goal of producing an estima...
Definition qgis.h:6273
@ MultiDatastream
A MultiDatastream groups a collection of Observations and the Observations in a MultiDatastream have ...
Definition qgis.h:6277
@ ObservedProperty
An ObservedProperty specifies the phenomenon of an Observation.
Definition qgis.h:6274
@ Invalid
An invalid/unknown entity.
Definition qgis.h:6268
@ FeatureOfInterest
In the context of the Internet of Things, many Observations’ FeatureOfInterest can be the Location of...
Definition qgis.h:6276
@ Datastream
A Datastream groups a collection of Observations measuring the same ObservedProperty and produced by ...
Definition qgis.h:6272
@ Observation
An Observation is the act of measuring or otherwise determining the value of a property.
Definition qgis.h:6275
@ Location
A Location entity locates the Thing or the Things it associated with. A Thing’s Location entity is de...
Definition qgis.h:6270
@ Thing
A Thing is an object of the physical world (physical things) or the information world (virtual things...
Definition qgis.h:6269
@ HistoricalLocation
A Thing’s HistoricalLocation entity set provides the times of the current (i.e., last known) and prev...
Definition qgis.h:6271
RelationshipCardinality
Relationship cardinality.
Definition qgis.h:4511
@ ManyToMany
Many to many relationship.
Definition qgis.h:4515
@ ManyToOne
Many to one relationship.
Definition qgis.h:4514
@ OneToOne
One to one relationship.
Definition qgis.h:4512
@ OneToMany
One to many relationship.
Definition qgis.h:4513
@ MultiPointZ
MultiPointZ.
Definition qgis.h:303
@ NoGeometry
No geometry.
Definition qgis.h:298
@ PointZ
PointZ.
Definition qgis.h:299
@ MultiLineStringZ
MultiLineStringZ.
Definition qgis.h:304
@ MultiPolygonZ
MultiPolygonZ.
Definition qgis.h:305
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:68
void setId(QgsFeatureId id)
Sets the feature id for this feature.
QgsGeometry geometry
Definition qgsfeature.h:71
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:55
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:565
@ UnknownCount
Provider returned an unknown feature count.
Definition qgis.h:566
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:7145
QString qgsEnumValueToKey(const T &value, bool *returnOk=nullptr)
Returns the value for the given key of an enum.
Definition qgis.h:7126
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:63
#define QgsDebugError(str)
Definition qgslogger.h:59
#define QgsSetRequestInitiatorClass(request, _class)