From fe6abba251c5a1b7df4de112f40b6368486195f8 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 1 Jan 2020 10:54:33 +1000 Subject: [PATCH] [api] Add constructor for QgsSpatialIndex which allows for a callback function during bulk index load from a feature iterator Allows single-iteration of a source for dual purposes simultaneously, e.g. doing other feature-based operations while still gaining the full advantage of the bulk loaded spatial index without having to do multiple feature iterations --- .../auto_generated/qgsspatialindex.sip.in | 1 + src/core/qgsspatialindex.cpp | 24 ++++++++-- src/core/qgsspatialindex.h | 17 +++++++ tests/src/core/testqgsspatialindex.cpp | 45 +++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) diff --git a/python/core/auto_generated/qgsspatialindex.sip.in b/python/core/auto_generated/qgsspatialindex.sip.in index ceac9b0ed42..2444bbe550b 100644 --- a/python/core/auto_generated/qgsspatialindex.sip.in +++ b/python/core/auto_generated/qgsspatialindex.sip.in @@ -63,6 +63,7 @@ that of the spatial index construction. .. versionadded:: 2.8 %End + explicit QgsSpatialIndex( const QgsFeatureSource &source, QgsFeedback *feedback = 0, QgsSpatialIndex::Flags flags = 0 ); %Docstring Constructor - creates R-tree and bulk loads it with features from the source. diff --git a/src/core/qgsspatialindex.cpp b/src/core/qgsspatialindex.cpp index 21e62fc5a66..f3e58eccb68 100644 --- a/src/core/qgsspatialindex.cpp +++ b/src/core/qgsspatialindex.cpp @@ -163,10 +163,12 @@ class QgsFeatureIteratorDataStream : public IDataStream { public: //! constructor - needs to load all data to a vector for later access when bulk loading - explicit QgsFeatureIteratorDataStream( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr, QgsSpatialIndex::Flags flags = nullptr ) + explicit QgsFeatureIteratorDataStream( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr, QgsSpatialIndex::Flags flags = nullptr, + const std::function< bool( const QgsFeature & ) > *callback = nullptr ) : mFi( fi ) , mFeedback( feedback ) , mFlags( flags ) + , mCallback( callback ) { readNextEntry(); } @@ -207,6 +209,15 @@ class QgsFeatureIteratorDataStream : public IDataStream QgsFeatureId id; while ( mFi.nextFeature( f ) ) { + if ( mCallback ) + { + bool res = ( *mCallback )( f ); + if ( !res ) + { + mNextData = nullptr; + return; + } + } if ( QgsSpatialIndex::featureInfo( f, r, id ) ) { mNextData = new RTree::Data( 0, nullptr, r, id ); @@ -222,6 +233,7 @@ class QgsFeatureIteratorDataStream : public IDataStream RTree::Data *mNextData = nullptr; QgsFeedback *mFeedback = nullptr; QgsSpatialIndex::Flags mFlags = nullptr; + const std::function< bool( const QgsFeature & ) > *mCallback = nullptr; }; @@ -253,10 +265,11 @@ class QgsSpatialIndexData : public QSharedData * of \a feedback is not transferred, and callers must take care that the lifetime of feedback exceeds * that of the spatial index construction. */ - explicit QgsSpatialIndexData( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr, QgsSpatialIndex::Flags flags = nullptr ) + explicit QgsSpatialIndexData( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr, QgsSpatialIndex::Flags flags = nullptr, + const std::function< bool( const QgsFeature & ) > *callback = nullptr ) : mFlags( flags ) { - QgsFeatureIteratorDataStream fids( fi, feedback, mFlags ); + QgsFeatureIteratorDataStream fids( fi, feedback, mFlags, callback ); initTree( &fids ); if ( flags & QgsSpatialIndex::FlagStoreFeatureGeometries ) mGeometries = fids.geometries; @@ -335,6 +348,11 @@ QgsSpatialIndex::QgsSpatialIndex( const QgsFeatureIterator &fi, QgsFeedback *fee d = new QgsSpatialIndexData( fi, feedback, flags ); } +QgsSpatialIndex::QgsSpatialIndex( const QgsFeatureIterator &fi, const std::function< bool( const QgsFeature & )> &callback, QgsSpatialIndex::Flags flags ) +{ + d = new QgsSpatialIndexData( fi, nullptr, flags, &callback ); +} + QgsSpatialIndex::QgsSpatialIndex( const QgsFeatureSource &source, QgsFeedback *feedback, QgsSpatialIndex::Flags flags ) { d = new QgsSpatialIndexData( source.getFeatures( QgsFeatureRequest().setNoAttributes() ), feedback, flags ); diff --git a/src/core/qgsspatialindex.h b/src/core/qgsspatialindex.h index 2ced803d9a7..cc6c3ed47fd 100644 --- a/src/core/qgsspatialindex.h +++ b/src/core/qgsspatialindex.h @@ -95,6 +95,23 @@ class CORE_EXPORT QgsSpatialIndex : public QgsFeatureSink */ explicit QgsSpatialIndex( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr, QgsSpatialIndex::Flags flags = nullptr ); +#ifndef SIP_RUN + + /** + * Constructor - creates R-tree and bulk loads it with features from the iterator. + * This is much faster approach than creating an empty index and then inserting features one by one. + * + * This construct and bulk load variant allows for a \a callback function to be specified, which is + * called for each added feature in turn. It allows for bulk spatial index load along with other feature + * based operations on a single iteration through a feature source. If \a callback returns FALSE, the + * load and iteration is canceled. + * + * \note Not available in Python bindings + * \since QGIS 2.12 + */ + explicit QgsSpatialIndex( const QgsFeatureIterator &fi, const std::function< bool( const QgsFeature & ) > &callback, QgsSpatialIndex::Flags flags = nullptr ); +#endif + /** * Constructor - creates R-tree and bulk loads it with features from the source. * This is much faster approach than creating an empty index and then inserting features one by one. diff --git a/tests/src/core/testqgsspatialindex.cpp b/tests/src/core/testqgsspatialindex.cpp index 31bca1bf8c5..c359964d945 100644 --- a/tests/src/core/testqgsspatialindex.cpp +++ b/tests/src/core/testqgsspatialindex.cpp @@ -234,6 +234,51 @@ class TestQgsSpatialIndex : public QObject delete indexInsert; } + void bulkLoadWithCallback() + { + std::unique_ptr< QgsVectorLayer > vl = qgis::make_unique< QgsVectorLayer >( QStringLiteral( "Point" ), QStringLiteral( "x" ), QStringLiteral( "memory" ) ); + QList< QgsFeatureId > addedIds; + for ( int i = 0; i < 10; ++i ) + { + QgsFeature f( i ); + QgsGeometry g = QgsGeometry::fromPointXY( QgsPointXY( i, 1 ) ); + f.setGeometry( g ); + vl->dataProvider()->addFeature( f ); + addedIds.append( f.id() ); + } + QCOMPARE( vl->featureCount(), 10L ); + + QgsFeatureIds ids; + QgsSpatialIndex i( vl->getFeatures(), [ & ]( const QgsFeature & f )->bool + { + ids.insert( f.id() ); + return true; + } ); + + QCOMPARE( ids.size(), 10 ); + for ( int i = 0; i < 10; ++i ) + QVERIFY( ids.contains( addedIds.at( i ) ) ); + + QList res = i.intersects( QgsRectangle( 1.5, 0, 3.5, 10 ) ); + QCOMPARE( res.size(), 2 ); + QVERIFY( res.contains( addedIds.at( 2 ) ) ); + QVERIFY( res.contains( addedIds.at( 3 ) ) ); + + // try canceling + ids.clear(); + QgsSpatialIndex i2( vl->getFeatures(), [ & ]( const QgsFeature & f )->bool + { + ids.insert( f.id() ); + return false; + } ); + + QCOMPARE( ids.size(), 1 ); + QVERIFY( ids.contains( addedIds.at( 0 ) ) ); + + res = i2.intersects( QgsRectangle( 1.5, 0, 3.5, 10 ) ); + QVERIFY( res.isEmpty() ); + } + void testRetrieveGeometries() { QgsVectorLayer *vl = new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "x" ), QStringLiteral( "memory" ) );