From b05a9ee543e63e741d60dc2815cd7b8a59d5b0fe Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Thu, 18 Jul 2024 01:45:24 +0200 Subject: [PATCH] Create converter for ArcGis FeatureServer type classBreaks --- .../providers/arcgis/qgsarcgisrestutils.cpp | 130 ++++++++++++- src/providers/arcgisrest/qgsafsprovider.cpp | 5 + tests/src/python/test_provider_afs.py | 183 +++++++++++++++++- 3 files changed, 314 insertions(+), 4 deletions(-) diff --git a/src/core/providers/arcgis/qgsarcgisrestutils.cpp b/src/core/providers/arcgis/qgsarcgisrestutils.cpp index 07b4976f7485..13288f087780 100644 --- a/src/core/providers/arcgis/qgsarcgisrestutils.cpp +++ b/src/core/providers/arcgis/qgsarcgisrestutils.cpp @@ -45,6 +45,13 @@ #include #include +#include "qgsclassificationcustom.h" +#include "qgsclassificationequalinterval.h" +#include "qgsclassificationfixedinterval.h" +#include "qgsclassificationjenks.h" +#include "qgsclassificationquantile.h" +#include "qgsclassificationstandarddeviation.h" +#include "qgsgraduatedsymbolrenderer.h" QMetaType::Type QgsArcGisRestUtils::convertFieldType( const QString &esriFieldType ) { @@ -1021,8 +1028,127 @@ QgsFeatureRenderer *QgsArcGisRestUtils::convertRenderer( const QVariantMap &rend } else if ( type == QLatin1String( "classBreaks" ) ) { - // currently unsupported - return nullptr; + const QString attrName = rendererData.value( QStringLiteral( "field" ) ).toString(); + std::unique_ptr< QgsGraduatedSymbolRenderer > graduatedRenderer = std::make_unique< QgsGraduatedSymbolRenderer >( attrName ); + + const QVariantList classBreakInfos = rendererData.value( QStringLiteral( "classBreakInfos" ) ).toList(); + const QVariantMap authoringInfo = rendererData.value( QStringLiteral( "authoringInfo" ) ).toMap(); + QVariantMap symbolData; + + QString esriMode = authoringInfo.value( QStringLiteral( "classificationMethod" ) ).toString(); + if ( esriMode.isEmpty() ) + { + esriMode = rendererData.value( QStringLiteral( "classificationMethod" ) ).toString(); + } + + if ( esriMode == QLatin1String( "esriClassifyDefinedInterval" ) ) + { + QgsClassificationFixedInterval *method = new QgsClassificationFixedInterval(); + graduatedRenderer->setClassificationMethod( method ); + } + else if ( esriMode == QLatin1String( "esriClassifyEqualInterval" ) ) + { + QgsClassificationEqualInterval *method = new QgsClassificationEqualInterval(); + graduatedRenderer->setClassificationMethod( method ); + } + else if ( esriMode == QLatin1String( "esriClassifyGeometricalInterval" ) ) + { + QgsClassificationCustom *method = new QgsClassificationCustom(); + graduatedRenderer->setClassificationMethod( method ); + } + else if ( esriMode == QLatin1String( "esriClassifyManual" ) ) + { + QgsClassificationCustom *method = new QgsClassificationCustom(); + graduatedRenderer->setClassificationMethod( method ); + } + else if ( esriMode == QLatin1String( "esriClassifyNaturalBreaks" ) ) + { + QgsClassificationJenks *method = new QgsClassificationJenks(); + graduatedRenderer->setClassificationMethod( method ); + } + else if ( esriMode == QLatin1String( "esriClassifyQuantile" ) ) + { + QgsClassificationQuantile *method = new QgsClassificationQuantile(); + graduatedRenderer->setClassificationMethod( method ); + } + else if ( esriMode == QLatin1String( "esriClassifyStandardDeviation" ) ) + { + QgsClassificationStandardDeviation *method = new QgsClassificationStandardDeviation(); + graduatedRenderer->setClassificationMethod( method ); + } + else + { + QgsDebugError( QStringLiteral( "ESRI classification mode %1 is not currently supported" ).arg( esriMode ) ); + } + + + if ( !classBreakInfos.isEmpty() ) + { + symbolData = classBreakInfos.at( 0 ).toMap().value( QStringLiteral( "symbol" ) ).toMap(); + } + std::unique_ptr< QgsSymbol > symbol( QgsArcGisRestUtils::convertSymbol( symbolData ) ); + double transparency = rendererData.value( QStringLiteral( "transparency" ) ).toDouble(); + + double opacity = ( 100.0 - transparency ) / 100.0; + + if ( !symbol ) + return nullptr; + else + { + symbol->setOpacity( opacity ); + graduatedRenderer->setSourceSymbol( symbol.release() ); + } + + const QVariantList visualVariablesData = rendererData.value( QStringLiteral( "visualVariables" ) ).toList(); + double lastValue = rendererData.value( QStringLiteral( "minValue" ) ).toDouble(); + for ( const QVariant &visualVariable : visualVariablesData ) + { + const QVariantList stops = visualVariable.toMap().value( QStringLiteral( "stops" ) ).toList(); + for ( const QVariant &stop : stops ) + { + const QVariantMap stopData = stop.toMap(); + const QString label = stopData.value( QStringLiteral( "label" ) ).toString(); + const double breakpoint = stopData.value( QStringLiteral( "value" ) ).toDouble(); + std::unique_ptr< QgsSymbol > symbolForStop( graduatedRenderer->sourceSymbol()->clone() ); + + if ( visualVariable.toMap().value( QStringLiteral( "type" ) ).toString() == QStringLiteral( "colorInfo" ) ) + { + // handle color change stops: + QColor fillColor = convertColor( stopData.value( QStringLiteral( "color" ) ) ); + symbolForStop->setColor( fillColor ); + + QgsRendererRange range; + + range.setLowerValue( lastValue ); + range.setUpperValue( breakpoint ); + range.setLabel( label ); + range.setSymbol( symbolForStop.release() ); + + lastValue = breakpoint; + graduatedRenderer->addClass( range ); + } + } + } + lastValue = rendererData.value( QStringLiteral( "minValue" ) ).toDouble(); + for ( const QVariant &classBreakInfo : classBreakInfos ) + { + const QVariantMap symbolData = classBreakInfo.toMap().value( QStringLiteral( "symbol" ) ).toMap(); + std::unique_ptr< QgsSymbol > symbol( QgsArcGisRestUtils::convertSymbol( symbolData ) ); + double classMaxValue = classBreakInfo.toMap().value( QStringLiteral( "classMaxValue" ) ).toDouble(); + const QString label = classBreakInfo.toMap().value( QStringLiteral( "label" ) ).toString(); + + QgsRendererRange range; + + range.setLowerValue( lastValue ); + range.setUpperValue( classMaxValue ); + range.setLabel( label ); + range.setSymbol( symbol.release() ); + + lastValue = classMaxValue; + graduatedRenderer->addClass( range ); + } + + return graduatedRenderer.release(); } else if ( type == QLatin1String( "heatmap" ) ) { diff --git a/src/providers/arcgisrest/qgsafsprovider.cpp b/src/providers/arcgisrest/qgsafsprovider.cpp index a55be15cdf30..f1b97b425dec 100644 --- a/src/providers/arcgisrest/qgsafsprovider.cpp +++ b/src/providers/arcgisrest/qgsafsprovider.cpp @@ -313,6 +313,11 @@ QgsAfsProvider::QgsAfsProvider( const QString &uri, const ProviderOptions &optio // renderer mRendererDataMap = layerData.value( QStringLiteral( "drawingInfo" ) ).toMap().value( QStringLiteral( "renderer" ) ).toMap(); mLabelingDataList = layerData.value( QStringLiteral( "drawingInfo" ) ).toMap().value( QStringLiteral( "labelingInfo" ) ).toList(); + const QVariant transparency = layerData.value( QStringLiteral( "drawingInfo" ) ).toMap().value( QStringLiteral( "transparency" ) ); + if ( transparency.isValid() ) + { + mRendererDataMap.insert( QStringLiteral( "transparency" ), transparency ); + } mValid = true; } diff --git a/tests/src/python/test_provider_afs.py b/tests/src/python/test_provider_afs.py index 184669584f56..1f3de45780f6 100644 --- a/tests/src/python/test_provider_afs.py +++ b/tests/src/python/test_provider_afs.py @@ -39,6 +39,9 @@ QgsVectorDataProviderTemporalCapabilities, QgsVectorLayer, QgsWkbTypes, + QgsGraduatedSymbolRenderer, + QgsSymbol, + QgsRendererRange, ) import unittest from qgis.testing import start_app, QgisTestCase @@ -1122,8 +1125,8 @@ def testFieldAlias(self): self.assertEqual(vl.fields().at(1).name(), 'second') self.assertFalse(vl.fields().at(1).alias()) - def testRenderer(self): - """ Test that renderer is correctly acquired from provider """ + def testCategorizedRenderer(self): + """ Test that the categorized renderer is correctly acquired from provider """ endpoint = self.basetestpath + '/renderer_fake_qgis_http_endpoint' with open(sanitize(endpoint, '?f=json'), 'wb') as f: @@ -1223,6 +1226,182 @@ def testRenderer(self): self.assertEqual(vl.renderer().categories()[0].value(), 'US') self.assertEqual(vl.renderer().categories()[1].value(), 'Canada') + def testGraduatedRenderer(self): + """ Test that the graduated renderer is correctly acquired from provider """ + + endpoint = self.basetestpath + '/class_breaks_renderer_fake_qgis_http_endpoint' + with open(sanitize(endpoint, '?f=json'), 'wb') as f: + f.write(b"""{ + "currentVersion": 11.2, + "id": 0, + "name": "Test graduated renderer", + "type": "Feature Layer", + "useStandardizedQueries": true, + "geometryType": "esriGeometryPolygon", + "minScale": 0, + "maxScale": 1155581, + "extent": { + "xmin": -17771274.9623, + "ymin": 2175061.919500001, + "xmax": -7521909.497300002, + "ymax": 9988155.384400003, + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857 + } + }, + "drawingInfo": { + "renderer": { + "visualVariables": [ + { + "type": "colorInfo", + "field": "SUM", + "valueExpression": null, + "stops": [ + { + "value": 10151, + "color": [ + 255, + 196, + 174, + 255 + ], + "label": "< 10,151" + }, + { + "value": 632613.25, + "color": [ + 249, + 129, + 108, + 255 + ], + "label": null + }, + { + "value": 1255075.5, + "color": [ + 236, + 82, + 68, + 255 + ], + "label": "1,255,075" + }, + { + "value": 1877537.75, + "color": [ + 194, + 61, + 51, + 255 + ], + "label": null + }, + { + "value": 2500000, + "color": [ + 123, + 66, + 56, + 255 + ], + "label": "> 2,500,000" + } + ] + }, + { + "type": "sizeInfo", + "target": "outline", + "expression": "view.scale", + "valueExpression": "$view.scale", + "stops": [ + { + "size": 1.5, + "value": 3468153 + }, + { + "size": 0.75, + "value": 10837979 + }, + { + "size": 0.375, + "value": 43351915 + }, + { + "size": 0, + "value": 86703831 + } + ] + } + ], + "authoringInfo": { + "classificationMethod": "esriClassifyEqualInterval", + "visualVariables": [ + { + "type": "colorInfo", + "minSliderValue": 10151, + "maxSliderValue": 15185477, + "theme": "high-to-low" + } + ] + }, + "type": "classBreaks", + "field": "SUM", + "minValue": -9007199254740991, + "classBreakInfos": [ + { + "symbol": { + "color": [ + 170, + 170, + 170, + 255 + ], + "outline": { + "color": [ + 194, + 194, + 194, + 64 + ], + "width": 0.75, + "type": "esriSLS", + "style": "esriSLSSolid" + }, + "type": "esriSFS", + "style": "esriSFSSolid" + }, + "classMaxValue": 9007199254740991 + } + ] + }, + "transparency": 20 + }, + "allowGeometryUpdates": true + }""") + + with open(sanitize(endpoint, '/query?f=json_where=1=1&returnIdsOnly=true'), 'wb') as f: + f.write(b""" + { + "objectIdFieldName": "OBJECTID", + "objectIds": [ + 1 + ] + } + """) + + # Create test layer + vl = QgsVectorLayer("url='http://" + endpoint + "' crs='epsg:3857'", 'test', 'arcgisfeatureserver') + self.assertTrue(vl.isValid()) + self.assertIsNotNone(vl.dataProvider().createRenderer()) + self.assertIsInstance(vl.renderer(), QgsGraduatedSymbolRenderer) + self.assertIsInstance(vl.renderer().sourceSymbol(), QgsSymbol) + self.assertIsInstance(vl.renderer().ranges()[0], QgsRendererRange) + self.assertEqual(len(vl.renderer().ranges()), 6) + self.assertEqual(vl.renderer().ranges()[0][0], -9007199254740991) + self.assertEqual(vl.renderer().ranges()[-1][1], 9007199254740991) + def testBboxRestriction(self): """ Test limiting provider to features within a preset bounding box