Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 38f49e5

Browse files
committedMar 27, 2023
Move merge selected features logic to QgsVectorLayer
1 parent e6f2275 commit 38f49e5

File tree

4 files changed

+162
-91
lines changed

4 files changed

+162
-91
lines changed
 

‎python/core/auto_generated/vector/qgsvectorlayer.sip.in

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2652,6 +2652,19 @@ Configuration and logic to apply automatically on any edit happening on this lay
26522652
Returns the manager of the stored expressions for this layer.
26532653

26542654
.. versionadded:: 3.10
2655+
%End
2656+
2657+
bool mergeSelectedFeatures( const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage );
2658+
%Docstring
2659+
Merge selected features into a single one.
2660+
2661+
:param mergeAttributes: are the resulting attributes in the merged feature
2662+
:param unionGeometry: is the resulting geometry of the merged feature
2663+
:param errorMessage: will be set to a descriptive error message if any occurs
2664+
2665+
:return: ``True`` if the merge was successful, or ``False`` if the operation failed.
2666+
2667+
.. versionadded:: 3.30
26552668
%End
26562669

26572670
public slots:

‎src/app/qgisapp.cpp

Lines changed: 23 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -9527,9 +9527,8 @@ void QgisApp::mergeSelectedFeatures()
95279527
return;
95289528
}
95299529

9530-
//get selected feature ids (as a QSet<int> )
9531-
const QgsFeatureIds &featureIdSet = vl->selectedFeatureIds();
9532-
if ( featureIdSet.size() < 2 )
9530+
// Check at least two features are selected
9531+
if ( vl->selectedFeatureIds().size() < 2 )
95339532
{
95349533
visibleMessageBar()->pushMessage(
95359534
tr( "Not enough features selected" ),
@@ -9603,104 +9602,37 @@ void QgisApp::mergeSelectedFeatures()
96039602
}
96049603
return;
96059604
}
9606-
}
9607-
9608-
QgsAttributes attrs = d.mergedAttributes();
9609-
QgsAttributeMap newAttributes;
9610-
QString errorMessage;
9611-
QgsFeatureId mergeFeatureId = FID_NULL;
9612-
for ( int i = 0; i < attrs.count(); ++i )
9613-
{
9614-
QVariant val = attrs.at( i );
9615-
bool isDefaultValue = vl->fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
9616-
vl->dataProvider() &&
9617-
vl->dataProvider()->defaultValueClause( vl->fields().fieldOriginIndex( i ) ) == val;
9618-
bool isPrimaryKey = vl->fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
9619-
vl->dataProvider() &&
9620-
vl->dataProvider()->pkAttributeIndexes().contains( vl->fields().fieldOriginIndex( i ) );
9621-
9622-
if ( isPrimaryKey && !isDefaultValue )
9623-
{
9624-
QgsFeatureRequest request;
9625-
request.setFlags( QgsFeatureRequest::Flag::NoGeometry );
9626-
// Handle multi pks
9627-
if ( vl->dataProvider()->pkAttributeIndexes().count() > 1 && vl->dataProvider()->pkAttributeIndexes().count() <= attrs.count() )
9628-
{
9629-
const auto pkIdxList { vl->dataProvider()->pkAttributeIndexes() };
9630-
QStringList conditions;
9631-
QStringList fieldNames;
9632-
for ( const int &pkIdx : std::as_const( pkIdxList ) )
9633-
{
9634-
const QgsField pkField { vl->fields().field( pkIdx ) };
9635-
conditions.push_back( QgsExpression::createFieldEqualityExpression( pkField.name(), attrs.at( pkIdx ), pkField.type( ) ) );
9636-
fieldNames.push_back( pkField.name() );
9637-
}
9638-
request.setSubsetOfAttributes( fieldNames, vl->fields( ) );
9639-
request.setFilterExpression( conditions.join( QStringLiteral( " AND " ) ) );
9640-
}
9641-
else // single pk
9642-
{
9643-
const QgsField pkField { vl->fields().field( i ) };
9644-
request.setSubsetOfAttributes( QStringList() << pkField.name(), vl->fields( ) );
9645-
request.setFilterExpression( QgsExpression::createFieldEqualityExpression( pkField.name(), val, pkField.type( ) ) );
9646-
}
9647-
9648-
QgsFeature f;
9649-
QgsFeatureIterator featureIterator = vl->getFeatures( request );
9650-
if ( featureIterator.nextFeature( f ) )
9605+
else if ( !QgsWkbTypes::isMultiType( vl->wkbType() ) )
9606+
{
9607+
const QgsGeometryCollection *c = qgsgeometry_cast<const QgsGeometryCollection *>( unionGeom.constGet() );
9608+
if ( ( c && c->partCount() > 1 ) || !unionGeom.convertToSingleType() )
96519609
{
9652-
mergeFeatureId = f.id( );
9610+
visibleMessageBar()->pushMessage(
9611+
tr( "Merge failed" ),
9612+
tr( "Resulting geometry type (multipart) is incompatible with layer type (singlepart)." ),
9613+
Qgis::MessageLevel::Critical );
9614+
return;
96539615
}
96549616
}
9655-
9656-
// convert to destination data type
9657-
if ( !isDefaultValue && !vl->fields().at( i ).convertCompatible( val, &errorMessage ) )
9658-
{
9659-
visibleMessageBar()->pushMessage(
9660-
tr( "Invalid result" ),
9661-
tr( "Could not store value '%1' in field of type %2: %3" ).arg( attrs.at( i ).toString(), vl->fields().at( i ).typeName(), errorMessage ),
9662-
Qgis::MessageLevel::Warning );
9663-
}
9664-
newAttributes[ i ] = val;
9665-
}
9666-
9667-
vl->beginEditCommand( tr( "Merged features" ) );
9668-
9669-
QgsFeature mergeFeature;
9670-
if ( mergeFeatureId == FID_NULL )
9671-
{
9672-
// Create new feature
9673-
mergeFeature = QgsVectorLayerUtils::createFeature( vl, unionGeom, newAttributes );
9674-
}
9675-
else
9676-
{
9677-
// Merge into existing feature
9678-
featureIdsAfter.remove( mergeFeatureId );
9679-
}
9680-
9681-
// Delete other features
9682-
QgsFeatureIds::const_iterator feature_it = featureIdsAfter.constBegin();
9683-
for ( ; feature_it != featureIdsAfter.constEnd(); ++feature_it )
9684-
{
9685-
vl->deleteFeature( *feature_it );
96869617
}
96879618

9619+
QString errorMessage;
9620+
bool success = vl->mergeSelectedFeatures( d.mergedAttributes(), unionGeom, errorMessage );
96889621

9689-
if ( mergeFeatureId == FID_NULL )
9622+
if ( !success )
96909623
{
9691-
// Add the new feature
9692-
vl->addFeature( mergeFeature );
9624+
visibleMessageBar()->pushMessage(
9625+
tr( "Merge failed" ),
9626+
errorMessage,
9627+
Qgis::MessageLevel::Critical );
96939628
}
9694-
else
9629+
else if ( success && !errorMessage.isEmpty() )
96959630
{
9696-
// Modify merge feature
9697-
vl->changeGeometry( mergeFeatureId, unionGeom );
9698-
vl->changeAttributeValues( mergeFeatureId, newAttributes );
9631+
visibleMessageBar()->pushMessage(
9632+
tr( "Invalid result" ),
9633+
errorMessage,
9634+
Qgis::MessageLevel::Warning );
96999635
}
9700-
9701-
vl->endEditCommand();
9702-
9703-
vl->triggerRepaint();
97049636
}
97059637

