Skip to content

Commit

Permalink
Cache validity check results
Browse files Browse the repository at this point in the history
For non-point geometry subclasses (points are always valid!) we
now cache the results of a geometry validity check. Subsequent
checks utilise the cached result wherever possible.

Because QgsGeometry/QgsFeature objects are implicitly shared, this
means that we avoid a *lot* of duplicate validity checks as
features and geometries are thrown around during processing model
execution.
  • Loading branch information
nyalldawson committed Feb 28, 2019
1 parent 16c114b commit 75697d7
Show file tree
Hide file tree
Showing 19 changed files with 149 additions and 25 deletions.
14 changes: 14 additions & 0 deletions python/core/auto_generated/geometry/qgsabstractgeometry.sip.in
Expand Up @@ -607,6 +607,20 @@ Converts the geometry to a specified type.
.. versionadded:: 2.14 .. versionadded:: 2.14
%End %End


virtual bool isValid( QString &error /Out/, int flags = 0 ) const = 0;
%Docstring
Checks validity of the geometry, and returns ``True`` if the geometry is valid.

:param flags: indicates optional flags which control the type of validity checking performed
(corresponding to QgsGeometry.ValidityFlags).

:return: - ``True`` if geometry is valid
- error: will be set to the validity error message


.. versionadded:: 3.8
%End



QgsGeometryPartIterator parts(); QgsGeometryPartIterator parts();
%Docstring %Docstring
Expand Down
2 changes: 2 additions & 0 deletions python/core/auto_generated/geometry/qgscurve.sip.in
Expand Up @@ -172,6 +172,8 @@ Returns a geometry without curves. Caller takes ownership


virtual QgsRectangle boundingBox() const; virtual QgsRectangle boundingBox() const;


virtual bool isValid( QString &error /Out/, int flags = 0 ) const;



virtual double xAt( int index ) const = 0; virtual double xAt( int index ) const = 0;
%Docstring %Docstring
Expand Down
Expand Up @@ -206,6 +206,8 @@ Returns a geometry without curves. Caller takes ownership


virtual QgsPoint vertexAt( QgsVertexId id ) const; virtual QgsPoint vertexAt( QgsVertexId id ) const;


virtual bool isValid( QString &error /Out/, int flags = 0 ) const;



virtual bool addZValue( double zValue = 0 ); virtual bool addZValue( double zValue = 0 );


Expand Down
2 changes: 2 additions & 0 deletions python/core/auto_generated/geometry/qgsmultipoint.sip.in
Expand Up @@ -50,6 +50,8 @@ Multi point geometry collection.


virtual double segmentLength( QgsVertexId startVertex ) const; virtual double segmentLength( QgsVertexId startVertex ) const;


virtual bool isValid( QString &error /Out/, int flags = 0 ) const;





virtual QgsMultiPoint *createEmptyWithSameType() const /Factory/; virtual QgsMultiPoint *createEmptyWithSameType() const /Factory/;
Expand Down
2 changes: 2 additions & 0 deletions python/core/auto_generated/geometry/qgspoint.sip.in
Expand Up @@ -371,6 +371,8 @@ M value is preserved.


virtual QgsAbstractGeometry *boundary() const /Factory/; virtual QgsAbstractGeometry *boundary() const /Factory/;


virtual bool isValid( QString &error /Out/, int flags = 0 ) const;



virtual bool insertVertex( QgsVertexId position, const QgsPoint &vertex ); virtual bool insertVertex( QgsVertexId position, const QgsPoint &vertex );


Expand Down
8 changes: 5 additions & 3 deletions python/core/auto_generated/geometry/qgssurface.sip.in
Expand Up @@ -25,14 +25,16 @@ Ownership is transferred to the caller.
%End %End


virtual QgsRectangle boundingBox() const; virtual QgsRectangle boundingBox() const;
%Docstring
Returns the minimal bounding box for the geometry virtual bool isValid( QString &error /Out/, int flags = 0 ) const;
%End



