Merge pull request #31374 from troopa81/parallelize_snap_caching

[FEATURE] Parallelize snap index build
This commit is contained in:
Matthias Kuhn 2019-09-06 13:29:06 +02:00 committed by GitHub
commit 87b1aa9a5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 485 additions and 136 deletions

View File

@ -31,7 +31,8 @@ Works with one layer.
explicit QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateReferenceSystem &destinationCrs = QgsCoordinateReferenceSystem(),
const QgsCoordinateTransformContext &transformContext = QgsCoordinateTransformContext(),
const QgsRectangle *extent = 0 );
const QgsRectangle *extent = 0,
bool asynchronous = false );
%Docstring
Construct point locator for a ``layer``.
@ -40,6 +41,10 @@ do the searches on data reprojected to the given CRS. For accurate reprojection
to set the correct ``transformContext`` if a ``destinationCrs`` is specified. This is usually taken
from the current :py:func:`QgsProject.transformContext()`
if ``asynchronous`` is ``False``, point locator init() method will block until point locator index
is completely built. if ``True``, index building will be done in another thread and init() method returns
immediately. initFinished() signal will be emitted once the initialization is over.
If ``extent`` is not ``None``, the locator will index only a subset of the layer which falls within that extent.
%End
@ -96,8 +101,13 @@ Configure render context - if not ``None``, it will use to index only visible f
%Docstring
Prepare the index for queries. Does nothing if the index already exists.
If the number of features is greater than the value of maxFeaturesToIndex, creation of index is stopped
to make sure we do not run out of memory. If maxFeaturesToIndex is -1, no limits are used. Returns
``False`` if the creation of index has been prematurely stopped due to the limit of features, otherwise ``True``
to make sure we do not run out of memory. If maxFeaturesToIndex is -1, no limits are used.
This method is either blocking or non blocking according to ``asynchronous`` parameter passed
in the constructor.
Returns false if the creation of index is blocking and has been prematurely stopped due to the limit of features, otherwise true
.. seealso:: :py:class:`QgsPointLocator`
%End
bool hasIndex() const;
@ -228,8 +238,27 @@ Returns how many geometries are cached in the index
.. versionadded:: 2.14
%End
bool isIndexing() const;
%Docstring
Returns ``True`` if the point locator is currently indexing the data.
This method is useful if constructor parameter ``asynchronous`` is ``True``
.. seealso:: :py:class:`QgsPointLocator`
%End
signals:
void initFinished( bool ok );
%Docstring
Emitted whenever index has been built and initialization is finished
:param ok: ``False`` if the creation of index has been prematurely stopped due to the limit of
features, otherwise ``True``
%End
protected:
bool rebuildIndex( int maxFeaturesToIndex = -1 );
protected slots:
void destroyIndex();
};

View File

@ -35,9 +35,14 @@ which keeps the configuration in sync with map canvas (e.g. current view, active
%End
public:
QgsSnappingUtils( QObject *parent /TransferThis/ = 0, bool enableSnappingForInvisibleFeature = true );
QgsSnappingUtils( QObject *parent /TransferThis/ = 0, bool enableSnappingForInvisibleFeature = true,
bool asynchronous = false );
%Docstring
Constructor for QgsSnappingUtils
:param parent: parent object
:param enableSnappingForInvisibleFeature: ``True`` if we want to snap feature even if there are not visible
:param asynchronous: indicated if point locator creation has to be made asynchronously (see :py:class:`QgsPointLocator`())
%End
~QgsSnappingUtils();
@ -172,13 +177,19 @@ Emitted when the snapping settings object changes.
%End
protected:
virtual void prepareIndexStarting( int count );
virtual void prepareIndexStarting( int count );
%Docstring
Called when starting to index - can be overridden and e.g. progress dialog can be provided
This methods is now deprecated and never called
.. deprecated:: since QGIS 3.10
%End
virtual void prepareIndexProgress( int index );
virtual void prepareIndexProgress( int index );
%Docstring
Called when finished indexing a layer. When index == count the indexing is complete
This methods is now deprecated and never called
.. deprecated:: since QGIS 3.10
%End
void clearAllLocators();

View File

@ -23,13 +23,16 @@ Snapping utils instance that is connected to a canvas and updates the configurat
#include "qgsmapcanvassnappingutils.h"
%End
public:
QgsMapCanvasSnappingUtils( QgsMapCanvas *canvas, QObject *parent = 0 );
protected:
virtual void prepareIndexStarting( int count );
virtual void prepareIndexProgress( int index );
QgsMapCanvasSnappingUtils( QgsMapCanvas *canvas, QObject *parent = 0, bool asynchronous = false );
%Docstring
Construct map canvas snapping utils object
:param canvas: map canvas
:param parent: parent object
:param asynchronous: if ``True`` snapping cache index will be non blocking and done in another thread,
if ``False`` it will block until indexing is done
%End
};

