Skip to content
Permalink
Browse files

Ensure feature clipping occurs for render only, and doesn't affect

feature geometry when used in rule based renderers and other
geometry dependent rendering options
  • Loading branch information
nyalldawson committed Jul 2, 2020
1 parent 9ec6c21 commit 5cbdc4c896a8b73e7fd88bf88267568a3e42edf7
@@ -795,6 +795,39 @@ Returns the list of clipping regions to apply during the render.

These regions are always in the final destination CRS for the map.

.. versionadded:: 3.16
%End

QgsGeometry featureClipGeometry() const;
%Docstring
Returns the geometry to use to clip features at render time.

When vector features are rendered, they should be clipped to this geometry.

.. warning::

The clipping must take effect for rendering the feature's symbol only,
and should never be applied directly to the feature being rendered. Doing so would
impact the results of rendering rules which rely on feature geometry, such as
a rule-based renderer using the feature's area.

.. seealso:: :py:func:`setFeatureClipGeometry`


.. versionadded:: 3.16
%End

void setFeatureClipGeometry( const QgsGeometry &geometry );
%Docstring
Sets a ``geometry`` to use to clip features at render time.

.. note::

This is not usually set directly, but rather specified by calling QgsMapSettings::py:func:`~QgsRenderContext.addClippingRegion`
prior to constructing a QgsRenderContext.

.. seealso:: :py:func:`featureClipGeometry`

.. versionadded:: 3.16
%End

@@ -68,6 +68,7 @@ QgsRenderContext::QgsRenderContext( const QgsRenderContext &rh )
, mCustomRenderingFlags( rh.mCustomRenderingFlags )
, mDisabledSymbolLayers()
, mClippingRegions( rh.mClippingRegions )
, mFeatureClipGeometry( rh.mFeatureClipGeometry )
#ifdef QGISDEBUG
, mHasTransformContext( rh.mHasTransformContext )
#endif
@@ -102,6 +103,7 @@ QgsRenderContext &QgsRenderContext::operator=( const QgsRenderContext &rh )
mHasRenderedFeatureHandlers = rh.mHasRenderedFeatureHandlers;
mCustomRenderingFlags = rh.mCustomRenderingFlags;
mClippingRegions = rh.mClippingRegions;
mFeatureClipGeometry = rh.mFeatureClipGeometry;
setIsTemporal( rh.isTemporal() );
if ( isTemporal() )
setTemporalRange( rh.temporalRange() );
@@ -502,4 +504,14 @@ QList<QgsMapClippingRegion> QgsRenderContext::clippingRegions() const
return mClippingRegions;
}

QgsGeometry QgsRenderContext::featureClipGeometry() const
{
return mFeatureClipGeometry;
}

void QgsRenderContext::setFeatureClipGeometry( const QgsGeometry &geometry )
{
mFeatureClipGeometry = geometry;
}


@@ -796,6 +796,33 @@ class CORE_EXPORT QgsRenderContext : public QgsTemporalRangeObject
*/
QList< QgsMapClippingRegion > clippingRegions() const;

/**
* Returns the geometry to use to clip features at render time.
*
* When vector features are rendered, they should be clipped to this geometry.
*
* \warning The clipping must take effect for rendering the feature's symbol only,
* and should never be applied directly to the feature being rendered. Doing so would
* impact the results of rendering rules which rely on feature geometry, such as
* a rule-based renderer using the feature's area.
*
* \see setFeatureClipGeometry()
*
* \since QGIS 3.16
*/
QgsGeometry featureClipGeometry() const;

/**
* Sets a \a geometry to use to clip features at render time.
*
* \note This is not usually set directly, but rather specified by calling QgsMapSettings:addClippingRegion()
* prior to constructing a QgsRenderContext.
*
* \see featureClipGeometry()
* \since QGIS 3.16
*/
void setFeatureClipGeometry( const QgsGeometry &geometry );

private:

