Merge pull request #31648 from troopa81/fix_snaptocurrentlayer

Parallelize snap caching
This commit is contained in:
Hugo Mercier 2019-10-31 09:31:20 +01:00 committed by GitHub
commit e86c2afb4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 728 additions and 193 deletions

View File

@ -92,12 +92,19 @@ Configure render context - if not ``None``, it will use to index only visible f
typedef QFlags<QgsPointLocator::Type> Types;
bool init( int maxFeaturesToIndex = -1 );
bool init( int maxFeaturesToIndex = -1, bool relaxed = false );
%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 ``relaxed`` parameter passed
in the constructor. 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.
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;
@ -176,58 +183,64 @@ interpolation of the Z value.
};
Match nearestVertex( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0 );
Match nearestVertex( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
%Docstring
Find nearest vertex to the specified point - up to distance specified by tolerance
Optional filter may discard unwanted matches.
This method is either blocking or non blocking according to ``relaxed`` parameter passed
%End
Match nearestEdge( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0 );
Match nearestEdge( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
%Docstring
Find nearest edge to the specified point - up to distance specified by tolerance
Optional filter may discard unwanted matches.
This method is either blocking or non blocking according to ``relaxed`` parameter passed
%End
Match nearestArea( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0 );
Match nearestArea( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
%Docstring
Find nearest area to the specified point - up to distance specified by tolerance
Optional filter may discard unwanted matches.
This will first perform a pointInPolygon and return first result.
If no match is found and tolerance is not 0, it will return nearestEdge.
This method is either blocking or non blocking according to ``relaxed`` parameter passed
.. versionadded:: 3.0
%End
MatchList edgesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = 0 );
MatchList edgesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
%Docstring
Find edges within a specified recangle
Optional filter may discard unwanted matches.
%End
MatchList edgesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0 );
%Docstring
Override of edgesInRect that construct rectangle from a center point and tolerance
This method is either blocking or non blocking according to ``relaxed`` parameter passed
%End
MatchList verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = 0 );
MatchList edgesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
%Docstring
Override of edgesInRect that construct rectangle from a center point and tolerance
This method is either blocking or non blocking according to ``relaxed`` parameter passed
%End
MatchList verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
%Docstring
Find vertices within a specified recangle
This method is either blocking or non blocking according to ``relaxed`` parameter passed
Optional filter may discard unwanted matches.
.. versionadded:: 3.6
%End
MatchList verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0 );
MatchList verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
%Docstring
Override of verticesInRect that construct rectangle from a center point and tolerance
This method is either blocking or non blocking according to ``relaxed`` parameter passed
.. versionadded:: 3.6
%End
MatchList pointInPolygon( const QgsPointXY &point );
%Docstring
find out if the point is in any polygons
%End
MatchList pointInPolygon( const QgsPointXY &point, bool relaxed = false );
int cachedGeometryCount() const;
%Docstring
@ -236,8 +249,33 @@ 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 ``relaxed`` is ``True``
.. seealso:: :py:class:`QgsPointLocator`
%End
void waitForIndexingFinished();
%Docstring
If the point locator has been initialized relaxedly and is currently indexing,
this methods waits for the indexing to be finished
%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

