Merge pull request #51371 from elpaso/bugfix-sld-wellknown-polygon-fill

SLD: implement import/export of wellknown polygon pattern fills
This commit is contained in:
Alessandro Pasotti 2023-01-05 08:35:44 +01:00 committed by GitHub
commit 863a126175
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 252 additions and 11 deletions

View File

@ -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 symbolLayerIdx = 0; symbolLayerIdx < mMarkerSymbol->symbolLayerCount(); symbolLayerIdx++ )
{
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( symbolLayerIdx );
// 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 <VendorOption>
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<QgsMarkerSymbolLayer *>( 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 )

View File

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

View File

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

View File

@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
<StyledLayerDescriptor xmlns="http://www.opengis.net/sld" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ogc="http://www.opengis.net/ogc" version="1.1.0" xsi:schemaLocation="http://www.opengis.net/sld http://schemas.opengis.net/sld/1.1.0/StyledLayerDescriptor.xsd" xmlns:se="http://www.opengis.net/se">
<NamedLayer>
<se:Name>Single symbol fill</se:Name>
<UserStyle>
<se:Name>Single symbol fill</se:Name>
<se:FeatureTypeStyle>
<se:Rule>
<se:Name>Single symbol</se:Name>
<se:PolygonSymbolizer>
<se:Fill>
<se:GraphicFill>
<se:Graphic>
<se:Mark>
<se:WellKnownName>circle</se:WellKnownName>
<se:Fill>
<se:SvgParameter name="fill">#db1e2a</se:SvgParameter>
</se:Fill>
<se:Stroke>
<se:SvgParameter name="stroke">#801119</se:SvgParameter>
<se:SvgParameter name="stroke-width">1.5</se:SvgParameter>
</se:Stroke>
</se:Mark>
<se:Size>14</se:Size>
</se:Graphic>
</se:GraphicFill>
</se:Fill>
<sld:VendorOption name="graphic-margin">{}</sld:VendorOption>
</se:PolygonSymbolizer>
<se:PolygonSymbolizer>
<se:Stroke>
<se:SvgParameter name="stroke">#ff0000</se:SvgParameter>
<se:SvgParameter name="stroke-width">2</se:SvgParameter>
<se:SvgParameter name="stroke-linejoin">bevel</se:SvgParameter>
</se:Stroke>
</se:PolygonSymbolizer>
</se:Rule>
</se:FeatureTypeStyle>
</UserStyle>
</NamedLayer>
</StyledLayerDescriptor>
"""
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()