diff --git a/src/app/qgsmaptooltrimextendfeature.cpp b/src/app/qgsmaptooltrimextendfeature.cpp index a372773441a4..339b2e6d8b78 100644 --- a/src/app/qgsmaptooltrimextendfeature.cpp +++ b/src/app/qgsmaptooltrimextendfeature.cpp @@ -48,6 +48,7 @@ QgsMapToolTrimExtendFeature::QgsMapToolTrimExtendFeature( QgsMapCanvas *canvas ) : QgsMapToolEdit( canvas ) { mToolName = tr( "Trim/Extend feature" ); + connect( mCanvas, &QgsMapCanvas::extentsChanged, this, &QgsMapToolTrimExtendFeature::extendLimit ); } static bool getPoints( const QgsPointLocator::Match &match, QgsPoint &p1, QgsPoint &p2 ) @@ -89,15 +90,19 @@ void QgsMapToolTrimExtendFeature::canvasMoveEvent( QgsMapMouseEvent *e ) QgsPointXY p1, p2; match.edgePoints( p1, p2 ); - + mLimitLayer = match.layer(); mRubberBandLimit.reset( createRubberBand( Qgis::GeometryType::Line ) ); + mRubberBandLimitExtend.reset( createRubberBand( Qgis::GeometryType::Line ) ); + mRubberBandLimitExtend->setLineStyle( Qt::DotLine ); mRubberBandLimit->addPoint( p1 ); mRubberBandLimit->addPoint( p2 ); mRubberBandLimit->show(); + extendLimit(); } else if ( mRubberBandLimit ) { mRubberBandLimit->hide(); + mRubberBandLimitExtend.reset(); } break; case StepExtend: @@ -136,16 +141,30 @@ void QgsMapToolTrimExtendFeature::canvasMoveEvent( QgsMapMouseEvent *e ) if ( !getPoints( match, pExtend1, pExtend2 ) ) break; + QgsCoordinateTransform transform( mLimitLayer->crs(), mVlayer->crs(), mCanvas->mapSettings().transformContext() ); + + QgsPoint pLimit1Projected( pLimit1 ); + QgsPoint pLimit2Projected( pLimit2 ); + if ( !transform.isShortCircuited() ) + { + const QgsPointXY transformedP1 = transform.transform( pLimit1 ); + const QgsPointXY transformedP2 = transform.transform( pLimit2 ); + pLimit1Projected.setX( transformedP1.x() ); + pLimit1Projected.setY( transformedP1.y() ); + pLimit2Projected.setX( transformedP2.x() ); + pLimit2Projected.setY( transformedP2.y() ); + } + // No need to trim/extend if segments are continuous - if ( ( ( pLimit1 == pExtend1 ) || ( pLimit1 == pExtend2 ) ) || ( ( pLimit2 == pExtend1 ) || ( pLimit2 == pExtend2 ) ) ) + if ( ( ( pLimit1Projected == pExtend1 ) || ( pLimit1Projected == pExtend2 ) ) || ( ( pLimit2Projected == pExtend1 ) || ( pLimit2Projected == pExtend2 ) ) ) break; - mSegmentIntersects = QgsGeometryUtils::segmentIntersection( pLimit1, pLimit2, pExtend1, pExtend2, mIntersection, mIsIntersection, 1e-8, true ); + mSegmentIntersects = QgsGeometryUtils::segmentIntersection( pLimit1Projected, pLimit2Projected, pExtend1, pExtend2, mIntersection, mIsIntersection, 1e-8, true ); if ( mIs3DLayer && QgsWkbTypes::hasZ( match.layer()->wkbType() ) ) { /* Z Interpolation */ - const QgsLineString line( pLimit1, pLimit2 ); + const QgsLineString line( pLimit1Projected, pLimit2Projected ); mIntersection = QgsGeometryUtils::closestPoint( line, QgsPoint( mIntersection ) ); } @@ -168,7 +187,7 @@ void QgsMapToolTrimExtendFeature::canvasMoveEvent( QgsMapMouseEvent *e ) index += 1; } // TRIM PART - else if ( QgsGeometryUtils::leftOfLine( QgsPoint( mMapPoint ), pLimit1, pLimit2 ) != QgsGeometryUtils::leftOfLine( pExtend1, pLimit1, pLimit2 ) ) + else if ( QgsGeometryUtils::leftOfLine( QgsPoint( mMapPoint ), pLimit1Projected, pLimit2Projected ) != QgsGeometryUtils::leftOfLine( pExtend1, pLimit1Projected, pLimit2Projected ) ) { // Part where the mouse is (+) will be trimmed /* | @@ -194,8 +213,10 @@ void QgsMapToolTrimExtendFeature::canvasMoveEvent( QgsMapMouseEvent *e ) if ( mIsModified ) { mGeom.removeDuplicateNodes(); - mRubberBandExtend->setToGeometry( mGeom, mVlayer ); + // Densify by count to better display the intersection when layer crs != map crs + mRubberBandExtend->setToGeometry( mGeom.densifyByCount( 10 ), mVlayer ); mRubberBandExtend->show(); + extendLimit(); } } else @@ -265,15 +286,15 @@ void QgsMapToolTrimExtendFeature::canvasReleaseEvent( QgsMapMouseEvent *e ) emit messageEmitted( tr( "Couldn't trim or extend the feature." ) ); } - // If Ctrl or Shift is pressed, keep the tool active with its reference feature - if ( !( e->modifiers() & ( Qt::ControlModifier | Qt::ShiftModifier ) ) ) - deactivate(); + // If Shift is pressed, keep the tool active with its reference feature + if ( !( e->modifiers() & Qt::ShiftModifier ) ) + reset(); break; } } else if ( e->button() == Qt::RightButton ) { - deactivate(); + reset(); } } @@ -286,11 +307,73 @@ void QgsMapToolTrimExtendFeature::keyPressEvent( QKeyEvent *e ) if ( e && e->key() == Qt::Key_Escape ) { - deactivate(); + reset(); } } -void QgsMapToolTrimExtendFeature::deactivate() + +void QgsMapToolTrimExtendFeature::extendLimit() +{ + if ( !mRubberBandLimitExtend ) + { + return; + } + + QgsVectorLayer *refLayer = qobject_cast( mCanvas->currentLayer() ); + refLayer = refLayer ? refLayer : mVlayer ? mVlayer + : mLimitLayer; + + // Compute intersection between the line that extends the limit segment and the + // edges of the map canvas + QgsPointXY p1 = toLayerCoordinates( refLayer, *mRubberBandLimit->getPoint( 0, 0 ) ); + QgsPointXY p2 = toLayerCoordinates( refLayer, *mRubberBandLimit->getPoint( 0, 1 ) ); + QgsPoint canvasTopLeft = QgsPoint( toLayerCoordinates( refLayer, QPoint( 0, 0 ) ) ); + QgsPoint canvasTopRight = QgsPoint( toLayerCoordinates( refLayer, QPoint( mCanvas->width(), 0 ) ) ); + QgsPoint canvasBottomLeft = QgsPoint( toLayerCoordinates( refLayer, QPoint( 0, mCanvas->height() ) ) ); + QgsPoint canvasBottomRight = QgsPoint( toLayerCoordinates( refLayer, QPoint( mCanvas->width(), mCanvas->height() ) ) ); + + QList points; + points << p1 << p2; + + QgsPoint intersection; + if ( QgsGeometryUtils::lineIntersection( QgsPoint( p1 ), QgsPoint( p2 ) - QgsPoint( p1 ), canvasTopLeft, canvasTopRight - canvasTopLeft, intersection ) ) + { + points << QgsPointXY( intersection ); + } + if ( QgsGeometryUtils::lineIntersection( QgsPoint( p1 ), QgsPoint( p2 ) - QgsPoint( p1 ), canvasTopRight, canvasBottomRight - canvasTopRight, intersection ) ) + { + points << QgsPointXY( intersection ); + } + if ( QgsGeometryUtils::lineIntersection( QgsPoint( p1 ), QgsPoint( p2 ) - QgsPoint( p1 ), canvasBottomRight, canvasBottomLeft - canvasBottomRight, intersection ) ) + { + points << QgsPointXY( intersection ); + } + if ( QgsGeometryUtils::lineIntersection( QgsPoint( p1 ), QgsPoint( p2 ) - QgsPoint( p1 ), canvasBottomLeft, canvasTopLeft - canvasBottomLeft, intersection ) ) + { + points << QgsPointXY( intersection ); + } + + // Reorder the points by x/y coordinates + std::sort( points.begin(), points.end(), []( const QgsPointXY &a, const QgsPointXY &b ) -> bool { + if ( a.x() == b.x() ) + return a.y() < b.y(); + return a.x() < b.x(); + } ); + + // Keep only the closest intersection points from the original points + const int p1Idx = points.indexOf( p1 ); + const int p2Idx = points.indexOf( p2 ); + const int first = std::max( 0, std::min( p1Idx, p2Idx ) - 1 ); + const int last = std::min( static_cast( points.size() ) - 1, std::max( p1Idx, p2Idx ) + 1 ); + const QgsPolylineXY polyline = points.mid( first, last - first + 1 ).toVector(); + + // Densify the polyline to display a more accurate prediction when layer crs != canvas crs + QgsGeometry geom = QgsGeometry::fromPolylineXY( polyline ).densifyByCount( 10 ); + + mRubberBandLimitExtend->setToGeometry( geom, refLayer ); + mRubberBandLimitExtend->show(); +} +void QgsMapToolTrimExtendFeature::reset() { mStep = StepLimit; mIsModified = false; @@ -298,9 +381,31 @@ void QgsMapToolTrimExtendFeature::deactivate() mIsIntersection = false; mSegmentIntersects = false; mRubberBandLimit.reset(); + mRubberBandLimitExtend.reset(); mRubberBandExtend.reset(); mRubberBandIntersection.reset(); - QgsMapTool::deactivate(); mVlayer = nullptr; mLimitLayer = nullptr; } +void QgsMapToolTrimExtendFeature::activate() +{ + QgsMapTool::activate(); + + // Save the original snapping configuration + mOriginalSnappingConfig = mCanvas->snappingUtils()->config(); + + // Enable Snapping & Snapping on Segment + QgsSnappingConfig snappingConfig = mOriginalSnappingConfig; + snappingConfig.setEnabled( true ); + Qgis::SnappingTypes flags = snappingConfig.typeFlag(); + flags |= Qgis::SnappingType::Segment; + snappingConfig.setTypeFlag( flags ); + mCanvas->snappingUtils()->setConfig( snappingConfig ); +} +void QgsMapToolTrimExtendFeature::deactivate() +{ + reset(); + // Restore the original snapping configuration + mCanvas->snappingUtils()->setConfig( mOriginalSnappingConfig ); + QgsMapTool::deactivate(); +} diff --git a/src/app/qgsmaptooltrimextendfeature.h b/src/app/qgsmaptooltrimextendfeature.h index 20283a13d028..45a9992b1ea6 100644 --- a/src/app/qgsmaptooltrimextendfeature.h +++ b/src/app/qgsmaptooltrimextendfeature.h @@ -19,6 +19,7 @@ #include "qgsmaptooledit.h" #include "qgis_app.h" #include "qgsrubberband.h" +#include "qgssnappingconfig.h" class APP_EXPORT QgsMapToolTrimExtendFeature : public QgsMapToolEdit { @@ -35,12 +36,22 @@ class APP_EXPORT QgsMapToolTrimExtendFeature : public QgsMapToolEdit void keyPressEvent( QKeyEvent *e ) override; + //! called when map tool is being activated + void activate() override; + //! called when map tool is being deactivated void deactivate() override; + private slots: + // Recompute the extended limit + void extendLimit(); + void reset(); + private: - //! Rubberband that shows the limit + //! Rubberband that highlights the limit segment std::unique_ptr mRubberBandLimit; + //! Rubberband that extends the limit segment + std::unique_ptr mRubberBandLimitExtend; //! Rubberband that shows the feature being extended std::unique_ptr mRubberBandExtend; //! Rubberband that shows the intersection point @@ -74,6 +85,9 @@ class APP_EXPORT QgsMapToolTrimExtendFeature : public QgsMapToolEdit }; //! The first step (0): choose the limit. The second step (1): choose the segment to trim/extend Step mStep = StepLimit; + + //! Snapping config that will be restored on deactivation + QgsSnappingConfig mOriginalSnappingConfig; }; #endif // QGSMAPTOOLTRIMEXTENDFEATURE_H diff --git a/src/ui/qgisapp.ui b/src/ui/qgisapp.ui index d79d92e86bcc..a77ab3c90bf4 100644 --- a/src/ui/qgisapp.ui +++ b/src/ui/qgisapp.ui @@ -1181,7 +1181,7 @@ Shift+O to turn segments into straight or curve lines. Trim/Extend Feature - Click to set the reference segment -- Click on another segment to trim it or extend it to the reference segment (Use Shift or Ctrl to keep the reference segment active) +- Click on another segment to trim it or extend it to the reference segment (Use Shift to keep the reference segment active) Note : Segment snapping must be enabled in the snapping Toolbar diff --git a/tests/src/app/testqgsmaptooltrimextendfeature.cpp b/tests/src/app/testqgsmaptooltrimextendfeature.cpp index 81a60abb86ed..3716e2446619 100644 --- a/tests/src/app/testqgsmaptooltrimextendfeature.cpp +++ b/tests/src/app/testqgsmaptooltrimextendfeature.cpp @@ -65,7 +65,7 @@ class TestQgsMapToolTrimExtendFeature : public QObject // | \| | // | + (2,1) | // (0,0) +-------------------+ (3,0) - vlPolygon.reset( new QgsVectorLayer( QStringLiteral( "MultiPolygon?field=fld:int" ), QStringLiteral( "x" ), QStringLiteral( "memory" ) ) ); + vlPolygon.reset( new QgsVectorLayer( QStringLiteral( "MultiPolygon?crs=EPSG:3946&field=fld:int" ), QStringLiteral( "x" ), QStringLiteral( "memory" ) ) ); const int idx = vlPolygon->fields().indexFromName( QStringLiteral( "fld" ) ); QVERIFY( idx != -1 ); f1.initAttributes( 1 ); @@ -606,7 +606,7 @@ class TestQgsMapToolTrimExtendFeature : public QObject pt.toQPointF().toPoint(), Qt::LeftButton, Qt::NoButton, - Qt::ControlModifier + Qt::ShiftModifier ) ); tool->canvasReleaseEvent( event.get() );