mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-25 00:58:06 -05:00
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:
commit
749aa705a9
@ -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 );
|
||||
|
@ -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
|
||||
|
||||
};
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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 )
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 )
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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 };
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user