diff --git a/python/core/auto_generated/qgspointlocator.sip.in b/python/core/auto_generated/qgspointlocator.sip.in index 5fb6829f360..47cdc0a2a40 100644 --- a/python/core/auto_generated/qgspointlocator.sip.in +++ b/python/core/auto_generated/qgspointlocator.sip.in @@ -200,6 +200,21 @@ Optional filter may discard unwanted matches. Override of edgesInRect that construct rectangle from a center point and tolerance %End + MatchList verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = 0 ); +%Docstring +Find vertices within a specified recangle +Optional filter may discard unwanted matches. + +.. versionadded:: 3.6 +%End + + MatchList verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0 ); +%Docstring +Override of verticesInRect that construct rectangle from a center point and tolerance + +.. versionadded:: 3.6 +%End + MatchList pointInPolygon( const QgsPointXY &point ); %Docstring diff --git a/src/app/vertextool/qgsvertexeditor.cpp b/src/app/vertextool/qgsvertexeditor.cpp index 9c8b7895067..f97ff7f7a0e 100644 --- a/src/app/vertextool/qgsvertexeditor.cpp +++ b/src/app/vertextool/qgsvertexeditor.cpp @@ -306,7 +306,7 @@ QgsVertexEditor::QgsVertexEditor( QgsMapCanvas *canvas ) layout->setContentsMargins( 0, 0, 0, 0 ); mHintLabel = new QLabel( this ); - mHintLabel->setText( QStringLiteral( "%1\n\n%2" ).arg( tr( "Right click on the edge of an editable feature to show its table of vertices." ), + mHintLabel->setText( QStringLiteral( "%1\n\n%2" ).arg( tr( "Right click on an editable feature to show its table of vertices." ), tr( "When a feature is bound to this panel, dragging a rectangle to select vertices on the canvas will only select those of the bound feature." ) ) ); mHintLabel->setWordWrap( true ); mHintLabel->setAlignment( Qt::AlignHCenter | Qt::AlignVCenter ); diff --git a/src/app/vertextool/qgsvertextool.cpp b/src/app/vertextool/qgsvertextool.cpp index ffb58f3f76b..4d53c07cf1f 100644 --- a/src/app/vertextool/qgsvertextool.cpp +++ b/src/app/vertextool/qgsvertextool.cpp @@ -457,33 +457,6 @@ void QgsVertexTool::cadCanvasPressEvent( QgsMapMouseEvent *e ) if ( !mDraggingVertex && !mDraggingEdge ) mSelectionRectStartPos.reset( new QPoint( e->pos() ) ); } - - if ( e->button() == Qt::RightButton ) - { - if ( !mSelectionRect && !mDraggingVertex && !mDraggingEdge ) - { - QgsPointLocator::Match m = snapToEditableLayer( e ); - if ( !m.isValid() ) - { - // as the last resort check if we are on top of a feature if there is no vertex or edge snap - m = snapToPolygonInterior( e ); - } - - if ( m.isValid() && m.layer() ) - { - updateVertexEditor( m.layer(), m.featureId() ); - } - else - { - // there's really nothing under the cursor - let's deselect any feature we may have - mSelectedFeature.reset(); - if ( mVertexEditor ) - { - mVertexEditor->updateEditor( nullptr ); - } - } - } - } } void QgsVertexTool::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) @@ -593,8 +566,17 @@ void QgsVertexTool::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) } else if ( e->button() == Qt::RightButton ) { - // cancel action - stopDragging(); + if ( mDraggingVertex || mDraggingEdge ) + { + // cancel action + stopDragging(); + } + else if ( !mSelectionRect ) + { + // Right-click to select/delect a feature for editing (also gets selected in vertex editor). + // If there are multiple features at one location, cycle through them with subsequent right clicks. + tryToSelectFeature( e ); + } } } @@ -603,6 +585,13 @@ void QgsVertexTool::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) void QgsVertexTool::cadCanvasMoveEvent( QgsMapMouseEvent *e ) { + if ( mSelectedFeatureAlternatives && ( e->pos() - mSelectedFeatureAlternatives->screenPoint ).manhattanLength() >= QApplication::startDragDistance() ) + { + // as soon as the mouse moves more than just a tiny bit, previously stored alternatives info + // is probably not valid anymore and will need to be re-calculated + mSelectedFeatureAlternatives.reset(); + } + if ( mSelectionMethod == SelectionRange ) { rangeMethodMoveEvent( e ); @@ -684,6 +673,9 @@ void QgsVertexTool::mouseMoveDraggingEdge( QgsMapMouseEvent *e ) void QgsVertexTool::canvasDoubleClickEvent( QgsMapMouseEvent *e ) { + if ( e->button() != Qt::LeftButton ) + return; + QgsPointLocator::Match m = snapToEditableLayer( e ); if ( !m.hasEdge() ) return; @@ -852,6 +844,126 @@ QgsPointLocator::Match QgsVertexTool::snapToPolygonInterior( QgsMapMouseEvent *e } +QList QgsVertexTool::findEditableLayerMatches( const QgsPointXY &mapPoint, QgsVectorLayer *layer ) +{ + QgsPointLocator::MatchList matchList; + + if ( !layer->isEditable() ) + return matchList; + + QgsSnappingUtils *snapUtils = canvas()->snappingUtils(); + QgsPointLocator *locator = snapUtils->locatorForLayer( layer ); + + if ( layer->geometryType() == QgsWkbTypes::PolygonGeometry ) + { + matchList << locator->pointInPolygon( mapPoint ); + } + + double tolerance = QgsTolerance::vertexSearchRadius( canvas()->mapSettings() ); + matchList << locator->edgesInRect( mapPoint, tolerance ); + matchList << locator->verticesInRect( mapPoint, tolerance ); + + return matchList; +} + + +QSet > QgsVertexTool::findAllEditableFeatures( const QgsPointXY &mapPoint ) +{ + QSet< QPair > alternatives; + + // if there is a current layer, it should have priority over other layers + // because sometimes there may be match from multiple layers at one location + // and selecting current layer is an easy way for the user to prioritize a layer + if ( QgsVectorLayer *currentVlayer = currentVectorLayer() ) + { + for ( const QgsPointLocator::Match &m : findEditableLayerMatches( mapPoint, currentVlayer ) ) + { + alternatives.insert( qMakePair( m.layer(), m.featureId() ) ); + } + } + + if ( mMode == AllLayers ) + { + const auto layers = canvas()->layers(); + for ( QgsMapLayer *layer : layers ) + { + QgsVectorLayer *vlayer = qobject_cast( layer ); + if ( !vlayer ) + continue; + + for ( const QgsPointLocator::Match &m : findEditableLayerMatches( mapPoint, vlayer ) ) + { + alternatives.insert( qMakePair( m.layer(), m.featureId() ) ); + } + } + } + + return alternatives; +} + + +void QgsVertexTool::tryToSelectFeature( QgsMapMouseEvent *e ) +{ + if ( !mSelectedFeatureAlternatives ) + { + // this is the first right-click on this location so we currently do not have information + // about editable features at this mouse location - let's build the alternatives info + QSet< QPair > alternatives = findAllEditableFeatures( toMapCoordinates( e->pos() ) ); + if ( !alternatives.isEmpty() ) + { + QgsPointLocator::Match m = snapToEditableLayer( e ); + if ( !m.isValid() ) + { + // as the last resort check if we are on top of a feature if there is no vertex or edge snap + m = snapToPolygonInterior( e ); + } + + mSelectedFeatureAlternatives.reset( new SelectedFeatureAlternatives ); + mSelectedFeatureAlternatives->screenPoint = e->pos(); + mSelectedFeatureAlternatives->index = 0; + if ( m.isValid() ) + { + // ideally the feature that would get normally highlighted should be also the first choice + // because as user moves mouse, different features are highlighted, so the highlighted feature + // should be first to get selected + QPair firstChoice( m.layer(), m.featureId() ); + mSelectedFeatureAlternatives->alternatives.append( firstChoice ); + alternatives.remove( firstChoice ); + } + mSelectedFeatureAlternatives->alternatives.append( alternatives.toList() ); + } + } + else + { + // we have had right-click before on this mouse location - so let's just cycle in our alternatives + // move to the next alternative + if ( mSelectedFeatureAlternatives->index < mSelectedFeatureAlternatives->alternatives.count() - 1 ) + ++mSelectedFeatureAlternatives->index; + else + mSelectedFeatureAlternatives->index = -1; + } + + if ( mSelectedFeatureAlternatives && mSelectedFeatureAlternatives->index != -1 ) + { + // we have a feature to select + QPair alternative = mSelectedFeatureAlternatives->alternatives.at( mSelectedFeatureAlternatives->index ); + updateVertexEditor( alternative.first, alternative.second ); + updateFeatureBand( QgsPointLocator::Match( QgsPointLocator::Area, alternative.first, alternative.second, 0, QgsPointXY() ) ); + } + else + { + // there's really nothing under the cursor or while cycling through the list of available features + // we got to the end of the list - let's deselect any feature we may have had selected + mSelectedFeature.reset(); + if ( mVertexEditor ) + { + mVertexEditor->updateEditor( nullptr ); + } + updateFeatureBand( QgsPointLocator::Match() ); + } +} + + bool QgsVertexTool::isNearEndpointMarker( const QgsPointXY &mapPoint ) { if ( !mEndpointMarkerCenter ) diff --git a/src/app/vertextool/qgsvertextool.h b/src/app/vertextool/qgsvertextool.h index f86dab1faa4..cba42ccb8c7 100644 --- a/src/app/vertextool/qgsvertextool.h +++ b/src/app/vertextool/qgsvertextool.h @@ -146,8 +146,32 @@ class APP_EXPORT QgsVertexTool : public QgsMapToolAdvancedDigitizing */ QgsPointLocator::Match snapToEditableLayer( QgsMapMouseEvent *e ); + /** + * Tries to find a match in polygon interiors. This is useful for mouse move + * events to keep features highlighted to see their area. + */ QgsPointLocator::Match snapToPolygonInterior( QgsMapMouseEvent *e ); + /** + * Returns a list of all matches at the given map point. That is a concatenation + * of all vertex, edge and area matches (vertex/edge matches using standard search tolerance). + * Layer is only searched if it is editable. + */ + QList findEditableLayerMatches( const QgsPointXY &mapPoint, QgsVectorLayer *layer ); + + /** + * Returns a set of all matches at the given map point from all editable layers (respecting the mode). + * The set does not contain only the closest match from each layer, but all matches in the standard + * vertex search tolerance. It also includes area matches. + */ + QSet > findAllEditableFeatures( const QgsPointXY &mapPoint ); + + /** + * Implements behavior for mouse right-click to select a feature for editing (and in case of multiple + * features in one place, repeated right-clicks will cycle through the features). + */ + void tryToSelectFeature( QgsMapMouseEvent *e ); + //! check whether we are still close to the mEndpointMarker bool isNearEndpointMarker( const QgsPointXY &mapPoint ); @@ -416,6 +440,21 @@ class APP_EXPORT QgsVertexTool : public QgsMapToolAdvancedDigitizing //! Dock widget which allows editing vertices std::unique_ptr mVertexEditor; + /** + * Data structure that stores alternative features to the currently selected (locked) feature. + * This is used when user clicks with right mouse button multiple times in one location + * to easily switch to the desired feature. + */ + struct SelectedFeatureAlternatives + { + QPoint screenPoint; + QList< QPair > alternatives; + int index = -1; + }; + + //! Keeps information about other possible features to select with right click. Null if no info is currently held. + std::unique_ptr mSelectedFeatureAlternatives; + // support for validation of geometries //! data structure for validation of one geometry of a vector layer diff --git a/src/core/qgspointlocator.cpp b/src/core/qgspointlocator.cpp index dc911488d2d..45fd087b412 100644 --- a/src/core/qgspointlocator.cpp +++ b/src/core/qgspointlocator.cpp @@ -564,6 +564,52 @@ class QgsPointLocator_VisitorEdgesInRect : public IVisitor QgsPointLocator::MatchFilter *mFilter = nullptr; }; +//////////////////////////////////////////////////////////////////////////// + +/** + * \ingroup core + * Helper class used when traversing the index looking for vertices - builds a list of matches. + * \note not available in Python bindings +*/ +class QgsPointLocator_VisitorVerticesInRect : public IVisitor +{ + public: + QgsPointLocator_VisitorVerticesInRect( QgsPointLocator *pl, QgsPointLocator::MatchList &lst, const QgsRectangle &srcRect, QgsPointLocator::MatchFilter *filter = nullptr ) + : mLocator( pl ) + , mList( lst ) + , mSrcRect( srcRect ) + , mFilter( filter ) + {} + + void visitNode( const INode &n ) override { Q_UNUSED( n ); } + void visitData( std::vector &v ) override { Q_UNUSED( v ); } + + void visitData( const IData &d ) override + { + QgsFeatureId id = d.getIdentifier(); + const QgsGeometry *geom = mLocator->mGeoms.value( id ); + + for ( QgsAbstractGeometry::vertex_iterator it = geom->vertices_begin(); it != geom->vertices_end(); ++it ) + { + if ( mSrcRect.contains( *it ) ) + { + QgsPointLocator::Match m( QgsPointLocator::Vertex, mLocator->mLayer, id, 0, *it, geom->vertexNrFromVertexId( it.vertexId() ) ); + + // in range queries the filter may reject some matches + if ( mFilter && !mFilter->acceptMatch( m ) ) + continue; + + mList << m; + } + } + } + + private: + QgsPointLocator *mLocator = nullptr; + QgsPointLocator::MatchList &mList; + QgsRectangle mSrcRect; + QgsPointLocator::MatchFilter *mFilter = nullptr; +}; //////////////////////////////////////////////////////////////////////////// @@ -1020,6 +1066,28 @@ QgsPointLocator::MatchList QgsPointLocator::edgesInRect( const QgsPointXY &point return edgesInRect( rect, filter ); } +QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter ) +{ + if ( !mRTree ) + { + init(); + if ( !mRTree ) // still invalid? + return MatchList(); + } + + MatchList lst; + QgsPointLocator_VisitorVerticesInRect visitor( this, lst, rect, filter ); + mRTree->intersectsWithQuery( rect2region( rect ), visitor ); + + return lst; +} + +QgsPointLocator::MatchList QgsPointLocator::verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter ) +{ + QgsRectangle rect( point.x() - tolerance, point.y() - tolerance, point.x() + tolerance, point.y() + tolerance ); + return verticesInRect( rect, filter ); +} + QgsPointLocator::MatchList QgsPointLocator::pointInPolygon( const QgsPointXY &point ) { diff --git a/src/core/qgspointlocator.h b/src/core/qgspointlocator.h index 1e136b49c2f..9fc9251182c 100644 --- a/src/core/qgspointlocator.h +++ b/src/core/qgspointlocator.h @@ -256,6 +256,19 @@ class CORE_EXPORT QgsPointLocator : public QObject //! Override of edgesInRect that construct rectangle from a center point and tolerance MatchList edgesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr ); + /** + * Find vertices within a specified recangle + * Optional filter may discard unwanted matches. + * \since QGIS 3.6 + */ + MatchList verticesInRect( const QgsRectangle &rect, QgsPointLocator::MatchFilter *filter = nullptr ); + + /** + * Override of verticesInRect that construct rectangle from a center point and tolerance + * \since QGIS 3.6 + */ + MatchList verticesInRect( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr ); + // point-in-polygon query // TODO: function to return just the first match? @@ -300,6 +313,7 @@ class CORE_EXPORT QgsPointLocator : public QObject friend class QgsPointLocator_VisitorNearestEdge; friend class QgsPointLocator_VisitorArea; friend class QgsPointLocator_VisitorEdgesInRect; + friend class QgsPointLocator_VisitorVerticesInRect; }; diff --git a/tests/src/core/testqgspointlocator.cpp b/tests/src/core/testqgspointlocator.cpp index 116a04a4810..8b7138e848f 100644 --- a/tests/src/core/testqgspointlocator.cpp +++ b/tests/src/core/testqgspointlocator.cpp @@ -210,6 +210,24 @@ class TestQgsPointLocator : public QObject QCOMPARE( lst3.count(), 2 ); } + void testVerticesInTolerance() + { + QgsPointLocator loc( mVL ); + QgsPointLocator::MatchList lst = loc.verticesInRect( QgsPointXY( 0, 2 ), 0.5 ); + QCOMPARE( lst.count(), 0 ); + + QgsPointLocator::MatchList lst2 = loc.verticesInRect( QgsPointXY( 0, 1.5 ), 0.5 ); + QCOMPARE( lst2.count(), 2 ); // one matching point, but it is the first point in ring, so it is duplicated + QCOMPARE( lst2[0].vertexIndex(), 0 ); + QCOMPARE( lst2[1].vertexIndex(), 3 ); + + QgsPointLocator::MatchList lst3 = loc.verticesInRect( QgsPointXY( 0, 1.5 ), 1 ); + QCOMPARE( lst3.count(), 3 ); + QCOMPARE( lst3[0].vertexIndex(), 0 ); + QCOMPARE( lst3[1].vertexIndex(), 2 ); + QCOMPARE( lst3[2].vertexIndex(), 3 ); + } + void testLayerUpdates() {