Navigation Menu

Skip to content

Commit

Permalink
Super-optimised version of geometry bounding box intersects test
Browse files Browse the repository at this point in the history
Apply some fancy logic to make this test as cheap as possible
to run
  • Loading branch information
nyalldawson committed Mar 11, 2021
1 parent 581d9af commit 7c41012
Show file tree
Hide file tree
Showing 22 changed files with 244 additions and 11 deletions.
10 changes: 10 additions & 0 deletions python/core/auto_generated/geometry/qgsabstractgeometry.sip.in
Expand Up @@ -521,6 +521,16 @@ Returns ``True`` if the geometry is empty
virtual bool hasCurvedSegments() const;
%Docstring
Returns ``True`` if the geometry contains curved segments
%End

virtual bool boundingBoxIntersects( const QgsRectangle &rectangle ) const /HoldGIL/;
%Docstring
Returns ``True`` if the bounding box of this geometry intersects with a ``rectangle``.

Since this test only considers the bounding box of the geometry, is is very fast to
calculate and handles invalid geometries.

.. versionadded:: 3.20
%End

virtual QgsAbstractGeometry *segmentize( double tolerance = M_PI / 180., SegmentationToleranceType toleranceType = MaximumAngle ) const /Factory/;
Expand Down
2 changes: 2 additions & 0 deletions python/core/auto_generated/geometry/qgscompoundcurve.sip.in
Expand Up @@ -82,6 +82,8 @@ of the curve.

virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false );

virtual bool boundingBoxIntersects( const QgsRectangle &rectangle ) const /HoldGIL/;


int nCurves() const /HoldGIL/;
%Docstring
Expand Down
1 change: 1 addition & 0 deletions python/core/auto_generated/geometry/qgscurve.sip.in
Expand Up @@ -280,6 +280,7 @@ Returns the curve's orientation, e.g. clockwise or counter-clockwise.




};

/************************************************************************
Expand Down
2 changes: 2 additions & 0 deletions python/core/auto_generated/geometry/qgscurvepolygon.sip.in
Expand Up @@ -71,6 +71,8 @@ Curve polygon geometry type

virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false );

virtual bool boundingBoxIntersects( const QgsRectangle &rectangle ) const /HoldGIL/;



int numInteriorRings() const /HoldGIL/;
Expand Down
Expand Up @@ -102,6 +102,8 @@ Returns a geometry from within the collection.

virtual int vertexNumberFromVertexId( QgsVertexId id ) const;

virtual bool boundingBoxIntersects( const QgsRectangle &rectangle ) const /HoldGIL/;


void reserve( int size ) /HoldGIL/;
%Docstring
Expand Down
2 changes: 2 additions & 0 deletions python/core/auto_generated/geometry/qgslinestring.sip.in
Expand Up @@ -424,6 +424,8 @@ segment in the line.

virtual bool isClosed() const /HoldGIL/;

virtual bool boundingBoxIntersects( const QgsRectangle &rectangle ) const /HoldGIL/;


QVector< QgsVertexId > collectDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ) const;
%Docstring
Expand Down
2 changes: 2 additions & 0 deletions python/core/auto_generated/geometry/qgspoint.sip.in
Expand Up @@ -436,6 +436,8 @@ Angle undefined. Always returns 0.0

virtual double segmentLength( QgsVertexId startVertex ) const;

virtual bool boundingBoxIntersects( const QgsRectangle &rectangle ) const /HoldGIL/;


virtual bool addZValue( double zValue = 0 );

Expand Down
5 changes: 5 additions & 0 deletions src/core/geometry/qgsabstractgeometry.cpp
Expand Up @@ -307,6 +307,11 @@ bool QgsAbstractGeometry::hasCurvedSegments() const
return false;
}

bool QgsAbstractGeometry::boundingBoxIntersects( const QgsRectangle &rectangle ) const
{
return boundingBox().intersects( rectangle );
}

QgsAbstractGeometry *QgsAbstractGeometry::segmentize( double tolerance, SegmentationToleranceType toleranceType ) const
{
Q_UNUSED( tolerance )
Expand Down
10 changes: 10 additions & 0 deletions src/core/geometry/qgsabstractgeometry.h
Expand Up @@ -537,6 +537,16 @@ class CORE_EXPORT QgsAbstractGeometry
*/
virtual bool hasCurvedSegments() const;

/**
* Returns TRUE if the bounding box of this geometry intersects with a \a rectangle.
*
* Since this test only considers the bounding box of the geometry, is is very fast to
* calculate and handles invalid geometries.
*
* \since QGIS 3.20
*/
virtual bool boundingBoxIntersects( const QgsRectangle &rectangle ) const SIP_HOLDGIL;