@ -38,6 +38,9 @@ which keeps the configuration in sync with map canvas (e.g. current view, active
QgsSnappingUtils( QObject *parent /TransferThis/ = 0, bool enableSnappingForInvisibleFeature = true );
%Docstring
Constructor for QgsSnappingUtils
:param parent: parent object
:param enableSnappingForInvisibleFeature: ``True`` if we want to snap feature even if there are not visible
%End
~QgsSnappingUtils();
@ -45,14 +48,27 @@ Constructor for QgsSnappingUtils
QgsPointLocator *locatorForLayer( QgsVectorLayer *vl );
%Docstring
Gets a point locator for the given layer. If such locator does not exist, it will be created
:param vl: the vector layer
%End
QgsPointLocator::Match snapToMap( QPoint point, QgsPointLocator::MatchFilter *filter = 0 );
QgsPointLocator::Match snapToMap( QPoint point, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
%Docstring
Snap to map according to the current configuration. Optional filter allows discarding unwanted matches.
%End
QgsPointLocator::Match snapToMap( const QgsPointXY &pointMap, QgsPointLocator::MatchFilter *filter = 0 );
Snap to map according to the current configuration.
:param point: point in canvas coordinates
:param filter: allows discarding unwanted matches.
:param relaxed: ``True`` if this method is non blocking and the matching result can be invalid while indexing
%End
QgsPointLocator::Match snapToMap( const QgsPointXY &pointMap, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
%Docstring
Snap to map according to the current configuration.
:param pointMap: point in map coordinates
:param filter: allows discarding unwanted matches.
:param relaxed: ``True`` if this method is non blocking and the matching result can be invalid while indexing
%End
QgsPointLocator::Match snapToCurrentLayer( QPoint point, QgsPointLocator::Types type, QgsPointLocator::MatchFilter *filter = 0 );
%Docstring
@ -172,13 +188,14 @@ Emitted when the snapping settings object changes.
%End
protected:
virtual void prepareIndexStarting( int count );
%Docstring
Called when starting to index - can be overridden and e.g. progress dialog can be provided
Called when starting to index with snapToMap - can be overridden and e.g. progress dialog can be provided
%End
virtual void prepareIndexProgress( int index );
%Docstring
Called when finished indexing a layer. When index == count the indexing is complete
Called when finished indexing a layer with snapToMap. When index == count the indexing is complete
%End
void clearAllLocators();

View File

@ -23,7 +23,15 @@ Snapping utils instance that is connected to a canvas and updates the configurat
#include "qgsmapcanvassnappingutils.h"
%End
public:
QgsMapCanvasSnappingUtils( QgsMapCanvas *canvas, QObject *parent = 0 );
%Docstring
Construct map canvas snapping utils object
:param canvas: map canvas
:param parent: parent object
if ``False`` it will block until indexing is done
%End
protected:
virtual void prepareIndexStarting( int count );

View File

@ -86,7 +86,7 @@ void QgsMapToolTrimExtendFeature::canvasMoveEvent( QgsMapMouseEvent *e )
{
case StepLimit:
match = mCanvas->snappingUtils()->snapToMap( mMapPoint, &filter );
match = mCanvas->snappingUtils()->snapToMap( mMapPoint, &filter, true );
if ( match.isValid() )
{
mIs3DLayer = QgsWkbTypes::hasZ( match.layer()->wkbType() );
@ -128,7 +128,7 @@ void QgsMapToolTrimExtendFeature::canvasMoveEvent( QgsMapMouseEvent *e )
}
filter.setLayer( mVlayer );
match = mCanvas->snappingUtils()->snapToMap( mMapPoint, &filter );
match = mCanvas->snappingUtils()->snapToMap( mMapPoint, &filter, true );
if ( match.isValid() )
{
@ -234,7 +234,7 @@ void QgsMapToolTrimExtendFeature::canvasReleaseEvent( QgsMapMouseEvent *e )
switch ( mStep )
{
case StepLimit:
match = mCanvas->snappingUtils()->snapToMap( mMapPoint, &filter );
match = mCanvas->snappingUtils()->snapToMap( mMapPoint, &filter, true );
if ( mRubberBandLimit && mRubberBandLimit->isVisible() )
{
if ( getPoints( match, pLimit1, pLimit2 ) )
@ -248,7 +248,7 @@ void QgsMapToolTrimExtendFeature::canvasReleaseEvent( QgsMapMouseEvent *e )
if ( mIsModified )
{
filter.setLayer( mVlayer );
match = mCanvas->snappingUtils()->snapToMap( mMapPoint, &filter );
match = mCanvas->snappingUtils()->snapToMap( mMapPoint, &filter, true );
if ( match.layer() )
{
@ -308,4 +308,3 @@ void QgsMapToolTrimExtendFeature::deactivate()
mVlayer = nullptr;
mLimitLayer = nullptr;
}

View File

@ -760,7 +760,7 @@ QgsPointLocator::Match QgsVertexTool::snapToEditableLayer( QgsMapMouseEvent *e )
snapUtils->setConfig( config );
SelectedMatchFilter filter( tol, mLockedFeature.get() );
m = snapUtils->snapToMap( mapPoint, &filter );
m = snapUtils->snapToMap( mapPoint, &filter, true );
// we give priority to snap matches that are from selected features
if ( filter.hasSelectedMatch() )
@ -787,7 +787,7 @@ QgsPointLocator::Match QgsVertexTool::snapToEditableLayer( QgsMapMouseEvent *e )
snapUtils->setConfig( config );
SelectedMatchFilter filter( tol, mLockedFeature.get() );
m = snapUtils->snapToMap( mapPoint, &filter );
m = snapUtils->snapToMap( mapPoint, &filter, true );
// we give priority to snap matches that are from selected features
if ( filter.hasSelectedMatch() )
@ -802,7 +802,7 @@ QgsPointLocator::Match QgsVertexTool::snapToEditableLayer( QgsMapMouseEvent *e )
if ( mLastSnap )
{
OneFeatureFilter filterLast( mLastSnap->layer(), mLastSnap->featureId() );
QgsPointLocator::Match lastMatch = snapUtils->snapToMap( mapPoint, &filterLast );
QgsPointLocator::Match lastMatch = snapUtils->snapToMap( mapPoint, &filterLast, true );
// but skip the the previously used feature if it would only snap to segment, while now we have snap to vertex
// so that if there is a point on a line, it gets priority (as is usual with combined vertex+segment snapping)
bool matchHasVertexLastHasEdge = m.hasVertex() && lastMatch.hasEdge();
@ -834,7 +834,7 @@ QgsPointLocator::Match QgsVertexTool::snapToPolygonInterior( QgsMapMouseEvent *e
{
if ( currentVlayer->isEditable() && currentVlayer->geometryType() == QgsWkbTypes::PolygonGeometry )
{
QgsPointLocator::MatchList matchList = snapUtils->locatorForLayer( currentVlayer )->pointInPolygon( mapPoint );
QgsPointLocator::MatchList matchList = snapUtils->locatorForLayer( currentVlayer )->pointInPolygon( mapPoint, true );
if ( !matchList.isEmpty() )
{
m = matchList.first();
@ -854,7 +854,7 @@ QgsPointLocator::Match QgsVertexTool::snapToPolygonInterior( QgsMapMouseEvent *e
if ( vlayer->isEditable() && vlayer->geometryType() == QgsWkbTypes::PolygonGeometry )
{
QgsPointLocator::MatchList matchList = snapUtils->locatorForLayer( vlayer )->pointInPolygon( mapPoint );
QgsPointLocator::MatchList matchList = snapUtils->locatorForLayer( vlayer )->pointInPolygon( mapPoint, true );
if ( !matchList.isEmpty() )
{
m = matchList.first();
@ -886,7 +886,7 @@ QList<QgsPointLocator::Match> QgsVertexTool::findEditableLayerMatches( const Qgs
if ( layer->geometryType() == QgsWkbTypes::PolygonGeometry )
{
matchList << locator->pointInPolygon( mapPoint );
matchList << locator->pointInPolygon( mapPoint, true );
}
double tolerance = QgsTolerance::vertexSearchRadius( canvas()->mapSettings() );
@ -1773,7 +1773,7 @@ QList<QgsPointLocator::Match> QgsVertexTool::layerVerticesSnappedToPoint( QgsVec
{
MatchCollectingFilter myfilter( this );
QgsPointLocator *loc = canvas()->snappingUtils()->locatorForLayer( layer );
loc->nearestVertex( mapPoint, 0, &myfilter );
loc->nearestVertex( mapPoint, 0, &myfilter, true );
return myfilter.matches;
}

View File

@ -317,6 +317,7 @@ SET(QGIS_CORE_SRCS
qgspluginlayerregistry.cpp
qgspointxy.cpp
qgspointlocator.cpp
qgspointlocatorinittask.cpp
qgsproject.cpp
qgsprojectbadlayerhandler.cpp
qgsprojectfiletransform.cpp
@ -713,6 +714,7 @@ SET(QGIS_CORE_MOC_HDRS
qgspluginlayer.h
qgspointxy.h
qgspointlocator.h
qgspointlocatorinittask.h
qgsproject.h
qgsprojectviewsettings.h
qgsproxyprogresstask.h

View File

@ -41,13 +41,13 @@ QgsCadUtils::AlignMapPointOutput QgsCadUtils::alignMapPoint( const QgsPointXY &o
res.softLockCommonAngle = -1;
// try to snap to anything
QgsPointLocator::Match snapMatch = ctx.snappingUtils->snapToMap( originalMapPoint );
QgsPointLocator::Match snapMatch = ctx.snappingUtils->snapToMap( originalMapPoint, nullptr, true );
QgsPointXY point = snapMatch.isValid() ? snapMatch.point() : originalMapPoint;
// try to snap explicitly to a segment - useful for some constraints
QgsPointXY edgePt0, edgePt1;
EdgesOnlyFilter edgesOnlyFilter;
QgsPointLocator::Match edgeMatch = ctx.snappingUtils->snapToMap( originalMapPoint, &edgesOnlyFilter );
QgsPointLocator::Match edgeMatch = ctx.snappingUtils->snapToMap( originalMapPoint, &edgesOnlyFilter, true );
if ( edgeMatch.hasEdge() )
edgeMatch.edgePoints( edgePt0, edgePt1 );

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;
@ -544,6 +548,10 @@ QgsPointLocator::QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateRefe
QgsPointLocator::~QgsPointLocator()
{
// don't delete a locator if there is an indexing task running on it
if ( mIsIndexing )
waitForIndexingFinished();
destroyIndex();
}
@ -554,6 +562,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 +573,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 +590,107 @@ void QgsPointLocator::setRenderContext( const QgsRenderContext *context )
}
bool QgsPointLocator::init( int maxFeaturesToIndex )
void QgsPointLocator::onInitTaskFinished()
{
return hasIndex() ? true : rebuildIndex( maxFeaturesToIndex );
// Check that we don't call this method twice, when calling waitForFinished
// for instance (because of taskCompleted signal)
if ( !mIsIndexing )
return;
mIsIndexing = false;
mRenderer.reset();
mSource.reset();
// treat added and deleted feature while indexing
for ( QgsFeatureId fid : mAddedFeatures )
onFeatureAdded( fid );
mAddedFeatures.clear();
for ( QgsFeatureId fid : mDeletedFeatures )
onFeatureDeleted( fid );
mDeletedFeatures.clear();
emit initFinished( mInitTask->isBuildOK() );
}
bool QgsPointLocator::init( int maxFeaturesToIndex, bool relaxed )
{
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 ( relaxed )
{
mInitTask = new QgsPointLocatorInitTask( this );
connect( mInitTask, &QgsPointLocatorInitTask::taskTerminated, this, &QgsPointLocator::onInitTaskFinished );
connect( mInitTask, &QgsPointLocatorInitTask::taskCompleted, this, &QgsPointLocator::onInitTaskFinished );
QgsApplication::taskManager()->addTask( mInitTask );
return true;
}
else
{
const bool ok = rebuildIndex( maxFeaturesToIndex );
mIsIndexing = false;
emit initFinished( ok );
return ok;
}
}
void QgsPointLocator::waitForIndexingFinished()
{
mInitTask->waitForFinished();
onInitTaskFinished();
}
bool QgsPointLocator::hasIndex() const
{
return mRTree || mIsEmptyLayer;
return mIsIndexing || mRTree || mIsEmptyLayer;
}
bool QgsPointLocator::prepare( bool relaxed )
{
if ( mIsIndexing )
{
if ( relaxed )
return false;
else
waitForIndexingFinished();
}
if ( !mRTree )
{
init( -1, relaxed );
if ( ( relaxed && mIsIndexing ) || !mRTree ) // relaxed 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 +715,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 +736,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 +800,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 +824,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 +900,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
@ -826,14 +944,10 @@ void QgsPointLocator::onAttributeValueChanged( QgsFeatureId fid, int idx, const
}
QgsPointLocator::Match QgsPointLocator::nearestVertex( const QgsPointXY &point, double tolerance, MatchFilter *filter )
QgsPointLocator::Match QgsPointLocator::nearestVertex( const QgsPointXY &point, double tolerance, MatchFilter *filter, bool relaxed )
{
if ( !mRTree )
{
init();
if ( !mRTree ) // still invalid?
return Match();
}
if ( !prepare( relaxed ) )
return Match();
Match m;
QgsPointLocator_VisitorNearestVertex visitor( this, m, point, filter );
@ -844,14 +958,10 @@ QgsPointLocator::Match QgsPointLocator::nearestVertex( const QgsPointXY &point,
return m;
}
QgsPointLocator::Match QgsPointLocator::nearestEdge( const QgsPointXY &point, double tolerance, MatchFilter *filter )
QgsPointLocator::Match QgsPointLocator::nearestEdge( const QgsPointXY &point, double tolerance, MatchFilter *filter, bool relaxed )
{
if ( !mRTree )
{
init();
if ( !mRTree ) // still invalid?
return Match();
}
if ( !prepare( relaxed ) )
return Match();
QgsWkbTypes::GeometryType geomType = mLayer->geometryType();
if ( geomType == QgsWkbTypes::PointGeometry )
@ -866,14 +976,10 @@ QgsPointLocator::Match QgsPointLocator::nearestEdge( const QgsPointXY &point, do
return m;
}
QgsPointLocator::Match QgsPointLocator::nearestArea( const QgsPointXY &point, double tolerance, MatchFilter *filter )
QgsPointLocator::Match QgsPointLocator::nearestArea( const QgsPointXY &point, double tolerance, MatchFilter *filter, bool relaxed )
{
if ( !mRTree )
{
init();
if ( !mRTree ) // still invalid?
return Match();
}
if ( !prepare( relaxed ) )
return Match();
MatchList mlist = pointInPolygon( point );
if ( !mlist.isEmpty() && mlist.at( 0 ).isValid() )
@ -900,14 +1006,10 @@ QgsPointLocator::Match QgsPointLocator::nearestArea( const QgsPointXY &point, do
}
QgsPointLocator::MatchList QgsPointLocator::edgesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter )
QgsPointLocator::MatchList QgsPointLocator::edgesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter, bool relaxed )
{
if ( !mRTree )
{
init();
if ( !mRTree ) // still invalid?
return MatchList();
}
if ( !prepare( relaxed ) )
return MatchList();
QgsWkbTypes::GeometryType geomType = mLayer->geometryType();
if ( geomType == QgsWkbTypes::PointGeometry )
@ -920,20 +1022,16 @@ QgsPointLocator::MatchList QgsPointLocator::edgesInRect( const QgsRectangle &rec
return lst;
}
QgsPointLocator::MatchList QgsPointLocator::edgesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter )
QgsPointLocator::MatchList QgsPointLocator::edgesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter, bool relaxed )
{
QgsRectangle rect( point.x() - tolerance, point.y() - tolerance, point.x() + tolerance, point.y() + tolerance );
return edgesInRect( rect, filter );
return edgesInRect( rect, filter, relaxed );
}
QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter )
QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter, bool relaxed )
{
if ( !mRTree )
{
init();
if ( !mRTree ) // still invalid?
return MatchList();
}
if ( !prepare( relaxed ) )
return MatchList();
MatchList lst;
QgsPointLocator_VisitorVerticesInRect visitor( this, lst, rect, filter );
@ -942,21 +1040,16 @@ QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsRectangle &
return lst;
}
QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter )
QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter, bool relaxed )
{
QgsRectangle rect( point.x() - tolerance, point.y() - tolerance, point.x() + tolerance, point.y() + tolerance );
return verticesInRect( rect, filter );
return verticesInRect( rect, filter, relaxed );
}
QgsPointLocator::MatchList QgsPointLocator::pointInPolygon( const QgsPointXY &point )
QgsPointLocator::MatchList QgsPointLocator::pointInPolygon( const QgsPointXY &point, bool relaxed )
{
if ( !mRTree )
{
init();
if ( !mRTree ) // still invalid?
return MatchList();
}
if ( !prepare( relaxed ) )
return MatchList();
QgsWkbTypes::GeometryType geomType = mLayer->geometryType();
if ( geomType == QgsWkbTypes::PointGeometry || geomType == QgsWkbTypes::LineGeometry )

View File

@ -20,6 +20,7 @@ class QgsPointXY;
class QgsFeatureRenderer;
class QgsRenderContext;
class QgsRectangle;
class QgsVectorLayerFeatureSource;
#include "qgis_core.h"
#include "qgspointxy.h"
@ -30,6 +31,7 @@ class QgsRectangle;
#include "qgsgeometryutils.h"
#include "qgsvectorlayer.h"
#include "qgslinestring.h"
#include "qgspointlocatorinittask.h"
#include <memory>
class QgsPointLocator_VisitorNearestVertex;
@ -122,10 +124,17 @@ 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
*/
bool init( int maxFeaturesToIndex = -1 );
* 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 relaxed parameter passed
* in the constructor. 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.
*
* 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, bool relaxed = false );
//! Indicate whether the data have been already indexed
bool hasIndex() const;
@ -251,50 +260,65 @@ class CORE_EXPORT QgsPointLocator : public QObject
/**
* Find nearest vertex to the specified point - up to distance specified by tolerance
* Optional filter may discard unwanted matches.
* This method is either blocking or non blocking according to \a relaxed parameter passed
*/
Match nearestVertex( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr );
Match nearestVertex( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr, bool relaxed = false );
/**
* Find nearest edge to the specified point - up to distance specified by tolerance
* Optional filter may discard unwanted matches.
* This method is either blocking or non blocking according to \a relaxed parameter passed
*/
Match nearestEdge( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr );
Match nearestEdge( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr, bool relaxed = false );
/**
* Find nearest area to the specified point - up to distance specified by tolerance
* Optional filter may discard unwanted matches.
* This will first perform a pointInPolygon and return first result.
* If no match is found and tolerance is not 0, it will return nearestEdge.
* This method is either blocking or non blocking according to \a relaxed parameter passed
* \since QGIS 3.0
*/
Match nearestArea( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr );
Match nearestArea( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr, bool relaxed = false );
/**
* Find edges within a specified recangle
* Optional filter may discard unwanted matches.
* This method is either blocking or non blocking according to \a relaxed parameter passed
*/
MatchList edgesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = nullptr );
//! Override of edgesInRect that construct rectangle from a center point and tolerance
MatchList edgesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr );
MatchList edgesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = nullptr, bool relaxed = false );
/**
* Override of edgesInRect that construct rectangle from a center point and tolerance
* This method is either blocking or non blocking according to \a relaxed parameter passed
*/
MatchList edgesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr, bool relaxed = false );
/**
* Find vertices within a specified recangle
* This method is either blocking or non blocking according to \a relaxed parameter passed
* Optional filter may discard unwanted matches.
* \since QGIS 3.6
*/
MatchList verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = nullptr );
MatchList verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = nullptr, bool relaxed = false );
/**
* Override of verticesInRect that construct rectangle from a center point and tolerance
* This method is either blocking or non blocking according to \a relaxed parameter passed
* \since QGIS 3.6
*/
MatchList verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr );
MatchList verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr, bool relaxed = false );
// point-in-polygon query
// TODO: function to return just the first match?
//! find out if the point is in any polygons
MatchList pointInPolygon( const QgsPointXY &point );
/**
* find out if the \a point is in any polygons
* This method is either blocking or non blocking according to \a relaxed parameter passed
*/
//!
MatchList pointInPolygon( const QgsPointXY &point, bool relaxed = false );
/**
* Returns how many geometries are cached in the index
@ -302,17 +326,49 @@ 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 relaxed is TRUE
*
* \see QgsPointLocator()
*/
bool isIndexing() const { return mIsIndexing; }
/**
* If the point locator has been initialized relaxedly and is currently indexing,
* this methods waits for the indexing to be finished
*/
void waitForIndexingFinished();
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 onInitTaskFinished();
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
* \param relaxed TRUE if index build has to be non blocking
*/
bool prepare( bool relaxed );
//! Storage manager
std::unique_ptr< SpatialIndex::IStorageManager > mStorage;
@ -329,12 +385,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 mIsIndexing = false;
QgsFeatureIds mAddedFeatures;
QgsFeatureIds mDeletedFeatures;
QPointer<QgsPointLocatorInitTask> mInitTask;
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,38 @@
/***************************************************************************
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 "qgspointlocator.h"
#include "qgsvectorlayer.h"
/// @cond PRIVATE
QgsPointLocatorInitTask::QgsPointLocatorInitTask( QgsPointLocator *loc )
: QgsTask( tr( "Indexing %1" ).arg( loc->layer()->id() ), QgsTask::Flags() )
, mLoc( loc )
{}
bool QgsPointLocatorInitTask::isBuildOK() const
{
return mBuildOK;
}
bool QgsPointLocatorInitTask::run()
{
mBuildOK = mLoc->rebuildIndex();
return true;
}
/// @endcond

View File

@ -0,0 +1,59 @@
/***************************************************************************
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"
class QgsPointLocator;
class QgsPointLocatorInitTask : public QgsTask
{
Q_OBJECT
public:
QgsPointLocatorInitTask( QgsPointLocator *loc );
/**
* Returns TRUE when the task has finished and the index build was ok
*/
bool isBuildOK() const;
bool run();
private:
QgsPointLocator *mLoc = nullptr;
bool mBuildOK = false;
};
/// @endcond
#endif // QGSPOINTLOCATORINITTASK_H

View File

@ -41,7 +41,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 );
connect( vlpl, &QgsPointLocator::initFinished, this, &QgsSnappingUtils::onInitFinished );
mLocators.insert( vl, vlpl );
}
return mLocators.value( vl );
@ -59,10 +60,16 @@ void QgsSnappingUtils::clearAllLocators()
QgsPointLocator *QgsSnappingUtils::locatorForLayerUsingStrategy( QgsVectorLayer *vl, const QgsPointXY &pointMap, double tolerance )
{
if ( vl->geometryType() == QgsWkbTypes::NullGeometry || mStrategy == IndexNeverFull )
return nullptr;
QgsRectangle aoi( pointMap.x() - tolerance, pointMap.y() - tolerance,
pointMap.x() + tolerance, pointMap.y() + tolerance );
if ( isIndexPrepared( vl, aoi ) )
return locatorForLayer( vl );
QgsPointLocator *loc = locatorForLayer( vl );
if ( loc->isIndexing() || isIndexPrepared( loc, aoi ) )
return loc;
else
return temporaryLocatorForLayer( vl, pointMap, tolerance );
}
@ -74,18 +81,16 @@ 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 );
connect( vlpl, &QgsPointLocator::initFinished, this, &QgsSnappingUtils::onInitFinished );
mTemporaryLocators.insert( vl, vlpl );
return mTemporaryLocators.value( vl );
}
bool QgsSnappingUtils::isIndexPrepared( QgsVectorLayer *vl, const QgsRectangle &areaOfInterest )
bool QgsSnappingUtils::isIndexPrepared( QgsPointLocator *loc, const QgsRectangle &areaOfInterest )
{
if ( vl->geometryType() == QgsWkbTypes::NullGeometry || mStrategy == IndexNeverFull )
return false;
QgsPointLocator *loc = locatorForLayer( vl );
if ( mStrategy == IndexAlwaysFull && loc->hasIndex() )
return true;
@ -183,22 +188,22 @@ static void _replaceIfBetter( QgsPointLocator::Match &bestMatch, const QgsPointL
bestMatch = candidateMatch; // the other match is better!
}
static void _updateBestMatch( QgsPointLocator::Match &bestMatch, const QgsPointXY &pointMap, QgsPointLocator *loc, QgsPointLocator::Types type, double tolerance, QgsPointLocator::MatchFilter *filter )
static void _updateBestMatch( QgsPointLocator::Match &bestMatch, const QgsPointXY &pointMap, QgsPointLocator *loc, QgsPointLocator::Types type, double tolerance, QgsPointLocator::MatchFilter *filter, bool relaxed )
{
if ( type & QgsPointLocator::Vertex )
{
_replaceIfBetter( bestMatch, loc->nearestVertex( pointMap, tolerance, filter ), tolerance );
_replaceIfBetter( bestMatch, loc->nearestVertex( pointMap, tolerance, filter, relaxed ), tolerance );
}
if ( bestMatch.type() != QgsPointLocator::Vertex && ( type & QgsPointLocator::Edge ) )
{
_replaceIfBetter( bestMatch, loc->nearestEdge( pointMap, tolerance, filter ), tolerance );
_replaceIfBetter( bestMatch, loc->nearestEdge( pointMap, tolerance, filter, relaxed ), tolerance );
}
if ( bestMatch.type() != QgsPointLocator::Vertex && bestMatch.type() != QgsPointLocator::Edge && ( type & QgsPointLocator::Area ) )
{
// if edges were detected, set tolerance to 0 to only do pointInPolygon (and avoid redo nearestEdge)
if ( type & QgsPointLocator::Edge )
tolerance = 0;
_replaceIfBetter( bestMatch, loc->nearestArea( pointMap, tolerance, filter ), tolerance );
_replaceIfBetter( bestMatch, loc->nearestArea( pointMap, tolerance, filter, relaxed ), tolerance );
}
}
@ -219,10 +224,9 @@ static QgsPointLocator::Types _snappingTypeToPointLocatorType( QgsSnappingConfig
}
}
QgsPointLocator::Match QgsSnappingUtils::snapToMap( QPoint point, QgsPointLocator::MatchFilter *filter )
QgsPointLocator::Match QgsSnappingUtils::snapToMap( QPoint point, QgsPointLocator::MatchFilter *filter, bool relaxed )
{
return snapToMap( mMapSettings.mapToPixel().toMapCoordinates( point ), filter );
return snapToMap( mMapSettings.mapToPixel().toMapCoordinates( point ), filter, relaxed );
}
inline QgsRectangle _areaOfInterest( const QgsPointXY &point, double tolerance )
@ -231,7 +235,7 @@ inline QgsRectangle _areaOfInterest( const QgsPointXY &point, double tolerance )
point.x() + tolerance, point.y() + tolerance );
}
QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap, QgsPointLocator::MatchFilter *filter )
QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap, QgsPointLocator::MatchFilter *filter, bool relaxed )
{
if ( !mMapSettings.hasValidSettings() || !mSnappingConfig.enabled() )
{
@ -247,7 +251,7 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap,
double tolerance = QgsTolerance::toleranceInProjectUnits( mSnappingConfig.tolerance(), mCurrentLayer, mMapSettings, mSnappingConfig.units() );
QgsPointLocator::Types type = _snappingTypeToPointLocatorType( mSnappingConfig.type() );
prepareIndex( QList<LayerAndAreaOfInterest>() << qMakePair( mCurrentLayer, _areaOfInterest( pointMap, tolerance ) ) );
prepareIndex( QList<LayerAndAreaOfInterest>() << qMakePair( mCurrentLayer, _areaOfInterest( pointMap, tolerance ) ), relaxed );
// use ad-hoc locator
QgsPointLocator *loc = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
@ -255,11 +259,14 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap,
return QgsPointLocator::Match();
QgsPointLocator::Match bestMatch;
_updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter );
_updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter, relaxed );
if ( mSnappingConfig.intersectionSnapping() )
{
QgsPointLocator *locEdges = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
if ( !locEdges )
return QgsPointLocator::Match();
QgsPointLocator::MatchList edges = locEdges->edgesInRect( pointMap, tolerance );
_replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), tolerance );
}
@ -274,7 +281,7 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap,
double tolerance = QgsTolerance::toleranceInProjectUnits( layerConfig.tolerance, layerConfig.layer, mMapSettings, layerConfig.unit );
layers << qMakePair( layerConfig.layer, _areaOfInterest( pointMap, tolerance ) );
}
prepareIndex( layers );
prepareIndex( layers, relaxed );
QgsPointLocator::Match bestMatch;
QgsPointLocator::MatchList edges; // for snap on intersection
@ -285,7 +292,7 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap,
double tolerance = QgsTolerance::toleranceInProjectUnits( layerConfig.tolerance, layerConfig.layer, mMapSettings, layerConfig.unit );
if ( QgsPointLocator *loc = locatorForLayerUsingStrategy( layerConfig.layer, pointMap, tolerance ) )
{
_updateBestMatch( bestMatch, pointMap, loc, layerConfig.type, tolerance, filter );
_updateBestMatch( bestMatch, pointMap, loc, layerConfig.type, tolerance, filter, relaxed );
if ( mSnappingConfig.intersectionSnapping() )
{
@ -312,7 +319,7 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap,
for ( QgsMapLayer *layer : constLayers )
if ( QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer ) )
layers << qMakePair( vl, aoi );
prepareIndex( layers );
prepareIndex( layers, relaxed );
QgsPointLocator::MatchList edges; // for snap on intersection
QgsPointLocator::Match bestMatch;
@ -322,7 +329,7 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap,
QgsVectorLayer *vl = entry.first;
if ( QgsPointLocator *loc = locatorForLayerUsingStrategy( vl, pointMap, tolerance ) )
{
_updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter );
_updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter, relaxed );
if ( mSnappingConfig.intersectionSnapping() )
edges << loc->edgesInRect( pointMap, tolerance );
@ -338,12 +345,20 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap,
return QgsPointLocator::Match();
}
void QgsSnappingUtils::prepareIndex( const QList<LayerAndAreaOfInterest> &layers )
void QgsSnappingUtils::onInitFinished( bool ok )
{
if ( mIsIndexing )
return;
mIsIndexing = true;
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, bool relaxed )
{
// check if we need to build any index
QList<LayerAndAreaOfInterest> layersToIndex;
const auto constLayers = layers;
@ -354,25 +369,34 @@ void QgsSnappingUtils::prepareIndex( const QList<LayerAndAreaOfInterest> &layers
if ( vl->geometryType() == QgsWkbTypes::NullGeometry || mStrategy == IndexNeverFull )
continue;
if ( !isIndexPrepared( vl, entry.second ) )
QgsPointLocator *loc = locatorForLayer( vl );
if ( !loc->isIndexing() && !isIndexPrepared( loc, entry.second ) )
layersToIndex << entry;
}
if ( !layersToIndex.isEmpty() )
{
// build indexes
QTime t;
t.start();
int i = 0;
prepareIndexStarting( layersToIndex.count() );
const auto constLayersToIndex = layersToIndex;
for ( const LayerAndAreaOfInterest &entry : constLayersToIndex )
if ( !relaxed )
{
t.start();
prepareIndexStarting( layersToIndex.count() );
}
for ( const LayerAndAreaOfInterest &entry : layersToIndex )
{
QgsVectorLayer *vl = entry.first;
QTime tt;
tt.start();
QgsPointLocator *loc = locatorForLayer( vl );
if ( loc->isIndexing() && !relaxed )
{
loc->waitForIndexingFinished();
}
if ( !mEnableSnappingForInvisibleFeature )
{
QgsRenderContext ctx = QgsRenderContext::fromMapSettings( mMapSettings );
@ -383,7 +407,7 @@ void QgsSnappingUtils::prepareIndex( const QList<LayerAndAreaOfInterest> &layers
{
QgsRectangle rect( mMapSettings.visibleExtent() );
loc->setExtent( &rect );
loc->init();
loc->init( -1, relaxed );
}
else if ( mStrategy == IndexHybrid )
{
@ -410,7 +434,7 @@ void QgsSnappingUtils::prepareIndex( const QList<LayerAndAreaOfInterest> &layers
if ( indexReasonableArea == -1 )
{
// we can safely index the whole layer
loc->init();
loc->init( -1, relaxed );
}
else
{
@ -422,24 +446,20 @@ 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, relaxed );
}
}
else // full index strategy
loc->init();
loc->init( relaxed );
QgsDebugMsg( QStringLiteral( "Index init: %1 ms (%2)" ).arg( tt.elapsed() ).arg( vl->id() ) );
prepareIndexProgress( ++i );
if ( !relaxed )
prepareIndexProgress( ++i );
}
QgsDebugMsg( QStringLiteral( "Prepare index total: %1 ms" ).arg( t.elapsed() ) );
if ( !relaxed )
QgsDebugMsg( QStringLiteral( "Prepare index total: %1 ms" ).arg( t.elapsed() ) );
}
mIsIndexing = false;
}
QgsSnappingConfig QgsSnappingUtils::config() const
@ -484,7 +504,7 @@ QgsPointLocator::Match QgsSnappingUtils::snapToCurrentLayer( QPoint point, QgsPo
return QgsPointLocator::Match();
QgsPointLocator::Match bestMatch;
_updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter );
_updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter, false );
return bestMatch;
}

