Skip to content

Commit 2655535

Browse files
committed
Add API for QgsSpatialIndex to optionally store feature geometries
This potentially avoids a second expensive feature request after building a spatial index and later needing to re-request features which match spatial index search. It's non-default, as it requires the index to store all feature geometries, so it's more memory expensive.
1 parent 68b4602 commit 2655535

5 files changed

Lines changed: 237 additions & 18 deletions

File tree

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

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,19 @@ QgsSpatialIndex objects are implicitly shared and can be inexpensively copied.
3939
public:
4040

4141

42-
QgsSpatialIndex();
42+
enum Flag
43+
{
44+
FlagStoreFeatureGeometries,
45+
};
46+
typedef QFlags<QgsSpatialIndex::Flag> Flags;
47+
48+
49+
QgsSpatialIndex( QgsSpatialIndex::Flags flags = 0 );
4350
%Docstring
4451
Constructor for QgsSpatialIndex. Creates an empty R-tree index.
4552
%End
4653

47-
explicit QgsSpatialIndex( const QgsFeatureIterator &fi, QgsFeedback *feedback = 0 );
54+
explicit QgsSpatialIndex( const QgsFeatureIterator &fi, QgsFeedback *feedback = 0, QgsSpatialIndex::Flags flags = 0 );
4855
%Docstring
4956
Constructor - creates R-tree and bulk loads it with features from the iterator.
5057
This is much faster approach than creating an empty index and then inserting features one by one.
@@ -56,7 +63,7 @@ that of the spatial index construction.
5663
.. versionadded:: 2.8
5764
%End
5865

59-
explicit QgsSpatialIndex( const QgsFeatureSource &source, QgsFeedback *feedback = 0 );
66+
explicit QgsSpatialIndex( const QgsFeatureSource &source, QgsFeedback *feedback = 0, QgsSpatialIndex::Flags flags = 0 );
6067
%Docstring
6168
Constructor - creates R-tree and bulk loads it with features from the source.
6269
This is much faster approach than creating an empty index and then inserting features one by one.
@@ -152,13 +159,39 @@ by the ``neighbours`` argument.
152159
%End
153160

154161

162+
SIP_PYOBJECT geometry( QgsFeatureId id ) const /TypeHint="QgsGeometry"/;
163+
%Docstring
164+
Returns the stored geometry for the indexed feature with matching ``id``. A KeyError will be raised if no
165+
geometry with the specified feature id exists in the index.
166+
167+
Geometry is only stored if the QgsSpatialIndex was created with the FlagStoreFeatureGeometries flag.
168+
169+
.. versionadded:: 3.6
170+
%End
171+
%MethodCode
172+
std::unique_ptr< QgsGeometry > g = qgis::make_unique< QgsGeometry >( sipCpp->geometry( a0 ) );
173+
if ( g->isNull() )
174+
{
175+
PyErr_SetString( PyExc_KeyError, QStringLiteral( "No geometry with feature id %1 exists in the index." ).arg( a0 ).toUtf8().constData() );
176+
sipIsErr = 1;
177+
}
178+
else
179+
{
180+
sipRes = sipConvertFromType( g.release(), sipType_QgsGeometry, Py_None );
181+
}
182+
%End
183+
184+
155185
int refs() const;
156186
%Docstring
157187
Gets reference count - just for debugging!
158188
%End
159189

160190
};
161191

