Merge pull request #62295 from uclaros/fix-reshape-topological

Add all topological points to all editable layers when reshaping
This commit is contained in:
Julien Cabieces 2025-06-18 17:37:14 +02:00 committed by GitHub
commit 69fabc24f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 134 additions and 88 deletions

View File

@ -343,6 +343,7 @@ Merge features into a single one.
%End
};
/************************************************************************

View File

@ -343,6 +343,7 @@ Merge features into a single one.
%End
};
/************************************************************************

View File

@ -144,68 +144,8 @@ void QgsMapToolAddFeature::featureDigitized( const QgsFeature &feature )
}
if ( topologicalEditing )
{
QgsFeatureRequest request = QgsFeatureRequest().setNoAttributes().setFlags( Qgis::FeatureRequestFlag::NoGeometry ).setLimit( 1 );
const QgsRectangle bbox = feature.geometry().boundingBox();
const QList<QgsMapLayer *> layers = canvas()->layers( true );
for ( QgsMapLayer *layer : layers )
{
QgsVectorLayer *vectorLayer = qobject_cast<QgsVectorLayer *>( layer );
QgsRectangle searchRect;
QgsFeature f;
QgsCoordinateTransform transform;
if ( !vectorLayer || !vectorLayer->isEditable() )
continue;
if ( !( vectorLayer->geometryType() == Qgis::GeometryType::Polygon || vectorLayer->geometryType() == Qgis::GeometryType::Line ) )
continue;
if ( vectorLayer->crs() == vlayer->crs() )
{
searchRect = QgsRectangle( bbox );
}
else
{
transform = QgsCoordinateTransform( vlayer->crs(), vectorLayer->crs(), vectorLayer->transformContext() );
searchRect = transform.transformBoundingBox( bbox );
}
searchRect.grow( QgsVectorLayerEditUtils::getTopologicalSearchRadius( vectorLayer ) );
request.setFilterRect( searchRect );
// We check that there is actually at least one feature intersecting our geometry in the layer to avoid creating an empty edit command and calling costly addTopologicalPoint
if ( !vectorLayer->getFeatures( request ).nextFeature( f ) )
continue;
vectorLayer->beginEditCommand( tr( "Topological points added by 'Add Feature'" ) );
int res = 2;
if ( vectorLayer->crs() != vlayer->crs() )
{
QgsGeometry transformedGeom = feature.geometry();
try
{
// transform digitized geometry from vlayer crs to vectorLayer crs and add topological points
transformedGeom.transform( transform );
res = vectorLayer->addTopologicalPoints( transformedGeom );
}
catch ( QgsCsException &cse )
{
Q_UNUSED( cse )
QgsDebugError( QStringLiteral( "transformation to vectorLayer coordinate failed" ) );
}
}
else
{
res = vectorLayer->addTopologicalPoints( feature.geometry() );
}
if ( res == 0 ) // i.e. if any points were added
vectorLayer->endEditCommand();
else
vectorLayer->destroyEditCommand();
}
QgsVectorLayerEditUtils::addTopologicalPointsToLayers( feature.geometry(), vlayer, layers, mToolName );
}
}
}

View File

@ -24,6 +24,8 @@
#include "qgsvectorlayer.h"
#include "qgisapp.h"
#include "qgsmapmouseevent.h"
#include "qgsvectorlayereditutils.h"
#include "qgsmultipoint.h"
QgsMapToolReshape::QgsMapToolReshape( QgsMapCanvas *canvas )
: QgsMapToolCapture( canvas, QgisApp::instance()->cadDockWidget(), QgsMapToolCapture::CaptureLine )
@ -211,18 +213,13 @@ void QgsMapToolReshape::reshape( QgsVectorLayer *vlayer )
if ( reshapeDone )
{
// Add topological points due to snapping
// Add topological points
if ( QgsProject::instance()->topologicalEditing() )
{
const QList<QgsPointLocator::Match> sm = snappingMatches();
Q_ASSERT( pts.size() == sm.size() );
for ( int i = 0; i < sm.size(); ++i )
{
if ( sm.at( i ).layer() )
{
sm.at( i ).layer()->addTopologicalPoints( pts.at( i ) );
}
}
//check if we need to add topological points to other layers
const QList<QgsMapLayer *> layers = canvas()->layers( true );
QgsGeometry pointsAsGeom( new QgsMultiPoint( pts ) );
QgsVectorLayerEditUtils::addTopologicalPointsToLayers( pointsAsGeom, vlayer, layers, mToolName );
}
vlayer->endEditCommand();

View File

@ -231,6 +231,74 @@ double QgsVectorLayerEditUtils::getTopologicalSearchRadius( const QgsVectorLayer
}
return threshold;
}
void QgsVectorLayerEditUtils::addTopologicalPointsToLayers( const QgsGeometry &geom, QgsVectorLayer *vlayer, const QList<QgsMapLayer *> &layers, const QString &toolName )
{
QgsFeatureRequest request = QgsFeatureRequest().setNoAttributes().setFlags( Qgis::FeatureRequestFlag::NoGeometry ).setLimit( 1 );
QgsFeature f;
for ( QgsMapLayer *layer : layers )
{
QgsVectorLayer *vectorLayer = qobject_cast<QgsVectorLayer *>( layer );
if ( vectorLayer && vectorLayer->isEditable() && vectorLayer->isSpatial() && ( vectorLayer->geometryType() == Qgis::GeometryType::Line || vectorLayer->geometryType() == Qgis::GeometryType::Polygon ) )
{
// boundingBox() is cached, it doesn't matter calling it in the loop
QgsRectangle bbox = geom.boundingBox();
QgsCoordinateTransform ct;
if ( vectorLayer->crs() != vlayer->crs() )
{
ct = QgsCoordinateTransform( vlayer->crs(), vectorLayer->crs(), vectorLayer->transformContext() );
try
{
bbox = ct.transformBoundingBox( bbox );
}
catch ( QgsCsException & )
{
QgsDebugError( QStringLiteral( "Bounding box transformation failed, skipping topological points for layer %1" ).arg( vlayer->id() ) );
continue;
}
}
bbox.grow( getTopologicalSearchRadius( vectorLayer ) );
request.setFilterRect( bbox );
// We check that there is actually at least one feature intersecting our geometry in the layer to avoid creating an empty edit command and calling costly addTopologicalPoint
if ( !vectorLayer->getFeatures( request ).nextFeature( f ) )
continue;
vectorLayer->beginEditCommand( QObject::tr( "Topological points added by '%1'" ).arg( toolName ) );
int returnValue = 2;
if ( vectorLayer->crs() != vlayer->crs() )
{
try
{
// transform digitized geometry from vlayer crs to vectorLayer crs and add topological points
QgsGeometry transformedGeom( geom );
transformedGeom.transform( ct );
returnValue = vectorLayer->addTopologicalPoints( transformedGeom );
}
catch ( QgsCsException & )
{
QgsDebugError( QStringLiteral( "transformation to vectorLayer coordinate failed" ) );
}
}
else
{
returnValue = vectorLayer->addTopologicalPoints( geom );
}
if ( returnValue == 0 )
{
vectorLayer->endEditCommand();
}
else
{
// the layer was not modified, leave the undo buffer intact
vectorLayer->destroyEditCommand();
}
}
}
}
///@endcond
Qgis::GeometryOperationResult QgsVectorLayerEditUtils::addRing( const QVector<QgsPointXY> &ring, const QgsFeatureIds &targetFeatureIds, QgsFeatureId *modifiedFeatureId )

View File

@ -284,6 +284,8 @@ class CORE_EXPORT QgsVectorLayerEditUtils
///@cond PRIVATE
static double getTopologicalSearchRadius( const QgsVectorLayer *layer ) SIP_SKIP;
static void addTopologicalPointsToLayers( const QgsGeometry &geom, QgsVectorLayer *vlayer, const QList<QgsMapLayer *> &layers, const QString &toolName ) SIP_SKIP;
///@endcond
private:

View File

@ -44,6 +44,7 @@ class TestQgsMapToolReshape : public QObject
void testReshapeZ();
void testTopologicalEditing();
void testTopologicalEditingNoSnap();
void testAvoidIntersectionAndTopoEdit();
void reshapeWithBindingLine();
void testWithTracing();
@ -153,10 +154,10 @@ void TestQgsMapToolReshape::initTestCase()
QCOMPARE( mLayerPolygonZ->getFeature( 1 ).geometry().asWkt(), wkt5 );
mLayerTopo->startEditing();
const QString wkt6 = "Polygon ((0 0, 4 0, 4 4, 0 4))";
const QString wkt6 = "Polygon ((0 0, 4 0, 4 4, 0 4, 0 0))";
QgsFeature f6;
f6.setGeometry( QgsGeometry::fromWkt( wkt6 ) );
const QString wkt7 = "Polygon ((7 0, 8 0, 8 4, 7 4))";
const QString wkt7 = "Polygon ((7 0, 8 0, 8 4, 7 4, 7 0))";
QgsFeature f7;
f7.setGeometry( QgsGeometry::fromWkt( wkt7 ) );
QgsFeatureList flistTopo;
@ -168,10 +169,10 @@ void TestQgsMapToolReshape::initTestCase()
mLayerTopo2->startEditing();
QgsFeature f8;
f8.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "Polygon ((0 5, 4 5, 4 7, 0 7))" ) ) );
f8.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "Polygon ((0 5, 4 5, 4 7, 0 7, 0 5))" ) ) );
mLayerTopo2->dataProvider()->addFeatures( QgsFeatureList() << f8 );
QCOMPARE( mLayerTopo2->featureCount(), 1 );
QCOMPARE( mLayerTopo2->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 5, 4 5, 4 7, 0 7))" ) );
QCOMPARE( mLayerTopo2->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 5, 4 5, 4 7, 0 7, 0 5))" ) );
mLayerLine->startEditing();
const QString wkt9 = QStringLiteral( "LineString (0 0, 10 10)" );
@ -267,25 +268,61 @@ void TestQgsMapToolReshape::testTopologicalEditing()
QgsSettingsRegistryCore::settingsDigitizingDefaultZValue->setValue( 333 );
utils.mouseClick( 4, 4, Qt::LeftButton, Qt::KeyboardModifiers(), true );
utils.mouseClick( 7, 2, Qt::LeftButton, Qt::KeyboardModifiers(), true );
utils.mouseClick( 8, 2, Qt::LeftButton, Qt::KeyboardModifiers(), true );
utils.mouseClick( 4, 0, Qt::LeftButton, Qt::KeyboardModifiers(), true );
utils.mouseClick( 4, 0, Qt::RightButton );
const QString wkt = "Polygon ((4 0, 8 2, 4 4, 0 4, 0 0, 4 0))";
const QString wkt2 = "Polygon ((7 0, 8 0, 8 2, 8 4, 7 4))";
const QString wkt2 = "Polygon ((7 0, 8 0, 8 2, 8 4, 7 4, 7 0))";
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), wkt );
QCOMPARE( mLayerTopo->getFeature( 2 ).geometry().asWkt(), wkt2 );
mLayerTopo->undoStack()->undo();
QCOMPARE( mLayerTopo2->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 5, 4 5, 4 7, 0 7))" ) );
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 0, 4 0, 4 4, 0 4))" ) );
QCOMPARE( mLayerTopo->getFeature( 2 ).geometry().asWkt(), QStringLiteral( "Polygon ((7 0, 8 0, 8 4, 7 4))" ) );
QCOMPARE( mLayerTopo2->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 5, 4 5, 4 7, 0 7, 0 5))" ) );
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 0, 4 0, 4 4, 0 4, 0 0))" ) );
QCOMPARE( mLayerTopo->getFeature( 2 ).geometry().asWkt(), QStringLiteral( "Polygon ((7 0, 8 0, 8 4, 7 4, 7 0))" ) );
QgsProject::instance()->setTopologicalEditing( topologicalEditing );
}
void TestQgsMapToolReshape::testTopologicalEditingNoSnap()
{
QgsProject::instance()->addMapLayers( QList<QgsMapLayer *>() << mLayerTopo );
mCanvas->setLayers( QList<QgsMapLayer *>() << mLayerTopo );
QgsSnappingConfig cfg = mCanvas->snappingUtils()->config();
cfg.setEnabled( false );
mCanvas->snappingUtils()->setConfig( cfg );
const bool topologicalEditing = QgsProject::instance()->topologicalEditing();
QgsProject::instance()->setTopologicalEditing( true );
mCanvas->setCurrentLayer( mLayerTopo );
TestQgsMapToolAdvancedDigitizingUtils utils( mCaptureTool );
utils.mouseClick( 4, 4, Qt::LeftButton, Qt::KeyboardModifiers(), true );
utils.mouseClick( 8, 2, Qt::LeftButton, Qt::KeyboardModifiers(), true );
utils.mouseClick( 4, 0, Qt::LeftButton, Qt::KeyboardModifiers(), true );
utils.mouseClick( 4, 0, Qt::RightButton );
const QString wkt = "Polygon ((4 0, 8 2, 4 4, 0 4, 0 0, 4 0))";
const QString wkt2 = "Polygon ((7 0, 8 0, 8 2, 8 4, 7 4, 7 0))";
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), wkt );
QCOMPARE( mLayerTopo->getFeature( 2 ).geometry().asWkt(), wkt2 );
mLayerTopo->undoStack()->undo();
QCOMPARE( mLayerTopo2->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 5, 4 5, 4 7, 0 7, 0 5))" ) );
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 0, 4 0, 4 4, 0 4, 0 0))" ) );
QCOMPARE( mLayerTopo->getFeature( 2 ).geometry().asWkt(), QStringLiteral( "Polygon ((7 0, 8 0, 8 4, 7 4, 7 0))" ) );
QgsProject::instance()->setTopologicalEditing( topologicalEditing );
cfg.setEnabled( true );
mCanvas->snappingUtils()->setConfig( cfg );
}
void TestQgsMapToolReshape::testAvoidIntersectionAndTopoEdit()
{
QList<QgsMapLayer *> layers = { mLayerTopo, mLayerTopo2 };
@ -312,15 +349,15 @@ void TestQgsMapToolReshape::testAvoidIntersectionAndTopoEdit()
utils.mouseClick( 4, 5, Qt::RightButton );
QCOMPARE( mLayerTopo2->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 5, 0 7, 4 7, 4 5, 3 4, 1 4, 0 5))" ) );
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 0, 4 0, 4 4, 3 4, 1 4, 0 4))" ) );
QCOMPARE( mLayerTopo->getFeature( 2 ).geometry().asWkt(), QStringLiteral( "Polygon ((7 0, 8 0, 8 4, 7 4))" ) );
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 0, 4 0, 4 4, 3 4, 1 4, 0 4, 0 0))" ) );
QCOMPARE( mLayerTopo->getFeature( 2 ).geometry().asWkt(), QStringLiteral( "Polygon ((7 0, 8 0, 8 4, 7 4, 7 0))" ) );
mLayerTopo2->undoStack()->undo();
mLayerTopo->undoStack()->undo();
QCOMPARE( mLayerTopo2->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 5, 4 5, 4 7, 0 7))" ) );
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 0, 4 0, 4 4, 0 4))" ) );
QCOMPARE( mLayerTopo->getFeature( 2 ).geometry().asWkt(), QStringLiteral( "Polygon ((7 0, 8 0, 8 4, 7 4))" ) );
QCOMPARE( mLayerTopo2->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 5, 4 5, 4 7, 0 7, 0 5))" ) );
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 0, 4 0, 4 4, 0 4, 0 0))" ) );
QCOMPARE( mLayerTopo->getFeature( 2 ).geometry().asWkt(), QStringLiteral( "Polygon ((7 0, 8 0, 8 4, 7 4, 7 0))" ) );
QgsProject::instance()->setTopologicalEditing( topologicalEditing );
QgsProject::instance()->setAvoidIntersectionsMode( mode );
@ -417,13 +454,13 @@ void TestQgsMapToolReshape::testWithTracing()
utils.mouseClick( 7, 4, Qt::LeftButton, Qt::KeyboardModifiers(), true );
utils.mouseClick( 8, 5, Qt::RightButton );
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), "Polygon ((0 0, 4 0, 4 4, 0 4))" );
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), "Polygon ((0 0, 4 0, 4 4, 0 4, 0 0))" );
QCOMPARE( mLayerTopo->getFeature( 2 ).geometry().asWkt(), "Polygon ((7 0, 8 0, 8 4, 7 4, 4 4, 4 0, 7 0))" );
mLayerTopo->undoStack()->undo();
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 0, 4 0, 4 4, 0 4))" ) );
QCOMPARE( mLayerTopo->getFeature( 2 ).geometry().asWkt(), QStringLiteral( "Polygon ((7 0, 8 0, 8 4, 7 4))" ) );
QCOMPARE( mLayerTopo->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "Polygon ((0 0, 4 0, 4 4, 0 4, 0 0))" ) );
QCOMPARE( mLayerTopo->getFeature( 2 ).geometry().asWkt(), QStringLiteral( "Polygon ((7 0, 8 0, 8 4, 7 4, 7 0))" ) );
QgsProject::instance()->setTopologicalEditing( topologicalEditing );
}