QGIS API Documentation 4.1.0-Master (d6fb7a379fb)
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
418 if ( modifiedUrlString.contains( "tile/" ) )
419 {
420 modifiedUrlString = modifiedUrlString.left( modifiedUrlString.indexOf( "tile/" ) + 4 ) + modifiedUrlString.mid( modifiedUrlString.indexOf( "tile/" ) + 4 ).replace( '/', '_' );
421 }
422
423 QgsDebugMsgLevel( u"Get %1 (after laundering)"_s.arg( modifiedUrlString ), 2 );
424 modifiedUrl = QUrl::fromLocalFile( modifiedUrlString );
425 if ( !QFile::exists( modifiedUrlString ) )
426 {
427 QgsDebugError( u"Local test file %1 for URL %2 does not exist!!!"_s.arg( modifiedUrlString, url.toString() ) );
428 }
429 }
430
431 return modifiedUrl;
432}
433
434void QgsArcGisRestQueryUtils::adjustBaseUrl( QString &baseUrl, const QString &name )
435{
436 const QStringList parts = name.split( '/' );
437 QString checkString;
438 for ( const QString &part : parts )
439 {
440 if ( !checkString.isEmpty() )
441 checkString += QString( '/' );
442
443 checkString += part;
444 if ( baseUrl.indexOf( QRegularExpression( checkString.replace( '/', "\\/"_L1 ) + u"\\/?$"_s ) ) > -1 )
445 {
446 baseUrl = baseUrl.left( baseUrl.length() - checkString.length() - 1 );
447 break;
448 }
449 }
450}
451
452void QgsArcGisRestQueryUtils::visitFolderItems( const std::function< void( const QString &, const QString & ) > &visitor, const QVariantMap &serviceData, const QString &baseUrl )
453{
454 QString base( baseUrl );
455 bool baseChecked = false;
456 if ( !base.endsWith( '/' ) )
457 base += '/'_L1;
458
459 const QStringList folderList = serviceData.value( u"folders"_s ).toStringList();
460 for ( const QString &folder : folderList )
461 {
462 if ( !baseChecked )
463 {
464 adjustBaseUrl( base, folder );
465 baseChecked = true;
466 }
467 visitor( folder, base + folder );
468 }
469}
470
471void QgsArcGisRestQueryUtils::visitServiceItems( const std::function<void( const QString &, const QString &, Qgis::ArcGisRestServiceType )> &visitor, const QVariantMap &serviceData, const QString &baseUrl )
472{
473 QString base( baseUrl );
474 bool baseChecked = false;
475 if ( !base.endsWith( '/' ) )
476 base += '/'_L1;
477
478 const QVariantList serviceList = serviceData.value( u"services"_s ).toList();
479 for ( const QVariant &service : serviceList )
480 {
481 const QVariantMap serviceMap = service.toMap();
482 const QString serviceTypeString = serviceMap.value( u"type"_s ).toString();
483 const Qgis::ArcGisRestServiceType serviceType = QgsArcGisRestUtils::serviceTypeFromString( serviceTypeString );
484
485 switch ( serviceType )
486 {
491 // supported
492 break;
493
498 // unsupported
499 continue;
500 }
501
502 const QString serviceName = serviceMap.value( u"name"_s ).toString();
503 const QString displayName = serviceName.split( '/' ).last();
504 if ( !baseChecked )
505 {
506 adjustBaseUrl( base, serviceName );
507 baseChecked = true;
508 }
509
510 visitor( displayName, base + serviceName + '/' + serviceTypeString, serviceType );
511 }
512}
513
515 const std::function< void( const LayerItemDetails &details )> &visitor, const QVariantMap &serviceData, const QString &parentUrl, const QString &parentSupportedFormats, Qgis::ArcGisRestServiceType serviceType
516)
517{
518 const QgsCoordinateReferenceSystem crs = QgsArcGisRestUtils::convertSpatialReference( serviceData.value( u"spatialReference"_s ).toMap() );
519
520 bool found = false;
521 const QList<QByteArray> supportedFormats = QImageReader::supportedImageFormats();
522 const QStringList supportedImageFormatTypes = serviceData.value( u"supportedImageFormatTypes"_s ).toString().isEmpty() ? parentSupportedFormats.split( ',' )
523 : serviceData.value( u"supportedImageFormatTypes"_s ).toString().split( ',' );
524 QString format = supportedImageFormatTypes.value( 0 );
525 for ( const QString &encoding : supportedImageFormatTypes )
526 {
527 for ( const QByteArray &fmt : supportedFormats )
528 {
529 if ( encoding.startsWith( fmt, Qt::CaseInsensitive ) )
530 {
531 format = encoding;
532 found = true;
533 break;
534 }
535 }
536 if ( found )
537 break;
538 }
539 Qgis::ArcGisRestServiceCapabilities capabilities = QgsArcGisRestUtils::serviceCapabilitiesFromString( serviceData.value( u"capabilities"_s ).toString() );
540 if ( serviceType == Qgis::ArcGisRestServiceType::ImageServer )
541 {
542 // consider ImageServices as having both render and query capabilities, so we can load them
543 // as either raster or vector
544 capabilities.setFlag( Qgis::ArcGisRestServiceCapability::Map, true );
545 capabilities.setFlag( Qgis::ArcGisRestServiceCapability::Query, true );
546 }
547
548 const QVariantList layerInfoList = serviceData.value( u"layers"_s ).toList();
549 for ( const QVariant &layerInfo : layerInfoList )
550 {
551 const QVariantMap layerInfoMap = layerInfo.toMap();
552
553 LayerItemDetails details;
554 details.layerId = layerInfoMap.value( u"id"_s ).toString();
555 details.parentLayerId = layerInfoMap.value( u"parentLayerId"_s ).toString();
556 details.name = layerInfoMap.value( u"name"_s ).toString();
557 details.description = layerInfoMap.value( u"description"_s ).toString();
558
559 const QString geometryType = layerInfoMap.value( u"geometryType"_s ).toString();
560#if 0
561 // we have a choice here -- if geometryType is unknown and the service reflects that it supports Map capabilities,
562 // then we can't be sure whether or not the individual sublayers support Query or Map requests only. So we either:
563 // 1. Send off additional requests for each individual layer's capabilities (too expensive)
564 // 2. Err on the side of only showing services we KNOW will work for layer -- but this has the side effect that layers
565 // which ARE available as feature services will only show as raster mapserver layers, which is VERY bad/restrictive
566 // 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
567 // server configuration
568 // We opt for 3, because otherwise we're making it impossible for users to load valid vector layers into QGIS
569
570 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Map ) )
571 {
572 if ( geometryType.isEmpty() )
573 continue;
574 }
575#endif
576
577 // deal with the easy stuff first -- if we found a scene server layer, then it can ONLY be loaded as a scene
578 if ( serviceType == Qgis::ArcGisRestServiceType::SceneServer )
579 {
582 details.url = parentUrl;
583 details.isParentLayer = false;
584 details.crs = crs;
585 details.format = format;
586 details.isMapServerWithQueryCapability = false;
587 visitor( details );
588 continue;
589 }
590
591 // Yes, potentially we may visit twice, once as as a raster (if applicable), and once as a vector (if applicable)!
592 bool exposedAsVector = false;
594 {
595 exposedAsVector = true;
596 const Qgis::WkbType wkbType = QgsArcGisRestUtils::convertGeometryType( geometryType );
598 details.geometryType = QgsWkbTypes::geometryType( wkbType );
599 details.url = parentUrl + '/' + details.layerId;
600 details.format = format;
602 if ( !layerInfoMap.value( u"subLayerIds"_s ).toList().empty() )
603 {
604 details.isParentLayer = true;
606 visitor( details );
607 }
608 else
609 {
610 details.isParentLayer = false;
611 details.crs = crs;
612 visitor( details );
613 }
614 }
615
616 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Map ) && ( serviceType == Qgis::ArcGisRestServiceType::FeatureServer || serviceType == Qgis::ArcGisRestServiceType::MapServer ) )
617 {
619 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Query ) )
620 wkbType = QgsArcGisRestUtils::convertGeometryType( geometryType );
621
623 details.geometryType = QgsWkbTypes::geometryType( wkbType );
624 details.url = parentUrl + '/' + details.layerId;
625 details.format = format;
626 details.isMapServerWithQueryCapability = exposedAsVector;
627 if ( !layerInfoMap.value( u"subLayerIds"_s ).toList().empty() )
628 {
629 if ( !exposedAsVector )
630 {
631 details.isParentLayer = true;
633 visitor( details );
634 }
635 }
636 else
637 {
638 details.isParentLayer = false;
639 details.crs = crs;
640 visitor( details );
641 }
642 }
643 }
644
645 const QVariantList tableInfoList = serviceData.value( u"tables"_s ).toList();
646 if ( capabilities.testFlag( Qgis::ArcGisRestServiceCapability::Query ) && serviceType == Qgis::ArcGisRestServiceType::FeatureServer )
647 {
648 for ( const QVariant &tableInfo : tableInfoList )
649 {
650 const QVariantMap tableInfoMap = tableInfo.toMap();
651
652 LayerItemDetails details;
653 details.layerId = tableInfoMap.value( u"id"_s ).toString();
654 details.parentLayerId = tableInfoMap.value( u"parentLayerId"_s ).toString();
655 details.name = tableInfoMap.value( u"name"_s ).toString();
656 details.description = tableInfoMap.value( u"description"_s ).toString();
657
660 details.url = parentUrl + '/' + details.layerId;
661 details.format = format;
662 details.isMapServerWithQueryCapability = false;
663 if ( !tableInfoMap.value( u"subLayerIds"_s ).toList().empty() )
664 {
665 details.isParentLayer = true;
667 visitor( details );
668 }
669 else
670 {
671 details.isParentLayer = false;
672 details.crs = crs;
673 visitor( details );
674 }
675 }
676 }
677
678 if ( layerInfoList.isEmpty() && tableInfoList.isEmpty() && serviceType != Qgis::ArcGisRestServiceType::Unknown )
679 {
680 // haven't found any layers yet. But maybe the definition is for a layer itself.
681 switch ( serviceType )
682 {
684 {
685 LayerItemDetails details;
686 details.layerId = serviceData.value( u"id"_s ).toString();
687 details.name = serviceData.value( u"name"_s ).toString();
688 details.description = serviceData.value( u"description"_s ).toString();
689 const QString geometryType = serviceData.value( u"geometryType"_s ).toString();
690 const Qgis::WkbType wkbType = QgsArcGisRestUtils::convertGeometryType( geometryType );
692 details.geometryType = QgsWkbTypes::geometryType( wkbType );
693 details.url = parentUrl;
694 details.format = format;
696 details.isParentLayer = false;
697 details.crs = crs;
698 visitor( details );
699 break;
700 }
702 {
703 LayerItemDetails details;
704 details.name = serviceData.value( u"serviceDescription"_s ).toString();
705 details.description = serviceData.value( u"description"_s ).toString();
708 details.url = parentUrl;
709 details.isParentLayer = false;
710 details.crs = crs;
711 details.format = format;
712 details.isMapServerWithQueryCapability = false;
713 details.isMapServerSpecialAllLayersOption = false;
714 visitor( details );
715 break;
716 }
718 {
719 LayerItemDetails details;
720 details.layerId = QString();
721 details.parentLayerId = QString();
722 details.name = serviceData.value( u"name"_s ).toString();
723 details.description = serviceData.value( u"description"_s ).toString();
726 details.url = parentUrl;
727 details.isParentLayer = false;
728 details.crs = crs;
729 details.format = format;
730 details.isMapServerWithQueryCapability = false;
731 visitor( details );
732 break;
733 }
735 {
736 LayerItemDetails details;
737 details.layerId = serviceData.value( u"id"_s ).toString();
738 details.name = serviceData.value( u"name"_s ).toString();
739 details.description = serviceData.value( u"description"_s ).toString();
742 details.url = parentUrl;
743 details.isParentLayer = false;
744 details.crs = crs;
745 details.format = format;
746 details.isMapServerWithQueryCapability = false;
747 visitor( details );
748 break;
749 }
754 break;
755 }
756 }
757
758 // Add root MapServer as raster layer when multiple layers are listed
759 if ( serviceType != Qgis::ArcGisRestServiceType::FeatureServer && layerInfoList.count() > 1 && serviceData.contains( u"supportedImageFormatTypes"_s ) )
760 {
761 LayerItemDetails details;
762 details.parentLayerId = QString();
765 details.url = parentUrl;
766 details.isParentLayer = false;
767 details.crs = crs;
768 details.format = format;
769 details.isMapServerWithQueryCapability = false;
771 visitor( details );
772 }
773}
774
775
777
778//
779// QgsArcGisAsyncQuery
780//
781
782QgsArcGisAsyncQuery::QgsArcGisAsyncQuery( QObject *parent )
783 : QObject( parent )
784{}
785
786QgsArcGisAsyncQuery::~QgsArcGisAsyncQuery()
787{
788 if ( mReply )
789 mReply->deleteLater();
790}
791
792void QgsArcGisAsyncQuery::start( const QUrl &url, const QString &authCfg, QByteArray *result, bool allowCache, const QgsHttpHeaders &headers, const QString &urlPrefix )
793{
794 mResult = result;
795 QUrl mUrl = url;
796 if ( !urlPrefix.isEmpty() )
797 mUrl = QUrl( urlPrefix + url.toString() );
798 QNetworkRequest request( mUrl );
799
800 headers.updateNetworkRequest( request );
801
802 if ( !authCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, authCfg ) )
803 {
804 const QString error = tr( "network request update failed for authentication config" );
805 emit failed( u"Network"_s, error );
806 return;
807 }
808
809 QgsSetRequestInitiatorClass( request, u"QgsArcGisAsyncQuery"_s );
810 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
811 if ( allowCache )
812 {
813 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
814 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
815 }
816
817 mReply = QgsNetworkAccessManager::instance()->get( request );
818 connect( mReply, &QNetworkReply::finished, this, &QgsArcGisAsyncQuery::handleReply );
819}
820
821void QgsArcGisAsyncQuery::handleReply()
822{
823 mReply->deleteLater();
824 // Handle network errors
825 if ( mReply->error() != QNetworkReply::NoError )
826 {
827 QgsDebugError( u"Network error: %1"_s.arg( mReply->errorString() ) );
828 emit failed( u"Network error"_s, mReply->errorString() );
829 return;
830 }
831
832 // Handle HTTP redirects
833 const QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
834 if ( !QgsVariantUtils::isNull( redirect ) )
835 {
836 QNetworkRequest request = mReply->request();
837 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
838 QgsSetRequestInitiatorClass( request, u"QgsArcGisAsyncQuery"_s );
839 QgsDebugMsgLevel( "redirecting to " + redirect.toUrl().toString(), 2 );
840 request.setUrl( redirect.toUrl() );
841 mReply = QgsNetworkAccessManager::instance()->get( request );
842 connect( mReply, &QNetworkReply::finished, this, &QgsArcGisAsyncQuery::handleReply );
843 return;
844 }
845
846 *mResult = mReply->readAll();
847 mResult = nullptr;
848 emit finished();
849}
850
851//
852// QgsArcGisAsyncParallelQuery
853//
854
855QgsArcGisAsyncParallelQuery::QgsArcGisAsyncParallelQuery( const QString &authcfg, const QgsHttpHeaders &requestHeaders, QObject *parent )
856 : QObject( parent )
857 , mAuthCfg( authcfg )
858 , mRequestHeaders( requestHeaders )
859{}
860
861void QgsArcGisAsyncParallelQuery::start( const QVector<QUrl> &urls, QVector<QByteArray> *results, bool allowCache )
862{
863 Q_ASSERT( results->size() == urls.size() );
864 mResults = results;
865 mPendingRequests = mResults->size();
866 for ( int i = 0, n = urls.size(); i < n; ++i )
867 {
868 QNetworkRequest request( urls[i] );
869 QgsSetRequestInitiatorClass( request, u"QgsArcGisAsyncParallelQuery"_s );
870 QgsSetRequestInitiatorId( request, QString::number( i ) );
871
872 mRequestHeaders.updateNetworkRequest( request );
873 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
874 {
875 const QString error = tr( "network request update failed for authentication config" );
876 mErrors.append( error );
877 QgsMessageLog::logMessage( error, tr( "Network" ) );
878 continue;
879 }
880
881 request.setAttribute( QNetworkRequest::HttpPipeliningAllowedAttribute, true );
882 if ( allowCache )
883 {
884 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
885 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
886 request.setRawHeader( "Connection", "keep-alive" );
887 }
888 QNetworkReply *reply = QgsNetworkAccessManager::instance()->get( request );
889 reply->setProperty( "idx", i );
890 connect( reply, &QNetworkReply::finished, this, &QgsArcGisAsyncParallelQuery::handleReply );
891 }
892}
893
894void QgsArcGisAsyncParallelQuery::handleReply()
895{
896 QNetworkReply *reply = qobject_cast<QNetworkReply *>( QObject::sender() );
897 const QVariant redirect = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
898 const int idx = reply->property( "idx" ).toInt();
899 reply->deleteLater();
900 if ( reply->error() != QNetworkReply::NoError )
901 {
902 // Handle network errors
903 mErrors.append( reply->errorString() );
904 --mPendingRequests;
905 }
906 else if ( !QgsVariantUtils::isNull( redirect ) )
907 {
908 // Handle HTTP redirects
909 QNetworkRequest request = reply->request();
910 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
911 QgsSetRequestInitiatorClass( request, u"QgsArcGisAsyncParallelQuery"_s );
912 QgsDebugMsgLevel( "redirecting to " + redirect.toUrl().toString(), 2 );
913 request.setUrl( redirect.toUrl() );
914 reply = QgsNetworkAccessManager::instance()->get( request );
915 reply->setProperty( "idx", idx );
916 connect( reply, &QNetworkReply::finished, this, &QgsArcGisAsyncParallelQuery::handleReply );
917 }
918 else
919 {
920 // All OK
921 ( *mResults )[idx] = reply->readAll();
922 --mPendingRequests;
923 }
924 if ( mPendingRequests == 0 )
925 {
926 emit finished( mErrors );
927 mResults = nullptr;
928 mErrors.clear();
929 }
930}
931
ArcGisRestServiceType
Available ArcGIS REST service types.
Definition qgis.h:4686
@ GeocodeServer
GeocodeServer.
Definition qgis.h:4692
@ SceneServer
SceneServer.
Definition qgis.h:4694
@ Unknown
Other unknown/unsupported type.
Definition qgis.h:4693
@ GlobeServer
GlobeServer.
Definition qgis.h:4690
@ ImageServer
ImageServer.
Definition qgis.h:4689
@ FeatureServer
FeatureServer.
Definition qgis.h:4687
@ 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:4723
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.