[FEATURE] Add new snapping option for "Line Endpoints"

When enabled, this snapping mode snaps to the beginning or end
vertex of lines only. When snapping to a polygon layer, only
the first vertex in rings will be snapped to.

Refs Natural resources Canada Contract: 3000720707
This commit is contained in:
Nyall Dawson 2021-03-01 13:38:33 +10:00
parent 6d40826766
commit b8baabf3f4
18 changed files with 601 additions and 157 deletions

View File

@ -401,7 +401,7 @@
<file>themes/default/mActionSaveEdits.svg</file>
<file>themes/default/mActionSaveMapAsImage.svg</file>
<file>themes/default/mActionScaleBar.svg</file>
<file>themes/default/mActionScaleFeature.svg</file>
<file>themes/default/mActionScaleFeature.svg</file>
<file>themes/default/mActionScriptOpen.svg</file>
<file>themes/default/mActionSelect.svg</file>
<file>themes/default/mActionSelectAll.svg</file>
@ -907,6 +907,7 @@
<file>themes/default/transformation.svg</file>
<file>themes/default/mIconCodeEditor.svg</file>
<file>themes/default/console/iconSyntaxErrorConsoleParams.svg</file>
<file>themes/default/mIconSnappingEndpoint.svg</file>
</qresource>
<qresource prefix="/images/tips">
<file alias="symbol_levels.png">qgis_tips/symbol_levels.png</file>

View File

