diff --git a/python/PyQt6/core/auto_additions/qgssymbollayerutils.py b/python/PyQt6/core/auto_additions/qgssymbollayerutils.py index 67d3ae05fb3..e0dd6bf23d0 100644 --- a/python/PyQt6/core/auto_additions/qgssymbollayerutils.py +++ b/python/PyQt6/core/auto_additions/qgssymbollayerutils.py @@ -22,6 +22,7 @@ try: QgsSymbolLayerUtils.decodeBrushStyle = staticmethod(QgsSymbolLayerUtils.decodeBrushStyle) QgsSymbolLayerUtils.encodeSldBrushStyle = staticmethod(QgsSymbolLayerUtils.encodeSldBrushStyle) QgsSymbolLayerUtils.decodeSldBrushStyle = staticmethod(QgsSymbolLayerUtils.decodeSldBrushStyle) + QgsSymbolLayerUtils.hasSldSymbolizer = staticmethod(QgsSymbolLayerUtils.hasSldSymbolizer) QgsSymbolLayerUtils.decodeCoordinateReference = staticmethod(QgsSymbolLayerUtils.decodeCoordinateReference) QgsSymbolLayerUtils.encodeCoordinateReference = staticmethod(QgsSymbolLayerUtils.encodeCoordinateReference) QgsSymbolLayerUtils.decodeArrowHeadType = staticmethod(QgsSymbolLayerUtils.decodeArrowHeadType) diff --git a/python/PyQt6/core/auto_generated/symbology/qgssymbollayerutils.sip.in b/python/PyQt6/core/auto_generated/symbology/qgssymbollayerutils.sip.in index e6b87527649..4e60a056d2d 100644 --- a/python/PyQt6/core/auto_generated/symbology/qgssymbollayerutils.sip.in +++ b/python/PyQt6/core/auto_generated/symbology/qgssymbollayerutils.sip.in @@ -60,6 +60,13 @@ Contains utility functions for working with symbols and symbol layers. static QString encodeSldBrushStyle( Qt::BrushStyle style ); static Qt::BrushStyle decodeSldBrushStyle( const QString &str ); + static bool hasSldSymbolizer( const QDomElement &element ); +%Docstring +Returns ``True`` if a DOM ``element`` contains an SLD Symbolizer element. + +.. versionadded:: 3.42 +%End + static Qgis::SymbolCoordinateReference decodeCoordinateReference( const QString &string, bool *ok /Out/ = 0 ); %Docstring Decodes a ``string`` representing a symbol coordinate reference mode. diff --git a/python/core/auto_additions/qgssymbollayerutils.py b/python/core/auto_additions/qgssymbollayerutils.py index 67d3ae05fb3..e0dd6bf23d0 100644 --- a/python/core/auto_additions/qgssymbollayerutils.py +++ b/python/core/auto_additions/qgssymbollayerutils.py @@ -22,6 +22,7 @@ try: QgsSymbolLayerUtils.decodeBrushStyle = staticmethod(QgsSymbolLayerUtils.decodeBrushStyle) QgsSymbolLayerUtils.encodeSldBrushStyle = staticmethod(QgsSymbolLayerUtils.encodeSldBrushStyle) QgsSymbolLayerUtils.decodeSldBrushStyle = staticmethod(QgsSymbolLayerUtils.decodeSldBrushStyle) + QgsSymbolLayerUtils.hasSldSymbolizer = staticmethod(QgsSymbolLayerUtils.hasSldSymbolizer) QgsSymbolLayerUtils.decodeCoordinateReference = staticmethod(QgsSymbolLayerUtils.decodeCoordinateReference) QgsSymbolLayerUtils.encodeCoordinateReference = staticmethod(QgsSymbolLayerUtils.encodeCoordinateReference) QgsSymbolLayerUtils.decodeArrowHeadType = staticmethod(QgsSymbolLayerUtils.decodeArrowHeadType) diff --git a/python/core/auto_generated/symbology/qgssymbollayerutils.sip.in b/python/core/auto_generated/symbology/qgssymbollayerutils.sip.in index e6b87527649..4e60a056d2d 100644 --- a/python/core/auto_generated/symbology/qgssymbollayerutils.sip.in +++ b/python/core/auto_generated/symbology/qgssymbollayerutils.sip.in @@ -60,6 +60,13 @@ Contains utility functions for working with symbols and symbol layers. static QString encodeSldBrushStyle( Qt::BrushStyle style ); static Qt::BrushStyle decodeSldBrushStyle( const QString &str ); + static bool hasSldSymbolizer( const QDomElement &element ); +%Docstring +Returns ``True`` if a DOM ``element`` contains an SLD Symbolizer element. + +.. versionadded:: 3.42 +%End + static Qgis::SymbolCoordinateReference decodeCoordinateReference( const QString &string, bool *ok /Out/ = 0 ); %Docstring Decodes a ``string`` representing a symbol coordinate reference mode. diff --git a/src/core/symbology/qgscategorizedsymbolrenderer.cpp b/src/core/symbology/qgscategorizedsymbolrenderer.cpp index 6f58a2081eb..daa303306ad 100644 --- a/src/core/symbology/qgscategorizedsymbolrenderer.cpp +++ b/src/core/symbology/qgscategorizedsymbolrenderer.cpp @@ -150,7 +150,6 @@ void QgsRendererCategory::toSld( QDomDocument &doc, QDomElement &element, QVaria } QDomElement ruleElem = doc.createElement( QStringLiteral( "se:Rule" ) ); - element.appendChild( ruleElem ); QDomElement nameElem = doc.createElement( QStringLiteral( "se:Name" ) ); nameElem.appendChild( doc.createTextNode( mLabel ) ); @@ -199,6 +198,14 @@ void QgsRendererCategory::toSld( QDomDocument &doc, QDomElement &element, QVaria QgsSymbolLayerUtils::applyScaleDependency( doc, ruleElem, props ); mSymbol->toSld( doc, ruleElem, props ); + if ( !QgsSymbolLayerUtils::hasSldSymbolizer( ruleElem ) ) + { + // symbol could not be converted to SLD, or is an "empty" symbol. In this case we do not generate a rule, as + // SLD spec requires a Symbolizer element to be present + return; + } + + element.appendChild( ruleElem ); } /////////////////// diff --git a/src/core/symbology/qgsrendererrange.cpp b/src/core/symbology/qgsrendererrange.cpp index 6d6622a3a82..e8aeb9f54a1 100644 --- a/src/core/symbology/qgsrendererrange.cpp +++ b/src/core/symbology/qgsrendererrange.cpp @@ -138,7 +138,6 @@ void QgsRendererRange::toSld( QDomDocument &doc, QDomElement &element, QVariantM QString attrName = props[ QStringLiteral( "attribute" )].toString(); QDomElement ruleElem = doc.createElement( QStringLiteral( "se:Rule" ) ); - element.appendChild( ruleElem ); QDomElement nameElem = doc.createElement( QStringLiteral( "se:Name" ) ); nameElem.appendChild( doc.createTextNode( mLabel ) ); @@ -160,6 +159,14 @@ void QgsRendererRange::toSld( QDomDocument &doc, QDomElement &element, QVariantM QgsSymbolLayerUtils::createFunctionElement( doc, ruleElem, filterFunc ); mSymbol->toSld( doc, ruleElem, props ); + if ( !QgsSymbolLayerUtils::hasSldSymbolizer( ruleElem ) ) + { + // symbol could not be converted to SLD, or is an "empty" symbol. In this case we do not generate a rule, as + // SLD spec requires a Symbolizer element to be present + return; + } + + element.appendChild( ruleElem ); } ////////// diff --git a/src/core/symbology/qgsrulebasedrenderer.cpp b/src/core/symbology/qgsrulebasedrenderer.cpp index 4ed4f107a4e..8c0e2b828f5 100644 --- a/src/core/symbology/qgsrulebasedrenderer.cpp +++ b/src/core/symbology/qgsrulebasedrenderer.cpp @@ -375,7 +375,6 @@ void QgsRuleBasedRenderer::Rule::toSld( QDomDocument &doc, QDomElement &element, if ( mSymbol ) { QDomElement ruleElem = doc.createElement( QStringLiteral( "se:Rule" ) ); - element.appendChild( ruleElem ); //XXX: is the rule identifier, but our the Rule objects // have no properties could be used as identifier. Use the label. @@ -409,6 +408,13 @@ void QgsRuleBasedRenderer::Rule::toSld( QDomDocument &doc, QDomElement &element, QgsSymbolLayerUtils::applyScaleDependency( doc, ruleElem, props ); mSymbol->toSld( doc, ruleElem, props ); + + // Only create rules if symbol could be converted to SLD, and is not an "empty" symbol. Otherwise we do not generate a rule, as + // SLD spec requires a Symbolizer element to be present + if ( QgsSymbolLayerUtils::hasSldSymbolizer( ruleElem ) ) + { + element.appendChild( ruleElem ); + } } // loop into children rule list diff --git a/src/core/symbology/qgssymbollayerutils.cpp b/src/core/symbology/qgssymbollayerutils.cpp index ccc296f59d8..99784f69162 100644 --- a/src/core/symbology/qgssymbollayerutils.cpp +++ b/src/core/symbology/qgssymbollayerutils.cpp @@ -391,6 +391,20 @@ Qt::BrushStyle QgsSymbolLayerUtils::decodeSldBrushStyle( const QString &str ) return Qt::NoBrush; } +bool QgsSymbolLayerUtils::hasSldSymbolizer( const QDomElement &element ) +{ + const QDomNodeList children = element.childNodes(); + for ( int i = 0; i < children.size(); ++i ) + { + const QDomElement childElement = children.at( i ).toElement(); + if ( childElement.tagName() == QLatin1String( "se:LineSymbolizer" ) + || childElement.tagName() == QLatin1String( "se:PointSymbolizer" ) + || childElement.tagName() == QLatin1String( "se:PolygonSymbolizer" ) ) + return true; + } + return false; +} + Qgis::SymbolCoordinateReference QgsSymbolLayerUtils::decodeCoordinateReference( const QString &string, bool *ok ) { const QString compareString = string.trimmed(); diff --git a/src/core/symbology/qgssymbollayerutils.h b/src/core/symbology/qgssymbollayerutils.h index 6aa4b813428..233f56ed799 100644 --- a/src/core/symbology/qgssymbollayerutils.h +++ b/src/core/symbology/qgssymbollayerutils.h @@ -94,6 +94,13 @@ class CORE_EXPORT QgsSymbolLayerUtils static QString encodeSldBrushStyle( Qt::BrushStyle style ); static Qt::BrushStyle decodeSldBrushStyle( const QString &str ); + /** + * Returns TRUE if a DOM \a element contains an SLD Symbolizer element. + * + * \since QGIS 3.42 + */ + static bool hasSldSymbolizer( const QDomElement &element ); + /** * Decodes a \a string representing a symbol coordinate reference mode. * diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 16aa32a3f77..cad5816969a 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -301,6 +301,7 @@ ADD_PYTHON_TEST(PyQgsRenderContext test_qgsrendercontext.py) ADD_PYTHON_TEST(PyQgsRenderedItemResults test_qgsrendereditemresults.py) ADD_PYTHON_TEST(PyQgsRenderer test_qgsrenderer.py) ADD_PYTHON_TEST(PyQgsReport test_qgsreport.py) +ADD_PYTHON_TEST(PyQgsRuleBasedRenderer test_qgsrulebasedrenderer.py) ADD_PYTHON_TEST(PyQgsScaleBarRendererRegistry test_qgsscalebarrendererregistry.py) ADD_PYTHON_TEST(PyQgsScaleBarRenderers test_qgsscalebarrenderers.py) ADD_PYTHON_TEST(PyQgsScaleCalculator test_qgsscalecalculator.py) diff --git a/tests/src/python/test_qgscategorizedsymbolrenderer.py b/tests/src/python/test_qgscategorizedsymbolrenderer.py index b8a55d13b17..bc2606837be 100644 --- a/tests/src/python/test_qgscategorizedsymbolrenderer.py +++ b/tests/src/python/test_qgscategorizedsymbolrenderer.py @@ -935,6 +935,13 @@ class TestQgsCategorizedSymbolRenderer(QgisTestCase): symbol_f = createMarkerSymbol() renderer.addCategory(QgsRendererCategory(None, symbol_f, 'f', True, '4')) + # this category should NOT be included in the SLD, as it would otherwise result + # in an invalid se:rule with no symbolizer element + symbol_which_is_empty_in_sld = createFillSymbol() + symbol_which_is_empty_in_sld[0].setBrushStyle(Qt.NoBrush) + symbol_which_is_empty_in_sld[0].setStrokeStyle(Qt.NoPen) + renderer.addCategory(QgsRendererCategory(None, symbol_which_is_empty_in_sld, 'empty', True, '4')) + dom = QDomDocument() root = dom.createElement("FakeRoot") dom.appendChild(root) diff --git a/tests/src/python/test_qgsgraduatedsymbolrenderer.py b/tests/src/python/test_qgsgraduatedsymbolrenderer.py index c22748c2a9a..81a00b68c29 100644 --- a/tests/src/python/test_qgsgraduatedsymbolrenderer.py +++ b/tests/src/python/test_qgsgraduatedsymbolrenderer.py @@ -24,6 +24,7 @@ from qgis.core import ( QgsRendererRange, QgsRendererRangeLabelFormat, QgsVectorLayer, + QgsFillSymbol, ) import unittest from qgis.testing import start_app, QgisTestCase @@ -44,6 +45,13 @@ def createMarkerSymbol(): return symbol +def createFillSymbol(): + symbol = QgsFillSymbol.createSimple({ + "color": "100,150,50" + }) + return symbol + + def createMemoryLayer(values): ml = QgsVectorLayer("Point?crs=epsg:4236&field=id:integer&field=value:double", "test_data", "memory") @@ -563,6 +571,135 @@ class TestQgsGraduatedSymbolRenderer(QgisTestCase): self.assertTrue(ok) self.assertEqual(exp, """(log("field_name") >= 15.5) AND (log("field_name") <= 16.5)""") + def test_to_sld(self): + renderer = QgsGraduatedSymbolRenderer() + renderer.setClassAttribute('field_name') + + symbol_a = createMarkerSymbol() + renderer.addClassRange(QgsRendererRange(1, 2, symbol_a, 'a', True, '0')) + symbol_b = createMarkerSymbol() + renderer.addClassRange(QgsRendererRange(5, 6, symbol_b, 'b', True, '1')) + symbol_c = createMarkerSymbol() + renderer.addClassRange(QgsRendererRange(15.5, 16.5, symbol_c, 'c', False, '2')) + + # this category should NOT be included in the SLD, as it would otherwise result + # in an invalid se:rule with no symbolizer element + symbol_which_is_empty_in_sld = createFillSymbol() + symbol_which_is_empty_in_sld[0].setBrushStyle(Qt.NoBrush) + symbol_which_is_empty_in_sld[0].setStrokeStyle(Qt.NoPen) + renderer.addClassRange( + QgsRendererRange(25.5, 26.5, symbol_which_is_empty_in_sld, 'd', False, '2')) + + dom = QDomDocument() + root = dom.createElement("FakeRoot") + dom.appendChild(root) + renderer.toSld(dom, root, {}) + + expected = """ + + a + + a + + + + + field_name + 1 + + + field_name + 2 + + + + + + + square + + #649632 + + + #232323 + 0.5 + + + 11 + + + + + b + + b + + + + + field_name + 5 + + + field_name + 6 + + + + + + + square + + #649632 + + + #232323 + 0.5 + + + 11 + + + + + c + + c + + + + + field_name + 15.5 + + + field_name + 16.5 + + + + + + + square + + #649632 + + + #232323 + 0.5 + + + 11 + + + + +""" + + self.assertEqual(dom.toString(), expected) + if __name__ == "__main__": unittest.main() diff --git a/tests/src/python/test_qgsrulebasedrenderer.py b/tests/src/python/test_qgsrulebasedrenderer.py new file mode 100644 index 00000000000..2b91ee352a5 --- /dev/null +++ b/tests/src/python/test_qgsrulebasedrenderer.py @@ -0,0 +1,133 @@ +"""QGIS Unit tests for QgsRuleBasedRenderer + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" + +from qgis.PyQt.QtCore import Qt +from qgis.PyQt.QtXml import QDomDocument +from qgis.core import ( + QgsMarkerSymbol, + QgsFillSymbol, + QgsRuleBasedRenderer +) +import unittest +from qgis.testing import start_app, QgisTestCase + +start_app() + + +# =========================================================== +# Utility functions + + +def createMarkerSymbol(): + symbol = QgsMarkerSymbol.createSimple({ + "color": "100,150,50", + "name": "square", + "size": "3.0" + }) + return symbol + + +def createFillSymbol(): + symbol = QgsFillSymbol.createSimple({ + "color": "100,150,50" + }) + return symbol + + +class TestQgsRuleBasedSymbolRenderer(QgisTestCase): + + def test_to_sld(self): + root_rule = QgsRuleBasedRenderer.Rule(None) + symbol_a = createMarkerSymbol() + root_rule.appendChild( + QgsRuleBasedRenderer.Rule(symbol_a, filterExp='"something"=1', label="label a", description="rule a") + ) + symbol_b = createMarkerSymbol() + root_rule.appendChild( + QgsRuleBasedRenderer.Rule(symbol_b, filterExp='"something"=2', label="label b", description="rule b") + ) + + # this rule should NOT be included in the SLD, as it would otherwise result + # in an invalid se:rule with no symbolizer element + symbol_which_is_empty_in_sld = createFillSymbol() + symbol_which_is_empty_in_sld[0].setBrushStyle(Qt.NoBrush) + symbol_which_is_empty_in_sld[0].setStrokeStyle(Qt.NoPen) + root_rule.appendChild( + QgsRuleBasedRenderer.Rule(symbol_which_is_empty_in_sld, filterExp='"something"=3', label="label c", description="rule c")) + + renderer = QgsRuleBasedRenderer(root_rule) + + dom = QDomDocument() + root = dom.createElement("FakeRoot") + dom.appendChild(root) + renderer.toSld(dom, root, {}) + + expected = """ + + label a + + label a + rule a + + + + something + 1 + + + + + + square + + #649632 + + + #232323 + 0.5 + + + 11 + + + + + label b + + label b + rule b + + + + something + 2 + + + + + + square + + #649632 + + + #232323 + 0.5 + + + 11 + + + + +""" + self.assertEqual(dom.toString(), expected) + + +if __name__ == "__main__": + unittest.main()