From a50c6b48297331b990ccffaa74397fccc8b41432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bartoletti?= Date: Wed, 5 Feb 2025 09:17:46 +0100 Subject: [PATCH] fix(QgsMapToolCapture): Allow snapping point with different crs --- .../maptools/qgsmaptoolcapture.sip.in | 9 +- .../maptools/qgsmaptoolcapture.sip.in | 9 +- src/gui/maptools/qgsmaptoolcapture.cpp | 9 +- src/gui/maptools/qgsmaptoolcapture.h | 9 +- .../app/maptooladdfeatureline/CMakeLists.txt | 1 + .../testqgsmaptooladdfeaturelinezm_crs.cpp | 170 ++++++++++++++++++ 6 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 tests/src/app/maptooladdfeatureline/testqgsmaptooladdfeaturelinezm_crs.cpp diff --git a/python/PyQt6/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in b/python/PyQt6/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in index 27dc9782f016..259ca9233af5 100644 --- a/python/PyQt6/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in +++ b/python/PyQt6/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in @@ -222,14 +222,13 @@ Converts a point to map coordinates and layer coordinates int fetchLayerPoint( const QgsPointLocator::Match &match, QgsPoint &layerPoint ); %Docstring -Fetches the original point from the source layer if it has the same -CRS as the current layer. -If topological editing is activated, the points are projected to the -current layer CRS. +Fetches the original point from the source layer. +If topological editing is activated. +The points are projected to the current layer CRS. :return: 0 in case of success - 1 if not applicable (CRS mismatch / invalid layer) + 1 if not applicable (invalid layer) 2 in case of failure %End diff --git a/python/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in b/python/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in index 10a922324d03..3c72259465f2 100644 --- a/python/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in +++ b/python/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in @@ -222,14 +222,13 @@ Converts a point to map coordinates and layer coordinates int fetchLayerPoint( const QgsPointLocator::Match &match, QgsPoint &layerPoint ); %Docstring -Fetches the original point from the source layer if it has the same -CRS as the current layer. -If topological editing is activated, the points are projected to the -current layer CRS. +Fetches the original point from the source layer. +If topological editing is activated. +The points are projected to the current layer CRS. :return: 0 in case of success - 1 if not applicable (CRS mismatch / invalid layer) + 1 if not applicable (invalid layer) 2 in case of failure %End diff --git a/src/gui/maptools/qgsmaptoolcapture.cpp b/src/gui/maptools/qgsmaptoolcapture.cpp index 0477f72e2cf6..7d1ff5265abf 100644 --- a/src/gui/maptools/qgsmaptoolcapture.cpp +++ b/src/gui/maptools/qgsmaptoolcapture.cpp @@ -642,10 +642,6 @@ int QgsMapToolCapture::fetchLayerPoint( const QgsPointLocator::Match &match, Qgs { if ( ( match.hasVertex() || match.hasLineEndpoint() ) ) { - if ( sourceLayer->crs() != vlayer->crs() ) - { - return 1; - } QgsFeature f; QgsFeatureRequest request; request.setFilterFid( match.featureId() ); @@ -657,6 +653,7 @@ int QgsMapToolCapture::fetchLayerPoint( const QgsPointLocator::Match &match, Qgs { return 2; } + layerPoint = f.geometry().constGet()->vertexAt( vId ); if ( QgsWkbTypes::hasZ( vlayer->wkbType() ) && !layerPoint.is3D() ) layerPoint.addZValue( defaultZValue() ); @@ -674,6 +671,10 @@ int QgsMapToolCapture::fetchLayerPoint( const QgsPointLocator::Match &match, Qgs layerPoint.dropMValue(); } + if ( sourceLayer->crs() != vlayer->crs() ) + { + layerPoint = toLayerCoordinates( vlayer, layerPoint ); + } return 0; } return 2; diff --git a/src/gui/maptools/qgsmaptoolcapture.h b/src/gui/maptools/qgsmaptoolcapture.h index 57222ac541c4..283987060950 100644 --- a/src/gui/maptools/qgsmaptoolcapture.h +++ b/src/gui/maptools/qgsmaptoolcapture.h @@ -241,13 +241,12 @@ class GUI_EXPORT QgsMapToolCapture : public QgsMapToolAdvancedDigitizing // TODO QGIS 4.0 returns an enum instead of a magic constant /** - * Fetches the original point from the source layer if it has the same - * CRS as the current layer. - * If topological editing is activated, the points are projected to the - * current layer CRS. + * Fetches the original point from the source layer. + * If topological editing is activated. + * The points are projected to the current layer CRS. * \returns * 0 in case of success - * 1 if not applicable (CRS mismatch / invalid layer) + * 1 if not applicable (invalid layer) * 2 in case of failure */ int fetchLayerPoint( const QgsPointLocator::Match &match, QgsPoint &layerPoint ); diff --git a/tests/src/app/maptooladdfeatureline/CMakeLists.txt b/tests/src/app/maptooladdfeatureline/CMakeLists.txt index 219b408610d2..9f87a4575521 100644 --- a/tests/src/app/maptooladdfeatureline/CMakeLists.txt +++ b/tests/src/app/maptooladdfeatureline/CMakeLists.txt @@ -18,6 +18,7 @@ set(TESTS testqgsmaptooladdfeaturelinez.cpp testqgsmaptooladdfeaturelinem.cpp testqgsmaptooladdfeaturelinezm.cpp + testqgsmaptooladdfeaturelinezm_crs.cpp ) foreach(TESTSRC ${TESTS}) diff --git a/tests/src/app/maptooladdfeatureline/testqgsmaptooladdfeaturelinezm_crs.cpp b/tests/src/app/maptooladdfeatureline/testqgsmaptooladdfeaturelinezm_crs.cpp new file mode 100644 index 000000000000..d3e7bc1f6ffd --- /dev/null +++ b/tests/src/app/maptooladdfeatureline/testqgsmaptooladdfeaturelinezm_crs.cpp @@ -0,0 +1,170 @@ +/*************************************************************************** + testqgsmaptooladdfeaturelinezm.cpp + ---------------------- + Date : February 2025 + Copyright : (C) 2025 by Loïc Bartoletti + Email : loic dot bartoletti at oslandia dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstest.h" +#include "qgisapp.h" +#include "qgsadvanceddigitizingdockwidget.h" +#include "qgsgeometry.h" +#include "qgsmapcanvas.h" +#include "qgsmapcanvassnappingutils.h" +#include "qgssnappingconfig.h" +#include "qgsmaptooladdfeature.h" +#include "qgsproject.h" +#include "qgssettings.h" +#include "qgssettingsregistrycore.h" +#include "qgsvectorlayer.h" +#include "qgswkbtypes.h" +#include "qgsmapmouseevent.h" +#include +#include + +// Comparison operator for QgsGeometry. +bool operator==( const QgsGeometry &g1, const QgsGeometry &g2 ) +{ + if ( g1.isNull() && g2.isNull() ) + return true; + else + return g1.equals( g2 ); +} + +namespace QTest +{ + // Pretty-printing for QgsGeometry. + template<> char *toString( const QgsGeometry &geom ) + { + QByteArray ba = geom.asWkt().toLatin1(); + return qstrdup( ba.data() ); + } +} // namespace QTest + +class TestQgsMapToolAddFeatureLineZMCRS : public QObject +{ + Q_OBJECT + public: + TestQgsMapToolAddFeatureLineZMCRS() = default; + + private slots: + void initTestCase(); + void cleanupTestCase(); + void testZMCRS(); + + private: + QgisApp *mQgisApp = nullptr; + QgsMapCanvas *mCanvas = nullptr; + QgsMapToolAddFeature *mCaptureTool = nullptr; + QgsVectorLayer *mCaptureLayer = nullptr; + QgsVectorLayer *mSnappingLayer = nullptr; +}; + +void TestQgsMapToolAddFeatureLineZMCRS::initTestCase() +{ + QgsApplication::init(); + QgsApplication::initQgis(); + mQgisApp = new QgisApp(); + + + mCanvas = new QgsMapCanvas(); + mCanvas->setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3857" ) ) ); + mCanvas->setFrameStyle( QFrame::NoFrame ); + mCanvas->resize( 512, 512 ); + mCanvas->setExtent( QgsRectangle( 0, 0, 10, 10 ) ); + mCanvas->show(); + + + mCaptureLayer = new QgsVectorLayer( QStringLiteral( "LineStringZM?crs=EPSG:3857" ), QStringLiteral( "Line Capture Layer" ), QStringLiteral( "memory" ) ); + QVERIFY( mCaptureLayer->isValid() ); + QgsProject::instance()->addMapLayers( { mCaptureLayer } ); + + + mSnappingLayer = new QgsVectorLayer( QStringLiteral( "PointZM?crs=EPSG:4326" ), QStringLiteral( "Snapping Points" ), QStringLiteral( "memory" ) ); + QVERIFY( mSnappingLayer->isValid() ); + + QgsFeature snapFeature( mSnappingLayer->fields() ); + QgsPoint snapPoint( 7, 4, 999, 888 ); + snapFeature.setGeometry( QgsGeometry::fromPoint( snapPoint ) ); + mSnappingLayer->startEditing(); + mSnappingLayer->addFeature( snapFeature ); + mSnappingLayer->commitChanges(); + QCOMPARE( mSnappingLayer->featureCount(), ( long ) 1 ); + + + QgsProject::instance()->addMapLayers( { mSnappingLayer, mCaptureLayer } ); + mCanvas->setLayers( { mCaptureLayer, mSnappingLayer } ); + mCanvas->setSnappingUtils( new QgsMapCanvasSnappingUtils( mCanvas, this ) ); +} + +void TestQgsMapToolAddFeatureLineZMCRS::cleanupTestCase() +{ + delete mCaptureTool; + delete mCanvas; + QgsApplication::exitQgis(); +} + +void TestQgsMapToolAddFeatureLineZMCRS::testZMCRS() +{ + QgsSettingsRegistryCore::settingsDigitizingDefaultMValue->setValue( 222 ); + QgsSettingsRegistryCore::settingsDigitizingDefaultZValue->setValue( 456 ); + + mCanvas->setCurrentLayer( mCaptureLayer ); + mCaptureLayer->startEditing(); + + QgsSnappingConfig snapConfig = mCanvas->snappingUtils()->config(); + snapConfig.setEnabled( true ); + snapConfig.setTolerance( 2 ); + snapConfig.setUnits( Qgis::MapToolUnit::Layer ); + snapConfig.setMode( Qgis::SnappingMode::AllLayers ); + snapConfig.setTypeFlag( Qgis::SnappingType::Vertex ); + mCanvas->snappingUtils()->setConfig( snapConfig ); + mCanvas->snappingUtils()->locatorForLayer( mSnappingLayer )->init(); + mCanvas->snappingUtils()->locatorForLayer( mCaptureLayer )->init(); + mCaptureTool = new QgsMapToolAddFeature( mCanvas, QgisApp::instance()->cadDockWidget(), QgsMapToolCapture::CaptureLine ); + mCanvas->setMapTool( mCaptureTool ); + + QSet oldFids; + QgsFeatureIterator it = mCaptureLayer->getFeatures(); + QgsFeature f; + while ( it.nextFeature( f ) ) + oldFids.insert( f.id() ); + + QgsPointXY p2XY = mCanvas->mapSettings().layerToMapCoordinates( mSnappingLayer, QgsPointXY( 7, 4 ) ); + QPoint p1( int( ( 0.0 / 10 ) * 512 ), int( ( ( 10 - 1.0 ) / 10 ) * 512 ) ); // (0,1) + QPoint p2( int( ( p2XY.x() / 10 ) * 512 ), int( ( ( 10 - p2XY.y() ) / 10 ) * 512 ) ); // (7,4) – should snap + QPoint p3( int( ( 2.0 / 10 ) * 512 ), int( ( ( 10 - 2.0 ) / 10 ) * 512 ) ); // (2,2) – should remain unsnapped + + QTest::mouseClick( mCanvas->viewport(), Qt::LeftButton, Qt::NoModifier, p1 ); + QTest::mouseMove( mCanvas->viewport(), p2 ); + QTest::mouseClick( mCanvas->viewport(), Qt::LeftButton, Qt::NoModifier, p2 ); + QTest::mouseClick( mCanvas->viewport(), Qt::LeftButton, Qt::NoModifier, p3 ); + QTest::mouseClick( mCanvas->viewport(), Qt::RightButton, Qt::NoModifier, p3 ); + + QgsFeature newFeature; + QgsFeatureIterator itNew = mCaptureLayer->getFeatures(); + while ( itNew.nextFeature( f ) ) + { + if ( !oldFids.contains( f.id() ) ) + { + newFeature = f; + break; + } + } + QVERIFY( newFeature.isValid() ); + + QString expectedWkt = QStringLiteral( "LineString ZM (0 1 456 222, 7 4 999 888, 2 2 456 222)" ); + qDebug() << "Captured geometry:" << newFeature.geometry().asWkt(); + QCOMPARE( newFeature.geometry().asWkt( 0 ), expectedWkt ); +} + +QGSTEST_MAIN( TestQgsMapToolAddFeatureLineZMCRS ) +#include "testqgsmaptooladdfeaturelinezm_crs.moc"