Skip to content
Permalink
Browse files

[vertex tool] right-click to loop through editable features

Until now mouse right-click could only select and deselect the highlighted
feature to "lock" vertex tool (and numerical editor) to it, so that it is
easier to focus only on editing of the particular feature. It was however
still difficult to pick the right feature in case there were multiple
features in one location or very close to each other. This is now solved
by the fact that repeated right button clicks will loop through the editable
features. So if there are two features in one location (A, B) then repeated
right-clicks will select: A - B - nothing - A - B - nothing ...
  • Loading branch information
wonder-sk committed Feb 5, 2019
1 parent c62a4ae commit 4f905cc2e8a6cd6dc19bd4d91b2afc3021faba6b
@@ -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
@@ -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 );
@@ -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<QgsPointLocator::Match> 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<QPair<QgsVectorLayer *, QgsFeatureId> > QgsVertexTool::findAllEditableFeatures( const QgsPointXY &mapPoint )
{
QSet< QPair<QgsVectorLayer *, QgsFeatureId> > 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<QgsVectorLayer *>( 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<QgsVectorLayer *, QgsFeatureId> > 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<QgsVectorLayer *, QgsFeatureId> 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<QgsVectorLayer *, QgsFeatureId> 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 )
@@ -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<QgsPointLocator::Match> 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<QPair<QgsVectorLayer *, QgsFeatureId> > 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<QgsVertexEditor> 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<QgsVectorLayer *, QgsFeatureId> > 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<SelectedFeatureAlternatives> mSelectedFeatureAlternatives;

// support for validation of geometries

//! data structure for validation of one geometry of a vector layer
@@ -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<const IData *> &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 )
{

0 comments on commit 4f905cc

Please sign in to comment.
You can’t perform that action at this time.