Skip to content

Commit c8a4dff

Browse files
committedFeb 19, 2019
Add maximum search distance parameter to QgsSpatialIndex::nearestNeighbor
1 parent 6737480 commit c8a4dff

File tree

4 files changed

+99
-31
lines changed

4 files changed

+99
-31
lines changed
 

‎python/core/auto_generated/qgsspatialindex.sip.in

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,16 +147,24 @@ Returns a list of features with a bounding box which intersects the specified ``
147147
when required.
148148
%End
149149

150-
QList<QgsFeatureId> nearestNeighbor( const QgsPointXY &point, int neighbors ) const;
150+
QList<QgsFeatureId> nearestNeighbor( const QgsPointXY &point, int neighbors = 1, double maxDistance = 0 ) const;
151151
%Docstring
152-
Returns nearest neighbors to a ``point``. The number of neighbours returned is specified
153-
by the ``neighbours`` argument.
152+
Returns nearest neighbors to a ``point``. The number of neighbors returned is specified
153+
by the ``neighbors`` argument.
154+
155+
If the ``maxDistance`` argument is greater than 0, then only features within the specified
156+
distance of ``point`` will be considered.
157+
158+
Note that in some cases the number of returned features may differ from the requested
159+
number of ``neighbors``. E.g. if not enough features exist within the ``maxDistance`` of the
160+
search point. If multiple features are equidistant from the search ``point`` then the
161+
number of returned feature IDs may exceed ``neighbors``.
154162

155163
.. warning::
156164

157165
If this QgsSpatialIndex object was not constructed with the FlagStoreFeatureGeometries flag,
158-
then the nearest neighbour test is performed based on the feature bounding boxes ONLY, so for non-point
159-
geometry features this method is not guaranteed to return the actual closest neighbours.
166+
then the nearest neighbor test is performed based on the feature bounding boxes ONLY, so for non-point
167+
geometry features this method is not guaranteed to return the actual closest neighbors.
160168
%End
161169

162170

‎src/core/qgsspatialindex.cpp

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -88,29 +88,60 @@ class QgsSpatialIndexCopyVisitor : public SpatialIndex::IVisitor
8888
};
8989

9090
///@cond PRIVATE
91-
class QgsNearestNeighbourComparator : public INearestNeighborComparator
91+
class QgsNearestNeighborComparator : public INearestNeighborComparator
9292
{
9393
public:
9494

95-
QgsNearestNeighbourComparator( const QHash< QgsFeatureId, QgsGeometry > &geometries, const QgsPointXY &point )
95+
QgsNearestNeighborComparator( const QHash< QgsFeatureId, QgsGeometry > *geometries, const QgsPointXY &point, double maxDistance )
9696
: mGeometries( geometries )
9797
, mPoint( QgsGeometry::fromPointXY( point ) )
98+
, mMaxDistance( maxDistance )
9899
{
99100
}
100101

101-
QHash< QgsFeatureId, QgsGeometry > mGeometries;
102+
const QHash< QgsFeatureId, QgsGeometry > *mGeometries = nullptr;
102103
QgsGeometry mPoint;
104+
double mMaxDistance = 0;
105+
QSet< QgsFeatureId > mFeaturesOutsideMaxDistance;
103106

104-
double getMinimumDistance( const IShape &, const IShape & ) override
107+
double getMinimumDistance( const IShape &query, const IShape &entry ) override
105108
{
106-
Q_ASSERT( false ); // not implemented!
107-
return 0;
109+
return query.getMinimumDistance( entry );
108110
}
109111

110-
double getMinimumDistance( const IShape &, const IData &data ) override
112+
double getMinimumDistance( const IShape &query, const IData &data ) override
111113
{
112-
QgsGeometry other = mGeometries.value( data.getIdentifier() );
113-
return other.distance( mPoint );
114+
// start with the default implementation, which gives distance to bounding box only
115+
IShape *pS;
116+
data.getShape( &pS );
117+
double dist = query.getMinimumDistance( *pS );
118+
delete pS;
119+
120+
// if doing exact distance search, AND either no max distance specified OR the
121+
// distance to the bounding box is less than the max distance, calculate the exact
122+
// distance now.
123+
// (note: if bounding box is already greater than the distance away then max distance, there's no
124+
// point doing this more expensive calculation, since we can't possibly use this feature anyway!)
125+
if ( mGeometries && ( mMaxDistance <= 0.0 || dist <= mMaxDistance ) )
126+
{
127+
QgsGeometry other = mGeometries->value( data.getIdentifier() );
128+
dist = other.distance( mPoint );
129+
}
130+
131+
if ( mMaxDistance > 0 && dist > mMaxDistance )
132+
{
133+
// feature is outside of maximum distance threshold. Flag it,
134+
// but "trick" libspatialindex into considering it as just outside
135+
// our max distance region. This means if there's no other closer features (i.e.,
136+
// within our actual maximum search distance), the libspatialindex
137+
// nearest neighbor test will use this feature and not needlessly continue hunting
138+
// through the remaining more distant features in the index.
139+
// TODO: add proper API to libspatialindex to allow a maximum search distance in
140+
// nearest neighbor tests
141+
mFeaturesOutsideMaxDistance.insert( data.getIdentifier() );
142+
return mMaxDistance + 0.00000001;
143+
}
144+
return dist;
114145
}
115146
};
116147

