Skip to content

Commit

Permalink
Apply painter clip regions when rendering vector layers
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Jul 2, 2020
1 parent 30a3582 commit 0e67b65
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 1 deletion.
14 changes: 14 additions & 0 deletions python/core/auto_generated/qgsmapclippingutils.sip.in
Expand Up @@ -54,6 +54,20 @@ The returned geometry will be automatically reprojected into the same CRS as the
:param shouldClip: will be set to ``True`` if layer's features should be filtered, i.e. one or more clipping regions applies to the layer

:return: combined clipping region for use when rendering features
%End

static QPainterPath calculatePainterClipRegion( const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, bool &shouldClip );
%Docstring
Returns a QPainterPath representing the intersection of clipping ``regions`` from ``context`` which should be used to clip the painter
during rendering.

The returned coordinates are in painter coordinates for the destination ``context``.

:param regions: list of clip regions which apply to the layer
:param context: a render context
:param shouldClip: will be set to ``True`` if the clipping path should be applied

:return: combined painter clipping region for use when rendering maps
%End
};

Expand Down
39 changes: 39 additions & 0 deletions src/core/qgsmapclippingutils.cpp
Expand Up @@ -122,3 +122,42 @@ QgsGeometry QgsMapClippingUtils::calculateFeatureIntersectionGeometry( const QLi

return result;
}

QPainterPath QgsMapClippingUtils::calculatePainterClipRegion( const QList<QgsMapClippingRegion> &regions, const QgsRenderContext &context, bool &shouldClip )
{
QgsGeometry result;
bool first = true;
shouldClip = false;
for ( const QgsMapClippingRegion &region : regions )
{
if ( region.geometry().type() != QgsWkbTypes::PolygonGeometry )
continue;

if ( region.featureClip() != QgsMapClippingRegion::FeatureClippingType::PainterClip )
continue;

shouldClip = true;
if ( first )
{
result = region.geometry();
first = false;
}
else
{
result = result.intersection( region.geometry() );
}
}

if ( !shouldClip )
return QPainterPath();

// filter out polygon parts from result only
result.convertGeometryCollectionToSubclass( QgsWkbTypes::PolygonGeometry );

// transform to painter coordinates
result.mapToPixel( context.mapToPixel() );

QPainterPath path;
path.addPolygon( result.asQPolygonF() );
return path;
}
15 changes: 15 additions & 0 deletions src/core/qgsmapclippingutils.h
Expand Up @@ -19,6 +19,7 @@
#include "qgis_core.h"
#include "qgis_sip.h"
#include <QList>
#include <QPainterPath>

class QgsRenderContext;
class QgsMapLayer;
Expand Down Expand Up @@ -69,6 +70,20 @@ class CORE_EXPORT QgsMapClippingUtils
* \returns combined clipping region for use when rendering features
*/
static QgsGeometry calculateFeatureIntersectionGeometry( const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, bool &shouldClip );

/**
* Returns a QPainterPath representing the intersection of clipping \a regions from \a context which should be used to clip the painter
* during rendering.
*
* The returned coordinates are in painter coordinates for the destination \a context.
*
* \param regions list of clip regions which apply to the layer
* \param context a render context
* \param shouldClip will be set to TRUE if the clipping path should be applied
*
* \returns combined painter clipping region for use when rendering maps
*/
static QPainterPath calculatePainterClipRegion( const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, bool &shouldClip );
};

#endif // QGSMAPCLIPPINGUTILS_H
5 changes: 5 additions & 0 deletions src/core/qgsvectorlayerrenderer.cpp
Expand Up @@ -189,6 +189,11 @@ bool QgsVectorLayerRenderer::render()
requestExtent = requestExtent.intersect( mClipFilterGeom.boundingBox() );

mClipFeatureGeom = QgsMapClippingUtils::calculateFeatureIntersectionGeometry( mClippingRegions, context, mApplyClipGeometries );

bool needsPainterClipPath = false;
const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, context, needsPainterClipPath );
if ( needsPainterClipPath )
context.painter()->setClipPath( path, Qt::IntersectClip );
}
mRenderer->modifyRequestExtent( requestExtent, context );

