Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[FEATURE] Add new variable @geometry_ring_num for data defined
styling when rendering polygon rings

The variable is available whenever a polygon outline is being
rendered (e.g. as a simple line, marker line, etc). It will
be set to 0 for the exterior ring, and 1, 2, 3... for interior
rings.
  • Loading branch information
nyalldawson committed Mar 31, 2021
1 parent 684a422 commit 8f510cf
Show file tree
Hide file tree
Showing 15 changed files with 142 additions and 2 deletions.
2 changes: 2 additions & 0 deletions python/core/auto_generated/qgsexpressioncontext.sip.in
Expand Up @@ -791,6 +791,8 @@ Clears all cached values from the context.
static const QString EXPR_SYMBOL_ANGLE;
static const QString EXPR_GEOMETRY_PART_COUNT;
static const QString EXPR_GEOMETRY_PART_NUM;

static const QString EXPR_GEOMETRY_RING_NUM;
static const QString EXPR_GEOMETRY_POINT_COUNT;
static const QString EXPR_GEOMETRY_POINT_NUM;
static const QString EXPR_CLUSTER_SIZE;
Expand Down
1 change: 1 addition & 0 deletions src/core/expression/qgsexpression.cpp
Expand Up @@ -842,6 +842,7 @@ void QgsExpression::initVariableHelp()
//symbol variables
sVariableHelpTexts()->insert( QStringLiteral( "geometry_part_count" ), QCoreApplication::translate( "variable_help", "Number of parts in rendered feature's geometry." ) );
sVariableHelpTexts()->insert( QStringLiteral( "geometry_part_num" ), QCoreApplication::translate( "variable_help", "Current geometry part number for feature being rendered." ) );
sVariableHelpTexts()->insert( QStringLiteral( "geometry_ring_num" ), QCoreApplication::translate( "variable_help", "Current geometry ring number for feature being rendered (for polygon features only). The exterior ring has corresponds to a value of 0." ) );
sVariableHelpTexts()->insert( QStringLiteral( "geometry_point_count" ), QCoreApplication::translate( "variable_help", "Number of points in the rendered geometry's part. It is only meaningful for line geometries and for symbol layers that set this variable." ) );
sVariableHelpTexts()->insert( QStringLiteral( "geometry_point_num" ), QCoreApplication::translate( "variable_help", "Current point number in the rendered geometry's part. It is only meaningful for line geometries and for symbol layers that set this variable." ) );

Expand Down
1 change: 1 addition & 0 deletions src/core/qgsexpressioncontext.cpp
Expand Up @@ -24,6 +24,7 @@ const QString QgsExpressionContext::EXPR_SYMBOL_COLOR( QStringLiteral( "symbol_c
const QString QgsExpressionContext::EXPR_SYMBOL_ANGLE( QStringLiteral( "symbol_angle" ) );
const QString QgsExpressionContext::EXPR_GEOMETRY_PART_COUNT( QStringLiteral( "geometry_part_count" ) );
const QString QgsExpressionContext::EXPR_GEOMETRY_PART_NUM( QStringLiteral( "geometry_part_num" ) );
const QString QgsExpressionContext::EXPR_GEOMETRY_RING_NUM( QStringLiteral( "geometry_ring_num" ) );
const QString QgsExpressionContext::EXPR_GEOMETRY_POINT_COUNT( QStringLiteral( "geometry_point_count" ) );
const QString QgsExpressionContext::EXPR_GEOMETRY_POINT_NUM( QStringLiteral( "geometry_point_num" ) );
const QString QgsExpressionContext::EXPR_CLUSTER_SIZE( QStringLiteral( "cluster_size" ) );
Expand Down
6 changes: 6 additions & 0 deletions src/core/qgsexpressioncontext.h
Expand Up @@ -727,6 +727,12 @@ class CORE_EXPORT QgsExpressionContext
static const QString EXPR_GEOMETRY_PART_COUNT;
//! Inbuilt variable name for geometry part number variable
static const QString EXPR_GEOMETRY_PART_NUM;

/**
* Inbuilt variable name for geometry ring number variable.
* \since QGIS 3.20
*/
static const QString EXPR_GEOMETRY_RING_NUM;
//! Inbuilt variable name for point count variable
static const QString EXPR_GEOMETRY_POINT_COUNT;
//! Inbuilt variable name for point number variable
Expand Down
20 changes: 20 additions & 0 deletions src/core/symbology/qgslinesymbollayer.cpp
Expand Up @@ -279,6 +279,9 @@ void QgsSimpleLineSymbolLayer::renderPolygonStroke( const QPolygonF &points, con
return;
}

QgsExpressionContextScope *scope = new QgsExpressionContextScope();
QgsExpressionContextScopePopper scopePopper( context.renderContext().expressionContext(), scope );

if ( mDrawInsidePolygon )
p->save();

Expand Down Expand Up @@ -307,6 +310,8 @@ void QgsSimpleLineSymbolLayer::renderPolygonStroke( const QPolygonF &points, con
p->setClipPath( clipPath, Qt::IntersectClip );
}

scope->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_RING_NUM, 0, true ) );

