-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BUGFIX][FEATURE][NEEDS-DOCS] Disable snapping on invisible features. Second version #6750
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking much better - this is close now!
Don't forget to add unit tests too.
src/core/qgspointlocator.cpp
Outdated
connect( mLayer, &QgsVectorLayer::styleChanged, this, &QgsPointLocator::destroyIndex ); | ||
connect( mLayer, &QgsVectorLayer::layerModified, this, &QgsPointLocator::destroyIndex ); | ||
// used when a style is (de)activated | ||
connect( mLayer, &QgsVectorLayer::repaintRequested, this, &QgsPointLocator::destroyIndex ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about destroying the index on a repaint and layer modified - these signals are thrown whenever a feature is edited in a layer - so by destroying the index when that occurs it's forcing a full rebuild, instead of the faster update handling done by onFeatureDeleted/onGeometryChanged. (I'd also suggest working on a test data set with 100k+ features while test running this.. that'll be bound to reveal any performance regressions!)
I think you'll need to fix the missing styleChanged signal instead.
src/core/qgspointlocator.cpp
Outdated
@@ -685,7 +694,8 @@ bool QgsPointLocator::rebuildIndex( int maxFeaturesToIndex ) | |||
return true; // nothing to index | |||
|
|||
QgsFeatureRequest request; | |||
request.setSubsetOfAttributes( QgsAttributeList() ); | |||
//request.setSubsetOfAttributes( QgsAttributeList() ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fetching attributes here isn't required, is it? I think this is leftover from an earlier iteration and will cause a performance drop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I add this request in an else
src/core/qgspointlocator.cpp
Outdated
@@ -685,7 +694,8 @@ bool QgsPointLocator::rebuildIndex( int maxFeaturesToIndex ) | |||
return true; // nothing to index | |||
|
|||
QgsFeatureRequest request; | |||
request.setSubsetOfAttributes( QgsAttributeList() ); | |||
//request.setSubsetOfAttributes( QgsAttributeList() ); | |||
request.setFilterFids( mFeatureIds ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be:
if ( !mFeatureIds.empty() )
request.setFilterFids( mFeatureIds );
(I think ideally QgsFeatureRequest should ignore empty feature lists, but at the moment it still sets the filter type to FilterFids even if an empty list is given -- that would need careful testing though, so go with the easy option here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done with an else for request.setSubsetOfAttributes
src/core/qgssnappingutils.cpp
Outdated
@@ -330,6 +330,57 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap, | |||
} | |||
|
|||
|
|||
QgsFeatureIds QgsSnappingUtils::isMatchAVisibleLayer( QgsVectorLayer *layer ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to findVisibleFeatures
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/core/qgssnappingutils.cpp
Outdated
renderer->stopRender( context ); | ||
} | ||
} | ||
else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this else branch is needed - if the invisible skipping is disabled, you could return an empty list here. That'll be handled correctly by the request in QgsPointLocator::rebuildIndex and avoid the expensive full layer iteration performed here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/core/qgssnappingutils.cpp
Outdated
@@ -359,7 +411,13 @@ void QgsSnappingUtils::prepareIndex( const QList<LayerAndAreaOfInterest> &layers | |||
QgsVectorLayer *vl = entry.first; | |||
QTime tt; | |||
tt.start(); | |||
|
|||
QgsFeatureIds fids = isMatchAVisibleLayer( vl ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think here you may need to check:
if ( mEnableSnappingForInvisibleFeature && fids.empty() )
continue; //no visible features, skip layer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done with a minor change
if ( !mEnableSnappingForInvisibleFeature && fids.empty() )
continue; //no visible features, skip layer
src/core/qgssnappingutils.cpp
Outdated
} | ||
|
||
|
||
QgsFeatureIterator fi = layer->getFeatures(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use an optimised request here:
- use the renderer filter (as explored earlier) to partially limit the requested features
- set the subset of attributes to match only those required by the renderer
- maybe limit to mapSettings extent? (not sure)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should limit to map extent.
ping @wonder-sk
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The decision whether to limit to mapSettings extent should depend on the indexing strategy of the locator - if the locator indexes only a particular extent, the request should use it, but if the locator indexes everything, no extent should be set.
Looking at the current approach, one thing that worries me is that now we need to iterate over the vector layer twice: first to check whether the features are actually visible and store the fids, second time to build the actual locator index. Another problem is that most of the time all (or nearly all) features will be visible, and constructing feature requests with large number of filtered FIDs is going to be inefficient - for example, for postgres that means a generating and processing a query like this: "SELECT .... WHERE fid IN (1, 2, 3, 4, ..., 10000)"
How about updating QgsPointLocator::init() method to have optional check of feature visibility based on a passed feature renderer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@3nids @wonder-sk @nyalldawson
Thank you for your reviews.
With my last commit, I have good performance for normal case (at least my use case : cadastre, road and networks, land survey, etc.). It hangs only on very big layer (more than 100k) when is filtered.
I wonder, if mapExtent should be the solution for locators created from QgsMapCanvasSnappingUtils?
How about updating QgsPointLocator::init() method to have optional check of feature visibility >based on a passed feature renderer?
Can you explain more please, I'm not sure if I understand? Is mEnableSnappingForInvisibleFeature flag not do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So right now in the first phase you build a set of feature IDs that will be used, then in the second phase you pass it to QgsPointLocator which uses the feature IDs in the rebuildIndex() method. What I am suggesting is to merge these two phases into one: rebuildIndex() would have access to a renderer (if enableSnappingForInvisibleFeature=false) and check its willRenderFeature() and add only visible features to the index. In this way we would avoid creation of the temporary set of feature IDs and we would not need to iterate over the layers twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wonder-sk OK I Understand. It was my first idea but I haven't been able to fix all incomplete type and circular include. Maybe I need to take a closer look.
src/core/qgssnappingutils.h
Outdated
* | ||
* \since QGIS 3.2 | ||
*/ | ||
void setEnableSnappingForInvisibleFeature( bool enableIt ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/enableIt/enable/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Sorry it was forgotten from precedent version.
src/core/qgssnappingutils.cpp
Outdated
|
||
if ( layer ) | ||
{ | ||
if ( !mEnableSnappingForInvisibleFeature ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should not be checked here but rather before the call at line 407 in prepareIndex()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
I will add tests later |
If it's OK, can you merge it please ? |
From a quick glance there's still some outstanding comments which haven't been addressed yet - e.g. not rebuilding the index on a triggerRepaint, and @wonder-sk's request to avoid the setFilterFids call |
Now, it's done :) |
src/core/qgspointlocator.cpp
Outdated
@@ -636,6 +637,9 @@ QgsPointLocator::QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateRefe | |||
connect( mLayer, &QgsVectorLayer::featureDeleted, this, &QgsPointLocator::onFeatureDeleted ); | |||
connect( mLayer, &QgsVectorLayer::geometryChanged, this, &QgsPointLocator::onGeometryChanged ); | |||
connect( mLayer, &QgsVectorLayer::dataChanged, this, &QgsPointLocator::destroyIndex ); | |||
connect( mLayer, &QgsVectorLayer::rendererChanged, this, &QgsPointLocator::destroyIndex ); | |||
connect( mLayer, &QgsVectorLayer::styleChanged, this, &QgsPointLocator::destroyIndex ); | |||
connect( mLayer, &QgsVectorLayer::layerModified, this, &QgsPointLocator::destroyIndex ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wonder-sk can you think of a way to avoid this? My interpretation is that it will force a full rebuild when a feature is changed (since it could change the renderer result). But looking above, it seems we rebuild the index on a dataChanged already anyway...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, unfortunately, but it's necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a feeling this may be a blocker: this effectively means that even with every single change of geometry we would be destroying the index - that's going to make digitizing slow... Currently if there is e.g. 1-2 seconds initial lag when the index if first built, such lag would happen after every single move of a node when editing geometries...
Why is it necessary to destroy the index? I would expect we just need to modify existing handlers of featureAdded, featureDeleted, geometryChanged signal handlers to accommodate renderer-dependent search.
src/core/qgspointlocator.cpp
Outdated
@@ -685,7 +695,8 @@ bool QgsPointLocator::rebuildIndex( int maxFeaturesToIndex ) | |||
return true; // nothing to index | |||
|
|||
QgsFeatureRequest request; | |||
request.setSubsetOfAttributes( QgsAttributeList() ); | |||
// request.setSubsetOfAttributes( QgsAttributeList() ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd leave this, and then in the branch below when a renderer is set you should set the subset of attributes to that required by the renderer.
|
||
bool filter = false; | ||
std::unique_ptr< QgsFeatureRenderer > renderer( mLayer->renderer() ? mLayer->renderer()->clone() : nullptr ); | ||
QgsRenderContext *ctx = nullptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't you just use mContext directly instead of ctx?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No sorry, it doesn't work :/
{ | ||
// setup scale for scale dependent visibility (rule based) | ||
renderer->startRender( *ctx, mLayer->fields() ); | ||
filter = renderer->capabilities() & QgsFeatureRenderer::Filter; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set request's filter expression here to renderer->filter()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, if I do request.setFilterExpression(renderer->filter());
with a request.setSubsetOfAttributes( QgsAttributeList() );
before. I can't snap on features when the style is RuleBasedRenderer and one of filter is unchecked. For a layer with a categorized renderer, if you uncheck a category, invisible feature can be snapped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's why in this branch you should set the subset of attributes to those required by the renderer (renderer->usedAttributes())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh yes. Thank you!
src/core/qgspointlocator.cpp
Outdated
@@ -832,6 +875,8 @@ void QgsPointLocator::onFeatureDeleted( QgsFeatureId fid ) | |||
mRTree->deleteData( rect2region( mGeoms[fid]->boundingBox() ), fid ); | |||
delete mGeoms.take( fid ); | |||
} | |||
|
|||
mFeatureIds.remove( fid ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this isn't needed - it's just a leftover from an earlier version
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/core/qgssnappingutils.h
Outdated
* | ||
* \returns the QgsFeatureIds to index | ||
*/ | ||
QgsFeatureIds findVisibleFeatures( QgsVectorLayer *layer ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove leftover declaration
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
Great work @lbartoletti - this iteration looks excellent |
Are you OK to merge it now ? Thank you all. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work @lbartoletti however I think we need to sort out the performance issue when layer gets modified before merging this...
@@ -331,6 +331,7 @@ bool QgsSymbolLegendNode::setData( const QVariant &value, int role ) | |||
vlayer->renderer()->checkLegendSymbolItem( mItem.ruleKey(), value == Qt::Checked ); | |||
|
|||
emit dataChanged(); | |||
emit vlayer->styleChanged(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to replace this with vlayer->emitStyleChanged() - we should not emit signals on behalf of other classes...
src/core/qgspointlocator.cpp
Outdated
@@ -636,6 +637,9 @@ QgsPointLocator::QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateRefe | |||
connect( mLayer, &QgsVectorLayer::featureDeleted, this, &QgsPointLocator::onFeatureDeleted ); | |||
connect( mLayer, &QgsVectorLayer::geometryChanged, this, &QgsPointLocator::onGeometryChanged ); | |||
connect( mLayer, &QgsVectorLayer::dataChanged, this, &QgsPointLocator::destroyIndex ); | |||
connect( mLayer, &QgsVectorLayer::rendererChanged, this, &QgsPointLocator::destroyIndex ); | |||
connect( mLayer, &QgsVectorLayer::styleChanged, this, &QgsPointLocator::destroyIndex ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the index should get destroyed only when the point locator is dependent on visible features
src/core/qgspointlocator.cpp
Outdated
@@ -636,6 +637,9 @@ QgsPointLocator::QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateRefe | |||
connect( mLayer, &QgsVectorLayer::featureDeleted, this, &QgsPointLocator::onFeatureDeleted ); | |||
connect( mLayer, &QgsVectorLayer::geometryChanged, this, &QgsPointLocator::onGeometryChanged ); | |||
connect( mLayer, &QgsVectorLayer::dataChanged, this, &QgsPointLocator::destroyIndex ); | |||
connect( mLayer, &QgsVectorLayer::rendererChanged, this, &QgsPointLocator::destroyIndex ); | |||
connect( mLayer, &QgsVectorLayer::styleChanged, this, &QgsPointLocator::destroyIndex ); | |||
connect( mLayer, &QgsVectorLayer::layerModified, this, &QgsPointLocator::destroyIndex ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a feeling this may be a blocker: this effectively means that even with every single change of geometry we would be destroying the index - that's going to make digitizing slow... Currently if there is e.g. 1-2 seconds initial lag when the index if first built, such lag would happen after every single move of a node when editing geometries...
Why is it necessary to destroy the index? I would expect we just need to modify existing handlers of featureAdded, featureDeleted, geometryChanged signal handlers to accommodate renderer-dependent search.
src/core/qgspointlocator.cpp
Outdated
mContext = std::unique_ptr<QgsRenderContext>( new QgsRenderContext( context ) ); | ||
|
||
destroyIndex(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to me there is no way to turn off visibility-dependent behavior of point locator after it has been enabled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible to configure the behavior in the preferences and then rebuild the index
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it necessary to destroy the index?
It's necessary to destroy the index, if you change the style of your layer, since the subject is especially to filter invisible feature.
Initially there was a bug when you checked a "sub style". I added possible cases.
In use, only styleChanged is really needed. What do you think if I do this:
remove:
connect( mLayer, &QgsVectorLayer::rendererChanged, this, &QgsPointLocator::destroyIndex ); connect( mLayer, &QgsVectorLayer::styleChanged, this, &QgsPointLocator::destroyIndex ); connect( mLayer, &QgsVectorLayer::layerModified, this, &QgsPointLocator::destroyIndex );
add connect( mLayer, &QgsVectorLayer::styleChanged, this, &QgsPointLocator::destroyIndex );
in QgsPointLocator::setRenderContext
src/core/qgspointlocator.cpp
Outdated
if ( ctx && renderer ) | ||
{ | ||
ctx->expressionContext().setFeature( f ); | ||
if ( filter && !renderer->willRenderFeature( f, *ctx ) ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like the "filter" variable could be already checked in the "if" statement two lines above...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right. done.
src/core/qgspointlocator.h
Outdated
@@ -278,11 +285,15 @@ class CORE_EXPORT QgsPointLocator : public QObject | |||
//! flag whether the layer is currently empty (i.e. mRTree is null but it is not necessary to rebuild it) | |||
bool mIsEmptyLayer; | |||
|
|||
QgsFeatureIds mFeatureIds; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not used anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes a leftover...
QVERIFY( !m.hasVertex() ); | ||
|
||
u.setEnableSnappingForInvisibleFeature( true ); | ||
mVL->styleChanged(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should not be necessary to emit styleChanged() here - the setEnableSnappingForInvisibleFeature() should take care of the invalidation of point locators.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes right,
If I have a QgsPointLocator instance with a QgsRenderContext assigned, there is no way to unassign it: I think the setRenderContext() method should have pointer to QgsRenderContext as an argument - if it is nullptr, the internal mContext would be also reset to nullptr. Moving of connect() to setRenderContext() and avoiding layerModified() signal looks good, but I see two new problems:
|
… and onAttributeValueChanged
@wonder-sk I have added support onFeatureAdded and onAttributeValueChanged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great - nearly ready to merge.
Could you please add a test that check that a feature is registered (or not) when it is added to a layer, depending on whether it is visible or not?
src/core/qgspointlocator.cpp
Outdated
@@ -792,6 +842,30 @@ void QgsPointLocator::onFeatureAdded( QgsFeatureId fid ) | |||
if ( !f.hasGeometry() ) | |||
return; | |||
|
|||
std::unique_ptr< QgsFeatureRenderer > renderer( mLayer->renderer() ? mLayer->renderer()->clone() : nullptr ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to be cloned inside "if ( mContext )" block, so we do not copy it when snapping also to invisible features.
src/core/qgspointlocator.cpp
Outdated
Q_UNUSED( idx ); | ||
Q_UNUSED( value ); | ||
onFeatureDeleted( fid ); | ||
onFeatureAdded( fid ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two functions should be called only when snapping to visible features...
@wonder-sk is it ok for you? Thanks. |
Looking good! |
Thank you all. |
Docs for this PR qgis/QGIS#6750
@lbartoletti it should also not snap to layers not visible within that zoom when digitizing. Do you want me to file a bug? |
Also, features not available for snapping can be traced. Now we have inconsistent behaviours for snapping and tracing. |
@saberraz yes please, create this two issues. Thanks. |
Description
Following the discussions, I open a new PR with modifications for #6657 / Fixes #16838.
If you are ok with the new propositions, I will add tests
A comment, I thought that by activating/deactivating a style, the styleChanged or rendererChanged signal would be emitted. This is not the case, I added the repaintRequested signal. See the video
I tested on a data set with the cadastre and all networks (water, sanitation, road, etc.) on an old computer and for me it is usable.
Checklist
fixes #11111
in the commit message next to the description[FEATURE]
in the commit message[needs-docs]
in the commit message and containt sufficient information in the commit message to be documentedscripts/prepare-commit.sh
script before each commit