Expand Down
39 changes: 38 additions & 1 deletion tests/src/python/test_qgsmapclippingutils.py
Expand Up @@ -22,7 +22,8 @@
QgsVectorLayer,
QgsCoordinateTransform,
QgsCoordinateReferenceSystem,
QgsProject
QgsProject,
QgsMapToPixel
)


Expand Down Expand Up @@ -112,6 +113,42 @@ def testCalculateFeatureIntersectionGeometry(self):
self.assertTrue(should_clip)
self.assertEqual(geom.asWkt(0), 'Polygon ((11132 0, 0 0, 0 111325, 11132 111325, 11132 0))')

def testPainterClipPath(self):
region = QgsMapClippingRegion(QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))'))
region.setFeatureClip(QgsMapClippingRegion.FeatureClippingType.PainterClip)
region2 = QgsMapClippingRegion(QgsGeometry.fromWkt('Polygon((0 0, 0.1 0, 0.1 2, 0 2, 0 0))'))
region2.setFeatureClip(QgsMapClippingRegion.FeatureClippingType.Intersects)
region3 = QgsMapClippingRegion(QgsGeometry.fromWkt('Polygon((0 0, 0.1 0, 0.1 2, 0 2, 0 0))'))
region3.setFeatureClip(QgsMapClippingRegion.FeatureClippingType.PainterClip)

rc = QgsRenderContext()

path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([], rc)
self.assertFalse(should_clip)
self.assertEqual(path.elementCount(), 0)

path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([region], rc)
self.assertTrue(should_clip)
self.assertEqual(QgsGeometry.fromQPolygonF(path.toFillPolygon()).asWkt(1), 'Polygon ((0 1, 1 1, 1 0, 0 0, 0 1))')

# region2 is a Intersects type clipping region, should not apply here
path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([region2], rc)
self.assertFalse(should_clip)
self.assertEqual(path.elementCount(), 0)

path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([region, region2], rc)
self.assertTrue(should_clip)
self.assertEqual(QgsGeometry.fromQPolygonF(path.toFillPolygon()).asWkt(1), 'Polygon ((0 1, 1 1, 1 0, 0 0, 0 1))')

path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([region, region2, region3], rc)
self.assertTrue(should_clip)
self.assertEqual(QgsGeometry.fromQPolygonF(path.toFillPolygon()).asWkt(1), 'Polygon ((0.1 1, 0 1, 0 0, 0.1 0, 0.1 1))')

rc.setMapToPixel(QgsMapToPixel(5, 10, 11, 200, 150, 0))
path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([region, region3], rc)
self.assertTrue(should_clip)
self.assertEqual(QgsGeometry.fromQPolygonF(path.toFillPolygon()).asWkt(0), 'Polygon ((98 77, 98 77, 98 77, 98 77, 98 77))')


if __name__ == '__main__':
unittest.main()
42 changes: 42 additions & 0 deletions tests/src/python/test_qgsvectorlayerrenderer.py
Expand Up @@ -129,6 +129,48 @@ def testRenderWithIntersectionRegions(self):
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())

sym1 = QgsFillSymbol.createSimple({'color': '#ff00ff', 'outline_color': '#000000', 'outline_width': '1'})
renderer = QgsSingleSymbolRenderer(sym1)
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])

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.PainterClip)
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.PainterClip)
mapsettings.addClippingRegion(region)
mapsettings.addClippingRegion(region2)

renderchecker = QgsMultiRenderChecker()
renderchecker.setMapSettings(mapsettings)
renderchecker.setControlPathPrefix('vectorlayerrenderer')
renderchecker.setControlName('expected_painterclip_region')
result = renderchecker.runTest('expected_painterclip_region')
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_painterclip_region')
result = renderchecker.runTest('expected_painterclip_region')
self.report += renderchecker.report()
self.assertTrue(result)


if __name__ == '__main__':
unittest.main()
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 0e67b65

Please sign in to comment.