renderPolyline( points, context );
}
break;
Expand All @@ -323,8 +328,14 @@ void QgsSimpleLineSymbolLayer::renderPolygonStroke( const QPolygonF &points, con
case InteriorRingsOnly:
{
mOffset = -mOffset; // invert the offset for rings!
int ringIndex = 1;
for ( const QPolygonF &ring : std::as_const( *rings ) )
{
scope->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_RING_NUM, ringIndex, true ) );

renderPolyline( ring, context );
ringIndex++;
}
mOffset = -mOffset;
}
break;
Expand Down Expand Up @@ -1310,12 +1321,19 @@ void QgsTemplatedLineSymbolLayerBase::renderPolygonStroke( const QPolygonF &poin
context.renderContext().setGeometry( curvePolygon->exteriorRing() );
}

QgsExpressionContextScope *scope = new QgsExpressionContextScope();
QgsExpressionContextScopePopper scopePopper( context.renderContext().expressionContext(), scope );

switch ( mRingFilter )
{
case AllRings:
case ExteriorRingOnly:
{
scope->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_RING_NUM, 0, true ) );

renderPolyline( points, context );
break;
}
case InteriorRingsOnly:
break;
}
Expand All @@ -1334,6 +1352,8 @@ void QgsTemplatedLineSymbolLayerBase::renderPolygonStroke( const QPolygonF &poin
{
context.renderContext().setGeometry( curvePolygon->interiorRing( i ) );
}
scope->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_RING_NUM, i + 1, true ) );

renderPolyline( rings->at( i ), context );
}
mOffset = -mOffset;
Expand Down
13 changes: 13 additions & 0 deletions src/core/symbology/qgssymbollayer.cpp
Expand Up @@ -30,6 +30,7 @@
#include "qgsmultipoint.h"
#include "qgslegendpatchshape.h"
#include "qgsstyle.h"
#include "qgsexpressioncontextutils.h"

#include <QSize>
#include <QPainter>
Expand Down Expand Up @@ -691,12 +692,18 @@ void QgsLineSymbolLayer::drawPreviewIcon( QgsSymbolRenderContext &context, QSize

void QgsLineSymbolLayer::renderPolygonStroke( const QPolygonF &points, const QVector<QPolygonF> *rings, QgsSymbolRenderContext &context )
{
QgsExpressionContextScope *scope = new QgsExpressionContextScope();
QgsExpressionContextScopePopper scopePopper( context.renderContext().expressionContext(), scope );

switch ( mRingFilter )
{
case AllRings:
case ExteriorRingOnly:
{
scope->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_RING_NUM, 0, true ) );
renderPolyline( points, context );
break;
}
case InteriorRingsOnly:
break;
}
Expand All @@ -708,8 +715,14 @@ void QgsLineSymbolLayer::renderPolygonStroke( const QPolygonF &points, const QVe
case AllRings:
case InteriorRingsOnly:
{
int ringIndex = 1;
for ( const QPolygonF &ring : std::as_const( *rings ) )
{
scope->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_RING_NUM, ringIndex, true ) );

renderPolyline( ring, context );
ringIndex++;
}
}
break;
case ExteriorRingOnly:
Expand Down
2 changes: 2 additions & 0 deletions src/gui/symbology/qgslayerpropertieswidget.cpp
Expand Up @@ -264,6 +264,7 @@ QgsExpressionContext QgsLayerPropertiesWidget::createExpressionContext() const
expContext << symbolScope;
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_PART_COUNT, 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_PART_NUM, 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_RING_NUM, 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_POINT_COUNT, 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_POINT_NUM, 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_layer_count" ), 1, true ) );
Expand All @@ -283,6 +284,7 @@ QgsExpressionContext QgsLayerPropertiesWidget::createExpressionContext() const

