Skip to content

Commit

Permalink
Respect map clipping regions during raster layer rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Jul 2, 2020
1 parent 0e67b65 commit 9a97892
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 26 deletions.
4 changes: 4 additions & 0 deletions python/core/auto_generated/qgsmapclippingregion.sip.in
Expand Up @@ -52,13 +52,17 @@ Sets the clipping region ``geometry`` (in the destination map CRS).
%Docstring
Returns the feature clipping type.

This setting is only used while rendering vector layers, for other layer types it is ignored.

.. seealso:: :py:func:`setFeatureClip`
%End

void setFeatureClip( FeatureClippingType type );
%Docstring
Sets the feature clipping ``type``.

This setting is only used while rendering vector layers, for other layer types it is ignored.

.. seealso:: :py:func:`featureClip`
%End

Expand Down
5 changes: 3 additions & 2 deletions python/core/auto_generated/qgsmapclippingutils.sip.in
Expand Up @@ -9,6 +9,7 @@




class QgsMapClippingUtils
{
%Docstring
Expand Down Expand Up @@ -56,10 +57,10 @@ The returned geometry will be automatically reprojected into the same CRS as the
:return: combined clipping region for use when rendering features
%End

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

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

Expand Down
4 changes: 4 additions & 0 deletions src/core/qgsmapclippingregion.h
Expand Up @@ -66,6 +66,8 @@ class CORE_EXPORT QgsMapClippingRegion
/**
* Returns the feature clipping type.
*
* This setting is only used while rendering vector layers, for other layer types it is ignored.
*
* \see setFeatureClip()
*/
FeatureClippingType featureClip() const
Expand All @@ -76,6 +78,8 @@ class CORE_EXPORT QgsMapClippingRegion
/**
* Sets the feature clipping \a type.
*
* This setting is only used while rendering vector layers, for other layer types it is ignored.
*
* \see featureClip()
*/
void setFeatureClip( FeatureClippingType type )
Expand Down
18 changes: 15 additions & 3 deletions src/core/qgsmapclippingutils.cpp
Expand Up @@ -123,7 +123,7 @@ QgsGeometry QgsMapClippingUtils::calculateFeatureIntersectionGeometry( const QLi
return result;
}

QPainterPath QgsMapClippingUtils::calculatePainterClipRegion( const QList<QgsMapClippingRegion> &regions, const QgsRenderContext &context, bool &shouldClip )
QPainterPath QgsMapClippingUtils::calculatePainterClipRegion( const QList<QgsMapClippingRegion> &regions, const QgsRenderContext &context, QgsMapLayerType layerType, bool &shouldClip )
{
QgsGeometry result;
bool first = true;
Expand All @@ -133,8 +133,20 @@ QPainterPath QgsMapClippingUtils::calculatePainterClipRegion( const QList<QgsMap
if ( region.geometry().type() != QgsWkbTypes::PolygonGeometry )
continue;

if ( region.featureClip() != QgsMapClippingRegion::FeatureClippingType::PainterClip )
continue;
switch ( layerType )
{
case QgsMapLayerType::VectorLayer:
if ( region.featureClip() != QgsMapClippingRegion::FeatureClippingType::PainterClip )
continue;
break;

case QgsMapLayerType::MeshLayer:
case QgsMapLayerType::RasterLayer:
case QgsMapLayerType::PluginLayer:
// for these layer types, we ignore the region's featureClip behavior.
break;

}

shouldClip = true;
if ( first )
Expand Down
6 changes: 4 additions & 2 deletions src/core/qgsmapclippingutils.h
Expand Up @@ -18,6 +18,8 @@

#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgsmaplayer.h"

#include <QList>
#include <QPainterPath>

Expand Down Expand Up @@ -73,7 +75,7 @@ class CORE_EXPORT QgsMapClippingUtils

/**
* Returns a QPainterPath representing the intersection of clipping \a regions from \a context which should be used to clip the painter
* during rendering.
* during rendering of a layer of the specified \a layerType.
*
* The returned coordinates are in painter coordinates for the destination \a context.
*
Expand All @@ -83,7 +85,7 @@ class CORE_EXPORT QgsMapClippingUtils
*
* \returns combined painter clipping region for use when rendering maps
*/
static QPainterPath calculatePainterClipRegion( const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, bool &shouldClip );
static QPainterPath calculatePainterClipRegion( const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, QgsMapLayerType layerType, bool &shouldClip );
};

#endif // QGSMAPCLIPPINGUTILS_H
2 changes: 1 addition & 1 deletion src/core/qgsvectorlayerrenderer.cpp
Expand Up @@ -191,7 +191,7 @@ bool QgsVectorLayerRenderer::render()
mClipFeatureGeom = QgsMapClippingUtils::calculateFeatureIntersectionGeometry( mClippingRegions, context, mApplyClipGeometries );

bool needsPainterClipPath = false;
const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, context, needsPainterClipPath );
const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, context, QgsMapLayerType::VectorLayer, needsPainterClipPath );
if ( needsPainterClipPath )
context.painter()->setClipPath( path, Qt::IntersectClip );
}
Expand Down
14 changes: 14 additions & 0 deletions src/core/raster/qgsrasterlayerrenderer.cpp
Expand Up @@ -25,6 +25,7 @@
#include "qgsproject.h"
#include "qgsexception.h"
#include "qgsrasterlayertemporalproperties.h"
#include "qgsmapclippingutils.h"

