Skip to content

Commit

Permalink
Merge pull request #51371 from elpaso/bugfix-sld-wellknown-polygon-fill
Browse files Browse the repository at this point in the history
SLD: implement import/export of wellknown polygon pattern fills
  • Loading branch information
elpaso committed Jan 5, 2023
2 parents c9453c4 + 7bd0207 commit 863a126
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 11 deletions.
139 changes: 132 additions & 7 deletions src/core/symbology/qgsfillsymbollayer.cpp
Expand Up @@ -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() )
Expand All @@ -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 );
Expand All @@ -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 )
Expand Down
19 changes: 17 additions & 2 deletions src/core/symbology/qgssymbollayerutils.cpp
Expand Up @@ -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 )
Expand Down
1 change: 0 additions & 1 deletion tests/src/python/test_qgssymbollayer.py
Expand Up @@ -813,7 +813,6 @@ def testQgsLinePatternFillSymbolLayer(self):
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
Expand Down
104 changes: 103 additions & 1 deletion tests/src/python/test_qgssymbollayer_readsld.py
Expand Up @@ -25,13 +25,16 @@

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,
Expand Down Expand Up @@ -466,6 +469,105 @@ def testLineOpacity():
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()

0 comments on commit 863a126

Please sign in to comment.