protected: protected:


virtual void clearCache() const; virtual void clearCache() const;



}; };


/************************************************************************ /************************************************************************
Expand Down
13 changes: 13 additions & 0 deletions src/core/geometry/qgsabstractgeometry.h
Expand Up @@ -577,6 +577,19 @@ class CORE_EXPORT QgsAbstractGeometry
*/ */
virtual bool convertTo( QgsWkbTypes::Type type ); virtual bool convertTo( QgsWkbTypes::Type type );


/**
* Checks validity of the geometry, and returns TRUE if the geometry is valid.
*
* \param error will be set to the validity error message
* \param flags indicates optional flags which control the type of validity checking performed
* (corresponding to QgsGeometry::ValidityFlags).
*
* \returns TRUE if geometry is valid
*
* \since QGIS 3.8
*/
virtual bool isValid( QString &error SIP_OUT, int flags = 0 ) const = 0;

#ifndef SIP_RUN #ifndef SIP_RUN


/** /**
Expand Down
22 changes: 22 additions & 0 deletions src/core/geometry/qgscurve.cpp
Expand Up @@ -21,6 +21,7 @@
#include "qgslinestring.h" #include "qgslinestring.h"
#include "qgspoint.h" #include "qgspoint.h"
#include "qgsmultipoint.h" #include "qgsmultipoint.h"
#include "qgsgeos.h"


bool QgsCurve::operator==( const QgsAbstractGeometry &other ) const bool QgsCurve::operator==( const QgsAbstractGeometry &other ) const
{ {
Expand Down Expand Up @@ -188,6 +189,25 @@ QgsRectangle QgsCurve::boundingBox() const
return mBoundingBox; return mBoundingBox;
} }


bool QgsCurve::isValid( QString &error, int flags ) const
{
if ( flags == 0 && mHasCachedValidity )
{
// use cached validity results
error = mValidityFailureReason;
return error.isEmpty();
}

QgsGeos geos( this );
bool res = geos.isValid( &error, flags & QgsGeometry::FlagAllowSelfTouchingHoles, nullptr );
if ( flags == 0 )
{
mValidityFailureReason = !res ? error : QString();
mHasCachedValidity = true;
}
return res;
}

QPolygonF QgsCurve::asQPolygonF() const QPolygonF QgsCurve::asQPolygonF() const
{ {
const int nb = numPoints(); const int nb = numPoints();
Expand Down Expand Up @@ -224,6 +244,8 @@ QgsCurve::Orientation QgsCurve::orientation() const
void QgsCurve::clearCache() const void QgsCurve::clearCache() const
{ {
mBoundingBox = QgsRectangle(); mBoundingBox = QgsRectangle();
mHasCachedValidity = false;
mValidityFailureReason.clear();
QgsAbstractGeometry::clearCache(); QgsAbstractGeometry::clearCache();
} }


Expand Down
4 changes: 4 additions & 0 deletions src/core/geometry/qgscurve.h
Expand Up @@ -162,6 +162,7 @@ class CORE_EXPORT QgsCurve: public QgsAbstractGeometry
QgsCurve *toCurveType() const override SIP_FACTORY; QgsCurve *toCurveType() const override SIP_FACTORY;


QgsRectangle boundingBox() const override; QgsRectangle boundingBox() const override;
bool isValid( QString &error SIP_OUT, int flags = 0 ) const override;


/** /**
* Returns the x-coordinate of the specified node in the line string. * Returns the x-coordinate of the specified node in the line string.
Expand Down Expand Up @@ -290,6 +291,9 @@ class CORE_EXPORT QgsCurve: public QgsAbstractGeometry
private: private:


mutable QgsRectangle mBoundingBox; mutable QgsRectangle mBoundingBox;

mutable bool mHasCachedValidity = false;
mutable QString mValidityFailureReason;
}; };


#endif // QGSCURVE_H #endif // QGSCURVE_H
10 changes: 1 addition & 9 deletions src/core/geometry/qgsgeometry.cpp
Expand Up @@ -2504,15 +2504,7 @@ bool QgsGeometry::isGeosValid( const QgsGeometry::ValidityFlags flags ) const
return false; return false;
} }


// avoid calling geos for trivial point geometries return d->geometry->isValid( mLastError, static_cast< int >( flags ) );
if ( QgsWkbTypes::geometryType( d->geometry->wkbType() ) == QgsWkbTypes::PointGeometry )
{
return true;
}

QgsGeos geos( d->geometry.get() );
mLastError.clear();
return geos.isValid( &mLastError, flags & FlagAllowSelfTouchingHoles, nullptr );
} }


bool QgsGeometry::isSimple() const bool QgsGeometry::isSimple() const
Expand Down
22 changes: 22 additions & 0 deletions src/core/geometry/qgsgeometrycollection.cpp
Expand Up @@ -26,6 +26,7 @@ email : marco.hugentobler at sourcepole dot com
#include "qgspolygon.h" #include "qgspolygon.h"
#include "qgsmultipolygon.h" #include "qgsmultipolygon.h"
#include "qgswkbptr.h" #include "qgswkbptr.h"
#include "qgsgeos.h"
#include <memory> #include <memory>


QgsGeometryCollection::QgsGeometryCollection() QgsGeometryCollection::QgsGeometryCollection()
Expand Down Expand Up @@ -458,6 +459,8 @@ QgsRectangle QgsGeometryCollection::calculateBoundingBox() const
void QgsGeometryCollection::clearCache() const void QgsGeometryCollection::clearCache() const
{ {
mBoundingBox = QgsRectangle(); mBoundingBox = QgsRectangle();
mHasCachedValidity = false;
mValidityFailureReason.clear();
QgsAbstractGeometry::clearCache(); QgsAbstractGeometry::clearCache();
} }


Expand Down Expand Up @@ -772,6 +775,25 @@ QgsPoint QgsGeometryCollection::vertexAt( QgsVertexId id ) const
return mGeometries[id.part]->vertexAt( id ); return mGeometries[id.part]->vertexAt( id );
} }


bool QgsGeometryCollection::isValid( QString &error, int flags ) const
{
if ( flags == 0 && mHasCachedValidity )
{
// use cached validity results
error = mValidityFailureReason;
return error.isEmpty();
}

QgsGeos geos( this );
bool res = geos.isValid( &error, flags & QgsGeometry::FlagAllowSelfTouchingHoles, nullptr );
if ( flags == 0 )
{
mValidityFailureReason = !res ? error : QString();
mHasCachedValidity = true;
}
return res;
}

bool QgsGeometryCollection::addZValue( double zValue ) bool QgsGeometryCollection::addZValue( double zValue )
{ {
if ( QgsWkbTypes::hasZ( mWkbType ) ) if ( QgsWkbTypes::hasZ( mWkbType ) )
Expand Down
3 changes: 3 additions & 0 deletions src/core/geometry/qgsgeometrycollection.h
Expand Up @@ -206,6 +206,7 @@ class CORE_EXPORT QgsGeometryCollection: public QgsAbstractGeometry
int ringCount( int part = 0 ) const override; int ringCount( int part = 0 ) const override;
int partCount() const override; int partCount() const override;
QgsPoint vertexAt( QgsVertexId id ) const override; QgsPoint vertexAt( QgsVertexId id ) const override;
bool isValid( QString &error SIP_OUT, int flags = 0 ) const override;


bool addZValue( double zValue = 0 ) override; bool addZValue( double zValue = 0 ) override;
bool addMValue( double mValue = 0 ) override; bool addMValue( double mValue = 0 ) override;
Expand Down Expand Up @@ -321,6 +322,8 @@ class CORE_EXPORT QgsGeometryCollection: public QgsAbstractGeometry
private: private:


mutable QgsRectangle mBoundingBox; mutable QgsRectangle mBoundingBox;
mutable bool mHasCachedValidity = false;
mutable QString mValidityFailureReason;
}; };


// clazy:excludeall=qstring-allocations // clazy:excludeall=qstring-allocations
Expand Down
5 changes: 5 additions & 0 deletions src/core/geometry/qgsmultipoint.cpp
Expand Up @@ -182,6 +182,11 @@ double QgsMultiPoint::segmentLength( QgsVertexId ) const
return 0.0; return 0.0;
} }


bool QgsMultiPoint::isValid( QString &, int ) const
{
return true;
}

void QgsMultiPoint::filterVertices( const std::function<bool ( const QgsPoint & )> &filter ) void QgsMultiPoint::filterVertices( const std::function<bool ( const QgsPoint & )> &filter )
{ {
mGeometries.erase( std::remove_if( mGeometries.begin(), mGeometries.end(), // clazy:exclude=detaching-member mGeometries.erase( std::remove_if( mGeometries.begin(), mGeometries.end(), // clazy:exclude=detaching-member
Expand Down
1 change: 1 addition & 0 deletions src/core/geometry/qgsmultipoint.h
Expand Up @@ -45,6 +45,7 @@ class CORE_EXPORT QgsMultiPoint: public QgsGeometryCollection
QgsAbstractGeometry *boundary() const override SIP_FACTORY; QgsAbstractGeometry *boundary() const override SIP_FACTORY;
int vertexNumberFromVertexId( QgsVertexId id ) const override; int vertexNumberFromVertexId( QgsVertexId id ) const override;
double segmentLength( QgsVertexId startVertex ) const override; double segmentLength( QgsVertexId startVertex ) const override;
bool isValid( QString &error SIP_OUT, int flags = 0 ) const override;


#ifndef SIP_RUN #ifndef SIP_RUN
void filterVertices( const std::function< bool( const QgsPoint & ) > &filter ) override; void filterVertices( const std::function< bool( const QgsPoint & ) > &filter ) override;
Expand Down
5 changes: 5 additions & 0 deletions src/core/geometry/qgspoint.cpp
Expand Up @@ -349,6 +349,11 @@ QgsAbstractGeometry *QgsPoint::boundary() const
return nullptr; return nullptr;
} }


bool QgsPoint::isValid( QString &, int ) const
{
return true;
}

bool QgsPoint::insertVertex( QgsVertexId position, const QgsPoint &vertex ) bool QgsPoint::insertVertex( QgsVertexId position, const QgsPoint &vertex )
{ {
Q_UNUSED( position ); Q_UNUSED( position );
Expand Down
1 change: 1 addition & 0 deletions src/core/geometry/qgspoint.h
Expand Up @@ -444,6 +444,7 @@ class CORE_EXPORT QgsPoint: public QgsAbstractGeometry
int nCoordinates() const override; int nCoordinates() const override;
int vertexNumberFromVertexId( QgsVertexId id ) const override; int vertexNumberFromVertexId( QgsVertexId id ) const override;
QgsAbstractGeometry *boundary() const override SIP_FACTORY; QgsAbstractGeometry *boundary() const override SIP_FACTORY;
bool isValid( QString &error SIP_OUT, int flags = 0 ) const override;


//low-level editing //low-level editing
bool insertVertex( QgsVertexId position, const QgsPoint &vertex ) override; bool insertVertex( QgsVertexId position, const QgsPoint &vertex ) override;
Expand Down
29 changes: 28 additions & 1 deletion src/core/geometry/qgssurface.cpp
Expand Up @@ -18,5 +18,32 @@
#include "qgssurface.h" #include "qgssurface.h"
#include "qgspoint.h" #include "qgspoint.h"
#include "qgspolygon.h" #include "qgspolygon.h"

#include "qgsgeos.h"
#include <memory> #include <memory>

bool QgsSurface::isValid( QString &error, int flags ) const
{
if ( flags == 0 && mHasCachedValidity )
{
// use cached validity results
error = mValidityFailureReason;
return error.isEmpty();
}

QgsGeos geos( this );
bool res = geos.isValid( &error, flags & QgsGeometry::FlagAllowSelfTouchingHoles, nullptr );
if ( flags == 0 )
{
mValidityFailureReason = !res ? error : QString();
mHasCachedValidity = true;
}
return res;
}

void QgsSurface::clearCache() const
{
mBoundingBox = QgsRectangle();
mHasCachedValidity = false;
mValidityFailureReason.clear();
QgsAbstractGeometry::clearCache();
}
10 changes: 6 additions & 4 deletions src/core/geometry/qgssurface.h
Expand Up @@ -39,9 +39,6 @@ class CORE_EXPORT QgsSurface: public QgsAbstractGeometry
*/ */
virtual QgsPolygon *surfaceToPolygon() const = 0 SIP_FACTORY; virtual QgsPolygon *surfaceToPolygon() const = 0 SIP_FACTORY;


