diff --git a/images/images.qrc b/images/images.qrc index 219691db49a..2e26158bbe5 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -700,6 +700,7 @@ themes/default/mIconSnappingMiddle.svg themes/default/mIconSnappingOnScale.svg themes/default/mIconSnappingVertex.svg + themes/default/mIconSnappingSelf.svg themes/default/mIconSnappingSegment.svg themes/default/mIconTopologicalEditing.svg themes/default/mIconSnappingIntersection.svg diff --git a/images/themes/default/mIconSnappingSelf.svg b/images/themes/default/mIconSnappingSelf.svg new file mode 100644 index 00000000000..0acabecae37 --- /dev/null +++ b/images/themes/default/mIconSnappingSelf.svg @@ -0,0 +1,141 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/core/auto_generated/qgscadutils.sip.in b/python/core/auto_generated/qgscadutils.sip.in index bdb47304da7..742621e103d 100644 --- a/python/core/auto_generated/qgscadutils.sip.in +++ b/python/core/auto_generated/qgscadutils.sip.in @@ -53,6 +53,8 @@ The QgsCadUtils class provides routines for CAD editing. QgsPointXY finalMapPoint; + QgsPointLocator::Match snapMatch; + QgsPointLocator::Match edgeMatch; double softLockCommonAngle; diff --git a/python/core/auto_generated/qgssnappingconfig.sip.in b/python/core/auto_generated/qgssnappingconfig.sip.in index 77c6645ea46..58dc70ad976 100644 --- a/python/core/auto_generated/qgssnappingconfig.sip.in +++ b/python/core/auto_generated/qgssnappingconfig.sip.in @@ -343,6 +343,20 @@ Returns if the snapping on intersection is enabled void setIntersectionSnapping( bool enabled ); %Docstring Sets if the snapping on intersection is enabled +%End + + bool selfSnapping() const; +%Docstring +Returns if self snapping (snapping to the currently digitised feature) is enabled + +.. versionadded:: 3.14 +%End + + void setSelfSnapping( bool enabled ); +%Docstring +Sets if self snapping (snapping to the currently digitised feature) is enabled + +.. versionadded:: 3.14 %End SIP_PYDICT individualLayerSettings() const; diff --git a/python/core/auto_generated/qgssnappingutils.sip.in b/python/core/auto_generated/qgssnappingutils.sip.in index 8d50edf4fee..9c418399933 100644 --- a/python/core/auto_generated/qgssnappingutils.sip.in +++ b/python/core/auto_generated/qgssnappingutils.sip.in @@ -166,6 +166,42 @@ Set if invisible features must be snapped or not. .. versionadded:: 3.2 %End + void addExtraSnapLayer( QgsVectorLayer *vl ); +%Docstring +Supply an extra snapping layer (typically a memory layer). +This is can be used by map tools to provide additionnal +snappings points. + +.. seealso:: :py:func:`removeExtraSnapLayer` + +.. seealso:: :py:func:`getExtraSnapLayers` + +.. versionadded:: 3.14 +%End + + void removeExtraSnapLayer( QgsVectorLayer *vl ); +%Docstring +Removes an extra snapping layer + +.. seealso:: :py:func:`addExtraSnapLayer` + +.. seealso:: :py:func:`getExtraSnapLayers` + +.. versionadded:: 3.14 +%End + + QSet getExtraSnapLayers(); +%Docstring +Returns the list of extra snapping layers + +.. seealso:: :py:func:`addExtraSnapLayer` + +.. seealso:: :py:func:`removeExtraSnapLayer` + +.. versionadded:: 3.14 +%End + + public slots: void setConfig( const QgsSnappingConfig &snappingConfig ); diff --git a/src/app/qgssnappingwidget.cpp b/src/app/qgssnappingwidget.cpp index 28aa4cdc197..abdcfaca69c 100644 --- a/src/app/qgssnappingwidget.cpp +++ b/src/app/qgssnappingwidget.cpp @@ -287,6 +287,14 @@ QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas, tracingMenu->addAction( widgetAction ); mEnableTracingAction->setMenu( tracingMenu ); + // self-snapping button + mSelfSnappingAction = new QAction( tr( "Self-snapping" ), this ); + mSelfSnappingAction->setCheckable( true ); + mSelfSnappingAction->setIcon( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingSelf.svg" ) ) ); + mSelfSnappingAction->setToolTip( tr( "Enable Self-snapping" ) ); + mSelfSnappingAction->setObjectName( QStringLiteral( "SelfSnappingAction" ) ); + connect( mSelfSnappingAction, &QAction::toggled, this, &QgsSnappingWidget::enableSelfSnapping ); + // layout if ( mDisplayMode == ToolBar ) { @@ -316,6 +324,7 @@ QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas, mAvoidIntersectionsModeAction = tb->addWidget( mAvoidIntersectionsModeButton ); tb->addAction( mIntersectionSnappingAction ); tb->addAction( mEnableTracingAction ); + tb->addAction( mSelfSnappingAction ); } else { @@ -350,6 +359,12 @@ QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas, interButton->setToolButtonStyle( Qt::ToolButtonTextBesideIcon ); layout->addWidget( interButton ); + QToolButton *selfsnapButton = new QToolButton(); + selfsnapButton->addAction( mSelfSnappingAction ); + selfsnapButton->setDefaultAction( mSelfSnappingAction ); + selfsnapButton->setToolButtonStyle( Qt::ToolButtonTextBesideIcon ); + layout->addWidget( selfsnapButton ); + layout->setContentsMargins( 0, 0, 0, 0 ); layout->setAlignment( Qt::AlignRight ); layout->setSpacing( mDisplayMode == Widget ? 3 : 0 ); @@ -488,6 +503,11 @@ void QgsSnappingWidget::projectSnapSettingsChanged() mIntersectionSnappingAction->setChecked( config.intersectionSnapping() ); } + if ( config.selfSnapping() != mSelfSnappingAction->isChecked() ) + { + mSelfSnappingAction->setChecked( config.selfSnapping() ); + } + toggleSnappingWidgets( config.enabled() ); } @@ -552,6 +572,7 @@ void QgsSnappingWidget::toggleSnappingWidgets( bool enabled ) mAdvancedConfigWidget->setEnabled( enabled ); } mIntersectionSnappingAction->setEnabled( enabled ); + mSelfSnappingAction->setEnabled( enabled ); mEnableTracingAction->setEnabled( enabled ); } @@ -593,6 +614,12 @@ void QgsSnappingWidget::enableIntersectionSnapping( bool enabled ) mProject->setSnappingConfig( mConfig ); } +void QgsSnappingWidget::enableSelfSnapping( bool enabled ) +{ + mConfig.setSelfSnapping( enabled ); + mProject->setSnappingConfig( mConfig ); +} + void QgsSnappingWidget::onSnappingTreeLayersChanged() { mLayerTreeView->expandAll(); diff --git a/src/app/qgssnappingwidget.h b/src/app/qgssnappingwidget.h index 42081e6edf6..2d56eda0840 100644 --- a/src/app/qgssnappingwidget.h +++ b/src/app/qgssnappingwidget.h @@ -114,6 +114,8 @@ class APP_EXPORT QgsSnappingWidget : public QWidget void enableIntersectionSnapping( bool enabled ); + void enableSelfSnapping( bool enabled ); + void modeButtonTriggered( QAction *action ); void avoidIntersectionsModeButtonTriggered( QAction *action ); void typeButtonTriggered( QAction *action ); @@ -172,6 +174,7 @@ class APP_EXPORT QgsSnappingWidget : public QWidget QAction *mIntersectionSnappingAction = nullptr; QAction *mEnableTracingAction = nullptr; QgsDoubleSpinBox *mTracingOffsetSpinBox = nullptr; + QAction *mSelfSnappingAction = nullptr; QTreeView *mLayerTreeView = nullptr; QWidget *mAdvancedConfigWidget = nullptr; QgsFloatingWidget *mAdvancedConfigContainer = nullptr; diff --git a/src/core/qgssnappingconfig.cpp b/src/core/qgssnappingconfig.cpp index 2095212119d..86988973a0f 100644 --- a/src/core/qgssnappingconfig.cpp +++ b/src/core/qgssnappingconfig.cpp @@ -179,6 +179,7 @@ bool QgsSnappingConfig::operator==( const QgsSnappingConfig &other ) const && mTolerance == other.mTolerance && mUnits == other.mUnits && mIntersectionSnapping == other.mIntersectionSnapping + && mSelfSnapping == other.mSelfSnapping && mIndividualLayerSettings == other.mIndividualLayerSettings && mScaleDependencyMode == other.mScaleDependencyMode && mMinimumScale == other.mMinimumScale @@ -218,6 +219,7 @@ void QgsSnappingConfig::reset() mUnits = units; } mIntersectionSnapping = false; + mSelfSnapping = false; // set advanced config if ( mProject ) @@ -343,6 +345,16 @@ void QgsSnappingConfig::setIntersectionSnapping( bool enabled ) mIntersectionSnapping = enabled; } +bool QgsSnappingConfig::selfSnapping() const +{ + return mSelfSnapping; +} + +void QgsSnappingConfig::setSelfSnapping( bool enabled ) +{ + mSelfSnapping = enabled; +} + QHash QgsSnappingConfig::individualLayerSettings() const { return mIndividualLayerSettings; @@ -459,6 +471,9 @@ void QgsSnappingConfig::readProject( const QDomDocument &doc ) if ( snapSettingsElem.hasAttribute( QStringLiteral( "intersection-snapping" ) ) ) mIntersectionSnapping = snapSettingsElem.attribute( QStringLiteral( "intersection-snapping" ) ) == QLatin1String( "1" ); + if ( snapSettingsElem.hasAttribute( QStringLiteral( "self-snapping" ) ) ) + mSelfSnapping = snapSettingsElem.attribute( QStringLiteral( "self-snapping" ) ) == QLatin1String( "1" ); + // do not clear the settings as they must be automatically synchronized with current layers QDomNodeList nodes = snapSettingsElem.elementsByTagName( QStringLiteral( "individual-layer-settings" ) ); if ( nodes.count() ) @@ -504,6 +519,7 @@ void QgsSnappingConfig::writeProject( QDomDocument &doc ) snapSettingsElem.setAttribute( QStringLiteral( "tolerance" ), mTolerance ); snapSettingsElem.setAttribute( QStringLiteral( "unit" ), static_cast( mUnits ) ); snapSettingsElem.setAttribute( QStringLiteral( "intersection-snapping" ), QString::number( mIntersectionSnapping ) ); + snapSettingsElem.setAttribute( QStringLiteral( "self-snapping" ), QString::number( mSelfSnapping ) ); snapSettingsElem.setAttribute( QStringLiteral( "scaleDependencyMode" ), QString::number( mScaleDependencyMode ) ); snapSettingsElem.setAttribute( QStringLiteral( "minScale" ), mMinimumScale ); snapSettingsElem.setAttribute( QStringLiteral( "maxScale" ), mMaximumScale ); diff --git a/src/core/qgssnappingconfig.h b/src/core/qgssnappingconfig.h index 8768065c5a0..43fe7906a74 100644 --- a/src/core/qgssnappingconfig.h +++ b/src/core/qgssnappingconfig.h @@ -335,6 +335,20 @@ class CORE_EXPORT QgsSnappingConfig //! Sets if the snapping on intersection is enabled void setIntersectionSnapping( bool enabled ); + /** + * Returns if self snapping (snapping to the currently digitised feature) is enabled + * + * \since QGIS 3.14 + */ + bool selfSnapping() const; + + /** + * Sets if self snapping (snapping to the currently digitised feature) is enabled + * + * \since QGIS 3.14 + */ + void setSelfSnapping( bool enabled ); + //! Returns individual snapping settings for all layers #ifndef SIP_RUN QHash individualLayerSettings() const; @@ -458,6 +472,7 @@ class CORE_EXPORT QgsSnappingConfig double mMaximumScale = 0.0; QgsTolerance::UnitType mUnits = QgsTolerance::ProjectUnits; bool mIntersectionSnapping = false; + bool mSelfSnapping = false; QHash mIndividualLayerSettings; diff --git a/src/core/qgssnappingutils.cpp b/src/core/qgssnappingutils.cpp index 0dd8be67a3a..4d246cc1287 100644 --- a/src/core/qgssnappingutils.cpp +++ b/src/core/qgssnappingutils.cpp @@ -275,6 +275,7 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap, return QgsPointLocator::Match(); QgsPointLocator::Match bestMatch; + QgsPointLocator::MatchList edges; // for snap on intersection _updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter, relaxed ); if ( mSnappingConfig.intersectionSnapping() ) @@ -282,8 +283,19 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap, QgsPointLocator *locEdges = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance ); if ( !locEdges ) return QgsPointLocator::Match(); + edges = locEdges->edgesInRect( pointMap, tolerance ); + } - QgsPointLocator::MatchList edges = locEdges->edgesInRect( pointMap, tolerance ); + for ( QgsVectorLayer *vl : mExtraSnapLayers ) + { + QgsPointLocator *loc = locatorForLayer( vl ); + _updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter, false ); + if ( mSnappingConfig.intersectionSnapping() ) + edges << loc->edgesInRect( pointMap, tolerance ); + } + + if ( mSnappingConfig.intersectionSnapping() ) + { _replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), tolerance ); } @@ -322,7 +334,8 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap, QgsPointLocator::Match bestMatch; QgsPointLocator::MatchList edges; // for snap on intersection - double maxSnapIntTolerance = 0; + double maxTolerance = 0; + QgsPointLocator::Type maxTypes; for ( const LayerConfig &layerConfig : qgis::as_const( filteredConfigs ) ) { @@ -334,13 +347,24 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap, if ( mSnappingConfig.intersectionSnapping() ) { edges << loc->edgesInRect( pointMap, tolerance ); - maxSnapIntTolerance = std::max( maxSnapIntTolerance, tolerance ); } + // We keep the maximum tolerance for intersection snapping and extra snapping + maxTolerance = std::max( maxTolerance, tolerance ); + // To avoid yet an additionnal setting, on extra snappings, we use the combination of all enabled snap types + maxTypes = static_cast( maxTypes | layerConfig.type ); } } + for ( QgsVectorLayer *vl : mExtraSnapLayers ) + { + QgsPointLocator *loc = locatorForLayer( vl ); + _updateBestMatch( bestMatch, pointMap, loc, maxTypes, maxTolerance, filter, false ); + if ( mSnappingConfig.intersectionSnapping() ) + edges << loc->edgesInRect( pointMap, maxTolerance ); + } + if ( mSnappingConfig.intersectionSnapping() ) - _replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), maxSnapIntTolerance ); + _replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), maxTolerance ); return bestMatch; } @@ -373,6 +397,14 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap, } } + for ( QgsVectorLayer *vl : mExtraSnapLayers ) + { + QgsPointLocator *loc = locatorForLayer( vl ); + _updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter, false ); + if ( mSnappingConfig.intersectionSnapping() ) + edges << loc->edgesInRect( pointMap, tolerance ); + } + if ( mSnappingConfig.intersectionSnapping() ) _replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), tolerance ); diff --git a/src/core/qgssnappingutils.h b/src/core/qgssnappingutils.h index 252f82f7f33..ee5379dca76 100644 --- a/src/core/qgssnappingutils.h +++ b/src/core/qgssnappingutils.h @@ -185,6 +185,48 @@ class CORE_EXPORT QgsSnappingUtils : public QObject */ void setEnableSnappingForInvisibleFeature( bool enable ); + /** + * Supply an extra snapping layer (typically a memory layer). + * This is can be used by map tools to provide additionnal + * snappings points. + * + * \see removeExtraSnapLayer() + * \see getExtraSnapLayers() + * + * \since QGIS 3.14 + */ + void addExtraSnapLayer( QgsVectorLayer *vl ) + { + mExtraSnapLayers.insert( vl ); + } + + /** + * Removes an extra snapping layer + * + * \see addExtraSnapLayer() + * \see getExtraSnapLayers() + * + * \since QGIS 3.14 + */ + void removeExtraSnapLayer( QgsVectorLayer *vl ) + { + mExtraSnapLayers.remove( vl ); + } + + /** + * Returns the list of extra snapping layers + * + * \see addExtraSnapLayer() + * \see removeExtraSnapLayer() + * + * \since QGIS 3.14 + */ + QSet getExtraSnapLayers() + { + return mExtraSnapLayers; + } + + public slots: /** @@ -257,6 +299,8 @@ class CORE_EXPORT QgsSnappingUtils : public QObject LocatorsMap mTemporaryLocators; //! list of layer IDs that are too large to be indexed (hybrid strategy will use temporary locators for those) QSet mHybridNonindexableLayers; + //! list of additionnal snapping layers + QSet mExtraSnapLayers; /** * a record for each layer seen: diff --git a/src/gui/qgsmaptoolcapture.cpp b/src/gui/qgsmaptoolcapture.cpp index fc37dc76762..281c1df2ad5 100644 --- a/src/gui/qgsmaptoolcapture.cpp +++ b/src/gui/qgsmaptoolcapture.cpp @@ -51,6 +51,15 @@ QgsMapToolCapture::QgsMapToolCapture( QgsMapCanvas *canvas, QgsAdvancedDigitizin connect( canvas, &QgsMapCanvas::currentLayerChanged, this, &QgsMapToolCapture::currentLayerChanged ); + mExtraSnapLayer = new QgsVectorLayer( "LineString?crs=0", "extra snap", "memory" ); + mExtraSnapLayer->startEditing(); + QgsFeature f; + mExtraSnapLayer->addFeature( f ); + mExtraSnapFeatureId = f.id(); + + connect( QgsProject::instance(), &QgsProject::snappingConfigChanged, + this, &QgsMapToolCapture::updateExtraSnapLayer ); + currentLayerChanged( canvas->currentLayer() ); } @@ -63,6 +72,8 @@ QgsMapToolCapture::~QgsMapToolCapture() mValidator->deleteLater(); mValidator = nullptr; } + mExtraSnapLayer->deleteLater(); + mExtraSnapLayer = nullptr; } QgsMapToolCapture::Capabilities QgsMapToolCapture::capabilities() const @@ -75,6 +86,7 @@ void QgsMapToolCapture::activate() if ( mTempRubberBand ) mTempRubberBand->show(); + mCanvas->snappingUtils()->addExtraSnapLayer( mExtraSnapLayer ); QgsMapToolAdvancedDigitizing::activate(); } @@ -85,6 +97,7 @@ void QgsMapToolCapture::deactivate() mSnapIndicator->setMatch( QgsPointLocator::Match() ); + mCanvas->snappingUtils()->removeExtraSnapLayer( mExtraSnapLayer ); QgsMapToolAdvancedDigitizing::deactivate(); } @@ -513,6 +526,7 @@ int QgsMapToolCapture::addVertex( const QgsPointXY &point, const QgsPointLocator // ordinary digitizing mRubberBand->addPoint( point ); mCaptureCurve.addVertex( layerPoint ); + updateExtraSnapLayer(); mSnappingMatches.append( match ); } @@ -574,6 +588,7 @@ int QgsMapToolCapture::addCurve( QgsCurve *c ) c->transform( ct, QgsCoordinateTransform::ReverseTransform ); } mCaptureCurve.addCurve( c ); + updateExtraSnapLayer(); for ( int i = 0; i < c->length(); ++i ) mSnappingMatches.append( QgsPointLocator::Match() ); @@ -583,6 +598,7 @@ int QgsMapToolCapture::addCurve( QgsCurve *c ) void QgsMapToolCapture::clearCurve() { mCaptureCurve.clear(); + updateExtraSnapLayer(); } QList QgsMapToolCapture::snappingMatches() const @@ -627,6 +643,7 @@ void QgsMapToolCapture::undo() vertexToRemove.vertex = size() - 1; mCaptureCurve.deleteVertex( vertexToRemove ); mSnappingMatches.removeAt( vertexToRemove.vertex ); + updateExtraSnapLayer(); mCadDockWidget->removePreviousPoint(); @@ -676,6 +693,7 @@ void QgsMapToolCapture::stopCapturing() mCapturing = false; mCaptureCurve.clear(); + updateExtraSnapLayer(); mSnappingMatches.clear(); if ( currentVectorLayer() ) currentVectorLayer()->triggerRepaint(); @@ -695,6 +713,7 @@ void QgsMapToolCapture::clean() void QgsMapToolCapture::closePolygon() { mCaptureCurve.close(); + updateExtraSnapLayer(); } void QgsMapToolCapture::validateGeometry() @@ -795,6 +814,7 @@ void QgsMapToolCapture::setPoints( const QVector &pointList ) QgsLineString *line = new QgsLineString( pointList ); mCaptureCurve.clear(); mCaptureCurve.addCurve( line ); + updateExtraSnapLayer(); mSnappingMatches.clear(); for ( int i = 0; i < line->length(); ++i ) mSnappingMatches.append( QgsPointLocator::Match() ); @@ -805,6 +825,7 @@ void QgsMapToolCapture::setPoints( const QgsPointSequence &pointList ) QgsLineString *line = new QgsLineString( pointList ); mCaptureCurve.clear(); mCaptureCurve.addCurve( line ); + updateExtraSnapLayer(); mSnappingMatches.clear(); for ( int i = 0; i < line->length(); ++i ) mSnappingMatches.append( QgsPointLocator::Match() ); @@ -868,3 +889,19 @@ QgsPoint QgsMapToolCapture::mapPoint( const QgsMapMouseEvent &e ) const return newPoint; } + +void QgsMapToolCapture::updateExtraSnapLayer() +{ + if ( canvas()->snappingUtils()->config().selfSnapping() && mCanvas->currentLayer() ) + { + // the current layer may have changed + mExtraSnapLayer->setCrs( mCanvas->currentLayer()->crs() ); + QgsGeometry geom = QgsGeometry( mCaptureCurve.clone() ); + mExtraSnapLayer->changeGeometry( mExtraSnapFeatureId, geom ); + } + else + { + QgsGeometry geom; + mExtraSnapLayer->changeGeometry( mExtraSnapFeatureId, geom ); + } +} diff --git a/src/gui/qgsmaptoolcapture.h b/src/gui/qgsmaptoolcapture.h index c815e0ce569..d2e696aaa1b 100644 --- a/src/gui/qgsmaptoolcapture.h +++ b/src/gui/qgsmaptoolcapture.h @@ -22,6 +22,7 @@ #include "qgscompoundcurve.h" #include "qgsgeometry.h" #include "qobjectuniqueptr.h" +#include "qgssnappingutils.h" #include #include @@ -132,6 +133,8 @@ class GUI_EXPORT QgsMapToolCapture : public QgsMapToolAdvancedDigitizing private slots: void addError( const QgsGeometry::Error &error ); void currentLayerChanged( QgsMapLayer *layer ); + //! Update the extra snap layer, this should be called whenever the capturecurve changes + void updateExtraSnapLayer(); protected: @@ -312,6 +315,11 @@ class GUI_EXPORT QgsMapToolCapture : public QgsMapToolAdvancedDigitizing QList< QgsGeometry::Error > mGeomErrors; QList< QgsVertexMarker * > mGeomErrorMarkers; + //! A layer containing the current capture curve to provide additionnal snapping + QgsVectorLayer *mExtraSnapLayer = nullptr; + //! The feature in that layer (for updating) + QgsFeatureId mExtraSnapFeatureId; + bool mCaptureModeFromLayer = false; std::unique_ptr mSnapIndicator; diff --git a/tests/src/app/testqgsmaptooladdfeatureline.cpp b/tests/src/app/testqgsmaptooladdfeatureline.cpp index 9838e203f2a..b744fb1422b 100644 --- a/tests/src/app/testqgsmaptooladdfeatureline.cpp +++ b/tests/src/app/testqgsmaptooladdfeatureline.cpp @@ -71,6 +71,7 @@ class TestQgsMapToolAddFeatureLine : public QObject void testZMSnapping(); void testTopologicalEditingZ(); void testCloseLine(); + void testSelfSnapping(); private: QgisApp *mQgisApp = nullptr; @@ -85,6 +86,7 @@ class TestQgsMapToolAddFeatureLine : public QObject QgsVectorLayer *mLayerTopoZ = nullptr; QgsVectorLayer *mLayerLine2D = nullptr; QgsVectorLayer *mLayerCloseLine = nullptr; + QgsVectorLayer *mLayerSelfSnapLine = nullptr; QgsFeatureId mFidLineF1 = 0; QgsFeatureId mFidCurvedF1 = 0; }; @@ -208,8 +210,15 @@ void TestQgsMapToolAddFeatureLine::initTestCase() mLayerLine2D->addFeature( lineString2DF ); QCOMPARE( mLayerLine2D->featureCount(), ( long )1 ); - mCanvas->setLayers( QList() << mLayerLine << mLayerLineCurved << mLayerLineZ << mLayerPointZM << mLayerTopoZ << mLayerLine2D ); + // make testing layers + mLayerSelfSnapLine = new QgsVectorLayer( QStringLiteral( "LineString?crs=EPSG:27700" ), QStringLiteral( "layer line" ), QStringLiteral( "memory" ) ); + QVERIFY( mLayerSelfSnapLine->isValid() ); + QgsProject::instance()->addMapLayers( QList() << mLayerSelfSnapLine ); + mLayerSelfSnapLine->startEditing(); + + // add layers to canvas + mCanvas->setLayers( QList() << mLayerLine << mLayerLineCurved << mLayerLineZ << mLayerPointZM << mLayerTopoZ << mLayerLine2D << mLayerSelfSnapLine ); mCanvas->setSnappingUtils( new QgsMapCanvasSnappingUtils( mCanvas, this ) ); // create the tool @@ -578,5 +587,55 @@ void TestQgsMapToolAddFeatureLine::testCloseLine() mLayerCloseLine->undoStack()->undo(); } + +void TestQgsMapToolAddFeatureLine::testSelfSnapping() +{ + TestQgsMapToolAdvancedDigitizingUtils utils( mCaptureTool ); + + mCanvas->setCurrentLayer( mLayerSelfSnapLine ); + + QSet oldFids = utils.existingFeatureIds(); + + QgsSnappingConfig cfg = mCanvas->snappingUtils()->config(); + cfg.setEnabled( true ); + cfg.setMode( QgsSnappingConfig::AllLayers ); + cfg.setTypeFlag( QgsSnappingConfig::VertexFlag ); + cfg.setTolerance( 50 ); + cfg.setUnits( QgsTolerance::Pixels ); + mCanvas->snappingUtils()->setConfig( cfg ); + + + QString targetWkt = "LineString (2 5, 3 5, 3 6, 2 5)"; + + // Without self snapping, endpoint won't snap to start point + cfg.setSelfSnapping( false ); + mCanvas->snappingUtils()->setConfig( cfg ); + + utils.mouseClick( 2, 5, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + utils.mouseClick( 3, 5, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + utils.mouseClick( 3, 6, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + utils.mouseClick( 2, 5.1, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + utils.mouseClick( 2, 5.1, Qt::RightButton ); + + QgsFeatureId newFid1 = utils.newFeatureId( oldFids ); + QVERIFY( ! mLayerSelfSnapLine->getFeature( newFid1 ).geometry().equals( QgsGeometry::fromWkt( targetWkt ) ) ); + mLayerSelfSnapLine->undoStack()->undo(); + + // With self snapping, endpoint will snap to start point + cfg.setSelfSnapping( true ); + mCanvas->snappingUtils()->setConfig( cfg ); + + utils.mouseClick( 2, 5, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + utils.mouseClick( 3, 5, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + utils.mouseClick( 3, 6, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + utils.mouseClick( 2, 5.1, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + utils.mouseClick( 2, 5.1, Qt::RightButton ); + + QgsFeatureId newFid2 = utils.newFeatureId( oldFids ); + QCOMPARE( mLayerSelfSnapLine->getFeature( newFid2 ).geometry(), QgsGeometry::fromWkt( targetWkt ) ); + mLayerSelfSnapLine->undoStack()->undo(); + +} + QGSTEST_MAIN( TestQgsMapToolAddFeatureLine ) #include "testqgsmaptooladdfeatureline.moc" diff --git a/tests/src/core/testqgssnappingutils.cpp b/tests/src/core/testqgssnappingutils.cpp index d806a0351ca..ed3b1d411cc 100644 --- a/tests/src/core/testqgssnappingutils.cpp +++ b/tests/src/core/testqgssnappingutils.cpp @@ -529,6 +529,61 @@ class TestQgsSnappingUtils : public QObject QVERIFY( m5.isValid() ); QVERIFY( m5.hasVertex() ); } + + void testExtraSnapLayers() + { + // START COPYPASTE + QgsMapSettings mapSettings; + mapSettings.setOutputSize( QSize( 100, 100 ) ); + mapSettings.setExtent( QgsRectangle( 0, 0, 1, 1 ) ); + QVERIFY( mapSettings.hasValidSettings() ); + + QgsSnappingUtils u; + QgsSnappingConfig snappingConfig = u.config(); + u.setMapSettings( mapSettings ); + snappingConfig.setEnabled( true ); + snappingConfig.setTypeFlag( QgsSnappingConfig::VertexFlag ); + snappingConfig.setMode( QgsSnappingConfig::AllLayers ); + snappingConfig.setTolerance( 5 ); + snappingConfig.setUnits( QgsTolerance::Pixels ); + u.setConfig( snappingConfig ); + + // additional vector layer + QgsVectorLayer *extraVL = new QgsVectorLayer( QStringLiteral( "Point?field=fId:int" ), QStringLiteral( "x" ), QStringLiteral( "memory" ) ); + extraVL->startEditing(); + + // we start with one point: (5, 5) (at 50, 50 on screen) + QgsFeature f3( extraVL->fields() ); + f3.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 0.50, 0.50 ) ) ); + extraVL->addFeature( f3 ); + QVERIFY( extraVL->featureCount() == 1 ); + + // Without the extra snapping layer, we have no snap + QgsPointLocator::Match m1 = u.snapToMap( QgsPointXY( 0.50, 0.50 ) ); + QVERIFY( !m1.isValid() ); + + // We add the snapping layer, we have snap + u.addExtraSnapLayer( extraVL ); + QgsPointLocator::Match m2 = u.snapToMap( QgsPointXY( 0.50, 0.50 ) ); + QVERIFY( m2.isValid() ); + + // We add to the snapping layer, the snap changed + QgsFeature f4( extraVL->fields() ); + f4.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 0.75, 0.75 ) ) ); + extraVL->addFeature( f4 ); + QVERIFY( extraVL->featureCount() == 2 ); + QgsPointLocator::Match m3 = u.snapToMap( QgsPointXY( 0.50, 0.50 ) ); + QgsPointLocator::Match m4 = u.snapToMap( QgsPointXY( 0.75, 0.75 ) ); + QVERIFY( m3.isValid() ); + QVERIFY( m4.isValid() ); + + // We remove the snapping layer, we have no snap + u.removeExtraSnapLayer( extraVL ); + QgsPointLocator::Match m5 = u.snapToMap( QgsPointXY( 0.50, 0.50 ) ); + QgsPointLocator::Match m6 = u.snapToMap( QgsPointXY( 0.75, 0.75 ) ); + QVERIFY( !m5.isValid() ); + QVERIFY( !m6.isValid() ); + } }; QGSTEST_MAIN( TestQgsSnappingUtils )