Skip to content

Commit

Permalink
Fixes #49728 : Fix layout legend when geometry generator is involved
Browse files Browse the repository at this point in the history
  • Loading branch information
troopa81 authored and nyalldawson committed May 25, 2023
1 parent f9020ca commit 453abdd
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 6 deletions.
28 changes: 23 additions & 5 deletions src/core/layertree/qgslayertreemodellegendnode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -687,11 +687,15 @@ QSizeF QgsSymbolLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemC
if ( QgsMarkerSymbol *markerSymbol = dynamic_cast<QgsMarkerSymbol *>( s ) )
{
const double size = markerSymbol->size( *context ) / context->scaleFactor();
height = size;
width = size;
if ( size > 0 )
{
height = size;
width = size;
}
}

const std::unique_ptr<QgsSymbol> minMaxSizeSymbol( QgsSymbolLayerUtils::restrictedSizeSymbol( s, minSymbolSize, maxSymbolSize, context, width, height ) );
bool restrictedSizeSymbolOK;
const std::unique_ptr<QgsSymbol> minMaxSizeSymbol( QgsSymbolLayerUtils::restrictedSizeSymbol( s, minSymbolSize, maxSymbolSize, context, width, height, &restrictedSizeSymbolOK ) );
if ( minMaxSizeSymbol )
{
s = minMaxSizeSymbol.get();
Expand Down Expand Up @@ -739,12 +743,14 @@ QSizeF QgsSymbolLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemC
// QGIS 4.0 -- ctx->context will be mandatory
const bool useAdvancedEffects = ctx->context ? ctx->context->flags() & Qgis::RenderContextFlag::UseAdvancedEffects : settings.useAdvancedEffects();
Q_NOWARN_DEPRECATED_POP

if ( opacity != 255 && useAdvancedEffects )
{
// if this is a semi transparent layer, we need to draw symbol to an image (to flatten it first)

const int maxBleed = static_cast< int >( std::ceil( QgsSymbolLayerUtils::estimateMaxSymbolBleed( s, *context ) ) );

//semi transparent layer, so need to draw symbol to an image (to flatten it first)
//create image which is same size as legend rect, in case symbol bleeds outside its allotted space
// create image which is same size as legend rect, in case symbol bleeds outside its allotted space
const QSize symbolSize( static_cast< int >( std::round( width * dotsPerMM ) ), static_cast<int >( std::round( height * dotsPerMM ) ) );
const QSize tempImageSize( symbolSize.width() + maxBleed * 2, symbolSize.height() + maxBleed * 2 );
QImage tempImage = QImage( tempImageSize, QImage::Format_ARGB32 );
Expand All @@ -764,6 +770,18 @@ QSizeF QgsSymbolLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemC
//draw rendered symbol image
p->drawImage( -maxBleed, -maxBleed, tempImage );
}
else if ( !restrictedSizeSymbolOK )
{
// if there is no restricted symbol size (because of geometry generator mainly) we need to ensure
// that there is no drawing outside the given size
const int maxBleed = static_cast< int >( std::ceil( QgsSymbolLayerUtils::estimateMaxSymbolBleed( s, *context ) ) );
const QSize symbolSize( static_cast< int >( std::round( width * dotsPerMM ) ), static_cast<int >( std::round( height * dotsPerMM ) ) );
const QSize maxSize( symbolSize.width() + maxBleed * 2, symbolSize.height() + maxBleed * 2 );
p->save();
p->setClipRect( -maxBleed, -maxBleed, maxSize.width(), maxSize.height(), Qt::IntersectClip );
s->drawPreviewIcon( p, symbolSize, context, false, nullptr, &patchShape );
p->restore();
}
else
{
s->drawPreviewIcon( p, QSize( static_cast< int >( std::round( width * dotsPerMM ) ), static_cast< int >( std::round( height * dotsPerMM ) ) ), context, false, nullptr, &patchShape );
Expand Down
13 changes: 13 additions & 0 deletions src/core/symbology/qgssymbollayerutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5067,6 +5067,19 @@ QgsSymbol *QgsSymbolLayerUtils::restrictedSizeSymbol( const QgsSymbol *s, double
}
else if ( lineSymbol )
{
const QgsSymbolLayerList sls = s->symbolLayers();
for ( const QgsSymbolLayer *sl : std::as_const( sls ) )
{
// geometry generators involved, there is no way to get a restricted size symbol
if ( sl->type() != Qgis::SymbolType::Line )
{
if ( ok )
*ok = false;

return nullptr;
}
}

size = lineSymbol->width( *context );
}
else
Expand Down
135 changes: 134 additions & 1 deletion tests/src/python/test_qgslayoutlegend.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
QgsCategorizedSymbolRenderer,
QgsCoordinateReferenceSystem,
QgsExpression,
QgsFeature,
QgsFillSymbol,
QgsFontUtils,
QgsGeometry,
QgsGeometryGeneratorSymbolLayer,
QgsLayout,
QgsLayoutItem,
QgsLayoutItemLegend,
Expand All @@ -41,7 +44,9 @@
QgsRendererCategory,
QgsRuleBasedRenderer,
QgsSingleSymbolRenderer,
QgsVectorLayer,
QgsSymbol,
QgsTextFormat,
QgsVectorLayer
)
from qgis.testing import start_app, unittest

Expand Down Expand Up @@ -895,6 +900,134 @@ def test_rulebased_child_filter(self):
TestQgsLayoutItemLegend.report += checker.report()
self.assertTrue(result, message)

def testGeomGeneratorPoints(self):
"""Test legend behavior when geometry generator on points is involved"""
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
QgsProject.instance().addMapLayers([point_layer])

sub_symbol = QgsFillSymbol.createSimple({'color': '#8888ff', 'outline_style': 'no'})
sym = QgsMarkerSymbol()
buffer_layer = QgsGeometryGeneratorSymbolLayer.create(
{'geometryModifier': 'buffer($geometry, 0.05)'})
buffer_layer.setSymbolType(QgsSymbol.Fill)
buffer_layer.setSubSymbol(sub_symbol)
sym.changeSymbolLayer(0, buffer_layer)
point_layer.setRenderer(QgsSingleSymbolRenderer(sym))

s = QgsMapSettings()
s.setLayers([point_layer])
layout = QgsLayout(QgsProject.instance())
layout.initializeDefaults()

map = QgsLayoutItemMap(layout)
map.attemptSetSceneRect(QRectF(20, 20, 80, 80))
map.setFrameEnabled(True)
map.setLayers([point_layer])
layout.addLayoutItem(map)

legend = QgsLayoutItemLegend(layout)
legend.setTitle("Legend")
legend.attemptSetSceneRect(QRectF(120, 20, 80, 80))
legend.setFrameEnabled(True)
legend.setFrameStrokeWidth(QgsLayoutMeasurement(2))
legend.setBackgroundColor(QColor(200, 200, 200))
legend.setTitle('')
legend.setLegendFilterByMapEnabled(True)
legend.setSymbolWidth(20)
legend.setSymbolHeight(10)

text_format = QgsTextFormat()
text_format.setFont(QgsFontUtils.getStandardTestFont("Bold"))
text_format.setSize(16)

for legend_item in [QgsLegendStyle.Title, QgsLegendStyle.Group, QgsLegendStyle.Subgroup,
QgsLegendStyle.Symbol, QgsLegendStyle.SymbolLabel]:
style = legend.style(legend_item)
style.setTextFormat(text_format)
legend.setStyle(legend_item, style)

# disable auto resizing
legend.setResizeToContents(False)

layout.addLayoutItem(legend)
legend.setLinkedMap(map)

map.setExtent(QgsRectangle(-102.51, 41.16, -102.36, 41.30))

checker = QgsLayoutChecker(
'composer_legend_geomgenerator_point', layout)
checker.setControlPathPrefix("composer_legend")
result, message = checker.testLayout()
TestQgsLayoutItemLegend.report += checker.report()
self.assertTrue(result, message)
QgsProject.instance().removeMapLayers([point_layer.id()])

def testGeomGeneratorLines(self):
"""Test legend behavior when geometry generator on lines is involved"""
line_path = os.path.join(TEST_DATA_DIR, 'lines.shp')
line_layer = QgsVectorLayer(line_path, 'lines', 'ogr')
QgsProject.instance().addMapLayers([line_layer])

sub_symbol = QgsFillSymbol.createSimple({'color': '#8888ff', 'outline_style': 'no'})
sym = QgsLineSymbol()
buffer_layer = QgsGeometryGeneratorSymbolLayer.create(
{'geometryModifier': 'buffer($geometry, 0.2)'})
buffer_layer.setSymbolType(QgsSymbol.Fill)
buffer_layer.setSubSymbol(sub_symbol)
sym.changeSymbolLayer(0, buffer_layer)
line_layer.setRenderer(QgsSingleSymbolRenderer(sym))

s = QgsMapSettings()
s.setLayers([line_layer])
layout = QgsLayout(QgsProject.instance())
layout.initializeDefaults()

map = QgsLayoutItemMap(layout)
map.attemptSetSceneRect(QRectF(20, 20, 80, 80))
map.setFrameEnabled(True)
map.setLayers([line_layer])
layout.addLayoutItem(map)
map.setExtent(line_layer.extent())

legend = QgsLayoutItemLegend(layout)
legend.setTitle("Legend")
legend.attemptSetSceneRect(QRectF(120, 20, 80, 80))
legend.setFrameEnabled(True)
legend.setFrameStrokeWidth(QgsLayoutMeasurement(2))
legend.setBackgroundColor(QColor(200, 200, 200))
legend.setTitle('')
legend.setLegendFilterByMapEnabled(True)
legend.setSymbolWidth(20)
legend.setSymbolHeight(10)

text_format = QgsTextFormat()
text_format.setFont(QgsFontUtils.getStandardTestFont("Bold"))
text_format.setSize(16)

for legend_item in [QgsLegendStyle.Title, QgsLegendStyle.Group, QgsLegendStyle.Subgroup,
QgsLegendStyle.Symbol, QgsLegendStyle.SymbolLabel]:
style = legend.style(legend_item)
style.setTextFormat(text_format)
legend.setStyle(legend_item, style)

# disable auto resizing
legend.setResizeToContents(False)

layout.addLayoutItem(legend)
legend.setLinkedMap(map)

map.setExtent(QgsRectangle(-100.3127, 35.7607, -98.5259, 36.5145))

checker = QgsLayoutChecker(
'composer_legend_geomgenerator_line', layout)
checker.setControlPathPrefix("composer_legend")
result, message = checker.testLayout()
TestQgsLayoutItemLegend.report += checker.report()
self.assertTrue(result, message)

QgsProject.instance().removeMapLayers([line_layer.id()])


if __name__ == '__main__':
unittest.main()
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 453abdd

Please sign in to comment.