/**
* Returns a version of the geometry without curves. Caller takes ownership of
* the returned geometry.
Expand Down
29 changes: 29 additions & 0 deletions src/core/geometry/qgscompoundcurve.cpp
Expand Up @@ -465,6 +465,35 @@ bool QgsCompoundCurve::removeDuplicateNodes( double epsilon, bool useZValues )
return result;
}

bool QgsCompoundCurve::boundingBoxIntersects( const QgsRectangle &rectangle ) const
{
if ( mCurves.empty() )
return false;

// if we already have the bounding box calculated, then this check is trivial!
if ( !mBoundingBox.isNull() )
{
return mBoundingBox.intersects( rectangle );
}

// otherwise loop through each member curve and test the bounding box intersection.
// This gives us a chance to use optimisations which may be present on the individual
// curve subclasses, and at worst it will cause a calculation of the bounding box
// of each individual member curve which we would have to do anyway... (and these
// bounding boxes are cached, so would be reused without additional expense)
for ( const QgsCurve *curve : mCurves )
{
if ( curve->boundingBoxIntersects( rectangle ) )
return true;
}

// even if we don't intersect the bounding box of any member curves, we may still intersect the
// bounding box of the overall compound curve.
// so here we fall back to the non-optimised base class check which has to first calculate
// the overall bounding box of the compound curve..
return QgsAbstractGeometry::boundingBoxIntersects( rectangle );
}

const QgsCurve *QgsCompoundCurve::curveAt( int i ) const
{
if ( i < 0 || i >= mCurves.size() )
Expand Down
1 change: 1 addition & 0 deletions src/core/geometry/qgscompoundcurve.h
Expand Up @@ -72,6 +72,7 @@ class CORE_EXPORT QgsCompoundCurve: public QgsCurve

QgsCompoundCurve *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY;
bool removeDuplicateNodes( double epsilon = 4 * std::numeric_limits<double>::epsilon(), bool useZValues = false ) override;
bool boundingBoxIntersects( const QgsRectangle &rectangle ) const override SIP_HOLDGIL;

/**
* Returns the number of curves in the geometry.
Expand Down
7 changes: 5 additions & 2 deletions src/core/geometry/qgscurve.h
Expand Up @@ -292,10 +292,13 @@ class CORE_EXPORT QgsCurve: public QgsAbstractGeometry
QVector<double> &outX, QVector<double> &outY, QVector<double> &outZ, QVector<double> &outM ) const;
#endif

private:

/**
* Cached bounding box.
*/
mutable QgsRectangle mBoundingBox;

private:

mutable bool mHasCachedValidity = false;
mutable QString mValidityFailureReason;
};
Expand Down
33 changes: 33 additions & 0 deletions src/core/geometry/qgscurvepolygon.cpp
Expand Up @@ -612,6 +612,39 @@ bool QgsCurvePolygon::removeDuplicateNodes( double epsilon, bool useZValues )
return result;
}

bool QgsCurvePolygon::boundingBoxIntersects( const QgsRectangle &rectangle ) const
{
if ( !mExteriorRing && mInteriorRings.empty() )
return false;

// if we already have the bounding box calculated, then this check is trivial!
if ( !mBoundingBox.isNull() )
{
return mBoundingBox.intersects( rectangle );
}

// loop through each ring and test the bounding box intersection.
// This gives us a chance to use optimisations which may be present on the individual
// ring geometry subclasses, and at worst it will cause a calculation of the bounding box
// of each individual ring geometry which we would have to do anyway... (and these
// bounding boxes are cached, so would be reused without additional expense)
if ( mExteriorRing && mExteriorRing->boundingBoxIntersects( rectangle ) )
return true;

for ( const QgsCurve *ring : mInteriorRings )
{
if ( ring->boundingBoxIntersects( rectangle ) )
return true;
}

// even if we don't intersect the bounding box of any rings, we may still intersect the
// bounding box of the overall polygon (we are considering worst case scenario here and
// the polygon is invalid, with rings outside the exterior ring!)
// so here we fall back to the non-optimised base class check which has to first calculate
// the overall bounding box of the polygon..
return QgsSurface::boundingBoxIntersects( rectangle );
}