@ -0,0 +1 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M 5.5,5.5 12,18 18,8" fill="none" stroke="#8cbe8c" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><path d="m 4.5,4.5 h 3 v 3 h -3 z" fill="#bebebe" stroke="#8c8c8c"/><path d="m 16.5,6.5 h 3 v 3 h -3 z" fill="#bebebe" stroke="#8c8c8c"/></svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@ -94,6 +94,7 @@ Configure render context - if not ``None``, it will use to index only visible f
Area,
Centroid,
MiddleOfSegment,
LineEndpoint,
All
};
@ -134,23 +135,30 @@ construct invalid match
bool isValid() const;
bool hasVertex() const;
%Docstring
Returns true if the Match is a vertex
Returns ``True`` if the Match is a vertex
%End
bool hasEdge() const;
%Docstring
Returns true if the Match is an edge
Returns ``True`` if the Match is an edge
%End
bool hasCentroid() const;
%Docstring
Returns true if the Match is a centroid
Returns ``True`` if the Match is a centroid
%End
bool hasArea() const;
%Docstring
Returns true if the Match is an area
Returns ``True`` if the Match is an area
%End
bool hasMiddleSegment() const;
%Docstring
Returns true if the Match is the middle of a segment
Returns ``True`` if the Match is the middle of a segment
%End
bool hasLineEndpoint() const;
%Docstring
Returns ``True`` if the Match is a line endpoint (start or end vertex).
.. versionadded:: 3.20
%End
double distance() const;
@ -231,6 +239,15 @@ Optional filter may discard unwanted matches.
This method is either blocking or non blocking according to ``relaxed`` parameter passed
.. versionadded:: 3.12
%End
Match nearestLineEndpoints( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
%Docstring
Find nearest line endpoint (start or end 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
.. versionadded:: 3.20
%End
Match nearestEdge( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );

View File

@ -47,6 +47,7 @@ This is a container for configuration of the snapping of the project
AreaFlag,
CentroidFlag,
MiddleOfSegmentFlag,
LineEndpointFlag,
};
typedef QFlags<QgsSnappingConfig::SnappingTypes> SnappingTypeFlag;
@ -58,14 +59,21 @@ This is a container for configuration of the snapping of the project
PerLayer
};
static const QString snappingTypeFlagToString( SnappingTypeFlag type );
static QString snappingTypeFlagToString( SnappingTypeFlag type );
%Docstring
Convenient method to returns the translated name of the enum type
QgsSnappingConfig.SnappingTypeFlag
QgsSnappingConfig.SnappingTypeFlag.
.. versionadded:: 3.12
%End
static QIcon snappingTypeFlagToIcon( SnappingTypeFlag type );
%Docstring
Convenient method to return an icon corresponding to the enum type
QgsSnappingConfig.SnappingTypeFlag.
.. versionadded:: 3.20
%End
class IndividualLayerSettings
{

View File

@ -1075,12 +1075,23 @@ QgsOptions::QgsOptions( QWidget *parent, Qt::WindowFlags fl, const QList<QgsOpti
//default snap mode
mSnappingEnabledDefault->setChecked( mSettings->value( QStringLiteral( "/qgis/digitizing/default_snap_enabled" ), false ).toBool() );
mDefaultSnapModeComboBox->addItem( tr( "No Snapping" ), QgsSnappingConfig::NoSnapFlag );
mDefaultSnapModeComboBox->addItem( tr( "Vertex" ), QgsSnappingConfig::VertexFlag );
mDefaultSnapModeComboBox->addItem( tr( "Segment" ), QgsSnappingConfig::SegmentFlag );
mDefaultSnapModeComboBox->addItem( tr( "Centroid" ), QgsSnappingConfig::CentroidFlag );
mDefaultSnapModeComboBox->addItem( tr( "Middle of Segments" ), QgsSnappingConfig::MiddleOfSegmentFlag );
mDefaultSnapModeComboBox->addItem( tr( "Area" ), QgsSnappingConfig::AreaFlag );
for ( QgsSnappingConfig::SnappingTypes type :
{
QgsSnappingConfig::NoSnapFlag,
QgsSnappingConfig::VertexFlag,
QgsSnappingConfig::SegmentFlag,
QgsSnappingConfig::CentroidFlag,
QgsSnappingConfig::MiddleOfSegmentFlag,
QgsSnappingConfig::LineEndpointFlag,
QgsSnappingConfig::AreaFlag,
} )
{
mDefaultSnapModeComboBox->addItem( QgsSnappingConfig::snappingTypeFlagToIcon( type ),
QgsSnappingConfig::snappingTypeFlagToString( type ),
type );
}
QgsSnappingConfig::SnappingTypeFlag defaultSnapMode = mSettings->flagValue( QStringLiteral( "/qgis/digitizing/default_snap_type" ), QgsSnappingConfig::VertexFlag );
mDefaultSnapModeComboBox->setCurrentIndex( mDefaultSnapModeComboBox->findData( static_cast<int>( defaultSnapMode ) ) );
mDefaultSnappingToleranceSpinBox->setValue( mSettings->value( QStringLiteral( "/qgis/digitizing/default_snapping_tolerance" ), Qgis::DEFAULT_SNAP_TOLERANCE ).toDouble() );

View File

@ -71,21 +71,22 @@ QWidget *QgsSnappingLayerDelegate::createEditor( QWidget *parent, const QStyleOp
mTypeButton->setToolTip( tr( "Snapping Type" ) );
mTypeButton->setPopupMode( QToolButton::InstantPopup );
SnapTypeMenu *typeMenu = new SnapTypeMenu( tr( "Set Snapping Mode" ), parent );
QAction *mVertexAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingVertex.svg" ) ), tr( "Vertex" ), typeMenu );
QAction *mSegmentAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingSegment.svg" ) ), tr( "Segment" ), typeMenu );
QAction *mAreaAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingArea.svg" ) ), tr( "Area" ), typeMenu );
QAction *mCentroidAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingCentroid.svg" ) ), tr( "Centroid" ), typeMenu );
QAction *mMiddleAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingMiddle.svg" ) ), tr( "Middle of Segments" ), typeMenu );
mVertexAction->setCheckable( true );
mSegmentAction->setCheckable( true );
mAreaAction->setCheckable( true );
mCentroidAction->setCheckable( true );
mMiddleAction->setCheckable( true );
typeMenu->addAction( mVertexAction );
typeMenu->addAction( mSegmentAction );
typeMenu->addAction( mAreaAction );
typeMenu->addAction( mCentroidAction );
typeMenu->addAction( mMiddleAction );
for ( QgsSnappingConfig::SnappingTypes type :
{
QgsSnappingConfig::VertexFlag,
QgsSnappingConfig::SegmentFlag,
QgsSnappingConfig::AreaFlag,
QgsSnappingConfig::CentroidFlag,
QgsSnappingConfig::MiddleOfSegmentFlag,
QgsSnappingConfig::LineEndpointFlag
} )
{
QAction *action = new QAction( QgsSnappingConfig::snappingTypeFlagToIcon( type ), QgsSnappingConfig::snappingTypeFlagToString( type ), typeMenu );
action->setData( type );
action->setCheckable( true );
typeMenu->addAction( action );
}
mTypeButton->setMenu( typeMenu );
mTypeButton->setObjectName( QStringLiteral( "SnappingTypeButton" ) );
mTypeButton->setToolButtonStyle( Qt::ToolButtonTextBesideIcon );
@ -161,13 +162,11 @@ void QgsSnappingLayerDelegate::setEditorData( QWidget *editor, const QModelIndex
QToolButton *tb = qobject_cast<QToolButton *>( editor );
if ( tb )
{
QList<QAction *>actions = tb->menu()->actions();
actions.at( 0 )->setChecked( type & QgsSnappingConfig::VertexFlag );
actions.at( 1 )->setChecked( type & QgsSnappingConfig::SegmentFlag );
actions.at( 2 )->setChecked( type & QgsSnappingConfig::AreaFlag );
actions.at( 3 )->setChecked( type & QgsSnappingConfig::CentroidFlag );
actions.at( 4 )->setChecked( type & QgsSnappingConfig::MiddleOfSegmentFlag );
const QList<QAction *> actions = tb->menu()->actions();
for ( QAction *action : actions )
{
action->setChecked( type & static_cast< QgsSnappingConfig::SnappingTypeFlag >( action->data().toInt() ) );
}
}
}
else if ( index.column() == QgsSnappingLayerTreeModel::ToleranceColumn )
@ -212,18 +211,17 @@ void QgsSnappingLayerDelegate::setModelData( QWidget *editor, QAbstractItemModel
QToolButton *t = qobject_cast<QToolButton *>( editor );
if ( t )
{
QList<QAction *> actions = t->menu()->actions();
const QList<QAction *> actions = t->menu()->actions();
QgsSnappingConfig::SnappingTypeFlag type = QgsSnappingConfig::NoSnapFlag;
if ( actions.at( 0 )->isChecked() )
type = static_cast<QgsSnappingConfig::SnappingTypeFlag>( type | QgsSnappingConfig::VertexFlag );
if ( actions.at( 1 )->isChecked() )
type = static_cast<QgsSnappingConfig::SnappingTypeFlag>( type | QgsSnappingConfig::SegmentFlag );
if ( actions.at( 2 )->isChecked() )
type = static_cast<QgsSnappingConfig::SnappingTypeFlag>( type | QgsSnappingConfig::AreaFlag );
if ( actions.at( 3 )->isChecked() )
type = static_cast<QgsSnappingConfig::SnappingTypeFlag>( type | QgsSnappingConfig::CentroidFlag );
if ( actions.at( 4 )->isChecked() )
type = static_cast<QgsSnappingConfig::SnappingTypeFlag>( type | QgsSnappingConfig::MiddleOfSegmentFlag );
for ( QAction *action : actions )
{
if ( action->isChecked() )
{
const QgsSnappingConfig::SnappingTypeFlag actionFlag = static_cast<QgsSnappingConfig::SnappingTypeFlag>( action->data().toInt() );
type = static_cast<QgsSnappingConfig::SnappingTypeFlag>( type | actionFlag );
}
}
model->setData( index, static_cast<int>( type ), Qt::EditRole );
}

View File

@ -188,21 +188,24 @@ QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas,
mTypeButton->setToolTip( tr( "Snapping Type" ) );
mTypeButton->setPopupMode( QToolButton::InstantPopup );
SnapTypeMenu *typeMenu = new SnapTypeMenu( tr( "Set Snapping Mode" ), this );
mVertexAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingVertex.svg" ) ), tr( "Vertex" ), typeMenu );
mSegmentAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingSegment.svg" ) ), tr( "Segment" ), typeMenu );
mAreaAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingArea.svg" ) ), tr( "Area" ), typeMenu );
mCentroidAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingCentroid.svg" ) ), tr( "Centroid" ), typeMenu );
mMiddleAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingMiddle.svg" ) ), tr( "Middle of Segments" ), typeMenu );
mVertexAction->setCheckable( true );
mSegmentAction->setCheckable( true );
mAreaAction->setCheckable( true );
mCentroidAction->setCheckable( true );
mMiddleAction->setCheckable( true );
typeMenu->addAction( mVertexAction );
typeMenu->addAction( mSegmentAction );
typeMenu->addAction( mAreaAction );
typeMenu->addAction( mCentroidAction );
typeMenu->addAction( mMiddleAction );
for ( QgsSnappingConfig::SnappingTypes type :
{
QgsSnappingConfig::VertexFlag,
QgsSnappingConfig::SegmentFlag,
QgsSnappingConfig::AreaFlag,
QgsSnappingConfig::CentroidFlag,
QgsSnappingConfig::MiddleOfSegmentFlag,
QgsSnappingConfig::LineEndpointFlag
} )
{
QAction *action = new QAction( QgsSnappingConfig::snappingTypeFlagToIcon( type ), QgsSnappingConfig::snappingTypeFlagToString( type ), typeMenu );
action->setData( type );
action->setCheckable( true );
typeMenu->addAction( action );
mSnappingFlagActions << action;
}
mTypeButton->setMenu( typeMenu );
mTypeButton->setObjectName( QStringLiteral( "SnappingTypeButton" ) );
if ( mDisplayMode == Widget )
@ -456,37 +459,13 @@ void QgsSnappingWidget::projectSnapSettingsChanged()
updateToleranceDecimals();
}
// Clear
mVertexAction->setChecked( false );
mSegmentAction->setChecked( false );
mAreaAction->setChecked( false );
mCentroidAction->setChecked( false );
mMiddleAction->setChecked( false );
if ( config.typeFlag() & QgsSnappingConfig::VertexFlag )
// update snapping flag actions
for ( QAction *action : qgis::as_const( mSnappingFlagActions ) )
{
mTypeButton->setDefaultAction( mVertexAction );
mVertexAction->setChecked( true );
}
if ( config.typeFlag() & QgsSnappingConfig::SegmentFlag )
{
mTypeButton->setDefaultAction( mSegmentAction );
mSegmentAction->setChecked( true );
}
if ( config.typeFlag() & QgsSnappingConfig::AreaFlag )
{
mTypeButton->setDefaultAction( mAreaAction );
mAreaAction->setChecked( true );
}
if ( config.typeFlag() & QgsSnappingConfig::CentroidFlag )
{
mTypeButton->setDefaultAction( mCentroidAction );
mCentroidAction->setChecked( true );
}
if ( config.typeFlag() & QgsSnappingConfig::MiddleOfSegmentFlag )
{
mTypeButton->setDefaultAction( mMiddleAction );
mMiddleAction->setChecked( true );
const QgsSnappingConfig::SnappingTypeFlag actionFlag = static_cast<QgsSnappingConfig::SnappingTypeFlag>( action->data().toInt() );
action->setChecked( config.typeFlag() & actionFlag );
if ( action->isChecked() )
mTypeButton->setDefaultAction( action );
}
if ( static_cast<QgsTolerance::UnitType>( mUnitsComboBox->currentData().toInt() ) != config.units() )
@ -722,27 +701,27 @@ void QgsSnappingWidget::typeButtonTriggered( QAction *action )
{
unsigned int type = static_cast<int>( mConfig.typeFlag() );
mTypeButton->setDefaultAction( action );
if ( action == mVertexAction )
const QgsSnappingConfig::SnappingTypeFlag actionFlag = static_cast<QgsSnappingConfig::SnappingTypeFlag>( action->data().toInt() );
type ^= actionFlag;
if ( type & actionFlag )
{
type ^= static_cast<int>( QgsSnappingConfig::VertexFlag );
// user checked the action, set as new default
mTypeButton->setDefaultAction( action );
}
else if ( action == mSegmentAction )
else
{
type ^= static_cast<int>( QgsSnappingConfig::SegmentFlag );
}
else if ( action == mAreaAction )
{
type ^= static_cast<int>( QgsSnappingConfig::AreaFlag );
}
else if ( action == mCentroidAction )
{
type ^= static_cast<int>( QgsSnappingConfig::CentroidFlag );
}
else if ( action == mMiddleAction )
{
type ^= static_cast<int>( QgsSnappingConfig::MiddleOfSegmentFlag );
// user unchecked the action -- find out which ones we should set as new default action
for ( QAction *flagAction : qgis::as_const( mSnappingFlagActions ) )
{
if ( type & static_cast<QgsSnappingConfig::SnappingTypeFlag>( flagAction->data().toInt() ) )
{
mTypeButton->setDefaultAction( flagAction );
break;
}
}
}
mConfig.setTypeFlag( static_cast<QgsSnappingConfig::SnappingTypeFlag>( type ) );
mProject->setSnappingConfig( mConfig );
}

View File

@ -157,11 +157,7 @@ class APP_EXPORT QgsSnappingWidget : public QWidget
QAction *mEditAdvancedConfigAction = nullptr;
QToolButton *mTypeButton = nullptr;
QAction *mTypeAction = nullptr; // hide widget does not work on toolbar, action needed
QAction *mVertexAction = nullptr;
QAction *mSegmentAction = nullptr;
QAction *mAreaAction = nullptr;
QAction *mCentroidAction = nullptr;
QAction *mMiddleAction = nullptr;
QList< QAction * > mSnappingFlagActions;
QDoubleSpinBox *mToleranceSpinBox = nullptr;
QgsScaleWidget *mMinScaleWidget = nullptr;
QgsScaleWidget *mMaxScaleWidget = nullptr;

View File

@ -26,6 +26,7 @@
#include "qgsvectorlayerfeatureiterator.h"
#include "qgsexpressioncontextutils.h"
#include "qgslinestring.h"
#include "qgscurvepolygon.h"
#include "qgspointlocatorinittask.h"
#include <spatialindex/SpatialIndex.h>
@ -158,8 +159,8 @@ class QgsPointLocator_VisitorNearestCentroid : public IVisitor
, mFilter( filter )
{}
void visitNode( const INode &n ) override { Q_UNUSED( n ); }
void visitData( std::vector<const IData *> &v ) override { Q_UNUSED( v ); }
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
{
@ -210,8 +211,8 @@ class QgsPointLocator_VisitorNearestMiddleOfSegment: public IVisitor
, mFilter( filter )
{}
void visitNode( const INode &n ) override { Q_UNUSED( n ); }
void visitData( std::vector<const IData *> &v ) override { Q_UNUSED( v ); }
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
{
@ -245,6 +246,111 @@ class QgsPointLocator_VisitorNearestMiddleOfSegment: public IVisitor
QgsPointLocator::MatchFilter *mFilter = nullptr;
};
////////////////////////////////////////////////////////////////////////////
/**
* \ingroup core
* \brief Helper class used when traversing the index looking for line endpoints (start or end vertex) - builds a list of matches.
* \note not available in Python bindings
* \since QGIS 3.20
*/
class QgsPointLocator_VisitorNearestLineEndpoint : public IVisitor
{
public:
/**
* \ingroup core
* \brief Helper class used when traversing the index looking for line endpoints (start or end vertex) - builds a list of matches.
*/
QgsPointLocator_VisitorNearestLineEndpoint( QgsPointLocator *pl, QgsPointLocator::Match &m, const QgsPointXY &srcPoint, QgsPointLocator::MatchFilter *filter = nullptr )
: mLocator( pl )
, mBest( m )
, mSrcPoint( srcPoint )
, 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 );
QgsPointXY bestPoint;
int bestVertexNumber = -1;
auto replaceIfBetter = [this, &bestPoint, &bestVertexNumber]( const QgsPoint & candidate, int vertexNumber )
{
if ( bestPoint.isEmpty() || candidate.distanceSquared( mSrcPoint.x(), mSrcPoint.y() ) < bestPoint.sqrDist( mSrcPoint ) )
{
bestPoint = QgsPointXY( candidate );
bestVertexNumber = vertexNumber;
}
};
switch ( QgsWkbTypes::geometryType( geom->wkbType() ) )
{
case QgsWkbTypes::PointGeometry:
case QgsWkbTypes::UnknownGeometry:
case QgsWkbTypes::NullGeometry:
return;
case QgsWkbTypes::LineGeometry:
{
int partStartVertexNum = 0;
for ( auto partIt = geom->const_parts_begin(); partIt != geom->const_parts_end(); ++partIt )
{
if ( const QgsCurve *curve = qgsgeometry_cast< const QgsCurve * >( *partIt ) )
{
replaceIfBetter( curve->startPoint(), partStartVertexNum );
replaceIfBetter( curve->endPoint(), partStartVertexNum + curve->numPoints() - 1 );
partStartVertexNum += curve->numPoints();
}
}
break;
}
case QgsWkbTypes::PolygonGeometry:
{
int partStartVertexNum = 0;
for ( auto partIt = geom->const_parts_begin(); partIt != geom->const_parts_end(); ++partIt )
{
if ( const QgsCurvePolygon *polygon = qgsgeometry_cast< const QgsCurvePolygon * >( *partIt ) )
{
if ( polygon->exteriorRing() )
{
replaceIfBetter( polygon->exteriorRing()->startPoint(), partStartVertexNum );
partStartVertexNum += polygon->exteriorRing()->numPoints();
}
for ( int i = 0; i < polygon->numInteriorRings(); ++i )
{
const QgsCurve *ring = polygon->interiorRing( i );
replaceIfBetter( ring->startPoint(), partStartVertexNum );
partStartVertexNum += ring->numPoints();
}
}
}
break;
}
}
QgsPointLocator::Match m( QgsPointLocator::LineEndpoint, mLocator->mLayer, id, std::sqrt( mSrcPoint.sqrDist( bestPoint ) ), bestPoint, bestVertexNumber );
// in range queries the filter may reject some matches
if ( mFilter && !mFilter->acceptMatch( m ) )
return;
if ( !mBest.isValid() || m.distance() < mBest.distance() )
mBest = m;
}
private:
QgsPointLocator *mLocator = nullptr;
QgsPointLocator::Match &mBest;
QgsPointXY mSrcPoint;
QgsPointLocator::MatchFilter *mFilter = nullptr;
};
////////////////////////////////////////////////////////////////////////////
@ -1209,6 +1315,21 @@ QgsPointLocator::Match QgsPointLocator::nearestMiddleOfSegment( const QgsPointXY
return m;
}
QgsPointLocator::Match QgsPointLocator::nearestLineEndpoints( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter, bool relaxed )
{
if ( !prepare( relaxed ) )
return Match();
Match m;
QgsPointLocator_VisitorNearestLineEndpoint visitor( this, m, point, filter );
QgsRectangle rect( point.x() - tolerance, point.y() - tolerance, point.x() + tolerance, point.y() + tolerance );
mRTree->intersectsWithQuery( rect2region( rect ), visitor );
if ( m.isValid() && m.distance() > tolerance )
return Match(); // make sure that only match strictly within the tolerance is returned
return m;
}
QgsPointLocator::Match QgsPointLocator::nearestEdge( const QgsPointXY &point, double tolerance, MatchFilter *filter, bool relaxed )
{
if ( !prepare( relaxed ) )

View File

@ -153,12 +153,13 @@ class CORE_EXPORT QgsPointLocator : public QObject
enum Type
{
Invalid = 0, //!< Invalid
Vertex = 1, //!< Snapped to a vertex. Can be a vertex of the geometry or an intersection.
Edge = 2, //!< Snapped to an edge
Area = 4, //!< Snapped to an area
Centroid = 8, //!< Snapped to a centroid
MiddleOfSegment = 16, //!< Snapped to the middle of a segment
All = Vertex | Edge | Area | Centroid | MiddleOfSegment //!< Combination of all types
Vertex = 1 << 0, //!< Snapped to a vertex. Can be a vertex of the geometry or an intersection.
Edge = 1 << 1, //!< Snapped to an edge
Area = 1 << 2, //!< Snapped to an area
Centroid = 1 << 3, //!< Snapped to a centroid
MiddleOfSegment = 1 << 4, //!< Snapped to the middle of a segment
LineEndpoint = 1 << 5, //!< Start or end points of lines only (since QGIS 3.20)
All = Vertex | Edge | Area | Centroid | MiddleOfSegment //!< Combination of all types. Note LineEndpoint is not included as endpoints made redundant by the presence of the Vertex flag.
};
Q_DECLARE_FLAGS( Types, Type )
@ -204,17 +205,24 @@ class CORE_EXPORT QgsPointLocator : public QObject
QgsPointLocator::Type type() const { return mType; }
bool isValid() const { return mType != Invalid; }
//! Returns true if the Match is a vertex
//! Returns TRUE if the Match is a vertex
bool hasVertex() const { return mType == Vertex; }
//! Returns true if the Match is an edge
//! Returns TRUE if the Match is an edge
bool hasEdge() const { return mType == Edge; }
//! Returns true if the Match is a centroid
//! Returns TRUE if the Match is a centroid
bool hasCentroid() const { return mType == Centroid; }
//! Returns true if the Match is an area
//! Returns TRUE if the Match is an area
bool hasArea() const { return mType == Area; }
//! Returns true if the Match is the middle of a segment
//! Returns TRUE if the Match is the middle of a segment
bool hasMiddleSegment() const { return mType == MiddleOfSegment; }
/**
* Returns TRUE if the Match is a line endpoint (start or end vertex).
*
* \since QGIS 3.20
*/
bool hasLineEndpoint() const { return mType == LineEndpoint; }
/**
* for vertex / edge match
* units depending on what class returns it (geom.cache: layer units, map canvas snapper: dest crs units)
@ -333,6 +341,14 @@ class CORE_EXPORT QgsPointLocator : public QObject
*/
Match nearestMiddleOfSegment( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr, bool relaxed = false );
/**
* Find nearest line endpoint (start or end 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
* \since 3.20
*/
Match nearestLineEndpoints( 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.
@ -474,6 +490,7 @@ class CORE_EXPORT QgsPointLocator : public QObject
friend class TestQgsPointLocator;
friend class QgsPointLocator_VisitorCentroidsInRect;
friend class QgsPointLocator_VisitorMiddlesInRect;
friend class QgsPointLocator_VisitorNearestLineEndpoint;
};

View File

@ -163,7 +163,6 @@ bool QgsSnappingConfig::IndividualLayerSettings::operator ==( const QgsSnappingC
&& mMaximumScale == other.mMaximumScale;
}
QgsSnappingConfig::QgsSnappingConfig( QgsProject *project )
: mProject( project )
{
@ -280,6 +279,50 @@ QgsSnappingConfig::SnappingType QgsSnappingConfig::type() const
return QgsSnappingConfig::SnappingType::Vertex;
}
QString QgsSnappingConfig::snappingTypeFlagToString( SnappingTypeFlag type )
{
switch ( type )
{
case QgsSnappingConfig::NoSnapFlag:
return QObject::tr( "No Snapping" );
case QgsSnappingConfig::VertexFlag:
return QObject::tr( "Vertex" );
case QgsSnappingConfig::SegmentFlag:
return QObject::tr( "Segment" );
case QgsSnappingConfig::AreaFlag:
return QObject::tr( "Area" );
case QgsSnappingConfig::CentroidFlag:
return QObject::tr( "Centroid" );
case QgsSnappingConfig::MiddleOfSegmentFlag:
return QObject::tr( "Middle of Segments" );
case QgsSnappingConfig::LineEndpointFlag:
return QObject::tr( "Line Endpoints" );
}
return QString();
}
QIcon QgsSnappingConfig::snappingTypeFlagToIcon( SnappingTypeFlag type )
{
switch ( type )
{
case QgsSnappingConfig::NoSnapFlag:
return QIcon();
case QgsSnappingConfig::VertexFlag:
return QgsApplication::getThemeIcon( QStringLiteral( "/mIconSnappingVertex.svg" ) );
case QgsSnappingConfig::SegmentFlag:
return QgsApplication::getThemeIcon( QStringLiteral( "/mIconSnappingSegment.svg" ) );
case QgsSnappingConfig::AreaFlag:
return QgsApplication::getThemeIcon( QStringLiteral( "/mIconSnappingArea.svg" ) );
case QgsSnappingConfig::CentroidFlag:
return QgsApplication::getThemeIcon( QStringLiteral( "/mIconSnappingCentroid.svg" ) );
case QgsSnappingConfig::MiddleOfSegmentFlag:
return QgsApplication::getThemeIcon( QStringLiteral( "/mIconSnappingMiddle.svg" ) );
case QgsSnappingConfig::LineEndpointFlag:
return QgsApplication::getThemeIcon( QStringLiteral( "/mIconSnappingEndpoint.svg" ) );
}
return QIcon();
}
void QgsSnappingConfig::setType( QgsSnappingConfig::SnappingType type )
{
switch ( type )
@ -413,7 +456,7 @@ void QgsSnappingConfig::readProject( const QDomDocument &doc )
mEnabled = snapSettingsElem.attribute( QStringLiteral( "enabled" ) ) == QLatin1String( "1" );
if ( snapSettingsElem.hasAttribute( QStringLiteral( "mode" ) ) )
mMode = ( SnappingMode )snapSettingsElem.attribute( QStringLiteral( "mode" ) ).toInt();
mMode = static_cast< SnappingMode >( snapSettingsElem.attribute( QStringLiteral( "mode" ) ).toInt() );
if ( snapSettingsElem.hasAttribute( QStringLiteral( "type" ) ) )
{
@ -424,7 +467,7 @@ void QgsSnappingConfig::readProject( const QDomDocument &doc )
if ( versionElem.hasAttribute( QStringLiteral( "version" ) ) )
{
version = versionElem.attribute( QStringLiteral( "version" ) );
QRegularExpression re( "([\\d]+)\\.([\\d]+)" );
QRegularExpression re( QStringLiteral( "([\\d]+)\\.([\\d]+)" ) );
QRegularExpressionMatch match = re.match( version );
if ( match.hasMatch() )
{
@ -471,7 +514,7 @@ void QgsSnappingConfig::readProject( const QDomDocument &doc )
mMaximumScale = snapSettingsElem.attribute( QStringLiteral( "maxScale" ) ).toDouble();
if ( snapSettingsElem.hasAttribute( QStringLiteral( "unit" ) ) )
mUnits = ( QgsTolerance::UnitType )snapSettingsElem.attribute( QStringLiteral( "unit" ) ).toInt();
mUnits = static_cast< QgsTolerance::UnitType >( snapSettingsElem.attribute( QStringLiteral( "unit" ) ).toInt() );
if ( snapSettingsElem.hasAttribute( QStringLiteral( "intersection-snapping" ) ) )
mIntersectionSnapping = snapSettingsElem.attribute( QStringLiteral( "intersection-snapping" ) ) == QLatin1String( "1" );
@ -499,7 +542,7 @@ void QgsSnappingConfig::readProject( const QDomDocument &doc )
bool enabled = settingElement.attribute( QStringLiteral( "enabled" ) ) == QLatin1String( "1" );
QgsSnappingConfig::SnappingTypeFlag type = static_cast<QgsSnappingConfig::SnappingTypeFlag>( settingElement.attribute( QStringLiteral( "type" ) ).toInt() );
double tolerance = settingElement.attribute( QStringLiteral( "tolerance" ) ).toDouble();
QgsTolerance::UnitType units = ( QgsTolerance::UnitType )settingElement.attribute( QStringLiteral( "units" ) ).toInt();
QgsTolerance::UnitType units = static_cast< QgsTolerance::UnitType >( settingElement.attribute( QStringLiteral( "units" ) ).toInt() );
double minScale = settingElement.attribute( QStringLiteral( "minScale" ) ).toDouble();
double maxScale = settingElement.attribute( QStringLiteral( "maxScale" ) ).toDouble();

View File

@ -70,11 +70,12 @@ class CORE_EXPORT QgsSnappingConfig
enum SnappingTypes
{
NoSnapFlag = 0, //!< No snapping
VertexFlag = 1, //!< On vertices
SegmentFlag = 2, //!< On segments
AreaFlag = 4, //!< On Area
CentroidFlag = 8, //!< On centroid
MiddleOfSegmentFlag = 16, //!< On Middle segment
VertexFlag = 1 << 0, //!< On vertices
SegmentFlag = 1 << 1, //!< On segments
AreaFlag = 1 << 2, //!< On Area
CentroidFlag = 1 << 3, //!< On centroid
MiddleOfSegmentFlag = 1 << 4, //!< On Middle segment
LineEndpointFlag = 1 << 5, //!< Start or end points of lines only (since QGIS 3.20)
};
Q_ENUM( SnappingTypes )
Q_DECLARE_FLAGS( SnappingTypeFlag, SnappingTypes )
@ -94,24 +95,19 @@ class CORE_EXPORT QgsSnappingConfig
/**
* Convenient method to returns the translated name of the enum type
* QgsSnappingConfig::SnappingTypeFlag
* QgsSnappingConfig::SnappingTypeFlag.
*
* \since QGIS 3.12
*/
static const QString snappingTypeFlagToString( SnappingTypeFlag type )
{
switch ( type )
{
case QgsSnappingConfig::NoSnapFlag: return QObject::tr( "No Snapping" );
case QgsSnappingConfig::VertexFlag: return QObject::tr( "Vertex" );
case QgsSnappingConfig::SegmentFlag: return QObject::tr( "Segment" );
case QgsSnappingConfig::AreaFlag: return QObject::tr( "Area" );
case QgsSnappingConfig::CentroidFlag: return QObject::tr( "Centroid" );
case QgsSnappingConfig::MiddleOfSegmentFlag: return QObject::tr( "Middle of Segments" );
}
return QString();
}
static QString snappingTypeFlagToString( SnappingTypeFlag type );
/**
* Convenient method to return an icon corresponding to the enum type
* QgsSnappingConfig::SnappingTypeFlag.
*
* \since QGIS 3.20
*/
static QIcon snappingTypeFlagToIcon( SnappingTypeFlag type );
/**
* \ingroup core

View File

@ -180,13 +180,23 @@ static void _replaceIfBetter( QgsPointLocator::Match &bestMatch, const QgsPointL
return;
// ORDER
// LineEndpoint
// Vertex, Intersection
// Middle
// Centroid
// Edge
// Area
// First Vertex, or intersection
// first line endpoint -- these are like vertex matches, but even more strict
if ( ( bestMatch.type() & QgsPointLocator::LineEndpoint ) && !( candidateMatch.type() & QgsPointLocator::LineEndpoint ) )
return;
if ( candidateMatch.type() & QgsPointLocator::LineEndpoint )
{
bestMatch = candidateMatch;
return;
}
// Second Vertex, or intersection
if ( ( bestMatch.type() & QgsPointLocator::Vertex ) && !( candidateMatch.type() & QgsPointLocator::Vertex ) )
return;
if ( candidateMatch.type() & QgsPointLocator::Vertex )
@ -231,6 +241,10 @@ static void _updateBestMatch( QgsPointLocator::Match &bestMatch, const QgsPointX
{
_replaceIfBetter( bestMatch, loc->nearestMiddleOfSegment( pointMap, tolerance, filter ), tolerance );
}
if ( type & QgsPointLocator::LineEndpoint )
{
_replaceIfBetter( bestMatch, loc->nearestLineEndpoints( pointMap, tolerance, filter ), tolerance );
}
}

View File

@ -700,7 +700,7 @@ bool QgsAdvancedDigitizingDockWidget::applyConstraints( QgsMapMouseEvent *e )
*/
e->setMapPoint( point );
mSnapMatch = context.snappingUtils->snapToMap( point, nullptr, true );
if ( ( mSnapMatch.hasVertex() && ( point == mSnapMatch.point() ) ) || ( mSnapMatch.hasEdge() && QgsProject::instance()->topologicalEditing() ) )
if ( ( ( mSnapMatch.hasVertex() || mSnapMatch.hasLineEndpoint() ) && ( point == mSnapMatch.point() ) ) || ( mSnapMatch.hasEdge() && QgsProject::instance()->topologicalEditing() ) )
{
e->snapPoint();
}

View File

@ -331,7 +331,7 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private
/**
* Is it snapped to a vertex
*/
inline bool snappedToVertex() const { return ( mSnapMatch.isValid() && mSnapMatch.hasVertex() ); }
inline bool snappedToVertex() const { return ( mSnapMatch.isValid() && ( mSnapMatch.hasVertex() || mSnapMatch.hasLineEndpoint() ) ); }
/**
* Snapped to a segment

View File

@ -462,7 +462,7 @@ int QgsMapToolCapture::fetchLayerPoint( const QgsPointLocator::Match &match, Qgs
{
QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( mCanvas->currentLayer() );
QgsVectorLayer *sourceLayer = match.layer();
if ( match.isValid() && ( match.hasVertex() || ( QgsProject::instance()->topologicalEditing() && ( match.hasEdge() || match.hasMiddleSegment() ) ) ) && sourceLayer &&
if ( match.isValid() && ( match.hasVertex() || match.hasLineEndpoint() || ( QgsProject::instance()->topologicalEditing() && ( match.hasEdge() || match.hasMiddleSegment() ) ) ) && sourceLayer &&
( sourceLayer->crs() == vlayer->crs() ) )
{
QgsFeature f;

View File

@ -97,6 +97,10 @@ void QgsSnapIndicator::setMatch( const QgsPointLocator::Match &match )
{
iconType = QgsVertexMarker::ICON_RHOMBUS; // area snap
}
else if ( match.hasLineEndpoint() )
{
iconType = QgsVertexMarker::ICON_BOX; // line endpoint snap
}
else // must be segment snap
{
iconType = QgsVertexMarker::ICON_DOUBLE_TRIANGLE;

View File

@ -437,7 +437,245 @@ class TestQgsSnappingUtils : public QObject
QVERIFY( m2.isValid() );
QCOMPARE( m2.type(), QgsPointLocator::Centroid );
QCOMPARE( m2.point(), QgsPointXY( 2.5, 2.5 ) );
}
void testSnapOnLineEndpoints()
{
std::unique_ptr<QgsVectorLayer> vSnapCentroidMiddle( new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "m" ), QStringLiteral( "memory" ) ) );
QgsFeature f1;
QgsGeometry f1g = QgsGeometry::fromWkt( "LineString (0 0, 0 5, 5 5, 5 0)" );
f1.setGeometry( f1g );
QgsFeatureList flist;
flist << f1;
vSnapCentroidMiddle->dataProvider()->addFeatures( flist );
QVERIFY( vSnapCentroidMiddle->dataProvider()->featureCount() == 1 );
QgsMapSettings mapSettings;
mapSettings.setOutputSize( QSize( 100, 100 ) );
mapSettings.setExtent( QgsRectangle( 0, 0, 10, 10 ) );
QVERIFY( mapSettings.hasValidSettings() );
QgsSnappingUtils u;
u.setMapSettings( mapSettings );
QgsSnappingConfig snappingConfig = u.config();
snappingConfig.setEnabled( true );
snappingConfig.setMode( QgsSnappingConfig::AdvancedConfiguration );
QgsSnappingConfig::IndividualLayerSettings layerSettings( true, static_cast<QgsSnappingConfig::SnappingTypeFlag>( QgsSnappingConfig::LineEndpointFlag ), 0.2, QgsTolerance::ProjectUnits, 0.0, 0.0 );
snappingConfig.setIndividualLayerSettings( vSnapCentroidMiddle.get(), layerSettings );
u.setConfig( snappingConfig );
// snap to start
QgsPointLocator::Match m = u.snapToMap( QgsPointXY( 0, -0.1 ) );
QVERIFY( m.isValid() );
QCOMPARE( m.type(), QgsPointLocator::LineEndpoint );
QCOMPARE( m.point(), QgsPointXY( 0.0, 0.0 ) );
QVERIFY( m.hasLineEndpoint() );
QVERIFY( !m.hasEdge() );
QCOMPARE( m.vertexIndex(), 0 );
// snap to end
m = u.snapToMap( QgsPointXY( 5, -0.1 ) );
QVERIFY( m.isValid() );
QCOMPARE( m.type(), QgsPointLocator::LineEndpoint );
QCOMPARE( m.point(), QgsPointXY( 5.0, 0.0 ) );
QVERIFY( m.hasLineEndpoint() );
QVERIFY( !m.hasEdge() );
QCOMPARE( m.vertexIndex(), 3 );
// try to snap to a non start/end vertex
QgsPointLocator::Match m2 = u.snapToMap( QgsPointXY( -0.1, 5 ) );
QVERIFY( !m2.isValid() );
}
void testSnapOnLineEndpointsMultiLine()
{
std::unique_ptr<QgsVectorLayer> vSnapCentroidMiddle( new QgsVectorLayer( QStringLiteral( "MultiLineString" ), QStringLiteral( "m" ), QStringLiteral( "memory" ) ) );
QgsFeature f1;
QgsGeometry f1g = QgsGeometry::fromWkt( "MultiLineString ((0 0, 0 5, 5 5, 5 0), (0 -0.1, 0 -5, 5 -0.5))" );
f1.setGeometry( f1g );
QgsFeatureList flist;
flist << f1;
vSnapCentroidMiddle->dataProvider()->addFeatures( flist );
QVERIFY( vSnapCentroidMiddle->dataProvider()->featureCount() == 1 );
QgsMapSettings mapSettings;
mapSettings.setOutputSize( QSize( 100, 100 ) );
mapSettings.setExtent( QgsRectangle( 0, 0, 10, 10 ) );
QVERIFY( mapSettings.hasValidSettings() );
QgsSnappingUtils u;
u.setMapSettings( mapSettings );
QgsSnappingConfig snappingConfig = u.config();
snappingConfig.setEnabled( true );
snappingConfig.setMode( QgsSnappingConfig::AdvancedConfiguration );
QgsSnappingConfig::IndividualLayerSettings layerSettings( true, static_cast<QgsSnappingConfig::SnappingTypeFlag>( QgsSnappingConfig::LineEndpointFlag ), 0.2, QgsTolerance::ProjectUnits, 0.0, 0.0 );
snappingConfig.setIndividualLayerSettings( vSnapCentroidMiddle.get(), layerSettings );
u.setConfig( snappingConfig );
// snap to start
QgsPointLocator::Match m = u.snapToMap( QgsPointXY( 0, 0.1 ) );
QVERIFY( m.isValid() );
QCOMPARE( m.type(), QgsPointLocator::LineEndpoint );
QCOMPARE( m.point(), QgsPointXY( 0.0, 0.0 ) );
QVERIFY( m.hasLineEndpoint() );
QVERIFY( !m.hasEdge() );
QCOMPARE( m.vertexIndex(), 0 );
m = u.snapToMap( QgsPointXY( 0, -0.07 ) );
QVERIFY( m.isValid() );
QCOMPARE( m.type(), QgsPointLocator::LineEndpoint );
QCOMPARE( m.point(), QgsPointXY( 0.0, -0.1 ) );
QVERIFY( m.hasLineEndpoint() );
QVERIFY( !m.hasEdge() );
QCOMPARE( m.vertexIndex(), 4 );
// snap to end
m = u.snapToMap( QgsPointXY( 5, -0.1 ) );
QVERIFY( m.isValid() );
QCOMPARE( m.type(), QgsPointLocator::LineEndpoint );
QCOMPARE( m.point(), QgsPointXY( 5.0, 0.0 ) );
QVERIFY( m.hasLineEndpoint() );
QVERIFY( !m.hasEdge() );
QCOMPARE( m.vertexIndex(), 3 );
m = u.snapToMap( QgsPointXY( 5, -0.4 ) );
QVERIFY( m.isValid() );
QCOMPARE( m.type(), QgsPointLocator::LineEndpoint );
QCOMPARE( m.point(), QgsPointXY( 5.0, -0.5 ) );
QVERIFY( m.hasLineEndpoint() );
QVERIFY( !m.hasEdge() );
QCOMPARE( m.vertexIndex(), 6 );
// try to snap to a non start/end vertex
QgsPointLocator::Match m2 = u.snapToMap( QgsPointXY( -0.1, 5 ) );
QVERIFY( !m2.isValid() );
m2 = u.snapToMap( QgsPointXY( 0, -5 ) );
QVERIFY( !m2.isValid() );
}
void testSnapOnPolygonEndpoints()
{
std::unique_ptr<QgsVectorLayer> vSnapCentroidMiddle( new QgsVectorLayer( QStringLiteral( "Polygon" ), QStringLiteral( "m" ), QStringLiteral( "memory" ) ) );
QgsFeature f1;
QgsGeometry f1g = QgsGeometry::fromWkt( "Polygon ((1 0, 0 5, 5 5, 5 0, 1 0),(3 2, 3.5 2, 3.5 3, 3 2))" );
f1.setGeometry( f1g );
QgsFeatureList flist;
flist << f1;
vSnapCentroidMiddle->dataProvider()->addFeatures( flist );
QVERIFY( vSnapCentroidMiddle->dataProvider()->featureCount() == 1 );
QgsMapSettings mapSettings;
mapSettings.setOutputSize( QSize( 100, 100 ) );
mapSettings.setExtent( QgsRectangle( 0, 0, 10, 10 ) );
QVERIFY( mapSettings.hasValidSettings() );
QgsSnappingUtils u;
u.setMapSettings( mapSettings );
QgsSnappingConfig snappingConfig = u.config();
snappingConfig.setEnabled( true );
snappingConfig.setMode( QgsSnappingConfig::AdvancedConfiguration );
QgsSnappingConfig::IndividualLayerSettings layerSettings( true, static_cast<QgsSnappingConfig::SnappingTypeFlag>( QgsSnappingConfig::LineEndpointFlag ), 0.2, QgsTolerance::ProjectUnits, 0.0, 0.0 );
snappingConfig.setIndividualLayerSettings( vSnapCentroidMiddle.get(), layerSettings );
u.setConfig( snappingConfig );
// snap to start of exterior
QgsPointLocator::Match m = u.snapToMap( QgsPointXY( 1, -0.1 ) );
QVERIFY( m.isValid() );
QCOMPARE( m.type(), QgsPointLocator::LineEndpoint );
QCOMPARE( m.point(), QgsPointXY( 1.0, 0.0 ) );
QVERIFY( m.hasLineEndpoint() );
QVERIFY( !m.hasEdge() );
QCOMPARE( m.vertexIndex(), 0 );
// snap to ring start
m = u.snapToMap( QgsPointXY( 3, 2.1 ) );
QVERIFY( m.isValid() );
QCOMPARE( m.type(), QgsPointLocator::LineEndpoint );
QCOMPARE( m.point(), QgsPointXY( 3.0, 2.0 ) );
QVERIFY( m.hasLineEndpoint() );
QVERIFY( !m.hasEdge() );
QCOMPARE( m.vertexIndex(), 5 );
// try to snap to a non start/end vertex
QgsPointLocator::Match m2 = u.snapToMap( QgsPointXY( -0.1, 5 ) );
QVERIFY( !m2.isValid() );
m2 = u.snapToMap( QgsPointXY( 3.51, 3 ) );
QVERIFY( !m2.isValid() );
}
void testSnapOnMultiPolygonEndpoints()
{
std::unique_ptr<QgsVectorLayer> vSnapCentroidMiddle( new QgsVectorLayer( QStringLiteral( "MultiPolygon" ), QStringLiteral( "m" ), QStringLiteral( "memory" ) ) );
QgsFeature f1;
QgsGeometry f1g = QgsGeometry::fromWkt( "MultiPolygon (((1 0, 0 5, 5 5, 5 0, 1 0),(3 2, 3.5 2, 3.5 3, 3 2)), ((10 0, 10 5, 15 5, 15 0, 10 0),(13 2, 13.5 2, 13.5 3, 13 2)) )" );
f1.setGeometry( f1g );
QgsFeatureList flist;
flist << f1;
vSnapCentroidMiddle->dataProvider()->addFeatures( flist );
QVERIFY( vSnapCentroidMiddle->dataProvider()->featureCount() == 1 );
QgsMapSettings mapSettings;
mapSettings.setOutputSize( QSize( 100, 100 ) );
mapSettings.setExtent( QgsRectangle( 0, 0, 10, 10 ) );
QVERIFY( mapSettings.hasValidSettings() );
QgsSnappingUtils u;
u.setMapSettings( mapSettings );
QgsSnappingConfig snappingConfig = u.config();
snappingConfig.setEnabled( true );
snappingConfig.setMode( QgsSnappingConfig::AdvancedConfiguration );
QgsSnappingConfig::IndividualLayerSettings layerSettings( true, static_cast<QgsSnappingConfig::SnappingTypeFlag>( QgsSnappingConfig::LineEndpointFlag ), 0.2, QgsTolerance::ProjectUnits, 0.0, 0.0 );
snappingConfig.setIndividualLayerSettings( vSnapCentroidMiddle.get(), layerSettings );
u.setConfig( snappingConfig );
// snap to start of exterior
QgsPointLocator::Match m = u.snapToMap( QgsPointXY( 1, -0.1 ) );
QVERIFY( m.isValid() );
QCOMPARE( m.type(), QgsPointLocator::LineEndpoint );
QCOMPARE( m.point(), QgsPointXY( 1.0, 0.0 ) );
QVERIFY( m.hasLineEndpoint() );
QVERIFY( !m.hasEdge() );
QCOMPARE( m.vertexIndex(), 0 );
m = u.snapToMap( QgsPointXY( 10, -0.1 ) );
QVERIFY( m.isValid() );
QCOMPARE( m.type(), QgsPointLocator::LineEndpoint );
QCOMPARE( m.point(), QgsPointXY( 10.0, 0.0 ) );
QVERIFY( m.hasLineEndpoint() );
QVERIFY( !m.hasEdge() );
QCOMPARE( m.vertexIndex(), 9 );
// snap to ring start
m = u.snapToMap( QgsPointXY( 3, 2.1 ) );
QVERIFY( m.isValid() );
QCOMPARE( m.type(), QgsPointLocator::LineEndpoint );
QCOMPARE( m.point(), QgsPointXY( 3.0, 2.0 ) );
QVERIFY( m.hasLineEndpoint() );
QVERIFY( !m.hasEdge() );
QCOMPARE( m.vertexIndex(), 5 );
m = u.snapToMap( QgsPointXY( 13, 2.1 ) );
QVERIFY( m.isValid() );
QCOMPARE( m.type(), QgsPointLocator::LineEndpoint );
QCOMPARE( m.point(), QgsPointXY( 13.0, 2.0 ) );
QVERIFY( m.hasLineEndpoint() );
QVERIFY( !m.hasEdge() );
QCOMPARE( m.vertexIndex(), 14 );
// try to snap to a non start/end vertex
QgsPointLocator::Match m2 = u.snapToMap( QgsPointXY( -0.1, 5 ) );
QVERIFY( !m2.isValid() );
m2 = u.snapToMap( QgsPointXY( 3.51, 3 ) );
QVERIFY( !m2.isValid() );
m2 = u.snapToMap( QgsPointXY( 10, 5 ) );
QVERIFY( !m2.isValid() );
m2 = u.snapToMap( QgsPointXY( 13.51, 3 ) );
QVERIFY( !m2.isValid() );
}
void testSnapOnCurrentLayer()