@@ -442,7 +473,7 @@ QList<QgsFeatureId> QgsSpatialIndex::intersects( const QgsRectangle &rect ) cons
442473
return list;
443474
}
444475

445-
QList<QgsFeatureId> QgsSpatialIndex::nearestNeighbor( const QgsPointXY &point, int neighbors ) const
476+
QList<QgsFeatureId> QgsSpatialIndex::nearestNeighbor( const QgsPointXY &point, const int neighbors, const double maxDistance ) const
446477
{
447478
QList<QgsFeatureId> list;
448479
QgisVisitor visitor( list );
@@ -451,15 +482,18 @@ QList<QgsFeatureId> QgsSpatialIndex::nearestNeighbor( const QgsPointXY &point, i
451482
Point p( pt, 2 );
452483

453484
QMutexLocker locker( &d->mMutex );
454-
if ( d->mFlags & QgsSpatialIndex::FlagStoreFeatureGeometries )
455-
{
456-
QgsNearestNeighbourComparator nnc( d->mGeometries, point );
457-
d->mRTree->nearestNeighborQuery( neighbors, p, visitor, nnc );
458-
}
459-
else
485+
QgsNearestNeighborComparator nnc( d->mFlags & QgsSpatialIndex::FlagStoreFeatureGeometries ? &d->mGeometries : nullptr,
486+
point, maxDistance );
487+
d->mRTree->nearestNeighborQuery( neighbors, p, visitor, nnc );
488+
489+
if ( maxDistance > 0 )
460490
{
461-
// simple bounding box search - meaningless for non-points
462-
d->mRTree->nearestNeighborQuery( neighbors, p, visitor );
491+
// trim features outside of max distance
492+
list.erase( std::remove_if( list.begin(), list.end(),
493+
[&nnc]( QgsFeatureId id )
494+
{
495+
return nnc.mFeaturesOutsideMaxDistance.contains( id );
496+
} ), list.end() );
463497
}
464498

465499
return list;

‎src/core/qgsspatialindex.h

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class CORE_EXPORT QgsSpatialIndex : public QgsFeatureSink
7474
//! Flags controlling index behavior
7575
enum Flag
7676
{
77-
FlagStoreFeatureGeometries = 1 << 0, //!< Indicates that the spatial index should also store feature geometries. This requires more memory, but can speed up operations by avoiding additional requests to data providers to fetch matching feature geometries. Additionally, it is required for non-bounding box nearest neighbour searches.
77+
FlagStoreFeatureGeometries = 1 << 0, //!< Indicates that the spatial index should also store feature geometries. This requires more memory, but can speed up operations by avoiding additional requests to data providers to fetch matching feature geometries. Additionally, it is required for non-bounding box nearest neighbor searches.
7878
};
7979
Q_DECLARE_FLAGS( Flags, Flag )
8080

@@ -175,14 +175,22 @@ class CORE_EXPORT QgsSpatialIndex : public QgsFeatureSink
175175
QList<QgsFeatureId> intersects( const QgsRectangle &rectangle ) const;
176176

177177
/**
178-
* Returns nearest neighbors to a \a point. The number of neighbours returned is specified
179-
* by the \a neighbours argument.
178+
* Returns nearest neighbors to a \a point. The number of neighbors returned is specified
179+
* by the \a neighbors argument.
180+
*
181+
* If the \a maxDistance argument is greater than 0, then only features within the specified
182+
* distance of \a point will be considered.
183+
*
184+
* Note that in some cases the number of returned features may differ from the requested
185+
* number of \a neighbors. E.g. if not enough features exist within the \a maxDistance of the
186+
* search point. If multiple features are equidistant from the search \a point then the
187+
* number of returned feature IDs may exceed \a neighbors.
180188
*
181189
* \warning If this QgsSpatialIndex object was not constructed with the FlagStoreFeatureGeometries flag,
182-
* then the nearest neighbour test is performed based on the feature bounding boxes ONLY, so for non-point
183-
* geometry features this method is not guaranteed to return the actual closest neighbours.
190+
* then the nearest neighbor test is performed based on the feature bounding boxes ONLY, so for non-point
191+
* geometry features this method is not guaranteed to return the actual closest neighbors.
184192
*/
185-
QList<QgsFeatureId> nearestNeighbor( const QgsPointXY &point, int neighbors ) const;
193+
QList<QgsFeatureId> nearestNeighbor( const QgsPointXY &point, int neighbors = 1, double maxDistance = 0 ) const;
186194

187195
#ifndef SIP_RUN
188196

‎tests/src/core/testqgsspatialindex.cpp

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,15 +315,33 @@ class TestQgsSpatialIndex : public QObject
315315
f1.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString(1 1, 3 1, 3 3)" ) ) );
316316
QgsFeature f2( 2 );
317317
f2.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString(0 1, 0 3)" ) ) );
318+
QgsFeature f3( 3 );
319+
f3.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString(0 4, 1 5, 3 3)" ) ) );
318320
i.addFeature( f1 );
319321
i2.addFeature( f1 );
320322
i.addFeature( f2 );
321323
i2.addFeature( f2 );
324+
i.addFeature( f3 );
325+
i2.addFeature( f3 );
322326

323327
// i does not store feature geometries, so nearest neighbour search uses bounding box only
324-
QCOMPARE( i.nearestNeighbor( QgsPointXY( 1, 3 ), 1 ), QList< QgsFeatureId >() << 1 );
328+
QCOMPARE( i.nearestNeighbor( QgsPointXY( 1, 2.9 ), 1 ), QList< QgsFeatureId >() << 1 );
329+
QCOMPARE( i.nearestNeighbor( QgsPointXY( 1, 2.9 ), 2 ), QList< QgsFeatureId >() << 1 << 3 );
325330
// i2 does store feature geometries, so nearest neighbour is exact
326-
QCOMPARE( i2.nearestNeighbor( QgsPointXY( 1, 3 ), 1 ), QList< QgsFeatureId >() << 2 );
331+
QCOMPARE( i2.nearestNeighbor( QgsPointXY( 1, 2.9 ), 1 ), QList< QgsFeatureId >() << 2 );
332+
QCOMPARE( i2.nearestNeighbor( QgsPointXY( 1, 2.9 ), 2 ), QList< QgsFeatureId >() << 2 << 3 );
333+
334+
// with maximum distance
335+
QCOMPARE( i.nearestNeighbor( QgsPointXY( 1, 2.9 ), 1, 0.5 ), QList< QgsFeatureId >() << 1 );
336+
QCOMPARE( i2.nearestNeighbor( QgsPointXY( 1, 2.9 ), 1, 0.5 ), QList< QgsFeatureId >() );
337+
QCOMPARE( i.nearestNeighbor( QgsPointXY( 1, 2.9 ), 2, 0.5 ), QList< QgsFeatureId >() << 1 << 3 );
338+
QCOMPARE( i2.nearestNeighbor( QgsPointXY( 1, 2.9 ), 2, 0.5 ), QList< QgsFeatureId >() );
339+
QCOMPARE( i.nearestNeighbor( QgsPointXY( 1, 2.9 ), 1, 1.1 ), QList< QgsFeatureId >() << 1 );
340+
QCOMPARE( i2.nearestNeighbor( QgsPointXY( 1, 2.9 ), 1, 1.1 ), QList< QgsFeatureId >() << 2 );
341+
QCOMPARE( i.nearestNeighbor( QgsPointXY( 1, 2.9 ), 2, 1.1 ), QList< QgsFeatureId >() << 1 << 3 );
342+
QCOMPARE( i2.nearestNeighbor( QgsPointXY( 1, 2.9 ), 2, 1.1 ), QList< QgsFeatureId >() << 2 );
343+
QCOMPARE( i.nearestNeighbor( QgsPointXY( 1, 2.9 ), 2, 2 ), QList< QgsFeatureId >() << 1 << 3 );
344+
QCOMPARE( i2.nearestNeighbor( QgsPointXY( 1, 2.9 ), 2, 2 ), QList< QgsFeatureId >() << 2 << 3 );
327345

328346
}
329347

0 commit comments

Comments
 (0)
Please sign in to comment.