Flags mFlags;
@@ -888,6 +915,7 @@ class CORE_EXPORT QgsRenderContext : public QgsTemporalRangeObject
QSet<const QgsSymbolLayer *> mDisabledSymbolLayers;

QList< QgsMapClippingRegion > mClippingRegions;
QgsGeometry mFeatureClipGeometry;

#ifdef QGISDEBUG
bool mHasTransformContext = false;
@@ -295,6 +295,11 @@ bool QgsVectorLayerRenderer::render()
context.setVectorSimplifyMethod( vectorMethod );
}

if ( mApplyClipGeometries )
{
context.setFeatureClipGeometry( mClipFeatureGeom );
}

QgsFeatureIterator fit = mSource->getFeatures( featureRequest );
// Attach an interruption checker so that iterators that have potentially
// slow fetchFeature() implementations, such as in the WFS provider, can
@@ -352,12 +357,6 @@ void QgsVectorLayerRenderer::drawRenderer( QgsFeatureIterator &fit )
if ( clipEngine && !clipEngine->intersects( fet.geometry().constGet() ) )
continue; // skip features outside of clipping region

if ( mApplyClipGeometries )
{
QgsGeometry original = fet.geometry();
fet.setGeometry( original.intersection( mClipFeatureGeom ) );
}

context.expressionContext().setFeature( fet );

bool sel = context.showSelection() && mSelectedFeatureIds.contains( fet.id() );
@@ -452,12 +451,6 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureIterator &fit )
if ( clipEngine && !clipEngine->intersects( fet.geometry().constGet() ) )
continue; // skip features outside of clipping region

if ( mApplyClipGeometries )
{
QgsGeometry original = fet.geometry();
fet.setGeometry( original.intersection( mClipFeatureGeom ) );
}

context.expressionContext().setFeature( fet );
QgsSymbol *sym = mRenderer->symbolForFeature( fet, context );
if ( !sym )
@@ -896,6 +896,14 @@ void QgsSymbol::renderFeature( const QgsFeature &feature, QgsRenderContext &cont
static_cast< QgsMapToPixelSimplifier::SimplifyAlgorithm >( context.vectorSimplifyMethod().simplifyAlgorithm() ) );
segmentizedGeometry = simplifier.simplify( segmentizedGeometry );
}
if ( !context.featureClipGeometry().isEmpty() )
{
// apply feature clipping from context to the rendered geometry only -- just like the render time simplification,
// we should NEVER apply this to the geometry attached to the feature itself. Doing so causes issues with certain
// renderer settings, e.g. if polygons are being rendered using a rule based renderer based on the feature's area,
// then we need to ensure that the original feature area is used instead of the clipped area..
segmentizedGeometry = segmentizedGeometry.intersection( context.featureClipGeometry() );
}

QgsGeometry renderedBoundsGeom;

@@ -520,6 +520,14 @@ def testClippingRegion(self):
self.assertEqual(rc.clippingRegions()[0].geometry().asWkt(), 'Polygon ((0 0, 1 0, 1 1, 0 1, 0 0))')
self.assertEqual(rc.clippingRegions()[1].geometry().asWkt(), 'Polygon ((10 0, 11 0, 11 1, 10 1, 10 0))')

def testFeatureClipGeometry(self):
rc = QgsRenderContext()
self.assertTrue(rc.featureClipGeometry().isNull())
rc.setFeatureClipGeometry(QgsGeometry.fromWkt('Polygon(( 0 0, 1 0 , 1 1 , 0 1, 0 0 ))'))
self.assertEqual(rc.featureClipGeometry().asWkt(), 'Polygon ((0 0, 1 0, 1 1, 0 1, 0 0))')
rc2 = QgsRenderContext(rc)
self.assertEqual(rc2.featureClipGeometry().asWkt(), 'Polygon ((0 0, 1 0, 1 1, 0 1, 0 0))')