View File

@ -52,20 +52,37 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
public:
//! Constructor for QgsSnappingUtils
/**
* Constructor for QgsSnappingUtils
* \param parent parent object
* \param enableSnappingForInvisibleFeature TRUE if we want to snap feature even if there are not visible
*/
QgsSnappingUtils( QObject *parent SIP_TRANSFERTHIS = nullptr, bool enableSnappingForInvisibleFeature = true );
~QgsSnappingUtils() override;
// main actions
//! Gets a point locator for the given layer. If such locator does not exist, it will be created
/**
* Gets a point locator for the given layer. If such locator does not exist, it will be created
* \param vl the vector layer
*/
QgsPointLocator *locatorForLayer( QgsVectorLayer *vl );
//! Snap to map according to the current configuration. Optional filter allows discarding unwanted matches.
QgsPointLocator::Match snapToMap( QPoint point, QgsPointLocator::MatchFilter *filter = nullptr );
QgsPointLocator::Match snapToMap( const QgsPointXY &pointMap, QgsPointLocator::MatchFilter *filter = nullptr );
/**
* Snap to map according to the current configuration.
* \param point point in canvas coordinates
* \param filter allows discarding unwanted matches.
* \param relaxed TRUE if this method is non blocking and the matching result can be invalid while indexing
*/
QgsPointLocator::Match snapToMap( QPoint point, QgsPointLocator::MatchFilter *filter = nullptr, bool relaxed = false );
//! Snap to current layer
/**
* Snap to map according to the current configuration.
* \param pointMap point in map coordinates
* \param filter allows discarding unwanted matches.
* \param relaxed TRUE if this method is non blocking and the matching result can be invalid while indexing
*/
QgsPointLocator::Match snapToMap( const QgsPointXY &pointMap, QgsPointLocator::MatchFilter *filter = nullptr, bool relaxed = false );
//! Snap to current layer
QgsPointLocator::Match snapToCurrentLayer( QPoint point, QgsPointLocator::Types type, QgsPointLocator::MatchFilter *filter = nullptr );
@ -190,14 +207,20 @@ 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 ) }
//! Called when starting to index with snapToMap - 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 with snapToMap. When index == count the indexing is complete
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
@ -210,10 +233,10 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
typedef QPair< QgsVectorLayer *, QgsRectangle > LayerAndAreaOfInterest;
//! find out whether the strategy would index such layer or just use a temporary locator
bool isIndexPrepared( QgsVectorLayer *vl, const QgsRectangle &areaOfInterest );
//! Returns TRUE if \a loc index is ready to be used in the area of interest \a areaOfInterest
bool isIndexPrepared( QgsPointLocator *loc, const QgsRectangle &areaOfInterest );
//! initialize index for layers where it makes sense (according to the indexing strategy)
void prepareIndex( const QList<LayerAndAreaOfInterest> &layers );
void prepareIndex( const QList<LayerAndAreaOfInterest> &layers, bool relaxed );
private:
// environment
@ -249,12 +272,8 @@ 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;
};