QgsPolygon *QgsCurvePolygon::toPolygon( double tolerance, SegmentationToleranceType toleranceType ) const
{
std::unique_ptr< QgsPolygon > poly( new QgsPolygon() );
Expand Down
1 change: 1 addition & 0 deletions src/core/geometry/qgscurvepolygon.h
Expand Up @@ -66,6 +66,7 @@ class CORE_EXPORT QgsCurvePolygon: public QgsSurface
QgsAbstractGeometry *boundary() const override SIP_FACTORY;
QgsCurvePolygon *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY;
bool removeDuplicateNodes( double epsilon = 4 * std::numeric_limits<double>::epsilon(), bool useZValues = false ) override;
bool boundingBoxIntersects( const QgsRectangle &rectangle ) const override SIP_HOLDGIL;

//curve polygon interface

Expand Down
11 changes: 2 additions & 9 deletions src/core/geometry/qgsgeometry.cpp
Expand Up @@ -1160,14 +1160,7 @@ bool QgsGeometry::boundingBoxIntersects( const QgsRectangle &rectangle ) const
return false;
}

// optimise trivial case for point intersections
if ( QgsWkbTypes::flatType( d->geometry->wkbType() ) == QgsWkbTypes::Point )
{
const QgsPoint *point = qgsgeometry_cast< const QgsPoint * >( d->geometry.get() );
return rectangle.contains( QgsPointXY( point->x(), point->y() ) );
}

return d->geometry->boundingBox().intersects( rectangle );
return d->geometry->boundingBoxIntersects( rectangle );
}

bool QgsGeometry::boundingBoxIntersects( const QgsGeometry &geometry ) const
Expand All @@ -1177,7 +1170,7 @@ bool QgsGeometry::boundingBoxIntersects( const QgsGeometry &geometry ) const
return false;
}

return d->geometry->boundingBox().intersects( geometry.constGet()->boundingBox() );
return d->geometry->boundingBoxIntersects( geometry.constGet()->boundingBox() );
}

bool QgsGeometry::contains( const QgsPointXY *p ) const
Expand Down
29 changes: 29 additions & 0 deletions src/core/geometry/qgsgeometrycollection.cpp
Expand Up @@ -200,6 +200,35 @@ int QgsGeometryCollection::vertexNumberFromVertexId( QgsVertexId id ) const
return -1; // should not happen
}

bool QgsGeometryCollection::boundingBoxIntersects( const QgsRectangle &rectangle ) const
{
if ( mGeometries.empty() )
return false;

// if we already have the bounding box calculated, then this check is trivial!
if ( !mBoundingBox.isNull() )
{
return mBoundingBox.intersects( rectangle );
}

// otherwise loop through each member geometry and test the bounding box intersection.
// This gives us a chance to use optimisations which may be present on the individual
// geometry subclasses, and at worst it will cause a calculation of the bounding box
// of each individual member geometry which we would have to do anyway... (and these
// bounding boxes are cached, so would be reused without additional expense)
for ( const QgsAbstractGeometry *geometry : mGeometries )
{
if ( geometry->boundingBoxIntersects( rectangle ) )
return true;
}

// even if we don't intersect the bounding box of any member geometries, we may still intersect the
// bounding box of the overall collection.
// so here we fall back to the non-optimised base class check which has to first calculate
// the overall bounding box of the collection..
return QgsAbstractGeometry::boundingBoxIntersects( rectangle );
}

