Skip to content

Commit

Permalink
[FEATURE] Add new snapping option for "Line Endpoints"
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nyalldawson committed Mar 4, 2021
1 parent 6d40826 commit b8baabf
Show file tree
Hide file tree
Showing 18 changed files with 601 additions and 157 deletions.
3 changes: 2 additions & 1 deletion images/images.qrc
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions images/themes/default/mIconSnappingEndpoint.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 22 additions & 5 deletions python/core/auto_generated/qgspointlocator.sip.in
Expand Up @@ -94,6 +94,7 @@ Configure render context - if not ``None``, it will use to index only visible f
Area,
Centroid,
MiddleOfSegment,
LineEndpoint,
All
};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 );
Expand Down
12 changes: 10 additions & 2 deletions python/core/auto_generated/qgssnappingconfig.sip.in
Expand Up @@ -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;

Expand All @@ -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
{
Expand Down
23 changes: 17 additions & 6 deletions src/app/options/qgsoptions.cpp
Expand Up @@ -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() );
Expand Down
64 changes: 31 additions & 33 deletions src/app/qgssnappinglayertreemodel.cpp
Expand Up @@ -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 );
Expand Down Expand Up @@ -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 )
Expand Down Expand Up @@ -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 );
}

Expand Down
103 changes: 41 additions & 62 deletions src/app/qgssnappingwidget.cpp
Expand Up @@ -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 )
Expand Down Expand Up @@ -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 )
{
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 )
// update snapping flag actions
for ( QAction *action : qgis::as_const( mSnappingFlagActions ) )
{
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() )
Expand Down Expand Up @@ -722,27 +701,27 @@ void QgsSnappingWidget::typeButtonTriggered( QAction *action )
{
unsigned int type = static_cast<int>( mConfig.typeFlag() );

mTypeButton->setDefaultAction( action );
if ( action == mVertexAction )
{
type ^= static_cast<int>( QgsSnappingConfig::VertexFlag );
}
else if ( action == mSegmentAction )
{
type ^= static_cast<int>( QgsSnappingConfig::SegmentFlag );
}
else if ( action == mAreaAction )
{
type ^= static_cast<int>( QgsSnappingConfig::AreaFlag );
}
else if ( action == mCentroidAction )
const QgsSnappingConfig::SnappingTypeFlag actionFlag = static_cast<QgsSnappingConfig::SnappingTypeFlag>( action->data().toInt() );
type ^= actionFlag;

if ( type & actionFlag )
{
type ^= static_cast<int>( QgsSnappingConfig::CentroidFlag );
// user checked the action, set as new default
mTypeButton->setDefaultAction( action );
}
else if ( action == mMiddleAction )
else
{
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 );
}
Expand Down
6 changes: 1 addition & 5 deletions src/app/qgssnappingwidget.h
Expand Up @@ -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;
Expand Down

0 comments on commit b8baabf

Please sign in to comment.