QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgssensorthingsshareddata.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgssensorthingsshareddata.h
3 ----------------
4 begin : November 2023
5 copyright : (C) 2013 Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
19#include "qgslogger.h"
20#include "qgsreadwritelocker.h"
24#include "qgsjsonutils.h"
25
26#include <QCryptographicHash>
27#include <QFile>
28#include <nlohmann/json.hpp>
29
31
32QgsSensorThingsSharedData::QgsSensorThingsSharedData( const QString &uri )
33{
34 const QVariantMap uriParts = QgsSensorThingsProviderMetadata().decodeUri( uri );
35
36 mEntityType = qgsEnumKeyToValue( uriParts.value( QStringLiteral( "entity" ) ).toString(), Qgis::SensorThingsEntity::Invalid );
37 mFields = QgsSensorThingsUtils::fieldsForEntityType( mEntityType );
38 mGeometryField = QgsSensorThingsUtils::geometryFieldForEntityType( mEntityType );
39 // use initial value of maximum page size as default
40 mMaximumPageSize = uriParts.value( QStringLiteral( "pageSize" ), mMaximumPageSize ).toInt();
41 // will default to 0 if not specified, i.e. no limit
42 mFeatureLimit = uriParts.value( QStringLiteral( "featureLimit" ) ).toInt();
43 mFilterExtent = uriParts.value( QStringLiteral( "bounds" ) ).value< QgsRectangle >();
44 mSubsetString = uriParts.value( QStringLiteral( "sql" ) ).toString();
45
47 {
48 const QString geometryType = uriParts.value( QStringLiteral( "geometryType" ) ).toString();
49 if ( geometryType.compare( QLatin1String( "point" ), Qt::CaseInsensitive ) == 0 )
50 {
51 mGeometryType = Qgis::WkbType::PointZ;
52 }
53 else if ( geometryType.compare( QLatin1String( "multipoint" ), Qt::CaseInsensitive ) == 0 )
54 {
55 mGeometryType = Qgis::WkbType::MultiPointZ;
56 }
57 else if ( geometryType.compare( QLatin1String( "line" ), Qt::CaseInsensitive ) == 0 )
58 {
59 mGeometryType = Qgis::WkbType::MultiLineStringZ;
60 }
61 else if ( geometryType.compare( QLatin1String( "polygon" ), Qt::CaseInsensitive ) == 0 )
62 {
63 mGeometryType = Qgis::WkbType::MultiPolygonZ;
64 }
65 // geometry is always GeoJSON spec (for now, at least), so CRS will always be WGS84
66 mSourceCRS = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) );
67 }
68 else
69 {
70 mGeometryType = Qgis::WkbType::NoGeometry;
71 }
72
73 QgsDataSourceUri dsUri;
74 dsUri.setEncodedUri( uri );
75 mAuthCfg = dsUri.authConfigId();
76 mHeaders = dsUri.httpHeaders();
77
78 mRootUri = uriParts.value( QStringLiteral( "url" ) ).toString();
79}
80
81QUrl QgsSensorThingsSharedData::parseUrl( const QUrl &url, bool *isTestEndpoint )
82{
83 if ( isTestEndpoint )
84 *isTestEndpoint = false;
85
86 QUrl modifiedUrl( url );
87 if ( modifiedUrl.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
88 {
89 if ( isTestEndpoint )
90 *isTestEndpoint = true;
91
92 // Just for testing with local files instead of http:// resources
93 QString modifiedUrlString = modifiedUrl.toString();
94 // Qt5 does URL encoding from some reason (of the FILTER parameter for example)
95 modifiedUrlString = QUrl::fromPercentEncoding( modifiedUrlString.toUtf8() );
96 modifiedUrlString.replace( QLatin1String( "fake_qgis_http_endpoint/" ), QLatin1String( "fake_qgis_http_endpoint_" ) );
97 QgsDebugMsgLevel( QStringLiteral( "Get %1" ).arg( modifiedUrlString ), 2 );
98 modifiedUrlString = modifiedUrlString.mid( QStringLiteral( "http://" ).size() );
99 QString args = modifiedUrlString.indexOf( '?' ) >= 0 ? modifiedUrlString.mid( modifiedUrlString.indexOf( '?' ) ) : QString();
100 if ( modifiedUrlString.size() > 150 )
101 {
102 args = QCryptographicHash::hash( args.toUtf8(), QCryptographicHash::Md5 ).toHex();
103 }
104 else
105 {
106 args.replace( QLatin1String( "?" ), QLatin1String( "_" ) );
107 args.replace( QLatin1String( "&" ), QLatin1String( "_" ) );
108 args.replace( QLatin1String( "$" ), QLatin1String( "_" ) );
109 args.replace( QLatin1String( "<" ), QLatin1String( "_" ) );
110 args.replace( QLatin1String( ">" ), QLatin1String( "_" ) );
111 args.replace( QLatin1String( "'" ), QLatin1String( "_" ) );
112 args.replace( QLatin1String( "\"" ), QLatin1String( "_" ) );
113 args.replace( QLatin1String( " " ), QLatin1String( "_" ) );
114 args.replace( QLatin1String( ":" ), QLatin1String( "_" ) );
115 args.replace( QLatin1String( "/" ), QLatin1String( "_" ) );
116 args.replace( QLatin1String( "\n" ), QLatin1String( "_" ) );
117 }
118#ifdef Q_OS_WIN
119 // Passing "urls" like "http://c:/path" to QUrl 'eats' the : after c,
120 // so we must restore it
121 if ( modifiedUrlString[1] == '/' )
122 {
123 modifiedUrlString = modifiedUrlString[0] + ":/" + modifiedUrlString.mid( 2 );
124 }
125#endif
126 modifiedUrlString = modifiedUrlString.mid( 0, modifiedUrlString.indexOf( '?' ) ) + args;
127 QgsDebugMsgLevel( QStringLiteral( "Get %1 (after laundering)" ).arg( modifiedUrlString ), 2 );
128 modifiedUrl = QUrl::fromLocalFile( modifiedUrlString );
129 if ( !QFile::exists( modifiedUrlString ) )
130 {
131 QgsDebugError( QStringLiteral( "Local test file %1 for URL %2 does not exist!!!" ).arg( modifiedUrlString, url.toString() ) );
132 }
133 }
134
135 return modifiedUrl;
136}
137
138QgsRectangle QgsSensorThingsSharedData::extent() const
139{
140 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
141
142 // Since we can't retrieve the actual layer extent via SensorThings API, we use a pessimistic
143 // global extent until we've retrieved all the features from the layer
144 return hasCachedAllFeatures() ? mFetchedFeatureExtent
145 : ( !mFilterExtent.isNull() ? mFilterExtent : QgsRectangle( -180, -90, 180, 90 ) );
146}
147
148long long QgsSensorThingsSharedData::featureCount( QgsFeedback *feedback ) const
149{
150 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
151 if ( mFeatureCount >= 0 )
152 return mFeatureCount;
153
154 locker.changeMode( QgsReadWriteLocker::Write );
155 mError.clear();
156
157 // return no features, just the total count
158 QString countUri = QStringLiteral( "%1?$top=0&$count=true" ).arg( mEntityBaseUri );
159 const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType );
160 const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, mFilterExtent );
161 QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter, mSubsetString } );
162 if ( !filterString.isEmpty() )
163 filterString = QStringLiteral( "&$filter=" ) + filterString;
164 if ( !filterString.isEmpty() )
165 countUri += filterString;
166
167 const QUrl url = parseUrl( QUrl( countUri ) );
168
169 QNetworkRequest request( url );
170 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsSensorThingsSharedData" ) );
171 mHeaders.updateNetworkRequest( request );
172
173 QgsBlockingNetworkRequest networkRequest;
174 networkRequest.setAuthCfg( mAuthCfg );
175 const QgsBlockingNetworkRequest::ErrorCode error = networkRequest.get( request, false, feedback );
176
177 if ( feedback && feedback->isCanceled() )
178 return mFeatureCount;
179
180 // Handle network errors
182 {
183 QgsDebugError( QStringLiteral( "Network error: %1" ).arg( networkRequest.errorMessage() ) );
184 mError = networkRequest.errorMessage();
185 }
186 else
187 {
188 const QgsNetworkReplyContent content = networkRequest.reply();
189 try
190 {
191 auto rootContent = json::parse( content.content().toStdString() );
192 if ( !rootContent.contains( "@iot.count" ) )
193 {
194 mError = QObject::tr( "No '@iot.count' value in response" );
195 return mFeatureCount;
196 }
197
198 mFeatureCount = rootContent["@iot.count"].get<long long>();
199 if ( mFeatureLimit > 0 && mFeatureCount > mFeatureLimit )
200 mFeatureCount = mFeatureLimit;
201 }
202 catch ( const json::parse_error &ex )
203 {
204 mError = QObject::tr( "Error parsing response: %1" ).arg( ex.what() );
205 }
206 }
207
208 return mFeatureCount;
209}
210
211QString QgsSensorThingsSharedData::subsetString() const
212{
213 return mSubsetString;
214}
215
216bool QgsSensorThingsSharedData::hasCachedAllFeatures() const
217{
218 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
219 return mHasCachedAllFeatures
220 || ( mFeatureCount > 0 && mCachedFeatures.size() == mFeatureCount )
221 || ( mFeatureLimit > 0 && mCachedFeatures.size() >= mFeatureLimit );
222}
223
224bool QgsSensorThingsSharedData::getFeature( QgsFeatureId id, QgsFeature &f, QgsFeedback *feedback )
225{
226 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
227
228 // If cached, return cached feature
229 QMap<QgsFeatureId, QgsFeature>::const_iterator it = mCachedFeatures.constFind( id );
230 if ( it != mCachedFeatures.constEnd() )
231 {
232 f = it.value();
233 return true;
234 }
235
236 if ( hasCachedAllFeatures() )
237 return false; // all features are cached, and we didn't find a match
238
239 bool featureFetched = false;
240
241 if ( mNextPage.isEmpty() )
242 {
243 locker.changeMode( QgsReadWriteLocker::Write );
244
245 int thisPageSize = mMaximumPageSize;
246 if ( mFeatureLimit > 0 && ( mCachedFeatures.size() + thisPageSize ) > mFeatureLimit )
247 thisPageSize = mFeatureLimit - mCachedFeatures.size();
248
249 mNextPage = QStringLiteral( "%1?$top=%2&$count=false" ).arg( mEntityBaseUri ).arg( thisPageSize );
250 const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType );
251 const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, mFilterExtent );
252 const QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter, mSubsetString } );
253 if ( !filterString.isEmpty() )
254 mNextPage += QStringLiteral( "&$filter=" ) + filterString;
255 }
256
257 locker.unlock();
258
259 processFeatureRequest( mNextPage, feedback, [id, &f, &featureFetched]( const QgsFeature & feature )
260 {
261 if ( feature.id() == id )
262 {
263 f = feature;
264 featureFetched = true;
265 // don't break here -- store all the features we retrieved in this page first!
266 }
267 }, [&featureFetched, this]
268 {
269 return !featureFetched && !hasCachedAllFeatures();
270 }, [this]
271 {
272 mNextPage.clear();
273 mHasCachedAllFeatures = true;
274 } );
275
276 return featureFetched;
277}
278
279QgsFeatureIds QgsSensorThingsSharedData::getFeatureIdsInExtent( const QgsRectangle &extent, QgsFeedback *feedback, const QString &thisPage, QString &nextPage, const QgsFeatureIds &alreadyFetchedIds )
280{
281 const QgsRectangle requestExtent = mFilterExtent.isNull() ? extent : extent.intersect( mFilterExtent );
282 const QgsGeometry extentGeom = QgsGeometry::fromRect( requestExtent );
283 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
284
285 if ( hasCachedAllFeatures() || mCachedExtent.contains( extentGeom ) )
286 {
287 // all features cached locally, rely on local spatial index
288 return qgis::listToSet( mSpatialIndex.intersects( requestExtent ) );
289 }
290
291 const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType );
292 const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, requestExtent );
293 QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter, mSubsetString } );
294 if ( !filterString.isEmpty() )
295 filterString = QStringLiteral( "&$filter=" ) + filterString;
296 int thisPageSize = mMaximumPageSize;
297 QString queryUrl;
298 if ( !thisPage.isEmpty() )
299 {
300 queryUrl = thisPage;
301 const thread_local QRegularExpression topRe( QStringLiteral( "\\$top=\\d+" ) );
302 const QRegularExpressionMatch match = topRe.match( queryUrl );
303 if ( match.hasMatch() )
304 {
305 if ( mFeatureLimit > 0 && ( mCachedFeatures.size() + thisPageSize ) > mFeatureLimit )
306 thisPageSize = mFeatureLimit - mCachedFeatures.size();
307 queryUrl = queryUrl.left( match.capturedStart( 0 ) ) + QStringLiteral( "$top=%1" ).arg( thisPageSize ) + queryUrl.mid( match.capturedEnd( 0 ) );
308 }
309 }
310 else
311 {
312 queryUrl = QStringLiteral( "%1?$top=%2&$count=false%3" ).arg( mEntityBaseUri ).arg( thisPageSize ).arg( filterString );
313 }
314
315 if ( thisPage.isEmpty() && mCachedExtent.intersects( extentGeom ) )
316 {
317 // we have SOME of the results from this extent cached. Let's return those first.
318 // This is slightly nicer from a rendering point of view, because panning the map won't see features
319 // previously visible disappear temporarily while we wait for them to be included in the service's result set...
320 nextPage = queryUrl;
321 return qgis::listToSet( mSpatialIndex.intersects( requestExtent ) );
322 }
323
324 locker.unlock();
325
326 QgsFeatureIds ids;
327
328 bool noMoreFeatures = false;
329 bool hasFirstPage = false;
330 const bool res = processFeatureRequest( queryUrl, feedback, [&ids, &alreadyFetchedIds]( const QgsFeature & feature )
331 {
332 if ( !alreadyFetchedIds.contains( feature.id() ) )
333 ids.insert( feature.id() );
334 }, [&hasFirstPage]
335 {
336 if ( !hasFirstPage )
337 {
338 hasFirstPage = true;
339 return true;
340 }
341
342 return false;
343 }, [&noMoreFeatures]
344 {
345 noMoreFeatures = true;
346 } );
347 if ( noMoreFeatures && res && ( !feedback || !feedback->isCanceled() ) )
348 {
349 locker.changeMode( QgsReadWriteLocker::Write );
350 mCachedExtent = QgsGeometry::unaryUnion( { mCachedExtent, extentGeom } );
351 }
352 nextPage = noMoreFeatures || !res ? QString() : queryUrl;
353
354 return ids;
355}
356
357void QgsSensorThingsSharedData::clearCache()
358{
359 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Write );
360
361 mFeatureCount = static_cast< long long >( Qgis::FeatureCountState::Uncounted );
362 mCachedFeatures.clear();
363 mIotIdToFeatureId.clear();
364 mSpatialIndex = QgsSpatialIndex();
365 mFetchedFeatureExtent = QgsRectangle();
366}
367
368bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFeedback *feedback, const std::function< void( const QgsFeature & ) > &fetchedFeatureCallback, const std::function<bool ()> &continueFetchingCallback, const std::function<void ()> &onNoMoreFeaturesCallback )
369{
370 // copy some members before we unlock the read/write locker
371
372 QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
373 const QString authcfg = mAuthCfg;
374 const QgsHttpHeaders headers = mHeaders;
375 const QgsFields fields = mFields;
376
377 while ( continueFetchingCallback() )
378 {
379 // don't lock while doing the fetch
380 locker.unlock();
381
382 // from: https://docs.ogc.org/is/18-088/18-088.html#nextLink
383 // "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"
384 //
385 // ie don't mess with this URL!!
386 const QUrl url = parseUrl( nextPage );
387
388 QNetworkRequest request( url );
389 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsSensorThingsSharedData" ) );
390 headers.updateNetworkRequest( request );
391
392 QgsBlockingNetworkRequest networkRequest;
393 networkRequest.setAuthCfg( authcfg );
394 const QgsBlockingNetworkRequest::ErrorCode error = networkRequest.get( request, false, feedback );
395 if ( feedback && feedback->isCanceled() )
396 {
397 return false;
398 }
399
401 {
402 QgsDebugError( QStringLiteral( "Network error: %1" ).arg( networkRequest.errorMessage() ) );
403 locker.changeMode( QgsReadWriteLocker::Write );
404 mError = networkRequest.errorMessage();
405 QgsDebugMsgLevel( QStringLiteral( "Query returned empty result" ), 2 );
406 return false;
407 }
408 else
409 {
410 const QgsNetworkReplyContent content = networkRequest.reply();
411 try
412 {
413 const auto rootContent = json::parse( content.content().toStdString() );
414 if ( !rootContent.contains( "value" ) )
415 {
416 locker.changeMode( QgsReadWriteLocker::Write );
417 mError = QObject::tr( "No 'value' in response" );
418 QgsDebugMsgLevel( QStringLiteral( "No 'value' in response" ), 2 );
419 return false;
420 }
421 else
422 {
423 // all good, got a batch of features
424 const auto &values = rootContent["value"];
425 if ( values.empty() )
426 {
427 locker.changeMode( QgsReadWriteLocker::Write );
428
429 onNoMoreFeaturesCallback();
430
431 return true;
432 }
433 else
434 {
435 locker.changeMode( QgsReadWriteLocker::Write );
436 for ( const auto &featureData : values )
437 {
438 auto getString = []( const basic_json<> &json, const char *tag ) -> QVariant
439 {
440 if ( !json.contains( tag ) )
441 return QVariant();
442
443 const auto &jObj = json[tag];
444 if ( jObj.is_number_integer() )
445 {
446 return QString::number( jObj.get<int>() );
447 }
448 else if ( jObj.is_number_unsigned() )
449 {
450 return QString::number( jObj.get<unsigned>() );
451 }
452 else if ( jObj.is_boolean() )
453 {
454 return QString::number( jObj.get<bool>() );
455 }
456 else if ( jObj.is_number_float() )
457 {
458 return QString::number( jObj.get<double>() );
459 }
460
461 return QString::fromStdString( json[tag].get<std::string >() );
462 };
463
464 auto getDateTime = []( const basic_json<> &json, const char *tag ) -> QVariant
465 {
466 if ( !json.contains( tag ) )
467 return QVariant();
468
469 const auto &jObj = json[tag];
470 if ( jObj.is_string() )
471 {
472 const QString dateTimeString = QString::fromStdString( json[tag].get<std::string >() );
473 return QDateTime::fromString( dateTimeString, Qt::ISODateWithMs );
474 }
475
476 return QVariant();
477 };
478
479 auto getVariantMap = []( const basic_json<> &json, const char *tag ) -> QVariant
480 {
481 if ( !json.contains( tag ) )
482 return QVariant();
483
484 return QgsJsonUtils::jsonToVariant( json[tag] );
485 };
486
487 auto getStringList = []( const basic_json<> &json, const char *tag ) -> QVariant
488 {
489 if ( !json.contains( tag ) )
490 return QVariant();
491
492 const auto &jObj = json[tag];
493 if ( jObj.is_string() )
494 {
495 return QStringList{ QString::fromStdString( json[tag].get<std::string >() ) };
496 }
497 else if ( jObj.is_array() )
498 {
499 QStringList res;
500 for ( const auto &element : jObj )
501 {
502 if ( element.is_string() )
503 res.append( QString::fromStdString( element.get<std::string >() ) );
504 }
505 return res;
506 }
507
508 return QVariant();
509 };
510
511 auto getDateTimeRange = []( const basic_json<> &json, const char *tag ) -> std::pair< QVariant, QVariant >
512 {
513 if ( !json.contains( tag ) )
514 return { QVariant(), QVariant() };
515
516 const auto &jObj = json[tag];
517 if ( jObj.is_string() )
518 {
519 const QString rangeString = QString::fromStdString( json[tag].get<std::string >() );
520 const QStringList rangeParts = rangeString.split( '/' );
521 if ( rangeParts.size() == 2 )
522 {
523 return
524 {
525 QDateTime::fromString( rangeParts.at( 0 ), Qt::ISODateWithMs ),
526 QDateTime::fromString( rangeParts.at( 1 ), Qt::ISODateWithMs )
527 };
528 }
529 }
530
531 return { QVariant(), QVariant() };
532 };
533
534 // Set attributes
535 const QString iotId = getString( featureData, "@iot.id" ).toString();
536 auto existingFeatureIdIt = mIotIdToFeatureId.constFind( iotId );
537 if ( existingFeatureIdIt != mIotIdToFeatureId.constEnd() )
538 {
539 // we've previously fetched and cached this feature, skip it
540 fetchedFeatureCallback( *mCachedFeatures.find( *existingFeatureIdIt ) );
541 continue;
542 }
543
544 QgsFeature feature( fields );
545 feature.setId( mNextFeatureId++ );
546
547 const QString selfLink = getString( featureData, "@iot.selfLink" ).toString();
548
549 const QVariant properties = getVariantMap( featureData, "properties" );
550 // NOLINTBEGIN(bugprone-branch-clone)
551 switch ( mEntityType )
552 {
554 break;
555
557 feature.setAttributes(
559 << iotId
560 << selfLink
561 << getString( featureData, "name" )
562 << getString( featureData, "description" )
563 << properties
564 );
565 break;
566
568 feature.setAttributes(
570 << iotId
571 << selfLink
572 << getString( featureData, "name" )
573 << getString( featureData, "description" )
574 << properties
575 );
576 break;
577
579 feature.setAttributes(
581 << iotId
582 << selfLink
583 << getDateTime( featureData, "time" )
584 );
585 break;
586
588 {
589 std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( featureData, "phenomenonTime" );
590 std::pair< QVariant, QVariant > resultTime = getDateTimeRange( featureData, "resultTime" );
591 feature.setAttributes(
593 << iotId
594 << selfLink
595 << getString( featureData, "name" )
596 << getString( featureData, "description" )
597 << getVariantMap( featureData, "unitOfMeasurement" )
598 << getString( featureData, "observationType" )
599 << properties
600 << phenomenonTime.first
601 << phenomenonTime.second
602 << resultTime.first
603 << resultTime.second
604 );
605 break;
606 }
607
609 feature.setAttributes(
611 << iotId
612 << selfLink
613 << getString( featureData, "name" )
614 << getString( featureData, "description" )
615 << getString( featureData, "metadata" )
616 << properties
617 );
618 break;
619
621 feature.setAttributes(
623 << iotId
624 << selfLink
625 << getString( featureData, "name" )
626 << getString( featureData, "definition" )
627 << getString( featureData, "description" )
628 << properties
629 );
630 break;
631
633 {
634 std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( featureData, "phenomenonTime" );
635 std::pair< QVariant, QVariant > validTime = getDateTimeRange( featureData, "validTime" );
636 feature.setAttributes(
638 << iotId
639 << selfLink
640 << phenomenonTime.first
641 << phenomenonTime.second
642 << getString( featureData, "result" ) // TODO -- result type handling!
643 << getDateTime( featureData, "resultTime" )
644 << getStringList( featureData, "resultQuality" )
645 << validTime.first
646 << validTime.second
647 << getVariantMap( featureData, "parameters" )
648 );
649 break;
650 }
651
653 feature.setAttributes(
655 << iotId
656 << selfLink
657 << getString( featureData, "name" )
658 << getString( featureData, "description" )
659 << properties
660 );
661 break;
662 }
663 // NOLINTEND(bugprone-branch-clone)
664
665 // Set geometry
666 if ( mGeometryType != Qgis::WkbType::NoGeometry )
667 {
668 if ( featureData.contains( mGeometryField.toLocal8Bit().constData() ) )
669 {
670 const auto &geometryPart = featureData[mGeometryField.toLocal8Bit().constData()];
671 if ( geometryPart.contains( "geometry" ) )
672 feature.setGeometry( QgsJsonUtils::geometryFromGeoJson( geometryPart["geometry"] ) );
673 else
674 feature.setGeometry( QgsJsonUtils::geometryFromGeoJson( geometryPart ) );
675 }
676 }
677
678 mCachedFeatures.insert( feature.id(), feature );
679 mIotIdToFeatureId.insert( iotId, feature.id() );
680 mSpatialIndex.addFeature( feature );
681 mFetchedFeatureExtent.combineExtentWith( feature.geometry().boundingBox() );
682
683 fetchedFeatureCallback( feature );
684
685 if ( mFeatureLimit > 0 && mFeatureLimit <= mCachedFeatures.size() )
686 break;
687 }
688 locker.unlock();
689
690 if ( rootContent.contains( "@iot.nextLink" ) && ( mFeatureLimit == 0 || mFeatureLimit > mCachedFeatures.size() ) )
691 {
692 nextPage = QString::fromStdString( rootContent["@iot.nextLink"].get<std::string>() );
693 }
694 else
695 {
696 onNoMoreFeaturesCallback();
697 }
698
699 // if target feature was added to cache, return it
700 if ( !continueFetchingCallback() )
701 {
702 return true;
703 }
704 }
705 }
706 }
707 catch ( const json::parse_error &ex )
708 {
709 locker.changeMode( QgsReadWriteLocker::Write );
710 mError = QObject::tr( "Error parsing response: %1" ).arg( ex.what() );
711 QgsDebugMsgLevel( QStringLiteral( "Error parsing response: %1" ).arg( ex.what() ), 2 );
712 return false;
713 }
714 }
715 }
716 return false;
717}
718
@ Sensor
A Sensor is an instrument that observes a property or phenomenon with the goal of producing an estima...
@ ObservedProperty
An ObservedProperty specifies the phenomenon of an Observation.
@ Invalid
An invalid/unknown entity.
@ FeatureOfInterest
In the context of the Internet of Things, many Observations’ FeatureOfInterest can be the Location of...
@ Datastream
A Datastream groups a collection of Observations measuring the same ObservedProperty and produced by ...
@ Observation
An Observation is the act of measuring or otherwise determining the value of a property.
@ Location
A Location entity locates the Thing or the Things it associated with. A Thing’s Location entity is de...
@ Thing
A Thing is an object of the physical world (physical things) or the information world (virtual things...
@ HistoricalLocation
A Thing’s HistoricalLocation entity set provides the times of the current (i.e., last known) and prev...
@ MultiPointZ
MultiPointZ.
@ NoGeometry
No geometry.
@ PointZ
PointZ.
@ MultiLineStringZ
MultiLineStringZ.
@ MultiPolygonZ
MultiPolygonZ.
A vector of attributes.
Definition: qgsattributes.h:59
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr)
Performs a "get" operation on the specified request.
void setAuthCfg(const QString &authCfg)
Sets the authentication config id which should be used during the request.
QString errorMessage() const
Returns the error message string, after a get(), post(), head() or put() request has been made.
@ NoError
No error was encountered.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get(), post(), head() or put() request has been mad...
This class represents a coordinate reference system (CRS).
Class for storing the component parts of a RDBMS data source URI (e.g.
void setEncodedUri(const QByteArray &uri)
Sets the complete encoded uri.
QgsHttpHeaders httpHeaders() const
Returns http headers.
QString authConfigId() const
Returns any associated authentication configuration ID stored in the URI.
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition: qgsfeature.h:56
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
Definition: qgsfeature.cpp:160
void setId(QgsFeatureId id)
Sets the feature id for this feature.
Definition: qgsfeature.cpp:122
QgsGeometry geometry
Definition: qgsfeature.h:67
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
Definition: qgsfeature.cpp:167
Q_GADGET QgsFeatureId id
Definition: qgsfeature.h:64
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:45
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:162
static QgsGeometry fromRect(const QgsRectangle &rect)
Creates a new geometry from a QgsRectangle.
static QgsGeometry unaryUnion(const QVector< QgsGeometry > &geometries, const QgsGeometryParameters &parameters=QgsGeometryParameters())
Compute the unary union on a list of geometries.
QgsRectangle boundingBox() const
Returns the bounding box of the geometry.
This class implements simple http header management.
bool updateNetworkRequest(QNetworkRequest &request) const
Updates a request by adding all the HTTP headers.
static QgsGeometry geometryFromGeoJson(const json &geometry)
Parses a GeoJSON "geometry" value to a QgsGeometry object.
static QVariant jsonToVariant(const json &value)
Converts a JSON value to a QVariant, in case of parsing error an invalid QVariant is returned.
Encapsulates a network reply within a container which is inexpensive to copy and safe to pass between...
QByteArray content() const
Returns the reply content.
The QgsReadWriteLocker class is a convenience class that simplifies locking and unlocking QReadWriteL...
@ Write
Lock for write.
@ Read
Lock for read.
A rectangle specified with double values.
Definition: qgsrectangle.h:42
bool isNull() const
Test if the rectangle is null (holding no spatial information).
Definition: qgsrectangle.h:505
QgsRectangle intersect(const QgsRectangle &rect) const
Returns the intersection with the given rectangle.
Definition: qgsrectangle.h:355
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 bool entityTypeHasGeometry(Qgis::SensorThingsEntity type)
Returns true if the specified entity type can have geometry attached.
static QString geometryFieldForEntityType(Qgis::SensorThingsEntity type)
Returns the geometry field for a specified entity type.
static QgsFields fieldsForEntityType(Qgis::SensorThingsEntity type)
Returns the fields which correspond to 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.
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:5417
QSet< QgsFeatureId > QgsFeatureIds
Definition: qgsfeatureid.h:37
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
Definition: qgsfeatureid.h:28
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugError(str)
Definition: qgslogger.h:38
#define QgsSetRequestInitiatorClass(request, _class)
QgsSQLStatement::Node * parse(const QString &str, QString &parserErrorMsg, bool allowFragments)