192+
QFlags<QgsSpatialIndex::Flag> operator|(QgsSpatialIndex::Flag f1, QFlags<QgsSpatialIndex::Flag> f2);
193+
194+
162195
/************************************************************************
163196
* This file has been generated automatically from *
164197
* *

‎src/core/qgsspatialindex.cpp‎

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,10 @@ class QgsFeatureIteratorDataStream : public IDataStream
9898
{
9999
public:
100100
//! constructor - needs to load all data to a vector for later access when bulk loading
101-
explicit QgsFeatureIteratorDataStream( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr )
101+
explicit QgsFeatureIteratorDataStream( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr, QgsSpatialIndex::Flags flags = nullptr )
102102
: mFi( fi )
103103
, mFeedback( feedback )
104+
, mFlags( flags )
104105
{
105106
readNextEntry();
106107
}
@@ -131,6 +132,8 @@ class QgsFeatureIteratorDataStream : public IDataStream
131132
//! sets the stream pointer to the first entry, if possible.
132133
void rewind() override { Q_ASSERT( false && "not available" ); }
133134

135+
QHash< QgsFeatureId, QgsGeometry > geometries;
136+
134137
protected:
135138
void readNextEntry()
136139
{
@@ -142,6 +145,8 @@ class QgsFeatureIteratorDataStream : public IDataStream
142145
if ( QgsSpatialIndex::featureInfo( f, r, id ) )
143146
{
144147
mNextData = new RTree::Data( 0, nullptr, r, id );
148+
if ( mFlags & QgsSpatialIndex::FlagStoreFeatureGeometries )
149+
geometries.insert( f.id(), f.geometry() );
145150
return;
146151
}
147152
}
@@ -151,6 +156,8 @@ class QgsFeatureIteratorDataStream : public IDataStream
151156
QgsFeatureIterator mFi;
152157
RTree::Data *mNextData = nullptr;
153158
QgsFeedback *mFeedback = nullptr;
159+
QgsSpatialIndex::Flags mFlags = nullptr;
160+
154161
};
155162

156163

@@ -163,11 +170,16 @@ class QgsFeatureIteratorDataStream : public IDataStream
163170
class QgsSpatialIndexData : public QSharedData
164171
{
165172
public:
166-
QgsSpatialIndexData()
173+
QgsSpatialIndexData( QgsSpatialIndex::Flags flags )
174+
: mFlags( flags )
167175
{
168176
initTree();
169177
}
170178

179+
QgsSpatialIndex::Flags mFlags = nullptr;
180+
181+
QHash< QgsFeatureId, QgsGeometry > mGeometries;
182+
171183
/**
172184
* Constructor for QgsSpatialIndexData which bulk loads features from the specified feature iterator
173185
* \a fi.
@@ -176,14 +188,19 @@ class QgsSpatialIndexData : public QSharedData
176188
* of \a feedback is not transferred, and callers must take care that the lifetime of feedback exceeds
177189
* that of the spatial index construction.
178190
*/
179-
explicit QgsSpatialIndexData( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr )
191+
explicit QgsSpatialIndexData( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr, QgsSpatialIndex::Flags flags = nullptr )
192+
: mFlags( flags )
180193
{
181-
QgsFeatureIteratorDataStream fids( fi, feedback );
194+
QgsFeatureIteratorDataStream fids( fi, feedback, mFlags );
182195
initTree( &fids );
196+
if ( flags & QgsSpatialIndex::FlagStoreFeatureGeometries )
197+
mGeometries = fids.geometries;
183198
}
184199

185200
QgsSpatialIndexData( const QgsSpatialIndexData &other )
186201
: QSharedData( other )
202+
, mFlags( other.mFlags )
203+
, mGeometries( other.mGeometries )
187204
{
188205
QMutexLocker locker( &other.mMutex );
189206

@@ -241,19 +258,19 @@ class QgsSpatialIndexData : public QSharedData
241258
// -------------------------------------------------------------------------
242259

243260

244-
QgsSpatialIndex::QgsSpatialIndex()
261+
QgsSpatialIndex::QgsSpatialIndex( QgsSpatialIndex::Flags flags )
245262
{
246-
d = new QgsSpatialIndexData;
263+
d = new QgsSpatialIndexData( flags );
247264
}
248265

249-
QgsSpatialIndex::QgsSpatialIndex( const QgsFeatureIterator &fi, QgsFeedback *feedback )
266+
QgsSpatialIndex::QgsSpatialIndex( const QgsFeatureIterator &fi, QgsFeedback *feedback, QgsSpatialIndex::Flags flags )
250267
{
251-
d = new QgsSpatialIndexData( fi, feedback );
268+
d = new QgsSpatialIndexData( fi, feedback, flags );
252269
}
253270

254-
QgsSpatialIndex::QgsSpatialIndex( const QgsFeatureSource &source, QgsFeedback *feedback )
271+
QgsSpatialIndex::QgsSpatialIndex( const QgsFeatureSource &source, QgsFeedback *feedback, QgsSpatialIndex::Flags flags )
255272
{
256-
d = new QgsSpatialIndexData( source.getFeatures( QgsFeatureRequest().setNoAttributes() ), feedback );
273+
d = new QgsSpatialIndexData( source.getFeatures( QgsFeatureRequest().setNoAttributes() ), feedback, flags );
257274
}
258275

259276
QgsSpatialIndex::QgsSpatialIndex( const QgsSpatialIndex &other ) //NOLINT
@@ -306,7 +323,16 @@ bool QgsSpatialIndex::addFeature( QgsFeature &feature, QgsFeatureSink::Flags )
306323
if ( !featureInfo( feature, rect, id ) )
307324
return false;
308325

309-
return addFeature( id, rect );
326+
if ( addFeature( id, rect ) )
327+
{
328+
if ( d->mFlags & QgsSpatialIndex::FlagStoreFeatureGeometries )
329+
{
330+
QMutexLocker locker( &d->mMutex );
331+
d->mGeometries.insert( feature.id(), feature.geometry() );
332+
}
333+
return true;
334+
}
335+
return false;
310336
}
311337

