Merge pull request #4402 from nyalldawson/iterator_invalid_geom

Add invalid geometry handling to QgsFeatureRequest/QgsVectorLayerFeatureIterator
This commit is contained in:
Nyall Dawson 2017-04-25 18:32:35 +10:00 committed by GitHub
commit 997b6304c8
7 changed files with 293 additions and 28 deletions

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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 )

View File

@ -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 ) )

View File

@ -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

View File

@ -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()