///@cond PRIVATE

Expand Down Expand Up @@ -253,6 +254,8 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender
mPipe->provider()->temporalCapabilities()->setRequestedTemporalRange( QgsDateTimeRange() );
mPipe->provider()->temporalCapabilities()->setIntervalHandlingMethod( temporalProperties->intervalHandlingMethod() );
}

mClippingRegions = QgsMapClippingUtils::collectClippingRegionsForLayer( *renderContext(), layer );
}

QgsRasterLayerRenderer::~QgsRasterLayerRenderer()
Expand Down Expand Up @@ -280,6 +283,15 @@ bool QgsRasterLayerRenderer::render()
// procedure to use :
//

renderContext()->painter()->save();
if ( !mClippingRegions.empty() )
{
bool needsPainterClipPath = false;
const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, *renderContext(), QgsMapLayerType::RasterLayer, needsPainterClipPath );
if ( needsPainterClipPath )
renderContext()->painter()->setClipPath( path, Qt::IntersectClip );
}

QgsRasterProjector *projector = mPipe->projector();
bool restoreOldResamplingStage = false;
QgsRasterPipe::ResamplingStage oldResamplingState = mPipe->resamplingStage();
Expand Down Expand Up @@ -309,6 +321,8 @@ bool QgsRasterLayerRenderer::render()
mPipe->setResamplingStage( oldResamplingState );
}

renderContext()->painter()->restore();

const QStringList errors = mFeedback->errors();
for ( const QString &error : errors )
{
Expand Down
3 changes: 3 additions & 0 deletions src/core/raster/qgsrasterlayerrenderer.h
Expand Up @@ -20,6 +20,7 @@

#include "qgsmaplayerrenderer.h"
#include "qgsrasterdataprovider.h"
#include "qgsmapclippingregion.h"

class QPainter;

Expand Down Expand Up @@ -86,6 +87,8 @@ class CORE_EXPORT QgsRasterLayerRenderer : public QgsMapLayerRenderer
//! feedback class for cancellation and preview generation
QgsRasterLayerRendererFeedback *mFeedback = nullptr;

QList< QgsMapClippingRegion > mClippingRegions;

friend class QgsRasterLayerRendererFeedback;
};

Expand Down
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -214,6 +214,7 @@ ADD_PYTHON_TEST(PyQgsRasterBandComboBox test_qgsrasterbandcombobox.py)
ADD_PYTHON_TEST(PyQgsRasterFileWriter test_qgsrasterfilewriter.py)
ADD_PYTHON_TEST(PyQgsRasterFileWriterTask test_qgsrasterfilewritertask.py)
ADD_PYTHON_TEST(PyQgsRasterLayer test_qgsrasterlayer.py)
ADD_PYTHON_TEST(PyQgsRasterLayerRenderer test_qgsrasterlayerrenderer.py)
ADD_PYTHON_TEST(PyQgsRasterColorRampShader test_qgsrastercolorrampshader.py)
ADD_PYTHON_TEST(PyQgsRasterRange test_qgsrasterrange.py)
ADD_PYTHON_TEST(PyQgsRasterResampler test_qgsrasterresampler.py)
Expand Down
42 changes: 24 additions & 18 deletions tests/src/python/test_qgsmapclippingutils.py
Expand Up @@ -23,7 +23,8 @@
QgsCoordinateTransform,
QgsCoordinateReferenceSystem,
QgsProject,
QgsMapToPixel
QgsMapToPixel,
QgsMapLayerType
)


Expand Down Expand Up @@ -123,31 +124,36 @@ def testPainterClipPath(self):

rc = QgsRenderContext()