312338
bool QgsSpatialIndex::addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags )
@@ -370,6 +396,8 @@ bool QgsSpatialIndex::deleteFeature( const QgsFeature &f )
370396

371397
QMutexLocker locker( &d->mMutex );
372398
// TODO: handle exceptions
399+
if ( d->mFlags & QgsSpatialIndex::FlagStoreFeatureGeometries )
400+
d->mGeometries.remove( f.id() );
373401
return d->mRTree->deleteData( r, FID_TO_NUMBER( id ) );
374402
}
375403

@@ -400,6 +428,12 @@ QList<QgsFeatureId> QgsSpatialIndex::nearestNeighbor( const QgsPointXY &point, i
400428
return list;
401429
}
402430

431+
QgsGeometry QgsSpatialIndex::geometry( QgsFeatureId id ) const
432+
{
433+
QMutexLocker locker( &d->mMutex );
434+
return d->mGeometries.value( id );
435+
}
436+
403437
QAtomicInt QgsSpatialIndex::refs() const
404438
{
405439
return d->ref;

‎src/core/qgsspatialindex.h‎

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,17 @@ class CORE_EXPORT QgsSpatialIndex : public QgsFeatureSink
7171

7272
/* creation of spatial index */
7373

74+
//! Flags controlling index behavior
75+
enum Flag
76+
{
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.
78+
};
79+
Q_DECLARE_FLAGS( Flags, Flag )
80+
7481
/**
7582
* Constructor for QgsSpatialIndex. Creates an empty R-tree index.
7683
*/
77-
QgsSpatialIndex();
84+
QgsSpatialIndex( QgsSpatialIndex::Flags flags = nullptr );
7885

7986
/**
8087
* Constructor - creates R-tree and bulk loads it with features from the iterator.
@@ -86,7 +93,7 @@ class CORE_EXPORT QgsSpatialIndex : public QgsFeatureSink
8693
*
8794
* \since QGIS 2.8
8895
*/
89-
explicit QgsSpatialIndex( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr );
96+
explicit QgsSpatialIndex( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr, QgsSpatialIndex::Flags flags = nullptr );
9097

9198
/**
9299
* Constructor - creates R-tree and bulk loads it with features from the source.
@@ -99,7 +106,7 @@ class CORE_EXPORT QgsSpatialIndex : public QgsFeatureSink
99106
*
100107
* \since QGIS 3.0
101108
*/
102-
explicit QgsSpatialIndex( const QgsFeatureSource &source, QgsFeedback *feedback = nullptr );
109+
explicit QgsSpatialIndex( const QgsFeatureSource &source, QgsFeedback *feedback = nullptr, QgsSpatialIndex::Flags flags = nullptr );
103110

104111
//! Copy constructor
105112
QgsSpatialIndex( const QgsSpatialIndex &other );
@@ -176,6 +183,42 @@ class CORE_EXPORT QgsSpatialIndex : public QgsFeatureSink
176183
*/
177184
QList<QgsFeatureId> nearestNeighbor( const QgsPointXY &point, int neighbors ) const;
178185

186+
#ifndef SIP_RUN
187+
188+
/**
189+
* Returns the stored geometry for the indexed feature with matching \a id.
190+
*
191+
* Geometry is only stored if the QgsSpatialIndex was created with the FlagStoreFeatureGeometries flag.
192+
*
193+
* \since QGIS 3.6
194+
*/
195+
QgsGeometry geometry( QgsFeatureId id ) const;
196+
197+
#else
198+
199+
/**
200+
* Returns the stored geometry for the indexed feature with matching \a id. A KeyError will be raised if no
201+
* geometry with the specified feature id exists in the index.
202+
*
203+
* Geometry is only stored if the QgsSpatialIndex was created with the FlagStoreFeatureGeometries flag.
204+
*
205+
* \since QGIS 3.6
206+
*/
207+
SIP_PYOBJECT geometry( QgsFeatureId id ) const SIP_TYPEHINT( QgsGeometry );
208+
% MethodCode
209+
std::unique_ptr< QgsGeometry > g = qgis::make_unique< QgsGeometry >( sipCpp->geometry( a0 ) );
210+
if ( g->isNull() )
211+
{
212+
PyErr_SetString( PyExc_KeyError, QStringLiteral( "No geometry with feature id %1 exists in the index." ).arg( a0 ).toUtf8().constData() );
213+
sipIsErr = 1;
214+
}
215+
else
216+
{
217+
sipRes = sipConvertFromType( g.release(), sipType_QgsGeometry, Py_None );
218+
}
219+
% End
220+
#endif
221+
179222
/* debugging */
180223

181224
//! Gets reference count - just for debugging!
@@ -214,4 +257,6 @@ class CORE_EXPORT QgsSpatialIndex : public QgsFeatureSink
214257

215258
};
216259