expContext.setHighlightedVariables( QStringList() << QgsExpressionContext::EXPR_ORIGINAL_VALUE << QgsExpressionContext::EXPR_SYMBOL_COLOR
<< QgsExpressionContext::EXPR_GEOMETRY_PART_COUNT << QgsExpressionContext::EXPR_GEOMETRY_PART_NUM
<< QgsExpressionContext::EXPR_GEOMETRY_RING_NUM
<< QgsExpressionContext::EXPR_GEOMETRY_POINT_COUNT << QgsExpressionContext::EXPR_GEOMETRY_POINT_NUM
<< QgsExpressionContext::EXPR_CLUSTER_COLOR << QgsExpressionContext::EXPR_CLUSTER_SIZE
<< QStringLiteral( "symbol_layer_count" ) << QStringLiteral( "symbol_layer_index" ) );
Expand Down
2 changes: 2 additions & 0 deletions src/gui/symbology/qgssymbollayerwidget.cpp
Expand Up @@ -79,6 +79,7 @@ QgsExpressionContext QgsSymbolLayerWidget::createExpressionContext() const
expContext << symbolScope;
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_PART_COUNT, 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_PART_NUM, 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_RING_NUM, 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_POINT_COUNT, 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_POINT_NUM, 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_layer_count" ), 1, true ) );
Expand All @@ -99,6 +100,7 @@ QgsExpressionContext QgsSymbolLayerWidget::createExpressionContext() const
QStringList highlights;
highlights << QgsExpressionContext::EXPR_ORIGINAL_VALUE << QgsExpressionContext::EXPR_SYMBOL_COLOR
<< QgsExpressionContext::EXPR_GEOMETRY_PART_COUNT << QgsExpressionContext::EXPR_GEOMETRY_PART_NUM
<< QgsExpressionContext::EXPR_GEOMETRY_RING_NUM
<< QgsExpressionContext::EXPR_GEOMETRY_POINT_COUNT << QgsExpressionContext::EXPR_GEOMETRY_POINT_NUM
<< QgsExpressionContext::EXPR_CLUSTER_COLOR << QgsExpressionContext::EXPR_CLUSTER_SIZE
<< QStringLiteral( "symbol_layer_count" ) << QStringLiteral( "symbol_layer_index" );
Expand Down
1 change: 1 addition & 0 deletions src/gui/symbology/qgssymbolslistwidget.cpp
Expand Up @@ -467,6 +467,7 @@ QgsExpressionContext QgsSymbolsListWidget::createExpressionContext() const

expContext.setHighlightedVariables( QStringList() << QgsExpressionContext::EXPR_ORIGINAL_VALUE << QgsExpressionContext::EXPR_SYMBOL_COLOR
<< QgsExpressionContext::EXPR_GEOMETRY_PART_COUNT << QgsExpressionContext::EXPR_GEOMETRY_PART_NUM
<< QgsExpressionContext::EXPR_GEOMETRY_RING_NUM
<< QgsExpressionContext::EXPR_GEOMETRY_POINT_COUNT << QgsExpressionContext::EXPR_GEOMETRY_POINT_NUM
<< QgsExpressionContext::EXPR_CLUSTER_COLOR << QgsExpressionContext::EXPR_CLUSTER_SIZE
<< QStringLiteral( "symbol_layer_count" ) << QStringLiteral( "symbol_layer_index" ) );
Expand Down
69 changes: 67 additions & 2 deletions tests/src/python/test_qgsarrowsymbollayer.py
Expand Up @@ -26,7 +26,7 @@
import os

from qgis.PyQt.QtCore import QSize, QDir
from qgis.PyQt.QtGui import QColor
from qgis.PyQt.QtGui import QColor, QPainter, QImage

from qgis.core import (
QgsVectorLayer,
Expand All @@ -40,7 +40,11 @@
QgsProperty,
QgsSymbolLayer,
QgsMapSettings,
QgsSymbol
QgsSymbol,
QgsGeometry,
QgsFeature,
QgsRenderContext,
QgsRenderChecker
)

from qgis.testing import start_app, unittest
Expand Down Expand Up @@ -167,6 +171,20 @@ def testColors(self):
self.assertEqual(sym_layer.subSymbol().color(), QColor(250, 150, 200))
self.assertEqual(sym_layer.color(), QColor(250, 150, 200))

def testRingNumberVariable(self):
# test test geometry_ring_num variable
s3 = QgsFillSymbol()
s3.deleteSymbolLayer(0)
s3.appendSymbolLayer(
QgsArrowSymbolLayer())
s3.symbolLayer(0).setIsCurved(False)
s3.symbolLayer(0).subSymbol()[0].setDataDefinedProperty(QgsSymbolLayer.PropertyFillColor,
QgsProperty.fromExpression('case when @geometry_ring_num=0 then \'green\' when @geometry_ring_num=1 then \'blue\' when @geometry_ring_num=2 then \'red\' end'))

g = QgsGeometry.fromWkt('Polygon((0 0, 10 0, 10 10, 0 10, 0 0),(1 1, 1 2, 2 2, 2 1, 1 1),(8 8, 9 8, 9 9, 8 9, 8 8))')
rendered_image = self.renderGeometry(s3, g)
assert self.imageCheck('arrow_ring_num', 'arrow_ring_num', rendered_image)

