Merge pull request #53965 from nirvn/copy_paste_duplicate

Insure that copied features within the same layer respect project relationship(s) strength
This commit is contained in:
Mathieu Pellerin 2023-07-28 23:30:47 +07:00 committed by GitHub
commit 749aa705a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 318 additions and 152 deletions

View File

@ -41,7 +41,7 @@ This method calls the addFeature method of the backend :py:class:`QgsVectorLayer
virtual bool saveEdits( QgsVectorLayer *layer ) const;
virtual bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx = 0, double dy = 0, QString *errorMsg = 0, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = 0 ) const;
virtual bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx = 0, double dy = 0, QString *errorMsg = 0, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = 0, QString *childrenInfoMsg = 0 ) const;
void setVectorLayerTools( const QgsVectorLayerTools *tools );

View File

@ -78,7 +78,7 @@ Should be called, when the features should be committed but the editing session
%End
virtual bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request /In,Out/, double dx = 0, double dy = 0, QString *errorMsg /Out/ = 0, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = 0 ) const;
virtual bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request /In,Out/, double dx = 0, double dy = 0, QString *errorMsg /Out/ = 0, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = 0, QString *childrenInfoMsg = 0 ) const;
%Docstring
Copy and move features with defined translation.
@ -89,6 +89,7 @@ Copy and move features with defined translation.
:param topologicalEditing: If ``True``, the function will perform topological
editing of the vertices of ``layer`` on ``layer`` and ``topologicalLayer``
:param topologicalLayer: The layer where vertices from the moved features of ``layer`` will be added
:param childrenInfoMsg: If given, it will contain messages related to the creation of child features
:return: - ``True`` if all features could be copied.
- errorMsg: If given, it will contain the error message
@ -111,6 +112,20 @@ This flag will override the layer and general settings regarding the automatic
opening of the attribute form dialog when digitizing is completed.
.. versionadded:: 3.14
%End
void setProject( QgsProject *project );
%Docstring
Sets the project to be used by operations when needed.
.. versionadded:: 3.34
%End
QgsProject *project() const;
%Docstring
Returns the project to be used by operations when needed.
.. versionadded:: 3.34
%End
};

View File

@ -1510,6 +1510,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipBadLayers
connect( QgsApplication::messageLog(), static_cast < void ( QgsMessageLog::* )( bool ) >( &QgsMessageLog::messageReceived ), this, &QgisApp::toggleLogMessageIcon );
connect( mMessageButton, &QAbstractButton::toggled, this, &QgisApp::toggleLogMessageIcon );
mVectorLayerTools = new QgsGuiVectorLayerTools();
mVectorLayerTools->setProject( QgsProject::instance() );
// Init the editor widget types
QgsGui::editorWidgetRegistry()->initEditors( mMapCanvas, mInfoBar );
@ -10127,148 +10128,186 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer )
return;
}
const bool duplicateFeature = clipboard()->layer() == pasteVectorLayer;
pasteVectorLayer->beginEditCommand( tr( "Features pasted" ) );
QgsFeatureList features = clipboard()->transformedCopyOf( pasteVectorLayer->crs(), pasteVectorLayer->fields() );
int nTotalFeatures = features.count();
QgsExpressionContext context = pasteVectorLayer->createExpressionContext();
QgsFeatureList compatibleFeatures( QgsVectorLayerUtils::makeFeaturesCompatible( features, pasteVectorLayer, QgsFeatureSink::RegeneratePrimaryKey ) );
QgsVectorLayerUtils::QgsFeaturesDataList newFeaturesDataList;
newFeaturesDataList.reserve( compatibleFeatures.size() );
QgsFeatureList pastedFeatures;
// Count collapsed geometries
int invalidGeometriesCount = 0;
for ( const auto &feature : std::as_const( compatibleFeatures ) )
if ( duplicateFeature )
{
QgsGeometry geom = feature.geometry();
if ( !( geom.isEmpty() || geom.isNull( ) ) )
{
// avoid intersection if enabled in digitize settings
QList<QgsVectorLayer *> avoidIntersectionsLayers;
switch ( QgsProject::instance()->avoidIntersectionsMode() )
{
case Qgis::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer:
avoidIntersectionsLayers.append( pasteVectorLayer );
break;
case Qgis::AvoidIntersectionsMode::AvoidIntersectionsLayers:
avoidIntersectionsLayers = QgsProject::instance()->avoidIntersectionsLayers();
break;
case Qgis::AvoidIntersectionsMode::AllowIntersections:
break;
}
if ( avoidIntersectionsLayers.size() > 0 )
{
geom.avoidIntersections( avoidIntersectionsLayers );
}
// count collapsed geometries
if ( geom.isEmpty() || geom.isNull( ) )
invalidGeometriesCount++;
}
QgsAttributeMap attrMap;
for ( int i = 0; i < feature.attributes().count(); i++ )
{
attrMap[i] = feature.attribute( i );
}
newFeaturesDataList << QgsVectorLayerUtils::QgsFeatureData( geom, attrMap );
}
// now create new feature using pasted feature as a template. This automatically handles default
// values and field constraints
QgsFeatureList newFeatures {QgsVectorLayerUtils::createFeatures( pasteVectorLayer, newFeaturesDataList, &context )};
// check constraints
bool hasStrongConstraints = false;
for ( const QgsField &field : pasteVectorLayer->fields() )
{
if ( ( field.constraints().constraints() & QgsFieldConstraints::ConstraintUnique && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintUnique ) & QgsFieldConstraints::ConstraintStrengthHard )
|| ( field.constraints().constraints() & QgsFieldConstraints::ConstraintNotNull && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintNotNull ) & QgsFieldConstraints::ConstraintStrengthHard )
|| ( field.constraints().constraints() & QgsFieldConstraints::ConstraintExpression && !field.constraints().constraintExpression().isEmpty() && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintExpression ) & QgsFieldConstraints::ConstraintStrengthHard )
)
{
hasStrongConstraints = true;
break;
}
}
if ( hasStrongConstraints )
{
QgsFeatureList validFeatures = newFeatures;
QgsFeatureList invalidFeatures;
QMutableListIterator<QgsFeature> it( validFeatures );
while ( it.hasNext() )
{
QgsFeature &f = it.next();
for ( int idx = 0; idx < pasteVectorLayer->fields().count(); ++idx )
{
QStringList errors;
if ( !QgsVectorLayerUtils::validateAttribute( pasteVectorLayer, f, idx, errors, QgsFieldConstraints::ConstraintStrengthHard, QgsFieldConstraints::ConstraintOriginNotSet ) )
{
invalidFeatures << f;
it.remove();
break;
}
}
}
if ( !invalidFeatures.isEmpty() )
{
newFeatures.clear();
QgsAttributeEditorContext context( createAttributeEditorContext() );
context.setAllowCustomUi( false );
context.setFormMode( QgsAttributeEditorContext::StandaloneDialog );
QgsFixAttributeDialog *dialog = new QgsFixAttributeDialog( pasteVectorLayer, invalidFeatures, this, context );
connect( dialog, &QgsFixAttributeDialog::finished, this, [ = ]( int feedback )
{
QgsFeatureList features = newFeatures;
switch ( feedback )
{
case QgsFixAttributeDialog::PasteValid:
//paste valid and fixed, vanish unfixed
features << validFeatures << dialog->fixedFeatures();
break;
case QgsFixAttributeDialog::PasteAll:
//paste all, even unfixed
features << validFeatures << dialog->fixedFeatures() << dialog->unfixedFeatures();
break;
}
pasteFeatures( pasteVectorLayer, invalidGeometriesCount, nTotalFeatures, features );
dialog->deleteLater();
} );
dialog->show();
return;
}
}
pasteFeatures( pasteVectorLayer, invalidGeometriesCount, nTotalFeatures, newFeatures );
}
void QgisApp::pasteFeatures( QgsVectorLayer *pasteVectorLayer, int invalidGeometriesCount, int nTotalFeatures, QgsFeatureList &features )
{
int nCopiedFeatures = features.count();
if ( pasteVectorLayer->addFeatures( features ) )
{
QgsFeatureIds newIds;
newIds.reserve( features.size() );
for ( const QgsFeature &f : std::as_const( features ) )
{
newIds << f.id();
}
pasteVectorLayer->selectByIds( newIds );
pastedFeatures = features;
}
else
{
nCopiedFeatures = 0;
QgsExpressionContext context = pasteVectorLayer->createExpressionContext();
QgsFeatureList compatibleFeatures( QgsVectorLayerUtils::makeFeaturesCompatible( features, pasteVectorLayer, QgsFeatureSink::RegeneratePrimaryKey ) );
QgsVectorLayerUtils::QgsFeaturesDataList newFeaturesDataList;
newFeaturesDataList.reserve( compatibleFeatures.size() );
for ( const auto &feature : std::as_const( compatibleFeatures ) )
{
QgsGeometry geom = feature.geometry();
if ( !( geom.isEmpty() || geom.isNull( ) ) )
{
// avoid intersection if enabled in digitize settings
QList<QgsVectorLayer *> avoidIntersectionsLayers;
switch ( QgsProject::instance()->avoidIntersectionsMode() )
{
case Qgis::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer:
avoidIntersectionsLayers.append( pasteVectorLayer );
break;
case Qgis::AvoidIntersectionsMode::AvoidIntersectionsLayers:
avoidIntersectionsLayers = QgsProject::instance()->avoidIntersectionsLayers();
break;
case Qgis::AvoidIntersectionsMode::AllowIntersections:
break;
}
if ( avoidIntersectionsLayers.size() > 0 )
{
geom.avoidIntersections( avoidIntersectionsLayers );
}
// count collapsed geometries
if ( geom.isEmpty() || geom.isNull( ) )
invalidGeometriesCount++;
}
QgsAttributeMap attrMap;
for ( int i = 0; i < feature.attributes().count(); i++ )
{
attrMap[i] = feature.attribute( i );
}
newFeaturesDataList << QgsVectorLayerUtils::QgsFeatureData( geom, attrMap );
}
// now create new feature using pasted feature as a template. This automatically handles default
// values and field constraints
pastedFeatures = QgsVectorLayerUtils::createFeatures( pasteVectorLayer, newFeaturesDataList, &context );
// check constraints
bool hasStrongConstraints = false;
for ( const QgsField &field : pasteVectorLayer->fields() )
{
if ( ( field.constraints().constraints() & QgsFieldConstraints::ConstraintUnique && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintUnique ) & QgsFieldConstraints::ConstraintStrengthHard )
|| ( field.constraints().constraints() & QgsFieldConstraints::ConstraintNotNull && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintNotNull ) & QgsFieldConstraints::ConstraintStrengthHard )
|| ( field.constraints().constraints() & QgsFieldConstraints::ConstraintExpression && !field.constraints().constraintExpression().isEmpty() && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintExpression ) & QgsFieldConstraints::ConstraintStrengthHard )
)
{
hasStrongConstraints = true;
break;
}
}
if ( hasStrongConstraints )
{
QgsFeatureList validFeatures = pastedFeatures;
QgsFeatureList invalidFeatures;
QMutableListIterator<QgsFeature> it( validFeatures );
while ( it.hasNext() )
{
QgsFeature &f = it.next();
for ( int idx = 0; idx < pasteVectorLayer->fields().count(); ++idx )
{
QStringList errors;
if ( !QgsVectorLayerUtils::validateAttribute( pasteVectorLayer, f, idx, errors, QgsFieldConstraints::ConstraintStrengthHard, QgsFieldConstraints::ConstraintOriginNotSet ) )
{
invalidFeatures << f;
it.remove();
break;
}
}
}
if ( !invalidFeatures.isEmpty() )
{
pastedFeatures.clear();
QgsAttributeEditorContext context( createAttributeEditorContext() );
context.setAllowCustomUi( false );
context.setFormMode( QgsAttributeEditorContext::StandaloneDialog );
QgsFixAttributeDialog *dialog = new QgsFixAttributeDialog( pasteVectorLayer, invalidFeatures, this, context );
connect( dialog, &QgsFixAttributeDialog::finished, this, [ = ]( int feedback )
{
QgsFeatureList features = pastedFeatures;
switch ( feedback )
{
case QgsFixAttributeDialog::PasteValid:
//paste valid and fixed, vanish unfixed
features << validFeatures << dialog->fixedFeatures();
break;
case QgsFixAttributeDialog::PasteAll:
//paste all, even unfixed
features << validFeatures << dialog->fixedFeatures() << dialog->unfixedFeatures();
break;
}
pasteFeatures( pasteVectorLayer, invalidGeometriesCount, nTotalFeatures, features );
dialog->deleteLater();
} );
dialog->show();
return;
}
}
}
pasteFeatures( pasteVectorLayer, invalidGeometriesCount, nTotalFeatures, pastedFeatures, duplicateFeature );
}
void QgisApp::pasteFeatures( QgsVectorLayer *pasteVectorLayer, int invalidGeometriesCount, int nTotalFeatures, QgsFeatureList &features, bool duplicateFeature )
{
int nCopiedFeatures = features.count();
QgsFeatureIds newIds;
newIds.reserve( features.size() );
QString childrenInfo;
if ( duplicateFeature )
{
QgsVectorLayerUtils::QgsDuplicateFeatureContext duplicateFeatureContext;
QMap<QString, int> duplicateFeatureCount;
for ( const QgsFeature &f : std::as_const( features ) )
{
QgsFeature duplicatedFeature = QgsVectorLayerUtils::duplicateFeature( pasteVectorLayer, f, QgsProject::instance(), duplicateFeatureContext );
newIds << duplicatedFeature.id();
const auto duplicateFeatureContextLayers = duplicateFeatureContext.layers();
for ( QgsVectorLayer *chl : duplicateFeatureContextLayers )
{
if ( duplicateFeatureCount.contains( chl->name() ) )
{
duplicateFeatureCount[chl->name()] += duplicateFeatureContext.duplicatedFeatures( chl ).size();
}
else
{
duplicateFeatureCount[chl->name()] = duplicateFeatureContext.duplicatedFeatures( chl ).size();
}
}
}
for ( auto it = duplicateFeatureCount.constBegin(); it != duplicateFeatureCount.constEnd(); ++it )
{
childrenInfo += ( tr( "\n%n children on layer %1 duplicated", nullptr, it.value() ).arg( it.key() ) );
}
}
else
{
if ( pasteVectorLayer->addFeatures( features ) )
{
for ( const QgsFeature &f : std::as_const( features ) )
{
newIds << f.id();
}
}
else
{
nCopiedFeatures = 0;
}
}
pasteVectorLayer->selectByIds( newIds );
pasteVectorLayer->endEditCommand();
pasteVectorLayer->updateExtents();
@ -10280,7 +10319,7 @@ void QgisApp::pasteFeatures( QgsVectorLayer *pasteVectorLayer, int invalidGeomet
}
else if ( nCopiedFeatures == nTotalFeatures )
{
message = tr( "%n feature(s) were pasted.", nullptr, nCopiedFeatures );
message = tr( "%n feature(s) were pasted.%1", nullptr, nCopiedFeatures ).arg( childrenInfo );
}
else
{

View File

@ -2351,8 +2351,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
/**
* Pastes the \a features to the \a pasteVectorLayer and gives feedback to the user
* according to \a invalidGeometryCount and \a nTotalFeatures
* \note Setting the \a duplicateFeature to TRUE will handle the pasting of features as duplicates of pre-existing features
*/
void pasteFeatures( QgsVectorLayer *pasteVectorLayer, int invalidGeometriesCount, int nTotalFeatures, QgsFeatureList &features );
void pasteFeatures( QgsVectorLayer *pasteVectorLayer, int invalidGeometriesCount, int nTotalFeatures, QgsFeatureList &features, bool duplicateFeature = false );
/**
* starts/stops for a vector layer \a vlayer

View File

@ -770,7 +770,6 @@ void QgsAttributeTableDialog::mActionCopySelectedRows_triggered()
if ( mMainView->view() == QgsDualView::AttributeTable )
{
const QList<QgsFeatureId> featureIds = mMainView->tableView()->selectedFeaturesIds();
QgsFeatureStore featureStore;
QgsFields fields = QgsFields( mLayer->fields() );
QStringList fieldNames;
@ -785,8 +784,9 @@ void QgsAttributeTableDialog::mActionCopySelectedRows_triggered()
}
fieldNames << columnConfig.name;
}
featureStore.setFields( fields );
QgsFeatureStore featureStore;
featureStore.setFields( fields );
QgsFeatureIterator it = mLayer->getFeatures( QgsFeatureRequest( qgis::listToSet( featureIds ) )
.setSubsetOfAttributes( fieldNames, mLayer->fields() ) );
QgsFeatureMap featureMap;
@ -803,7 +803,7 @@ void QgsAttributeTableDialog::mActionCopySelectedRows_triggered()
featureStore.setCrs( mLayer->crs() );
QgisApp::instance()->clipboard()->replaceWithCopyOf( featureStore );
QgisApp::instance()->clipboard()->replaceWithCopyOf( featureStore, fields == mLayer->fields() ? mLayer : nullptr );
}
else
{

View File

@ -59,6 +59,7 @@ void QgsClipboard::replaceWithCopyOf( QgsVectorLayer *src )
mFeatureFields = src->fields();
mFeatureClipboard = src->selectedFeatures();
mCRS = src->crs();
mFeatureLayer = src;
QgsDebugMsgLevel( QStringLiteral( "replaced QGIS clipboard." ), 2 );
setSystemClipboard();
@ -99,6 +100,7 @@ void QgsClipboard::replaceWithCopyOf( QgsVectorTileLayer *src )
}
mCRS = src->crs();
mFeatureLayer = src;
QgsDebugMsgLevel( QStringLiteral( "replaced QGIS clipboard." ), 2 );
setSystemClipboard();
@ -106,12 +108,13 @@ void QgsClipboard::replaceWithCopyOf( QgsVectorTileLayer *src )
emit changed();
}
void QgsClipboard::replaceWithCopyOf( QgsFeatureStore &featureStore )
void QgsClipboard::replaceWithCopyOf( QgsFeatureStore &featureStore, QgsVectorLayer *src )
{
QgsDebugMsgLevel( QStringLiteral( "features count = %1" ).arg( featureStore.features().size() ), 2 );
mFeatureFields = featureStore.fields();
mFeatureClipboard = featureStore.features();
mCRS = featureStore.crs();
mFeatureLayer = src;
setSystemClipboard();
mUseSystemClipboard = false;
emit changed();
@ -528,6 +531,14 @@ QgsFields QgsClipboard::fields() const
return retrieveFields();
}
QgsMapLayer *QgsClipboard::layer() const
{
if ( !mUseSystemClipboard )
return mFeatureLayer.data();
else
return nullptr;
}
void QgsClipboard::systemClipboardChanged()
{
if ( mIgnoreNextSystemClipboardChange )

View File

@ -26,6 +26,7 @@
#include "qgsfields.h"
#include "qgsfeature.h"
#include "qgscoordinatereferencesystem.h"
#include "qgsmaplayer.h"
#include "qgis_app.h"
class QgsVectorLayer;
@ -80,7 +81,7 @@ class APP_EXPORT QgsClipboard : public QObject
* Place a copy of features on the internal clipboard,
* destroying the previous contents.
*/
void replaceWithCopyOf( QgsFeatureStore &featureStore );
void replaceWithCopyOf( QgsFeatureStore &featureStore, QgsVectorLayer *src = nullptr );
/**
* Returns a copy of features on the internal clipboard.
@ -140,6 +141,8 @@ class APP_EXPORT QgsClipboard : public QObject
*/
QgsFields fields() const;
QgsMapLayer *layer() const;
private slots:
void systemClipboardChanged();
@ -184,6 +187,7 @@ class APP_EXPORT QgsClipboard : public QObject
QgsFeatureList mFeatureClipboard;
QgsFields mFeatureFields;
QgsCoordinateReferenceSystem mCRS;
QPointer<QgsMapLayer> mFeatureLayer;
//! True if next system clipboard change should be ignored
bool mIgnoreNextSystemClipboardChange = false;

View File

@ -245,13 +245,18 @@ void QgsMapToolMoveFeature::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
case CopyMove:
QgsFeatureRequest request;
request.setFilterFids( mMovedFeatures );
QString *errorMsg = new QString();
if ( !QgisApp::instance()->vectorLayerTools()->copyMoveFeatures( vlayer, request, dx, dy, errorMsg, QgsProject::instance()->topologicalEditing(), mSnapIndicator->match().layer() ) )
QString errorMsg;
QString childrenInfoMsg;
if ( !QgisApp::instance()->vectorLayerTools()->copyMoveFeatures( vlayer, request, dx, dy, &errorMsg, QgsProject::instance()->topologicalEditing(), mSnapIndicator->match().layer(), &childrenInfoMsg ) )
{
emit messageEmitted( *errorMsg, Qgis::MessageLevel::Critical );
emit messageEmitted( errorMsg, Qgis::MessageLevel::Critical );
deleteRubberband();
mSnapIndicator->setMatch( QgsPointLocator::Match() );
}
if ( !childrenInfoMsg.isEmpty() )
{
emit messageEmitted( childrenInfoMsg, Qgis::MessageLevel::Info );
}
break;
}

View File

@ -55,9 +55,9 @@ bool QgsTrackedVectorLayerTools::saveEdits( QgsVectorLayer *layer ) const
return mBackend->saveEdits( layer );
}
bool QgsTrackedVectorLayerTools::copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx, double dy, QString *errorMsg, const bool topologicalEditing, QgsVectorLayer *topologicalLayer ) const
bool QgsTrackedVectorLayerTools::copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx, double dy, QString *errorMsg, const bool topologicalEditing, QgsVectorLayer *topologicalLayer, QString *childrenInfoMsg ) const
{
return mBackend->copyMoveFeatures( layer, request, dx, dy, errorMsg, topologicalEditing, topologicalLayer );
return mBackend->copyMoveFeatures( layer, request, dx, dy, errorMsg, topologicalEditing, topologicalLayer, childrenInfoMsg );
}
void QgsTrackedVectorLayerTools::setVectorLayerTools( const QgsVectorLayerTools *tools )

View File

@ -50,7 +50,7 @@ class CORE_EXPORT QgsTrackedVectorLayerTools : public QgsVectorLayerTools
bool startEditing( QgsVectorLayer *layer ) const override;
bool stopEditing( QgsVectorLayer *layer, bool allowCancel ) const override;
bool saveEdits( QgsVectorLayer *layer ) const override;
bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx = 0, double dy = 0, QString *errorMsg = nullptr, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = nullptr ) const override;
bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx = 0, double dy = 0, QString *errorMsg = nullptr, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = nullptr, QString *childrenInfoMsg = nullptr ) const override;
/**
* Set the vector layer tools that will be used to interact with the data

View File

@ -19,13 +19,14 @@
#include "qgsfeaturerequest.h"
#include "qgslogger.h"
#include "qgsvectorlayerutils.h"
#include "qgsproject.h"
QgsVectorLayerTools::QgsVectorLayerTools()
: QObject( nullptr )
{}
bool QgsVectorLayerTools::copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx, double dy, QString *errorMsg, const bool topologicalEditing, QgsVectorLayer *topologicalLayer ) const
bool QgsVectorLayerTools::copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx, double dy, QString *errorMsg, const bool topologicalEditing, QgsVectorLayer *topologicalLayer, QString *childrenInfoMsg ) const
{
bool res = false;
if ( !layer || !layer->isEditable() )
@ -41,12 +42,34 @@ bool QgsVectorLayerTools::copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureReq
int noGeometryCount = 0;
QgsFeatureIds fidList;
QgsVectorLayerUtils::QgsDuplicateFeatureContext duplicateFeatureContext;
QMap<QString, int> duplicateFeatureCount;
while ( fi.nextFeature( f ) )
{
browsedFeatureCount++;
QgsFeature newFeature = QgsVectorLayerUtils::createFeature( layer, f.geometry(), f.attributes().toMap() );
QgsFeature newFeature;
if ( mProject )
{
newFeature = QgsVectorLayerUtils::duplicateFeature( layer, f, mProject, duplicateFeatureContext );
const auto duplicateFeatureContextLayers = duplicateFeatureContext.layers();
for ( QgsVectorLayer *chl : duplicateFeatureContextLayers )
{
if ( duplicateFeatureCount.contains( chl->name() ) )
{
duplicateFeatureCount[chl->name()] += duplicateFeatureContext.duplicatedFeatures( chl ).size();
}
else
{
duplicateFeatureCount[chl->name()] = duplicateFeatureContext.duplicatedFeatures( chl ).size();
}
}
}
else
{
newFeature = QgsVectorLayerUtils::createFeature( layer, f.geometry(), f.attributes().toMap() );
}
// translate
if ( newFeature.hasGeometry() )
@ -82,9 +105,20 @@ bool QgsVectorLayerTools::copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureReq
}
}
QString childrenInfo;
for ( auto it = duplicateFeatureCount.constBegin(); it != duplicateFeatureCount.constEnd(); ++it )
{
childrenInfo += ( tr( "\n%n children on layer %1 duplicated", nullptr, it.value() ).arg( it.key() ) );
}
request = QgsFeatureRequest();
request.setFilterFids( fidList );
if ( childrenInfoMsg && !childrenInfo.isEmpty() )
{
childrenInfoMsg->append( childrenInfo );
}
if ( !couldNotWriteCount && !noGeometryCount )
{
res = true;

View File

@ -25,6 +25,7 @@
class QgsFeatureRequest;
class QgsVectorLayer;
class QgsProject;
/**
* \ingroup core
@ -111,10 +112,11 @@ class CORE_EXPORT QgsVectorLayerTools : public QObject
* \param topologicalEditing If TRUE, the function will perform topological
* editing of the vertices of \a layer on \a layer and \a topologicalLayer
* \param topologicalLayer The layer where vertices from the moved features of \a layer will be added
* \param childrenInfoMsg If given, it will contain messages related to the creation of child features
* \returns TRUE if all features could be copied.
*
*/
virtual bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request SIP_INOUT, double dx = 0, double dy = 0, QString *errorMsg SIP_OUT = nullptr, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = nullptr ) const;
virtual bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request SIP_INOUT, double dx = 0, double dy = 0, QString *errorMsg SIP_OUT = nullptr, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = nullptr, QString *childrenInfoMsg = nullptr ) const;
/**
* Returns force suppress form popup status.
@ -134,8 +136,23 @@ class CORE_EXPORT QgsVectorLayerTools : public QObject
*/
void setForceSuppressFormPopup( bool forceSuppressFormPopup );
/**
* Sets the project to be used by operations when needed.
*
* \since QGIS 3.34
*/
void setProject( QgsProject *project ) { mProject = project; }
/**
* Returns the project to be used by operations when needed.
*
* \since QGIS 3.34
*/
QgsProject *project() const { return mProject; }
private:
QgsProject *mProject = nullptr;
bool mForceSuppressFormPopup { false };

View File

@ -13,10 +13,15 @@ __copyright__ = 'Copyright 2015, The QGIS Project'
import os
from qgis.core import (
QgsFeature,
QgsFeatureRequest,
QgsPoint,
QgsProject,
QgsRelation,
QgsRelationManager,
QgsVectorLayer,
QgsVectorLayerTools,
Qgis,
)
import unittest
from qgis.testing import start_app, QgisTestCase
@ -54,12 +59,38 @@ class TestQgsVectorLayerTools(QgisTestCase):
cls.dbconn = 'service=\'qgis_test\''
if 'QGIS_PGTEST_DB' in os.environ:
cls.dbconn = os.environ['QGIS_PGTEST_DB']
# Create test layer
# Create test layers
cls.vl = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."someData" (geom) sql=', 'layer', 'postgres')
QgsProject.instance().addMapLayer(cls.vl)
cls.vl2 = QgsVectorLayer('Point?crs=EPSG:4326&field=id:integer(10,0)', 'points', 'memory')
f = QgsFeature(cls.vl2.fields())
f.setGeometry(QgsPoint(1, 1))
f.setAttributes([1])
cls.vl2.startEditing()
cls.vl2.addFeature(f)
cls.vl2.commitChanges()
cls.vl3 = QgsVectorLayer('NoGeometry?crs=EPSG:4326&field=point_id:integer(10,0)', 'details', 'memory')
f = QgsFeature(cls.vl3.fields())
f.setAttributes([1])
cls.vl3.startEditing()
cls.vl3.addFeature(f)
cls.vl3.addFeature(f)
cls.vl3.commitChanges()
QgsProject.instance().addMapLayers([cls.vl, cls.vl2, cls.vl3])
relation = QgsRelation()
relation.setName('test')
relation.setReferencedLayer(cls.vl2.id())
relation.setReferencingLayer(cls.vl3.id())
relation.setStrength(Qgis.RelationshipStrength.Composition)
relation.addFieldPair('point_id', 'id')
QgsProject.instance().relationManager().addRelation(relation)
cls.vltools = SubQgsVectorLayerTools()
cls.vltools.setProject(QgsProject.instance())
def testCopyMoveFeature(self):
""" Test copy and move features"""
@ -73,6 +104,15 @@ class TestQgsVectorLayerTools(QgisTestCase):
self.assertAlmostEqual(geom.asPoint().x(), -65.42)
self.assertAlmostEqual(geom.asPoint().y(), 78.5)
def testCopyMoveFeatureRelationship(self):
""" Test copy and move features"""
rqst = QgsFeatureRequest()
rqst.setFilterFid(1)
self.vl2.startEditing()
(ok, rqst, msg) = self.vltools.copyMoveFeatures(self.vl2, rqst, -0.1, 0.2)
self.assertTrue(ok)
self.assertEqual(self.vl3.featureCount(), 4)
if __name__ == '__main__':
unittest.main()