if __name__ == '__main__':
unittest.main()
@@ -24,7 +24,8 @@
QgsSingleSymbolRenderer,
QgsMapSettings,
QgsFillSymbol,
QgsCoordinateReferenceSystem
QgsCoordinateReferenceSystem,
QgsRuleBasedRenderer
)
from qgis.testing import start_app, unittest
from utilities import (unitTestDataPath)
@@ -129,6 +130,63 @@ def testRenderWithIntersectionRegions(self):
self.report += renderchecker.report()
self.assertTrue(result)

def testIntersectionRuleBased(self):
"""
Test that rule based renderer using intersection clip paths correctly uses original feature area for rule
evaluation, not clipped area
"""
poly_layer = QgsVectorLayer(os.path.join(TEST_DATA_DIR, 'polys.shp'))
self.assertTrue(poly_layer.isValid())

sym1 = QgsFillSymbol.createSimple({'color': '#ff00ff', 'outline_color': '#000000', 'outline_width': '1'})
sym2 = QgsFillSymbol.createSimple({'color': '#00ffff', 'outline_color': '#000000', 'outline_width': '1'})

r1 = QgsRuleBasedRenderer.Rule(sym1, 0, 0, 'area($geometry)>25')
r2 = QgsRuleBasedRenderer.Rule(sym2, 0, 0, 'ELSE')

rootrule = QgsRuleBasedRenderer.Rule(None)
rootrule.appendChild(r1)
rootrule.appendChild(r2)
renderer = QgsRuleBasedRenderer(rootrule)
poly_layer.setRenderer(renderer)

mapsettings = QgsMapSettings()
mapsettings.setOutputSize(QSize(400, 400))
mapsettings.setOutputDpi(96)
mapsettings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3857'))
mapsettings.setExtent(QgsRectangle(-13875783.2, 2266009.4, -8690110.7, 6673344.5))
mapsettings.setLayers([poly_layer])
mapsettings.setEllipsoid('')

region = QgsMapClippingRegion(QgsGeometry.fromWkt(
'Polygon ((-11725957 5368254, -12222900 4807501, -12246014 3834025, -12014878 3496059, -11259833 3518307, -10751333 3621153, -10574129 4516741, -10847640 5194995, -11105742 5325957, -11725957 5368254))'))
region.setFeatureClip(QgsMapClippingRegion.FeatureClippingType.Intersect)
region2 = QgsMapClippingRegion(QgsGeometry.fromWkt(
'Polygon ((-11032549 5421399, -11533344 4693167, -11086481 4229112, -11167378 3742984, -10616504 3553984, -10161936 3925771, -9618766 4668482, -9472380 5620753, -10115709 5965063, -11032549 5421399))'))
region2.setFeatureClip(QgsMapClippingRegion.FeatureClippingType.Intersect)
mapsettings.addClippingRegion(region)
mapsettings.addClippingRegion(region2)

renderchecker = QgsMultiRenderChecker()
renderchecker.setMapSettings(mapsettings)
renderchecker.setControlPathPrefix('vectorlayerrenderer')
renderchecker.setControlName('expected_intersection_rule_based')
result = renderchecker.runTest('expected_intersection_rule_based')
self.report += renderchecker.report()
self.assertTrue(result)

# also try with symbol levels
renderer.setUsingSymbolLevels(True)
poly_layer.setRenderer(renderer)

renderchecker = QgsMultiRenderChecker()
renderchecker.setMapSettings(mapsettings)
renderchecker.setControlPathPrefix('vectorlayerrenderer')
renderchecker.setControlName('expected_intersection_rule_based')
result = renderchecker.runTest('expected_intersection_rule_based')
self.report += renderchecker.report()
self.assertTrue(result)

def testRenderWithPainterClipRegions(self):
poly_layer = QgsVectorLayer(os.path.join(TEST_DATA_DIR, 'polys.shp'))
self.assertTrue(poly_layer.isValid())
Binary file not shown.

0 comments on commit 5cbdc4c

Please sign in to comment.
You can’t perform that action at this time.