diff --git a/python/core/auto_generated/qgsmapclippingutils.sip.in b/python/core/auto_generated/qgsmapclippingutils.sip.in new file mode 100644 index 00000000000..79d7bfc9140 --- /dev/null +++ b/python/core/auto_generated/qgsmapclippingutils.sip.in @@ -0,0 +1,53 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsmapclippingutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsMapClippingUtils +{ +%Docstring + +Utility functions for use when clipping map renders. + +.. versionadded:: 3.16 +%End + +%TypeHeaderCode +#include "qgsmapclippingutils.h" +%End + public: + + static QList< QgsMapClippingRegion > collectClippingRegionsForLayer( const QgsRenderContext &context, const QgsMapLayer *layer ); +%Docstring +Collects the list of map clipping regions from a ``context`` which apply to a map ``layer``. +%End + + static QgsGeometry calculateFeatureRequestGeometry( const QList< QgsMapClippingRegion > ®ions, const QgsRenderContext &context, bool &shouldFilter ); +%Docstring +Returns the geometry representing the intersection of clipping ``regions`` from ``context``. + +The returned geometry will be automatically reprojected into the same CRS as the source layer, ready for use for filtering +a feature request. + +:param regions: list of clip regions which apply to the layer +:param context: a render context +:param shouldFilter: 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 filtering features to render +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsmapclippingutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 92d2c80dcae..a881f195296 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -102,6 +102,7 @@ %Include auto_generated/qgslocalizeddatapathregistry.sip %Include auto_generated/qgslogger.sip %Include auto_generated/qgsmapclippingregion.sip +%Include auto_generated/qgsmapclippingutils.sip %Include auto_generated/qgsmapdecoration.sip %Include auto_generated/qgsmaphittest.sip %Include auto_generated/qgsmaplayer.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 766f2f24bfd..87c4518978b 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -312,6 +312,7 @@ SET(QGIS_CORE_SRCS qgslocalizeddatapathregistry.cpp qgslogger.cpp qgsmapclippingregion.cpp + qgsmapclippingutils.cpp qgsmapdecoration.cpp qgsmaphittest.cpp qgsmaplayer.cpp @@ -861,6 +862,7 @@ SET(QGIS_CORE_HDRS qgslocalizeddatapathregistry.h qgslogger.h qgsmapclippingregion.h + qgsmapclippingutils.h qgsmapdecoration.h qgsmaphittest.h qgsmaplayer.h diff --git a/src/core/qgsmapclippingutils.cpp b/src/core/qgsmapclippingutils.cpp new file mode 100644 index 00000000000..9b4cd39962d --- /dev/null +++ b/src/core/qgsmapclippingutils.cpp @@ -0,0 +1,75 @@ +/*************************************************************************** + qgsmapclippingutils.cpp + -------------------------------------- + Date : June 2020 + Copyright : (C) 2020 by Nyall Dawson + Email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include "qgsmapclippingutils.h" +#include "qgsgeometry.h" +#include "qgsrendercontext.h" +#include "qgsmapclippingregion.h" +#include "qgslogger.h" +#include + +QList QgsMapClippingUtils::collectClippingRegionsForLayer( const QgsRenderContext &context, const QgsMapLayer *layer ) +{ + QList< QgsMapClippingRegion > res; + const QList< QgsMapClippingRegion > regions = context.clippingRegions(); + res.reserve( regions.size() ); + + std::copy_if( regions.begin(), regions.end(), std::back_inserter( res ), [layer]( const QgsMapClippingRegion & region ) + { + return region.appliesToLayer( layer ); + } ); + + return res; +} + +QgsGeometry QgsMapClippingUtils::calculateFeatureRequestGeometry( const QList< QgsMapClippingRegion > ®ions, const QgsRenderContext &context, bool &shouldFilter ) +{ + QgsGeometry result; + bool first = true; + shouldFilter = false; + for ( const QgsMapClippingRegion ®ion : regions ) + { + if ( region.geometry().type() != QgsWkbTypes::PolygonGeometry ) + continue; + + shouldFilter = true; + if ( first ) + { + result = region.geometry(); + first = false; + } + else + { + result = result.intersection( region.geometry() ); + } + } + + // filter out polygon parts from result only + result.convertGeometryCollectionToSubclass( QgsWkbTypes::PolygonGeometry ); + + // lastly transform back to layer CRS + try + { + result.transform( context.coordinateTransform(), QgsCoordinateTransform::ReverseTransform ); + } + catch ( QgsCsException & ) + { + QgsDebugMsg( QStringLiteral( "Could not transform clipping region to layer CRS" ) ); + shouldFilter = false; + return QgsGeometry(); + } + + return result; +} diff --git a/src/core/qgsmapclippingutils.h b/src/core/qgsmapclippingutils.h new file mode 100644 index 00000000000..098810e4c67 --- /dev/null +++ b/src/core/qgsmapclippingutils.h @@ -0,0 +1,61 @@ +/*************************************************************************** + qgsmapclippingutils.h + -------------------------------------- + Date : June 2020 + Copyright : (C) 2020 by Nyall Dawson + Email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef QGSMAPCLIPPINGUTILS_H +#define QGSMAPCLIPPINGUTILS_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include + +class QgsRenderContext; +class QgsMapLayer; +class QgsGeometry; +class QgsMapClippingRegion; + +/** + * \class QgsMapClippingUtils + * \ingroup core + * + * Utility functions for use when clipping map renders. + * + * \since QGIS 3.16 +*/ +class CORE_EXPORT QgsMapClippingUtils +{ + public: + + /** + * Collects the list of map clipping regions from a \a context which apply to a map \a layer. + */ + static QList< QgsMapClippingRegion > collectClippingRegionsForLayer( const QgsRenderContext &context, const QgsMapLayer *layer ); + + /** + * Returns the geometry representing the intersection of clipping \a regions from \a context. + * + * The returned geometry will be automatically reprojected into the same CRS as the source layer, ready for use for filtering + * a feature request. + * + * \param regions list of clip regions which apply to the layer + * \param context a render context + * \param shouldFilter will be set to TRUE if layer's features should be filtered, i.e. one or more clipping regions applies to the layer + * + * \returns combined clipping region for use when filtering features to render + */ + static QgsGeometry calculateFeatureRequestGeometry( const QList< QgsMapClippingRegion > ®ions, const QgsRenderContext &context, bool &shouldFilter ); + +}; + +#endif // QGSMAPCLIPPINGUTILS_H diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index ec3083c0501..7e92354fa19 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -149,6 +149,7 @@ ADD_PYTHON_TEST(PyQgsLocator test_qgslocator.py) ADD_PYTHON_TEST(PyQgsMapCanvas test_qgsmapcanvas.py) ADD_PYTHON_TEST(PyQgsMapCanvasAnnotationItem test_qgsmapcanvasannotationitem.py) ADD_PYTHON_TEST(PyQgsMapClippingRegion test_qgsmapclippingregion.py) +ADD_PYTHON_TEST(PyQgsMapClippingUtils test_qgsmapclippingutils.py) ADD_PYTHON_TEST(PyQgsMapLayer test_qgsmaplayer.py) ADD_PYTHON_TEST(PyQgsMapLayerAction test_qgsmaplayeraction.py) ADD_PYTHON_TEST(PyQgsMapLayerComboBox test_qgsmaplayercombobox.py) diff --git a/tests/src/python/test_qgsmapclippingutils.py b/tests/src/python/test_qgsmapclippingutils.py new file mode 100644 index 00000000000..a45494faa80 --- /dev/null +++ b/tests/src/python/test_qgsmapclippingutils.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsMapClippingUtils. + +.. 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 + +from qgis.testing import unittest +from qgis.core import ( + QgsMapClippingRegion, + QgsMapClippingUtils, + QgsMapSettings, + QgsRenderContext, + QgsGeometry, + QgsVectorLayer, + QgsCoordinateTransform, + QgsCoordinateReferenceSystem, + QgsProject +) + + +class TestQgsMapClippingUtils(unittest.TestCase): + + def testClippingRegionsForLayer(self): + layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer", + "addfeat", "memory") + layer2 = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer", + "addfeat", "memory") + + region = QgsMapClippingRegion(QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))')) + region2 = QgsMapClippingRegion(QgsGeometry.fromWkt('Polygon((0 0, 0.1 0, 0.1 2, 0 2, 0 0))')) + region2.setRestrictedLayers([layer]) + ms = QgsMapSettings() + ms.addClippingRegion(region) + ms.addClippingRegion(region2) + rc = QgsRenderContext.fromMapSettings(ms) + + regions = QgsMapClippingUtils.collectClippingRegionsForLayer(rc, layer) + self.assertEqual(len(regions), 2) + self.assertEqual(regions[0].geometry().asWkt(1), 'Polygon ((0 0, 1 0, 1 1, 0 1, 0 0))') + self.assertEqual(regions[1].geometry().asWkt(1), 'Polygon ((0 0, 0.1 0, 0.1 2, 0 2, 0 0))') + + regions = QgsMapClippingUtils.collectClippingRegionsForLayer(rc, layer2) + self.assertEqual(len(regions), 1) + self.assertEqual(regions[0].geometry().asWkt(1), 'Polygon ((0 0, 1 0, 1 1, 0 1, 0 0))') + + def testCalculateFeatureRequestGeometry(self): + layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer", + "addfeat", "memory") + layer2 = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer", + "addfeat", "memory") + + region = QgsMapClippingRegion(QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))')) + region2 = QgsMapClippingRegion(QgsGeometry.fromWkt('Polygon((0 0, 0.1 0, 0.1 2, 0 2, 0 0))')) + + rc = QgsRenderContext() + + geom, should_clip = QgsMapClippingUtils.calculateFeatureRequestGeometry([], rc) + self.assertFalse(should_clip) + self.assertTrue(geom.isNull()) + + geom, should_clip = QgsMapClippingUtils.calculateFeatureRequestGeometry([region], rc) + self.assertTrue(should_clip) + self.assertEqual(geom.asWkt(1), 'Polygon ((0 0, 1 0, 1 1, 0 1, 0 0))') + + geom, should_clip = QgsMapClippingUtils.calculateFeatureRequestGeometry([region, region2], rc) + self.assertTrue(should_clip) + self.assertEqual(geom.asWkt(1), 'Polygon ((0.1 0, 0 0, 0 1, 0.1 1, 0.1 0))') + + rc.setCoordinateTransform(QgsCoordinateTransform(QgsCoordinateReferenceSystem('EPSG:3857'), QgsCoordinateReferenceSystem('EPSG:4326'), QgsProject.instance())) + geom, should_clip = QgsMapClippingUtils.calculateFeatureRequestGeometry([region, region2], rc) + self.assertTrue(should_clip) + self.assertEqual(geom.asWkt(0), 'Polygon ((11132 0, 0 0, 0 111325, 11132 111325, 11132 0))') + + +if __name__ == '__main__': + unittest.main()