From 832c6fcdc08ab632f0ac952cfe7b76df95f92fce Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Wed, 4 Jan 2023 13:10:33 +0100 Subject: [PATCH 1/3] SLD: implement import/export of wellknown polygon pattern fills Tested with GeoServer. --- src/core/symbology/qgsfillsymbollayer.cpp | 139 +++++++++++++++++- src/core/symbology/qgssymbollayerutils.cpp | 19 ++- .../src/python/test_qgssymbollayer_readsld.py | 104 ++++++++++++- 3 files changed, 252 insertions(+), 10 deletions(-) diff --git a/src/core/symbology/qgsfillsymbollayer.cpp b/src/core/symbology/qgsfillsymbollayer.cpp index 4e72f0558a9..b49e20bb86b 100644 --- a/src/core/symbology/qgsfillsymbollayer.cpp +++ b/src/core/symbology/qgsfillsymbollayer.cpp @@ -4229,7 +4229,7 @@ QgsPointPatternFillSymbolLayer *QgsPointPatternFillSymbolLayer::clone() const void QgsPointPatternFillSymbolLayer::toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props ) const { - for ( int i = 0; i < mMarkerSymbol->symbolLayerCount(); i++ ) + for ( int symboLayerIdx = 0; symboLayerIdx < mMarkerSymbol->symbolLayerCount(); symboLayerIdx++ ) { QDomElement symbolizerElem = doc.createElement( QStringLiteral( "se:PolygonSymbolizer" ) ); if ( !props.value( QStringLiteral( "uom" ), QString() ).toString().isEmpty() ) @@ -4245,14 +4245,21 @@ void QgsPointPatternFillSymbolLayer::toSld( QDomDocument &doc, QDomElement &elem QDomElement graphicFillElem = doc.createElement( QStringLiteral( "se:GraphicFill" ) ); fillElem.appendChild( graphicFillElem ); + QgsSymbolLayer *layer = mMarkerSymbol->symbolLayer( symboLayerIdx ); + + // Converts to GeoServer "graphic-margin": symbol size must be subtracted from distance and then divided by 2 + const double markerSize { mMarkerSymbol->size() }; + // store distanceX, distanceY, displacementX, displacementY in a double dx = QgsSymbolLayerUtils::rescaleUom( mDistanceX, mDistanceXUnit, props ); double dy = QgsSymbolLayerUtils::rescaleUom( mDistanceY, mDistanceYUnit, props ); - QString dist = QgsSymbolLayerUtils::encodePoint( QPointF( dx, dy ) ); - QDomElement distanceElem = QgsSymbolLayerUtils::createVendorOptionElement( doc, QStringLiteral( "distance" ), dist ); - symbolizerElem.appendChild( distanceElem ); + // From: https://docs.geoserver.org/stable/en/user/styling/sld/extensions/margins.html + // top-bottom,right-left (two values, top and bottom sharing the same value) + const QString marginSpec = QString( "%1 %2" ).arg( qgsDoubleToString( ( dy - markerSize ) / 2, 2 ), qgsDoubleToString( ( dx - markerSize ) / 2, 2 ) ); + + QDomElement graphicMarginElem = QgsSymbolLayerUtils::createVendorOptionElement( doc, QStringLiteral( "graphic-margin" ), marginSpec ); + symbolizerElem.appendChild( graphicMarginElem ); - QgsSymbolLayer *layer = mMarkerSymbol->symbolLayer( i ); if ( QgsMarkerSymbolLayer *markerLayer = dynamic_cast( layer ) ) { markerLayer->writeSldMarker( doc, graphicFillElem, props ); @@ -4272,8 +4279,126 @@ void QgsPointPatternFillSymbolLayer::toSld( QDomDocument &doc, QDomElement &elem QgsSymbolLayer *QgsPointPatternFillSymbolLayer::createFromSld( QDomElement &element ) { - Q_UNUSED( element ) - return nullptr; + + // input element is PolygonSymbolizer + + QDomElement fillElem = element.firstChildElement( QStringLiteral( "Fill" ) ); + if ( fillElem.isNull() ) + return nullptr; + + QDomElement graphicFillElem = fillElem.firstChildElement( QStringLiteral( "GraphicFill" ) ); + if ( graphicFillElem.isNull() ) + return nullptr; + + QDomElement graphicElem = graphicFillElem.firstChildElement( QStringLiteral( "Graphic" ) ); + if ( graphicElem.isNull() ) + return nullptr; + + QgsSymbolLayer *simpleMarkerSl = QgsSymbolLayerUtils::createMarkerLayerFromSld( graphicFillElem ); + if ( !simpleMarkerSl ) + return nullptr; + + + QgsSymbolLayerList layers; + layers.append( simpleMarkerSl ); + + std::unique_ptr< QgsMarkerSymbol > marker = std::make_unique< QgsMarkerSymbol >( layers ); + + // Converts from GeoServer "graphic-margin": symbol size must be added and margin doubled + const double markerSize { marker->size() }; + + std::unique_ptr< QgsPointPatternFillSymbolLayer > pointPatternFillSl = std::make_unique< QgsPointPatternFillSymbolLayer >(); + pointPatternFillSl->setSubSymbol( marker.release() ); + + // Set distance X and Y from vendor options + QgsStringMap vendorOptions = QgsSymbolLayerUtils::getVendorOptionList( element ); + for ( QgsStringMap::iterator it = vendorOptions.begin(); it != vendorOptions.end(); ++it ) + { + if ( it.key() == QLatin1String( "graphic-margin" ) ) + { + + // This may not be correct in all cases, TODO: check "uom" + pointPatternFillSl->setDistanceXUnit( QgsUnitTypes::RenderUnit::RenderPixels ); + pointPatternFillSl->setDistanceYUnit( QgsUnitTypes::RenderUnit::RenderPixels ); + + const QStringList values = it.value().split( ' ' ); + + switch ( values.count( ) ) + { + case 1: // top-right-bottom-left (single value for all four margins) + { + bool ok; + const double v { values.at( 0 ).toDouble( &ok ) }; + if ( ok ) + { + pointPatternFillSl->setDistanceX( v * 2 + markerSize ); + pointPatternFillSl->setDistanceY( v * 2 + markerSize ); + } + break; + } + case 2: // top-bottom,right-left (two values, top and bottom sharing the same value) + { + bool ok; + const double vX { values.at( 1 ).toDouble( &ok ) }; + if ( ok ) + { + pointPatternFillSl->setDistanceX( vX * 2 + markerSize ); + } + const double vY { values.at( 0 ).toDouble( &ok ) }; + if ( ok ) + { + pointPatternFillSl->setDistanceY( vY * 2 + markerSize ); + } + break; + } + case 3: // top,right-left,bottom (three values, with right and left sharing the same value) + { + bool ok; + const double vX { values.at( 1 ).toDouble( &ok ) }; + if ( ok ) + { + pointPatternFillSl->setDistanceX( vX * 2 + markerSize ); + } + const double vYt { values.at( 0 ).toDouble( &ok ) }; + if ( ok ) + { + const double vYb { values.at( 2 ).toDouble( &ok ) }; + if ( ok ) + { + pointPatternFillSl->setDistanceY( ( vYt + vYb ) + markerSize ); + } + } + break; + } + case 4: // top,right,bottom,left (one explicit value per margin) + { + bool ok; + const double vYt { values.at( 0 ).toDouble( &ok ) }; + if ( ok ) + { + const double vYb { values.at( 2 ).toDouble( &ok ) }; + if ( ok ) + { + pointPatternFillSl->setDistanceY( ( vYt + vYb ) + markerSize ); + } + } + const double vXr { values.at( 1 ).toDouble( &ok ) }; + if ( ok ) + { + const double vXl { values.at( 3 ).toDouble( &ok ) }; + if ( ok ) + { + pointPatternFillSl->setDistanceX( ( vXr + vXl ) + markerSize ); + } + } + break; + } + default: + break; + } + } + } + return pointPatternFillSl.release(); } bool QgsPointPatternFillSymbolLayer::setSubSymbol( QgsSymbol *symbol ) diff --git a/src/core/symbology/qgssymbollayerutils.cpp b/src/core/symbology/qgssymbollayerutils.cpp index d26475ff1dc..33cad29cefe 100644 --- a/src/core/symbology/qgssymbollayerutils.cpp +++ b/src/core/symbology/qgssymbollayerutils.cpp @@ -1844,8 +1844,23 @@ bool QgsSymbolLayerUtils::needLinePatternFill( QDomElement &element ) bool QgsSymbolLayerUtils::needPointPatternFill( QDomElement &element ) { - Q_UNUSED( element ) - return false; + const QDomElement fillElem = element.firstChildElement( QStringLiteral( "Fill" ) ); + if ( fillElem.isNull() ) + return false; + + const QDomElement graphicFillElem = fillElem.firstChildElement( QStringLiteral( "GraphicFill" ) ); + if ( graphicFillElem.isNull() ) + return false; + + const QDomElement graphicElem = graphicFillElem.firstChildElement( QStringLiteral( "Graphic" ) ); + if ( graphicElem.isNull() ) + return false; + + const QDomElement markElem = graphicElem.firstChildElement( QStringLiteral( "Mark" ) ); + if ( markElem.isNull() ) + return false; + + return true; } bool QgsSymbolLayerUtils::needSvgFill( QDomElement &element ) diff --git a/tests/src/python/test_qgssymbollayer_readsld.py b/tests/src/python/test_qgssymbollayer_readsld.py index 3501b86b3ac..8a233e6d35d 100644 --- a/tests/src/python/test_qgssymbollayer_readsld.py +++ b/tests/src/python/test_qgssymbollayer_readsld.py @@ -25,13 +25,16 @@ import qgis # NOQA import os from qgis.PyQt.QtXml import QDomDocument +from qgis.PyQt.QtCore import QTemporaryDir from qgis.testing import start_app, unittest -from qgis.core import (QgsVectorLayer, +from qgis.core import (Qgis, + QgsVectorLayer, QgsFeature, QgsGeometry, QgsUnitTypes, QgsPointXY, QgsSvgMarkerSymbolLayer, + QgsSymbol, QgsEllipseSymbolLayer, QgsSimpleFillSymbolLayer, QgsSVGFillSymbolLayer, @@ -466,6 +469,105 @@ class TestQgsSymbolLayerReadSld(unittest.TestCase): self.assertEqual(settings.yOffset, 0) self.assertEqual(settings.offsetUnits, QgsUnitTypes.RenderPixels) + def test_read_circle(self): + """Test wellknown name circle polygon fill""" + + sld = """ + + + Single symbol fill + + Single symbol fill + + + Single symbol + + + + + + circle + + #db1e2a + + + #801119 + 1.5 + + + 14 + + + + {} + + + + #ff0000 + 2 + bevel + + + + + + + + """ + + tmp_dir = QTemporaryDir() + tmp_path = tmp_dir.path() + sld_path = os.path.join(tmp_path, 'circle_fill.sld') + + layer = createLayerWithOnePolygon() + + def _check_layer(layer, yMargin=10, xMargin=15): + """ + - QgsFillSymbol + - layers + - QgsPointPatternFillSymbolLayer + - subSymbol: QgsMarkerSymbol + - layers + - QgsSimpleMarkerSymbolLayer (shape) + """ + layer.loadSldStyle(sld_path) + point_pattern_fill_symbol_layer = layer.renderer().symbol().symbolLayers()[0] + marker = point_pattern_fill_symbol_layer.subSymbol() + self.assertEqual(marker.type(), QgsSymbol.SymbolType.Marker) + marker_symbol = marker.symbolLayers()[0] + self.assertEqual(marker_symbol.strokeColor().name(), '#801119') + self.assertEqual(marker_symbol.strokeWidth(), 1.5) + self.assertEqual(marker_symbol.shape(), Qgis.MarkerShape.Circle) + self.assertEqual(marker_symbol.size(), 14) + self.assertEqual(point_pattern_fill_symbol_layer.distanceXUnit(), QgsUnitTypes.RenderUnit.RenderPixels) + self.assertEqual(point_pattern_fill_symbol_layer.distanceYUnit(), QgsUnitTypes.RenderUnit.RenderPixels) + self.assertEqual(point_pattern_fill_symbol_layer.distanceX(), xMargin * 2 + marker_symbol.size()) + self.assertEqual(point_pattern_fill_symbol_layer.distanceY(), yMargin * 2 + marker_symbol.size()) + + with open(sld_path, 'w+') as f: + f.write(sld.format('25')) + _check_layer(layer, 25, 25) + + # From: https://docs.geoserver.org/stable/en/user/styling/sld/extensions/margins.html + # top,right,bottom,left (one explicit value per margin) + # top,right-left,bottom (three values, with right and left sharing the same value) + # top-bottom,right-left (two values, top and bottom sharing the same value) + # top-right-bottom-left (single value for all four margins) + + for margin in ('10 15', '10 15 10', '10 15 10 15'): + with open(sld_path, 'w+') as f: + f.write(sld.format(margin)) + _check_layer(layer) + + # Round trip + dom = QDomDocument() + root = dom.createElement("FakeRoot") + dom.appendChild(root) + result = layer.saveSldStyle(sld_path) + self.assertTrue(result) + + _check_layer(layer) + if __name__ == '__main__': unittest.main() From 189ce416e6cc7686547ca7752f6d1482cb151b91 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Wed, 4 Jan 2023 16:36:34 +0100 Subject: [PATCH 2/3] Fix test --- tests/src/python/test_qgssymbollayer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/src/python/test_qgssymbollayer.py b/tests/src/python/test_qgssymbollayer.py index 28c8eb7f13a..85ff6fd094d 100644 --- a/tests/src/python/test_qgssymbollayer.py +++ b/tests/src/python/test_qgssymbollayer.py @@ -813,7 +813,6 @@ class TestQgsSymbolLayer(unittest.TestCase): self.assertEqual(mSymbolLayer.subSymbol().color(), QColor(250, 150, 200)) self.assertEqual(mSymbolLayer.color(), QColor(250, 150, 200)) - @unittest.expectedFailure def testQgsPointPatternFillSymbolLayerSld(self): """ Create a new style from a .sld file and match test From 7bd02075dccb3008401de102e599dfda69521853 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Thu, 5 Jan 2023 08:29:32 +0100 Subject: [PATCH 3/3] typo --- src/core/symbology/qgsfillsymbollayer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/symbology/qgsfillsymbollayer.cpp b/src/core/symbology/qgsfillsymbollayer.cpp index b49e20bb86b..d020c9e6f0a 100644 --- a/src/core/symbology/qgsfillsymbollayer.cpp +++ b/src/core/symbology/qgsfillsymbollayer.cpp @@ -4229,7 +4229,7 @@ QgsPointPatternFillSymbolLayer *QgsPointPatternFillSymbolLayer::clone() const void QgsPointPatternFillSymbolLayer::toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props ) const { - for ( int symboLayerIdx = 0; symboLayerIdx < mMarkerSymbol->symbolLayerCount(); symboLayerIdx++ ) + for ( int symbolLayerIdx = 0; symbolLayerIdx < mMarkerSymbol->symbolLayerCount(); symbolLayerIdx++ ) { QDomElement symbolizerElem = doc.createElement( QStringLiteral( "se:PolygonSymbolizer" ) ); if ( !props.value( QStringLiteral( "uom" ), QString() ).toString().isEmpty() ) @@ -4245,7 +4245,7 @@ void QgsPointPatternFillSymbolLayer::toSld( QDomDocument &doc, QDomElement &elem QDomElement graphicFillElem = doc.createElement( QStringLiteral( "se:GraphicFill" ) ); fillElem.appendChild( graphicFillElem ); - QgsSymbolLayer *layer = mMarkerSymbol->symbolLayer( symboLayerIdx ); + QgsSymbolLayer *layer = mMarkerSymbol->symbolLayer( symbolLayerIdx ); // Converts to GeoServer "graphic-margin": symbol size must be subtracted from distance and then divided by 2 const double markerSize { mMarkerSymbol->size() };