mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-27 00:33:48 -05:00
Merge pull request #4402 from nyalldawson/iterator_invalid_geom
Add invalid geometry handling to QgsFeatureRequest/QgsVectorLayerFeatureIterator
This commit is contained in:
commit
997b6304c8
@ -26,6 +26,14 @@ class QgsFeatureRequest
|
||||
FilterFids //!< Filter using feature IDs
|
||||
};
|
||||
|
||||
//! Handling of features with invalid geometries
|
||||
enum InvalidGeometryCheck
|
||||
{
|
||||
GeometryNoCheck,
|
||||
GeometrySkipInvalid,
|
||||
GeometryAbortOnInvalid,
|
||||
};
|
||||
|
||||
/**
|
||||
* The OrderByClause class represents an order by clause for a QgsFeatureRequest.
|
||||
*
|
||||
@ -196,6 +204,53 @@ class QgsFeatureRequest
|
||||
//! Get feature IDs that should be fetched.
|
||||
const QgsFeatureIds& filterFids() const;
|
||||
|
||||
/**
|
||||
* Sets invalid geometry checking behavior.
|
||||
* \note Invalid geometry checking is not performed when retrieving features
|
||||
* directly from a QgsVectorDataProvider.
|
||||
* \see invalidGeometryCheck()
|
||||
* \since QGIS 3.0
|
||||
*/
|
||||
QgsFeatureRequest &setInvalidGeometryCheck( InvalidGeometryCheck check );
|
||||
|
||||
/**
|
||||
* Returns the invalid geometry checking behavior.
|
||||
* \see setInvalidGeometryCheck()
|
||||
* \since QGIS 3.0
|
||||
*/
|
||||
InvalidGeometryCheck invalidGeometryCheck() const;
|
||||
|
||||
/**
|
||||
* Sets a callback function to use when encountering an invalid geometry and
|
||||
* invalidGeometryCheck() is set to GeometryAbortOnInvalid. This function will be
|
||||
* called using the feature with invalid geometry as a parameter.
|
||||
* \since QGIS 3.0
|
||||
* \see invalidGeometryCallback()
|
||||
*/
|
||||
QgsFeatureRequest &setInvalidGeometryCallback( SIP_PYCALLABLE /AllowNone/ );
|
||||
%MethodCode
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
|
||||
sipCpp->setInvalidGeometryCallback([a0](const QgsFeature &arg) {
|
||||
SIP_BLOCK_THREADS
|
||||
Py_XDECREF( sipCallMethod(NULL, a0, "D", &arg, sipType_QgsFeature, NULL) );
|
||||
SIP_UNBLOCK_THREADS
|
||||
});
|
||||
|
||||
sipRes = sipCpp;
|
||||
|
||||
Py_END_ALLOW_THREADS
|
||||
%End
|
||||
|
||||
/**
|
||||
* Returns the callback function to use when encountering an invalid geometry and
|
||||
* invalidGeometryCheck() is set to GeometryAbortOnInvalid.
|
||||
* \since QGIS 3.0
|
||||
* \note not available in Python bindings
|
||||
* \see setInvalidGeometryCallback()
|
||||
*/
|
||||
// std::function< void( const QgsFeature & ) > invalidGeometryCallback() const;
|
||||
|
||||
/** Set the filter expression. {@see QgsExpression}
|
||||
* @param expression expression string
|
||||
* @see filterExpression
|
||||
|
@ -99,6 +99,13 @@ def features(layer, request=QgsFeatureRequest()):
|
||||
def __init__(self, layer, request):
|
||||
self.layer = layer
|
||||
self.selection = False
|
||||
|
||||
invalidFeaturesMethod = ProcessingConfig.getSetting(ProcessingConfig.FILTER_INVALID_GEOMETRIES)
|
||||
if invalidFeaturesMethod == self.IGNORE:
|
||||
request.setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid)
|
||||
elif invalidFeaturesMethod == self.RAISE_EXCEPTION:
|
||||
request.setInvalidGeometryCheck(QgsFeatureRequest.GeometryAbortOnInvalid)
|
||||
|
||||
if ProcessingConfig.getSetting(ProcessingConfig.USE_SELECTED)\
|
||||
and layer.selectedFeatureCount() > 0:
|
||||
self.iter = layer.selectedFeaturesIterator(request)
|
||||
@ -106,27 +113,6 @@ def features(layer, request=QgsFeatureRequest()):
|
||||
else:
|
||||
self.iter = layer.getFeatures(request)
|
||||
|
||||
invalidFeaturesMethod = ProcessingConfig.getSetting(ProcessingConfig.FILTER_INVALID_GEOMETRIES)
|
||||
|
||||
def filterFeature(f, ignoreInvalid):
|
||||
geom = f.geometry()
|
||||
if geom is None:
|
||||
ProcessingLog.addToLog(ProcessingLog.LOG_INFO,
|
||||
self.tr('Feature with NULL geometry found.'))
|
||||
elif not geom.isGeosValid():
|
||||
ProcessingLog.addToLog(ProcessingLog.LOG_ERROR,
|
||||
self.tr('GEOS geoprocessing error: One or more input features have invalid geometry.'))
|
||||
if ignoreInvalid:
|
||||
return False
|
||||
else:
|
||||
raise GeoAlgorithmExecutionException(self.tr('Features with invalid geometries found. Please fix these geometries or specify the "Ignore invalid input features" flag'))
|
||||
return True
|
||||
|
||||
if invalidFeaturesMethod == self.IGNORE:
|
||||
self.iter = filter(lambda x: filterFeature(x, True), self.iter)
|
||||
elif invalidFeaturesMethod == self.RAISE_EXCEPTION:
|
||||
self.iter = filter(lambda x: filterFeature(x, False), self.iter)
|
||||
|
||||
def __iter__(self):
|
||||
return self.iter
|
||||
|
||||
|
@ -76,6 +76,8 @@ QgsFeatureRequest &QgsFeatureRequest::operator=( const QgsFeatureRequest &rh )
|
||||
{
|
||||
mFilterExpression.reset( nullptr );
|
||||
}
|
||||
mInvalidGeometryFilter = rh.mInvalidGeometryFilter;
|
||||
mInvalidGeometryCallback = rh.mInvalidGeometryCallback;
|
||||
mExpressionContext = rh.mExpressionContext;
|
||||
mAttrs = rh.mAttrs;
|
||||
mSimplifyMethod = rh.mSimplifyMethod;
|
||||
@ -104,6 +106,18 @@ QgsFeatureRequest &QgsFeatureRequest::setFilterFids( const QgsFeatureIds &fids )
|
||||
return *this;
|
||||
}
|
||||
|
||||
QgsFeatureRequest &QgsFeatureRequest::setInvalidGeometryCheck( QgsFeatureRequest::InvalidGeometryCheck check )
|
||||
{
|
||||
mInvalidGeometryFilter = check;
|
||||
return *this;
|
||||
}
|
||||
|
||||
QgsFeatureRequest &QgsFeatureRequest::setInvalidGeometryCallback( std::function<void ( const QgsFeature & )> callback )
|
||||
{
|
||||
mInvalidGeometryCallback = callback;
|
||||
return *this;
|
||||
}
|
||||
|
||||
QgsFeatureRequest &QgsFeatureRequest::setFilterExpression( const QString &expression )
|
||||
{
|
||||
mFilter = FilterExpression;
|
||||
|
@ -85,6 +85,14 @@ class CORE_EXPORT QgsFeatureRequest
|
||||
FilterFids //!< Filter using feature IDs
|
||||
};
|
||||
|
||||
//! Handling of features with invalid geometries
|
||||
enum InvalidGeometryCheck
|
||||
{
|
||||
GeometryNoCheck = 0, //!< No invalid geometry checking
|
||||
GeometrySkipInvalid = 1, //!< Skip any features with invalid geometry. This requires a slow geometry validity check for every feature.
|
||||
GeometryAbortOnInvalid = 2, //!< Close iterator on encountering any features with invalid geometry. This requires a slow geometry validity check for every feature.
|
||||
};
|
||||
|
||||
/** \ingroup core
|
||||
* The OrderByClause class represents an order by clause for a QgsFeatureRequest.
|
||||
*
|
||||
@ -270,6 +278,40 @@ class CORE_EXPORT QgsFeatureRequest
|
||||
//! Get feature IDs that should be fetched.
|
||||
const QgsFeatureIds &filterFids() const { return mFilterFids; }
|
||||
|
||||
/**
|
||||
* Sets invalid geometry checking behavior.
|
||||
* \note Invalid geometry checking is not performed when retrieving features
|
||||
* directly from a QgsVectorDataProvider.
|
||||
* \see invalidGeometryCheck()
|
||||
* \since QGIS 3.0
|
||||
*/
|
||||
QgsFeatureRequest &setInvalidGeometryCheck( InvalidGeometryCheck check );
|
||||
|
||||
/**
|
||||
* Returns the invalid geometry checking behavior.
|
||||
* \see setInvalidGeometryCheck()
|
||||
* \since QGIS 3.0
|
||||
*/
|
||||
InvalidGeometryCheck invalidGeometryCheck() const { return mInvalidGeometryFilter; }
|
||||
|
||||
/**
|
||||
* Sets a callback function to use when encountering an invalid geometry and
|
||||
* invalidGeometryCheck() is set to GeometryAbortOnInvalid. This function will be
|
||||
* called using the feature with invalid geometry as a parameter.
|
||||
* \since QGIS 3.0
|
||||
* \see invalidGeometryCallback()
|
||||
*/
|
||||
QgsFeatureRequest &setInvalidGeometryCallback( std::function< void( const QgsFeature & ) > callback );
|
||||
|
||||
/**
|
||||
* Returns the callback function to use when encountering an invalid geometry and
|
||||
* invalidGeometryCheck() is set to GeometryAbortOnInvalid.
|
||||
* \since QGIS 3.0
|
||||
* \note not available in Python bindings
|
||||
* \see setInvalidGeometryCallback()
|
||||
*/
|
||||
std::function< void( const QgsFeature & ) > invalidGeometryCallback() const { return mInvalidGeometryCallback; }
|
||||
|
||||
/** Set the filter expression. {\see QgsExpression}
|
||||
* \param expression expression string
|
||||
* \see filterExpression
|
||||
@ -415,6 +457,8 @@ class CORE_EXPORT QgsFeatureRequest
|
||||
QgsSimplifyMethod mSimplifyMethod;
|
||||
long mLimit = -1;
|
||||
OrderBy mOrderBy;
|
||||
InvalidGeometryCheck mInvalidGeometryFilter = GeometryNoCheck;
|
||||
std::function< void( const QgsFeature & ) > mInvalidGeometryCallback;
|
||||
};
|
||||
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS( QgsFeatureRequest::Flags )
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include "qgsexpressioncontext.h"
|
||||
#include "qgsdistancearea.h"
|
||||
#include "qgsproject.h"
|
||||
#include "qgsmessagelog.h"
|
||||
|
||||
QgsVectorLayerFeatureSource::QgsVectorLayerFeatureSource( const QgsVectorLayer *layer )
|
||||
{
|
||||
@ -241,8 +242,15 @@ bool QgsVectorLayerFeatureIterator::fetchFeature( QgsFeature &f )
|
||||
if ( mFetchedFid )
|
||||
return false;
|
||||
bool res = nextFeatureFid( f );
|
||||
mFetchedFid = true;
|
||||
return res;
|
||||
if ( res && testFeature( f ) )
|
||||
{
|
||||
mFetchedFid = true;
|
||||
return res;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !mRequest.filterRect().isNull() )
|
||||
@ -305,6 +313,9 @@ bool QgsVectorLayerFeatureIterator::fetchFeature( QgsFeature &f )
|
||||
if ( !( mRequest.flags() & QgsFeatureRequest::NoGeometry ) )
|
||||
updateFeatureGeometry( f );
|
||||
|
||||
if ( !testFeature( f ) )
|
||||
continue;
|
||||
|
||||
return true;
|
||||
}
|
||||
// no more provider features
|
||||
@ -366,6 +377,9 @@ bool QgsVectorLayerFeatureIterator::fetchNextAddedFeature( QgsFeature &f )
|
||||
// skip features which are not accepted by the filter
|
||||
continue;
|
||||
|
||||
if ( !testFeature( *mFetchAddedFeaturesIt ) )
|
||||
continue;
|
||||
|
||||
useAddedFeature( *mFetchAddedFeaturesIt, f );
|
||||
|
||||
return true;
|
||||
@ -416,9 +430,12 @@ bool QgsVectorLayerFeatureIterator::fetchNextChangedGeomFeature( QgsFeature &f )
|
||||
|
||||
useChangedAttributeFeature( fid, *mFetchChangedGeomIt, f );
|
||||
|
||||
// return complete feature
|
||||
mFetchChangedGeomIt++;
|
||||
return true;
|
||||
if ( testFeature( f ) )
|
||||
{
|
||||
// return complete feature
|
||||
mFetchChangedGeomIt++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false; // no more changed geometries
|
||||
@ -440,7 +457,7 @@ bool QgsVectorLayerFeatureIterator::fetchNextChangedAttributeFeature( QgsFeature
|
||||
addVirtualAttributes( f );
|
||||
|
||||
mRequest.expressionContext()->setFeature( f );
|
||||
if ( mRequest.filterExpression()->evaluate( mRequest.expressionContext() ).toBool() )
|
||||
if ( mRequest.filterExpression()->evaluate( mRequest.expressionContext() ).toBool() && testFeature( f ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -658,6 +675,49 @@ void QgsVectorLayerFeatureIterator::createOrderedJoinList()
|
||||
}
|
||||
}
|
||||
|
||||
bool QgsVectorLayerFeatureIterator::testFeature( const QgsFeature &feature )
|
||||
{
|
||||
bool result = checkGeometryValidity( feature );
|
||||
return result;
|
||||
}
|
||||
|
||||
bool QgsVectorLayerFeatureIterator::checkGeometryValidity( const QgsFeature &feature )
|
||||
{
|
||||
if ( !feature.hasGeometry() )
|
||||
return true;
|
||||
|
||||
switch ( mRequest.invalidGeometryCheck() )
|
||||
{
|
||||
case QgsFeatureRequest::GeometryNoCheck:
|
||||
return true;
|
||||
|
||||
case QgsFeatureRequest::GeometrySkipInvalid:
|
||||
{
|
||||
if ( !feature.geometry().isGeosValid() )
|
||||
{
|
||||
QgsMessageLog::logMessage( QObject::tr( "Geometry error: One or more input features have invalid geometry." ), QString(), QgsMessageLog::CRITICAL );
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case QgsFeatureRequest::GeometryAbortOnInvalid:
|
||||
if ( !feature.geometry().isGeosValid() )
|
||||
{
|
||||
QgsMessageLog::logMessage( QObject::tr( "Geometry error: One or more input features have invalid geometry." ), QString(), QgsMessageLog::CRITICAL );
|
||||
close();
|
||||
if ( mRequest.invalidGeometryCallback() )
|
||||
{
|
||||
mRequest.invalidGeometryCallback()( feature );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void QgsVectorLayerFeatureIterator::prepareField( int fieldIdx )
|
||||
{
|
||||
switch ( mSource->mFields.fieldOrigin( fieldIdx ) )
|
||||
|
@ -221,6 +221,16 @@ class CORE_EXPORT QgsVectorLayerFeatureIterator : public QgsAbstractFeatureItera
|
||||
virtual bool providerCanSimplify( QgsSimplifyMethod::MethodType methodType ) const override;
|
||||
|
||||
void createOrderedJoinList();
|
||||
|
||||
/**
|
||||
* Performs any feature based validity checking, e.g. checking for geometry validity.
|
||||
*/
|
||||
bool testFeature( const QgsFeature &feature );
|
||||
|
||||
/**
|
||||
* Checks a feature's geometry for validity, if requested in feature request.
|
||||
*/
|
||||
bool checkGeometryValidity( const QgsFeature &feature );
|
||||
};
|
||||
|
||||
#endif // QGSVECTORLAYERFEATUREITERATOR_H
|
||||
|
@ -16,7 +16,14 @@ import qgis # NOQA
|
||||
|
||||
import os
|
||||
|
||||
from qgis.core import QgsVectorLayer, QgsFeatureRequest, QgsFeature, QgsField, NULL, QgsProject, QgsVectorLayerJoinInfo
|
||||
from qgis.core import (QgsVectorLayer,
|
||||
QgsFeatureRequest,
|
||||
QgsFeature,
|
||||
QgsField,
|
||||
NULL,
|
||||
QgsProject,
|
||||
QgsVectorLayerJoinInfo,
|
||||
QgsGeometry)
|
||||
from qgis.testing import start_app, unittest
|
||||
from qgis.PyQt.QtCore import QVariant
|
||||
|
||||
@ -273,6 +280,95 @@ class TestQgsFeatureIterator(unittest.TestCase):
|
||||
|
||||
QgsProject.instance().removeMapLayers([layer.id(), joinLayer.id()])
|
||||
|
||||
def test_invalidGeometryFilter(self):
|
||||
layer = QgsVectorLayer(
|
||||
"Polygon?field=x:string",
|
||||
"joinlayer", "memory")
|
||||
|
||||
# add some features, one has invalid geometry
|
||||
pr = layer.dataProvider()
|
||||
f1 = QgsFeature(1)
|
||||
f1.setAttributes(["a"])
|
||||
f1.setGeometry(QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))')) # valid
|
||||
f2 = QgsFeature(2)
|
||||
f2.setAttributes(["b"])
|
||||
f2.setGeometry(QgsGeometry.fromWkt('Polygon((0 0, 1 0, 0 1, 1 1, 0 0))')) # invalid
|
||||
f3 = QgsFeature(3)
|
||||
f3.setAttributes(["c"])
|
||||
f3.setGeometry(QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))')) # valid
|
||||
self.assertTrue(pr.addFeatures([f1, f2, f3]))
|
||||
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck))]
|
||||
self.assertEqual(res, ['a', 'b', 'c'])
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid))]
|
||||
self.assertEqual(res, ['a', 'c'])
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometryAbortOnInvalid))]
|
||||
self.assertEqual(res, ['a'])
|
||||
|
||||
# with callback
|
||||
self.callback_feature_val = None
|
||||
|
||||
def callback(feature):
|
||||
self.callback_feature_val = feature['x']
|
||||
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(
|
||||
QgsFeatureRequest.GeometryAbortOnInvalid).setInvalidGeometryCallback(callback))]
|
||||
self.assertEqual(res, ['a'])
|
||||
self.assertEqual(self.callback_feature_val, 'b')
|
||||
# clear callback
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(
|
||||
QgsFeatureRequest.GeometryAbortOnInvalid).setInvalidGeometryCallback(None))]
|
||||
self.assertEqual(res, ['a'])
|
||||
|
||||
# check with filter fids
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setFilterFid(f2.id()).setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck))]
|
||||
self.assertEqual(res, ['b'])
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setFilterFid(f2.id()).setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid))]
|
||||
self.assertEqual(res, [])
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setFilterFid(f2.id()).setInvalidGeometryCheck(QgsFeatureRequest.GeometryAbortOnInvalid))]
|
||||
self.assertEqual(res, [])
|
||||
|
||||
f4 = QgsFeature(4)
|
||||
f4.setAttributes(["d"])
|
||||
f4.setGeometry(QgsGeometry.fromWkt('Polygon((0 0, 1 0, 0 1, 1 1, 0 0))')) # invalid
|
||||
|
||||
# check with added features
|
||||
layer.startEditing()
|
||||
self.assertTrue(layer.addFeatures([f4]))
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck))]
|
||||
self.assertEqual(set(res), {'a', 'b', 'c', 'd'})
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid))]
|
||||
self.assertEqual(set(res), {'a', 'c'})
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometryAbortOnInvalid))]
|
||||
self.assertEqual(res, ['a'])
|
||||
|
||||
# check with features with changed geometry
|
||||
layer.rollBack()
|
||||
layer.startEditing()
|
||||
layer.changeGeometry(2, QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))')) # valid
|
||||
layer.changeGeometry(3, QgsGeometry.fromWkt('Polygon((0 0, 1 0, 0 1, 1 1, 0 0))'))# invalid
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck))]
|
||||
self.assertEqual(set(res), {'a', 'b', 'c'})
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid))]
|
||||
self.assertEqual(set(res), {'a', 'b'})
|
||||
res = [f['x'] for f in
|
||||
layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometryAbortOnInvalid))]
|
||||
self.assertEqual(res, ['a', 'b'])
|
||||
layer.rollBack()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
Loading…
x
Reference in New Issue
Block a user