[sld] Don't try to write rules/categorizes without symbolizers

Only create rules/categorized categories/graduated ranges if the
associated 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.
This commit is contained in:
Nyall Dawson 2024-11-19 10:36:07 +10:00
parent 52dd3047ac
commit 2abc9d92ca
13 changed files with 338 additions and 3 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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.

View File

@ -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 );
}
///////////////////

View File

@ -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 );
}
//////////

View File

@ -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: <se:Name> 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

View File

@ -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();

View File

@ -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.
*

View File

@ -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)

View File

@ -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)

View File

@ -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 = """<FakeRoot>
<se:Rule>
<se:Name>a</se:Name>
<se:Description>
<se:Title>a</se:Title>
</se:Description>
<ogc:Filter xmlns:ogc="http://www.opengis.net/ogc">
<ogc:And>
<ogc:PropertyIsGreaterThanOrEqualTo>
<ogc:PropertyName>field_name</ogc:PropertyName>
<ogc:Literal>1</ogc:Literal>
</ogc:PropertyIsGreaterThanOrEqualTo>
<ogc:PropertyIsLessThanOrEqualTo>
<ogc:PropertyName>field_name</ogc:PropertyName>
<ogc:Literal>2</ogc:Literal>
</ogc:PropertyIsLessThanOrEqualTo>
</ogc:And>
</ogc:Filter>
<se:PointSymbolizer>
<se:Graphic>
<se:Mark>
<se:WellKnownName>square</se:WellKnownName>
<se:Fill>
<se:SvgParameter name="fill">#649632</se:SvgParameter>
</se:Fill>
<se:Stroke>
<se:SvgParameter name="stroke">#232323</se:SvgParameter>
<se:SvgParameter name="stroke-width">0.5</se:SvgParameter>
</se:Stroke>
</se:Mark>
<se:Size>11</se:Size>
</se:Graphic>
</se:PointSymbolizer>
</se:Rule>
<se:Rule>
<se:Name>b</se:Name>
<se:Description>
<se:Title>b</se:Title>
</se:Description>
<ogc:Filter xmlns:ogc="http://www.opengis.net/ogc">
<ogc:And>
<ogc:PropertyIsGreaterThan>
<ogc:PropertyName>field_name</ogc:PropertyName>
<ogc:Literal>5</ogc:Literal>
</ogc:PropertyIsGreaterThan>
<ogc:PropertyIsLessThanOrEqualTo>
<ogc:PropertyName>field_name</ogc:PropertyName>
<ogc:Literal>6</ogc:Literal>
</ogc:PropertyIsLessThanOrEqualTo>
</ogc:And>
</ogc:Filter>
<se:PointSymbolizer>
<se:Graphic>
<se:Mark>
<se:WellKnownName>square</se:WellKnownName>
<se:Fill>
<se:SvgParameter name="fill">#649632</se:SvgParameter>
</se:Fill>
<se:Stroke>
<se:SvgParameter name="stroke">#232323</se:SvgParameter>
<se:SvgParameter name="stroke-width">0.5</se:SvgParameter>
</se:Stroke>
</se:Mark>
<se:Size>11</se:Size>
</se:Graphic>
</se:PointSymbolizer>
</se:Rule>
<se:Rule>
<se:Name>c</se:Name>
<se:Description>
<se:Title>c</se:Title>
</se:Description>
<ogc:Filter xmlns:ogc="http://www.opengis.net/ogc">
<ogc:And>
<ogc:PropertyIsGreaterThan>
<ogc:PropertyName>field_name</ogc:PropertyName>
<ogc:Literal>15.5</ogc:Literal>
</ogc:PropertyIsGreaterThan>
<ogc:PropertyIsLessThanOrEqualTo>
<ogc:PropertyName>field_name</ogc:PropertyName>
<ogc:Literal>16.5</ogc:Literal>
</ogc:PropertyIsLessThanOrEqualTo>
</ogc:And>
</ogc:Filter>
<se:PointSymbolizer>
<se:Graphic>
<se:Mark>
<se:WellKnownName>square</se:WellKnownName>
<se:Fill>
<se:SvgParameter name="fill">#649632</se:SvgParameter>
</se:Fill>
<se:Stroke>
<se:SvgParameter name="stroke">#232323</se:SvgParameter>
<se:SvgParameter name="stroke-width">0.5</se:SvgParameter>
</se:Stroke>
</se:Mark>
<se:Size>11</se:Size>
</se:Graphic>
</se:PointSymbolizer>
</se:Rule>
</FakeRoot>
"""
self.assertEqual(dom.toString(), expected)
if __name__ == "__main__":
unittest.main()

View File

@ -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 = """<FakeRoot>
<se:Rule>
<se:Name>label a</se:Name>
<se:Description>
<se:Title>label a</se:Title>
<se:Abstract>rule a</se:Abstract>
</se:Description>
<ogc:Filter xmlns:ogc="http://www.opengis.net/ogc">
<ogc:PropertyIsEqualTo>
<ogc:PropertyName>something</ogc:PropertyName>
<ogc:Literal>1</ogc:Literal>
</ogc:PropertyIsEqualTo>
</ogc:Filter>
<se:PointSymbolizer>
<se:Graphic>
<se:Mark>
<se:WellKnownName>square</se:WellKnownName>
<se:Fill>
<se:SvgParameter name="fill">#649632</se:SvgParameter>
</se:Fill>
<se:Stroke>
<se:SvgParameter name="stroke">#232323</se:SvgParameter>
<se:SvgParameter name="stroke-width">0.5</se:SvgParameter>
</se:Stroke>
</se:Mark>
<se:Size>11</se:Size>
</se:Graphic>
</se:PointSymbolizer>
</se:Rule>
<se:Rule>
<se:Name>label b</se:Name>
<se:Description>
<se:Title>label b</se:Title>
<se:Abstract>rule b</se:Abstract>
</se:Description>
<ogc:Filter xmlns:ogc="http://www.opengis.net/ogc">
<ogc:PropertyIsEqualTo>
<ogc:PropertyName>something</ogc:PropertyName>
<ogc:Literal>2</ogc:Literal>
</ogc:PropertyIsEqualTo>
</ogc:Filter>
<se:PointSymbolizer>
<se:Graphic>
<se:Mark>
<se:WellKnownName>square</se:WellKnownName>
<se:Fill>
<se:SvgParameter name="fill">#649632</se:SvgParameter>
</se:Fill>
<se:Stroke>
<se:SvgParameter name="stroke">#232323</se:SvgParameter>
<se:SvgParameter name="stroke-width">0.5</se:SvgParameter>
</se:Stroke>
</se:Mark>
<se:Size>11</se:Size>
</se:Graphic>
</se:PointSymbolizer>
</se:Rule>
</FakeRoot>
"""
self.assertEqual(dom.toString(), expected)
if __name__ == "__main__":
unittest.main()