void QgsGeometryCollection::reserve( int size )
{
mGeometries.reserve( size );
Expand Down
1 change: 1 addition & 0 deletions src/core/geometry/qgsgeometrycollection.h
Expand Up @@ -125,6 +125,7 @@ class CORE_EXPORT QgsGeometryCollection: public QgsAbstractGeometry
QgsAbstractGeometry *boundary() const override SIP_FACTORY;
void adjacentVertices( QgsVertexId vertex, QgsVertexId &previousVertex SIP_OUT, QgsVertexId &nextVertex SIP_OUT ) const override;
int vertexNumberFromVertexId( QgsVertexId id ) const override;
bool boundingBoxIntersects( const QgsRectangle &rectangle ) const override SIP_HOLDGIL;

/**
* Attempts to allocate memory for at least \a size geometries.
Expand Down
78 changes: 78 additions & 0 deletions src/core/geometry/qgslinestring.cpp
Expand Up @@ -387,6 +387,84 @@ bool QgsLineString::isClosed() const
return closed;
}

bool QgsLineString::boundingBoxIntersects( const QgsRectangle &rectangle ) const
{
if ( mX.empty() )
return false;

if ( !mBoundingBox.isNull() )
{
return mBoundingBox.intersects( rectangle );
}
const int nb = mX.size();

// We are a little fancy here!
if ( nb > 40 )
{
// if a large number of vertices, take some sample vertices at 1/5th increments through the linestring
// and test whether any are inside the rectangle. Maybe we can shortcut a lot of iterations by doing this!
// (why 1/5th? it's picked so that it works nicely for polygon rings which are almost rectangles, so the vertex extremities
// will fall on approximately these vertex indices)
if ( rectangle.contains( mX.at( 0 ), mY.at( 0 ) ) ||
rectangle.contains( mX.at( static_cast< int >( nb * 0.2 ) ), mY.at( static_cast< int >( nb * 0.2 ) ) ) ||
rectangle.contains( mX.at( static_cast< int >( nb * 0.4 ) ), mY.at( static_cast< int >( nb * 0.4 ) ) ) ||
rectangle.contains( mX.at( static_cast< int >( nb * 0.6 ) ), mY.at( static_cast< int >( nb * 0.6 ) ) ) ||
rectangle.contains( mX.at( static_cast< int >( nb * 0.8 ) ), mY.at( static_cast< int >( nb * 0.8 ) ) ) ||
rectangle.contains( mX.at( nb - 1 ), mY.at( nb - 1 ) ) )
return true;
}

// Be even MORE fancy! Given that bounding box calculation is non-free, cached, and we don't
// already have it, we start performing the bounding box calculation while we are testing whether
// each point falls inside the rectangle. That way if we end up testing the majority of the points
// anyway, we can update the cached bounding box with the results we've calculated along the way
// and save future calls to calculate the bounding box!
double xmin = std::numeric_limits<double>::max();
double ymin = std::numeric_limits<double>::max();
double xmax = -std::numeric_limits<double>::max();
double ymax = -std::numeric_limits<double>::max();

const double *x = mX.constData();
const double *y = mY.constData();
bool foundPointInRectangle = false;
for ( int i = 0; i < nb; ++i )
{
const double px = *x++;
xmin = std::min( xmin, px );
xmax = std::max( xmax, px );
const double py = *y++;
ymin = std::min( ymin, py );
ymax = std::max( ymax, py );

if ( !foundPointInRectangle && rectangle.contains( px, py ) )
{
foundPointInRectangle = true;

// now... we have a choice to make. If we've already looped through the majority of the points
// in this linestring then let's just continue to iterate through the remainder so that we can
// complete the overall bounding box calculation we've already mostly done. If however we're only
// just at the start of iterating the vertices, we shortcut out early and leave the bounding box
// uncalculated
if ( i < nb * 0.5 )
return true;
}
}

// at this stage we now know the overall bounding box of the linestring, so let's cache
// it so we don't ever have to calculate this again. We've done all the hard work anyway!
mBoundingBox = QgsRectangle( xmin, ymin, xmax, ymax, false );

if ( foundPointInRectangle )
return true;

// NOTE: if none of the points in the line actually fell inside the rectangle, it doesn't
// exclude that the OVERALL bounding box of the linestring itself intersects the rectangle!!
// So we fall back to the parent class method which compares the overall bounding box against
// the rectangle... and this will be very cheap now that we've already calculated and cached
// the linestring's bounding box!
return QgsCurve::boundingBoxIntersects( rectangle );
}

QVector< QgsVertexId > QgsLineString::collectDuplicateNodes( double epsilon, bool useZValues ) const
{
QVector< QgsVertexId > res;
Expand Down
1 change: 1 addition & 0 deletions src/core/geometry/qgslinestring.h
Expand Up @@ -591,6 +591,7 @@ class CORE_EXPORT QgsLineString: public QgsCurve
QgsLineString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY;
bool removeDuplicateNodes( double epsilon = 4 * std::numeric_limits<double>::epsilon(), bool useZValues = false ) override;
bool isClosed() const override SIP_HOLDGIL;
bool boundingBoxIntersects( const QgsRectangle &rectangle ) const override SIP_HOLDGIL;

/**
* Returns a list of any duplicate nodes contained in the geometry, within the specified tolerance.
Expand Down
5 changes: 5 additions & 0 deletions src/core/geometry/qgspoint.cpp
Expand Up @@ -537,6 +537,11 @@ double QgsPoint::segmentLength( QgsVertexId ) const
return 0.0;
}

bool QgsPoint::boundingBoxIntersects( const QgsRectangle &rectangle ) const
{
return rectangle.contains( mX, mY );
}

/***************************************************************************
* This class is considered CRITICAL and any change MUST be accompanied with
* full unit tests.
Expand Down
1 change: 1 addition & 0 deletions src/core/geometry/qgspoint.h
Expand Up @@ -529,6 +529,7 @@ class CORE_EXPORT QgsPoint: public QgsAbstractGeometry
QgsPoint vertexAt( QgsVertexId /*id*/ ) const override;
QgsPoint *toCurveType() const override SIP_FACTORY;
double segmentLength( QgsVertexId startVertex ) const override;
bool boundingBoxIntersects( const QgsRectangle &rectangle ) const override SIP_HOLDGIL;

bool addZValue( double zValue = 0 ) override;
bool addMValue( double mValue = 0 ) override;
Expand Down

0 comments on commit 7c41012

Please sign in to comment.