1 /***************************************************************************
2     qgsoapifprovider.cpp
3     ---------------------
4     begin                : October 2019
5     copyright            : (C) 2019 by Even Rouault
6     email                : even.rouault at spatialys.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 
16 #include "qgsexpressionnodeimpl.h"
17 #include "qgslogger.h"
18 #include "qgsmessagelog.h"
19 #include "qgsoapifprovider.h"
20 #include "qgsoapiflandingpagerequest.h"
21 #include "qgsoapifapirequest.h"
22 #include "qgsoapifcollection.h"
23 #include "qgsoapifitemsrequest.h"
24 #include "qgswfsconstants.h"
25 #include "qgswfsutils.h" // for isCompatibleType()
26 
27 #include <algorithm>
28 
29 const QString QgsOapifProvider::OAPIF_PROVIDER_KEY = QStringLiteral( "OAPIF" );
30 const QString QgsOapifProvider::OAPIF_PROVIDER_DESCRIPTION = QStringLiteral( "OGC API - Features data provider" );
31 
32 
QgsOapifProvider(const QString & uri,const ProviderOptions & options,QgsDataProvider::ReadFlags flags)33 QgsOapifProvider::QgsOapifProvider( const QString &uri, const ProviderOptions &options, QgsDataProvider::ReadFlags flags )
34   : QgsVectorDataProvider( uri, options, flags ),
35     mShared( new QgsOapifSharedData( uri ) )
36 {
37 
38   connect( mShared.get(), &QgsOapifSharedData::raiseError, this, &QgsOapifProvider::pushErrorSlot );
39   connect( mShared.get(), &QgsOapifSharedData::extentUpdated, this, &QgsOapifProvider::fullExtentCalculated );
40 
41   if ( uri.isEmpty() )
42   {
43     mValid = false;
44     return;
45   }
46 
47   mShared->mSourceCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( QStringLiteral( "EPSG:4326" ) );
48 
49   mSubsetString = mShared->mURI.filter();
50 
51   if ( !init() )
52   {
53     mValid = false;
54     return;
55   }
56 
57   QString errorMsg;
58   if ( !mShared->computeServerFilter( errorMsg ) )
59   {
60     QgsMessageLog::logMessage( errorMsg, tr( "OAPIF" ) );
61     mValid = false;
62     return;
63   }
64 }
65 
~QgsOapifProvider()66 QgsOapifProvider::~QgsOapifProvider()
67 {
68 }
69 
init()70 bool QgsOapifProvider::init()
71 {
72   const bool synchronous = true;
73   const bool forceRefresh = false;
74 
75   const QString url = QgsDataSourceUri( mShared->mURI.uri() ).param( QgsWFSConstants::URI_PARAM_URL );
76   const int pos = url.indexOf( '?' );
77   if ( pos >= 0 )
78   {
79     mShared->mExtraQueryParameters = url.mid( pos + 1 );
80   }
81 
82   QgsOapifLandingPageRequest landingPageRequest( mShared->mURI.uri() );
83   if ( !landingPageRequest.request( synchronous, forceRefresh ) )
84     return false;
85   if ( landingPageRequest.errorCode() != QgsBaseNetworkRequest::NoError )
86     return false;
87 
88   QgsOapifApiRequest apiRequest( mShared->mURI.uri(), mShared->appendExtraQueryParameters( landingPageRequest.apiUrl() ) );
89   if ( !apiRequest.request( synchronous, forceRefresh ) )
90     return false;
91   if ( apiRequest.errorCode() != QgsBaseNetworkRequest::NoError )
92     return false;
93 
94   mShared->mServerMaxFeatures = apiRequest.maxLimit();
95 
96   if ( mShared->mURI.maxNumFeatures() > 0 && mShared->mServerMaxFeatures > 0 && !mShared->mURI.pagingEnabled() )
97   {
98     mShared->mMaxFeatures = std::min( mShared->mURI.maxNumFeatures(), mShared->mServerMaxFeatures );
99   }
100   else if ( mShared->mURI.maxNumFeatures() > 0 )
101   {
102     mShared->mMaxFeatures = mShared->mURI.maxNumFeatures();
103   }
104   else if ( mShared->mServerMaxFeatures > 0 && !mShared->mURI.pagingEnabled() )
105   {
106     mShared->mMaxFeatures = mShared->mServerMaxFeatures;
107   }
108 
109   if ( mShared->mURI.pagingEnabled() && mShared->mURI.pageSize() > 0 )
110   {
111     if ( mShared->mServerMaxFeatures > 0 )
112     {
113       mShared->mPageSize = std::min( mShared->mURI.pageSize(), mShared->mServerMaxFeatures );
114     }
115     else
116     {
117       mShared->mPageSize = mShared->mURI.pageSize();
118     }
119   }
120   else if ( mShared->mURI.pagingEnabled() )
121   {
122     if ( apiRequest.defaultLimit() > 0 && apiRequest.maxLimit() > 0 )
123     {
124       // Use the default, but if it is below 1000, aim for 1000
125       // but clamp to the maximum limit
126       mShared->mPageSize = std::min( std::max( 1000, apiRequest.defaultLimit() ), apiRequest.maxLimit() );
127     }
128     else if ( apiRequest.defaultLimit() > 0 )
129       mShared->mPageSize = std::max( 1000, apiRequest.defaultLimit() );
130     else if ( apiRequest.maxLimit() > 0 )
131       mShared->mPageSize = apiRequest.maxLimit();
132     else
133       mShared->mPageSize = 100; // fallback to arbitrary page size
134   }
135 
136   mShared->mCollectionUrl =
137     landingPageRequest.collectionsUrl() + QStringLiteral( "/" ) + mShared->mURI.typeName();
138   QgsOapifCollectionRequest collectionRequest( mShared->mURI.uri(), mShared->appendExtraQueryParameters( mShared->mCollectionUrl ) );
139   if ( !collectionRequest.request( synchronous, forceRefresh ) )
140     return false;
141   if ( collectionRequest.errorCode() != QgsBaseNetworkRequest::NoError )
142     return false;
143 
144   mShared->mCapabilityExtent = collectionRequest.collection().mBbox;
145 
146   mLayerMetadata = collectionRequest.collection().mLayerMetadata;
147   // Merge contact info from /api
148   mLayerMetadata.setContacts( apiRequest.metadata().contacts() );
149 
150   mShared->mItemsUrl = mShared->mCollectionUrl +  QStringLiteral( "/items" );
151 
152   QgsOapifItemsRequest itemsRequest( mShared->mURI.uri(), mShared->appendExtraQueryParameters( mShared->mItemsUrl + QStringLiteral( "?limit=10" ) ) );
153   if ( mShared->mCapabilityExtent.isNull() )
154   {
155     itemsRequest.setComputeBbox();
156   }
157   if ( !itemsRequest.request( synchronous, forceRefresh ) )
158     return false;
159   if ( itemsRequest.errorCode() != QgsBaseNetworkRequest::NoError )
160     return false;
161 
162   if ( itemsRequest.numberMatched() >= 0 )
163   {
164     mShared->mHasNumberMatched = true;
165     if ( mSubsetString.isEmpty() )
166     {
167       mShared->setFeatureCount( itemsRequest.numberMatched(), true );
168     }
169   }
170 
171   if ( mShared->mCapabilityExtent.isNull() )
172   {
173     mShared->mCapabilityExtent = itemsRequest.bbox();
174   }
175 
176   mShared->mFields = itemsRequest.fields();
177   mShared->mWKBType = itemsRequest.wkbType();
178 
179   return true;
180 }
181 
pushErrorSlot(const QString & errorMsg)182 void QgsOapifProvider::pushErrorSlot( const QString &errorMsg )
183 {
184   pushError( errorMsg );
185 }
186 
featureSource() const187 QgsAbstractFeatureSource *QgsOapifProvider::featureSource() const
188 {
189   return new QgsBackgroundCachedFeatureSource( mShared );
190 }
191 
getFeatures(const QgsFeatureRequest & request) const192 QgsFeatureIterator QgsOapifProvider::getFeatures( const QgsFeatureRequest &request ) const
193 {
194   return QgsFeatureIterator( new QgsBackgroundCachedFeatureIterator( new QgsBackgroundCachedFeatureSource( mShared ), true, mShared, request ) );
195 }
196 
wkbType() const197 QgsWkbTypes::Type QgsOapifProvider::wkbType() const
198 {
199   return mShared->mWKBType;
200 }
201 
featureCount() const202 long long QgsOapifProvider::featureCount() const
203 {
204   if ( mUpdateFeatureCountAtNextFeatureCountRequest )
205   {
206     mUpdateFeatureCountAtNextFeatureCountRequest = false;
207 
208     QgsFeature f;
209     QgsFeatureRequest request;
210     request.setNoAttributes();
211     auto iter = getFeatures( request );
212     long long count = 0;
213     bool countExact = true;
214     while ( iter.nextFeature( f ) )
215     {
216       if ( count == 1000 ) // to avoid too long processing time
217       {
218         countExact = false;
219         break;
220       }
221       count ++;
222     }
223 
224     mShared->setFeatureCount( count, countExact );
225   }
226   return mShared->getFeatureCount();
227 }
228 
fields() const229 QgsFields QgsOapifProvider::fields() const
230 {
231   return mShared->mFields;
232 }
233 
crs() const234 QgsCoordinateReferenceSystem QgsOapifProvider::crs() const
235 {
236   return mShared->mSourceCrs;
237 }
238 
extent() const239 QgsRectangle QgsOapifProvider::extent() const
240 {
241   return mShared->consolidatedExtent();
242 }
243 
reloadProviderData()244 void QgsOapifProvider::reloadProviderData()
245 {
246   mUpdateFeatureCountAtNextFeatureCountRequest = true;
247   mShared->invalidateCache();
248 }
249 
isValid() const250 bool QgsOapifProvider::isValid() const
251 {
252   return mValid;
253 }
254 
capabilities() const255 QgsVectorDataProvider::Capabilities QgsOapifProvider::capabilities() const
256 {
257   return QgsVectorDataProvider::SelectAtId | QgsVectorDataProvider::ReadLayerMetadata | QgsVectorDataProvider::Capability::ReloadData;
258 }
259 
empty() const260 bool QgsOapifProvider::empty() const
261 {
262   if ( subsetString().isEmpty() && mShared->isFeatureCountExact() )
263   {
264     return mShared->getFeatureCount( false ) == 0;
265   }
266 
267   QgsFeature f;
268   QgsFeatureRequest request;
269   request.setNoAttributes();
270   request.setFlags( QgsFeatureRequest::NoGeometry );
271 
272   // Whoops, the provider returns an empty iterator when we are using
273   // a setLimit call in combination with a subsetString.
274   // Remove this method (and default to the QgsVectorDataProvider one)
275   // once this is fixed
276 #if 0
277   request.setLimit( 1 );
278 #endif
279   return !getFeatures( request ).nextFeature( f );
280 
281 };
282 
setSubsetString(const QString & filter,bool updateFeatureCount)283 bool QgsOapifProvider::setSubsetString( const QString &filter, bool updateFeatureCount )
284 {
285   QgsDebugMsgLevel( QStringLiteral( "filter = '%1'" ).arg( filter ), 4 );
286 
287   if ( filter == mSubsetString )
288     return true;
289 
290   if ( !filter.isEmpty() )
291   {
292     const QgsExpression filterExpression( filter );
293     if ( !filterExpression.isValid() )
294     {
295       QgsMessageLog::logMessage( filterExpression.parserErrorString(), tr( "OAPIF" ) );
296       return false;
297     }
298   }
299 
300   // Invalid and cancel current download before altering fields, etc...
301   // (crashes might happen if not done at the beginning)
302   mShared->invalidateCache();
303 
304   mSubsetString = filter;
305   clearMinMaxCache();
306 
307   // update URI
308   mShared->mURI.setFilter( filter );
309   setDataSourceUri( mShared->mURI.uri() );
310   QString errorMsg;
311   if ( !mShared->computeServerFilter( errorMsg ) )
312     QgsMessageLog::logMessage( errorMsg, tr( "OAPIF" ) );
313 
314 
315   if ( updateFeatureCount )
316   {
317     reloadData();
318   }
319   else
320   {
321     mShared->invalidateCache();
322     emit dataChanged();
323   }
324 
325   return true;
326 }
327 
filterTranslatedState() const328 QgsOapifProvider::FilterTranslationState QgsOapifProvider::filterTranslatedState() const
329 {
330   return mShared->mFilterTranslationState;
331 }
332 
clientSideFilterExpression() const333 const QString &QgsOapifProvider::clientSideFilterExpression() const
334 {
335   return mShared->mClientSideFilterExpression;
336 }
337 
name() const338 QString QgsOapifProvider::name() const
339 {
340   return OAPIF_PROVIDER_KEY;
341 }
342 
providerKey()343 QString QgsOapifProvider::providerKey()
344 {
345   return OAPIF_PROVIDER_KEY;
346 }
347 
description() const348 QString QgsOapifProvider::description() const
349 {
350   return OAPIF_PROVIDER_DESCRIPTION;
351 }
352 
353 // ---------------------------------
354 
QgsOapifSharedData(const QString & uri)355 QgsOapifSharedData::QgsOapifSharedData( const QString &uri )
356   : QgsBackgroundCachedSharedData( "oapif", tr( "OAPIF" ) )
357   , mURI( uri )
358 {
359   mHideProgressDialog = mURI.hideDownloadProgressDialog();
360 }
361 
~QgsOapifSharedData()362 QgsOapifSharedData::~QgsOapifSharedData()
363 {
364   QgsDebugMsgLevel( QStringLiteral( "~QgsOapifSharedData()" ), 4 );
365 
366   cleanup();
367 }
368 
appendExtraQueryParameters(const QString & url) const369 QString QgsOapifSharedData::appendExtraQueryParameters( const QString &url ) const
370 {
371   if ( mExtraQueryParameters.isEmpty() || url.indexOf( mExtraQueryParameters ) > 0 )
372     return url;
373   const int nPos = url.indexOf( '?' );
374   if ( nPos < 0 )
375     return url + '?' + mExtraQueryParameters;
376   return url + '&' + mExtraQueryParameters;
377 }
378 
isRestrictedToRequestBBOX() const379 bool QgsOapifSharedData::isRestrictedToRequestBBOX() const
380 {
381   return mURI.isRestrictedToRequestBBOX();
382 }
383 
384 
newFeatureDownloaderImpl(QgsFeatureDownloader * downloader,bool requestMadeFromMainThread)385 std::unique_ptr<QgsFeatureDownloaderImpl> QgsOapifSharedData::newFeatureDownloaderImpl( QgsFeatureDownloader *downloader, bool requestMadeFromMainThread )
386 {
387   return std::unique_ptr<QgsFeatureDownloaderImpl>( new QgsOapifFeatureDownloaderImpl( this, downloader, requestMadeFromMainThread ) );
388 }
389 
390 
invalidateCacheBaseUnderLock()391 void QgsOapifSharedData::invalidateCacheBaseUnderLock()
392 {
393 }
394 
getDateTimeValue(const QVariant & v)395 static QDateTime getDateTimeValue( const QVariant &v )
396 {
397   if ( v.type() == QVariant::String )
398     return QDateTime::fromString( v.toString(), Qt::ISODateWithMs );
399   else if ( v.type() == QVariant::DateTime )
400     return v.toDateTime();
401   return QDateTime();
402 }
403 
isDateTime(const QVariant & v)404 static bool isDateTime( const QVariant &v )
405 {
406   return getDateTimeValue( v ).isValid();
407 }
408 
getDateTimeValueAsString(const QVariant & v)409 static QString getDateTimeValueAsString( const QVariant &v )
410 {
411   if ( v.type() == QVariant::String )
412     return v.toString();
413   else if ( v.type() == QVariant::DateTime )
414     return v.toDateTime().toString( Qt::ISODateWithMs );
415   return QString();
416 }
417 
isDateTimeField(const QgsFields & fields,const QString & fieldName)418 static bool isDateTimeField( const QgsFields &fields, const QString &fieldName )
419 {
420   const int idx = fields.indexOf( fieldName );
421   if ( idx >= 0 )
422   {
423     const auto type = fields.at( idx ).type();
424     return type == QVariant::DateTime || type == QVariant::Date;
425   }
426   return false;
427 }
428 
collectTopLevelAndNodes(const QgsExpressionNode * node,std::vector<const QgsExpressionNode * > & topAndNodes)429 static void collectTopLevelAndNodes( const QgsExpressionNode *node,
430                                      std::vector<const QgsExpressionNode *> &topAndNodes )
431 {
432   if ( node->nodeType() == QgsExpressionNode::ntBinaryOperator )
433   {
434     const auto binNode = static_cast<const QgsExpressionNodeBinaryOperator *>( node );
435     const auto op = binNode->op();
436     if ( op == QgsExpressionNodeBinaryOperator::boAnd )
437     {
438       collectTopLevelAndNodes( binNode->opLeft(), topAndNodes );
439       collectTopLevelAndNodes( binNode->opRight(), topAndNodes );
440       return;
441     }
442   }
443   topAndNodes.push_back( node );
444 }
445 
translateNodeToServer(const QgsExpressionNode * rootNode,QgsOapifProvider::FilterTranslationState & translationState,QString & untranslatedPart)446 QString QgsOapifSharedData::translateNodeToServer(
447   const QgsExpressionNode *rootNode,
448   QgsOapifProvider::FilterTranslationState &translationState,
449   QString &untranslatedPart )
450 {
451   std::vector<const QgsExpressionNode *> topAndNodes;
452   collectTopLevelAndNodes( rootNode, topAndNodes );
453   QDateTime minDate;
454   QDateTime maxDate;
455   QString minDateStr;
456   QString maxDateStr;
457   bool hasTranslatedParts = false;
458   for ( size_t i = 0; i < topAndNodes.size(); /* do not increment here */ )
459   {
460     bool removeMe = false;
461     const auto node = topAndNodes[i];
462     if ( node->nodeType() == QgsExpressionNode::ntBinaryOperator )
463     {
464       const auto binNode = static_cast<const QgsExpressionNodeBinaryOperator *>( node );
465       const auto op = binNode->op();
466       if ( binNode->opLeft()->nodeType() == QgsExpressionNode::ntColumnRef &&
467            binNode->opRight()->nodeType() == QgsExpressionNode::ntLiteral )
468       {
469         const auto left = static_cast<const QgsExpressionNodeColumnRef *>( binNode->opLeft() );
470         const auto right = static_cast<const QgsExpressionNodeLiteral *>( binNode->opRight() );
471         if ( isDateTimeField( mFields, left->name() ) &&
472              isDateTime( right->value() ) )
473         {
474           if ( op == QgsExpressionNodeBinaryOperator::boGE ||
475                op == QgsExpressionNodeBinaryOperator::boGT ||
476                op == QgsExpressionNodeBinaryOperator::boEQ )
477           {
478             removeMe = true;
479             if ( !minDate.isValid() || getDateTimeValue( right->value() ) > minDate )
480             {
481               minDate = getDateTimeValue( right->value() );
482               minDateStr = getDateTimeValueAsString( right->value() );
483             }
484           }
485           if ( op == QgsExpressionNodeBinaryOperator::boLE ||
486                op == QgsExpressionNodeBinaryOperator::boLT ||
487                op == QgsExpressionNodeBinaryOperator::boEQ )
488           {
489             removeMe = true;
490             if ( !maxDate.isValid() || getDateTimeValue( right->value() ) < maxDate )
491             {
492               maxDate = getDateTimeValue( right->value() );
493               maxDateStr = getDateTimeValueAsString( right->value() );
494             }
495           }
496         }
497       }
498     }
499     if ( removeMe )
500     {
501       hasTranslatedParts = true;
502       topAndNodes.erase( topAndNodes.begin() + i );
503     }
504     else
505       ++i;
506   }
507 
508   QString ret;
509   if ( minDate.isValid() && maxDate.isValid() )
510   {
511     if ( minDate == maxDate )
512     {
513       ret = QStringLiteral( "datetime=" ) + minDateStr;
514     }
515     else
516     {
517       ret = QStringLiteral( "datetime=" ) + minDateStr + QStringLiteral( "%2F" ) + maxDateStr;
518     }
519   }
520   else if ( minDate.isValid() )
521   {
522     // TODO: use ellipsis '..' instead of dummy upper bound once more servers are compliant
523     ret = QStringLiteral( "datetime=" ) + minDateStr + QStringLiteral( "%2F9999-12-31T00:00:00Z" );
524   }
525   else if ( maxDate.isValid() )
526   {
527     // TODO: use ellipsis '..' instead of dummy upper bound once more servers are compliant
528     ret = QStringLiteral( "datetime=0000-01-01T00:00:00Z%2F" ) + maxDateStr;
529   }
530 
531   if ( !hasTranslatedParts )
532   {
533     untranslatedPart = rootNode->dump();
534     translationState = QgsOapifProvider::FilterTranslationState::FULLY_CLIENT;
535   }
536   else if ( topAndNodes.empty() )
537   {
538     untranslatedPart.clear();
539     translationState = QgsOapifProvider::FilterTranslationState::FULLY_SERVER;
540   }
541   else
542   {
543     translationState = QgsOapifProvider::FilterTranslationState::PARTIAL;
544 
545     // Collect part(s) of the filter to be evaluated on client-side
546     if ( topAndNodes.size() == 1 )
547     {
548       untranslatedPart = topAndNodes[0]->dump();
549     }
550     else
551     {
552       for ( size_t i = 0; i < topAndNodes.size(); ++i )
553       {
554         if ( i == 0 )
555           untranslatedPart = QStringLiteral( "(" );
556         else
557           untranslatedPart += QLatin1String( " AND (" );
558         untranslatedPart += topAndNodes[i]->dump();
559         untranslatedPart += QLatin1Char( ')' );
560       }
561     }
562   }
563 
564   return ret;
565 }
566 
computeServerFilter(QString & errorMsg)567 bool QgsOapifSharedData::computeServerFilter( QString &errorMsg )
568 {
569   errorMsg.clear();
570   mClientSideFilterExpression = mURI.filter();
571   mServerFilter.clear();
572   if ( mClientSideFilterExpression.isEmpty() )
573   {
574     mFilterTranslationState = QgsOapifProvider::FilterTranslationState::FULLY_SERVER;
575     return true;
576   }
577 
578   const QgsExpression expr( mClientSideFilterExpression );
579   const auto rootNode = expr.rootNode();
580   if ( !rootNode )
581     return false;
582   mServerFilter = translateNodeToServer( rootNode, mFilterTranslationState, mClientSideFilterExpression );
583   if ( mFilterTranslationState == QgsOapifProvider::FilterTranslationState::PARTIAL )
584   {
585     QgsDebugMsg( QStringLiteral( "Part of the filter will be evaluated on client-side: %1" ).arg( mClientSideFilterExpression ) );
586   }
587   else if ( mFilterTranslationState == QgsOapifProvider::FilterTranslationState::FULLY_CLIENT )
588   {
589     QgsDebugMsg( "Whole filter will be evaluated on client-side" );
590   }
591 
592   return true;
593 }
594 
pushError(const QString & errorMsg) const595 void QgsOapifSharedData::pushError( const QString &errorMsg ) const
596 {
597   QgsMessageLog::logMessage( errorMsg, tr( "OAPIF" ) );
598   emit raiseError( errorMsg );
599 }
600 
601 // ---------------------------------
602 
QgsOapifFeatureDownloaderImpl(QgsOapifSharedData * shared,QgsFeatureDownloader * downloader,bool requestMadeFromMainThread)603 QgsOapifFeatureDownloaderImpl::QgsOapifFeatureDownloaderImpl( QgsOapifSharedData *shared, QgsFeatureDownloader *downloader, bool requestMadeFromMainThread ):
604   QgsFeatureDownloaderImpl( shared, downloader ),
605   mShared( shared )
606 {
607   QGS_FEATURE_DOWNLOADER_IMPL_CONNECT_SIGNALS( requestMadeFromMainThread );
608 }
609 
~QgsOapifFeatureDownloaderImpl()610 QgsOapifFeatureDownloaderImpl::~QgsOapifFeatureDownloaderImpl()
611 {
612 }
613 
createProgressDialog()614 void QgsOapifFeatureDownloaderImpl::createProgressDialog()
615 {
616   QgsFeatureDownloaderImpl::createProgressDialog( mNumberMatched );
617   CONNECT_PROGRESS_DIALOG( QgsOapifFeatureDownloaderImpl );
618 }
619 
run(bool serializeFeatures,long long maxFeatures)620 void QgsOapifFeatureDownloaderImpl::run( bool serializeFeatures, long long maxFeatures )
621 {
622   QEventLoop loop;
623   connect( this, &QgsOapifFeatureDownloaderImpl::doStop, &loop, &QEventLoop::quit );
624 
625   const bool useProgressDialog = ( !mShared->mHideProgressDialog && maxFeatures != 1 );
626 
627   long long maxTotalFeatures = 0;
628   if ( maxFeatures > 0 && mShared->mMaxFeatures > 0 )
629   {
630     maxTotalFeatures = std::min( maxFeatures, mShared->mMaxFeatures );
631   }
632   else if ( maxFeatures > 0 )
633   {
634     maxTotalFeatures = maxFeatures;
635   }
636   else
637   {
638     maxTotalFeatures = mShared->mMaxFeatures;
639   }
640 
641   long long totalDownloadedFeatureCount = 0;
642   bool interrupted = false;
643   bool success = true;
644   QString errorMessage;
645   QString url;
646 
647   long long maxFeaturesThisRequest = maxTotalFeatures;
648   if ( mShared->mPageSize > 0 )
649   {
650     if ( maxFeaturesThisRequest > 0 )
651     {
652       maxFeaturesThisRequest = std::min( maxFeaturesThisRequest, mShared->mPageSize );
653     }
654     else
655     {
656       maxFeaturesThisRequest = mShared->mPageSize;
657     }
658   }
659   url = mShared->mItemsUrl;
660   bool hasQueryParam = false;
661   if ( maxFeaturesThisRequest > 0 )
662   {
663     url += QStringLiteral( "?limit=%1" ).arg( maxFeaturesThisRequest );
664     hasQueryParam = true;
665   }
666 
667   if ( !mShared->mServerFilter.isEmpty() )
668   {
669     url += ( hasQueryParam ? QStringLiteral( "&" ) : QStringLiteral( "?" ) );
670     url += mShared->mServerFilter;
671     hasQueryParam = true;
672   }
673 
674   const QgsRectangle &rect = mShared->currentRect();
675   if ( !rect.isNull() )
676   {
677     // Clamp to avoid server errors.
678     const double minx = std::max( -180.0, rect.xMinimum() );
679     const double miny = std::max( -90.0, rect.yMinimum() );
680     const double maxx = std::min( 180.0, rect.xMaximum() );
681     const double maxy = std::min( 90.0, rect.yMaximum() );
682     if ( minx > 180.0 || miny > 90.0 || maxx  < -180.0 || maxy < -90.0 )
683     {
684       // completely out of range. Servers could error out
685       url.clear();
686     }
687     else if ( minx > -180.0 || miny > -90.0 || maxx < 180.0 || maxy < 90.0 )
688     {
689       url += ( hasQueryParam ? QStringLiteral( "&" ) : QStringLiteral( "?" ) );
690       url += QStringLiteral( "bbox=%1,%2,%3,%4" )
691              .arg( qgsDoubleToString( minx ),
692                    qgsDoubleToString( miny ),
693                    qgsDoubleToString( maxx ),
694                    qgsDoubleToString( maxy ) );
695     }
696   }
697 
698   while ( !url.isEmpty() )
699   {
700     url = mShared->appendExtraQueryParameters( url );
701 
702     if ( maxTotalFeatures > 0 && totalDownloadedFeatureCount >= maxTotalFeatures )
703     {
704       break;
705     }
706 
707     QgsOapifItemsRequest itemsRequest( mShared->mURI.uri(), url );
708     connect( &itemsRequest, &QgsOapifItemsRequest::gotResponse, &loop, &QEventLoop::quit );
709     itemsRequest.request( false /* synchronous*/, true /* forceRefresh */ );
710     loop.exec( QEventLoop::ExcludeUserInputEvents );
711     if ( mStop )
712     {
713       interrupted = true;
714       success = false;
715       break;
716     }
717     if ( itemsRequest.errorCode() != QgsBaseNetworkRequest::NoError )
718     {
719       errorMessage = itemsRequest.errorMessage();
720       success = false;
721       break;
722     }
723     if ( itemsRequest.features().empty() )
724     {
725       break;
726     }
727     url = itemsRequest.nextUrl();
728 
729     // Consider if we should display a progress dialog
730     // We can only do that if we know how many features will be downloaded
731     if ( mNumberMatched < 0 && !mTimer && useProgressDialog && itemsRequest.numberMatched() > 0 )
732     {
733       mNumberMatched = itemsRequest.numberMatched();
734       CREATE_PROGRESS_DIALOG( QgsOapifFeatureDownloaderImpl );
735     }
736 
737     totalDownloadedFeatureCount += itemsRequest.features().size();
738     if ( !mStop )
739     {
740       emit updateProgress( totalDownloadedFeatureCount );
741     }
742 
743     QVector<QgsFeatureUniqueIdPair> featureList;
744     size_t i = 0;
745     const QgsFields srcFields = itemsRequest.fields();
746     const QgsFields dstFields = mShared->fields();
747     for ( const auto &pair : itemsRequest.features() )
748     {
749       // In the case the features of the current page have not the same schema
750       // as the layer, convert them
751       const QgsFeature &f = pair.first;
752       QgsFeature dstFeat( dstFields, f.id() );
753       dstFeat.setGeometry( f.geometry() );
754       const auto srcAttrs = f.attributes();
755       for ( int j = 0; j < dstFields.size(); j++ )
756       {
757         const int srcIdx = srcFields.indexOf( dstFields[j].name() );
758         if ( srcIdx >= 0 )
759         {
760           const QVariant &v = srcAttrs.value( srcIdx );
761           const auto dstFieldType = dstFields.at( j ).type();
762           if ( v.isNull() )
763             dstFeat.setAttribute( j, QVariant( dstFieldType ) );
764           else if ( QgsWFSUtils::isCompatibleType( v.type(), dstFieldType ) )
765             dstFeat.setAttribute( j, v );
766           else
767             dstFeat.setAttribute( j, QgsVectorDataProvider::convertValue( dstFieldType, v.toString() ) );
768         }
769       }
770 
771       QString uniqueId( pair.second );
772       if ( uniqueId.isEmpty() )
773       {
774         uniqueId = QgsBackgroundCachedSharedData::getMD5( f );
775       }
776 
777       featureList.push_back( QgsFeatureUniqueIdPair( dstFeat, uniqueId ) );
778 
779       if ( ( i > 0 && ( i % 1000 ) == 0 ) || i + 1 == itemsRequest.features().size() )
780       {
781         // We call it directly to avoid asynchronous signal notification, and
782         // as serializeFeatures() can modify the featureList to remove features
783         // that have already been cached, so as to avoid to notify them several
784         // times to subscribers
785         if ( serializeFeatures )
786           mShared->serializeFeatures( featureList );
787 
788         if ( !featureList.isEmpty() )
789         {
790           emitFeatureReceived( featureList );
791           emitFeatureReceived( featureList.size() );
792         }
793 
794         featureList.clear();
795       }
796       i++;
797     }
798 
799     if ( mShared->mPageSize <= 0 )
800     {
801       break;
802     }
803   }
804 
805   endOfRun( serializeFeatures, success, totalDownloadedFeatureCount, false /* truncatedResponse */, interrupted, errorMessage );
806 }
807 
808 // ---------------------------------
809 
createProvider(const QString & uri,const QgsDataProvider::ProviderOptions & options,QgsDataProvider::ReadFlags flags)810 QgsOapifProvider *QgsOapifProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, QgsDataProvider::ReadFlags flags )
811 {
812   return new QgsOapifProvider( uri, options, flags );
813 }
814 
QgsOapifProviderMetadata()815 QgsOapifProviderMetadata::QgsOapifProviderMetadata():
816   QgsProviderMetadata( QgsOapifProvider::OAPIF_PROVIDER_KEY, QgsOapifProvider::OAPIF_PROVIDER_DESCRIPTION ) {}
817