QGIS API Documentation 4.1.0-Master (3b8ef1f72a3)
Loading...
Searching...
No Matches
qgsarcgisrestquery.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsarcgisrestquery.cpp
3 ----------------------
4 begin : December 2020
5 copyright : (C) 2020 by 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
16#include "qgsarcgisrestquery.h"
17
18#include "qgsapplication.h"
19#include "qgsarcgisrestutils.h"
20#include "qgsauthmanager.h"
22#include "qgslogger.h"
23#include "qgsmessagelog.h"
26#include "qgsvariantutils.h"
27
28#include <QCryptographicHash>
29#include <QFile>
30#include <QImageReader>
31#include <QJsonParseError>
32#include <QRegularExpression>
33#include <QString>
34#include <QUrl>
35#include <QUrlQuery>
36
37#include "moc_qgsarcgisrestquery.cpp"
38
39using namespace Qt::StringLiterals;
40
42 const QString &baseurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, const QString &urlPrefix, bool forceRefresh
43)
44{
45 // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer?f=json
46 QUrl queryUrl( baseurl );
47 QUrlQuery query( queryUrl );
48 query.addQueryItem( u"f"_s, u"json"_s );
49 queryUrl.setQuery( query );
50 return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, nullptr, urlPrefix, forceRefresh );
51}
52
53QVariantMap QgsArcGisRestQueryUtils::getLayerInfo( const QString &layerurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, const QString &urlPrefix )
54{
55 // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/1?f=json
56 QUrl queryUrl( layerurl );
57 QUrlQuery query( queryUrl );
58 query.addQueryItem( u"f"_s, u"json"_s );
59 queryUrl.setQuery( query );
60 return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, nullptr, urlPrefix );
61}
62
64 const QString &layerurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, const QString &urlPrefix, const QgsRectangle &bbox, const QString &whereClause
65)
66{
67 // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/1/query?where=1%3D1&returnIdsOnly=true&f=json
68 QUrl queryUrl( layerurl + "/query" );
69 QUrlQuery query( queryUrl );
70 query.addQueryItem( u"f"_s, u"json"_s );
71 query.addQueryItem( u"where"_s, whereClause.isEmpty() ? u"1=1"_s : whereClause );
72 query.addQueryItem( u"returnIdsOnly"_s, u"true"_s );
73 if ( !bbox.isNull() )
74 {
75 query.addQueryItem( u"geometry"_s, u"%1,%2,%3,%4"_s.arg( bbox.xMinimum(), 0, 'f', -1 ).arg( bbox.yMinimum(), 0, 'f', -1 ).arg( bbox.xMaximum(), 0, 'f', -1 ).arg( bbox.yMaximum(), 0, 'f', -1 ) );
76 query.addQueryItem( u"geometryType"_s, u"esriGeometryEnvelope"_s );
77 query.addQueryItem( u"spatialRel"_s, u"esriSpatialRelEnvelopeIntersects"_s );
78 }
79 queryUrl.setQuery( query );
80 return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, nullptr, urlPrefix );
81}
82
83QgsRectangle QgsArcGisRestQueryUtils::getExtent( const QString &layerurl, const QString &whereClause, const QString &authcfg, const QgsHttpHeaders &requestHeaders, const QString &urlPrefix )
84{
85 // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/1/query?where=1%3D1&returnExtentOnly=true&f=json
86 QUrl queryUrl( layerurl + "/query" );
87 QUrlQuery query( queryUrl );
88 query.addQueryItem( u"f"_s, u"json"_s );
89 query.addQueryItem( u"where"_s, whereClause );
90 query.addQueryItem( u"returnExtentOnly"_s, u"true"_s );
91 queryUrl.setQuery( query );
92 QString errorTitle;
93 QString errorText;
94 const QVariantMap res = queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, nullptr, urlPrefix );
95 if ( res.isEmpty() )
96 {
97 QgsDebugError( u"getExtent failed: %1 - %2"_s.arg( errorTitle, errorText ) );
98 return QgsRectangle();
99 }
100
101 return QgsArcGisRestUtils::convertRectangle( res.value( u"extent"_s ) );
102}
103
105 const QString &layerurl,
106 const QString &authcfg,
107 const QList<quint32> &objectIds,
108 const QString &crs,
109 bool fetchGeometry,
110 const QStringList &fetchAttributes,
111 bool fetchM,
112 bool fetchZ,
113 QString &errorTitle,
114 QString &errorText,
115 const QgsHttpHeaders &requestHeaders,
116 QgsFeedback *feedback,
117 const QString &urlPrefix
118)
119{
120 QStringList ids;
121 for ( const int id : objectIds )
122 {
123 ids.append( QString::number( id ) );
124 }
125 QUrl queryUrl( layerurl + "/query" );
126 QUrlQuery query( queryUrl );
127 query.addQueryItem( u"f"_s, u"json"_s );
128 query.addQueryItem( u"objectIds"_s, ids.join( ','_L1 ) );
129 if ( !crs.isEmpty() && crs.contains( ':' ) )
130 {
131 const QString wkid = crs.indexOf( ':'_L1 ) >= 0 ? crs.split( ':' )[1] : QString();
132 query.addQueryItem( u"inSR"_s, wkid );
133 query.addQueryItem( u"outSR"_s, wkid );
134 }
135
136 query.addQueryItem( u"returnGeometry"_s, fetchGeometry ? u"true"_s : u"false"_s );
137
138 QString outFields;
139 if ( fetchAttributes.isEmpty() )
140 outFields = u"*"_s;
141 else
142 outFields = fetchAttributes.join( ',' );
143 query.addQueryItem( u"outFields"_s, outFields );
144
145 query.addQueryItem( u"returnM"_s, fetchM ? u"true"_s : u"false"_s );
146 query.addQueryItem( u"returnZ"_s, fetchZ ? u"true"_s : u"false"_s );
147 queryUrl.setQuery( query );
148 return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, feedback, urlPrefix );
149}
150
152 const QString &layerurl,
153 const QgsRectangle &filterRect,
154 QString &errorTitle,
155 QString &errorText,
156 const QString &authcfg,
157 const QgsHttpHeaders &requestHeaders,
158 QgsFeedback *feedback,
159 const QString &whereClause,
160 const QString &urlPrefix
161)
162{
163 QUrl queryUrl( layerurl + "/query" );
164 QUrlQuery query( queryUrl );
165 query.addQueryItem( u"f"_s, u"json"_s );
166 query.addQueryItem( u"where"_s, whereClause.isEmpty() ? u"1=1"_s : whereClause );
167 query.addQueryItem( u"returnIdsOnly"_s, u"true"_s );
168 query.addQueryItem( u"geometry"_s, u"%1,%2,%3,%4"_s.arg( filterRect.xMinimum(), 0, 'f', -1 ).arg( filterRect.yMinimum(), 0, 'f', -1 ).arg( filterRect.xMaximum(), 0, 'f', -1 ).arg( filterRect.yMaximum(), 0, 'f', -1 ) );
169 query.addQueryItem( u"geometryType"_s, u"esriGeometryEnvelope"_s );
170 query.addQueryItem( u"spatialRel"_s, u"esriSpatialRelEnvelopeIntersects"_s );
171 queryUrl.setQuery( query );
172 const QVariantMap objectIdData = queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, feedback, urlPrefix );
173
174 if ( objectIdData.isEmpty() )
175 {
176 return QList<quint32>();
177 }
178
179 QList<quint32> ids;
180 const QVariantList objectIdsList = objectIdData[u"objectIds"_s].toList();
181 ids.reserve( objectIdsList.size() );
182 for ( const QVariant &objectId : objectIdsList )
183 {
184 ids << objectId.toInt();
185 }
186 return ids;
187}
188
190 const QUrl &u, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, QgsFeedback *feedback, QString *contentType, const QString &urlPrefix, bool forceRefresh
191)
192{
193 QUrl url = parseUrl( u );
194
195 if ( !urlPrefix.isEmpty() )
196 url = QUrl( urlPrefix + url.toString() );
197
198 QNetworkRequest request( url );
199 QgsSetRequestInitiatorClass( request, u"QgsArcGisRestUtils"_s );
200 requestHeaders.updateNetworkRequest( request );
201
202 QgsBlockingNetworkRequest networkRequest;
203 networkRequest.setAuthCfg( authcfg );
204 const QgsBlockingNetworkRequest::ErrorCode error = networkRequest.get( request, forceRefresh, feedback );
205
206 if ( feedback && feedback->isCanceled() )
207 return QByteArray();
208
209 // Handle network errors
211 {
212 QgsDebugError( u"Network error: %1"_s.arg( networkRequest.errorMessage() ) );
213 errorTitle = u"Network error"_s;
214 errorText = networkRequest.errorMessage();
215
216 // try to get detailed error message from reply
217 const QString content = networkRequest.reply().content();
218 const thread_local QRegularExpression errorRx( u"Error: <.*?>(.*?)<"_s );
219 const QRegularExpressionMatch match = errorRx.match( content );
220 if ( match.hasMatch() )
221 {
222 errorText = match.captured( 1 );
223 }
224
225 return QByteArray();
226 }
227
228 const QgsNetworkReplyContent content = networkRequest.reply();
229 if ( contentType )
230 *contentType = content.rawHeader( "Content-Type" );
231 return content.content();
232}
233
235 const QUrl &url, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, QgsFeedback *feedback, const QString &urlPrefix, bool forceRefresh
236)
237{
238 const QByteArray reply = queryService( url, authcfg, errorTitle, errorText, requestHeaders, feedback, nullptr, urlPrefix, forceRefresh );
239 if ( !errorTitle.isEmpty() )
240 {
241 return QVariantMap();
242 }
243 if ( feedback && feedback->isCanceled() )
244 return QVariantMap();
245
246 // Parse data
247 QJsonParseError err;
248 const QJsonDocument doc = QJsonDocument::fromJson( reply, &err );
249 if ( doc.isNull() )
250 {
251 errorTitle = u"Parsing error"_s;
252 errorText = err.errorString();
253 QgsDebugError( u"Parsing error: %1"_s.arg( err.errorString() ) );
254 return QVariantMap();
255 }
256 const QVariantMap res = doc.object().toVariantMap();
257 if ( res.contains( u"error"_s ) )
258 {
259 const QVariantMap error = res.value( u"error"_s ).toMap();
260 errorText = error.value( u"message"_s ).toString();
261 errorTitle = QObject::tr( "Error %1" ).arg( error.value( u"code"_s ).toString() );
262 return QVariantMap();
263 }
264 return res;
265}
266
267QUrl QgsArcGisRestQueryUtils::parseUrl( const QUrl &url, bool *isTestEndpoint )
268{
269 if ( isTestEndpoint )
270 *isTestEndpoint = false;
271
272 QUrl modifiedUrl( url );
273 if ( modifiedUrl.toString().contains( "fake_qgis_http_endpoint"_L1 ) )
274 {
275 if ( isTestEndpoint )
276 *isTestEndpoint = true;
277
278 // Just for testing with local files instead of http:// resources
279 QString modifiedUrlString = modifiedUrl.toString();
280 // Qt5 does URL encoding from some reason (of the FILTER parameter for example)
281 modifiedUrlString = QUrl::fromPercentEncoding( modifiedUrlString.toUtf8() );
282 modifiedUrlString.replace( "fake_qgis_http_endpoint/"_L1, "fake_qgis_http_endpoint_"_L1 );
283 QgsDebugMsgLevel( u"Get %1"_s.arg( modifiedUrlString ), 2 );
284 modifiedUrlString = modifiedUrlString.mid( u"http://"_s.size() );
285 QString args = modifiedUrlString.indexOf( '?' ) >= 0 ? modifiedUrlString.mid( modifiedUrlString.indexOf( '?' ) ) : QString();
286 if ( modifiedUrlString.size() > 150 )
287 {
288 args = QCryptographicHash::hash( args.toUtf8(), QCryptographicHash::Md5 ).toHex();
289 }
290 else
291 {
292 args.replace( "?"_L1, "_"_L1 );
293 args.replace( "&"_L1, "_"_L1 );
294 args.replace( "<"_L1, "_"_L1 );
295 args.replace( ">"_L1, "_"_L1 );
296 args.replace( "'"_L1, "_"_L1 );
297 args.replace( "\""_L1, "_"_L1 );
298 args.replace( " "_L1, "_"_L1 );
299 args.replace( ":"_L1, "_"_L1 );
300 args.replace( "/"_L1, "_"_L1 );
301 args.replace( "\n"_L1, "_"_L1 );
302 }
303#ifdef Q_OS_WIN
304 // Passing "urls" like "http://c:/path" to QUrl 'eats' the : after c,
305 // so we must restore it
306 if ( modifiedUrlString[1] == '/' )
307 {
308 modifiedUrlString = modifiedUrlString[0] + ":/" + modifiedUrlString.mid( 2 );
309 }
310#endif
311 modifiedUrlString = modifiedUrlString.mid( 0, modifiedUrlString.indexOf( '?' ) ) + args;
312 QgsDebugMsgLevel( u"Get %1 (after laundering)"_s.arg( modifiedUrlString ), 2 );
313 modifiedUrl = QUrl::fromLocalFile( modifiedUrlString );
314 if ( !QFile::exists( modifiedUrlString ) )
315 {
316 QgsDebugError( u"Local test file %1 for URL %2 does not exist!!!"_s.arg( modifiedUrlString, url.toString() ) );
317 }
318 }
319
320 return modifiedUrl;
321}
322
323void QgsArcGisRestQueryUtils::adjustBaseUrl( QString &baseUrl, const QString &name )
324{
325 const QStringList parts = name.split( '/' );
326 QString checkString;
327 for ( const QString &part : parts )
328 {
329 if ( !checkString.isEmpty() )
330 checkString += QString( '/' );
331
332 checkString += part;
333 if ( baseUrl.indexOf( QRegularExpression( checkString.replace( '/', "\\/"_L1 ) + u"\\/?$"_s ) ) > -1 )
334 {
335 baseUrl = baseUrl.left( baseUrl.length() - checkString.length() - 1 );
336 break;
337 }
338 }
339}
340
341void QgsArcGisRestQueryUtils::visitFolderItems( const std::function< void( const QString &, const QString & ) > &visitor, const QVariantMap &serviceData, const QString &baseUrl )
342{
343 QString base( baseUrl );
344 bool baseChecked = false;
345 if ( !base.endsWith( '/' ) )
346 base += '/'_L1;
347
348 const QStringList folderList = serviceData.value( u"folders"_s ).toStringList();
349 for ( const QString &folder : folderList )
350 {
351 if ( !baseChecked )
352 {
353 adjustBaseUrl( base, folder );
354 baseChecked = true;
355 }
356 visitor( folder, base + folder );
357 }
358}
359
360void QgsArcGisRestQueryUtils::visitServiceItems( const std::function<void( const QString &, const QString &, Qgis::ArcGisRestServiceType )> &visitor, const QVariantMap &serviceData, const QString &baseUrl )
361{
362 QString base( baseUrl );
363 bool baseChecked = false;
364 if ( !base.endsWith( '/' ) )
365 base += '/'_L1;
366
367 const QVariantList serviceList = serviceData.value( u"services"_s ).toList();
368 for ( const QVariant &service : serviceList )
369 {
370 const QVariantMap serviceMap = service.toMap();
371 const QString serviceTypeString = serviceMap.value( u"type"_s ).toString();
372 const Qgis::ArcGisRestServiceType serviceType = QgsArcGisRestUtils::serviceTypeFromString( serviceTypeString );
373
374 switch ( serviceType )
375 {
380 // supported
381 break;
382
387 // unsupported
388 continue;
389 }
390
391 const QString serviceName = serviceMap.value( u"name"_s ).toString();
392 const QString displayName = serviceName.split( '/' ).last();
393 if ( !baseChecked )
394 {
395 adjustBaseUrl( base, serviceName );
396 baseChecked = true;
397 }
398
399 visitor( displayName, base + serviceName + '/' + serviceTypeString, serviceType );
400 }
401}
402
404 const std::function< void( const LayerItemDetails &details )> &visitor, const QVariantMap &serviceData, const QString &parentUrl, const QString &parentSupportedFormats, const ServiceTypeFilter filter
405)
406{
407 const QgsCoordinateReferenceSystem crs = QgsArcGisRestUtils::convertSpatialReference( serviceData.value( u"spatialReference"_s ).toMap() );
408
409 bool found = false;
410 const QList<QByteArray> supportedFormats = QImageReader::supportedImageFormats();
411 const QStringList supportedImageFormatTypes = serviceData.value( u"supportedImageFormatTypes"_s ).toString().isEmpty() ? parentSupportedFormats.split( ',' )
412 : serviceData.value( u"supportedImageFormatTypes"_s ).toString().split( ',' );
413 QString format = supportedImageFormatTypes.value( 0 );
414 for ( const QString &encoding : supportedImageFormatTypes )
415 {
416 for ( const QByteArray &fmt : supportedFormats )
417 {
418 if ( encoding.startsWith( fmt, Qt::CaseInsensitive ) )
419 {
420 format = encoding;
421 found = true;
422 break;
423 }
424 }
425 if ( found )
426 break;
427 }
428 Qgis::ArcGisRestServiceCapabilities capabilities = QgsArcGisRestUtils::serviceCapabilitiesFromString( serviceData.value( u"capabilities"_s ).toString() );
429
430 // If the requested layer type is vector, do not show raster-only layers (i.e. non query-able layers)
431
432 if ( serviceData.value( u"serviceDataType"_s ).toString().startsWith( "esriImageService"_L1 ) )
433 {
434 // consider ImageServices as having both render and query capabilities, so we can load them
435 // as either raster or vector
436 capabilities.setFlag( Qgis::ArcGisRestServiceCapability::Map, true );
437 capabilities.setFlag( Qgis::ArcGisRestServiceCapability::Query, true );
438 }
439 const QVariantList layerInfoList = serviceData.value( u"layers"_s ).toList();
440 for ( const QVariant &layerInfo : layerInfoList )
441 {
442 const QVariantMap layerInfoMap = layerInfo.toMap();
443
444 LayerItemDetails details;
445 details.layerId = layerInfoMap.value( u"id"_s ).toString();
446 details.parentLayerId = layerInfoMap.value( u"parentLayerId"_s ).toString();
447 details.name = layerInfoMap.value( u"name"_s ).toString();
448 details.description = layerInfoMap.value( u"description"_s ).toString();
449
450 const QString geometryType = layerInfoMap.value( u"geometryType"_s ).toString();
451#if 0
452 // we have a choice here -- if geometryType is unknown and the service reflects that it supports Map capabilities,
453 // then we can't be sure whether or not the individual sublayers support Query or Map requests only. So we either:
454 // 1. Send off additional requests for each individual layer's capabilities (too expensive)
455 // 2. Err on the side of only showing services we KNOW will work for layer -- but this has the side effect that layers
456 // which ARE available as feature services will only show as raster mapserver layers, which is VERY bad/restrictive
457 // 3. Err on the side of showing services we THINK may work, even though some of them may or may not work depending on the actual
458 // server configuration
459 // We opt for 3, because otherwise we're making it impossible for users to load valid vector layers into QGIS
460
461 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Map ) )
462 {
463 if ( geometryType.isEmpty() )
464 continue;
465 }
466#endif
467
468 if ( filter == ServiceTypeFilter::Scene )
469 {
472 details.url = parentUrl;
473 details.isParentLayer = false;
474 details.crs = crs;
475 details.format = format;
476 details.isMapServerWithQueryCapability = false;
477 visitor( details );
478 continue;
479 }
480
481 // Yes, potentially we may visit twice, once as as a raster (if applicable), and once as a vector (if applicable)!
482 bool exposedAsVector = false;
483 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Query ) && ( filter == ServiceTypeFilter::Vector || filter == ServiceTypeFilter::AllTypes ) )
484 {
485 exposedAsVector = true;
486 const Qgis::WkbType wkbType = QgsArcGisRestUtils::convertGeometryType( geometryType );
488 details.geometryType = QgsWkbTypes::geometryType( wkbType );
489 details.url = parentUrl + '/' + details.layerId;
490 details.format = format;
492 if ( !layerInfoMap.value( u"subLayerIds"_s ).toList().empty() )
493 {
494 details.isParentLayer = true;
496 visitor( details );
497 }
498 else
499 {
500 details.isParentLayer = false;
501 details.crs = crs;
502 visitor( details );
503 }
504 }
505
506 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Map ) && ( filter == ServiceTypeFilter::Raster || filter == ServiceTypeFilter::AllTypes ) )
507 {
509 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Query ) )
510 wkbType = QgsArcGisRestUtils::convertGeometryType( geometryType );
511
513 details.geometryType = QgsWkbTypes::geometryType( wkbType );
514 details.url = parentUrl + '/' + details.layerId;
515 details.format = format;
516 details.isMapServerWithQueryCapability = exposedAsVector;
517 if ( !layerInfoMap.value( u"subLayerIds"_s ).toList().empty() )
518 {
519 if ( !exposedAsVector )
520 {
521 details.isParentLayer = true;
523 visitor( details );
524 }
525 }
526 else
527 {
528 details.isParentLayer = false;
529 details.crs = crs;
530 visitor( details );
531 }
532 }
533 }
534
535 const QVariantList tableInfoList = serviceData.value( u"tables"_s ).toList();
536 for ( const QVariant &tableInfo : tableInfoList )
537 {
538 const QVariantMap tableInfoMap = tableInfo.toMap();
539
540 LayerItemDetails details;
541 details.layerId = tableInfoMap.value( u"id"_s ).toString();
542 details.parentLayerId = tableInfoMap.value( u"parentLayerId"_s ).toString();
543 details.name = tableInfoMap.value( u"name"_s ).toString();
544 details.description = tableInfoMap.value( u"description"_s ).toString();
545
546 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Query ) && ( filter == ServiceTypeFilter::Vector || filter == ServiceTypeFilter::AllTypes ) )
547 {
550 details.url = parentUrl + '/' + details.layerId;
551 details.format = format;
552 details.isMapServerWithQueryCapability = false;
553 if ( !tableInfoMap.value( u"subLayerIds"_s ).toList().empty() )
554 {
555 details.isParentLayer = true;
557 visitor( details );
558 }
559 else
560 {
561 details.isParentLayer = false;
562 details.crs = crs;
563 visitor( details );
564 }
565 }
566 }
567
568 // Add root MapServer as raster layer when multiple layers are listed
569 if ( filter != ServiceTypeFilter::Vector && layerInfoList.count() > 1 && serviceData.contains( u"supportedImageFormatTypes"_s ) )
570 {
571 LayerItemDetails details;
572 details.parentLayerId = QString();
575 details.url = parentUrl;
576 details.isParentLayer = false;
577 details.crs = crs;
578 details.format = format;
579 details.isMapServerWithQueryCapability = false;
581 visitor( details );
582 }
583
584 // Add root ImageServer as layer
585 if ( serviceData.value( u"serviceDataType"_s ).toString().startsWith( "esriImageService"_L1 ) )
586 {
587 LayerItemDetails details;
588 details.layerId = QString();
589 details.parentLayerId = QString();
590 details.name = serviceData.value( u"name"_s ).toString();
591 details.description = serviceData.value( u"description"_s ).toString();
594 details.url = parentUrl;
595 details.isParentLayer = false;
596 details.crs = crs;
597 details.format = format;
598 details.isMapServerWithQueryCapability = false;
599 visitor( details );
600 }
601}
602
603
605
606//
607// QgsArcGisAsyncQuery
608//
609
610QgsArcGisAsyncQuery::QgsArcGisAsyncQuery( QObject *parent )
611 : QObject( parent )
612{}
613
614QgsArcGisAsyncQuery::~QgsArcGisAsyncQuery()
615{
616 if ( mReply )
617 mReply->deleteLater();
618}
619
620void QgsArcGisAsyncQuery::start( const QUrl &url, const QString &authCfg, QByteArray *result, bool allowCache, const QgsHttpHeaders &headers, const QString &urlPrefix )
621{
622 mResult = result;
623 QUrl mUrl = url;
624 if ( !urlPrefix.isEmpty() )
625 mUrl = QUrl( urlPrefix + url.toString() );
626 QNetworkRequest request( mUrl );
627
628 headers.updateNetworkRequest( request );
629
630 if ( !authCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, authCfg ) )
631 {
632 const QString error = tr( "network request update failed for authentication config" );
633 emit failed( u"Network"_s, error );
634 return;
635 }
636
637 QgsSetRequestInitiatorClass( request, u"QgsArcGisAsyncQuery"_s );
638 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
639 if ( allowCache )
640 {
641 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
642 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
643 }
644
645 mReply = QgsNetworkAccessManager::instance()->get( request );
646 connect( mReply, &QNetworkReply::finished, this, &QgsArcGisAsyncQuery::handleReply );
647}
648
649void QgsArcGisAsyncQuery::handleReply()
650{
651 mReply->deleteLater();
652 // Handle network errors
653 if ( mReply->error() != QNetworkReply::NoError )
654 {
655 QgsDebugError( u"Network error: %1"_s.arg( mReply->errorString() ) );
656 emit failed( u"Network error"_s, mReply->errorString() );
657 return;
658 }
659
660 // Handle HTTP redirects
661 const QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
662 if ( !QgsVariantUtils::isNull( redirect ) )
663 {
664 QNetworkRequest request = mReply->request();
665 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
666 QgsSetRequestInitiatorClass( request, u"QgsArcGisAsyncQuery"_s );
667 QgsDebugMsgLevel( "redirecting to " + redirect.toUrl().toString(), 2 );
668 request.setUrl( redirect.toUrl() );
669 mReply = QgsNetworkAccessManager::instance()->get( request );
670 connect( mReply, &QNetworkReply::finished, this, &QgsArcGisAsyncQuery::handleReply );
671 return;
672 }
673
674 *mResult = mReply->readAll();
675 mResult = nullptr;
676 emit finished();
677}
678
679//
680// QgsArcGisAsyncParallelQuery
681//
682
683QgsArcGisAsyncParallelQuery::QgsArcGisAsyncParallelQuery( const QString &authcfg, const QgsHttpHeaders &requestHeaders, QObject *parent )
684 : QObject( parent )
685 , mAuthCfg( authcfg )
686 , mRequestHeaders( requestHeaders )
687{}
688
689void QgsArcGisAsyncParallelQuery::start( const QVector<QUrl> &urls, QVector<QByteArray> *results, bool allowCache )
690{
691 Q_ASSERT( results->size() == urls.size() );
692 mResults = results;
693 mPendingRequests = mResults->size();
694 for ( int i = 0, n = urls.size(); i < n; ++i )
695 {
696 QNetworkRequest request( urls[i] );
697 QgsSetRequestInitiatorClass( request, u"QgsArcGisAsyncParallelQuery"_s );
698 QgsSetRequestInitiatorId( request, QString::number( i ) );
699
700 mRequestHeaders.updateNetworkRequest( request );
701 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
702 {
703 const QString error = tr( "network request update failed for authentication config" );
704 mErrors.append( error );
705 QgsMessageLog::logMessage( error, tr( "Network" ) );
706 continue;
707 }
708
709 request.setAttribute( QNetworkRequest::HttpPipeliningAllowedAttribute, true );
710 if ( allowCache )
711 {
712 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
713 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
714 request.setRawHeader( "Connection", "keep-alive" );
715 }
716 QNetworkReply *reply = QgsNetworkAccessManager::instance()->get( request );
717 reply->setProperty( "idx", i );
718 connect( reply, &QNetworkReply::finished, this, &QgsArcGisAsyncParallelQuery::handleReply );
719 }
720}
721
722void QgsArcGisAsyncParallelQuery::handleReply()
723{
724 QNetworkReply *reply = qobject_cast<QNetworkReply *>( QObject::sender() );
725 const QVariant redirect = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
726 const int idx = reply->property( "idx" ).toInt();
727 reply->deleteLater();
728 if ( reply->error() != QNetworkReply::NoError )
729 {
730 // Handle network errors
731 mErrors.append( reply->errorString() );
732 --mPendingRequests;
733 }
734 else if ( !QgsVariantUtils::isNull( redirect ) )
735 {
736 // Handle HTTP redirects
737 QNetworkRequest request = reply->request();
738 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
739 QgsSetRequestInitiatorClass( request, u"QgsArcGisAsyncParallelQuery"_s );
740 QgsDebugMsgLevel( "redirecting to " + redirect.toUrl().toString(), 2 );
741 request.setUrl( redirect.toUrl() );
742 reply = QgsNetworkAccessManager::instance()->get( request );
743 reply->setProperty( "idx", idx );
744 connect( reply, &QNetworkReply::finished, this, &QgsArcGisAsyncParallelQuery::handleReply );
745 }
746 else
747 {
748 // All OK
749 ( *mResults )[idx] = reply->readAll();
750 --mPendingRequests;
751 }
752 if ( mPendingRequests == 0 )
753 {
754 emit finished( mErrors );
755 mResults = nullptr;
756 mErrors.clear();
757 }
758}
759
ArcGisRestServiceType
Available ArcGIS REST service types.
Definition qgis.h:4588
@ GeocodeServer
GeocodeServer.
Definition qgis.h:4594
@ SceneServer
SceneServer.
Definition qgis.h:4596
@ Unknown
Other unknown/unsupported type.
Definition qgis.h:4595
@ GlobeServer
GlobeServer.
Definition qgis.h:4592
@ ImageServer
ImageServer.
Definition qgis.h:4591
@ FeatureServer
FeatureServer.
Definition qgis.h:4589
@ Unknown
Unknown types.
Definition qgis.h:383
@ Null
No geometry.
Definition qgis.h:384
QFlags< ArcGisRestServiceCapability > ArcGisRestServiceCapabilities
Available ArcGIS REST service capabilities.
Definition qgis.h:4623
WkbType
The WKB type describes the number of dimensions a geometry has.
Definition qgis.h:294
@ Unknown
Unknown.
Definition qgis.h:295
static QgsAuthManager * authManager()
Returns the application's authentication manager instance.
static void visitFolderItems(const std::function< void(const QString &folderName, const QString &url)> &visitor, const QVariantMap &serviceData, const QString &baseUrl)
Calls the specified visitor function on all folder items found within the given service data.
static QVariantMap queryServiceJSON(const QUrl &url, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders=QgsHttpHeaders(), QgsFeedback *feedback=nullptr, const QString &urlPrefix=QString(), bool forceRefresh=false)
Performs a blocking request to a URL and returns the retrieved JSON content.
static QgsRectangle getExtent(const QString &layerurl, const QString &whereClause, const QString &authcfg, const QgsHttpHeaders &requestHeaders=QgsHttpHeaders(), const QString &urlPrefix=QString())
Retrieves the extent for the features matching a whereClause.
static QVariantMap getObjectIds(const QString &layerurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders=QgsHttpHeaders(), const QString &urlPrefix=QString(), const QgsRectangle &bbox=QgsRectangle(), const QString &whereClause=QString())
Retrieves all object IDs for the specified layer URL.
static QUrl parseUrl(const QUrl &url, bool *isTestEndpoint=nullptr)
Parses and processes a url.
static QByteArray queryService(const QUrl &url, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders=QgsHttpHeaders(), QgsFeedback *feedback=nullptr, QString *contentType=nullptr, const QString &urlPrefix=QString(), bool forceRefresh=false)
Performs a blocking request to a URL and returns the retrieved data.
static QVariantMap getServiceInfo(const QString &baseurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders=QgsHttpHeaders(), const QString &urlPrefix=QString(), bool forceRefresh=false)
Retrieves JSON service info for the specified base URL.
static QVariantMap getLayerInfo(const QString &layerurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders=QgsHttpHeaders(), const QString &urlPrefix=QString())
Retrieves JSON layer info for the specified layer URL.
static void visitServiceItems(const std::function< void(const QString &serviceName, const QString &url, Qgis::ArcGisRestServiceType serviceType)> &visitor, const QVariantMap &serviceData, const QString &baseUrl)
Calls the specified visitor function on all service items found within the given service data.
static QList< quint32 > getObjectIdsByExtent(const QString &layerurl, const QgsRectangle &filterRect, QString &errorTitle, QString &errorText, const QString &authcfg, const QgsHttpHeaders &requestHeaders=QgsHttpHeaders(), QgsFeedback *feedback=nullptr, const QString &whereClause=QString(), const QString &urlPrefix=QString())
Gets a list of object IDs which fall within the specified extent.
static void addLayerItems(const std::function< void(const LayerItemDetails &details)> &visitor, const QVariantMap &serviceData, const QString &parentUrl, const QString &parentSupportedFormats, const ServiceTypeFilter filter=ServiceTypeFilter::AllTypes)
Calls the specified visitor function on all layer items found within the given service data.
static QVariantMap getObjects(const QString &layerurl, const QString &authcfg, const QList< quint32 > &objectIds, const QString &crs, bool fetchGeometry, const QStringList &fetchAttributes, bool fetchM, bool fetchZ, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders=QgsHttpHeaders(), QgsFeedback *feedback=nullptr, const QString &urlPrefix=QString())
Retrieves all matching objects from the specified layer URL.
static QgsCoordinateReferenceSystem convertSpatialReference(const QVariantMap &spatialReferenceMap)
Converts a spatial reference JSON definition to a QgsCoordinateReferenceSystem value.
static Qgis::WkbType convertGeometryType(const QString &type)
Converts an ESRI REST geometry type to a WKB type.
static Qgis::ArcGisRestServiceType serviceTypeFromString(const QString &type)
Converts a string value to a REST service type.
static Qgis::ArcGisRestServiceCapabilities serviceCapabilitiesFromString(const QString &capabilities)
Parses a capabilities string to known values.
static QgsRectangle convertRectangle(const QVariant &value)
Converts a rectangle value to a QgsRectangle.
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).
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:56
Implements simple HTTP header management.
bool updateNetworkRequest(QNetworkRequest &request) const
Updates a request by adding all the HTTP headers.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true, const char *file=__builtin_FILE(), const char *function=__builtin_FUNCTION(), int line=__builtin_LINE(), Qgis::StringFormat format=Qgis::StringFormat::PlainText)
Adds a message to the log instance (and creates it if necessary).
static QgsNetworkAccessManager * instance(Qt::ConnectionType connectionType=Qt::BlockingQueuedConnection)
Returns a pointer to the active QgsNetworkAccessManager for the current thread.
Encapsulates a network reply within a container which is inexpensive to copy and safe to pass between...
QByteArray content() const
Returns the reply content.
QByteArray rawHeader(const QByteArray &headerName) const
Returns the content of the header with the specified headerName, or an empty QByteArray if the specif...
A rectangle specified with double values.
double xMinimum
double yMinimum
double xMaximum
double yMaximum
static bool isNull(const QVariant &variant, bool silenceNullWarnings=false)
Returns true if the specified variant should be considered a NULL value.
static Qgis::GeometryType geometryType(Qgis::WkbType type)
Returns the geometry type for a WKB type, e.g., both MultiPolygon and CurvePolygon would have a Polyg...
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:63
#define QgsDebugError(str)
Definition qgslogger.h:59
#define QgsSetRequestInitiatorClass(request, _class)
#define QgsSetRequestInitiatorId(request, str)
Encapsulates details relating to a layer item.
ServiceTypeFilter serviceType
Service type.
bool isMapServerWithQueryCapability
true if layer is a map server with the query capability
QString format
Map server image format.
QgsCoordinateReferenceSystem crs
Coordinate reference system.
bool isMapServerSpecialAllLayersOption
true if layer is the special map server "all layers" layer
bool isParentLayer
true if layer item represents a parent layer
Qgis::GeometryType geometryType
Geometry type.