/**
* Returns the minimal bounding box for the geometry
*/
QgsRectangle boundingBox() const override QgsRectangle boundingBox() const override
{ {
if ( mBoundingBox.isNull() ) if ( mBoundingBox.isNull() )
Expand All @@ -51,6 +48,9 @@ class CORE_EXPORT QgsSurface: public QgsAbstractGeometry
return mBoundingBox; return mBoundingBox;
} }


bool isValid( QString &error SIP_OUT, int flags = 0 ) const override;


#ifndef SIP_RUN #ifndef SIP_RUN


/** /**
Expand All @@ -75,9 +75,11 @@ class CORE_EXPORT QgsSurface: public QgsAbstractGeometry
#endif #endif
protected: protected:


void clearCache() const override { mBoundingBox = QgsRectangle(); QgsAbstractGeometry::clearCache(); } void clearCache() const override;


mutable QgsRectangle mBoundingBox; mutable QgsRectangle mBoundingBox;
mutable bool mHasCachedValidity = false;
mutable QString mValidityFailureReason;
}; };


#endif // QGSSURFACE_H #endif // QGSSURFACE_H
19 changes: 11 additions & 8 deletions tests/src/python/test_qgsgeometry.py
Expand Up @@ -5052,15 +5052,18 @@ def testIsGeosValid(self):
['Polygon((0 3, 3 0, 3 3, 0 0, 0 3))', False, False, 'Self-intersection'], ['Polygon((0 3, 3 0, 3 3, 0 0, 0 3))', False, False, 'Self-intersection'],
] ]
for t in tests: for t in tests:
# run each check 2 times to allow for testing of cached value
g1 = QgsGeometry.fromWkt(t[0]) g1 = QgsGeometry.fromWkt(t[0])
res = g1.isGeosValid() for i in range(2):
self.assertEqual(res, t[1], res = g1.isGeosValid()
"mismatch for {}, expected:\n{}\nGot:\n{}\n".format(t[0], t[1], res)) self.assertEqual(res, t[1],
if not res: "mismatch for {}, expected:\n{}\nGot:\n{}\n".format(t[0], t[1], res))
self.assertEqual(g1.lastError(), t[3], t[0]) if not res:
res = g1.isGeosValid(QgsGeometry.FlagAllowSelfTouchingHoles) self.assertEqual(g1.lastError(), t[3], t[0])
self.assertEqual(res, t[2], for i in range(2):
"mismatch for {}, expected:\n{}\nGot:\n{}\n".format(t[0], t[2], res)) res = g1.isGeosValid(QgsGeometry.FlagAllowSelfTouchingHoles)
self.assertEqual(res, t[2],
"mismatch for {}, expected:\n{}\nGot:\n{}\n".format(t[0], t[2], res))


def testValidateGeometry(self): def testValidateGeometry(self):
tests = [ tests = [
Expand Down

0 comments on commit 75697d7

Please sign in to comment.