Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
New class QgsMapClippingUtils with utility functions for helping
with map clipping
  • Loading branch information
nyalldawson committed Jul 2, 2020
1 parent b5ae078 commit b10b169
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 0 deletions.
53 changes: 53 additions & 0 deletions 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 > &regions, 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 *
************************************************************************/
1 change: 1 addition & 0 deletions python/core/core_auto.sip
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/core/CMakeLists.txt
Expand Up @@ -312,6 +312,7 @@ SET(QGIS_CORE_SRCS
qgslocalizeddatapathregistry.cpp
qgslogger.cpp
qgsmapclippingregion.cpp
qgsmapclippingutils.cpp
qgsmapdecoration.cpp
qgsmaphittest.cpp
qgsmaplayer.cpp
Expand Down Expand Up @@ -861,6 +862,7 @@ SET(QGIS_CORE_HDRS
qgslocalizeddatapathregistry.h
qgslogger.h
qgsmapclippingregion.h
qgsmapclippingutils.h
qgsmapdecoration.h
qgsmaphittest.h
qgsmaplayer.h
Expand Down
75 changes: 75 additions & 0 deletions 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 <algorithm>

QList<QgsMapClippingRegion> 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 > &regions, const QgsRenderContext &context, bool &shouldFilter )
{
QgsGeometry result;
bool first = true;
shouldFilter = false;
for ( const QgsMapClippingRegion &region : 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;
}
61 changes: 61 additions & 0 deletions 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 <QList>

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 > &regions, const QgsRenderContext &context, bool &shouldFilter );

};

#endif // QGSMAPCLIPPINGUTILS_H
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -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)
Expand Down
84 changes: 84 additions & 0 deletions 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()

0 comments on commit b10b169

Please sign in to comment.