QGIS API Documentation 3.99.0-Master (2fe06baccd8)
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 <QUrl>
34#include <QUrlQuery>
35
36#include "moc_qgsarcgisrestquery.cpp"
37
38QVariantMap QgsArcGisRestQueryUtils::getServiceInfo( const QString &baseurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, const QString &urlPrefix )
39{
40 // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer?f=json
41 QUrl queryUrl( baseurl );
42 QUrlQuery query( queryUrl );
43 query.addQueryItem( QStringLiteral( "f" ), QStringLiteral( "json" ) );
44 queryUrl.setQuery( query );
45 return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, nullptr, urlPrefix );
46}
47
48QVariantMap QgsArcGisRestQueryUtils::getLayerInfo( const QString &layerurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, const QString &urlPrefix )
49{
50 // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/1?f=json
51 QUrl queryUrl( layerurl );
52 QUrlQuery query( queryUrl );
53 query.addQueryItem( QStringLiteral( "f" ), QStringLiteral( "json" ) );
54 queryUrl.setQuery( query );
55 return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, nullptr, urlPrefix );
56}
57
58QVariantMap QgsArcGisRestQueryUtils::getObjectIds( const QString &layerurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, const QString &urlPrefix, const QgsRectangle &bbox, const QString &whereClause )
59{
60 // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/1/query?where=1%3D1&returnIdsOnly=true&f=json
61 QUrl queryUrl( layerurl + "/query" );
62 QUrlQuery query( queryUrl );
63 query.addQueryItem( QStringLiteral( "f" ), QStringLiteral( "json" ) );
64 query.addQueryItem( QStringLiteral( "where" ), whereClause.isEmpty() ? QStringLiteral( "1=1" ) : whereClause );
65 query.addQueryItem( QStringLiteral( "returnIdsOnly" ), QStringLiteral( "true" ) );
66 if ( !bbox.isNull() )
67 {
68 query.addQueryItem( QStringLiteral( "geometry" ), QStringLiteral( "%1,%2,%3,%4" )
69 .arg( bbox.xMinimum(), 0, 'f', -1 ).arg( bbox.yMinimum(), 0, 'f', -1 )
70 .arg( bbox.xMaximum(), 0, 'f', -1 ).arg( bbox.yMaximum(), 0, 'f', -1 ) );
71 query.addQueryItem( QStringLiteral( "geometryType" ), QStringLiteral( "esriGeometryEnvelope" ) );
72 query.addQueryItem( QStringLiteral( "spatialRel" ), QStringLiteral( "esriSpatialRelEnvelopeIntersects" ) );
73 }
74 queryUrl.setQuery( query );
75 return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, nullptr, urlPrefix );
76}
77
78QgsRectangle QgsArcGisRestQueryUtils::getExtent( const QString &layerurl, const QString &whereClause, const QString &authcfg, const QgsHttpHeaders &requestHeaders, const QString &urlPrefix )
79{
80 // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/1/query?where=1%3D1&returnExtentOnly=true&f=json
81 QUrl queryUrl( layerurl + "/query" );
82 QUrlQuery query( queryUrl );
83 query.addQueryItem( QStringLiteral( "f" ), QStringLiteral( "json" ) );
84 query.addQueryItem( QStringLiteral( "where" ), whereClause );
85 query.addQueryItem( QStringLiteral( "returnExtentOnly" ), QStringLiteral( "true" ) );
86 queryUrl.setQuery( query );
87 QString errorTitle;
88 QString errorText;
89 const QVariantMap res = queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, nullptr, urlPrefix );
90 if ( res.isEmpty() )
91 {
92 QgsDebugError( QStringLiteral( "getExtent failed: %1 - %2" ).arg( errorTitle, errorText ) );
93 return QgsRectangle();
94 }
95
96 return QgsArcGisRestUtils::convertRectangle( res.value( QStringLiteral( "extent" ) ) );
97}
98
99QVariantMap QgsArcGisRestQueryUtils::getObjects( const QString &layerurl, const QString &authcfg, const QList<quint32> &objectIds, const QString &crs,
100 bool fetchGeometry, const QStringList &fetchAttributes,
101 bool fetchM, bool fetchZ,
102 const QgsRectangle &filterRect,
103 QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, QgsFeedback *feedback, const QString &urlPrefix )
104{
105 QStringList ids;
106 for ( const int id : objectIds )
107 {
108 ids.append( QString::number( id ) );
109 }
110 QUrl queryUrl( layerurl + "/query" );
111 QUrlQuery query( queryUrl );
112 query.addQueryItem( QStringLiteral( "f" ), QStringLiteral( "json" ) );
113 query.addQueryItem( QStringLiteral( "objectIds" ), ids.join( QLatin1Char( ',' ) ) );
114 if ( !crs.isEmpty() && crs.contains( ':' ) )
115 {
116 const QString wkid = crs.indexOf( QLatin1Char( ':' ) ) >= 0 ? crs.split( ':' )[1] : QString();
117 query.addQueryItem( QStringLiteral( "inSR" ), wkid );
118 query.addQueryItem( QStringLiteral( "outSR" ), wkid );
119 }
120
121 query.addQueryItem( QStringLiteral( "returnGeometry" ), fetchGeometry ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
122
123 QString outFields;
124 if ( fetchAttributes.isEmpty() )
125 outFields = QStringLiteral( "*" );
126 else
127 outFields = fetchAttributes.join( ',' );
128 query.addQueryItem( QStringLiteral( "outFields" ), outFields );
129
130 query.addQueryItem( QStringLiteral( "returnM" ), fetchM ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
131 query.addQueryItem( QStringLiteral( "returnZ" ), fetchZ ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
132 if ( !filterRect.isNull() )
133 {
134 query.addQueryItem( QStringLiteral( "geometry" ), QStringLiteral( "%1,%2,%3,%4" )
135 .arg( filterRect.xMinimum(), 0, 'f', -1 ).arg( filterRect.yMinimum(), 0, 'f', -1 )
136 .arg( filterRect.xMaximum(), 0, 'f', -1 ).arg( filterRect.yMaximum(), 0, 'f', -1 ) );
137 query.addQueryItem( QStringLiteral( "geometryType" ), QStringLiteral( "esriGeometryEnvelope" ) );
138 query.addQueryItem( QStringLiteral( "spatialRel" ), QStringLiteral( "esriSpatialRelEnvelopeIntersects" ) );
139 }
140 queryUrl.setQuery( query );
141 return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, feedback, urlPrefix );
142}
143
144QList<quint32> QgsArcGisRestQueryUtils::getObjectIdsByExtent( const QString &layerurl, const QgsRectangle &filterRect, QString &errorTitle, QString &errorText, const QString &authcfg, const QgsHttpHeaders &requestHeaders, QgsFeedback *feedback, const QString &whereClause, const QString &urlPrefix )
145{
146 QUrl queryUrl( layerurl + "/query" );
147 QUrlQuery query( queryUrl );
148 query.addQueryItem( QStringLiteral( "f" ), QStringLiteral( "json" ) );
149 query.addQueryItem( QStringLiteral( "where" ), whereClause.isEmpty() ? QStringLiteral( "1=1" ) : whereClause );
150 query.addQueryItem( QStringLiteral( "returnIdsOnly" ), QStringLiteral( "true" ) );
151 query.addQueryItem( QStringLiteral( "geometry" ), QStringLiteral( "%1,%2,%3,%4" )
152 .arg( filterRect.xMinimum(), 0, 'f', -1 ).arg( filterRect.yMinimum(), 0, 'f', -1 )
153 .arg( filterRect.xMaximum(), 0, 'f', -1 ).arg( filterRect.yMaximum(), 0, 'f', -1 ) );
154 query.addQueryItem( QStringLiteral( "geometryType" ), QStringLiteral( "esriGeometryEnvelope" ) );
155 query.addQueryItem( QStringLiteral( "spatialRel" ), QStringLiteral( "esriSpatialRelEnvelopeIntersects" ) );
156 queryUrl.setQuery( query );
157 const QVariantMap objectIdData = queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, feedback, urlPrefix );
158
159 if ( objectIdData.isEmpty() )
160 {
161 return QList<quint32>();
162 }
163
164 QList<quint32> ids;
165 const QVariantList objectIdsList = objectIdData[QStringLiteral( "objectIds" )].toList();
166 ids.reserve( objectIdsList.size() );
167 for ( const QVariant &objectId : objectIdsList )
168 {
169 ids << objectId.toInt();
170 }
171 return ids;
172}
173
174QByteArray QgsArcGisRestQueryUtils::queryService( const QUrl &u, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, QgsFeedback *feedback, QString *contentType, const QString &urlPrefix )
175{
176 QUrl url = parseUrl( u );
177
178 if ( !urlPrefix.isEmpty() )
179 url = QUrl( urlPrefix + url.toString() );
180
181 QNetworkRequest request( url );
182 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsArcGisRestUtils" ) );
183 requestHeaders.updateNetworkRequest( request );
184
185 QgsBlockingNetworkRequest networkRequest;
186 networkRequest.setAuthCfg( authcfg );
187 const QgsBlockingNetworkRequest::ErrorCode error = networkRequest.get( request, false, feedback );
188
189 if ( feedback && feedback->isCanceled() )
190 return QByteArray();
191
192 // Handle network errors
194 {
195 QgsDebugError( QStringLiteral( "Network error: %1" ).arg( networkRequest.errorMessage() ) );
196 errorTitle = QStringLiteral( "Network error" );
197 errorText = networkRequest.errorMessage();
198
199 // try to get detailed error message from reply
200 const QString content = networkRequest.reply().content();
201 const thread_local QRegularExpression errorRx( QStringLiteral( "Error: <.*?>(.*?)<" ) );
202 const QRegularExpressionMatch match = errorRx.match( content );
203 if ( match.hasMatch() )
204 {
205 errorText = match.captured( 1 );
206 }
207
208 return QByteArray();
209 }
210
211 const QgsNetworkReplyContent content = networkRequest.reply();
212 if ( contentType )
213 *contentType = content.rawHeader( "Content-Type" );
214 return content.content();
215}
216
217QVariantMap QgsArcGisRestQueryUtils::queryServiceJSON( const QUrl &url, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, QgsFeedback *feedback, const QString &urlPrefix )
218{
219 const QByteArray reply = queryService( url, authcfg, errorTitle, errorText, requestHeaders, feedback, nullptr, urlPrefix );
220 if ( !errorTitle.isEmpty() )
221 {
222 return QVariantMap();
223 }
224 if ( feedback && feedback->isCanceled() )
225 return QVariantMap();
226
227 // Parse data
228 QJsonParseError err;
229 const QJsonDocument doc = QJsonDocument::fromJson( reply, &err );
230 if ( doc.isNull() )
231 {
232 errorTitle = QStringLiteral( "Parsing error" );
233 errorText = err.errorString();
234 QgsDebugError( QStringLiteral( "Parsing error: %1" ).arg( err.errorString() ) );
235 return QVariantMap();
236 }
237 const QVariantMap res = doc.object().toVariantMap();
238 if ( res.contains( QStringLiteral( "error" ) ) )
239 {
240 const QVariantMap error = res.value( QStringLiteral( "error" ) ).toMap();
241 errorText = error.value( QStringLiteral( "message" ) ).toString();
242 errorTitle = QObject::tr( "Error %1" ).arg( error.value( QStringLiteral( "code" ) ).toString() );
243 return QVariantMap();
244 }
245 return res;
246}
247
248QUrl QgsArcGisRestQueryUtils::parseUrl( const QUrl &url, bool *isTestEndpoint )
249{
250 if ( isTestEndpoint )
251 *isTestEndpoint = false;
252
253 QUrl modifiedUrl( url );
254 if ( modifiedUrl.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
255 {
256 if ( isTestEndpoint )
257 *isTestEndpoint = true;
258
259 // Just for testing with local files instead of http:// resources
260 QString modifiedUrlString = modifiedUrl.toString();
261 // Qt5 does URL encoding from some reason (of the FILTER parameter for example)
262 modifiedUrlString = QUrl::fromPercentEncoding( modifiedUrlString.toUtf8() );
263 modifiedUrlString.replace( QLatin1String( "fake_qgis_http_endpoint/" ), QLatin1String( "fake_qgis_http_endpoint_" ) );
264 QgsDebugMsgLevel( QStringLiteral( "Get %1" ).arg( modifiedUrlString ), 2 );
265 modifiedUrlString = modifiedUrlString.mid( QStringLiteral( "http://" ).size() );
266 QString args = modifiedUrlString.indexOf( '?' ) >= 0 ? modifiedUrlString.mid( modifiedUrlString.indexOf( '?' ) ) : QString();
267 if ( modifiedUrlString.size() > 150 )
268 {
269 args = QCryptographicHash::hash( args.toUtf8(), QCryptographicHash::Md5 ).toHex();
270 }
271 else
272 {
273 args.replace( QLatin1String( "?" ), QLatin1String( "_" ) );
274 args.replace( QLatin1String( "&" ), QLatin1String( "_" ) );
275 args.replace( QLatin1String( "<" ), QLatin1String( "_" ) );
276 args.replace( QLatin1String( ">" ), QLatin1String( "_" ) );
277 args.replace( QLatin1String( "'" ), QLatin1String( "_" ) );
278 args.replace( QLatin1String( "\"" ), QLatin1String( "_" ) );
279 args.replace( QLatin1String( " " ), QLatin1String( "_" ) );
280 args.replace( QLatin1String( ":" ), QLatin1String( "_" ) );
281 args.replace( QLatin1String( "/" ), QLatin1String( "_" ) );
282 args.replace( QLatin1String( "\n" ), QLatin1String( "_" ) );
283 }
284#ifdef Q_OS_WIN
285 // Passing "urls" like "http://c:/path" to QUrl 'eats' the : after c,
286 // so we must restore it
287 if ( modifiedUrlString[1] == '/' )
288 {
289 modifiedUrlString = modifiedUrlString[0] + ":/" + modifiedUrlString.mid( 2 );
290 }
291#endif
292 modifiedUrlString = modifiedUrlString.mid( 0, modifiedUrlString.indexOf( '?' ) ) + args;
293 QgsDebugMsgLevel( QStringLiteral( "Get %1 (after laundering)" ).arg( modifiedUrlString ), 2 );
294 modifiedUrl = QUrl::fromLocalFile( modifiedUrlString );
295 if ( !QFile::exists( modifiedUrlString ) )
296 {
297 QgsDebugError( QStringLiteral( "Local test file %1 for URL %2 does not exist!!!" ).arg( modifiedUrlString, url.toString() ) );
298 }
299 }
300
301 return modifiedUrl;
302}
303
304void QgsArcGisRestQueryUtils::adjustBaseUrl( QString &baseUrl, const QString &name )
305{
306 const QStringList parts = name.split( '/' );
307 QString checkString;
308 for ( const QString &part : parts )
309 {
310 if ( !checkString.isEmpty() )
311 checkString += QString( '/' );
312
313 checkString += part;
314 if ( baseUrl.indexOf( QRegularExpression( checkString.replace( '/', QLatin1String( "\\/" ) ) + QStringLiteral( "\\/?$" ) ) ) > -1 )
315 {
316 baseUrl = baseUrl.left( baseUrl.length() - checkString.length() - 1 );
317 break;
318 }
319 }
320}
321
322void QgsArcGisRestQueryUtils::visitFolderItems( const std::function< void( const QString &, const QString & ) > &visitor, const QVariantMap &serviceData, const QString &baseUrl )
323{
324 QString base( baseUrl );
325 bool baseChecked = false;
326 if ( !base.endsWith( '/' ) )
327 base += QLatin1Char( '/' );
328
329 const QStringList folderList = serviceData.value( QStringLiteral( "folders" ) ).toStringList();
330 for ( const QString &folder : folderList )
331 {
332 if ( !baseChecked )
333 {
334 adjustBaseUrl( base, folder );
335 baseChecked = true;
336 }
337 visitor( folder, base + folder );
338 }
339}
340
341void QgsArcGisRestQueryUtils::visitServiceItems( const std::function<void ( const QString &, const QString &, Qgis::ArcGisRestServiceType )> &visitor, const QVariantMap &serviceData, const QString &baseUrl )
342{
343 QString base( baseUrl );
344 bool baseChecked = false;
345 if ( !base.endsWith( '/' ) )
346 base += QLatin1Char( '/' );
347
348 const QVariantList serviceList = serviceData.value( QStringLiteral( "services" ) ).toList();
349 for ( const QVariant &service : serviceList )
350 {
351 const QVariantMap serviceMap = service.toMap();
352 const QString serviceTypeString = serviceMap.value( QStringLiteral( "type" ) ).toString();
353 const Qgis::ArcGisRestServiceType serviceType = QgsArcGisRestUtils::serviceTypeFromString( serviceTypeString );
354
355 switch ( serviceType )
356 {
361 // supported
362 break;
363
368 // unsupported
369 continue;
370 }
371
372 const QString serviceName = serviceMap.value( QStringLiteral( "name" ) ).toString();
373 const QString displayName = serviceName.split( '/' ).last();
374 if ( !baseChecked )
375 {
376 adjustBaseUrl( base, serviceName );
377 baseChecked = true;
378 }
379
380 visitor( displayName, base + serviceName + '/' + serviceTypeString, serviceType );
381 }
382}
383
384void QgsArcGisRestQueryUtils::addLayerItems( const std::function<void ( const QString &, ServiceTypeFilter, Qgis::GeometryType, const QString &, const QString &, const QString &, const QString &, bool, const QgsCoordinateReferenceSystem &, const QString & )> &visitor, const QVariantMap &serviceData, const QString &parentUrl, const QString &parentSupportedFormats, const ServiceTypeFilter filter )
385{
386 const QgsCoordinateReferenceSystem crs = QgsArcGisRestUtils::convertSpatialReference( serviceData.value( QStringLiteral( "spatialReference" ) ).toMap() );
387
388 bool found = false;
389 const QList<QByteArray> supportedFormats = QImageReader::supportedImageFormats();
390 const QStringList supportedImageFormatTypes = serviceData.value( QStringLiteral( "supportedImageFormatTypes" ) ).toString().isEmpty() ? parentSupportedFormats.split( ',' ) : serviceData.value( QStringLiteral( "supportedImageFormatTypes" ) ).toString().split( ',' );
391 QString format = supportedImageFormatTypes.value( 0 );
392 for ( const QString &encoding : supportedImageFormatTypes )
393 {
394 for ( const QByteArray &fmt : supportedFormats )
395 {
396 if ( encoding.startsWith( fmt, Qt::CaseInsensitive ) )
397 {
398 format = encoding;
399 found = true;
400 break;
401 }
402 }
403 if ( found )
404 break;
405 }
406 const QStringList capabilities = serviceData.value( QStringLiteral( "capabilities" ) ).toString().split( ',' );
407
408 // If the requested layer type is vector, do not show raster-only layers (i.e. non query-able layers)
409 const bool serviceMayHaveQueryCapability = capabilities.contains( QStringLiteral( "Query" ) ) ||
410 serviceData.value( QStringLiteral( "serviceDataType" ) ).toString().startsWith( QLatin1String( "esriImageService" ) );
411
412 const bool serviceMayRenderMaps = capabilities.contains( QStringLiteral( "Map" ) ) ||
413 serviceData.value( QStringLiteral( "serviceDataType" ) ).toString().startsWith( QLatin1String( "esriImageService" ) );
414
415 const QVariantList layerInfoList = serviceData.value( QStringLiteral( "layers" ) ).toList();
416 for ( const QVariant &layerInfo : layerInfoList )
417 {
418 const QVariantMap layerInfoMap = layerInfo.toMap();
419 const QString id = layerInfoMap.value( QStringLiteral( "id" ) ).toString();
420 const QString parentLayerId = layerInfoMap.value( QStringLiteral( "parentLayerId" ) ).toString();
421 const QString name = layerInfoMap.value( QStringLiteral( "name" ) ).toString();
422 const QString description = layerInfoMap.value( QStringLiteral( "description" ) ).toString();
423
424 if ( filter == ServiceTypeFilter::Scene )
425 {
426 visitor( parentLayerId, ServiceTypeFilter::Scene, Qgis::GeometryType::Unknown, id, name, description, parentUrl, false, crs, format );
427 continue;
428 }
429
430 // Yes, potentially we may visit twice, once as as a raster (if applicable), and once as a vector (if applicable)!
431 if ( serviceMayRenderMaps && ( filter == ServiceTypeFilter::Raster || filter == ServiceTypeFilter::AllTypes ) )
432 {
433 if ( !layerInfoMap.value( QStringLiteral( "subLayerIds" ) ).toList().empty() )
434 {
435 visitor( parentLayerId, ServiceTypeFilter::Raster, Qgis::GeometryType::Unknown, id, name, description, parentUrl + '/' + id, true, QgsCoordinateReferenceSystem(), format );
436 }
437 else
438 {
439 visitor( parentLayerId, ServiceTypeFilter::Raster, Qgis::GeometryType::Unknown, id, name, description, parentUrl + '/' + id, false, crs, format );
440 }
441 }
442
443 if ( serviceMayHaveQueryCapability && ( filter == ServiceTypeFilter::Vector || filter == ServiceTypeFilter::AllTypes ) )
444 {
445 const QString geometryType = layerInfoMap.value( QStringLiteral( "geometryType" ) ).toString();
446#if 0
447 // we have a choice here -- if geometryType is unknown and the service reflects that it supports Map capabilities,
448 // then we can't be sure whether or not the individual sublayers support Query or Map requests only. So we either:
449 // 1. Send off additional requests for each individual layer's capabilities (too expensive)
450 // 2. Err on the side of only showing services we KNOW will work for layer -- but this has the side effect that layers
451 // which ARE available as feature services will only show as raster mapserver layers, which is VERY bad/restrictive
452 // 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
453 // server configuration
454 // We opt for 3, because otherwise we're making it impossible for users to load valid vector layers into QGIS
455
456 if ( serviceMayRenderMaps )
457 {
458 if ( geometryType.isEmpty() )
459 continue;
460 }
461#endif
462
463 const Qgis::WkbType wkbType = QgsArcGisRestUtils::convertGeometryType( geometryType );
464
465
466 if ( !layerInfoMap.value( QStringLiteral( "subLayerIds" ) ).toList().empty() )
467 {
468 visitor( parentLayerId, ServiceTypeFilter::Vector, QgsWkbTypes::geometryType( wkbType ), id, name, description, parentUrl + '/' + id, true, QgsCoordinateReferenceSystem(), format );
469 }
470 else
471 {
472 visitor( parentLayerId, ServiceTypeFilter::Vector, QgsWkbTypes::geometryType( wkbType ), id, name, description, parentUrl + '/' + id, false, crs, format );
473 }
474 }
475 }
476
477 const QVariantList tableInfoList = serviceData.value( QStringLiteral( "tables" ) ).toList();
478 for ( const QVariant &tableInfo : tableInfoList )
479 {
480 const QVariantMap tableInfoMap = tableInfo.toMap();
481 const QString id = tableInfoMap.value( QStringLiteral( "id" ) ).toString();
482 const QString parentLayerId = tableInfoMap.value( QStringLiteral( "parentLayerId" ) ).toString();
483 const QString name = tableInfoMap.value( QStringLiteral( "name" ) ).toString();
484 const QString description = tableInfoMap.value( QStringLiteral( "description" ) ).toString();
485
486 if ( serviceMayHaveQueryCapability && ( filter == ServiceTypeFilter::Vector || filter == ServiceTypeFilter::AllTypes ) )
487 {
488 if ( !tableInfoMap.value( QStringLiteral( "subLayerIds" ) ).toList().empty() )
489 {
490 visitor( parentLayerId, ServiceTypeFilter::Vector, Qgis::GeometryType::Null, id, name, description, parentUrl + '/' + id, true, QgsCoordinateReferenceSystem(), format );
491 }
492 else
493 {
494 visitor( parentLayerId, ServiceTypeFilter::Vector, Qgis::GeometryType::Null, id, name, description, parentUrl + '/' + id, false, crs, format );
495 }
496 }
497 }
498
499 // Add root MapServer as raster layer when multiple layers are listed
500 if ( filter != ServiceTypeFilter::Vector && layerInfoList.count() > 1 && serviceData.contains( QStringLiteral( "supportedImageFormatTypes" ) ) )
501 {
502 const QString name = QStringLiteral( "(%1)" ).arg( QObject::tr( "All layers" ) );
503 const QString description = serviceData.value( QStringLiteral( "Comments" ) ).toString();
504 visitor( nullptr, ServiceTypeFilter::Raster, Qgis::GeometryType::Unknown, nullptr, name, description, parentUrl, false, crs, format );
505 }
506
507 // Add root ImageServer as layer
508 if ( serviceData.value( QStringLiteral( "serviceDataType" ) ).toString().startsWith( QLatin1String( "esriImageService" ) ) )
509 {
510 const QString name = serviceData.value( QStringLiteral( "name" ) ).toString();
511 const QString description = serviceData.value( QStringLiteral( "description" ) ).toString();
512 visitor( nullptr, ServiceTypeFilter::Raster, Qgis::GeometryType::Unknown, nullptr, name, description, parentUrl, false, crs, format );
513 }
514}
515
516
518
519//
520// QgsArcGisAsyncQuery
521//
522
523QgsArcGisAsyncQuery::QgsArcGisAsyncQuery( QObject *parent )
524 : QObject( parent )
525{
526}
527
528QgsArcGisAsyncQuery::~QgsArcGisAsyncQuery()
529{
530 if ( mReply )
531 mReply->deleteLater();
532}
533
534void QgsArcGisAsyncQuery::start( const QUrl &url, const QString &authCfg, QByteArray *result, bool allowCache, const QgsHttpHeaders &headers, const QString &urlPrefix )
535{
536 mResult = result;
537 QUrl mUrl = url;
538 if ( !urlPrefix.isEmpty() )
539 mUrl = QUrl( urlPrefix + url.toString() );
540 QNetworkRequest request( mUrl );
541
542 headers.updateNetworkRequest( request );
543
544 if ( !authCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, authCfg ) )
545 {
546 const QString error = tr( "network request update failed for authentication config" );
547 emit failed( QStringLiteral( "Network" ), error );
548 return;
549 }
550
551 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsArcGisAsyncQuery" ) );
552 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
553 if ( allowCache )
554 {
555 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
556 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
557 }
558
559 mReply = QgsNetworkAccessManager::instance()->get( request );
560 connect( mReply, &QNetworkReply::finished, this, &QgsArcGisAsyncQuery::handleReply );
561}
562
563void QgsArcGisAsyncQuery::handleReply()
564{
565 mReply->deleteLater();
566 // Handle network errors
567 if ( mReply->error() != QNetworkReply::NoError )
568 {
569 QgsDebugError( QStringLiteral( "Network error: %1" ).arg( mReply->errorString() ) );
570 emit failed( QStringLiteral( "Network error" ), mReply->errorString() );
571 return;
572 }
573
574 // Handle HTTP redirects
575 const QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
576 if ( !QgsVariantUtils::isNull( redirect ) )
577 {
578 QNetworkRequest request = mReply->request();
579 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
580 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsArcGisAsyncQuery" ) );
581 QgsDebugMsgLevel( "redirecting to " + redirect.toUrl().toString(), 2 );
582 request.setUrl( redirect.toUrl() );
583 mReply = QgsNetworkAccessManager::instance()->get( request );
584 connect( mReply, &QNetworkReply::finished, this, &QgsArcGisAsyncQuery::handleReply );
585 return;
586 }
587
588 *mResult = mReply->readAll();
589 mResult = nullptr;
590 emit finished();
591}
592
593//
594// QgsArcGisAsyncParallelQuery
595//
596
597QgsArcGisAsyncParallelQuery::QgsArcGisAsyncParallelQuery( const QString &authcfg, const QgsHttpHeaders &requestHeaders, QObject *parent )
598 : QObject( parent )
599 , mAuthCfg( authcfg )
600 , mRequestHeaders( requestHeaders )
601{
602}
603
604void QgsArcGisAsyncParallelQuery::start( const QVector<QUrl> &urls, QVector<QByteArray> *results, bool allowCache )
605{
606 Q_ASSERT( results->size() == urls.size() );
607 mResults = results;
608 mPendingRequests = mResults->size();
609 for ( int i = 0, n = urls.size(); i < n; ++i )
610 {
611 QNetworkRequest request( urls[i] );
612 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsArcGisAsyncParallelQuery" ) );
613 QgsSetRequestInitiatorId( request, QString::number( i ) );
614
615 mRequestHeaders.updateNetworkRequest( request );
616 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
617 {
618 const QString error = tr( "network request update failed for authentication config" );
619 mErrors.append( error );
620 QgsMessageLog::logMessage( error, tr( "Network" ) );
621 continue;
622 }
623
624 request.setAttribute( QNetworkRequest::HttpPipeliningAllowedAttribute, true );
625 if ( allowCache )
626 {
627 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
628 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
629 request.setRawHeader( "Connection", "keep-alive" );
630 }
631 QNetworkReply *reply = QgsNetworkAccessManager::instance()->get( request );
632 reply->setProperty( "idx", i );
633 connect( reply, &QNetworkReply::finished, this, &QgsArcGisAsyncParallelQuery::handleReply );
634 }
635}
636
637void QgsArcGisAsyncParallelQuery::handleReply()
638{
639 QNetworkReply *reply = qobject_cast<QNetworkReply *>( QObject::sender() );
640 const QVariant redirect = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
641 const int idx = reply->property( "idx" ).toInt();
642 reply->deleteLater();
643 if ( reply->error() != QNetworkReply::NoError )
644 {
645 // Handle network errors
646 mErrors.append( reply->errorString() );
647 --mPendingRequests;
648 }
649 else if ( !QgsVariantUtils::isNull( redirect ) )
650 {
651 // Handle HTTP redirects
652 QNetworkRequest request = reply->request();
653 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
654 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsArcGisAsyncParallelQuery" ) );
655 QgsDebugMsgLevel( "redirecting to " + redirect.toUrl().toString(), 2 );
656 request.setUrl( redirect.toUrl() );
657 reply = QgsNetworkAccessManager::instance()->get( request );
658 reply->setProperty( "idx", idx );
659 connect( reply, &QNetworkReply::finished, this, &QgsArcGisAsyncParallelQuery::handleReply );
660 }
661 else
662 {
663 // All OK
664 ( *mResults )[idx] = reply->readAll();
665 --mPendingRequests;
666 }
667 if ( mPendingRequests == 0 )
668 {
669 emit finished( mErrors );
670 mResults = nullptr;
671 mErrors.clear();
672 }
673}
674
ArcGisRestServiceType
Available ArcGIS REST service types.
Definition qgis.h:4371
@ GeocodeServer
GeocodeServer.
Definition qgis.h:4377
@ SceneServer
SceneServer.
Definition qgis.h:4379
@ Unknown
Other unknown/unsupported type.
Definition qgis.h:4378
@ GlobeServer
GlobeServer.
Definition qgis.h:4375
@ ImageServer
ImageServer.
Definition qgis.h:4374
@ FeatureServer
FeatureServer.
Definition qgis.h:4372
GeometryType
The geometry types are used to group Qgis::WkbType in a coarse way.
Definition qgis.h:358
@ Unknown
Unknown types.
Definition qgis.h:362
@ Null
No geometry.
Definition qgis.h:363
WkbType
The WKB type describes the number of dimensions a geometry has.
Definition qgis.h:277
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())
Performs a blocking request to a URL and returns the retrieved JSON content.
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, const QgsRectangle &filterRect, 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 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 getServiceInfo(const QString &baseurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders=QgsHttpHeaders(), const QString &urlPrefix=QString())
Retrieves JSON service info for the specified base URL.
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 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 addLayerItems(const std::function< void(const QString &parentLayerId, ServiceTypeFilter serviceType, Qgis::GeometryType geometryType, const QString &layerId, const QString &name, const QString &description, const QString &url, bool isParentLayer, const QgsCoordinateReferenceSystem &crs, const QString &format)> &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 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 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())
Performs a blocking request to a URL and returns the retrieved 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 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 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:53
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())
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:61
#define QgsDebugError(str)
Definition qgslogger.h:57
#define QgsSetRequestInitiatorClass(request, _class)
#define QgsSetRequestInitiatorId(request, str)