From a057d08920c4e4a6a170df04a716bec5cb57bf63 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 25 Jul 2024 12:11:50 +1000 Subject: [PATCH] Fix conversion of ArcGIS continuous color renderers These need to map to single symbol renderer with a color ramp transformer, as they should show in QGIS with continuous coloring too --- .../providers/arcgis/qgsarcgisrestutils.cpp | 134 ++++--- tests/src/python/test_provider_afs.py | 341 +++++++++++++++++- 2 files changed, 419 insertions(+), 56 deletions(-) diff --git a/src/core/providers/arcgis/qgsarcgisrestutils.cpp b/src/core/providers/arcgis/qgsarcgisrestutils.cpp index 13288f087780..eb9babbc2d98 100644 --- a/src/core/providers/arcgis/qgsarcgisrestutils.cpp +++ b/src/core/providers/arcgis/qgsarcgisrestutils.cpp @@ -16,6 +16,7 @@ #include "qgsarcgisrestutils.h" #include "qgsfields.h" #include "qgslogger.h" +#include "qgspropertytransformer.h" #include "qgsrectangle.h" #include "qgspallabeling.h" #include "qgssymbol.h" @@ -42,6 +43,7 @@ #include "qgsfillsymbol.h" #include "qgsvariantutils.h" #include "qgsmarkersymbollayer.h" +#include "qgscolorrampimpl.h" #include #include @@ -1029,7 +1031,6 @@ QgsFeatureRenderer *QgsArcGisRestUtils::convertRenderer( const QVariantMap &rend else if ( type == QLatin1String( "classBreaks" ) ) { 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(); @@ -1041,6 +1042,86 @@ QgsFeatureRenderer *QgsArcGisRestUtils::convertRenderer( const QVariantMap &rend esriMode = rendererData.value( QStringLiteral( "classificationMethod" ) ).toString(); } + if ( !classBreakInfos.isEmpty() ) + { + symbolData = classBreakInfos.at( 0 ).toMap().value( QStringLiteral( "symbol" ) ).toMap(); + } + std::unique_ptr< QgsSymbol > symbol( QgsArcGisRestUtils::convertSymbol( symbolData ) ); + if ( !symbol ) + return nullptr; + + const double transparency = rendererData.value( QStringLiteral( "transparency" ) ).toDouble(); + const double opacity = ( 100.0 - transparency ) / 100.0; + symbol->setOpacity( opacity ); + + const QVariantList visualVariablesData = rendererData.value( QStringLiteral( "visualVariables" ) ).toList(); + + for ( const QVariant &visualVariable : visualVariablesData ) + { + const QVariantMap visualVariableData = visualVariable.toMap(); + const QString variableType = visualVariableData.value( QStringLiteral( "type" ) ).toString(); + if ( variableType == QLatin1String( "sizeInfo" ) ) + { + continue; + } + else if ( variableType == QLatin1String( "colorInfo" ) ) + { + const QVariantList stops = visualVariableData.value( QStringLiteral( "stops" ) ).toList(); + if ( stops.size() < 2 ) + continue; + + // layer has continuous coloring, so convert to a symbol using color ramp assistant + bool ok = false; + const double minValue = stops.front().toMap().value( QStringLiteral( "value" ) ).toDouble( &ok ); + if ( !ok ) + continue; + const QColor minColor = convertColor( stops.front().toMap().value( QStringLiteral( "color" ) ) ); + + const double maxValue = stops.back().toMap().value( QStringLiteral( "value" ) ).toDouble( &ok ); + if ( !ok ) + continue; + const QColor maxColor = convertColor( stops.back().toMap().value( QStringLiteral( "color" ) ) ); + + QgsGradientStopsList gradientStops; + for ( int i = 1; i < stops.size() - 1; ++i ) + { + const QVariantMap stopData = stops.at( i ).toMap(); + const double breakpoint = stopData.value( QStringLiteral( "value" ) ).toDouble(); + const double scaledBreakpoint = ( breakpoint - minValue ) / ( maxValue - minValue ); + const QColor fillColor = convertColor( stopData.value( QStringLiteral( "color" ) ) ); + + gradientStops.append( QgsGradientStop( scaledBreakpoint, fillColor ) ); + } + + std::unique_ptr< QgsGradientColorRamp > colorRamp = std::make_unique< QgsGradientColorRamp >( + minColor, maxColor, false, gradientStops + ); + + QgsProperty colorProperty = QgsProperty::fromField( attrName ); + colorProperty.setTransformer( + new QgsColorRampTransformer( minValue, maxValue, colorRamp.release() ) + ); + for ( int layer = 0; layer < symbol->symbolLayerCount(); ++layer ) + { + symbol->symbolLayer( layer )->setDataDefinedProperty( QgsSymbolLayer::Property::FillColor, colorProperty ); + } + + std::unique_ptr< QgsSingleSymbolRenderer > singleSymbolRenderer = std::make_unique< QgsSingleSymbolRenderer >( symbol.release() ); + + return singleSymbolRenderer.release(); + } + else + { + QgsDebugError( QStringLiteral( "ESRI visualVariable type %1 is not currently supported" ).arg( variableType ) ); + } + } + + double lastValue = rendererData.value( QStringLiteral( "minValue" ) ).toDouble(); + + std::unique_ptr< QgsGraduatedSymbolRenderer > graduatedRenderer = std::make_unique< QgsGraduatedSymbolRenderer >( attrName ); + + graduatedRenderer->setSourceSymbol( symbol.release() ); + if ( esriMode == QLatin1String( "esriClassifyDefinedInterval" ) ) { QgsClassificationFixedInterval *method = new QgsClassificationFixedInterval(); @@ -1076,60 +1157,11 @@ QgsFeatureRenderer *QgsArcGisRestUtils::convertRenderer( const QVariantMap &rend QgsClassificationStandardDeviation *method = new QgsClassificationStandardDeviation(); graduatedRenderer->setClassificationMethod( method ); } - else + else if ( !esriMode.isEmpty() ) { 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(); diff --git a/tests/src/python/test_provider_afs.py b/tests/src/python/test_provider_afs.py index 1f3de45780f6..bdd582704538 100644 --- a/tests/src/python/test_provider_afs.py +++ b/tests/src/python/test_provider_afs.py @@ -23,6 +23,7 @@ QTime, ) from qgis.core import ( + Qgis, NULL, QgsApplication, QgsBox3d, @@ -42,6 +43,11 @@ QgsGraduatedSymbolRenderer, QgsSymbol, QgsRendererRange, + QgsSingleSymbolRenderer, + QgsFillSymbol, + QgsSymbolLayer, + QgsColorRampTransformer, + QgsGradientColorRamp ) import unittest from qgis.testing import start_app, QgisTestCase @@ -1226,8 +1232,11 @@ def testCategorizedRenderer(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 """ + def testGraduatedRendererContinuous(self): + """ + Test that the graduated renderer with continuous coloring + is correctly acquired from provider + """ endpoint = self.basetestpath + '/class_breaks_renderer_fake_qgis_http_endpoint' with open(sanitize(endpoint, '?f=json'), 'wb') as f: @@ -1391,6 +1400,296 @@ def testGraduatedRenderer(self): } """) + # 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(), QgsSingleSymbolRenderer) + self.assertIsInstance(vl.renderer().symbol(), QgsFillSymbol) + + prop = vl.renderer().symbol()[0].dataDefinedProperties().property(QgsSymbolLayer.Property.FillColor) + self.assertEqual(prop.propertyType(), Qgis.PropertyType.Field) + self.assertEqual(prop.field(), 'SUM') + self.assertIsInstance(prop.transformer(), QgsColorRampTransformer) + self.assertEqual(prop.transformer().minValue(), 10151) + self.assertEqual(prop.transformer().maxValue(), 2500000) + ramp = prop.transformer().colorRamp() + self.assertIsInstance(ramp, QgsGradientColorRamp) + self.assertEqual(ramp.color1().name(), '#ffc4ae') + self.assertEqual(ramp.color2().name(), '#7b4238') + self.assertEqual([stop.offset for stop in ramp.stops()], [0.25, 0.5, 0.75]) + self.assertEqual([stop.color.name() for stop in ramp.stops()], ['#f9816c', '#ec5244', '#c23d33']) + + def testGraduatedRendererClassedColor(self): + """ + Test that the graduated renderer with classified colors + 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": { + "type": "classBreaks", + "authoringInfo": { + "type": "classedColor", + "colorRamp": { + "type": "multipart", + "colorRamps": [ + { + "type": "algorithmic", + "algorithm": "esriCIELabAlgorithm", + "fromColor": [ + 229, + 237, + 206, + 255 + ], + "toColor": [ + 229, + 237, + 206, + 255 + ] + }, + { + "type": "algorithmic", + "algorithm": "esriCIELabAlgorithm", + "fromColor": [ + 155, + 196, + 194, + 255 + ], + "toColor": [ + 155, + 196, + 194, + 255 + ] + }, + { + "type": "algorithmic", + "algorithm": "esriCIELabAlgorithm", + "fromColor": [ + 105, + 168, + 184, + 255 + ], + "toColor": [ + 105, + 168, + 184, + 255 + ] + }, + { + "type": "algorithmic", + "algorithm": "esriCIELabAlgorithm", + "fromColor": [ + 75, + 127, + 153, + 255 + ], + "toColor": [ + 75, + 127, + 153, + 255 + ] + }, + { + "type": "algorithmic", + "algorithm": "esriCIELabAlgorithm", + "fromColor": [ + 48, + 86, + 122, + 255 + ], + "toColor": [ + 48, + 86, + 122, + 255 + ] + } + ] + }, + "classificationMethod": "esriClassifyNaturalBreaks" + }, + "field": "Value", + "classificationMethod": "esriClassifyNaturalBreaks", + "minValue": 7, + "classBreakInfos": [ + { + "symbol": { + "type": "esriSFS", + "style": "esriSFSSolid", + "color": [ + 230, + 238, + 207, + 255 + ], + "outline": { + "type": "esriSLS", + "style": "esriSLSSolid", + "color": [ + 110, + 110, + 110, + 255 + ], + "width": 0.7 + } + }, + "classMaxValue": 7, + "label": "7.000000" + }, + { + "symbol": { + "type": "esriSFS", + "style": "esriSFSSolid", + "color": [ + 155, + 196, + 193, + 255 + ], + "outline": { + "type": "esriSLS", + "style": "esriSLSSolid", + "color": [ + 110, + 110, + 110, + 255 + ], + "width": 0.7 + } + }, + "classMaxValue": 8, + "label": "7.000001 - 8.000000" + }, + { + "symbol": { + "type": "esriSFS", + "style": "esriSFSSolid", + "color": [ + 105, + 168, + 183, + 255 + ], + "outline": { + "type": "esriSLS", + "style": "esriSLSSolid", + "color": [ + 110, + 110, + 110, + 255 + ], + "width": 0.7 + } + }, + "classMaxValue": 11, + "label": "8.000001 - 11.000000" + }, + { + "symbol": { + "type": "esriSFS", + "style": "esriSFSSolid", + "color": [ + 75, + 126, + 152, + 255 + ], + "outline": { + "type": "esriSLS", + "style": "esriSLSSolid", + "color": [ + 110, + 110, + 110, + 255 + ], + "width": 0.7 + } + }, + "classMaxValue": 13, + "label": "11.000001 - 13.000000" + }, + { + "symbol": { + "type": "esriSFS", + "style": "esriSFSSolid", + "color": [ + 46, + 85, + 122, + 255 + ], + "outline": { + "type": "esriSLS", + "style": "esriSLSSolid", + "color": [ + 110, + 110, + 110, + 255 + ], + "width": 0.7 + } + }, + "classMaxValue": 20, + "label": "13.000001 - 20.000000" + } + ], + "legendOptions": { + "order": "ascendingValues" + } + }, + "scaleSymbols": true, + "transparency": 0, + "labelingInfo": null + }, + "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()) @@ -1398,9 +1697,41 @@ def testGraduatedRenderer(self): 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) + self.assertEqual(len(vl.renderer().ranges()), 5) + _range = vl.renderer().ranges()[0] + self.assertEqual(_range.lowerValue(), 7) + self.assertEqual(_range.upperValue(), 7) + self.assertEqual(_range.label(), '7.000000') + self.assertEqual(_range.symbol().color().name(), + '#e6eecf') + + _range = vl.renderer().ranges()[1] + self.assertEqual(_range.lowerValue(), 7) + self.assertEqual(_range.upperValue(), 8) + self.assertEqual(_range.label(), '7.000001 - 8.000000') + self.assertEqual(_range.symbol().color().name(), + '#9bc4c1') + + _range = vl.renderer().ranges()[2] + self.assertEqual(_range.lowerValue(), 8) + self.assertEqual(_range.upperValue(), 11) + self.assertEqual(_range.label(), '8.000001 - 11.000000') + self.assertEqual(_range.symbol().color().name(), + '#69a8b7') + + _range = vl.renderer().ranges()[3] + self.assertEqual(_range.lowerValue(), 11) + self.assertEqual(_range.upperValue(), 13) + self.assertEqual(_range.label(), '11.000001 - 13.000000') + self.assertEqual(_range.symbol().color().name(), + '#4b7e98') + + _range = vl.renderer().ranges()[4] + self.assertEqual(_range.lowerValue(), 13) + self.assertEqual(_range.upperValue(), 20) + self.assertEqual(_range.label(), '13.000001 - 20.000000') + self.assertEqual(_range.symbol().color().name(), + '#2e557a') def testBboxRestriction(self): """