Skip to content

Commit a989235

Browse files
committed
Allow specifying a destination CRS in QgsFeatureRequest
If set, all geometries will be reprojected from their original coordinate reference system to the destination CRS while iterating over features. If a CRS has been set as the destination CRS, then the filterRect parameter should be specified in the same CRS as this destination CRS. Additionally, a callback function can be specified on the request to be called if a transform exception is encountered while iterating over features. This is designed to make it easier for scripts and plugins to correctly reproject layers in an efficient and robust way, instead of having to implement lots of repeated code themselves and potentially missing some of the important considerations which come with reprojecting geometries & bounding boxes. Now, if a script wants the features from a layer in a specific CRS, they can call: crs = QgsCoordinateReferenceSystem('epsg:4326') request = QgsFeatureRequest().setDestinationCrs(crs) for f in layer.getFeatures(reqeuest): print('geometry in 4326 is {}.format(f.geometry().exportToWkt()))
1 parent fcc06ce commit a989235

31 files changed

+526
-96
lines changed

python/core/qgsfeatureiterator.sip

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,28 @@ end of iterating: free the resources / lock
102102
:rtype: bool
103103
%End
104104

105+
void transformFeatureGeometry( QgsFeature &feature, const QgsCoordinateTransform &transform ) const;
106+
%Docstring
107+
Transforms ``feature``'s geometry according to the specified coordinate ``transform``.
108+
If ``feature`` has no geometry or ``transform`` is invalid then calling this method
109+
has no effect and will be shortcut.
110+
Iterators should call this method before returning features to ensure that any
111+
QgsFeatureRequest.destinationCrs() set on the request is respected.
112+
.. versionadded:: 3.0
113+
%End
114+
115+
116+
QgsRectangle transformedFilterRect( const QgsCoordinateTransform &transform ) const;
117+
%Docstring
118+
Returns a rectangle representing the original request's QgsFeatureRequest.filterRect().
119+
If ``transform`` is a valid coordinate transform, the return rectangle will represent
120+
the requested filterRect() transformed to the source's coordinate reference system.
121+
Iterators should call this method and use the returned rectangle for filtering
122+
features to ensure that any QgsFeatureRequest.destinationCrs() set on the request is respected.
123+
.. versionadded:: 3.0
124+
:rtype: QgsRectangle
125+
%End
126+
105127

106128

107129

python/core/qgsfeaturerequest.sip

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,16 @@ construct a request with feature ID filter
267267
%Docstring
268268
construct a request with feature ID filter
269269
%End
270-
explicit QgsFeatureRequest( const QgsRectangle &rect );
270+
271+
explicit QgsFeatureRequest( const QgsRectangle &rectangle );
271272
%Docstring
272-
construct a request with rectangle filter
273+
Construct a request with ``rectangle`` bounding box filter.
274+
275+
When a destination CRS is set using setDestinationCrs(), ``rectangle``
276+
is expected to be in the same CRS as the destinationCrs(). Otherwise, ``rectangle``
277+
should use the same CRS as the source layer/provider.
273278
%End
279+
274280
explicit QgsFeatureRequest( const QgsExpression &expr, const QgsExpressionContext &context = QgsExpressionContext() );
275281
%Docstring
276282
construct a request with a filter expression
@@ -288,16 +294,28 @@ copy constructor
288294
:rtype: FilterType
289295
%End
290296

291-
QgsFeatureRequest &setFilterRect( const QgsRectangle &rect );
297+
QgsFeatureRequest &setFilterRect( const QgsRectangle &rectangle );
292298
%Docstring
293-
Set rectangle from which features will be taken. Empty rectangle removes the filter.
299+
Sets the ``rectangle`` from which features will be taken. An empty rectangle removes the filter.
300+
301+
When a destination CRS is set using setDestinationCrs(), ``rectangle``
302+
is expected to be in the same CRS as the destinationCrs(). Otherwise, ``rectangle``
303+
should use the same CRS as the source layer/provider.
304+
305+
.. seealso:: filterRect()
294306
:rtype: QgsFeatureRequest
295307
%End
296308

297309
const QgsRectangle &filterRect() const;
298310
%Docstring
299-
Get the rectangle from which features will be taken. If the returned
311+
Returns the rectangle from which features will be taken. If the returned
300312
rectangle is null, then no filter rectangle is set.
313+
314+
When a destination CRS is set using setDestinationCrs(), the rectangle
315+
will be in the same CRS as the destinationCrs(). Otherwise, the rectangle
316+
will use the same CRS as the source layer/provider.
317+
318+
.. seealso:: setFilterRect()
301319
:rtype: QgsRectangle
302320
%End
303321

@@ -528,6 +546,60 @@ Set a subset of attributes by names that will be fetched
528546
:rtype: QgsSimplifyMethod
529547
%End
530548

549+
QgsCoordinateReferenceSystem destinationCrs() const;
550+
%Docstring
551+
Returns the destination coordinate reference system for feature's geometries,
552+
or an invalid QgsCoordinateReferenceSystem if no reprojection will be done
553+
and all features will be left with their original geometry.
554+
.. seealso:: setDestinationCrs()
555+
.. versionadded:: 3.0
556+
:rtype: QgsCoordinateReferenceSystem
557+
%End
558+
559+
QgsFeatureRequest &setDestinationCrs( const QgsCoordinateReferenceSystem &crs );
560+
%Docstring
561+
Sets the destination ``crs`` for feature's geometries. If set, all
562+
geometries will be reprojected from their original coordinate reference
563+
system to this desired reference system. If ``crs`` is an invalid
564+
QgsCoordinateReferenceSystem then no reprojection will be done
565+
and all features will be left with their original geometry.
566+
567+
When a ``crs`` is set using setDestinationCrs(), then any filterRect()
568+
set on the request is expected to be in the same CRS as the destination
569+
CRS.
570+
571+
.. seealso:: destinationCrs()
572+
.. versionadded:: 3.0
573+
:rtype: QgsFeatureRequest
574+
%End
575+
576+
QgsFeatureRequest &setTransformErrorCallback( SIP_PYCALLABLE / AllowNone / );
577+
%Docstring
578+
Sets a callback function to use when encountering a transform error when iterating
579+
features and a destinationCrs() is set. This function will be
580+
called using the feature which encountered the transform error as a parameter.
581+
.. versionadded:: 3.0
582+
.. seealso:: transformErrorCallback()
583+
.. seealso:: setDestinationCrs()
584+
:rtype: QgsFeatureRequest
585+
%End
586+
%MethodCode
587+
Py_BEGIN_ALLOW_THREADS
588+
589+
sipCpp->setTransformErrorCallback( [a0]( const QgsFeature &arg )
590+
{
591+
SIP_BLOCK_THREADS
592+
Py_XDECREF( sipCallMethod( NULL, a0, "D", &arg, sipType_QgsFeature, NULL ) );
593+
SIP_UNBLOCK_THREADS
594+
} );
595+
596+
sipRes = sipCpp;
597+
598+
Py_END_ALLOW_THREADS
599+
%End
600+
601+
602+
531603
bool acceptFeature( const QgsFeature &feature );
532604
%Docstring
533605
Check if a feature is accepted by this requests filter

src/core/providers/memory/qgsmemoryfeatureiterator.cpp

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,31 @@
2727
QgsMemoryFeatureIterator::QgsMemoryFeatureIterator( QgsMemoryFeatureSource *source, bool ownSource, const QgsFeatureRequest &request )
2828
: QgsAbstractFeatureIteratorFromSource<QgsMemoryFeatureSource>( source, ownSource, request )
2929
{
30+
if ( mRequest.destinationCrs().isValid() && mRequest.destinationCrs() != mSource->mCrs )
31+
{
32+
mTransform = QgsCoordinateTransform( mSource->mCrs, mRequest.destinationCrs() );
33+
}
34+
mFilterRect = transformedFilterRect( mTransform );
35+
3036
if ( !mSource->mSubsetString.isEmpty() )
3137
{
3238
mSubsetExpression = new QgsExpression( mSource->mSubsetString );
3339
mSubsetExpression->prepare( &mSource->mExpressionContext );
3440
}
3541

36-
if ( !mRequest.filterRect().isNull() && mRequest.flags() & QgsFeatureRequest::ExactIntersect )
42+
if ( !mFilterRect.isNull() && mRequest.flags() & QgsFeatureRequest::ExactIntersect )
3743
{
38-
mSelectRectGeom = QgsGeometry::fromRect( request.filterRect() );
44+
mSelectRectGeom = QgsGeometry::fromRect( mFilterRect );
3945
mSelectRectEngine.reset( QgsGeometry::createGeometryEngine( mSelectRectGeom.geometry() ) );
4046
mSelectRectEngine->prepareGeometry();
4147
}
4248

4349
// if there's spatial index, use it!
4450
// (but don't use it when selection rect is not specified)
45-
if ( !mRequest.filterRect().isNull() && mSource->mSpatialIndex )
51+
if ( !mFilterRect.isNull() && mSource->mSpatialIndex )
4652
{
4753
mUsingFeatureIdList = true;
48-
mFeatureIdList = mSource->mSpatialIndex->intersects( mRequest.filterRect() );
54+
mFeatureIdList = mSource->mSpatialIndex->intersects( mFilterRect );
4955
QgsDebugMsg( "Features returned by spatial index: " + QString::number( mFeatureIdList.count() ) );
5056
}
5157
else if ( mRequest.filterType() == QgsFeatureRequest::FilterFid )
@@ -92,7 +98,7 @@ bool QgsMemoryFeatureIterator::nextFeatureUsingList( QgsFeature &feature )
9298
// option 1: we have a list of features to traverse
9399
while ( mFeatureIdListIterator != mFeatureIdList.constEnd() )
94100
{
95-
if ( !mRequest.filterRect().isNull() && mRequest.flags() & QgsFeatureRequest::ExactIntersect )
101+
if ( !mFilterRect.isNull() && mRequest.flags() & QgsFeatureRequest::ExactIntersect )
96102
{
97103
// do exact check in case we're doing intersection
98104
if ( mSource->mFeatures.value( *mFeatureIdListIterator ).hasGeometry() && mSelectRectEngine->intersects( *mSource->mFeatures.value( *mFeatureIdListIterator ).geometry().geometry() ) )
@@ -124,7 +130,10 @@ bool QgsMemoryFeatureIterator::nextFeatureUsingList( QgsFeature &feature )
124130
close();
125131

126132
if ( hasFeature )
133+
{
127134
feature.setFields( mSource->mFields ); // allow name-based attribute lookups
135+
transformFeatureGeometry( feature, mTransform );
136+
}
128137

129138
return hasFeature;
130139
}
@@ -137,7 +146,7 @@ bool QgsMemoryFeatureIterator::nextFeatureTraverseAll( QgsFeature &feature )
137146
// option 2: traversing the whole layer
138147
while ( mSelectIterator != mSource->mFeatures.constEnd() )
139148
{
140-
if ( mRequest.filterRect().isNull() )
149+
if ( mFilterRect.isNull() )
141150
{
142151
// selection rect empty => using all features
143152
hasFeature = true;
@@ -153,7 +162,7 @@ bool QgsMemoryFeatureIterator::nextFeatureTraverseAll( QgsFeature &feature )
153162
else
154163
{
155164
// check just bounding box against rect when not using intersection
156-
if ( mSelectIterator->hasGeometry() && mSelectIterator->geometry().boundingBox().intersects( mRequest.filterRect() ) )
165+
if ( mSelectIterator->hasGeometry() && mSelectIterator->geometry().boundingBox().intersects( mFilterRect ) )
157166
hasFeature = true;
158167
}
159168
}
@@ -178,6 +187,7 @@ bool QgsMemoryFeatureIterator::nextFeatureTraverseAll( QgsFeature &feature )
178187
++mSelectIterator;
179188
feature.setValid( true );
180189
feature.setFields( mSource->mFields ); // allow name-based attribute lookups
190+
transformFeatureGeometry( feature, mTransform );
181191
}
182192
else
183193
close();
@@ -216,6 +226,7 @@ QgsMemoryFeatureSource::QgsMemoryFeatureSource( const QgsMemoryProvider *p )
216226
, mFeatures( p->mFeatures )
217227
, mSpatialIndex( p->mSpatialIndex ? new QgsSpatialIndex( *p->mSpatialIndex ) : nullptr ) // just shallow copy
218228
, mSubsetString( p->mSubsetString )
229+
, mCrs( p->mCrs )
219230
{
220231
mExpressionContext << QgsExpressionContextUtils::globalScope()
221232
<< QgsExpressionContextUtils::projectScope( QgsProject::instance() );

src/core/providers/memory/qgsmemoryfeatureiterator.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class QgsMemoryFeatureSource : public QgsAbstractFeatureSource
4242
std::unique_ptr< QgsSpatialIndex > mSpatialIndex;
4343
QString mSubsetString;
4444
QgsExpressionContext mExpressionContext;
45+
QgsCoordinateReferenceSystem mCrs;
4546

4647
friend class QgsMemoryFeatureIterator;
4748
};
@@ -67,11 +68,13 @@ class QgsMemoryFeatureIterator : public QgsAbstractFeatureIteratorFromSource<Qgs
6768

6869
QgsGeometry mSelectRectGeom;
6970
std::unique_ptr< QgsGeometryEngine > mSelectRectEngine;
71+
QgsRectangle mFilterRect;
7072
QgsFeatureMap::const_iterator mSelectIterator;
7173
bool mUsingFeatureIdList = false;
7274
QList<QgsFeatureId> mFeatureIdList;
7375
QList<QgsFeatureId>::const_iterator mFeatureIdListIterator;
7476
QgsExpression *mSubsetExpression = nullptr;
77+
QgsCoordinateTransform mTransform;
7578

7679
};
7780

src/core/qgsfeatureiterator.cpp

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
#include "qgslogger.h"
1717

1818
#include "qgssimplifymethod.h"
19-
19+
#include "qgscsexception.h"
2020
#include "qgsexpressionsorter.h"
2121

2222
QgsAbstractFeatureIterator::QgsAbstractFeatureIterator( const QgsFeatureRequest &request )
@@ -98,6 +98,43 @@ bool QgsAbstractFeatureIterator::nextFeatureFilterFids( QgsFeature &f )
9898
return false;
9999
}
100100

101+
void QgsAbstractFeatureIterator::transformFeatureGeometry( QgsFeature &feature, const QgsCoordinateTransform &transform ) const
102+
{
103+
if ( transform.isValid() && feature.hasGeometry() )
104+
{
105+
try
106+
{
107+
QgsGeometry g = feature.geometry();
108+
g.transform( transform );
109+
feature.setGeometry( g );
110+
}
111+
catch ( QgsCsException & )
112+
{
113+
// transform error
114+
if ( mRequest.transformErrorCallback() )
115+
{
116+
mRequest.transformErrorCallback()( feature );
117+
}
118+
}
119+
}
120+
}
121+
122+
QgsRectangle QgsAbstractFeatureIterator::transformedFilterRect( const QgsCoordinateTransform &transform ) const
123+
{
124+
if ( mRequest.filterRect().isNull() )
125+
return QgsRectangle();
126+
127+
try
128+
{
129+
return transform.transformBoundingBox( mRequest.filterRect(), QgsCoordinateTransform::ReverseTransform );
130+
}
131+
catch ( QgsCsException & )
132+
{
133+
// can't reproject mFilterRect
134+
return mRequest.filterRect();
135+
}
136+
}
137+
101138
void QgsAbstractFeatureIterator::ref()
102139
{
103140
// Prepare if required the simplification of geometries to fetch:

src/core/qgsfeatureiterator.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,27 @@ class CORE_EXPORT QgsAbstractFeatureIterator
114114
*/
115115
virtual bool nextFeatureFilterFids( QgsFeature &f );
116116

117+
/**
118+
* Transforms \a feature's geometry according to the specified coordinate \a transform.
119+
* If \a feature has no geometry or \a transform is invalid then calling this method
120+
* has no effect and will be shortcut.
121+
* Iterators should call this method before returning features to ensure that any
122+
* QgsFeatureRequest::destinationCrs() set on the request is respected.
123+
* \since QGIS 3.0
124+
*/
125+
void transformFeatureGeometry( QgsFeature &feature, const QgsCoordinateTransform &transform ) const;
126+
127+
128+
/**
129+
* Returns a rectangle representing the original request's QgsFeatureRequest::filterRect().
130+
* If \a transform is a valid coordinate transform, the return rectangle will represent
131+
* the requested filterRect() transformed to the source's coordinate reference system.
132+
* Iterators should call this method and use the returned rectangle for filtering
133+
* features to ensure that any QgsFeatureRequest::destinationCrs() set on the request is respected.
134+
* \since QGIS 3.0
135+
*/
136+
QgsRectangle transformedFilterRect( const QgsCoordinateTransform &transform ) const;
137+
117138
//! A copy of the feature request.
118139
QgsFeatureRequest mRequest;
119140

src/core/qgsfeaturerequest.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ QgsFeatureRequest &QgsFeatureRequest::operator=( const QgsFeatureRequest &rh )
8383
mSimplifyMethod = rh.mSimplifyMethod;
8484
mLimit = rh.mLimit;
8585
mOrderBy = rh.mOrderBy;
86+
mCrs = rh.mCrs;
87+
mTransformErrorCallback = rh.mTransformErrorCallback;
8688
return *this;
8789
}
8890

@@ -234,6 +236,24 @@ QgsFeatureRequest &QgsFeatureRequest::setSimplifyMethod( const QgsSimplifyMethod
234236
return *this;
235237
}
236238

239+
240+
QgsCoordinateReferenceSystem QgsFeatureRequest::destinationCrs() const
241+
{
242+
return mCrs;
243+
}
244+
245+
QgsFeatureRequest &QgsFeatureRequest::setDestinationCrs( const QgsCoordinateReferenceSystem &crs )
246+
{
247+
mCrs = crs;
248+
return *this;
249+
}
250+
251+
QgsFeatureRequest &QgsFeatureRequest::setTransformErrorCallback( std::function<void ( const QgsFeature & )> callback )
252+
{
253+
mTransformErrorCallback = callback;
254+
return *this;
255+
}
256+
237257
bool QgsFeatureRequest::acceptFeature( const QgsFeature &feature )
238258
{
239259
if ( !mFilterRect.isNull() )

0 commit comments

Comments
 (0)