def testOpacityWithDataDefinedColor(self):
line_shp = os.path.join(TEST_DATA_DIR, 'lines.shp')
line_layer = QgsVectorLayer(line_shp, 'Lines', 'ogr')
Expand Down Expand Up @@ -239,6 +257,53 @@ def testDataDefinedOpacity(self):
self.report += renderchecker.report()
self.assertTrue(res)

def renderGeometry(self, symbol, geom):
f = QgsFeature()
f.setGeometry(geom)

image = QImage(200, 200, QImage.Format_RGB32)

painter = QPainter()
ms = QgsMapSettings()
extent = geom.get().boundingBox()
# buffer extent by 10%
if extent.width() > 0:
extent = extent.buffered((extent.height() + extent.width()) / 20.0)
else:
extent = extent.buffered(10)

ms.setExtent(extent)
ms.setOutputSize(image.size())
context = QgsRenderContext.fromMapSettings(ms)
context.setPainter(painter)
context.setScaleFactor(96 / 25.4) # 96 DPI

painter.begin(image)
try:
image.fill(QColor(0, 0, 0))
symbol.startRender(context)
symbol.renderFeature(f, context)
symbol.stopRender(context)
finally:
painter.end()

return image

def imageCheck(self, name, reference_image, image):
self.report += "<h2>Render {}</h2>\n".format(name)
temp_dir = QDir.tempPath() + '/'
file_name = temp_dir + 'symbol_' + name + ".png"
image.save(file_name, "PNG")
checker = QgsRenderChecker()
checker.setControlPathPrefix("symbol_arrow")
checker.setControlName("expected_" + reference_image)
checker.setRenderedImage(file_name)
checker.setColorTolerance(2)
result = checker.compareImages(name, 20)
self.report += checker.report()
print((self.report))
return result


if __name__ == '__main__':
unittest.main()
14 changes: 14 additions & 0 deletions tests/src/python/test_qgsmarkerlinesymbollayer.py
Expand Up @@ -151,6 +151,20 @@ def testRingFilter(self):
rendered_image = self.renderGeometry(s3, g)
assert self.imageCheck('markerline_interioronly', 'markerline_interioronly', rendered_image)

def testRingNumberVariable(self):
# test test geometry_ring_num variable
s3 = QgsFillSymbol()
s3.deleteSymbolLayer(0)
s3.appendSymbolLayer(
QgsMarkerLineSymbolLayer())
s3.symbolLayer(0).subSymbol()[0].setDataDefinedProperty(QgsSymbolLayer.PropertyFillColor,
QgsProperty.fromExpression('case when @geometry_ring_num=0 then \'green\' when @geometry_ring_num=1 then \'blue\' when @geometry_ring_num=2 then \'red\' end'))
s3.symbolLayer(0).setAverageAngleLength(0)

g = QgsGeometry.fromWkt('Polygon((0 0, 10 0, 10 10, 0 10, 0 0),(1 1, 1 2, 2 2, 2 1, 1 1),(8 8, 9 8, 9 9, 8 9, 8 8))')
rendered_image = self.renderGeometry(s3, g)
assert self.imageCheck('markerline_ring_num', 'markerline_ring_num', rendered_image)

def testPartNum(self):
# test geometry_part_num variable
s = QgsLineSymbol()
Expand Down
13 changes: 13 additions & 0 deletions tests/src/python/test_qgssimplelinesymbollayer.py
Expand Up @@ -271,6 +271,19 @@ def testDashCornerTweakDashRender(self):
rendered_image = self.renderGeometry(s, g)
assert self.imageCheck('simpleline_dashcornertweak', 'simpleline_dashcornertweak', rendered_image)

def testRingNumberVariable(self):
# test test geometry_ring_num variable
s3 = QgsFillSymbol()
s3.deleteSymbolLayer(0)
s3.appendSymbolLayer(
QgsSimpleLineSymbolLayer(color=QColor(255, 0, 0), width=2))
s3.symbolLayer(0).setDataDefinedProperty(QgsSymbolLayer.PropertyStrokeColor,
QgsProperty.fromExpression('case when @geometry_ring_num=0 then \'green\' when @geometry_ring_num=1 then \'blue\' when @geometry_ring_num=2 then \'red\' end'))

g = QgsGeometry.fromWkt('Polygon((0 0, 10 0, 10 10, 0 10, 0 0),(1 1, 1 2, 2 2, 2 1, 1 1),(8 8, 9 8, 9 9, 8 9, 8 8))')
rendered_image = self.renderGeometry(s3, g)
assert self.imageCheck('simpleline_ring_num', 'simpleline_ring_num', rendered_image)

def testRingFilter(self):
# test filtering rings during rendering

Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 8f510cf

Please sign in to comment.