Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[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 authored and nirvn committed Feb 15, 2019
1 parent 312d143 commit 19ff682
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 30 deletions.
15 changes: 15 additions & 0 deletions python/core/auto_generated/qgspointlocator.sip.in
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/app/vertextool/qgsvertexeditor.cpp
Expand Up @@ -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 );
Expand Down
170 changes: 141 additions & 29 deletions src/app/vertextool/qgsvertextool.cpp
Expand Up @@ -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 )
Expand Down Expand Up @@ -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 );
}
}
}

Expand All @@ -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 );
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 )
Expand Down
39 changes: 39 additions & 0 deletions src/app/vertextool/qgsvertextool.h
Expand Up @@ -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 );

Expand Down Expand Up @@ -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
Expand Down
68 changes: 68 additions & 0 deletions src/core/qgspointlocator.cpp
Expand Up @@ -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;
};


////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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 )
{
Expand Down

0 comments on commit 19ff682

Please sign in to comment.