Skip to content

Commit 75697d7

Browse files
committed
Cache validity check results
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.
1 parent 16c114b commit 75697d7

19 files changed

+149
-25
lines changed

python/core/auto_generated/geometry/qgsabstractgeometry.sip.in

+14
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,20 @@ Converts the geometry to a specified type.
607607
.. versionadded:: 2.14
608608
%End
609609

610+
virtual bool isValid( QString &error /Out/, int flags = 0 ) const = 0;
611+
%Docstring
612+
Checks validity of the geometry, and returns ``True`` if the geometry is valid.
613+
614+
:param flags: indicates optional flags which control the type of validity checking performed
615+
(corresponding to QgsGeometry.ValidityFlags).
616+
617+
:return: - ``True`` if geometry is valid
618+
- error: will be set to the validity error message
619+
620+
621+
.. versionadded:: 3.8
622+
%End
623+
610624

611625
QgsGeometryPartIterator parts();
612626
%Docstring

python/core/auto_generated/geometry/qgscurve.sip.in

+2
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ Returns a geometry without curves. Caller takes ownership
172172

173173
virtual QgsRectangle boundingBox() const;
174174

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

176178
virtual double xAt( int index ) const = 0;
177179
%Docstring

python/core/auto_generated/geometry/qgsgeometrycollection.sip.in

+2
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ Returns a geometry without curves. Caller takes ownership
206206

207207
virtual QgsPoint vertexAt( QgsVertexId id ) const;
208208

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

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

python/core/auto_generated/geometry/qgsmultipoint.sip.in

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ Multi point geometry collection.
5050

5151
virtual double segmentLength( QgsVertexId startVertex ) const;
5252

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

5456

5557
virtual QgsMultiPoint *createEmptyWithSameType() const /Factory/;

python/core/auto_generated/geometry/qgspoint.sip.in

+2
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ M value is preserved.
371371

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

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

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

python/core/auto_generated/geometry/qgssurface.sip.in

+5-3
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@ Ownership is transferred to the caller.
2525
%End
2626

2727
virtual QgsRectangle boundingBox() const;
28-
%Docstring
29-
Returns the minimal bounding box for the geometry
30-
%End
28+
29+
virtual bool isValid( QString &error /Out/, int flags = 0 ) const;
30+
31+
3132

3233
protected:
3334

3435
virtual void clearCache() const;
3536

37+
3638
};
3739

3840
/************************************************************************

src/core/geometry/qgsabstractgeometry.h

+13
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,19 @@ class CORE_EXPORT QgsAbstractGeometry
577577
*/
578578
virtual bool convertTo( QgsWkbTypes::Type type );
579579

580+
/**
581+
* Checks validity of the geometry, and returns TRUE if the geometry is valid.
582+
*
583+
* \param error will be set to the validity error message
584+
* \param flags indicates optional flags which control the type of validity checking performed
585+
* (corresponding to QgsGeometry::ValidityFlags).
586+
*
587+
* \returns TRUE if geometry is valid
588+
*
589+
* \since QGIS 3.8
590+
*/
591+
virtual bool isValid( QString &error SIP_OUT, int flags = 0 ) const = 0;
592+
580593
#ifndef SIP_RUN
581594