Binary file not shown.

View File

@ -937,7 +937,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipVersionCh
endProfile();
startProfile( QStringLiteral( "Snapping utils" ) );
mSnappingUtils = new QgsMapCanvasSnappingUtils( mMapCanvas, this );
mSnappingUtils = new QgsMapCanvasSnappingUtils( mMapCanvas, this, true );
mMapCanvas->setSnappingUtils( mSnappingUtils );
connect( QgsProject::instance(), &QgsProject::snappingConfigChanged, mSnappingUtils, &QgsSnappingUtils::setConfig );
connect( QgsProject::instance(), &QgsProject::collectAttachedFiles, this, &QgisApp::generateProjectAttachedFiles );

View File

@ -317,6 +317,7 @@ SET(QGIS_CORE_SRCS
qgspluginlayerregistry.cpp
qgspointxy.cpp
qgspointlocator.cpp
qgspointlocatorinittask.cpp
qgsproject.cpp
qgsprojectbadlayerhandler.cpp
qgsprojectfiletransform.cpp
@ -708,6 +709,7 @@ SET(QGIS_CORE_MOC_HDRS
qgspluginlayer.h
qgspointxy.h
qgspointlocator.h
qgspointlocatorinittask.h
qgsproject.h
qgsproxyprogresstask.h
qgsrelationmanager.h

View File

@ -22,11 +22,15 @@
#include "qgis.h"
#include "qgslogger.h"
#include "qgsrenderer.h"
#include "qgsapplication.h"
#include "qgsvectorlayerfeatureiterator.h"
#include "qgsexpressioncontextutils.h"
#include "qgslinestring.h"
#include "qgspointlocatorinittask.h"
#include <spatialindex/SpatialIndex.h>
#include <QLinkedListIterator>
#include <QtConcurrent>
using namespace SpatialIndex;
@ -522,8 +526,10 @@ class QgsPointLocator_DumpTree : public SpatialIndex::IQueryStrategy
////////////////////////////////////////////////////////////////////////////
QgsPointLocator::QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateReferenceSystem &destCRS, const QgsCoordinateTransformContext &transformContext, const QgsRectangle *extent )
QgsPointLocator::QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateReferenceSystem &destCRS, const QgsCoordinateTransformContext &transformContext, const QgsRectangle *extent,
bool asynchronous )
: mLayer( layer )
, mAsynchronous( asynchronous )
{
if ( destCRS.isValid() )
{
@ -554,6 +560,10 @@ QgsCoordinateReferenceSystem QgsPointLocator::destinationCrs() const
void QgsPointLocator::setExtent( const QgsRectangle *extent )
{
if ( mIsIndexing )
// already indexing, return!
return;
mExtent.reset( extent ? new QgsRectangle( *extent ) : nullptr );
destroyIndex();
@ -561,6 +571,10 @@ void QgsPointLocator::setExtent( const QgsRectangle *extent )
void QgsPointLocator::setRenderContext( const QgsRenderContext *context )
{
if ( mIsIndexing )
// already indexing, return!
return;
disconnect( mLayer, &QgsVectorLayer::styleChanged, this, &QgsPointLocator::destroyIndex );
destroyIndex();
@ -574,27 +588,93 @@ void QgsPointLocator::setRenderContext( const QgsRenderContext *context )
}
bool QgsPointLocator::init( int maxFeaturesToIndex )
void QgsPointLocator::onInitTaskTerminated()
{
return hasIndex() ? true : rebuildIndex( maxFeaturesToIndex );
mIsIndexing = false;
mRenderer.reset();
mSource.reset();
}
void QgsPointLocator::onRebuildIndexFinished( bool ok )
{
onInitTaskTerminated();
// treat added and deleted feature while indexing
for ( QgsFeatureId fid : mAddedFeatures )
onFeatureAdded( fid );
for ( QgsFeatureId fid : mDeletedFeatures )
onFeatureDeleted( fid );
emit initFinished( ok );
}
bool QgsPointLocator::init( int maxFeaturesToIndex )
{
const QgsWkbTypes::GeometryType geomType = mLayer->geometryType();
if ( geomType == QgsWkbTypes::NullGeometry // nothing to index
|| hasIndex()
|| mIsIndexing ) // already indexing, return!
return true;
mRenderer.reset( mLayer->renderer() ? mLayer->renderer()->clone() : nullptr );
mSource.reset( new QgsVectorLayerFeatureSource( mLayer ) );
if ( mContext )
{
mContext->expressionContext() << QgsExpressionContextUtils::layerScope( mLayer );
}
mIsIndexing = true;
if ( mAsynchronous )
{
QgsPointLocatorInitTask *task = new QgsPointLocatorInitTask( this );
connect( task, &QgsPointLocatorInitTask::rebuildIndexFinished, this, &QgsPointLocator::onRebuildIndexFinished );
connect( task, &QgsPointLocatorInitTask::taskTerminated, this, &QgsPointLocator::onInitTaskTerminated );
QgsApplication::taskManager()->addTask( task );
return true;
}
else
{
const bool ok = rebuildIndex( maxFeaturesToIndex );
mIsIndexing = false;
emit initFinished( ok );
return ok;
}
}
bool QgsPointLocator::hasIndex() const
{
return mRTree || mIsEmptyLayer;
return mIsIndexing || mRTree || mIsEmptyLayer;
}
bool QgsPointLocator::prepare()
{
if ( mIsIndexing )
return false;
if ( !mRTree )
{
init();
if ( mIsIndexing || !mRTree ) // asynchronous mode and currently indexing or still invalid?
return false;
}
return true;
}
bool QgsPointLocator::rebuildIndex( int maxFeaturesToIndex )
{
QTime t;
t.start();
QgsDebugMsg( QStringLiteral( "RebuildIndex start : %1" ).arg( mSource->id() ) );
destroyIndex();
QLinkedList<RTree::Data *> dataList;
QgsFeature f;
QgsWkbTypes::GeometryType geomType = mLayer->geometryType();
if ( geomType == QgsWkbTypes::NullGeometry )
return true; // nothing to index
QgsFeatureRequest request;
request.setNoAttributes();
@ -619,22 +699,20 @@ bool QgsPointLocator::rebuildIndex( int maxFeaturesToIndex )
}
bool filter = false;
std::unique_ptr< QgsFeatureRenderer > renderer( mLayer->renderer() ? mLayer->renderer()->clone() : nullptr );
QgsRenderContext *ctx = nullptr;
if ( mContext )
{
mContext->expressionContext() << QgsExpressionContextUtils::layerScope( mLayer );
ctx = mContext.get();
if ( renderer )
if ( mRenderer )
{
// setup scale for scale dependent visibility (rule based)
renderer->startRender( *ctx, mLayer->fields() );
filter = renderer->capabilities() & QgsFeatureRenderer::Filter;
request.setSubsetOfAttributes( renderer->usedAttributes( *ctx ), mLayer->fields() );
mRenderer->startRender( *ctx, mSource->fields() );
filter = mRenderer->capabilities() & QgsFeatureRenderer::Filter;
request.setSubsetOfAttributes( mRenderer->usedAttributes( *ctx ), mSource->fields() );
}
}
QgsFeatureIterator fi = mLayer->getFeatures( request );
QgsFeatureIterator fi = mSource->getFeatures( request );
int indexedCount = 0;
while ( fi.nextFeature( f ) )
@ -642,10 +720,10 @@ bool QgsPointLocator::rebuildIndex( int maxFeaturesToIndex )
if ( !f.hasGeometry() )
continue;
if ( filter && ctx && renderer )
if ( filter && ctx && mRenderer )
{
ctx->expressionContext().setFeature( f );
if ( !renderer->willRenderFeature( f, *ctx ) )
if ( !mRenderer->willRenderFeature( f, *ctx ) )
{
continue;
}
@ -706,10 +784,13 @@ bool QgsPointLocator::rebuildIndex( int maxFeaturesToIndex )
mRTree.reset( RTree::createAndBulkLoadNewRTree( RTree::BLM_STR, stream, *mStorage, fillFactor, indexCapacity,
leafCapacity, dimension, variant, indexId ) );
if ( ctx && renderer )
if ( ctx && mRenderer )
{
renderer->stopRender( *ctx );
mRenderer->stopRender( *ctx );
}
QgsDebugMsg( QStringLiteral( "RebuildIndex end : %1 ms (%2)" ).arg( t.elapsed() ).arg( mSource->id() ) );
return true;
}
@ -727,10 +808,17 @@ void QgsPointLocator::destroyIndex()
void QgsPointLocator::onFeatureAdded( QgsFeatureId fid )
{
if ( mIsIndexing )
{
// will modify index once current indexing is finished
mAddedFeatures << fid;
return;
}
if ( !mRTree )
{
if ( mIsEmptyLayer )
rebuildIndex(); // first feature - let's built the index
init(); // first feature - let's built the index
return; // nothing to do if we are not initialized yet
}
@ -796,6 +884,20 @@ void QgsPointLocator::onFeatureAdded( QgsFeatureId fid )
void QgsPointLocator::onFeatureDeleted( QgsFeatureId fid )
{
if ( mIsIndexing )
{
if ( mAddedFeatures.contains( fid ) )
{
mAddedFeatures.remove( fid );
}
else
{
// will modify index once current indexing is finished
mDeletedFeatures << fid;
}
return;
}
if ( !mRTree )
return; // nothing to do if we are not initialized yet
@ -828,12 +930,8 @@ void QgsPointLocator::onAttributeValueChanged( QgsFeatureId fid, int idx, const
QgsPointLocator::Match QgsPointLocator::nearestVertex( const QgsPointXY &point, double tolerance, MatchFilter *filter )
{
if ( !mRTree )
{
init();
if ( !mRTree ) // still invalid?
return Match();
}
if ( !prepare() )
return Match();
Match m;
QgsPointLocator_VisitorNearestVertex visitor( this, m, point, filter );
@ -846,12 +944,8 @@ QgsPointLocator::Match QgsPointLocator::nearestVertex( const QgsPointXY &point,
QgsPointLocator::Match QgsPointLocator::nearestEdge( const QgsPointXY &point, double tolerance, MatchFilter *filter )
{
if ( !mRTree )
{
init();
if ( !mRTree ) // still invalid?
return Match();
}
if ( !prepare() )
return Match();
QgsWkbTypes::GeometryType geomType = mLayer->geometryType();
if ( geomType == QgsWkbTypes::PointGeometry )
@ -868,12 +962,8 @@ QgsPointLocator::Match QgsPointLocator::nearestEdge( const QgsPointXY &point, do
QgsPointLocator::Match QgsPointLocator::nearestArea( const QgsPointXY &point, double tolerance, MatchFilter *filter )
{
if ( !mRTree )
{
init();
if ( !mRTree ) // still invalid?
return Match();
}
if ( !prepare() )
return Match();
MatchList mlist = pointInPolygon( point );
if ( !mlist.isEmpty() && mlist.at( 0 ).isValid() )
@ -902,12 +992,8 @@ QgsPointLocator::Match QgsPointLocator::nearestArea( const QgsPointXY &point, do
QgsPointLocator::MatchList QgsPointLocator::edgesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter )
{
if ( !mRTree )
{
init();
if ( !mRTree ) // still invalid?
return MatchList();
}
if ( !prepare() )
return MatchList();
QgsWkbTypes::GeometryType geomType = mLayer->geometryType();
if ( geomType == QgsWkbTypes::PointGeometry )
@ -928,12 +1014,8 @@ QgsPointLocator::MatchList QgsPointLocator::edgesInRect( const QgsPointXY &point
QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter )
{
if ( !mRTree )
{
init();
if ( !mRTree ) // still invalid?
return MatchList();
}
if ( !prepare() )
return MatchList();
MatchList lst;
QgsPointLocator_VisitorVerticesInRect visitor( this, lst, rect, filter );
@ -948,15 +1030,10 @@ QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsPointXY &po
return verticesInRect( rect, filter );
}
QgsPointLocator::MatchList QgsPointLocator::pointInPolygon( const QgsPointXY &point )
{
if ( !mRTree )
{
init();
if ( !mRTree ) // still invalid?
return MatchList();
}
if ( !prepare() )
return MatchList();
QgsWkbTypes::GeometryType geomType = mLayer->geometryType();
if ( geomType == QgsWkbTypes::PointGeometry || geomType == QgsWkbTypes::LineGeometry )

View File

@ -21,6 +21,7 @@ class QgsVectorLayer;
class QgsFeatureRenderer;
class QgsRenderContext;
class QgsRectangle;
class QgsVectorLayerFeatureSource;
#include "qgis_core.h"
#include "qgspointxy.h"
@ -65,11 +66,16 @@ class CORE_EXPORT QgsPointLocator : public QObject
* to set the correct \a transformContext if a \a destinationCrs is specified. This is usually taken
* from the current QgsProject::transformContext().
*
* if \a asynchronous is FALSE, point locator init() method will block until point locator index
* is completely built. if TRUE, index building will be done in another thread and init() method returns
* immediately. initFinished() signal will be emitted once the initialization is over.
*
* If \a extent is not NULLPTR, the locator will index only a subset of the layer which falls within that extent.
*/
explicit QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateReferenceSystem &destinationCrs = QgsCoordinateReferenceSystem(),
const QgsCoordinateTransformContext &transformContext = QgsCoordinateTransformContext(),
const QgsRectangle *extent = nullptr );
const QgsRectangle *extent = nullptr,
bool asynchronous = false );
~QgsPointLocator() override;
@ -120,9 +126,14 @@ class CORE_EXPORT QgsPointLocator : public QObject
/**
* Prepare the index for queries. Does nothing if the index already exists.
* If the number of features is greater than the value of maxFeaturesToIndex, creation of index is stopped
* to make sure we do not run out of memory. If maxFeaturesToIndex is -1, no limits are used. Returns
* FALSE if the creation of index has been prematurely stopped due to the limit of features, otherwise TRUE
*/
* to make sure we do not run out of memory. If maxFeaturesToIndex is -1, no limits are used.
*
* This method is either blocking or non blocking according to \a asynchronous parameter passed
* in the constructor.
* Returns false if the creation of index is blocking and has been prematurely stopped due to the limit of features, otherwise true
*
* \see QgsPointLocator()
*/
bool init( int maxFeaturesToIndex = -1 );
//! Indicate whether the data have been already indexed
@ -282,17 +293,41 @@ class CORE_EXPORT QgsPointLocator : public QObject
*/
int cachedGeometryCount() const { return mGeoms.count(); }
/**
* Returns TRUE if the point locator is currently indexing the data.
* This method is useful if constructor parameter \a asynchronous is TRUE
*
* \see QgsPointLocator()
*/
bool isIndexing() const { return mIsIndexing; }
signals:
/**
* Emitted whenever index has been built and initialization is finished
* \param ok FALSE if the creation of index has been prematurely stopped due to the limit of
* features, otherwise TRUE
*/
void initFinished( bool ok );
protected:
bool rebuildIndex( int maxFeaturesToIndex = -1 );
protected slots:
void destroyIndex();
private slots:
void onInitTaskTerminated();
void onRebuildIndexFinished( bool ok );
void onFeatureAdded( QgsFeatureId fid );
void onFeatureDeleted( QgsFeatureId fid );
void onGeometryChanged( QgsFeatureId fid, const QgsGeometry &geom );
void onAttributeValueChanged( QgsFeatureId fid, int idx, const QVariant &value );
private:
//! prepare index if need and returns TRUE if the index is ready to be used
bool prepare();
//! Storage manager
std::unique_ptr< SpatialIndex::IStorageManager > mStorage;
@ -309,12 +344,21 @@ class CORE_EXPORT QgsPointLocator : public QObject
std::unique_ptr< QgsRectangle > mExtent;
std::unique_ptr<QgsRenderContext> mContext;
std::unique_ptr<QgsFeatureRenderer> mRenderer;
std::unique_ptr<QgsVectorLayerFeatureSource> mSource;
int mMaxFeaturesToIndex = -1;
bool mAsynchronous = false;
bool mIsIndexing = false;
QgsFeatureIds mAddedFeatures;
QgsFeatureIds mDeletedFeatures;
friend class QgsPointLocator_VisitorNearestVertex;
friend class QgsPointLocator_VisitorNearestEdge;
friend class QgsPointLocator_VisitorArea;
friend class QgsPointLocator_VisitorEdgesInRect;
friend class QgsPointLocator_VisitorVerticesInRect;
friend class QgsPointLocatorInitTask;
friend class TestQgsPointLocator;
};

View File

@ -0,0 +1,34 @@
/***************************************************************************
qgspointlocatorinittask.cpp
--------------------------------------
Date : September 2019
Copyright : (C) 2019 by Julien Cabieces
Email : julien dot cabieces at oslandia dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgspointlocatorinittask.h"
#include "qgsvectorlayer.h"
/// @cond PRIVATE
QgsPointLocatorInitTask::QgsPointLocatorInitTask( QgsPointLocator *loc )
: QgsTask( tr( "Indexing %1" ).arg( loc->layer()->id() ), QgsTask::Flags() )
, mLoc( loc )
{}
bool QgsPointLocatorInitTask::run()
{
const bool ok = mLoc->rebuildIndex();
emit rebuildIndexFinished( ok );
return true;
}
/// @endcond

View File

@ -0,0 +1,56 @@
/***************************************************************************
qgspointlocatorinittask.h
--------------------------------------
Date : September 2019
Copyright : (C) 2019 by Julien Cabieces
Email : julien dot cabieces at oslandia dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSPOINTLOCATORINITTASK_H
#define QGSPOINTLOCATORINITTASK_H
/// @cond PRIVATE
//
// W A R N I N G
// -------------
//
// This file is not part of the QGIS API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
#define SIP_NO_FILE
#include "qgstaskmanager.h"
#include "qgspointlocator.h"
class QgsPointLocatorInitTask : public QgsTask
{
Q_OBJECT
public:
QgsPointLocatorInitTask( QgsPointLocator *loc );
bool run();
signals:
void rebuildIndexFinished( bool ok );
private:
QgsPointLocator *mLoc = nullptr;
};
/// @endcond
#endif // QGSPOINTLOCATORINITTASK_H

View File

@ -21,10 +21,12 @@
#include "qgslogger.h"
#include "qgsrenderer.h"
QgsSnappingUtils::QgsSnappingUtils( QObject *parent, bool enableSnappingForInvisibleFeature )
QgsSnappingUtils::QgsSnappingUtils( QObject *parent, bool enableSnappingForInvisibleFeature,
bool asynchronous )
: QObject( parent )
, mSnappingConfig( QgsProject::instance() )
, mEnableSnappingForInvisibleFeature( enableSnappingForInvisibleFeature )
, mAsynchronous( asynchronous )
{
}
@ -41,7 +43,8 @@ QgsPointLocator *QgsSnappingUtils::locatorForLayer( QgsVectorLayer *vl )
if ( !mLocators.contains( vl ) )
{
QgsPointLocator *vlpl = new QgsPointLocator( vl, destinationCrs(), mMapSettings.transformContext() );
QgsPointLocator *vlpl = new QgsPointLocator( vl, destinationCrs(), mMapSettings.transformContext(), nullptr, mAsynchronous );
connect( vlpl, &QgsPointLocator::initFinished, this, &QgsSnappingUtils::onInitFinished );
mLocators.insert( vl, vlpl );
}
return mLocators.value( vl );
@ -74,7 +77,9 @@ QgsPointLocator *QgsSnappingUtils::temporaryLocatorForLayer( QgsVectorLayer *vl,
QgsRectangle rect( pointMap.x() - tolerance, pointMap.y() - tolerance,
pointMap.x() + tolerance, pointMap.y() + tolerance );
QgsPointLocator *vlpl = new QgsPointLocator( vl, destinationCrs(), mMapSettings.transformContext(), &rect );
QgsPointLocator *vlpl = new QgsPointLocator( vl, destinationCrs(), mMapSettings.transformContext(), &rect, mAsynchronous );
connect( vlpl, &QgsPointLocator::initFinished, this, &QgsSnappingUtils::onInitFinished );
mTemporaryLocators.insert( vl, vlpl );
return mTemporaryLocators.value( vl );
}
@ -86,6 +91,9 @@ bool QgsSnappingUtils::isIndexPrepared( QgsVectorLayer *vl, const QgsRectangle &
QgsPointLocator *loc = locatorForLayer( vl );
if ( loc->isIndexing() )
return true;
if ( mStrategy == IndexAlwaysFull && loc->hasIndex() )
return true;
@ -338,12 +346,20 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap,
return QgsPointLocator::Match();
}
void QgsSnappingUtils::onInitFinished( bool ok )
{
QgsPointLocator *loc = static_cast<QgsPointLocator *>( sender() );
// point locator init didn't work out - too many features!
// let's make the allowed area smaller for the next time
if ( !ok )
{
mHybridMaxAreaPerLayer[loc->layer()->id()] /= 4;
}
}
void QgsSnappingUtils::prepareIndex( const QList<LayerAndAreaOfInterest> &layers )
{
if ( mIsIndexing )
return;
mIsIndexing = true;
// check if we need to build any index
QList<LayerAndAreaOfInterest> layersToIndex;
const auto constLayers = layers;
@ -362,8 +378,6 @@ void QgsSnappingUtils::prepareIndex( const QList<LayerAndAreaOfInterest> &layers
// build indexes
QTime t;
t.start();
int i = 0;
prepareIndexStarting( layersToIndex.count() );
const auto constLayersToIndex = layersToIndex;
for ( const LayerAndAreaOfInterest &entry : constLayersToIndex )
{
@ -422,24 +436,14 @@ void QgsSnappingUtils::prepareIndex( const QList<LayerAndAreaOfInterest> &layers
loc->setExtent( &rect );
// see if it's possible build index for this area
if ( !loc->init( mHybridPerLayerFeatureLimit ) )
{
// hmm that didn't work out - too many features!
// let's make the allowed area smaller for the next time
mHybridMaxAreaPerLayer[vl->id()] /= 4;
}
loc->init( mHybridPerLayerFeatureLimit );
}
}
else // full index strategy
loc->init();
QgsDebugMsg( QStringLiteral( "Index init: %1 ms (%2)" ).arg( tt.elapsed() ).arg( vl->id() ) );
prepareIndexProgress( ++i );
}
QgsDebugMsg( QStringLiteral( "Prepare index total: %1 ms" ).arg( t.elapsed() ) );
}
mIsIndexing = false;
}
QgsSnappingConfig QgsSnappingUtils::config() const

View File

@ -52,8 +52,14 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
public:
//! Constructor for QgsSnappingUtils
QgsSnappingUtils( QObject *parent SIP_TRANSFERTHIS = nullptr, bool enableSnappingForInvisibleFeature = true );
/**
* Constructor for QgsSnappingUtils
* \param parent parent object
* \param enableSnappingForInvisibleFeature TRUE if we want to snap feature even if there are not visible
* \param asynchronous indicated if point locator creation has to be made asynchronously (see QgsPointLocator())
*/
QgsSnappingUtils( QObject *parent SIP_TRANSFERTHIS = nullptr, bool enableSnappingForInvisibleFeature = true,
bool asynchronous = false );
~QgsSnappingUtils() override;
// main actions
@ -190,14 +196,27 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
void configChanged( const QgsSnappingConfig &snappingConfig );
protected:
//! Called when starting to index - can be overridden and e.g. progress dialog can be provided
virtual void prepareIndexStarting( int count ) { Q_UNUSED( count ) }
//! Called when finished indexing a layer. When index == count the indexing is complete
virtual void prepareIndexProgress( int index ) { Q_UNUSED( index ) }
/**
* This methods is now deprecated and never called
* \deprecated since QGIS 3.10
*/
Q_DECL_DEPRECATED virtual void prepareIndexStarting( int count ) { Q_UNUSED( count ); }
/**
* This methods is now deprecated and never called
* \deprecated since QGIS 3.10
*/
Q_DECL_DEPRECATED virtual void prepareIndexProgress( int index ) { Q_UNUSED( index ); }
//! Deletes all existing locators (e.g. when destination CRS has changed and we need to reindex)
void clearAllLocators();
private slots:
//! called whenever a point locator has finished
void onInitFinished( bool ok );
private:
void onIndividualLayerSettingsChanged( const QHash<QgsVectorLayer *, QgsSnappingConfig::IndividualLayerSettings> &layerSettings );
//! Gets destination CRS from map settings, or an invalid CRS if projections are disabled
@ -249,12 +268,11 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
//! if using hybrid strategy, how many features of one layer may be indexed (to limit amount of consumed memory)
int mHybridPerLayerFeatureLimit = 50000;
//! internal flag that an indexing process is going on. Prevents starting two processes in parallel.
bool mIsIndexing = false;
//! Disable or not the snapping on all features. By default is always TRUE except for non visible features on map canvas.
bool mEnableSnappingForInvisibleFeature = true;
//! true if we have to build point locator asynchronously
bool mAsynchronous = false;
};

View File

@ -21,8 +21,8 @@
#include <QApplication>
#include <QProgressDialog>
QgsMapCanvasSnappingUtils::QgsMapCanvasSnappingUtils( QgsMapCanvas *canvas, QObject *parent )
: QgsSnappingUtils( parent, QgsSettings().value( QStringLiteral( "/qgis/digitizing/snap_invisible_feature" ), false ).toBool() )
QgsMapCanvasSnappingUtils::QgsMapCanvasSnappingUtils( QgsMapCanvas *canvas, QObject *parent, bool asynchronous )
: QgsSnappingUtils( parent, QgsSettings().value( QStringLiteral( "/qgis/digitizing/snap_invisible_feature" ), false ).toBool(), asynchronous )
, mCanvas( canvas )
{
@ -58,24 +58,3 @@ void QgsMapCanvasSnappingUtils::canvasMapToolChanged()
{
setEnableSnappingForInvisibleFeature( QgsSettings().value( QStringLiteral( "/qgis/digitizing/snap_invisible_feature" ), false ).toBool() );
}
void QgsMapCanvasSnappingUtils::prepareIndexStarting( int count )
{
QApplication::setOverrideCursor( Qt::WaitCursor );
mProgress = new QProgressDialog( tr( "Indexing data…" ), QString(), 0, count, mCanvas->topLevelWidget() );
mProgress->setWindowModality( Qt::WindowModal );
}
void QgsMapCanvasSnappingUtils::prepareIndexProgress( int index )
{
if ( !mProgress )
return;
mProgress->setValue( index );
if ( index == mProgress->maximum() )
{
delete mProgress;
mProgress = nullptr;
QApplication::restoreOverrideCursor();
}
}

View File

@ -34,11 +34,16 @@ class GUI_EXPORT QgsMapCanvasSnappingUtils : public QgsSnappingUtils
{
Q_OBJECT
public:
QgsMapCanvasSnappingUtils( QgsMapCanvas *canvas, QObject *parent = nullptr );
protected:
void prepareIndexStarting( int count ) override;
void prepareIndexProgress( int index ) override;
/**
* Construct map canvas snapping utils object
*
* \param canvas map canvas
* \param parent parent object
* \param asynchronous if TRUE snapping cache index will be non blocking and done in another thread,
* if FALSE it will block until indexing is done
*/
QgsMapCanvasSnappingUtils( QgsMapCanvas *canvas, QObject *parent = nullptr, bool asynchronous = false );
private slots:
void canvasMapSettingsChanged();

View File

@ -162,7 +162,6 @@ void TestQgsMapToolReshape::initTestCase()
cfg.setType( QgsSnappingConfig::VertexAndSegment );
cfg.setEnabled( true );
mCanvas->snappingUtils()->setConfig( cfg );
mCanvas->setLayers( QList<QgsMapLayer *>() << mLayerLineZ << mLayerPointZ << mLayerPolygonZ );
mCanvas->setCurrentLayer( mLayerLineZ );

View File

@ -25,7 +25,7 @@
#include "testqgsmaptoolutils.h"
#include "qgsmaptoolreverseline.h"
#include "qgsmapmouseevent.h"
#include "qgssnappingutils.h"
class TestQgsMapToolReverseLine : public QObject
{
@ -60,7 +60,6 @@ void TestQgsMapToolReverseLine::initTestCase()
mCanvas = new QgsMapCanvas();
mCanvas->setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3946" ) ) );
}
void TestQgsMapToolReverseLine::cleanupTestCase()

View File

@ -247,9 +247,8 @@ void TestQgsVertexTool::initTestCase()
mCanvas->setLayers( QList<QgsMapLayer *>() << mLayerLine << mLayerPolygon << mLayerPoint << mLayerLineZ );
// TODO: set up snapping
mCanvas->setSnappingUtils( new QgsMapCanvasSnappingUtils( mCanvas, this ) );
QgsMapCanvasSnappingUtils *snappingUtils = new QgsMapCanvasSnappingUtils( mCanvas, this );
mCanvas->setSnappingUtils( snappingUtils );
// create vertex tool
mVertexTool = new QgsVertexTool( mCanvas, mAdvancedDigitizingDockWidget );

View File

@ -346,6 +346,96 @@ class TestQgsPointLocator : public QObject
delete vlEmptyGeom;
}
void testAsynchronousMode()
{
QgsPointLocator loc( mVL, QgsCoordinateReferenceSystem(), QgsCoordinateTransformContext(), nullptr, true );
QgsPointXY pt( 2, 2 );
QEventLoop loop;
connect( &loc, &QgsPointLocator::initFinished, &loop, &QEventLoop::quit );
// locator is not ready yet
QgsPointLocator::Match m = loc.nearestVertex( pt, 999 );
QVERIFY( !m.isValid() );
QVERIFY( loc.mIsIndexing );
// we block until initFinished is called from another thread
loop.exec();
QVERIFY( !loc.mIsIndexing );
// now locator is ready
m = loc.nearestVertex( pt, 999 );
QVERIFY( m.isValid() );
QVERIFY( m.hasVertex() );
QCOMPARE( m.layer(), mVL );
QCOMPARE( m.featureId(), ( QgsFeatureId )1 );
QCOMPARE( m.point(), QgsPointXY( 1, 1 ) );
QCOMPARE( m.distance(), std::sqrt( 2.0 ) );
QCOMPARE( m.vertexIndex(), 2 );
}
void testLayerUpdatesAsynchronous()
{
QgsPointLocator loc( mVL, QgsCoordinateReferenceSystem(), QgsCoordinateTransformContext(), nullptr, true );
QEventLoop loop;
connect( &loc, &QgsPointLocator::initFinished, &loop, &QEventLoop::quit );
// trigger locator initialization
QgsPointLocator::Match mAddV0 = loc.nearestVertex( QgsPointXY( 12, 12 ), 999 );
// locator is not ready yet
QVERIFY( !mAddV0.isValid() );
QVERIFY( loc.mIsIndexing );
mVL->startEditing();
// add a new feature
QgsFeature ff( 0 );
QgsPolygonXY polygon;
QgsPolylineXY polyline;
polyline << QgsPointXY( 10, 11 ) << QgsPointXY( 11, 10 ) << QgsPointXY( 11, 11 ) << QgsPointXY( 10, 11 );
polygon << polyline;
QgsGeometry ffGeom = QgsGeometry::fromPolygonXY( polygon ) ;
ff.setGeometry( ffGeom );
QgsFeatureList flist;
flist << ff;
bool resA = mVL->addFeature( ff );
QVERIFY( resA );
// indexing is still running, change geometry
QgsGeometry *newGeom = new QgsGeometry( ff.geometry() );
newGeom->moveVertex( 10, 10, 2 ); // change 11,11 to 10,10
mVL->changeGeometry( ff.id(), *newGeom );
delete newGeom;
// we block until initFinished is called from another thread
loop.exec();
QVERIFY( !loc.mIsIndexing );
// verify it is changed in the point locator
QgsPointLocator::Match mChV = loc.nearestVertex( QgsPointXY( 12, 12 ), 999 );
QVERIFY( mChV.isValid() );
QVERIFY( mChV.point() != QgsPointXY( 11, 11 ) ); // that point does not exist anymore
mChV = loc.nearestVertex( QgsPointXY( 9, 9 ), 999 );
QVERIFY( mChV.isValid() );
QVERIFY( mChV.point() == QgsPointXY( 10, 10 ) ); // updated point
// delete feature while no indexing is running
bool resD = mVL->deleteFeature( ff.id() );
QVERIFY( resD );
// verify it is deleted from the point locator
QgsPointLocator::Match mDelV = loc.nearestVertex( QgsPointXY( 12, 12 ), 999 );
QVERIFY( mDelV.isValid() );
QCOMPARE( mDelV.point(), QgsPointXY( 1, 1 ) );
mVL->rollBack();
}
};
QGSTEST_MAIN( TestQgsPointLocator )