path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([], rc)
self.assertFalse(should_clip)
self.assertEqual(path.elementCount(), 0)
for t in [QgsMapLayerType.VectorLayer, QgsMapLayerType.RasterLayer, QgsMapLayerType.MeshLayer]:
path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([], rc, t)
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))')
for t in [QgsMapLayerType.VectorLayer, QgsMapLayerType.RasterLayer, QgsMapLayerType.MeshLayer]:
path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([region], rc, t)
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)
# region2 is a Intersects type clipping region, should not apply for vector layers
path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([region2], rc, QgsMapLayerType.VectorLayer)
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))')
for t in [QgsMapLayerType.RasterLayer, QgsMapLayerType.MeshLayer]:
path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([region2], rc, t)
self.assertTrue(should_clip)
self.assertEqual(QgsGeometry.fromQPolygonF(path.toFillPolygon()).asWkt(1), 'Polygon ((0 1, 0.1 1, 0.1 -1, 0 -1, 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))')
for t in [QgsMapLayerType.VectorLayer, QgsMapLayerType.RasterLayer, QgsMapLayerType.MeshLayer]:
path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([region, region2, region3], rc, t)
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))')
for t in [QgsMapLayerType.VectorLayer, QgsMapLayerType.RasterLayer, QgsMapLayerType.MeshLayer]:
path, should_clip = QgsMapClippingUtils.calculatePainterClipRegion([region, region3], rc, t)
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__':
Expand Down
77 changes: 77 additions & 0 deletions tests/src/python/test_qgsrasterlayerrenderer.py
@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsRasterLayerRenderer
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
__author__ = 'Nyall Dawson'
__date__ = '2020-06'
__copyright__ = 'Copyright 2020, The QGIS Project'

import qgis # NOQA

import os

from qgis.PyQt.QtCore import QSize, QDir

from qgis.core import (QgsRasterLayer,
QgsMapClippingRegion,
QgsRectangle,
QgsMultiRenderChecker,
QgsGeometry,
QgsSingleSymbolRenderer,
QgsMapSettings,
QgsFillSymbol,
QgsCoordinateReferenceSystem
)
from qgis.testing import start_app, unittest
from utilities import (unitTestDataPath)

# Convenience instances in case you may need them
# not used in this test
start_app()
TEST_DATA_DIR = unitTestDataPath()


class TestQgsRasterLayerRenderer(unittest.TestCase):

def setUp(self):
self.report = "<h1>Python QgsRasterLayerRenderer Tests</h1>\n"

def tearDown(self):
report_file_path = "%s/qgistest.html" % QDir.tempPath()
with open(report_file_path, 'a') as report_file:
report_file.write(self.report)

def testRenderWithPainterClipRegions(self):
raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, 'rgb256x256.png'))
self.assertTrue(raster_layer.isValid())
raster_layer.setCrs(QgsCoordinateReferenceSystem('EPSG:3857'))

mapsettings = QgsMapSettings()
mapsettings.setOutputSize(QSize(400, 400))
mapsettings.setOutputDpi(96)
mapsettings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
mapsettings.setExtent(QgsRectangle(0.0001451, -0.0001291, 0.0021493, -0.0021306))
mapsettings.setLayers([raster_layer])

region = QgsMapClippingRegion(QgsGeometry.fromWkt('Polygon ((0.00131242078273144 -0.00059281669806561, 0.00066744230712249 -0.00110186995774045, 0.00065145110524788 -0.00152830200772984, 0.00141369839460392 -0.00189076925022083, 0.00210931567614912 -0.00094195793899443, 0.00169354442740946 -0.00067810310806349, 0.00131242078273144 -0.00059281669806561))'))
region.setFeatureClip(QgsMapClippingRegion.FeatureClippingType.PainterClip)
region2 = QgsMapClippingRegion(QgsGeometry.fromWkt('Polygon ((0.00067010750743492 -0.0007740503193111, 0.00064612070462302 -0.00151764120648011, 0.00153629760897587 -0.00158693641460339, 0.0014909892036645 -0.00063812510337699, 0.00106722235398754 -0.00055816909400397, 0.00067010750743492 -0.0007740503193111))'))
region2.setFeatureClip(QgsMapClippingRegion.FeatureClippingType.Intersect)
mapsettings.addClippingRegion(region)
mapsettings.addClippingRegion(region2)

renderchecker = QgsMultiRenderChecker()
renderchecker.setMapSettings(mapsettings)
renderchecker.setControlPathPrefix('rasterlayerrenderer')
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 9a97892

Please sign in to comment.