582595
/**

src/core/geometry/qgscurve.cpp

+22
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "qgslinestring.h"
2222
#include "qgspoint.h"
2323
#include "qgsmultipoint.h"
24+
#include "qgsgeos.h"
2425

2526
bool QgsCurve::operator==( const QgsAbstractGeometry &other ) const
2627
{
@@ -188,6 +189,25 @@ QgsRectangle QgsCurve::boundingBox() const
188189
return mBoundingBox;
189190
}
190191

192+
bool QgsCurve::isValid( QString &error, int flags ) const
193+
{
194+
if ( flags == 0 && mHasCachedValidity )
195+
{
196+
// use cached validity results
197+
error = mValidityFailureReason;
198+
return error.isEmpty();
199+
}
200+
201+
QgsGeos geos( this );
202+
bool res = geos.isValid( &error, flags & QgsGeometry::FlagAllowSelfTouchingHoles, nullptr );
203+
if ( flags == 0 )
204+
{
205+
mValidityFailureReason = !res ? error : QString();
206+
mHasCachedValidity = true;
207+
}
208+
return res;
209+
}
210+
191211
QPolygonF QgsCurve::asQPolygonF() const
192212
{
193213
const int nb = numPoints();
@@ -224,6 +244,8 @@ QgsCurve::Orientation QgsCurve::orientation() const
224244
void QgsCurve::clearCache() const
225245
{
226246
mBoundingBox = QgsRectangle();
247+
mHasCachedValidity = false;
248+
mValidityFailureReason.clear();
227249
QgsAbstractGeometry::clearCache();
228250
}
229251

src/core/geometry/qgscurve.h

+4
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ class CORE_EXPORT QgsCurve: public QgsAbstractGeometry
162162
QgsCurve *toCurveType() const override SIP_FACTORY;
163163

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

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

292293
mutable QgsRectangle mBoundingBox;
294+
295+
mutable bool mHasCachedValidity = false;
296+
mutable QString mValidityFailureReason;
293297
};
294298

295299
#endif // QGSCURVE_H

src/core/geometry/qgsgeometry.cpp

+1-9
Original file line numberDiff line numberDiff line change
@@ -2504,15 +2504,7 @@ bool QgsGeometry::isGeosValid( const QgsGeometry::ValidityFlags flags ) const
25042504
return false;
25052505
}
25062506

2507-
// avoid calling geos for trivial point geometries
2508-
if ( QgsWkbTypes::geometryType( d->geometry->wkbType() ) == QgsWkbTypes::PointGeometry )
2509-
{
2510-
return true;
2511-
}
2512-
2513-
QgsGeos geos( d->geometry.get() );
2514-
mLastError.clear();
2515-
return geos.isValid( &mLastError, flags & FlagAllowSelfTouchingHoles, nullptr );
2507+
return d->geometry->isValid( mLastError, static_cast< int >( flags ) );
25162508
}
25172509

25182510
bool QgsGeometry::isSimple() const

src/core/geometry/qgsgeometrycollection.cpp

+22
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ email : marco.hugentobler at sourcepole dot com
2626
#include "qgspolygon.h"
2727
#include "qgsmultipolygon.h"
2828
#include "qgswkbptr.h"
29+
#include "qgsgeos.h"
2930
#include <memory>
3031

3132
QgsGeometryCollection::QgsGeometryCollection()
@@ -458,6 +459,8 @@ QgsRectangle QgsGeometryCollection::calculateBoundingBox() const
458459
void QgsGeometryCollection::clearCache() const
459460
{
460461
mBoundingBox = QgsRectangle();
462+
mHasCachedValidity = false;
463+
mValidityFailureReason.clear();
461464
QgsAbstractGeometry::clearCache();
462465
}
463466

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

778+
bool QgsGeometryCollection::isValid( QString &error, int flags ) const
779+
{
780+
if ( flags == 0 && mHasCachedValidity )
781+
{
782+
// use cached validity results
783+
error = mValidityFailureReason;
784+
return error.isEmpty();
785+
}
786+
787+
QgsGeos geos( this );
788+
bool res = geos.isValid( &error, flags & QgsGeometry::FlagAllowSelfTouchingHoles, nullptr );
789+
if ( flags == 0 )
790+
{
791+
mValidityFailureReason = !res ? error : QString();
792+
mHasCachedValidity = true;
793+
}
794+
return res;
795+
}
796+
775797
bool QgsGeometryCollection::addZValue( double zValue )
776798
{
777799
if ( QgsWkbTypes::hasZ( mWkbType ) )

src/core/geometry/qgsgeometrycollection.h

+3
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ class CORE_EXPORT QgsGeometryCollection: public QgsAbstractGeometry
206206
int ringCount( int part = 0 ) const override;
207207
int partCount() const override;
208208
QgsPoint vertexAt( QgsVertexId id ) const override;
209+
bool isValid( QString &error SIP_OUT, int flags = 0 ) const override;
209210

210211
bool addZValue( double zValue = 0 ) override;
211212
bool addMValue( double mValue = 0 ) override;
@@ -321,6 +322,8 @@ class CORE_EXPORT QgsGeometryCollection: public QgsAbstractGeometry
321322
private:
322323

323324
mutable QgsRectangle mBoundingBox;
325+
mutable bool mHasCachedValidity = false;
326+
mutable QString mValidityFailureReason;
324327
};
325328

326329
// clazy:excludeall=qstring-allocations

src/core/geometry/qgsmultipoint.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ double QgsMultiPoint::segmentLength( QgsVertexId ) const
182182
return 0.0;
183183
}
184184

185+
bool QgsMultiPoint::isValid( QString &, int ) const
186+
{
187+
return true;
188+
}
189+
185190
void QgsMultiPoint::filterVertices( const std::function<bool ( const QgsPoint & )> &filter )
186191
{
187192
mGeometries.erase( std::remove_if( mGeometries.begin(), mGeometries.end(), // clazy:exclude=detaching-member

src/core/geometry/qgsmultipoint.h

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class CORE_EXPORT QgsMultiPoint: public QgsGeometryCollection
4545
QgsAbstractGeometry *boundary() const override SIP_FACTORY;
4646
int vertexNumberFromVertexId( QgsVertexId id ) const override;
4747
double segmentLength( QgsVertexId startVertex ) const override;
48+
bool isValid( QString &error SIP_OUT, int flags = 0 ) const override;
4849

4950
#ifndef SIP_RUN
5051
void filterVertices( const std::function< bool( const QgsPoint & ) > &filter ) override;

src/core/geometry/qgspoint.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,11 @@ QgsAbstractGeometry *QgsPoint::boundary() const
349349
return nullptr;
350350
}
351351

352+
bool QgsPoint::isValid( QString &, int ) const
353+
{
354+
return true;
355+
}
356+
352357
bool QgsPoint::insertVertex( QgsVertexId position, const QgsPoint &vertex )
353358
{
354359
Q_UNUSED( position );

src/core/geometry/qgspoint.h

+1
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ class CORE_EXPORT QgsPoint: public QgsAbstractGeometry
444444
int nCoordinates() const override;
445445
int vertexNumberFromVertexId( QgsVertexId id ) const override;
446446
QgsAbstractGeometry *boundary() const override SIP_FACTORY;
447+
bool isValid( QString &error SIP_OUT, int flags = 0 ) const override;
447448

448449
//low-level editing
449450
bool insertVertex( QgsVertexId position, const QgsPoint &vertex ) override;

src/core/geometry/qgssurface.cpp

+28-1
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,32 @@
1818
#include "qgssurface.h"
1919
#include "qgspoint.h"
2020
#include "qgspolygon.h"
21-
21+
#include "qgsgeos.h"
2222
#include <memory>
23+
24+
bool QgsSurface::isValid( QString &error, int flags ) const
25+
{
26+
if ( flags == 0 && mHasCachedValidity )
27+
{
28+
// use cached validity results
29+
error = mValidityFailureReason;
30+
return error.isEmpty();
31+
}
32+
33+
QgsGeos geos( this );
34+
bool res = geos.isValid( &error, flags & QgsGeometry::FlagAllowSelfTouchingHoles, nullptr );
35+
if ( flags == 0 )
36+
{
37+
mValidityFailureReason = !res ? error : QString();
38+
mHasCachedValidity = true;
39+
}
40+
return res;
41+
}
42+
43+
void QgsSurface::clearCache() const
44+
{
45+
mBoundingBox = QgsRectangle();
46+
mHasCachedValidity = false;
47+
mValidityFailureReason.clear();
48+
QgsAbstractGeometry::clearCache();
49+
}

src/core/geometry/qgssurface.h

+6-4
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ class CORE_EXPORT QgsSurface: public QgsAbstractGeometry
3939
*/
4040
virtual QgsPolygon *surfaceToPolygon() const = 0 SIP_FACTORY;
4141

