QGIS API Documentation 4.1.0-Master (376402f9aeb)
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 <ranges>
19
20#include "qgsapplication.h"
21#include "qgsarcgisrestutils.h"
22#include "qgsauthmanager.h"
24#include "qgslogger.h"
25#include "qgsmessagelog.h"
28#include "qgsvariantutils.h"
29
30#include <QCryptographicHash>
31#include <QFile>
32#include <QImageReader>
33#include <QJsonParseError>
34#include <QRegularExpression>
35#include <QString>
36#include <QUrl>
37#include <QUrlQuery>
38
39#include "moc_qgsarcgisrestquery.cpp"
40
41using namespace Qt::StringLiterals;
42
44{
45 if ( url.isEmpty() || !url.isValid() )
46 {
48 }
49
50 const QString path = url.path();
51 if ( path.isEmpty() )
52 {
54 }
55 const QStringList pathSegments = path.split( '/', Qt::SkipEmptyParts );
56
57 // iterate backwards through the URL segments.
58 // we want to catch both root services (.../FeatureServer)
59 // and layer-specific URLs (.../FeatureServer/0 or .../FeatureServer/query)
60 for ( const QString &segment : pathSegments | std::ranges::views::reverse )
61 {
62 if ( segment.compare( "FeatureServer"_L1, Qt::CaseInsensitive ) == 0 )
63 {
65 }
66 if ( segment.compare( "MapServer"_L1, Qt::CaseInsensitive ) == 0 )
67 {
69 }
70 if ( segment.compare( "ImageServer"_L1, Qt::CaseInsensitive ) == 0 )
71 {
73 }
74 if ( segment.compare( "SceneServer"_L1, Qt::CaseInsensitive ) == 0 )
75 {
77 }
78 }
79
81}
82
84{
85 if ( json.empty() )
86 {
88 }
89
90 // try to sniff an imageserver
91 if ( json.contains( u"pixelSizeX"_s )
92 || json.contains( u"bandCount"_s )
93 || json.contains( u"mensurationCapabilities"_s )
94 || json.value( u"serviceDataType"_s ).toString().startsWith( "esriImageService"_L1, Qt::CaseInsensitive ) )
95 {
97 }
98
99 // try to sniff a mapserver
100 if ( json.contains( u"mapName"_s ) || json.contains( u"singleFusedMapCache"_s ) ) // note that imageservers also have this tag, hence why we checked for those first
101 {
103 }
104
105 // try to sniff FeatureServer
106 if ( json.contains( u"hasVersionedData"_s ) || json.contains( u"syncEnabled"_s ) || json.contains( u"allowGeometryUpdates"_s ) || json.contains( u"supportsDisconnectedEditing"_s ) )
107 {
109 }
110
111 // try to sniff SceneServer -- this works for direct layer endpoints (e.g. SceneServer/0)
112 if ( json.contains( u"store"_s ) && json.contains( u"layerType"_s ) )
113 {
115 }
116
117 if ( json.contains( u"layers"_s ) )
118 {
119 const QVariantList layersList = json.value( u"layers"_s ).toList();
120 if ( !layersList.empty() )
121 {
122 const QVariantMap firstLayer = layersList.first().toMap();
123 // "layerType" is distinct to SceneServer layers:
124 if ( firstLayer.contains( u"layerType"_s ) || firstLayer.contains( u"store"_s ) )
125 {
127 }
128 }
129 }
130
131 // catch feature-server layer-level endpoints (e.g. MapServer/0)
132 // this can detect feature servers only -- eg for a mapserver layer it will be flagged
133 // as a feature server
134 if ( json.contains( u"type"_s ) )
135 {
136 const QString typeStr = json.value( u"type"_s ).toString();
137 if ( typeStr.compare( "Feature Layer"_L1, Qt::CaseInsensitive ) == 0 || typeStr.compare( "Table"_L1, Qt::CaseInsensitive ) == 0 )
138 {
140 }
141 }
142
144}
145
147 const QString &baseurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, const QString &urlPrefix, bool forceRefresh
148)
149{
150 // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer?f=json
151 QUrl queryUrl( baseurl );
152 QUrlQuery query( queryUrl );
153 query.addQueryItem( u"f"_s, u"json"_s );
154 queryUrl.setQuery( query );
155 return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, nullptr, urlPrefix, forceRefresh );
156}
157
158QVariantMap QgsArcGisRestQueryUtils::getLayerInfo( const QString &layerurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, const QString &urlPrefix )
159{
160 // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/1?f=json
161 QUrl queryUrl( layerurl );
162 QUrlQuery query( queryUrl );
163 query.addQueryItem( u"f"_s, u"json"_s );
164 queryUrl.setQuery( query );
165 return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, nullptr, urlPrefix );
166}
167
169 const QString &layerurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, const QString &urlPrefix, const QgsRectangle &bbox, const QString &whereClause
170)
171{
172 // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/1/query?where=1%3D1&returnIdsOnly=true&f=json
173 QUrl queryUrl( layerurl + "/query" );
174 QUrlQuery query( queryUrl );
175 query.addQueryItem( u"f"_s, u"json"_s );
176 query.addQueryItem( u"where"_s, whereClause.isEmpty() ? u"1=1"_s : whereClause );
177 query.addQueryItem( u"returnIdsOnly"_s, u"true"_s );
178 if ( !bbox.isNull() )
179 {
180 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 ) );
181 query.addQueryItem( u"geometryType"_s, u"esriGeometryEnvelope"_s );
182 query.addQueryItem( u"spatialRel"_s, u"esriSpatialRelEnvelopeIntersects"_s );
183 }
184 queryUrl.setQuery( query );
185 return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, nullptr, urlPrefix );
186}
187
188QgsRectangle QgsArcGisRestQueryUtils::getExtent( const QString &layerurl, const QString &whereClause, const QString &authcfg, const QgsHttpHeaders &requestHeaders, const QString &urlPrefix )
189{
190 // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/1/query?where=1%3D1&returnExtentOnly=true&f=json
191 QUrl queryUrl( layerurl + "/query" );
192 QUrlQuery query( queryUrl );
193 query.addQueryItem( u"f"_s, u"json"_s );
194 query.addQueryItem( u"where"_s, whereClause );
195 query.addQueryItem( u"returnExtentOnly"_s, u"true"_s );
196 queryUrl.setQuery( query );
197 QString errorTitle;
198 QString errorText;
199 const QVariantMap res = queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, nullptr, urlPrefix );
200 if ( res.isEmpty() )
201 {
202 QgsDebugError( u"getExtent failed: %1 - %2"_s.arg( errorTitle, errorText ) );
203 return QgsRectangle();
204 }
205
206 return QgsArcGisRestUtils::convertRectangle( res.value( u"extent"_s ) );
207}
208
210 const QString &layerurl,
211 const QString &authcfg,
212 const QList<quint32> &objectIds,
213 const QString &crs,
214 bool fetchGeometry,
215 const QStringList &fetchAttributes,
216 bool fetchM,
217 bool fetchZ,
218 QString &errorTitle,
219 QString &errorText,
220 const QgsHttpHeaders &requestHeaders,
221 QgsFeedback *feedback,
222 const QString &urlPrefix
223)
224{
225 QStringList ids;
226 for ( const int id : objectIds )
227 {
228 ids.append( QString::number( id ) );
229 }
230 QUrl queryUrl( layerurl + "/query" );
231 QUrlQuery query( queryUrl );
232 query.addQueryItem( u"f"_s, u"json"_s );
233 query.addQueryItem( u"objectIds"_s, ids.join( ','_L1 ) );
234 if ( !crs.isEmpty() && crs.contains( ':' ) )
235 {
236 const QString wkid = crs.indexOf( ':'_L1 ) >= 0 ? crs.split( ':' )[1] : QString();
237 query.addQueryItem( u"inSR"_s, wkid );
238 query.addQueryItem( u"outSR"_s, wkid );
239 }
240
241 query.addQueryItem( u"returnGeometry"_s, fetchGeometry ? u"true"_s : u"false"_s );
242
243 QString outFields;
244 if ( fetchAttributes.isEmpty() )
245 outFields = u"*"_s;
246 else
247 outFields = fetchAttributes.join( ',' );
248 query.addQueryItem( u"outFields"_s, outFields );
249
250 query.addQueryItem( u"returnM"_s, fetchM ? u"true"_s : u"false"_s );
251 query.addQueryItem( u"returnZ"_s, fetchZ ? u"true"_s : u"false"_s );
252 queryUrl.setQuery( query );
253 return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, feedback, urlPrefix );
254}
255
257 const QString &layerurl,
258 const QgsRectangle &filterRect,
259 QString &errorTitle,
260 QString &errorText,
261 const QString &authcfg,
262 const QgsHttpHeaders &requestHeaders,
263 QgsFeedback *feedback,
264 const QString &whereClause,
265 const QString &urlPrefix
266)
267{
268 QUrl queryUrl( layerurl + "/query" );
269 QUrlQuery query( queryUrl );
270 query.addQueryItem( u"f"_s, u"json"_s );
271 query.addQueryItem( u"where"_s, whereClause.isEmpty() ? u"1=1"_s : whereClause );
272 query.addQueryItem( u"returnIdsOnly"_s, u"true"_s );
273 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 ) );
274 query.addQueryItem( u"geometryType"_s, u"esriGeometryEnvelope"_s );
275 query.addQueryItem( u"spatialRel"_s, u"esriSpatialRelEnvelopeIntersects"_s );
276 queryUrl.setQuery( query );
277 const QVariantMap objectIdData = queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, feedback, urlPrefix );
278
279 if ( objectIdData.isEmpty() )
280 {
281 return QList<quint32>();
282 }
283
284 QList<quint32> ids;
285 const QVariantList objectIdsList = objectIdData[u"objectIds"_s].toList();
286 ids.reserve( objectIdsList.size() );
287 for ( const QVariant &objectId : objectIdsList )
288 {
289 ids << objectId.toInt();
290 }
291 return ids;
292}
293
295 const QUrl &u, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, QgsFeedback *feedback, QString *contentType, const QString &urlPrefix, bool forceRefresh
296)
297{
298 QUrl url = parseUrl( u );
299
300 if ( !urlPrefix.isEmpty() )
301 url = QUrl( urlPrefix + url.toString() );
302
303 QNetworkRequest request( url );
304 QgsSetRequestInitiatorClass( request, u"QgsArcGisRestUtils"_s );
305 requestHeaders.updateNetworkRequest( request );
306
307 QgsBlockingNetworkRequest networkRequest;
308 networkRequest.setAuthCfg( authcfg );
309 const QgsBlockingNetworkRequest::ErrorCode error = networkRequest.get( request, forceRefresh, feedback );
310
311 if ( feedback && feedback->isCanceled() )
312 return QByteArray();
313
314 // Handle network errors
316 {
317 QgsDebugError( u"Network error: %1"_s.arg( networkRequest.errorMessage() ) );
318 errorTitle = u"Network error"_s;
319 errorText = networkRequest.errorMessage();
320
321 // try to get detailed error message from reply
322 const QString content = networkRequest.reply().content();
323 const thread_local QRegularExpression errorRx( u"Error: <.*?>(.*?)<"_s );
324 const QRegularExpressionMatch match = errorRx.match( content );
325 if ( match.hasMatch() )
326 {
327 errorText = match.captured( 1 );
328 }
329
330 return QByteArray();
331 }
332
333 const QgsNetworkReplyContent content = networkRequest.reply();
334 if ( contentType )
335 *contentType = content.rawHeader( "Content-Type" );
336 return content.content();
337}
338
340 const QUrl &url, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders, QgsFeedback *feedback, const QString &urlPrefix, bool forceRefresh
341)
342{
343 const QByteArray reply = queryService( url, authcfg, errorTitle, errorText, requestHeaders, feedback, nullptr, urlPrefix, forceRefresh );
344 if ( !errorTitle.isEmpty() )
345 {
346 return QVariantMap();
347 }
348 if ( feedback && feedback->isCanceled() )
349 return QVariantMap();
350
351 // Parse data
352 QJsonParseError err;
353 const QJsonDocument doc = QJsonDocument::fromJson( reply, &err );
354 if ( doc.isNull() )
355 {
356 errorTitle = u"Parsing error"_s;
357 errorText = err.errorString();
358 QgsDebugError( u"Parsing error: %1"_s.arg( err.errorString() ) );
359 return QVariantMap();
360 }
361 const QVariantMap res = doc.object().toVariantMap();
362 if ( res.contains( u"error"_s ) )
363 {
364 const QVariantMap error = res.value( u"error"_s ).toMap();
365 errorText = error.value( u"message"_s ).toString();
366 errorTitle = QObject::tr( "Error %1" ).arg( error.value( u"code"_s ).toString() );
367 return QVariantMap();
368 }
369 return res;
370}
371
372QUrl QgsArcGisRestQueryUtils::parseUrl( const QUrl &url, bool *isTestEndpoint )
373{
374 if ( isTestEndpoint )
375 *isTestEndpoint = false;
376
377 QUrl modifiedUrl( url );
378 if ( modifiedUrl.toString().contains( "fake_qgis_http_endpoint"_L1 ) )
379 {
380 if ( isTestEndpoint )
381 *isTestEndpoint = true;
382
383 // Just for testing with local files instead of http:// resources
384 QString modifiedUrlString = modifiedUrl.toString();
385 // Qt5 does URL encoding from some reason (of the FILTER parameter for example)
386 modifiedUrlString = QUrl::fromPercentEncoding( modifiedUrlString.toUtf8() );
387 modifiedUrlString.replace( "fake_qgis_http_endpoint/"_L1, "fake_qgis_http_endpoint_"_L1 );
388 QgsDebugMsgLevel( u"Get %1"_s.arg( modifiedUrlString ), 2 );
389 modifiedUrlString = modifiedUrlString.mid( u"http://"_s.size() );
390 QString args = modifiedUrlString.indexOf( '?' ) >= 0 ? modifiedUrlString.mid( modifiedUrlString.indexOf( '?' ) ) : QString();
391 if ( modifiedUrlString.size() > 150 )
392 {
393 args = QCryptographicHash::hash( args.toUtf8(), QCryptographicHash::Md5 ).toHex();
394 }
395 else
396 {
397 args.replace( "?"_L1, "_"_L1 );
398 args.replace( "&"_L1, "_"_L1 );
399 args.replace( "<"_L1, "_"_L1 );
400 args.replace( ">"_L1, "_"_L1 );
401 args.replace( "'"_L1, "_"_L1 );
402 args.replace( "\""_L1, "_"_L1 );
403 args.replace( " "_L1, "_"_L1 );
404 args.replace( ":"_L1, "_"_L1 );
405 args.replace( "/"_L1, "_"_L1 );
406 args.replace( "\n"_L1, "_"_L1 );
407 }
408#ifdef Q_OS_WIN
409 // Passing "urls" like "http://c:/path" to QUrl 'eats' the : after c,
410 // so we must restore it
411 if ( modifiedUrlString[1] == '/' )
412 {
413 modifiedUrlString = modifiedUrlString[0] + ":/" + modifiedUrlString.mid( 2 );
414 }
415#endif
416 modifiedUrlString = modifiedUrlString.mid( 0, modifiedUrlString.indexOf( '?' ) ) + args;
417 QgsDebugMsgLevel( u"Get %1 (after laundering)"_s.arg( modifiedUrlString ), 2 );
418 modifiedUrl = QUrl::fromLocalFile( modifiedUrlString );
419 if ( !QFile::exists( modifiedUrlString ) )
420 {
421 QgsDebugError( u"Local test file %1 for URL %2 does not exist!!!"_s.arg( modifiedUrlString, url.toString() ) );
422 }
423 }
424
425 return modifiedUrl;
426}
427
428void QgsArcGisRestQueryUtils::adjustBaseUrl( QString &baseUrl, const QString &name )
429{
430 const QStringList parts = name.split( '/' );
431 QString checkString;
432 for ( const QString &part : parts )
433 {
434 if ( !checkString.isEmpty() )
435 checkString += QString( '/' );
436
437 checkString += part;
438 if ( baseUrl.indexOf( QRegularExpression( checkString.replace( '/', "\\/"_L1 ) + u"\\/?$"_s ) ) > -1 )
439 {
440 baseUrl = baseUrl.left( baseUrl.length() - checkString.length() - 1 );
441 break;
442 }
443 }
444}
445
446void QgsArcGisRestQueryUtils::visitFolderItems( const std::function< void( const QString &, const QString & ) > &visitor, const QVariantMap &serviceData, const QString &baseUrl )
447{
448 QString base( baseUrl );
449 bool baseChecked = false;
450 if ( !base.endsWith( '/' ) )
451 base += '/'_L1;
452
453 const QStringList folderList = serviceData.value( u"folders"_s ).toStringList();
454 for ( const QString &folder : folderList )
455 {
456 if ( !baseChecked )
457 {
458 adjustBaseUrl( base, folder );
459 baseChecked = true;
460 }
461 visitor( folder, base + folder );
462 }
463}
464
465void QgsArcGisRestQueryUtils::visitServiceItems( const std::function<void( const QString &, const QString &, Qgis::ArcGisRestServiceType )> &visitor, const QVariantMap &serviceData, const QString &baseUrl )
466{
467 QString base( baseUrl );
468 bool baseChecked = false;
469 if ( !base.endsWith( '/' ) )
470 base += '/'_L1;
471
472 const QVariantList serviceList = serviceData.value( u"services"_s ).toList();
473 for ( const QVariant &service : serviceList )
474 {
475 const QVariantMap serviceMap = service.toMap();
476 const QString serviceTypeString = serviceMap.value( u"type"_s ).toString();
477 const Qgis::ArcGisRestServiceType serviceType = QgsArcGisRestUtils::serviceTypeFromString( serviceTypeString );
478
479 switch ( serviceType )
480 {
485 // supported
486 break;
487
492 // unsupported
493 continue;
494 }
495
496 const QString serviceName = serviceMap.value( u"name"_s ).toString();
497 const QString displayName = serviceName.split( '/' ).last();
498 if ( !baseChecked )
499 {
500 adjustBaseUrl( base, serviceName );
501 baseChecked = true;
502 }
503
504 visitor( displayName, base + serviceName + '/' + serviceTypeString, serviceType );
505 }
506}
507
509 const std::function< void( const LayerItemDetails &details )> &visitor, const QVariantMap &serviceData, const QString &parentUrl, const QString &parentSupportedFormats, Qgis::ArcGisRestServiceType serviceType
510)
511{
512 const QgsCoordinateReferenceSystem crs = QgsArcGisRestUtils::convertSpatialReference( serviceData.value( u"spatialReference"_s ).toMap() );
513
514 bool found = false;
515 const QList<QByteArray> supportedFormats = QImageReader::supportedImageFormats();
516 const QStringList supportedImageFormatTypes = serviceData.value( u"supportedImageFormatTypes"_s ).toString().isEmpty() ? parentSupportedFormats.split( ',' )
517 : serviceData.value( u"supportedImageFormatTypes"_s ).toString().split( ',' );
518 QString format = supportedImageFormatTypes.value( 0 );
519 for ( const QString &encoding : supportedImageFormatTypes )
520 {
521 for ( const QByteArray &fmt : supportedFormats )
522 {
523 if ( encoding.startsWith( fmt, Qt::CaseInsensitive ) )
524 {
525 format = encoding;
526 found = true;
527 break;
528 }
529 }
530 if ( found )
531 break;
532 }
533 Qgis::ArcGisRestServiceCapabilities capabilities = QgsArcGisRestUtils::serviceCapabilitiesFromString( serviceData.value( u"capabilities"_s ).toString() );
534 if ( serviceType == Qgis::ArcGisRestServiceType::ImageServer )
535 {
536 // consider ImageServices as having both render and query capabilities, so we can load them
537 // as either raster or vector
538 capabilities.setFlag( Qgis::ArcGisRestServiceCapability::Map, true );
539 capabilities.setFlag( Qgis::ArcGisRestServiceCapability::Query, true );
540 }
541
542 const QVariantList layerInfoList = serviceData.value( u"layers"_s ).toList();
543 for ( const QVariant &layerInfo : layerInfoList )
544 {
545 const QVariantMap layerInfoMap = layerInfo.toMap();
546
547 LayerItemDetails details;
548 details.layerId = layerInfoMap.value( u"id"_s ).toString();
549 details.parentLayerId = layerInfoMap.value( u"parentLayerId"_s ).toString();
550 details.name = layerInfoMap.value( u"name"_s ).toString();
551 details.description = layerInfoMap.value( u"description"_s ).toString();
552
553 const QString geometryType = layerInfoMap.value( u"geometryType"_s ).toString();
554#if 0
555 // we have a choice here -- if geometryType is unknown and the service reflects that it supports Map capabilities,
556 // then we can't be sure whether or not the individual sublayers support Query or Map requests only. So we either:
557 // 1. Send off additional requests for each individual layer's capabilities (too expensive)
558 // 2. Err on the side of only showing services we KNOW will work for layer -- but this has the side effect that layers
559 // which ARE available as feature services will only show as raster mapserver layers, which is VERY bad/restrictive
560 // 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
561 // server configuration
562 // We opt for 3, because otherwise we're making it impossible for users to load valid vector layers into QGIS
563
564 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Map ) )
565 {
566 if ( geometryType.isEmpty() )
567 continue;
568 }
569#endif
570
571 // deal with the easy stuff first -- if we found a scene server layer, then it can ONLY be loaded as a scene
572 if ( serviceType == Qgis::ArcGisRestServiceType::SceneServer )
573 {
576 details.url = parentUrl;
577 details.isParentLayer = false;
578 details.crs = crs;
579 details.format = format;
580 details.isMapServerWithQueryCapability = false;
581 visitor( details );
582 continue;
583 }
584
585 // Yes, potentially we may visit twice, once as as a raster (if applicable), and once as a vector (if applicable)!
586 bool exposedAsVector = false;
588 {
589 exposedAsVector = true;
590 const Qgis::WkbType wkbType = QgsArcGisRestUtils::convertGeometryType( geometryType );
592 details.geometryType = QgsWkbTypes::geometryType( wkbType );
593 details.url = parentUrl + '/' + details.layerId;
594 details.format = format;
596 if ( !layerInfoMap.value( u"subLayerIds"_s ).toList().empty() )
597 {
598 details.isParentLayer = true;
600 visitor( details );
601 }
602 else
603 {
604 details.isParentLayer = false;
605 details.crs = crs;
606 visitor( details );
607 }
608 }
609
610 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Map ) && ( serviceType == Qgis::ArcGisRestServiceType::FeatureServer || serviceType == Qgis::ArcGisRestServiceType::MapServer ) )
611 {
613 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Query ) )
614 wkbType = QgsArcGisRestUtils::convertGeometryType( geometryType );
615
617 details.geometryType = QgsWkbTypes::geometryType( wkbType );
618 details.url = parentUrl + '/' + details.layerId;
619 details.format = format;
620 details.isMapServerWithQueryCapability = exposedAsVector;
621 if ( !layerInfoMap.value( u"subLayerIds"_s ).toList().empty() )
622 {
623 if ( !exposedAsVector )
624 {
625 details.isParentLayer = true;
627 visitor( details );
628 }
629 }
630 else
631 {
632 details.isParentLayer = false;
633 details.crs = crs;
634 visitor( details );
635 }
636 }
637 }
638
639 const QVariantList tableInfoList = serviceData.value( u"tables"_s ).toList();
640 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Query ) && serviceType == Qgis::ArcGisRestServiceType::FeatureServer )
641 {
642 for ( const QVariant &tableInfo : tableInfoList )
643 {
644 const QVariantMap tableInfoMap = tableInfo.toMap();
645
646 LayerItemDetails details;
647 details.layerId = tableInfoMap.value( u"id"_s ).toString();
648 details.parentLayerId = tableInfoMap.value( u"parentLayerId"_s ).toString();
649 details.name = tableInfoMap.value( u"name"_s ).toString();
650 details.description = tableInfoMap.value( u"description"_s ).toString();
651
654 details.url = parentUrl + '/' + details.layerId;
655 details.format = format;
656 details.isMapServerWithQueryCapability = false;
657 if ( !tableInfoMap.value( u"subLayerIds"_s ).toList().empty() )
658 {
659 details.isParentLayer = true;
661 visitor( details );
662 }
663 else
664 {
665 details.isParentLayer = false;
666 details.crs = crs;
667 visitor( details );
668 }
669 }
670 }
671
672 if ( layerInfoList.isEmpty() && tableInfoList.isEmpty() && serviceType != Qgis::ArcGisRestServiceType::Unknown )
673 {
674 // haven't found any layers yet. But maybe the definition is for a layer itself.
675 switch ( serviceType )
676 {
678 {
679 LayerItemDetails details;
680 details.layerId = serviceData.value( u"id"_s ).toString();
681 details.name = serviceData.value( u"name"_s ).toString();
682 details.description = serviceData.value( u"description"_s ).toString();
683 const QString geometryType = serviceData.value( u"geometryType"_s ).toString();
684 const Qgis::WkbType wkbType = QgsArcGisRestUtils::convertGeometryType( geometryType );
686 details.geometryType = QgsWkbTypes::geometryType( wkbType );
687 details.url = parentUrl;
688 details.format = format;
690 details.isParentLayer = false;
691 details.crs = crs;
692 visitor( details );
693 break;
694 }
696 {
697 LayerItemDetails details;
698 details.name = serviceData.value( u"serviceDescription"_s ).toString();
699 details.description = serviceData.value( u"description"_s ).toString();
702 details.url = parentUrl;
703 details.isParentLayer = false;
704 details.crs = crs;
705 details.format = format;
706 details.isMapServerWithQueryCapability = false;
707 details.isMapServerSpecialAllLayersOption = false;
708 visitor( details );
709 break;
710 }
712 {
713 LayerItemDetails details;
714 details.layerId = QString();
715 details.parentLayerId = QString();
716 details.name = serviceData.value( u"name"_s ).toString();
717 details.description = serviceData.value( u"description"_s ).toString();
720 details.url = parentUrl;
721 details.isParentLayer = false;
722 details.crs = crs;
723 details.format = format;
724 details.isMapServerWithQueryCapability = false;
725 visitor( details );
726 break;
727 }
729 {
730 LayerItemDetails details;
731 details.layerId = serviceData.value( u"id"_s ).toString();
732 details.name = serviceData.value( u"name"_s ).toString();
733 details.description = serviceData.value( u"description"_s ).toString();
736 details.url = parentUrl;
737 details.isParentLayer = false;
738 details.crs = crs;
739 details.format = format;
740 details.isMapServerWithQueryCapability = false;
741 visitor( details );
742 break;
743 }
748 break;
749 }
750 }
751
752 // Add root MapServer as raster layer when multiple layers are listed
753 if ( serviceType != Qgis::ArcGisRestServiceType::FeatureServer && layerInfoList.count() > 1 && serviceData.contains( u"supportedImageFormatTypes"_s ) )
754 {
755 LayerItemDetails details;
756 details.parentLayerId = QString();
759 details.url = parentUrl;
760 details.isParentLayer = false;
761 details.crs = crs;
762 details.format = format;
763 details.isMapServerWithQueryCapability = false;
765 visitor( details );
766 }
767}
768
769
771
772//
773// QgsArcGisAsyncQuery
774//
775
776QgsArcGisAsyncQuery::QgsArcGisAsyncQuery( QObject *parent )
777 : QObject( parent )
778{}
779
780QgsArcGisAsyncQuery::~QgsArcGisAsyncQuery()
781{
782 if ( mReply )
783 mReply->deleteLater();
784}
785
786void QgsArcGisAsyncQuery::start( const QUrl &url, const QString &authCfg, QByteArray *result, bool allowCache, const QgsHttpHeaders &headers, const QString &urlPrefix )
787{
788 mResult = result;
789 QUrl mUrl = url;
790 if ( !urlPrefix.isEmpty() )
791 mUrl = QUrl( urlPrefix + url.toString() );
792 QNetworkRequest request( mUrl );
793
794 headers.updateNetworkRequest( request );
795
796 if ( !authCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, authCfg ) )
797 {
798 const QString error = tr( "network request update failed for authentication config" );
799 emit failed( u"Network"_s, error );
800 return;
801 }
802
803 QgsSetRequestInitiatorClass( request, u"QgsArcGisAsyncQuery"_s );
804 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
805 if ( allowCache )
806 {
807 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
808 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
809 }
810
811 mReply = QgsNetworkAccessManager::instance()->get( request );
812 connect( mReply, &QNetworkReply::finished, this, &QgsArcGisAsyncQuery::handleReply );
813}
814
815void QgsArcGisAsyncQuery::handleReply()
816{
817 mReply->deleteLater();
818 // Handle network errors
819 if ( mReply->error() != QNetworkReply::NoError )
820 {
821 QgsDebugError( u"Network error: %1"_s.arg( mReply->errorString() ) );
822 emit failed( u"Network error"_s, mReply->errorString() );
823 return;
824 }
825
826 // Handle HTTP redirects
827 const QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
828 if ( !QgsVariantUtils::isNull( redirect ) )
829 {
830 QNetworkRequest request = mReply->request();
831 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
832 QgsSetRequestInitiatorClass( request, u"QgsArcGisAsyncQuery"_s );
833 QgsDebugMsgLevel( "redirecting to " + redirect.toUrl().toString(), 2 );
834 request.setUrl( redirect.toUrl() );
835 mReply = QgsNetworkAccessManager::instance()->get( request );
836 connect( mReply, &QNetworkReply::finished, this, &QgsArcGisAsyncQuery::handleReply );
837 return;
838 }
839
840 *mResult = mReply->readAll();
841 mResult = nullptr;
842 emit finished();
843}
844
845//
846// QgsArcGisAsyncParallelQuery
847//
848
849QgsArcGisAsyncParallelQuery::QgsArcGisAsyncParallelQuery( const QString &authcfg, const QgsHttpHeaders &requestHeaders, QObject *parent )
850 : QObject( parent )
851 , mAuthCfg( authcfg )
852 , mRequestHeaders( requestHeaders )
853{}
854
855void QgsArcGisAsyncParallelQuery::start( const QVector<QUrl> &urls, QVector<QByteArray> *results, bool allowCache )
856{
857 Q_ASSERT( results->size() == urls.size() );
858 mResults = results;
859 mPendingRequests = mResults->size();
860 for ( int i = 0, n = urls.size(); i < n; ++i )
861 {
862 QNetworkRequest request( urls[i] );
863 QgsSetRequestInitiatorClass( request, u"QgsArcGisAsyncParallelQuery"_s );
864 QgsSetRequestInitiatorId( request, QString::number( i ) );
865
866 mRequestHeaders.updateNetworkRequest( request );
867 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
868 {
869 const QString error = tr( "network request update failed for authentication config" );
870 mErrors.append( error );
871 QgsMessageLog::logMessage( error, tr( "Network" ) );
872 continue;
873 }
874
875 request.setAttribute( QNetworkRequest::HttpPipeliningAllowedAttribute, true );
876 if ( allowCache )
877 {
878 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
879 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
880 request.setRawHeader( "Connection", "keep-alive" );
881 }
882 QNetworkReply *reply = QgsNetworkAccessManager::instance()->get( request );
883 reply->setProperty( "idx", i );
884 connect( reply, &QNetworkReply::finished, this, &QgsArcGisAsyncParallelQuery::handleReply );
885 }
886}
887
888void QgsArcGisAsyncParallelQuery::handleReply()
889{
890 QNetworkReply *reply = qobject_cast<QNetworkReply *>( QObject::sender() );
891 const QVariant redirect = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
892 const int idx = reply->property( "idx" ).toInt();
893 reply->deleteLater();
894 if ( reply->error() != QNetworkReply::NoError )
895 {
896 // Handle network errors
897 mErrors.append( reply->errorString() );
898 --mPendingRequests;
899 }
900 else if ( !QgsVariantUtils::isNull( redirect ) )
901 {
902 // Handle HTTP redirects
903 QNetworkRequest request = reply->request();
904 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
905 QgsSetRequestInitiatorClass( request, u"QgsArcGisAsyncParallelQuery"_s );
906 QgsDebugMsgLevel( "redirecting to " + redirect.toUrl().toString(), 2 );
907 request.setUrl( redirect.toUrl() );
908 reply = QgsNetworkAccessManager::instance()->get( request );
909 reply->setProperty( "idx", idx );
910 connect( reply, &QNetworkReply::finished, this, &QgsArcGisAsyncParallelQuery::handleReply );
911 }
912 else
913 {
914 // All OK
915 ( *mResults )[idx] = reply->readAll();
916 --mPendingRequests;
917 }
918 if ( mPendingRequests == 0 )
919 {
920 emit finished( mErrors );
921 mResults = nullptr;
922 mErrors.clear();
923 }
924}
925
ArcGisRestServiceType
Available ArcGIS REST service types.
Definition qgis.h:4643
@ GeocodeServer
GeocodeServer.
Definition qgis.h:4649
@ SceneServer
SceneServer.
Definition qgis.h:4651
@ Unknown
Other unknown/unsupported type.
Definition qgis.h:4650
@ GlobeServer
GlobeServer.
Definition qgis.h:4647
@ ImageServer
ImageServer.
Definition qgis.h:4646
@ FeatureServer
FeatureServer.
Definition qgis.h:4644
@ 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:4680
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 Qgis::ArcGisRestServiceType sniffServiceTypeFromJson(const QVariantMap &json)
Attempts to resolve the service type from a json definition.
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 Qgis::ArcGisRestServiceType sniffServiceTypeFromUrl(const QUrl &url)
Attempts to resolve the service type from a url.
static void addLayerItems(const std::function< void(const LayerItemDetails &details)> &visitor, const QVariantMap &serviceData, const QString &parentUrl, const QString &parentSupportedFormats, Qgis::ArcGisRestServiceType serviceType)
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 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 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)
QLineF segment(int index, QRectF rect, double radius)
Encapsulates details relating to a layer item.
Qgis::ArcGisRestServiceType 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.