Skip to content
Permalink
Browse files

[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 8f510cf2d43664090d95da590487c802096d2f0d
@@ -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;
@@ -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." ) );

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

@@ -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;
@@ -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;
@@ -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;
}
@@ -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;
@@ -30,6 +30,7 @@
#include "qgsmultipoint.h"
#include "qgslegendpatchshape.h"
#include "qgsstyle.h"
#include "qgsexpressioncontextutils.h"

#include <QSize>
#include <QPainter>
@@ -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;
}
@@ -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:
@@ -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 ) );
@@ -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" ) );
@@ -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 ) );
@@ -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" );
@@ -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" ) );
@@ -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,
@@ -40,7 +40,11 @@
QgsProperty,
QgsSymbolLayer,
QgsMapSettings,
QgsSymbol
QgsSymbol,
QgsGeometry,
QgsFeature,
QgsRenderContext,
QgsRenderChecker
)

from qgis.testing import start_app, unittest
@@ -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')
@@ -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()
@@ -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()
@@ -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

Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit 8f510cf

Please sign in to comment.