97069638
void QgisApp::vertexTool()

‎src/core/vector/qgsvectorlayer.cpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5863,6 +5863,120 @@ void QgsVectorLayer::setAllowCommit( bool allowCommit )
58635863
emit allowCommitChanged();
58645864
}
58655865

5866+
bool QgsVectorLayer::mergeSelectedFeatures( const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage )
5867+
{
5868+
errorMessage.clear();
5869+
5870+
if ( selectedFeatureIds().size() < 2 )
5871+
{
5872+
errorMessage = tr( "Not enough features selected, the merge tool requires at least two selected features" );
5873+
return false;
5874+
}
5875+
5876+
QgsAttributeMap newAttributes;
5877+
QgsFeatureId mergeFeatureId = FID_NULL;
5878+
for ( int i = 0; i < mergeAttributes.count(); ++i )
5879+
{
5880+
QVariant val = mergeAttributes.at( i );
5881+
5882+
bool isDefaultValue = fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
5883+
dataProvider() &&
5884+
dataProvider()->defaultValueClause( fields().fieldOriginIndex( i ) ) == val;
5885+
5886+
// Check if features can merged into an existing one
5887+
if ( mergeFeatureId == FID_NULL )
5888+
{
5889+
bool isPrimaryKey = fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
5890+
dataProvider() &&
5891+
dataProvider()->pkAttributeIndexes().contains( fields().fieldOriginIndex( i ) );
5892+
5893+
if ( isPrimaryKey && !isDefaultValue )
5894+
{
5895+
QgsFeatureRequest request;
5896+
request.setFlags( QgsFeatureRequest::Flag::NoGeometry );
5897+
// Handle multi pks
5898+
if ( dataProvider()->pkAttributeIndexes().count() > 1 && dataProvider()->pkAttributeIndexes().count() <= mergeAttributes.count() )
5899+
{
5900+
const auto pkIdxList { dataProvider()->pkAttributeIndexes() };
5901+
QStringList conditions;
5902+
QStringList fieldNames;
5903+
for ( const int &pkIdx : std::as_const( pkIdxList ) )
5904+
{
5905+
const QgsField pkField { fields().field( pkIdx ) };
5906+
conditions.push_back( QgsExpression::createFieldEqualityExpression( pkField.name(), mergeAttributes.at( pkIdx ), pkField.type( ) ) );
5907+
fieldNames.push_back( pkField.name() );
5908+
}
5909+
request.setSubsetOfAttributes( fieldNames, fields( ) );
5910+
request.setFilterExpression( conditions.join( QStringLiteral( " AND " ) ) );
5911+
}
5912+
else // single pk
5913+
{
5914+
const QgsField pkField { fields().field( i ) };
5915+
request.setSubsetOfAttributes( QStringList() << pkField.name(), fields( ) );
5916+
request.setFilterExpression( QgsExpression::createFieldEqualityExpression( pkField.name(), val, pkField.type( ) ) );
5917+
}
5918+
5919+
QgsFeature f;
5920+
QgsFeatureIterator featureIterator = getFeatures( request );
5921+
if ( featureIterator.nextFeature( f ) )
5922+
{
5923+
mergeFeatureId = f.id( );
5924+
}
5925+
}
5926+
}
5927+
5928+
// convert to destination data type
5929+
QString errorMessageConvertCompatible;
5930+
if ( !isDefaultValue && !fields().at( i ).convertCompatible( val, &errorMessageConvertCompatible ) )
5931+
{
5932+
if ( errorMessage.isEmpty() )
5933+
errorMessage = tr( "Could not store value '%1' in field of type %2: %3" ).arg( mergeAttributes.at( i ).toString(), fields().at( i ).typeName(), errorMessageConvertCompatible );
5934+
}
5935+
newAttributes[ i ] = val;
5936+
}
5937+
5938+
beginEditCommand( tr( "Merged features" ) );
5939+
5940+
QgsFeatureIds featureIdsAfter = selectedFeatureIds();
5941+
5942+
QgsFeature mergeFeature;
5943+
if ( mergeFeatureId == FID_NULL )
5944+
{
5945+
// Create new feature
5946+
mergeFeature = QgsVectorLayerUtils::createFeature( this, unionGeometry, newAttributes );
5947+
}
5948+
else
5949+
{
5950+
// Merge into existing feature
5951+
featureIdsAfter.remove( mergeFeatureId );
5952+
}
5953+
5954+
// Delete other features
5955+
QgsFeatureIds::const_iterator feature_it = featureIdsAfter.constBegin();
5956+
for ( ; feature_it != featureIdsAfter.constEnd(); ++feature_it )
5957+
{
5958+
deleteFeature( *feature_it );
5959+
}
5960+
5961+
if ( mergeFeatureId == FID_NULL )
5962+
{
5963+
// Add the new feature
5964+
addFeature( mergeFeature );
5965+
}
5966+
else
5967+
{
5968+
// Modify merge feature
5969+
changeGeometry( mergeFeatureId, unionGeometry );
5970+
changeAttributeValues( mergeFeatureId, newAttributes );
5971+
}
5972+
5973+
endEditCommand();
5974+
5975+
triggerRepaint();
5976+
5977+
return true;
5978+
}
5979+
58665980
QgsGeometryOptions *QgsVectorLayer::geometryOptions() const
58675981
{
58685982
return mGeometryOptions.get();

‎src/core/vector/qgsvectorlayer.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2480,6 +2480,18 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
24802480
*/
24812481
QgsStoredExpressionManager *storedExpressionManager() { return mStoredExpressionManager; }
24822482

2483+
/**
2484+
* Merge selected features into a single one.
2485+
* \param mergeAttributes are the resulting attributes in the merged feature
2486+
* \param unionGeometry is the resulting geometry of the merged feature
2487+
* \param errorMessage will be set to a descriptive error message if any occurs
2488+
*
2489+
* \returns TRUE if the merge was successful, or FALSE if the operation failed.
2490+
*
2491+
* \since QGIS 3.30
2492+
*/
2493+
bool mergeSelectedFeatures( const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage );
2494+
24832495
public slots:
24842496

24852497
/**

0 commit comments

Comments
 (0)
Please sign in to comment.