1 /***************************************************************************
2     qgsarcgisrestutils.cpp
3     ----------------------
4     begin                : Nov 25, 2015
5     copyright            : (C) 2015 by Sandro Mani
6     email                : manisandro@gmail.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 "qgsarcgisrestutils.h"
17 #include "qgsfields.h"
18 #include "qgslogger.h"
19 #include "qgsnetworkaccessmanager.h"
20 #include "qgsrectangle.h"
21 #include "qgsfeedback.h"
22 #include "qgspallabeling.h"
23 #include "qgssymbol.h"
24 #include "qgssymbollayer.h"
25 #include "qgsauthmanager.h"
26 #include "qgssettings.h"
27 #include "qgslinesymbollayer.h"
28 #include "qgsfillsymbollayer.h"
29 #include "qgsrenderer.h"
30 #include "qgsrulebasedlabeling.h"
31 #include "qgssinglesymbolrenderer.h"
32 #include "qgscategorizedsymbolrenderer.h"
33 #include "qgsvectorlayerlabeling.h"
34 #include "qgsapplication.h"
35 #include "qgsmessagelog.h"
36 
37 #include <QEventLoop>
38 #include <QNetworkRequest>
39 #include "qgsblockingnetworkrequest.h"
40 #include <QJsonDocument>
41 #include <QJsonObject>
42 #include <QImageReader>
43 
mapEsriFieldType(const QString & esriFieldType)44 QVariant::Type QgsArcGisRestUtils::mapEsriFieldType( const QString &esriFieldType )
45 {
46   if ( esriFieldType == QLatin1String( "esriFieldTypeInteger" ) )
47     return QVariant::LongLong;
48   if ( esriFieldType == QLatin1String( "esriFieldTypeSmallInteger" ) )
49     return QVariant::Int;
50   if ( esriFieldType == QLatin1String( "esriFieldTypeDouble" ) )
51     return QVariant::Double;
52   if ( esriFieldType == QLatin1String( "esriFieldTypeSingle" ) )
53     return QVariant::Double;
54   if ( esriFieldType == QLatin1String( "esriFieldTypeString" ) )
55     return QVariant::String;
56   if ( esriFieldType == QLatin1String( "esriFieldTypeDate" ) )
57     return QVariant::DateTime;
58   if ( esriFieldType == QLatin1String( "esriFieldTypeGeometry" ) )
59     return QVariant::Invalid; // Geometry column should not appear as field
60   if ( esriFieldType == QLatin1String( "esriFieldTypeOID" ) )
61     return QVariant::LongLong;
62   if ( esriFieldType == QLatin1String( "esriFieldTypeBlob" ) )
63     return QVariant::ByteArray;
64   if ( esriFieldType == QLatin1String( "esriFieldTypeGlobalID" ) )
65     return QVariant::String;
66   if ( esriFieldType == QLatin1String( "esriFieldTypeRaster" ) )
67     return QVariant::ByteArray;
68   if ( esriFieldType == QLatin1String( "esriFieldTypeGUID" ) )
69     return QVariant::String;
70   if ( esriFieldType == QLatin1String( "esriFieldTypeXML" ) )
71     return QVariant::String;
72   return QVariant::Invalid;
73 }
74 
mapEsriGeometryType(const QString & esriGeometryType)75 QgsWkbTypes::Type QgsArcGisRestUtils::mapEsriGeometryType( const QString &esriGeometryType )
76 {
77   // http://resources.arcgis.com/en/help/arcobjects-cpp/componenthelp/index.html#//000w0000001p000000
78   if ( esriGeometryType == QLatin1String( "esriGeometryNull" ) )
79     return QgsWkbTypes::Unknown;
80   else if ( esriGeometryType == QLatin1String( "esriGeometryPoint" ) )
81     return QgsWkbTypes::Point;
82   else if ( esriGeometryType == QLatin1String( "esriGeometryMultipoint" ) )
83     return QgsWkbTypes::MultiPoint;
84   else if ( esriGeometryType == QLatin1String( "esriGeometryPolyline" ) )
85     return QgsWkbTypes::MultiCurve;
86   else if ( esriGeometryType == QLatin1String( "esriGeometryPolygon" ) )
87     return QgsWkbTypes::MultiPolygon;
88   else if ( esriGeometryType == QLatin1String( "esriGeometryEnvelope" ) )
89     return QgsWkbTypes::Polygon;
90   // Unsupported (either by qgis, or format unspecified by the specification)
91   //  esriGeometryCircularArc
92   //  esriGeometryEllipticArc
93   //  esriGeometryBezier3Curve
94   //  esriGeometryPath
95   //  esriGeometryRing
96   //  esriGeometryLine
97   //  esriGeometryAny
98   //  esriGeometryMultiPatch
99   //  esriGeometryTriangleStrip
100   //  esriGeometryTriangleFan
101   //  esriGeometryRay
102   //  esriGeometrySphere
103   //  esriGeometryTriangles
104   //  esriGeometryBag
105   return QgsWkbTypes::Unknown;
106 }
107 
parsePoint(const QVariantList & coordList,QgsWkbTypes::Type pointType)108 std::unique_ptr< QgsPoint > QgsArcGisRestUtils::parsePoint( const QVariantList &coordList, QgsWkbTypes::Type pointType )
109 {
110   int nCoords = coordList.size();
111   if ( nCoords < 2 )
112     return nullptr;
113   bool xok = false, yok = false;
114   double x = coordList[0].toDouble( &xok );
115   double y = coordList[1].toDouble( &yok );
116   if ( !xok || !yok )
117     return nullptr;
118   double z = nCoords >= 3 ? coordList[2].toDouble() : 0;
119   double m = nCoords >= 4 ? coordList[3].toDouble() : 0;
120   return qgis::make_unique< QgsPoint >( pointType, x, y, z, m );
121 }
122 
parseCircularString(const QVariantMap & curveData,QgsWkbTypes::Type pointType,const QgsPoint & startPoint)123 std::unique_ptr< QgsCircularString > QgsArcGisRestUtils::parseCircularString( const QVariantMap &curveData, QgsWkbTypes::Type pointType, const QgsPoint &startPoint )
124 {
125   const QVariantList coordsList = curveData[QStringLiteral( "c" )].toList();
126   if ( coordsList.isEmpty() )
127     return nullptr;
128   QVector<QgsPoint> points;
129   points.append( startPoint );
130   for ( const QVariant &coordData : coordsList )
131   {
132     std::unique_ptr< QgsPoint > point = parsePoint( coordData.toList(), pointType );
133     if ( !point )
134     {
135       return nullptr;
136     }
137     points.append( *point );
138   }
139   std::unique_ptr< QgsCircularString > curve = qgis::make_unique< QgsCircularString> ();
140   curve->setPoints( points );
141   return curve;
142 }
143 
parseCompoundCurve(const QVariantList & curvesList,QgsWkbTypes::Type pointType)144 std::unique_ptr< QgsCompoundCurve > QgsArcGisRestUtils::parseCompoundCurve( const QVariantList &curvesList, QgsWkbTypes::Type pointType )
145 {
146   // [[6,3],[5,3],{"b":[[3,2],[6,1],[2,4]]},[1,2],{"c": [[3,3],[1,4]]}]
147   std::unique_ptr< QgsCompoundCurve > compoundCurve = qgis::make_unique< QgsCompoundCurve >();
148   QgsLineString *lineString = new QgsLineString();
149   compoundCurve->addCurve( lineString );
150   for ( const QVariant &curveData : curvesList )
151   {
152     if ( curveData.type() == QVariant::List )
153     {
154       std::unique_ptr< QgsPoint > point = parsePoint( curveData.toList(), pointType );
155       if ( !point )
156       {
157         return nullptr;
158       }
159       lineString->addVertex( *point );
160     }
161     else if ( curveData.type() == QVariant::Map )
162     {
163       // The last point of the linestring is the start point of this circular string
164       std::unique_ptr< QgsCircularString > circularString = parseCircularString( curveData.toMap(), pointType, lineString->endPoint() );
165       if ( !circularString )
166       {
167         return nullptr;
168       }
169 
170       // If the previous curve had less than two points, remove it
171       if ( compoundCurve->curveAt( compoundCurve->nCurves() - 1 )->nCoordinates() < 2 )
172         compoundCurve->removeCurve( compoundCurve->nCurves() - 1 );
173 
174       const QgsPoint endPointCircularString = circularString->endPoint();
175       compoundCurve->addCurve( circularString.release() );
176 
177       // Prepare a new line string
178       lineString = new QgsLineString;
179       compoundCurve->addCurve( lineString );
180       lineString->addVertex( endPointCircularString );
181     }
182   }
183   return compoundCurve;
184 }
185 
parseEsriGeometryPoint(const QVariantMap & geometryData,QgsWkbTypes::Type pointType)186 std::unique_ptr< QgsPoint > QgsArcGisRestUtils::parseEsriGeometryPoint( const QVariantMap &geometryData, QgsWkbTypes::Type pointType )
187 {
188   // {"x" : <x>, "y" : <y>, "z" : <z>, "m" : <m>}
189   bool xok = false, yok = false;
190   double x = geometryData[QStringLiteral( "x" )].toDouble( &xok );
191   double y = geometryData[QStringLiteral( "y" )].toDouble( &yok );
192   if ( !xok || !yok )
193     return nullptr;
194   double z = geometryData[QStringLiteral( "z" )].toDouble();
195   double m = geometryData[QStringLiteral( "m" )].toDouble();
196   return qgis::make_unique< QgsPoint >( pointType, x, y, z, m );
197 }
198 
parseEsriGeometryMultiPoint(const QVariantMap & geometryData,QgsWkbTypes::Type pointType)199 std::unique_ptr< QgsMultiPoint > QgsArcGisRestUtils::parseEsriGeometryMultiPoint( const QVariantMap &geometryData, QgsWkbTypes::Type pointType )
200 {
201   // {"points" : [[ <x1>, <y1>, <z1>, <m1> ] , [ <x2>, <y2>, <z2>, <m2> ], ... ]}
202   const QVariantList coordsList = geometryData[QStringLiteral( "points" )].toList();
203 
204   std::unique_ptr< QgsMultiPoint > multiPoint = qgis::make_unique< QgsMultiPoint >();
205   multiPoint->reserve( coordsList.size() );
206   for ( const QVariant &coordData : coordsList )
207   {
208     const QVariantList coordList = coordData.toList();
209     std::unique_ptr< QgsPoint > p = parsePoint( coordList, pointType );
210     if ( !p )
211     {
212       continue;
213     }
214     multiPoint->addGeometry( p.release() );
215   }
216 
217   // second chance -- sometimes layers are reported as multipoint but features have single
218   // point geometries. Silently handle this and upgrade to multipoint.
219   std::unique_ptr< QgsPoint > p = parseEsriGeometryPoint( geometryData, pointType );
220   if ( p )
221     multiPoint->addGeometry( p.release() );
222 
223   if ( multiPoint->numGeometries() == 0 )
224   {
225     // didn't find any points, so reset geometry to null
226     multiPoint.reset();
227   }
228   return multiPoint;
229 }
230 
parseEsriGeometryPolyline(const QVariantMap & geometryData,QgsWkbTypes::Type pointType)231 std::unique_ptr< QgsMultiCurve > QgsArcGisRestUtils::parseEsriGeometryPolyline( const QVariantMap &geometryData, QgsWkbTypes::Type pointType )
232 {
233   // {"curvePaths": [[[0,0], {"c": [[3,3],[1,4]]} ]]}
234   QVariantList pathsList;
235   if ( geometryData[QStringLiteral( "paths" )].isValid() )
236     pathsList = geometryData[QStringLiteral( "paths" )].toList();
237   else if ( geometryData[QStringLiteral( "curvePaths" )].isValid() )
238     pathsList = geometryData[QStringLiteral( "curvePaths" )].toList();
239   if ( pathsList.isEmpty() )
240     return nullptr;
241   std::unique_ptr< QgsMultiCurve > multiCurve = qgis::make_unique< QgsMultiCurve >();
242   multiCurve->reserve( pathsList.size() );
243   for ( const QVariant &pathData : qgis::as_const( pathsList ) )
244   {
245     std::unique_ptr< QgsCompoundCurve > curve = parseCompoundCurve( pathData.toList(), pointType );
246     if ( !curve )
247     {
248       return nullptr;
249     }
250     multiCurve->addGeometry( curve.release() );
251   }
252   return multiCurve;
253 }
254 
parseEsriGeometryPolygon(const QVariantMap & geometryData,QgsWkbTypes::Type pointType)255 std::unique_ptr< QgsMultiSurface > QgsArcGisRestUtils::parseEsriGeometryPolygon( const QVariantMap &geometryData, QgsWkbTypes::Type pointType )
256 {
257   // {"curveRings": [[[0,0], {"c": [[3,3],[1,4]]} ]]}
258   QVariantList ringsList;
259   if ( geometryData[QStringLiteral( "rings" )].isValid() )
260     ringsList = geometryData[QStringLiteral( "rings" )].toList();
261   else if ( geometryData[QStringLiteral( "ringPaths" )].isValid() )
262     ringsList = geometryData[QStringLiteral( "ringPaths" )].toList();
263   if ( ringsList.isEmpty() )
264     return nullptr;
265 
266   QList< QgsCompoundCurve * > curves;
267   for ( int i = 0, n = ringsList.size(); i < n; ++i )
268   {
269     std::unique_ptr< QgsCompoundCurve > curve = parseCompoundCurve( ringsList[i].toList(), pointType );
270     if ( !curve )
271     {
272       continue;
273     }
274     curves.append( curve.release() );
275   }
276   if ( curves.count() == 0 )
277     return nullptr;
278 
279   std::sort( curves.begin(), curves.end(), []( const QgsCompoundCurve * a, const QgsCompoundCurve * b )->bool{ double a_area = 0.0; double b_area = 0.0; a->sumUpArea( a_area ); b->sumUpArea( b_area ); return std::abs( a_area ) > std::abs( b_area ); } );
280   std::unique_ptr< QgsMultiSurface > result = qgis::make_unique< QgsMultiSurface >();
281   result->reserve( curves.size() );
282   while ( !curves.isEmpty() )
283   {
284     QgsCompoundCurve *exterior = curves.takeFirst();
285     QgsCurvePolygon *newPolygon = new QgsCurvePolygon();
286     newPolygon->setExteriorRing( exterior );
287     std::unique_ptr<QgsGeometryEngine> engine( QgsGeometry::createGeometryEngine( newPolygon ) );
288     engine->prepareGeometry();
289 
290     QMutableListIterator< QgsCompoundCurve * > it( curves );
291     while ( it.hasNext() )
292     {
293       QgsCompoundCurve *curve = it.next();
294       QgsRectangle boundingBox = newPolygon->boundingBox();
295       if ( boundingBox.intersects( curve->boundingBox() ) )
296       {
297         QgsPoint point = curve->startPoint();
298         if ( engine->contains( &point ) )
299         {
300           newPolygon->addInteriorRing( curve );
301           it.remove();
302           engine.reset( QgsGeometry::createGeometryEngine( newPolygon ) );
303           engine->prepareGeometry();
304         }
305       }
306     }
307     result->addGeometry( newPolygon );
308   }
309   if ( result->numGeometries() == 0 )
310     return nullptr;
311 
312   return result;
313 }
314 
parseEsriEnvelope(const QVariantMap & geometryData)315 std::unique_ptr< QgsPolygon > QgsArcGisRestUtils::parseEsriEnvelope( const QVariantMap &geometryData )
316 {
317   // {"xmin" : -109.55, "ymin" : 25.76, "xmax" : -86.39, "ymax" : 49.94}
318   bool xminOk = false, yminOk = false, xmaxOk = false, ymaxOk = false;
319   double xmin = geometryData[QStringLiteral( "xmin" )].toDouble( &xminOk );
320   double ymin = geometryData[QStringLiteral( "ymin" )].toDouble( &yminOk );
321   double xmax = geometryData[QStringLiteral( "xmax" )].toDouble( &xmaxOk );
322   double ymax = geometryData[QStringLiteral( "ymax" )].toDouble( &ymaxOk );
323   if ( !xminOk || !yminOk || !xmaxOk || !ymaxOk )
324     return nullptr;
325   std::unique_ptr< QgsLineString > ext = qgis::make_unique< QgsLineString> ();
326   ext->addVertex( QgsPoint( xmin, ymin ) );
327   ext->addVertex( QgsPoint( xmax, ymin ) );
328   ext->addVertex( QgsPoint( xmax, ymax ) );
329   ext->addVertex( QgsPoint( xmin, ymax ) );
330   ext->addVertex( QgsPoint( xmin, ymin ) );
331   std::unique_ptr< QgsPolygon > poly = qgis::make_unique< QgsPolygon >();
332   poly->setExteriorRing( ext.release() );
333   return poly;
334 }
335 
parseEsriGeoJSON(const QVariantMap & geometryData,const QString & esriGeometryType,bool readM,bool readZ,QgsCoordinateReferenceSystem * crs)336 std::unique_ptr<QgsAbstractGeometry> QgsArcGisRestUtils::parseEsriGeoJSON( const QVariantMap &geometryData, const QString &esriGeometryType, bool readM, bool readZ, QgsCoordinateReferenceSystem *crs )
337 {
338   QgsWkbTypes::Type pointType = QgsWkbTypes::zmType( QgsWkbTypes::Point, readZ, readM );
339   if ( crs )
340   {
341     *crs = parseSpatialReference( geometryData[QStringLiteral( "spatialReference" )].toMap() );
342   }
343 
344   // http://resources.arcgis.com/en/help/arcgis-rest-api/index.html#/Geometry_Objects/02r3000000n1000000/
345   if ( esriGeometryType == QLatin1String( "esriGeometryNull" ) )
346     return nullptr;
347   else if ( esriGeometryType == QLatin1String( "esriGeometryPoint" ) )
348     return parseEsriGeometryPoint( geometryData, pointType );
349   else if ( esriGeometryType == QLatin1String( "esriGeometryMultipoint" ) )
350     return parseEsriGeometryMultiPoint( geometryData, pointType );
351   else if ( esriGeometryType == QLatin1String( "esriGeometryPolyline" ) )
352     return parseEsriGeometryPolyline( geometryData, pointType );
353   else if ( esriGeometryType == QLatin1String( "esriGeometryPolygon" ) )
354     return parseEsriGeometryPolygon( geometryData, pointType );
355   else if ( esriGeometryType == QLatin1String( "esriGeometryEnvelope" ) )
356     return parseEsriEnvelope( geometryData );
357   // Unsupported (either by qgis, or format unspecified by the specification)
358   //  esriGeometryCircularArc
359   //  esriGeometryEllipticArc
360   //  esriGeometryBezier3Curve
361   //  esriGeometryPath
362   //  esriGeometryRing
363   //  esriGeometryLine
364   //  esriGeometryAny
365   //  esriGeometryMultiPatch
366   //  esriGeometryTriangleStrip
367   //  esriGeometryTriangleFan
368   //  esriGeometryRay
369   //  esriGeometrySphere
370   //  esriGeometryTriangles
371   //  esriGeometryBag
372   return nullptr;
373 }
374 
parseSpatialReference(const QVariantMap & spatialReferenceMap)375 QgsCoordinateReferenceSystem QgsArcGisRestUtils::parseSpatialReference( const QVariantMap &spatialReferenceMap )
376 {
377   QString spatialReference = spatialReferenceMap[QStringLiteral( "latestWkid" )].toString();
378   if ( spatialReference.isEmpty() )
379     spatialReference = spatialReferenceMap[QStringLiteral( "wkid" )].toString();
380   if ( spatialReference.isEmpty() )
381     spatialReference = spatialReferenceMap[QStringLiteral( "wkt" )].toString();
382   else
383     spatialReference = QStringLiteral( "EPSG:%1" ).arg( spatialReference );
384   QgsCoordinateReferenceSystem crs;
385   crs.createFromString( spatialReference );
386   if ( !crs.isValid() )
387   {
388     // If not spatial reference, just use WGS84
389     crs.createFromString( QStringLiteral( "EPSG:4326" ) );
390   }
391   return crs;
392 }
393 
394 
getServiceInfo(const QString & baseurl,const QString & authcfg,QString & errorTitle,QString & errorText,const QgsStringMap & requestHeaders)395 QVariantMap QgsArcGisRestUtils::getServiceInfo( const QString &baseurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsStringMap &requestHeaders )
396 {
397   // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer?f=json
398   QUrl queryUrl( baseurl );
399   QUrlQuery query( queryUrl );
400   query.addQueryItem( QStringLiteral( "f" ), QStringLiteral( "json" ) );
401   queryUrl.setQuery( query );
402   return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders );
403 }
404 
getLayerInfo(const QString & layerurl,const QString & authcfg,QString & errorTitle,QString & errorText,const QgsStringMap & requestHeaders)405 QVariantMap QgsArcGisRestUtils::getLayerInfo( const QString &layerurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsStringMap &requestHeaders )
406 {
407   // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/1?f=json
408   QUrl queryUrl( layerurl );
409   QUrlQuery query( queryUrl );
410   query.addQueryItem( QStringLiteral( "f" ), QStringLiteral( "json" ) );
411   queryUrl.setQuery( query );
412   return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders );
413 }
414 
getObjectIds(const QString & layerurl,const QString & authcfg,QString & errorTitle,QString & errorText,const QgsStringMap & requestHeaders,const QgsRectangle & bbox)415 QVariantMap QgsArcGisRestUtils::getObjectIds( const QString &layerurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsStringMap &requestHeaders, const QgsRectangle &bbox )
416 {
417   // http://sampleserver5.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/1/query?where=1%3D1&returnIdsOnly=true&f=json
418   QUrl queryUrl( layerurl + "/query" );
419   QUrlQuery query( queryUrl );
420   query.addQueryItem( QStringLiteral( "f" ), QStringLiteral( "json" ) );
421   query.addQueryItem( QStringLiteral( "where" ), QStringLiteral( "1=1" ) );
422   query.addQueryItem( QStringLiteral( "returnIdsOnly" ), QStringLiteral( "true" ) );
423   if ( !bbox.isNull() )
424   {
425     query.addQueryItem( QStringLiteral( "geometry" ), QStringLiteral( "%1,%2,%3,%4" )
426                         .arg( bbox.xMinimum(), 0, 'f', -1 ).arg( bbox.yMinimum(), 0, 'f', -1 )
427                         .arg( bbox.xMaximum(), 0, 'f', -1 ).arg( bbox.yMaximum(), 0, 'f', -1 ) );
428     query.addQueryItem( QStringLiteral( "geometryType" ), QStringLiteral( "esriGeometryEnvelope" ) );
429     query.addQueryItem( QStringLiteral( "spatialRel" ), QStringLiteral( "esriSpatialRelEnvelopeIntersects" ) );
430   }
431   queryUrl.setQuery( query );
432   return queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders );
433 }
434 
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 QgsStringMap & requestHeaders,QgsFeedback * feedback)435 QVariantMap QgsArcGisRestUtils::getObjects( const QString &layerurl, const QString &authcfg, const QList<quint32> &objectIds, const QString &crs,
436     bool fetchGeometry, const QStringList &fetchAttributes,
437     bool fetchM, bool fetchZ,
438     const QgsRectangle &filterRect,
439     QString &errorTitle, QString &errorText, const QgsStringMap &requestHeaders, QgsFeedback *feedback )
440 {
441   QStringList ids;
442   for ( int id : objectIds )
443   {
444     ids.append( QString::number( id ) );
445   }
446   QUrl queryUrl( layerurl + "/query" );
447   QUrlQuery query( queryUrl );
448   query.addQueryItem( QStringLiteral( "f" ), QStringLiteral( "json" ) );
449   query.addQueryItem( QStringLiteral( "objectIds" ), ids.join( QLatin1Char( ',' ) ) );
450   QString wkid = crs.indexOf( QLatin1Char( ':' ) ) >= 0 ? crs.split( ':' )[1] : QString();
451   query.addQueryItem( QStringLiteral( "inSR" ), wkid );
452   query.addQueryItem( QStringLiteral( "outSR" ), wkid );
453 
454   query.addQueryItem( QStringLiteral( "returnGeometry" ), fetchGeometry ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
455 
456   QString outFields;
457   if ( fetchAttributes.isEmpty() )
458     outFields = QStringLiteral( "*" );
459   else
460     outFields = fetchAttributes.join( ',' );
461   query.addQueryItem( QStringLiteral( "outFields" ), outFields );
462 
463   query.addQueryItem( QStringLiteral( "returnM" ), fetchM ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
464   query.addQueryItem( QStringLiteral( "returnZ" ), fetchZ ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
465   if ( !filterRect.isNull() )
466   {
467     query.addQueryItem( QStringLiteral( "geometry" ), QStringLiteral( "%1,%2,%3,%4" )
468                         .arg( filterRect.xMinimum(), 0, 'f', -1 ).arg( filterRect.yMinimum(), 0, 'f', -1 )
469                         .arg( filterRect.xMaximum(), 0, 'f', -1 ).arg( filterRect.yMaximum(), 0, 'f', -1 ) );
470     query.addQueryItem( QStringLiteral( "geometryType" ), QStringLiteral( "esriGeometryEnvelope" ) );
471     query.addQueryItem( QStringLiteral( "spatialRel" ), QStringLiteral( "esriSpatialRelEnvelopeIntersects" ) );
472   }
473   queryUrl.setQuery( query );
474   return queryServiceJSON( queryUrl,  authcfg, errorTitle, errorText, requestHeaders, feedback );
475 }
476 
getObjectIdsByExtent(const QString & layerurl,const QgsRectangle & filterRect,QString & errorTitle,QString & errorText,const QString & authcfg,const QgsStringMap & requestHeaders,QgsFeedback * feedback)477 QList<quint32> QgsArcGisRestUtils::getObjectIdsByExtent( const QString &layerurl, const QgsRectangle &filterRect, QString &errorTitle, QString &errorText, const QString &authcfg, const QgsStringMap &requestHeaders, QgsFeedback *feedback )
478 {
479   QUrl queryUrl( layerurl + "/query" );
480   QUrlQuery query( queryUrl );
481   query.addQueryItem( QStringLiteral( "f" ), QStringLiteral( "json" ) );
482   query.addQueryItem( QStringLiteral( "where" ), QStringLiteral( "1=1" ) );
483   query.addQueryItem( QStringLiteral( "returnIdsOnly" ), QStringLiteral( "true" ) );
484   query.addQueryItem( QStringLiteral( "geometry" ), QStringLiteral( "%1,%2,%3,%4" )
485                       .arg( filterRect.xMinimum(), 0, 'f', -1 ).arg( filterRect.yMinimum(), 0, 'f', -1 )
486                       .arg( filterRect.xMaximum(), 0, 'f', -1 ).arg( filterRect.yMaximum(), 0, 'f', -1 ) );
487   query.addQueryItem( QStringLiteral( "geometryType" ), QStringLiteral( "esriGeometryEnvelope" ) );
488   query.addQueryItem( QStringLiteral( "spatialRel" ), QStringLiteral( "esriSpatialRelEnvelopeIntersects" ) );
489   queryUrl.setQuery( query );
490   const QVariantMap objectIdData = queryServiceJSON( queryUrl, authcfg, errorTitle, errorText, requestHeaders, feedback );
491 
492   if ( objectIdData.isEmpty() )
493   {
494     return QList<quint32>();
495   }
496 
497   QList<quint32> ids;
498   const QVariantList objectIdsList = objectIdData[QStringLiteral( "objectIds" )].toList();
499   ids.reserve( objectIdsList.size() );
500   for ( const QVariant &objectId : objectIdsList )
501   {
502     ids << objectId.toInt();
503   }
504   return ids;
505 }
506 
queryService(const QUrl & u,const QString & authcfg,QString & errorTitle,QString & errorText,const QgsStringMap & requestHeaders,QgsFeedback * feedback,QString * contentType)507 QByteArray QgsArcGisRestUtils::queryService( const QUrl &u, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsStringMap &requestHeaders, QgsFeedback *feedback, QString *contentType )
508 {
509   QUrl url = parseUrl( u );
510 
511   QNetworkRequest request( url );
512   QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsArcGisRestUtils" ) );
513   for ( auto it = requestHeaders.constBegin(); it != requestHeaders.constEnd(); ++it )
514   {
515     request.setRawHeader( it.key().toUtf8(), it.value().toUtf8() );
516   }
517 
518   QgsBlockingNetworkRequest networkRequest;
519   networkRequest.setAuthCfg( authcfg );
520   const QgsBlockingNetworkRequest::ErrorCode error = networkRequest.get( request, false, feedback );
521 
522   if ( feedback && feedback->isCanceled() )
523     return QByteArray();
524 
525   // Handle network errors
526   if ( error != QgsBlockingNetworkRequest::NoError )
527   {
528     QgsDebugMsg( QStringLiteral( "Network error: %1" ).arg( networkRequest.errorMessage() ) );
529     errorTitle = QStringLiteral( "Network error" );
530     errorText = networkRequest.errorMessage();
531     return QByteArray();
532   }
533 
534   const QgsNetworkReplyContent content = networkRequest.reply();
535   if ( contentType )
536     *contentType = content.rawHeader( "Content-Type" );
537   return content.content();
538 }
539 
queryServiceJSON(const QUrl & url,const QString & authcfg,QString & errorTitle,QString & errorText,const QgsStringMap & requestHeaders,QgsFeedback * feedback)540 QVariantMap QgsArcGisRestUtils::queryServiceJSON( const QUrl &url, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsStringMap &requestHeaders, QgsFeedback *feedback )
541 {
542   QByteArray reply = queryService( url, authcfg, errorTitle, errorText, requestHeaders, feedback );
543   if ( !errorTitle.isEmpty() )
544   {
545     return QVariantMap();
546   }
547   if ( feedback && feedback->isCanceled() )
548     return QVariantMap();
549 
550   // Parse data
551   QJsonParseError err;
552   QJsonDocument doc = QJsonDocument::fromJson( reply, &err );
553   if ( doc.isNull() )
554   {
555     errorTitle = QStringLiteral( "Parsing error" );
556     errorText = err.errorString();
557     QgsDebugMsg( QStringLiteral( "Parsing error: %1" ).arg( err.errorString() ) );
558     return QVariantMap();
559   }
560   const QVariantMap res = doc.object().toVariantMap();
561   if ( res.contains( QStringLiteral( "error" ) ) )
562   {
563     const QVariantMap error = res.value( QStringLiteral( "error" ) ).toMap();
564     errorText = error.value( QStringLiteral( "message" ) ).toString();
565     errorTitle = QObject::tr( "Error %1" ).arg( error.value( QStringLiteral( "code" ) ).toString() );
566     return QVariantMap();
567   }
568   return res;
569 }
570 
parseEsriSymbolJson(const QVariantMap & symbolData)571 std::unique_ptr<QgsSymbol> QgsArcGisRestUtils::parseEsriSymbolJson( const QVariantMap &symbolData )
572 {
573   const QString type = symbolData.value( QStringLiteral( "type" ) ).toString();
574   if ( type == QLatin1String( "esriSMS" ) )
575   {
576     // marker symbol
577     return parseEsriMarkerSymbolJson( symbolData );
578   }
579   else if ( type == QLatin1String( "esriSLS" ) )
580   {
581     // line symbol
582     return parseEsriLineSymbolJson( symbolData );
583   }
584   else if ( type == QLatin1String( "esriSFS" ) )
585   {
586     // fill symbol
587     return parseEsriFillSymbolJson( symbolData );
588   }
589   else if ( type == QLatin1String( "esriPFS" ) )
590   {
591     return parseEsriPictureFillSymbolJson( symbolData );
592   }
593   else if ( type == QLatin1String( "esriPMS" ) )
594   {
595     // picture marker
596     return parseEsriPictureMarkerSymbolJson( symbolData );
597   }
598   else if ( type == QLatin1String( "esriTS" ) )
599   {
600     // text symbol - not supported
601     return nullptr;
602   }
603   return nullptr;
604 }
605 
parseEsriLineSymbolJson(const QVariantMap & symbolData)606 std::unique_ptr<QgsLineSymbol> QgsArcGisRestUtils::parseEsriLineSymbolJson( const QVariantMap &symbolData )
607 {
608   QColor lineColor = parseEsriColorJson( symbolData.value( QStringLiteral( "color" ) ) );
609   if ( !lineColor.isValid() )
610     return nullptr;
611 
612   bool ok = false;
613   double widthInPoints = symbolData.value( QStringLiteral( "width" ) ).toDouble( &ok );
614   if ( !ok )
615     return nullptr;
616 
617   QgsSymbolLayerList layers;
618   Qt::PenStyle penStyle = parseEsriLineStyle( symbolData.value( QStringLiteral( "style" ) ).toString() );
619   std::unique_ptr< QgsSimpleLineSymbolLayer > lineLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >( lineColor, widthInPoints, penStyle );
620   lineLayer->setWidthUnit( QgsUnitTypes::RenderPoints );
621   layers.append( lineLayer.release() );
622 
623   std::unique_ptr< QgsLineSymbol > symbol = qgis::make_unique< QgsLineSymbol >( layers );
624   return symbol;
625 }
626 
parseEsriFillSymbolJson(const QVariantMap & symbolData)627 std::unique_ptr<QgsFillSymbol> QgsArcGisRestUtils::parseEsriFillSymbolJson( const QVariantMap &symbolData )
628 {
629   QColor fillColor = parseEsriColorJson( symbolData.value( QStringLiteral( "color" ) ) );
630   Qt::BrushStyle brushStyle = parseEsriFillStyle( symbolData.value( QStringLiteral( "style" ) ).toString() );
631 
632   const QVariantMap outlineData = symbolData.value( QStringLiteral( "outline" ) ).toMap();
633   QColor lineColor = parseEsriColorJson( outlineData.value( QStringLiteral( "color" ) ) );
634   Qt::PenStyle penStyle = parseEsriLineStyle( outlineData.value( QStringLiteral( "style" ) ).toString() );
635   bool ok = false;
636   double penWidthInPoints = outlineData.value( QStringLiteral( "width" ) ).toDouble( &ok );
637 
638   QgsSymbolLayerList layers;
639   std::unique_ptr< QgsSimpleFillSymbolLayer > fillLayer = qgis::make_unique< QgsSimpleFillSymbolLayer >( fillColor, brushStyle, lineColor, penStyle, penWidthInPoints );
640   fillLayer->setStrokeWidthUnit( QgsUnitTypes::RenderPoints );
641   layers.append( fillLayer.release() );
642 
643   std::unique_ptr< QgsFillSymbol > symbol = qgis::make_unique< QgsFillSymbol >( layers );
644   return symbol;
645 }
646 
parseEsriPictureFillSymbolJson(const QVariantMap & symbolData)647 std::unique_ptr<QgsFillSymbol> QgsArcGisRestUtils::parseEsriPictureFillSymbolJson( const QVariantMap &symbolData )
648 {
649   bool ok = false;
650 
651   double widthInPixels = symbolData.value( QStringLiteral( "width" ) ).toInt( &ok );
652   if ( !ok )
653     return nullptr;
654 
655   const double xScale = symbolData.value( QStringLiteral( "xscale" ) ).toDouble( &ok );
656   if ( !qgsDoubleNear( xScale, 0.0 ) )
657     widthInPixels *= xScale;
658 
659   const double angleCCW = symbolData.value( QStringLiteral( "angle" ) ).toDouble( &ok );
660   double angleCW = 0;
661   if ( ok )
662     angleCW = -angleCCW;
663 
664   const double xOffset = symbolData.value( QStringLiteral( "xoffset" ) ).toDouble();
665   const double yOffset = symbolData.value( QStringLiteral( "yoffset" ) ).toDouble();
666 
667   QString symbolPath( symbolData.value( QStringLiteral( "imageData" ) ).toString() );
668   symbolPath.prepend( QLatin1String( "base64:" ) );
669 
670   QgsSymbolLayerList layers;
671   std::unique_ptr< QgsRasterFillSymbolLayer > fillLayer = qgis::make_unique< QgsRasterFillSymbolLayer >( symbolPath );
672   fillLayer->setWidth( widthInPixels );
673   fillLayer->setAngle( angleCW );
674   fillLayer->setWidthUnit( QgsUnitTypes::RenderPoints );
675   fillLayer->setOffset( QPointF( xOffset, yOffset ) );
676   fillLayer->setOffsetUnit( QgsUnitTypes::RenderPoints );
677   layers.append( fillLayer.release() );
678 
679   const QVariantMap outlineData = symbolData.value( QStringLiteral( "outline" ) ).toMap();
680   QColor lineColor = parseEsriColorJson( outlineData.value( QStringLiteral( "color" ) ) );
681   Qt::PenStyle penStyle = parseEsriLineStyle( outlineData.value( QStringLiteral( "style" ) ).toString() );
682   double penWidthInPoints = outlineData.value( QStringLiteral( "width" ) ).toDouble( &ok );
683 
684   std::unique_ptr< QgsSimpleLineSymbolLayer > lineLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >( lineColor, penWidthInPoints, penStyle );
685   lineLayer->setWidthUnit( QgsUnitTypes::RenderPoints );
686   layers.append( lineLayer.release() );
687 
688   std::unique_ptr< QgsFillSymbol > symbol = qgis::make_unique< QgsFillSymbol >( layers );
689   return symbol;
690 }
691 
parseEsriMarkerShape(const QString & style)692 QgsSimpleMarkerSymbolLayerBase::Shape QgsArcGisRestUtils::parseEsriMarkerShape( const QString &style )
693 {
694   if ( style == QLatin1String( "esriSMSCircle" ) )
695     return QgsSimpleMarkerSymbolLayerBase::Circle;
696   else if ( style == QLatin1String( "esriSMSCross" ) )
697     return QgsSimpleMarkerSymbolLayerBase::Cross;
698   else if ( style == QLatin1String( "esriSMSDiamond" ) )
699     return QgsSimpleMarkerSymbolLayerBase::Diamond;
700   else if ( style == QLatin1String( "esriSMSSquare" ) )
701     return QgsSimpleMarkerSymbolLayerBase::Square;
702   else if ( style == QLatin1String( "esriSMSX" ) )
703     return QgsSimpleMarkerSymbolLayerBase::Cross2;
704   else if ( style == QLatin1String( "esriSMSTriangle" ) )
705     return QgsSimpleMarkerSymbolLayerBase::Triangle;
706   else
707     return QgsSimpleMarkerSymbolLayerBase::Circle;
708 }
709 
parseEsriMarkerSymbolJson(const QVariantMap & symbolData)710 std::unique_ptr<QgsMarkerSymbol> QgsArcGisRestUtils::parseEsriMarkerSymbolJson( const QVariantMap &symbolData )
711 {
712   QColor fillColor = parseEsriColorJson( symbolData.value( QStringLiteral( "color" ) ) );
713   bool ok = false;
714   const double sizeInPoints = symbolData.value( QStringLiteral( "size" ) ).toDouble( &ok );
715   if ( !ok )
716     return nullptr;
717   const double angleCCW = symbolData.value( QStringLiteral( "angle" ) ).toDouble( &ok );
718   double angleCW = 0;
719   if ( ok )
720     angleCW = -angleCCW;
721 
722   QgsSimpleMarkerSymbolLayerBase::Shape shape = parseEsriMarkerShape( symbolData.value( QStringLiteral( "style" ) ).toString() );
723 
724   const double xOffset = symbolData.value( QStringLiteral( "xoffset" ) ).toDouble();
725   const double yOffset = symbolData.value( QStringLiteral( "yoffset" ) ).toDouble();
726 
727   const QVariantMap outlineData = symbolData.value( QStringLiteral( "outline" ) ).toMap();
728   QColor lineColor = parseEsriColorJson( outlineData.value( QStringLiteral( "color" ) ) );
729   Qt::PenStyle penStyle = parseEsriLineStyle( outlineData.value( QStringLiteral( "style" ) ).toString() );
730   double penWidthInPoints = outlineData.value( QStringLiteral( "width" ) ).toDouble( &ok );
731 
732   QgsSymbolLayerList layers;
733   std::unique_ptr< QgsSimpleMarkerSymbolLayer > markerLayer = qgis::make_unique< QgsSimpleMarkerSymbolLayer >( shape, sizeInPoints, angleCW, QgsSymbol::ScaleArea, fillColor, lineColor );
734   markerLayer->setSizeUnit( QgsUnitTypes::RenderPoints );
735   markerLayer->setStrokeWidthUnit( QgsUnitTypes::RenderPoints );
736   markerLayer->setStrokeStyle( penStyle );
737   markerLayer->setStrokeWidth( penWidthInPoints );
738   markerLayer->setOffset( QPointF( xOffset, yOffset ) );
739   markerLayer->setOffsetUnit( QgsUnitTypes::RenderPoints );
740   layers.append( markerLayer.release() );
741 
742   std::unique_ptr< QgsMarkerSymbol > symbol = qgis::make_unique< QgsMarkerSymbol >( layers );
743   return symbol;
744 }
745 
parseEsriPictureMarkerSymbolJson(const QVariantMap & symbolData)746 std::unique_ptr<QgsMarkerSymbol> QgsArcGisRestUtils::parseEsriPictureMarkerSymbolJson( const QVariantMap &symbolData )
747 {
748   bool ok = false;
749   const double widthInPixels = symbolData.value( QStringLiteral( "width" ) ).toInt( &ok );
750   if ( !ok )
751     return nullptr;
752   const double heightInPixels = symbolData.value( QStringLiteral( "height" ) ).toInt( &ok );
753   if ( !ok )
754     return nullptr;
755 
756   const double angleCCW = symbolData.value( QStringLiteral( "angle" ) ).toDouble( &ok );
757   double angleCW = 0;
758   if ( ok )
759     angleCW = -angleCCW;
760 
761   const double xOffset = symbolData.value( QStringLiteral( "xoffset" ) ).toDouble();
762   const double yOffset = symbolData.value( QStringLiteral( "yoffset" ) ).toDouble();
763 
764   //const QString contentType = symbolData.value( QStringLiteral( "contentType" ) ).toString();
765 
766   QString symbolPath( symbolData.value( QStringLiteral( "imageData" ) ).toString() );
767   symbolPath.prepend( QLatin1String( "base64:" ) );
768 
769   QgsSymbolLayerList layers;
770   std::unique_ptr< QgsRasterMarkerSymbolLayer > markerLayer = qgis::make_unique< QgsRasterMarkerSymbolLayer >( symbolPath, widthInPixels, angleCW, QgsSymbol::ScaleArea );
771   markerLayer->setSizeUnit( QgsUnitTypes::RenderPoints );
772 
773   // only change the default aspect ratio if the server height setting requires this
774   if ( !qgsDoubleNear( static_cast< double >( heightInPixels ) / widthInPixels, markerLayer->defaultAspectRatio() ) )
775     markerLayer->setFixedAspectRatio( static_cast< double >( heightInPixels ) / widthInPixels );
776 
777   markerLayer->setOffset( QPointF( xOffset, yOffset ) );
778   markerLayer->setOffsetUnit( QgsUnitTypes::RenderPoints );
779   layers.append( markerLayer.release() );
780 
781   std::unique_ptr< QgsMarkerSymbol > symbol = qgis::make_unique< QgsMarkerSymbol >( layers );
782   return symbol;
783 }
784 
parseEsriLabeling(const QVariantList & labelingData)785 QgsAbstractVectorLayerLabeling *QgsArcGisRestUtils::parseEsriLabeling( const QVariantList &labelingData )
786 {
787   if ( labelingData.empty() )
788     return nullptr;
789 
790   QgsRuleBasedLabeling::Rule *root = new QgsRuleBasedLabeling::Rule( new QgsPalLayerSettings(), 0, 0, QString(), QString(), false );
791   root->setActive( true );
792 
793   int i = 1;
794   for ( const QVariant &lbl : labelingData )
795   {
796     const QVariantMap labeling = lbl.toMap();
797 
798     QgsPalLayerSettings *settings = new QgsPalLayerSettings();
799     QgsTextFormat format;
800 
801     const QString placement = labeling.value( QStringLiteral( "labelPlacement" ) ).toString();
802     if ( placement == QLatin1String( "esriServerPointLabelPlacementAboveCenter" ) )
803     {
804       settings->placement = QgsPalLayerSettings::OverPoint;
805       settings->quadOffset = QgsPalLayerSettings::QuadrantAbove;
806     }
807     else if ( placement == QLatin1String( "esriServerPointLabelPlacementBelowCenter" ) )
808     {
809       settings->placement = QgsPalLayerSettings::OverPoint;
810       settings->quadOffset = QgsPalLayerSettings::QuadrantBelow;
811     }
812     else if ( placement == QLatin1String( "esriServerPointLabelPlacementCenterCenter" ) )
813     {
814       settings->placement = QgsPalLayerSettings::OverPoint;
815       settings->quadOffset = QgsPalLayerSettings::QuadrantOver;
816     }
817     else if ( placement == QLatin1String( "esriServerPointLabelPlacementAboveLeft" ) )
818     {
819       settings->placement = QgsPalLayerSettings::OverPoint;
820       settings->quadOffset = QgsPalLayerSettings::QuadrantAboveLeft;
821     }
822     else if ( placement == QLatin1String( "esriServerPointLabelPlacementBelowLeft" ) )
823     {
824       settings->placement = QgsPalLayerSettings::OverPoint;
825       settings->quadOffset = QgsPalLayerSettings::QuadrantBelowLeft;
826     }
827     else if ( placement == QLatin1String( "esriServerPointLabelPlacementCenterLeft" ) )
828     {
829       settings->placement = QgsPalLayerSettings::OverPoint;
830       settings->quadOffset = QgsPalLayerSettings::QuadrantLeft;
831     }
832     else if ( placement == QLatin1String( "esriServerPointLabelPlacementAboveRight" ) )
833     {
834       settings->placement = QgsPalLayerSettings::OverPoint;
835       settings->quadOffset = QgsPalLayerSettings::QuadrantAboveRight;
836     }
837     else if ( placement == QLatin1String( "esriServerPointLabelPlacementBelowRight" ) )
838     {
839       settings->placement = QgsPalLayerSettings::OverPoint;
840       settings->quadOffset = QgsPalLayerSettings::QuadrantBelowRight;
841     }
842     else if ( placement == QLatin1String( "esriServerPointLabelPlacementCenterRight" ) )
843     {
844       settings->placement = QgsPalLayerSettings::OverPoint;
845       settings->quadOffset = QgsPalLayerSettings::QuadrantRight;
846     }
847     else if ( placement == QLatin1String( "esriServerLinePlacementAboveAfter" ) ||
848               placement == QLatin1String( "esriServerLinePlacementAboveStart" ) ||
849               placement == QLatin1String( "esriServerLinePlacementAboveAlong" ) )
850     {
851       settings->placement = QgsPalLayerSettings::Line;
852       settings->lineSettings().setPlacementFlags( QgsLabeling::LinePlacementFlag::AboveLine | QgsLabeling::LinePlacementFlag::MapOrientation );
853     }
854     else if ( placement == QLatin1String( "esriServerLinePlacementBelowAfter" ) ||
855               placement == QLatin1String( "esriServerLinePlacementBelowStart" ) ||
856               placement == QLatin1String( "esriServerLinePlacementBelowAlong" ) )
857     {
858       settings->placement = QgsPalLayerSettings::Line;
859       settings->lineSettings().setPlacementFlags( QgsLabeling::LinePlacementFlag::BelowLine | QgsLabeling::LinePlacementFlag::MapOrientation );
860     }
861     else if ( placement == QLatin1String( "esriServerLinePlacementCenterAfter" ) ||
862               placement == QLatin1String( "esriServerLinePlacementCenterStart" ) ||
863               placement == QLatin1String( "esriServerLinePlacementCenterAlong" ) )
864     {
865       settings->placement = QgsPalLayerSettings::Line;
866       settings->lineSettings().setPlacementFlags( QgsLabeling::LinePlacementFlag::OnLine | QgsLabeling::LinePlacementFlag::MapOrientation );
867     }
868     else if ( placement == QLatin1String( "esriServerPolygonPlacementAlwaysHorizontal" ) )
869     {
870       settings->placement = QgsPalLayerSettings::Horizontal;
871     }
872 
873     const double minScale = labeling.value( QStringLiteral( "minScale" ) ).toDouble();
874     const double maxScale = labeling.value( QStringLiteral( "maxScale" ) ).toDouble();
875 
876     QVariantMap symbol = labeling.value( QStringLiteral( "symbol" ) ).toMap();
877     format.setColor( parseEsriColorJson( symbol.value( QStringLiteral( "color" ) ) ) );
878     const double haloSize = symbol.value( QStringLiteral( "haloSize" ) ).toDouble();
879     if ( !qgsDoubleNear( haloSize, 0.0 ) )
880     {
881       QgsTextBufferSettings buffer;
882       buffer.setEnabled( true );
883       buffer.setSize( haloSize );
884       buffer.setSizeUnit( QgsUnitTypes::RenderPoints );
885       buffer.setColor( parseEsriColorJson( symbol.value( QStringLiteral( "haloColor" ) ) ) );
886       format.setBuffer( buffer );
887     }
888 
889     const QString fontFamily = symbol.value( QStringLiteral( "font" ) ).toMap().value( QStringLiteral( "family" ) ).toString();
890     const QString fontStyle = symbol.value( QStringLiteral( "font" ) ).toMap().value( QStringLiteral( "style" ) ).toString();
891     const QString fontWeight = symbol.value( QStringLiteral( "font" ) ).toMap().value( QStringLiteral( "weight" ) ).toString();
892     const int fontSize = symbol.value( QStringLiteral( "font" ) ).toMap().value( QStringLiteral( "size" ) ).toInt();
893     QFont font( fontFamily, fontSize );
894     font.setStyleName( fontStyle );
895     font.setWeight( fontWeight == QLatin1String( "bold" ) ? QFont::Bold : QFont::Normal );
896 
897     format.setFont( font );
898     format.setSize( fontSize );
899     format.setSizeUnit( QgsUnitTypes::RenderPoints );
900 
901     settings->setFormat( format );
902 
903     QString where = labeling.value( QStringLiteral( "where" ) ).toString();
904     QgsExpression exp( where );
905     // If the where clause isn't parsed as valid, don't use its
906     if ( !exp.isValid() )
907       where.clear();
908 
909     settings->fieldName = parseEsriLabelingExpression( labeling.value( QStringLiteral( "labelExpression" ) ).toString() );
910     settings->isExpression = true;
911 
912     QgsRuleBasedLabeling::Rule *child = new QgsRuleBasedLabeling::Rule( settings, maxScale, minScale, where, QObject::tr( "ASF label %1" ).arg( i++ ), false );
913     child->setActive( true );
914     root->appendChild( child );
915   }
916 
917   return new QgsRuleBasedLabeling( root );
918 }
919 
parseEsriRenderer(const QVariantMap & rendererData)920 QgsFeatureRenderer *QgsArcGisRestUtils::parseEsriRenderer( const QVariantMap &rendererData )
921 {
922   const QString type = rendererData.value( QStringLiteral( "type" ) ).toString();
923   if ( type == QLatin1String( "simple" ) )
924   {
925     const QVariantMap symbolProps = rendererData.value( QStringLiteral( "symbol" ) ).toMap();
926     std::unique_ptr< QgsSymbol > symbol = parseEsriSymbolJson( symbolProps );
927     if ( symbol )
928       return new QgsSingleSymbolRenderer( symbol.release() );
929     else
930       return nullptr;
931   }
932   else if ( type == QLatin1String( "uniqueValue" ) )
933   {
934     const QString field1 = rendererData.value( QStringLiteral( "field1" ) ).toString();
935     const QString field2 = rendererData.value( QStringLiteral( "field2" ) ).toString();
936     const QString field3 = rendererData.value( QStringLiteral( "field3" ) ).toString();
937     QString attribute;
938     if ( !field2.isEmpty() || !field3.isEmpty() )
939     {
940       const QString delimiter = rendererData.value( QStringLiteral( "fieldDelimiter" ) ).toString();
941       if ( !field3.isEmpty() )
942       {
943         attribute = QStringLiteral( "concat(\"%1\",'%2',\"%3\",'%4',\"%5\")" ).arg( field1, delimiter, field2, delimiter, field3 );
944       }
945       else
946       {
947         attribute = QStringLiteral( "concat(\"%1\",'%2',\"%3\")" ).arg( field1, delimiter, field2 );
948       }
949     }
950     else
951     {
952       attribute = field1;
953     }
954 
955     const QVariantList categories = rendererData.value( QStringLiteral( "uniqueValueInfos" ) ).toList();
956     QgsCategoryList categoryList;
957     for ( const QVariant &category : categories )
958     {
959       const QVariantMap categoryData = category.toMap();
960       const QString value = categoryData.value( QStringLiteral( "value" ) ).toString();
961       const QString label = categoryData.value( QStringLiteral( "label" ) ).toString();
962       std::unique_ptr< QgsSymbol > symbol = QgsArcGisRestUtils::parseEsriSymbolJson( categoryData.value( QStringLiteral( "symbol" ) ).toMap() );
963       if ( symbol )
964       {
965         categoryList.append( QgsRendererCategory( value, symbol.release(), label ) );
966       }
967     }
968 
969     std::unique_ptr< QgsSymbol > defaultSymbol = parseEsriSymbolJson( rendererData.value( QStringLiteral( "defaultSymbol" ) ).toMap() );
970     if ( defaultSymbol )
971     {
972       categoryList.append( QgsRendererCategory( QVariant(), defaultSymbol.release(), rendererData.value( QStringLiteral( "defaultLabel" ) ).toString() ) );
973     }
974 
975     if ( categoryList.empty() )
976       return nullptr;
977 
978     return new QgsCategorizedSymbolRenderer( attribute, categoryList );
979   }
980   else if ( type == QLatin1String( "classBreaks" ) )
981   {
982     // currently unsupported
983     return nullptr;
984   }
985   else if ( type == QLatin1String( "heatmap" ) )
986   {
987     // currently unsupported
988     return nullptr;
989   }
990   else if ( type == QLatin1String( "vectorField" ) )
991   {
992     // currently unsupported
993     return nullptr;
994   }
995   return nullptr;
996 }
997 
parseEsriLabelingExpression(const QString & string)998 QString QgsArcGisRestUtils::parseEsriLabelingExpression( const QString &string )
999 {
1000   QString expression = string;
1001 
1002   // Replace a few ArcGIS token to QGIS equivalents
1003   expression = expression.replace( QRegularExpression( "(?=([^\"\\\\]*(\\\\.|\"([^\"\\\\]*\\\\.)*[^\"\\\\]*\"))*[^\"]*$)(\\s|^)CONCAT(\\s|$)" ), QStringLiteral( "\\4||\\5" ) );
1004   expression = expression.replace( QRegularExpression( "(?=([^\"\\\\]*(\\\\.|\"([^\"\\\\]*\\\\.)*[^\"\\\\]*\"))*[^\"]*$)(\\s|^)NEWLINE(\\s|$)" ), QStringLiteral( "\\4'\\n'\\5" ) );
1005 
1006   // ArcGIS's double quotes are single quotes in QGIS
1007   expression = expression.replace( QRegularExpression( "\"(.*?(?<!\\\\))\"" ), QStringLiteral( "'\\1'" ) );
1008   expression = expression.replace( QRegularExpression( "\\\\\"" ), QStringLiteral( "\"" ) );
1009 
1010   // ArcGIS's square brakets are double quotes in QGIS
1011   expression = expression.replace( QRegularExpression( "\\[([^]]*)\\]" ), QStringLiteral( "\"\\1\"" ) );
1012 
1013   return expression;
1014 }
1015 
parseEsriColorJson(const QVariant & colorData)1016 QColor QgsArcGisRestUtils::parseEsriColorJson( const QVariant &colorData )
1017 {
1018   const QVariantList colorParts = colorData.toList();
1019   if ( colorParts.count() < 4 )
1020     return QColor();
1021 
1022   int red = colorParts.at( 0 ).toInt();
1023   int green = colorParts.at( 1 ).toInt();
1024   int blue = colorParts.at( 2 ).toInt();
1025   int alpha = colorParts.at( 3 ).toInt();
1026   return QColor( red, green, blue, alpha );
1027 }
1028 
parseEsriLineStyle(const QString & style)1029 Qt::PenStyle QgsArcGisRestUtils::parseEsriLineStyle( const QString &style )
1030 {
1031   if ( style == QLatin1String( "esriSLSSolid" ) )
1032     return Qt::SolidLine;
1033   else if ( style == QLatin1String( "esriSLSDash" ) )
1034     return Qt::DashLine;
1035   else if ( style == QLatin1String( "esriSLSDashDot" ) )
1036     return Qt::DashDotLine;
1037   else if ( style == QLatin1String( "esriSLSDashDotDot" ) )
1038     return Qt::DashDotDotLine;
1039   else if ( style == QLatin1String( "esriSLSDot" ) )
1040     return Qt::DotLine;
1041   else if ( style == QLatin1String( "esriSLSNull" ) )
1042     return Qt::NoPen;
1043   else
1044     return Qt::SolidLine;
1045 }
1046 
parseEsriFillStyle(const QString & style)1047 Qt::BrushStyle QgsArcGisRestUtils::parseEsriFillStyle( const QString &style )
1048 {
1049   if ( style == QLatin1String( "esriSFSBackwardDiagonal" ) )
1050     return Qt::BDiagPattern;
1051   else if ( style == QLatin1String( "esriSFSCross" ) )
1052     return Qt::CrossPattern;
1053   else if ( style == QLatin1String( "esriSFSDiagonalCross" ) )
1054     return Qt::DiagCrossPattern;
1055   else if ( style == QLatin1String( "esriSFSForwardDiagonal" ) )
1056     return Qt::FDiagPattern;
1057   else if ( style == QLatin1String( "esriSFSHorizontal" ) )
1058     return Qt::HorPattern;
1059   else if ( style == QLatin1String( "esriSFSNull" ) )
1060     return Qt::NoBrush;
1061   else if ( style == QLatin1String( "esriSFSSolid" ) )
1062     return Qt::SolidPattern;
1063   else if ( style == QLatin1String( "esriSFSVertical" ) )
1064     return Qt::VerPattern;
1065   else
1066     return Qt::SolidPattern;
1067 }
1068 
parseDateTime(const QVariant & value)1069 QDateTime QgsArcGisRestUtils::parseDateTime( const QVariant &value )
1070 {
1071   if ( value.isNull() )
1072     return QDateTime();
1073   bool ok = false;
1074   QDateTime dt = QDateTime::fromMSecsSinceEpoch( value.toLongLong( &ok ) );
1075   if ( !ok )
1076   {
1077     QgsDebugMsg( QStringLiteral( "Invalid value %1 for datetime" ).arg( value.toString() ) );
1078     return QDateTime();
1079   }
1080   else
1081     return dt;
1082 }
1083 
parseUrl(const QUrl & url)1084 QUrl QgsArcGisRestUtils::parseUrl( const QUrl &url )
1085 {
1086   QUrl modifiedUrl( url );
1087   if ( modifiedUrl.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
1088   {
1089     // Just for testing with local files instead of http:// resources
1090     QString modifiedUrlString = modifiedUrl.toString();
1091     // Qt5 does URL encoding from some reason (of the FILTER parameter for example)
1092     modifiedUrlString = QUrl::fromPercentEncoding( modifiedUrlString.toUtf8() );
1093     modifiedUrlString.replace( QLatin1String( "fake_qgis_http_endpoint/" ), QLatin1String( "fake_qgis_http_endpoint_" ) );
1094     QgsDebugMsg( QStringLiteral( "Get %1" ).arg( modifiedUrlString ) );
1095     modifiedUrlString = modifiedUrlString.mid( QStringLiteral( "http://" ).size() );
1096     QString args = modifiedUrlString.mid( modifiedUrlString.indexOf( '?' ) );
1097     if ( modifiedUrlString.size() > 150 )
1098     {
1099       args = QCryptographicHash::hash( args.toUtf8(), QCryptographicHash::Md5 ).toHex();
1100     }
1101     else
1102     {
1103       args.replace( QLatin1String( "?" ), QLatin1String( "_" ) );
1104       args.replace( QLatin1String( "&" ), QLatin1String( "_" ) );
1105       args.replace( QLatin1String( "<" ), QLatin1String( "_" ) );
1106       args.replace( QLatin1String( ">" ), QLatin1String( "_" ) );
1107       args.replace( QLatin1String( "'" ), QLatin1String( "_" ) );
1108       args.replace( QLatin1String( "\"" ), QLatin1String( "_" ) );
1109       args.replace( QLatin1String( " " ), QLatin1String( "_" ) );
1110       args.replace( QLatin1String( ":" ), QLatin1String( "_" ) );
1111       args.replace( QLatin1String( "/" ), QLatin1String( "_" ) );
1112       args.replace( QLatin1String( "\n" ), QLatin1String( "_" ) );
1113     }
1114 #ifdef Q_OS_WIN
1115     // Passing "urls" like "http://c:/path" to QUrl 'eats' the : after c,
1116     // so we must restore it
1117     if ( modifiedUrlString[1] == '/' )
1118     {
1119       modifiedUrlString = modifiedUrlString[0] + ":/" + modifiedUrlString.mid( 2 );
1120     }
1121 #endif
1122     modifiedUrlString = modifiedUrlString.mid( 0, modifiedUrlString.indexOf( '?' ) ) + args;
1123     QgsDebugMsg( QStringLiteral( "Get %1 (after laundering)" ).arg( modifiedUrlString ) );
1124     modifiedUrl = QUrl::fromLocalFile( modifiedUrlString );
1125   }
1126 
1127   return modifiedUrl;
1128 }
1129 
1130 ///////////////////////////////////////////////////////////////////////////////
1131 
QgsArcGisAsyncQuery(QObject * parent)1132 QgsArcGisAsyncQuery::QgsArcGisAsyncQuery( QObject *parent )
1133   : QObject( parent )
1134 {
1135 }
1136 
~QgsArcGisAsyncQuery()1137 QgsArcGisAsyncQuery::~QgsArcGisAsyncQuery()
1138 {
1139   if ( mReply )
1140     mReply->deleteLater();
1141 }
1142 
start(const QUrl & url,const QString & authCfg,QByteArray * result,bool allowCache,const QgsStringMap & headers)1143 void QgsArcGisAsyncQuery::start( const QUrl &url, const QString &authCfg, QByteArray *result, bool allowCache, const QgsStringMap &headers )
1144 {
1145   mResult = result;
1146   QNetworkRequest request( url );
1147 
1148   for ( auto it = headers.constBegin(); it != headers.constEnd(); ++it )
1149   {
1150     request.setRawHeader( it.key().toUtf8(), it.value().toUtf8() );
1151   }
1152 
1153   if ( !authCfg.isEmpty() &&  !QgsApplication::authManager()->updateNetworkRequest( request, authCfg ) )
1154   {
1155     const QString error = tr( "network request update failed for authentication config" );
1156     emit failed( QStringLiteral( "Network" ), error );
1157     return;
1158   }
1159 
1160   QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsArcGisAsyncQuery" ) );
1161   if ( allowCache )
1162   {
1163     request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
1164     request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
1165   }
1166   mReply = QgsNetworkAccessManager::instance()->get( request );
1167   connect( mReply, &QNetworkReply::finished, this, &QgsArcGisAsyncQuery::handleReply );
1168 }
1169 
handleReply()1170 void QgsArcGisAsyncQuery::handleReply()
1171 {
1172   mReply->deleteLater();
1173   // Handle network errors
1174   if ( mReply->error() != QNetworkReply::NoError )
1175   {
1176     QgsDebugMsg( QStringLiteral( "Network error: %1" ).arg( mReply->errorString() ) );
1177     emit failed( QStringLiteral( "Network error" ), mReply->errorString() );
1178     return;
1179   }
1180 
1181   // Handle HTTP redirects
1182   QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
1183   if ( !redirect.isNull() )
1184   {
1185     QNetworkRequest request = mReply->request();
1186     QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsArcGisAsyncQuery" ) );
1187     QgsDebugMsg( "redirecting to " + redirect.toUrl().toString() );
1188     request.setUrl( redirect.toUrl() );
1189     mReply = QgsNetworkAccessManager::instance()->get( request );
1190     connect( mReply, &QNetworkReply::finished, this, &QgsArcGisAsyncQuery::handleReply );
1191     return;
1192   }
1193 
1194   *mResult = mReply->readAll();
1195   mResult = nullptr;
1196   emit finished();
1197 }
1198 
1199 ///////////////////////////////////////////////////////////////////////////////
1200 
QgsArcGisAsyncParallelQuery(const QString & authcfg,const QgsStringMap & requestHeaders,QObject * parent)1201 QgsArcGisAsyncParallelQuery::QgsArcGisAsyncParallelQuery( const QString &authcfg, const QgsStringMap &requestHeaders, QObject *parent )
1202   : QObject( parent )
1203   , mAuthCfg( authcfg )
1204   , mRequestHeaders( requestHeaders )
1205 {
1206 }
1207 
start(const QVector<QUrl> & urls,QVector<QByteArray> * results,bool allowCache)1208 void QgsArcGisAsyncParallelQuery::start( const QVector<QUrl> &urls, QVector<QByteArray> *results, bool allowCache )
1209 {
1210   Q_ASSERT( results->size() == urls.size() );
1211   mResults = results;
1212   mPendingRequests = mResults->size();
1213   for ( int i = 0, n = urls.size(); i < n; ++i )
1214   {
1215     QNetworkRequest request( urls[i] );
1216     QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsArcGisAsyncParallelQuery" ) );
1217     QgsSetRequestInitiatorId( request, QString::number( i ) );
1218 
1219     for ( auto it = mRequestHeaders.constBegin(); it != mRequestHeaders.constEnd(); ++it )
1220     {
1221       request.setRawHeader( it.key().toUtf8(), it.value().toUtf8() );
1222     }
1223     if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
1224     {
1225       const QString error = tr( "network request update failed for authentication config" );
1226       mErrors.append( error );
1227       QgsMessageLog::logMessage( error, tr( "Network" ) );
1228       continue;
1229     }
1230 
1231     request.setAttribute( QNetworkRequest::HttpPipeliningAllowedAttribute, true );
1232     if ( allowCache )
1233     {
1234       request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
1235       request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
1236       request.setRawHeader( "Connection", "keep-alive" );
1237     }
1238     QNetworkReply *reply = QgsNetworkAccessManager::instance()->get( request );
1239     reply->setProperty( "idx", i );
1240     connect( reply, &QNetworkReply::finished, this, &QgsArcGisAsyncParallelQuery::handleReply );
1241   }
1242 }
1243 
handleReply()1244 void QgsArcGisAsyncParallelQuery::handleReply()
1245 {
1246   QNetworkReply *reply = qobject_cast<QNetworkReply *>( QObject::sender() );
1247   QVariant redirect = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
1248   int idx = reply->property( "idx" ).toInt();
1249   reply->deleteLater();
1250   if ( reply->error() != QNetworkReply::NoError )
1251   {
1252     // Handle network errors
1253     mErrors.append( reply->errorString() );
1254     --mPendingRequests;
1255   }
1256   else if ( !redirect.isNull() )
1257   {
1258     // Handle HTTP redirects
1259     QNetworkRequest request = reply->request();
1260     QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsArcGisAsyncParallelQuery" ) );
1261     QgsDebugMsg( "redirecting to " + redirect.toUrl().toString() );
1262     request.setUrl( redirect.toUrl() );
1263     reply = QgsNetworkAccessManager::instance()->get( request );
1264     reply->setProperty( "idx", idx );
1265     connect( reply, &QNetworkReply::finished, this, &QgsArcGisAsyncParallelQuery::handleReply );
1266   }
1267   else
1268   {
1269     // All OK
1270     ( *mResults )[idx] = reply->readAll();
1271     --mPendingRequests;
1272   }
1273   if ( mPendingRequests == 0 )
1274   {
1275     emit finished( mErrors );
1276     mResults = nullptr;
1277     mErrors.clear();
1278   }
1279 }
1280 
adjustBaseUrl(QString & baseUrl,const QString name)1281 void QgsArcGisRestUtils::adjustBaseUrl( QString &baseUrl, const QString name )
1282 {
1283   QStringList parts = name.split( '/' );
1284   QString checkString;
1285   for ( const QString &part : parts )
1286   {
1287     if ( !checkString.isEmpty() )
1288       checkString += QString( '/' );
1289 
1290     checkString += part;
1291     if ( baseUrl.indexOf( QRegularExpression( checkString.replace( '/', QLatin1String( "\\/" ) ) + QStringLiteral( "\\/?$" ) ) ) > -1 )
1292     {
1293       baseUrl = baseUrl.left( baseUrl.length() - checkString.length() - 1 );
1294       break;
1295     }
1296   }
1297 }
1298 
visitFolderItems(const std::function<void (const QString &,const QString &)> & visitor,const QVariantMap & serviceData,const QString & baseUrl)1299 void QgsArcGisRestUtils::visitFolderItems( const std::function< void( const QString &, const QString & ) > &visitor, const QVariantMap &serviceData, const QString &baseUrl )
1300 {
1301   QString base( baseUrl );
1302   bool baseChecked = false;
1303   if ( !base.endsWith( '/' ) )
1304     base += QLatin1Char( '/' );
1305 
1306   const QStringList folderList = serviceData.value( QStringLiteral( "folders" ) ).toStringList();
1307   for ( const QString &folder : folderList )
1308   {
1309     if ( !baseChecked )
1310     {
1311       adjustBaseUrl( base, folder );
1312       baseChecked = true;
1313     }
1314     visitor( folder, base + folder );
1315   }
1316 }
1317 
visitServiceItems(const std::function<void (const QString &,const QString &)> & visitor,const QVariantMap & serviceData,const QString & baseUrl,const ServiceTypeFilter filter)1318 void QgsArcGisRestUtils::visitServiceItems( const std::function< void( const QString &, const QString & ) > &visitor, const QVariantMap &serviceData, const QString &baseUrl, const ServiceTypeFilter filter )
1319 {
1320   QString base( baseUrl );
1321   bool baseChecked = false;
1322   if ( !base.endsWith( '/' ) )
1323     base += QLatin1Char( '/' );
1324 
1325   const QVariantList serviceList = serviceData.value( QStringLiteral( "services" ) ).toList();
1326   for ( const QVariant &service : serviceList )
1327   {
1328     const QVariantMap serviceMap = service.toMap();
1329     const QString serviceType = serviceMap.value( QStringLiteral( "type" ) ).toString();
1330     if ( serviceType != QLatin1String( "MapServer" ) && serviceType != QLatin1String( "ImageServer" ) && serviceType != QLatin1String( "FeatureServer" ) )
1331       continue;
1332 
1333     // If the requested service type is raster, do not show vector-only services
1334     if ( serviceType == QLatin1String( "FeatureServer" ) && filter == QgsArcGisRestUtils::Raster )
1335       continue;
1336 
1337     const QString serviceName = serviceMap.value( QStringLiteral( "name" ) ).toString();
1338     QString displayName = serviceName.split( '/' ).last();
1339     if ( !baseChecked )
1340     {
1341       adjustBaseUrl( base, serviceName );
1342       baseChecked = true;
1343     }
1344 
1345     visitor( displayName, base + serviceName + '/' + serviceType );
1346   }
1347 }
1348 
addLayerItems(const std::function<void (const QString &,const QString &,const QString &,const QString &,const QString &,bool,const QString &,const QString &)> & visitor,const QVariantMap & serviceData,const QString & parentUrl,const ServiceTypeFilter filter)1349 void QgsArcGisRestUtils::addLayerItems( const std::function< void( const QString &, const QString &, const QString &, const QString &, const QString &, bool, const QString &, const QString & )> &visitor, const QVariantMap &serviceData, const QString &parentUrl, const ServiceTypeFilter filter )
1350 {
1351   const QString authid = QgsArcGisRestUtils::parseSpatialReference( serviceData.value( QStringLiteral( "spatialReference" ) ).toMap() ).authid();
1352 
1353   QString format = QStringLiteral( "jpg" );
1354   bool found = false;
1355   const QList<QByteArray> supportedFormats = QImageReader::supportedImageFormats();
1356   const QStringList supportedImageFormatTypes = serviceData.value( QStringLiteral( "supportedImageFormatTypes" ) ).toString().split( ',' );
1357   for ( const QString &encoding : supportedImageFormatTypes )
1358   {
1359     for ( const QByteArray &fmt : supportedFormats )
1360     {
1361       if ( encoding.startsWith( fmt, Qt::CaseInsensitive ) )
1362       {
1363         format = encoding;
1364         found = true;
1365         break;
1366       }
1367     }
1368     if ( found )
1369       break;
1370   }
1371   const QStringList capabilities = serviceData.value( QStringLiteral( "capabilities" ) ).toString().split( ',' );
1372 
1373   // If the requested layer type is vector, do not show raster-only layers (i.e. non query-able layers)
1374   const bool serviceMayHaveQueryCapability = capabilities.contains( QStringLiteral( "Query" ) ) ||
1375       serviceData.value( QStringLiteral( "serviceDataType" ) ).toString().startsWith( QLatin1String( "esriImageService" ) );
1376   if ( filter == QgsArcGisRestUtils::Vector && !serviceMayHaveQueryCapability )
1377     return;
1378 
1379   const QVariantList layerInfoList = serviceData.value( QStringLiteral( "layers" ) ).toList();
1380   for ( const QVariant &layerInfo : layerInfoList )
1381   {
1382     const QVariantMap layerInfoMap = layerInfo.toMap();
1383     const QString id = layerInfoMap.value( QStringLiteral( "id" ) ).toString();
1384     const QString parentLayerId = layerInfoMap.value( QStringLiteral( "parentLayerId" ) ).toString();
1385     const QString name = layerInfoMap.value( QStringLiteral( "name" ) ).toString();
1386     const QString description = layerInfoMap.value( QStringLiteral( "description" ) ).toString();
1387 
1388     if ( !layerInfoMap.value( QStringLiteral( "subLayerIds" ) ).toList().empty() )
1389     {
1390       visitor( parentLayerId, id, name, description, parentUrl + '/' + id, true, QString(), format );
1391     }
1392     else
1393     {
1394       visitor( parentLayerId, id, name, description, parentUrl + '/' + id, false, authid, format );
1395     }
1396   }
1397 
1398   // Add root MapServer as raster layer when multiple layers are listed
1399   if ( filter != QgsArcGisRestUtils::Vector && layerInfoList.count() > 1 && serviceData.contains( QStringLiteral( "supportedImageFormatTypes" ) ) )
1400   {
1401     const QString name = QStringLiteral( "(%1)" ).arg( QObject::tr( "All layers" ) );
1402     const QString description = serviceData.value( QStringLiteral( "Comments" ) ).toString();
1403     visitor( 0, 0, name, description, parentUrl, false, authid, format );
1404   }
1405 
1406   // Add root ImageServer as layer
1407   if ( serviceData.value( QStringLiteral( "serviceDataType" ) ).toString().startsWith( QLatin1String( "esriImageService" ) ) )
1408   {
1409     const QString name = serviceData.value( QStringLiteral( "name" ) ).toString();
1410     const QString description = serviceData.value( QStringLiteral( "description" ) ).toString();
1411     visitor( 0, 0, name, description, parentUrl, false, authid, format );
1412   }
1413 }
1414