diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 64f2cfd10355..d455adbfabdf 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -229,6 +229,7 @@ set(QGIS_APP_SRCS locator/qgslocatoroptionswidget.cpp maptools/qgsappmaptools.cpp + maptools/qgsavoidintersectionsoperation.cpp maptools/qgsmaptoolsdigitizingtechniquemanager.cpp maptools/qgsmaptoolshapecircleabstract.cpp diff --git a/src/app/maptools/qgsavoidintersectionsoperation.cpp b/src/app/maptools/qgsavoidintersectionsoperation.cpp new file mode 100644 index 000000000000..b963ebf5241e --- /dev/null +++ b/src/app/maptools/qgsavoidintersectionsoperation.cpp @@ -0,0 +1,136 @@ +/*************************************************************************** + qgsavoidintersectionsoperation.cpp + --------------------- + begin : 2023/09/20 + copyright : (C) 2023 by Julien Cabieces + email : julien dot cabieces 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 + +#include "qgis.h" +#include "qgisapp.h" +#include "qgsavoidintersectionsoperation.h" +#include "qgsmessagebar.h" +#include "qgsmessagebaritem.h" +#include "qgsproject.h" +#include "qgsvectorlayer.h" + +Qgis::GeometryOperationResult QgsAvoidIntersectionsOperation::apply( QgsVectorLayer *layer, QgsFeatureId fid, QgsGeometry &geom, + const QHash > &ignoreFeatures ) +{ + QList avoidIntersectionsLayers; + switch ( QgsProject::instance()->avoidIntersectionsMode() ) + { + case Qgis::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer: + avoidIntersectionsLayers.append( layer ); + break; + case Qgis::AvoidIntersectionsMode::AvoidIntersectionsLayers: + avoidIntersectionsLayers = QgsProject::instance()->avoidIntersectionsLayers(); + break; + case Qgis::AvoidIntersectionsMode::AllowIntersections: + break; + } + + if ( avoidIntersectionsLayers.isEmpty() ) + return Qgis::GeometryOperationResult::NothingHappened; + + Qgis::GeometryOperationResult avoidIntersectionsReturn = geom.avoidIntersectionsV2( avoidIntersectionsLayers, ignoreFeatures ); + bool geomHasChanged = false; + switch ( avoidIntersectionsReturn ) + { + case Qgis::GeometryOperationResult::GeometryTypeHasChanged: // Geometry type was changed, let's try our best to make it compatible with the target layer + { + geomHasChanged = true; + const QVector newGeoms = geom.coerceToType( layer->wkbType() ); + if ( newGeoms.count() == 1 ) + { + geom = newGeoms.at( 0 ); + avoidIntersectionsReturn = Qgis::GeometryOperationResult::Success; + } + else // handle multi geometries + { + QgsFeatureList removedFeatures; + double largest = 0; + QgsFeature originalFeature = layer->getFeature( fid ); + int largestPartIndex = -1; + for ( int i = 0; i < newGeoms.size(); ++i ) + { + QgsGeometry currentPart = newGeoms.at( i ); + const double currentPartSize = layer->geometryType() == Qgis::GeometryType::Polygon ? currentPart.area() : currentPart.length(); + + QgsFeature partFeature( layer->fields() ); + partFeature.setAttributes( originalFeature.attributes() ); + partFeature.setGeometry( currentPart ); + removedFeatures.append( partFeature ); + if ( currentPartSize > largest ) + { + geom = currentPart; + largestPartIndex = i; + largest = currentPartSize; + } + } + removedFeatures.removeAt( largestPartIndex ); + QgsMessageBarItem *messageBarItem = QgisApp::instance()->messageBar()->createMessage( tr( "Avoid overlaps" ), tr( "Only the largest of multiple created geometries was preserved." ) ); + QPushButton *restoreButton = new QPushButton( tr( "Restore others" ) ); + QPointer layerPtr( layer ); + connect( restoreButton, &QPushButton::clicked, restoreButton, [ = ] + { + if ( !layerPtr ) + return; + layerPtr->beginEditCommand( tr( "Restored geometry parts removed by avoid overlaps" ) ); + QgsFeatureList unconstFeatures = removedFeatures; + QgisApp::instance()->pasteFeatures( layerPtr.data(), 0, removedFeatures.size(), unconstFeatures ); + } ); + messageBarItem->layout()->addWidget( restoreButton ); + QgisApp::instance()->messageBar()->pushWidget( messageBarItem, Qgis::MessageLevel::Info, 15 ); + } + break; + } + + case Qgis::GeometryOperationResult::InvalidBaseGeometry: + geomHasChanged = true; + emit messageEmitted( tr( "At least one geometry intersected is invalid. These geometries must be manually repaired." ), Qgis::MessageLevel::Warning ); + break; + + case Qgis::GeometryOperationResult::Success: + geomHasChanged = true; + break; + + case Qgis::GeometryOperationResult::NothingHappened: + case Qgis::GeometryOperationResult::InvalidInputGeometryType: + case Qgis::GeometryOperationResult::SelectionIsEmpty: + case Qgis::GeometryOperationResult::SelectionIsGreaterThanOne: + case Qgis::GeometryOperationResult::GeometryEngineError: + case Qgis::GeometryOperationResult::LayerNotEditable: + case Qgis::GeometryOperationResult::AddPartSelectedGeometryNotFound: + case Qgis::GeometryOperationResult::AddPartNotMultiGeometry: + case Qgis::GeometryOperationResult::AddRingNotClosed: + case Qgis::GeometryOperationResult::AddRingNotValid: + case Qgis::GeometryOperationResult::AddRingCrossesExistingRings: + case Qgis::GeometryOperationResult::AddRingNotInExistingFeature: + case Qgis::GeometryOperationResult::SplitCannotSplitPoint: + break; + } + + if ( QgsProject::instance()->topologicalEditing() && geomHasChanged ) + { + // then add the new points generated by avoidIntersections + QgsGeometry oldGeom = layer->getGeometry( fid ).convertToType( Qgis::GeometryType::Point, true ); + QgsGeometry difference = geom.convertToType( Qgis::GeometryType::Point, true ).difference( oldGeom ); + for ( auto it = difference.vertices_begin(); it != difference.vertices_end(); ++it ) + for ( QgsVectorLayer *targetLayer : avoidIntersectionsLayers ) + { + targetLayer->addTopologicalPoints( *it ); + } + } + + return avoidIntersectionsReturn; +} diff --git a/src/app/maptools/qgsavoidintersectionsoperation.h b/src/app/maptools/qgsavoidintersectionsoperation.h new file mode 100644 index 000000000000..ba199b271c7c --- /dev/null +++ b/src/app/maptools/qgsavoidintersectionsoperation.h @@ -0,0 +1,59 @@ +/*************************************************************************** + qgsavoidintersectionsoperation.h + --------------------- + begin : 2023/09/20 + copyright : (C) 2023 by Julien Cabieces + email : julien dot cabieces 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. * + * * + ***************************************************************************/ + +#ifndef QGSAVOIDINTERSECTIONSOPERATION_H +#define QGSAVOIDINTERSECTIONSOPERATION_H + +#include + +#include "qgis.h" +#include "qgis_gui.h" +#include "qgsfeatureid.h" +#include "qgspoint.h" + +class QgsVectorLayer; +class QgsGeometry; +class QgsMessageBar; + +/** + * \ingroup gui + * \brief Helper class to apply the avoid intersection operation on a geometry and treat resulting issues. + * \since QGIS 3.34 +*/ +class GUI_EXPORT QgsAvoidIntersectionsOperation : public QObject +{ + Q_OBJECT + + public: + + /** + * Contructor + */ + QgsAvoidIntersectionsOperation() = default; + + // TODO cartouche + // TODO don't return int, return enum + Qgis::GeometryOperationResult apply( QgsVectorLayer *layer, QgsFeatureId fid, QgsGeometry &geom, + const QHash > &ignoreFeatures = ( QHash >() ) ); + + signals: + + /** + * emmit a \a message with corresponding \a level + */ + void messageEmitted( const QString &message, Qgis::MessageLevel = Qgis::MessageLevel::Info ); +}; + +#endif diff --git a/src/app/qgsmaptoolreshape.cpp b/src/app/qgsmaptoolreshape.cpp index e3d7290d822f..6ead972945f6 100644 --- a/src/app/qgsmaptoolreshape.cpp +++ b/src/app/qgsmaptoolreshape.cpp @@ -13,6 +13,7 @@ * * ***************************************************************************/ +#include "qgsavoidintersectionsoperation.h" #include "qgsmaptoolreshape.h" #include "qgsfeatureiterator.h" #include "qgsgeometry.h" @@ -157,6 +158,8 @@ void QgsMapToolReshape::reshape( QgsVectorLayer *vlayer ) Qgis::GeometryOperationResult reshapeReturn = Qgis::GeometryOperationResult::Success; bool reshapeDone = false; const bool isBinding = isBindingLine( vlayer, bbox ); + QgsAvoidIntersectionsOperation avoidIntersections; + connect( &avoidIntersections, &QgsAvoidIntersectionsOperation::messageEmitted, this, &QgsMapTool::messageEmitted ); vlayer->beginEditCommand( tr( "Reshape" ) ); while ( fit.nextFeature( f ) ) @@ -182,41 +185,27 @@ void QgsMapToolReshape::reshape( QgsVectorLayer *vlayer ) QHash > ignoreFeatures; ignoreFeatures.insert( vlayer, vlayer->allFeatureIds() ); - QList avoidIntersectionsLayers; - switch ( QgsProject::instance()->avoidIntersectionsMode() ) - { - case Qgis::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer: - avoidIntersectionsLayers.append( vlayer ); - break; - case Qgis::AvoidIntersectionsMode::AvoidIntersectionsLayers: - avoidIntersectionsLayers = QgsProject::instance()->avoidIntersectionsLayers(); - break; - case Qgis::AvoidIntersectionsMode::AllowIntersections: - break; - } - Qgis::GeometryOperationResult res = Qgis::GeometryOperationResult::NothingHappened; - if ( avoidIntersectionsLayers.size() > 0 ) - { - res = geom.avoidIntersectionsV2( QgsProject::instance()->avoidIntersectionsLayers(), ignoreFeatures ); - if ( res == Qgis::GeometryOperationResult::InvalidInputGeometryType ) - { - emit messageEmitted( tr( "An error was reported during intersection removal" ), Qgis::MessageLevel::Critical ); - vlayer->destroyEditCommand(); - stopCapturing(); - return; - } - } - - if ( geom.isEmpty() ) //intersection removal might have removed the whole geometry - { - emit messageEmitted( tr( "The feature cannot be reshaped because the resulting geometry is empty" ), Qgis::MessageLevel::Critical ); - vlayer->destroyEditCommand(); - return; - } - if ( res == Qgis::GeometryOperationResult::InvalidBaseGeometry ) - { - emit messageEmitted( tr( "At least one geometry intersected is invalid. These geometries must be manually repaired." ), Qgis::MessageLevel::Warning ); - } + avoidIntersections.apply( vlayer, f.id(), geom, ignoreFeatures ); + + // TODO what do we do with those messages + // if ( avoidIntersectionsLayers.size() > 0 ) + // { + // res = geom.avoidIntersections( QgsProject::instance()->avoidIntersectionsLayers(), ignoreFeatures ); + // if ( res == 1 ) + // { + // emit messageEmitted( tr( "An error was reported during intersection removal" ), Qgis::MessageLevel::Critical ); + // vlayer->destroyEditCommand(); + // stopCapturing(); + // return; + // } + // } + + // if ( geom.isEmpty() ) //intersection removal might have removed the whole geometry + // { + // emit messageEmitted( tr( "The feature cannot be reshaped because the resulting geometry is empty" ), Qgis::MessageLevel::Critical ); + // vlayer->destroyEditCommand(); + // return; + // } } vlayer->changeGeometry( f.id(), geom ); @@ -227,7 +216,7 @@ void QgsMapToolReshape::reshape( QgsVectorLayer *vlayer ) if ( reshapeDone ) { - // Add topological points + // Add topological points due to snapping if ( QgsProject::instance()->topologicalEditing() ) { const QList sm = snappingMatches(); @@ -240,6 +229,7 @@ void QgsMapToolReshape::reshape( QgsVectorLayer *vlayer ) } } } + vlayer->endEditCommand(); } else diff --git a/src/app/vertextool/qgsvertextool.cpp b/src/app/vertextool/qgsvertextool.cpp index c9f1645246bf..a27c4a52ee8e 100644 --- a/src/app/vertextool/qgsvertextool.cpp +++ b/src/app/vertextool/qgsvertextool.cpp @@ -12,9 +12,11 @@ * (at your option) any later version. * * * ***************************************************************************/ +#include "qgsmaptool.h" #include "qgsmessagelog.h" #include "qgsvertextool.h" +#include "qgsavoidintersectionsoperation.h" #include "qgsadvanceddigitizingdockwidget.h" #include "qgscurve.h" #include "qgslinestring.h" @@ -2262,13 +2264,6 @@ void QgsVertexTool::moveVertex( const QgsPointXY &mapPoint, const QgsPointLocato if ( mapPointMatch->layer() ) targetLayers << mapPointMatch->layer(); - // add topological points on layer part of the avoid intersection - if ( QgsProject::instance()->avoidIntersectionsMode() == Qgis::AvoidIntersectionsMode::AvoidIntersectionsLayers ) - { - const QList layers = QgsProject::instance()->avoidIntersectionsLayers(); - targetLayers.unite( QSet( layers.constBegin(), layers.constEnd() ) ); - } - for ( auto itLayerEdits = edits.begin(); itLayerEdits != edits.end(); ++itLayerEdits ) { for ( QgsVectorLayer *targetLayer : targetLayers ) @@ -2405,98 +2400,17 @@ void QgsVertexTool::applyEditsToLayers( QgsVertexTool::VertexEdits &edits ) for ( auto itLayerEdits = edits.begin() ; itLayerEdits != edits.end(); ++itLayerEdits ) { QgsVectorLayer *layer = itLayerEdits.key(); - QList avoidIntersectionsLayers; - switch ( QgsProject::instance()->avoidIntersectionsMode() ) - { - case Qgis::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer: - avoidIntersectionsLayers.append( layer ); - break; - case Qgis::AvoidIntersectionsMode::AvoidIntersectionsLayers: - avoidIntersectionsLayers = QgsProject::instance()->avoidIntersectionsLayers(); - break; - case Qgis::AvoidIntersectionsMode::AllowIntersections: - break; - } layer->beginEditCommand( tr( "Moved vertex" ) ); - + QgsAvoidIntersectionsOperation avoidIntersections; + connect( &avoidIntersections, &QgsAvoidIntersectionsOperation::messageEmitted, this, &QgsMapTool::messageEmitted ); for ( auto itFeatEdit = itLayerEdits->begin() ; itFeatEdit != itLayerEdits->end(); ++itFeatEdit ) { - QgsGeometry featGeom = itFeatEdit.value().geom; - if ( avoidIntersectionsLayers.size() > 0 ) - { - Qgis::GeometryOperationResult avoidIntersectionsReturn = featGeom.avoidIntersectionsV2( avoidIntersectionsLayers, ignoreFeatures ); - - switch ( avoidIntersectionsReturn ) - { - case Qgis::GeometryOperationResult::GeometryTypeHasChanged: // Geometry type was changed, let's try our best to make it compatible with the target layer - { - const QVector newGeoms = featGeom.coerceToType( layer->wkbType() ); - if ( newGeoms.count() == 1 ) - { - featGeom = newGeoms.at( 0 ); - } - else // handle multi geometries - { - QgsFeatureList removedFeatures; - double largest = 0; - QgsFeature originalFeature = layer->getFeature( itFeatEdit.key() ); - int largestPartIndex = -1; - for ( int i = 0; i < newGeoms.size(); ++i ) - { - QgsGeometry currentPart = newGeoms.at( i ); - const double currentPartSize = layer->geometryType() == Qgis::GeometryType::Polygon ? currentPart.area() : currentPart.length(); - - QgsFeature partFeature( layer->fields() ); - partFeature.setAttributes( originalFeature.attributes() ); - partFeature.setGeometry( currentPart ); - removedFeatures.append( partFeature ); - if ( currentPartSize > largest ) - { - featGeom = currentPart; - largestPartIndex = i; - largest = currentPartSize; - } - } - removedFeatures.removeAt( largestPartIndex ); - QgsMessageBarItem *messageBarItem = QgisApp::instance()->messageBar()->createMessage( tr( "Avoid overlaps" ), tr( "Only the largest of multiple created geometries was preserved." ) ); - QPushButton *restoreButton = new QPushButton( tr( "Restore others" ) ); - QPointer layerPtr( layer ); - connect( restoreButton, &QPushButton::clicked, restoreButton, [ = ] - { - if ( !layerPtr ) - return; - layerPtr->beginEditCommand( tr( "Restored geometry parts removed by avoid overlaps" ) ); - QgsFeatureList unconstFeatures = removedFeatures; - QgisApp::instance()->pasteFeatures( layerPtr.data(), 0, removedFeatures.size(), unconstFeatures ); - } ); - messageBarItem->layout()->addWidget( restoreButton ); - QgisApp::instance()->messageBar()->pushWidget( messageBarItem, Qgis::MessageLevel::Info, 15 ); - } - break; - } - - case Qgis::GeometryOperationResult::InvalidBaseGeometry: - emit messageEmitted( tr( "At least one geometry intersected is invalid. These geometries must be manually repaired." ), Qgis::MessageLevel::Warning ); - break; - - default: - break; - } - - // if the geometry has been changed - if ( avoidIntersectionsReturn != Qgis::GeometryOperationResult::InvalidInputGeometryType && avoidIntersectionsReturn != Qgis::GeometryOperationResult::NothingHappened ) - { - // then add the new points generated by avoidIntersections - QgsGeometry oldGeom = layer->getGeometry( itFeatEdit.key() ).convertToType( Qgis::GeometryType::Point, true ); - QgsGeometry difference = featGeom.convertToType( Qgis::GeometryType::Point, true ).difference( oldGeom ); - itFeatEdit->newPoints.clear(); - for ( auto it = difference.vertices_begin(); it != difference.vertices_end(); ++it ) - itFeatEdit->newPoints << *it; - - itFeatEdit->geom = featGeom; - } - } + const Qgis::GeometryOperationResult res = avoidIntersections.apply( layer, itFeatEdit.key(), itFeatEdit->geom, ignoreFeatures ); + // TODO add a static method in avoidintersection that return true if avoidintersection happened or not according to return code + // avoid intersection happened, no need to add initial new points + if ( res != Qgis::GeometryOperationResult::InvalidInputGeometryType && res != Qgis::GeometryOperationResult::NothingHappened ) + itFeatEdit->newPoints.clear(); layer->changeGeometry( itFeatEdit.key(), itFeatEdit->geom ); } diff --git a/tests/src/app/testqgsvertextool.cpp b/tests/src/app/testqgsvertextool.cpp index d57d62c863ae..6d41ee9d5ae5 100644 --- a/tests/src/app/testqgsvertextool.cpp +++ b/tests/src/app/testqgsvertextool.cpp @@ -1311,8 +1311,7 @@ void TestQgsVertexTool::testAvoidIntersections() QCOMPARE( mLayerPolygon->getFeature( mFidPolygonF_topo1 ).geometry().asWkt( 1 ), "Polygon ((0 20, 10.7 15.7, 10 15, 10.7 14.3, 0 10, 0 20))" ); QCOMPARE( mLayerPolygon->getFeature( mFidPolygonF_topo2 ).geometry().asWkt( 1 ), "Polygon ((10 15, 10.7 14.3, 15 10, 15 20, 10.7 15.7, 10 15))" ); - mLayerPolygon->undoStack()->undo(); // undo topological points - mLayerPolygon->undoStack()->undo(); // undo move + mLayerPolygon->undoStack()->undo(); // undo move and topological points mLayerPolygon->undoStack()->undo(); // delete feature polygonF_topo2 mLayerPolygon->undoStack()->undo(); // delete feature polygonF_topo1 QCOMPARE( mLayerPolygon->featureCount(), ( long )1 );