260+
Q_DECLARE_OPERATORS_FOR_FLAGS( QgsSpatialIndex::Flags )
261+
217262
#endif //QGSSPATIALINDEX_H

‎tests/src/core/testqgsspatialindex.cpp‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <qgsspatialindex.h>
2424
#include <qgsvectordataprovider.h>
2525
#include <qgsvectorlayer.h>
26+
#include "qgslinestring.h"
2627

2728
static QgsFeature _pointFeature( QgsFeatureId id, qreal x, qreal y )
2829
{
@@ -233,6 +234,78 @@ class TestQgsSpatialIndex : public QObject
233234
delete indexInsert;
234235
}
235236

237+
void testRetrieveGeometries()
238+
{
239+
QgsVectorLayer *vl = new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "x" ), QStringLiteral( "memory" ) );
240+
int fid = 0;
241+
for ( int x = 0; x < 10; ++x )
242+
{
243+
QgsFeatureList flist;
244+
for ( int y = 100; y < 110; ++y )
245+
{
246+
QgsFeature f( fid++ );
247+
f.setGeometry( qgis::make_unique< QgsLineString >( QgsPoint( x, y ), QgsPoint( x + 0.5, y - 0.5 ) ) );
248+
flist << f;
249+
}
250+
vl->dataProvider()->addFeatures( flist );
251+
}
252+
253+
// iterator based population
254+
255+
// not storing geometries
256+
QgsSpatialIndex i1( vl->getFeatures() );
257+
QVERIFY( i1.geometry( 1 ).isNull() );
258+
QVERIFY( i1.geometry( 50 ).isNull() );
259+
260+
// storing geometries
261+
QgsSpatialIndex i2( vl->getFeatures(), nullptr, QgsSpatialIndex::FlagStoreFeatureGeometries );
262+
QCOMPARE( i2.geometry( 1 ).asWkt( 1 ), QStringLiteral( "LineString (0 100, 0.5 99.5)" ) );
263+
QCOMPARE( i2.geometry( 50 ).asWkt( 1 ), QStringLiteral( "LineString (4 109, 4.5 108.5)" ) );
264+
265+
// manual population
266+
267+
QgsSpatialIndex i3;
268+
QgsFeatureIterator fi = vl->getFeatures();
269+
QgsFeature f;
270+
while ( fi.nextFeature( f ) )
271+
{
272+
i3.addFeature( f );
273+
}
274+
QVERIFY( i3.geometry( 1 ).isNull() );
275+
QVERIFY( i3.geometry( 50 ).isNull() );
276+
277+
278+
// storing geometries
279+
QgsSpatialIndex i4( QgsSpatialIndex::FlagStoreFeatureGeometries );
280+
fi = vl->getFeatures();
281+
while ( fi.nextFeature( f ) )
282+
{
283+
i4.addFeature( f );
284+
}
285+
QCOMPARE( i4.geometry( 1 ).asWkt( 1 ), QStringLiteral( "LineString (0 100, 0.5 99.5)" ) );
286+
QCOMPARE( i4.geometry( 50 ).asWkt( 1 ), QStringLiteral( "LineString (4 109, 4.5 108.5)" ) );
287+
288+
289+
// not storing geometries
290+
QgsSpatialIndex i5( *vl );
291+
QVERIFY( i5.geometry( 1 ).isNull() );
292+
QVERIFY( i5.geometry( 50 ).isNull() );
293+
294+
// storing geometries
295+
QgsSpatialIndex i6( *vl, nullptr, QgsSpatialIndex::FlagStoreFeatureGeometries );
296+
QCOMPARE( i6.geometry( 1 ).asWkt( 1 ), QStringLiteral( "LineString (0 100, 0.5 99.5)" ) );
297+
QCOMPARE( i6.geometry( 50 ).asWkt( 1 ), QStringLiteral( "LineString (4 109, 4.5 108.5)" ) );
298+
299+
f = vl->getFeature( 1 );
300+
QVERIFY( i6.deleteFeature( f ) );
301+
QVERIFY( i6.geometry( 1 ).isNull() );
302+
QCOMPARE( i6.geometry( 50 ).asWkt( 1 ), QStringLiteral( "LineString (4 109, 4.5 108.5)" ) );
303+
f = vl->getFeature( 50 );
304+
305+
QVERIFY( i6.deleteFeature( f ) );
306+
QVERIFY( i6.geometry( 50 ).isNull() );
307+
}
308+
236309
};
237310

238311
QGSTEST_MAIN( TestQgsSpatialIndex )

0 commit comments

Comments
 (0)