View File

@ -670,7 +670,7 @@ bool QgsAdvancedDigitizingDockWidget::applyConstraints( QgsMapMouseEvent *e )
// set the point coordinates in the map event
e->setMapPoint( point );
mSnapMatch = context.snappingUtils->snapToMap( point );
mSnapMatch = context.snappingUtils->snapToMap( point, nullptr, true );
if ( mSnapMatch.isValid() )
{
@ -782,7 +782,7 @@ QList<QgsPointXY> QgsAdvancedDigitizingDockWidget::snapSegmentToAllLayers( const
localConfig.setType( QgsSnappingConfig::Segment );
snappingUtils->setConfig( localConfig );
match = snappingUtils->snapToMap( originalMapPoint );
match = snappingUtils->snapToMap( originalMapPoint, nullptr, true );
snappingUtils->setConfig( canvasConfig );

View File

@ -34,6 +34,14 @@ class GUI_EXPORT QgsMapCanvasSnappingUtils : public QgsSnappingUtils
{
Q_OBJECT
public:
/**
* Construct map canvas snapping utils object
*
* \param canvas map canvas
* \param parent parent object
* if FALSE it will block until indexing is done
*/
QgsMapCanvasSnappingUtils( QgsMapCanvas *canvas, QObject *parent = nullptr );
protected:

View File

@ -49,7 +49,7 @@ QgsPointXY QgsMapMouseEvent::snapPoint()
mHasCachedSnapResult = true;
QgsSnappingUtils *snappingUtils = mMapCanvas->snappingUtils();
mSnapMatch = snappingUtils->snapToMap( mMapPoint );
mSnapMatch = snappingUtils->snapToMap( mMapPoint, nullptr, true );
if ( mSnapMatch.isValid() )
{

View File

@ -119,6 +119,9 @@ void TestQgsMapToolAddFeaturePoint::initTestCase()
mCanvas->setLayers( QList<QgsMapLayer *>() << mLayerPointZ << mLayerPointZSnap );
mCanvas->setCurrentLayer( mLayerPointZ );
mCanvas->snappingUtils()->locatorForLayer( mLayerPointZ )->init();
mCanvas->snappingUtils()->locatorForLayer( mLayerPointZSnap )->init();
// create the tool
mCaptureTool = new QgsMapToolAddFeature( mCanvas, /*mAdvancedDigitizingDockWidget, */ QgsMapToolCapture::CapturePoint );
mCanvas->setMapTool( mCaptureTool );

View File

@ -162,10 +162,14 @@ void TestQgsMapToolReshape::initTestCase()
cfg.setType( QgsSnappingConfig::VertexAndSegment );
cfg.setEnabled( true );
mCanvas->snappingUtils()->setConfig( cfg );
mCanvas->setLayers( QList<QgsMapLayer *>() << mLayerLineZ << mLayerPointZ << mLayerPolygonZ );
mCanvas->setCurrentLayer( mLayerLineZ );
mCanvas->snappingUtils()->locatorForLayer( mLayerLineZ )->init();
mCanvas->snappingUtils()->locatorForLayer( mLayerPointZ )->init();
mCanvas->snappingUtils()->locatorForLayer( mLayerPolygonZ )->init();
mCanvas->snappingUtils()->locatorForLayer( mLayerTopo )->init();
// create the tool
mCaptureTool = new QgsMapToolReshape( mCanvas );
mCanvas->setMapTool( mCaptureTool );

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

@ -29,6 +29,7 @@
#include "qgsmapcanvas.h"
#include "qgsmapmouseevent.h"
#include "qgsmapcanvassnappingutils.h"
#include "qgisapp.h"
#include "qgsmaptooltrimextendfeature.h"
@ -173,6 +174,12 @@ class TestQgsMapToolTrimExtendFeature : public QObject
snappingConfig.setMode( QgsSnappingConfig::AllLayers );
mSnappingUtils->setConfig( snappingConfig );
mSnappingUtils->locatorForLayer( vlPolygon.get() )->init();
mSnappingUtils->locatorForLayer( vlMultiLine.get() )->init();
mSnappingUtils->locatorForLayer( vlLineZ.get() )->init();
mSnappingUtils->locatorForLayer( vlTopoEdit.get() )->init();
mSnappingUtils->locatorForLayer( vlTopoLimit.get() )->init();
mCanvas->setSnappingUtils( mSnappingUtils );
}

View File

@ -21,7 +21,7 @@
#include "qgsgeometry.h"
#include "qgsmapcanvas.h"
#include "qgsmapmouseevent.h"
#include "qgssnappingutils.h"
/**
* \ingroup UnitTests

View File

@ -26,6 +26,7 @@
#include "qgslinestring.h"
#include "qgssnappingconfig.h"
#include "qgssettings.h"
#include "testqgsmaptoolutils.h"
bool operator==( const QgsGeometry &g1, const QgsGeometry &g2 )
{
@ -138,6 +139,7 @@ class TestQgsVertexTool : public QObject
private:
QgsMapCanvas *mCanvas = nullptr;
QgisApp *mQgisApp = nullptr;
QgsAdvancedDigitizingDockWidget *mAdvancedDigitizingDockWidget = nullptr;
QgsVertexTool *mVertexTool = nullptr;
QgsVectorLayer *mLayerLine = nullptr;
@ -161,6 +163,7 @@ void TestQgsVertexTool::initTestCase()
// init QGIS's paths - true means that all path will be inited from prefix
QgsApplication::init();
QgsApplication::initQgis();
mQgisApp = new QgisApp();
// Set up the QSettings environment
QCoreApplication::setOrganizationName( QStringLiteral( "QGIS" ) );
@ -247,9 +250,13 @@ void TestQgsVertexTool::initTestCase()
mCanvas->setLayers( QList<QgsMapLayer *>() << mLayerLine << mLayerPolygon << mLayerPoint << mLayerLineZ );
// TODO: set up snapping
QgsMapCanvasSnappingUtils *snappingUtils = new QgsMapCanvasSnappingUtils( mCanvas, this );
mCanvas->setSnappingUtils( snappingUtils );
mCanvas->setSnappingUtils( new QgsMapCanvasSnappingUtils( mCanvas, this ) );
snappingUtils->locatorForLayer( mLayerLine )->init();
snappingUtils->locatorForLayer( mLayerPolygon )->init();
snappingUtils->locatorForLayer( mLayerPoint )->init();
snappingUtils->locatorForLayer( mLayerLineZ )->init();
// create vertex tool
mVertexTool = new QgsVertexTool( mCanvas, mAdvancedDigitizingDockWidget );
@ -868,6 +875,8 @@ void TestQgsVertexTool::testActiveLayerPriority()
// make one layer active and check its vertex is used
mCanvas->snappingUtils()->locatorForLayer( layerLine2 )->init();
mCanvas->setCurrentLayer( mLayerLine );
mouseClick( 1, 1, Qt::LeftButton );

View File

@ -22,6 +22,7 @@
#include "qgssnappingutils.h"
#include "qgsvectorlayer.h"
/**
* \ingroup UnitTests
* This is a unit test for the QgsCadUtils class.
@ -98,6 +99,8 @@ void TestQgsCadUtils::initTestCase()
mSnappingUtils = new QgsSnappingUtils;
mSnappingUtils->setConfig( snapConfig );
mSnappingUtils->setMapSettings( mMapSettings );
mSnappingUtils->locatorForLayer( mLayerPolygon )->init();
}
//runs after all tests

View File

@ -346,6 +346,134 @@ class TestQgsPointLocator : public QObject
delete vlEmptyGeom;
}
void testAsynchronousMode()
{
QgsPointLocator loc( mVL, QgsCoordinateReferenceSystem(), QgsCoordinateTransformContext(), nullptr );
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, nullptr, true );
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 );
QEventLoop loop;
connect( &loc, &QgsPointLocator::initFinished, &loop, &QEventLoop::quit );
// trigger locator initialization
QgsPointLocator::Match mAddV0 = loc.nearestVertex( QgsPointXY( 12, 12 ), 999, nullptr, true );
// 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();
}
void testWaitForIndexingFinished()
{
QgsPointLocator loc( mVL, QgsCoordinateReferenceSystem(), QgsCoordinateTransformContext(), nullptr );
QgsPointXY pt( 2, 2 );
// locator is not ready yet
QgsPointLocator::Match m = loc.nearestVertex( pt, 999, nullptr, true );
QVERIFY( !m.isValid() );
QVERIFY( loc.mIsIndexing );
// non relaxed call, this will block until the first indexing is finished
// so the match point has to be valid
m = loc.nearestVertex( pt, 999, nullptr );
QVERIFY( m.isValid() );
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 testDeleteLocator()
{
QgsPointLocator *loc = new QgsPointLocator( mVL, QgsCoordinateReferenceSystem(), QgsCoordinateTransformContext(), nullptr );
QgsPointXY pt( 2, 2 );
// delete locator while we are indexing (could happen when closing project for instance)
loc->nearestVertex( pt, 999, nullptr, true );
delete loc;
}
};
QGSTEST_MAIN( TestQgsPointLocator )

View File

@ -401,10 +401,26 @@ class TestQgsSnappingUtils : public QObject
QCOMPARE( m2.point(), QgsPointXY( 5.0, 2.5 ) );
}
void testSnapOnCurrentLayer()
{
QgsMapSettings mapSettings;
mapSettings.setOutputSize( QSize( 100, 100 ) );
mapSettings.setExtent( QgsRectangle( 0, 0, 1, 1 ) );
QVERIFY( mapSettings.hasValidSettings() );
QgsSnappingUtils u( nullptr, true );
u.setMapSettings( mapSettings );
u.setCurrentLayer( mVL );
QgsPointLocator::Match m = u.snapToCurrentLayer( QPoint( 100, 100 ), QgsPointLocator::Vertex );
QVERIFY( m.isValid() );
QVERIFY( m.hasVertex() );
QCOMPARE( m.point(), QgsPointXY( 1, 0 ) );
}
};
QGSTEST_MAIN( TestQgsSnappingUtils )
#include "testqgssnappingutils.moc"