42-
/**
43-
* Returns the minimal bounding box for the geometry
44-
*/
4542
QgsRectangle boundingBox() const override
4643
{
4744
if ( mBoundingBox.isNull() )
@@ -51,6 +48,9 @@ class CORE_EXPORT QgsSurface: public QgsAbstractGeometry
5148
return mBoundingBox;
5249
}
5350

51+
bool isValid( QString &error SIP_OUT, int flags = 0 ) const override;
52+
53+
5454
#ifndef SIP_RUN
5555

5656
/**
@@ -75,9 +75,11 @@ class CORE_EXPORT QgsSurface: public QgsAbstractGeometry
7575
#endif
7676
protected:
7777

78-
void clearCache() const override { mBoundingBox = QgsRectangle(); QgsAbstractGeometry::clearCache(); }
78+
void clearCache() const override;
7979

8080
mutable QgsRectangle mBoundingBox;
81+
mutable bool mHasCachedValidity = false;
82+
mutable QString mValidityFailureReason;
8183
};
8284

8385
#endif // QGSSURFACE_H

tests/src/python/test_qgsgeometry.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -5052,15 +5052,18 @@ def testIsGeosValid(self):
50525052
['Polygon((0 3, 3 0, 3 3, 0 0, 0 3))', False, False, 'Self-intersection'],
50535053
]
50545054
for t in tests:
5055+
# run each check 2 times to allow for testing of cached value
50555056
g1 = QgsGeometry.fromWkt(t[0])
5056-
res = g1.isGeosValid()
5057-
self.assertEqual(res, t[1],
5058-
"mismatch for {}, expected:\n{}\nGot:\n{}\n".format(t[0], t[1], res))
5059-
if not res:
5060-
self.assertEqual(g1.lastError(), t[3], t[0])
5061-
res = g1.isGeosValid(QgsGeometry.FlagAllowSelfTouchingHoles)
5062-
self.assertEqual(res, t[2],
5063-
"mismatch for {}, expected:\n{}\nGot:\n{}\n".format(t[0], t[2], res))
5057+
for i in range(2):
5058+
res = g1.isGeosValid()
5059+
self.assertEqual(res, t[1],
5060+
"mismatch for {}, expected:\n{}\nGot:\n{}\n".format(t[0], t[1], res))
5061+
if not res:
5062+
self.assertEqual(g1.lastError(), t[3], t[0])
5063+
for i in range(2):
5064+
res = g1.isGeosValid(QgsGeometry.FlagAllowSelfTouchingHoles)
5065+
self.assertEqual(res, t[2],
5066+
"mismatch for {}, expected:\n{}\nGot:\n{}\n".format(t[0], t[2], res))
50645067

50655068
def testValidateGeometry(self):
50665069
tests = [

0 commit comments

Comments
 (0)