Skip to content

Commit

Permalink
Move merge logic to QgsVectorLayerEditUtils
Browse files Browse the repository at this point in the history
  • Loading branch information
domi4484 authored and m-kuhn committed Mar 30, 2023
1 parent 41b0477 commit 9224dbe
Show file tree
Hide file tree
Showing 8 changed files with 409 additions and 141 deletions.
13 changes: 0 additions & 13 deletions python/core/auto_generated/vector/qgsvectorlayer.sip.in
Expand Up @@ -2652,19 +2652,6 @@ Configuration and logic to apply automatically on any edit happening on this lay
Returns the manager of the stored expressions for this layer.

.. versionadded:: 3.10
%End

bool mergeSelectedFeatures( const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage );
%Docstring
Merge selected features into a single one.

:param mergeAttributes: are the resulting attributes in the merged feature
:param unionGeometry: is the resulting geometry of the merged feature
:param errorMessage: will be set to a descriptive error message if any occurs

:return: ``True`` if the merge was successful, or ``False`` if the operation failed.

.. versionadded:: 3.30
%End

public slots:
Expand Down
14 changes: 14 additions & 0 deletions python/core/auto_generated/vector/qgsvectorlayereditutils.sip.in
Expand Up @@ -310,6 +310,20 @@ editing.
:return: 2 in case vertex already exists or point does not intersect segment

.. versionadded:: 3.16
%End

bool mergeFeatures( const QgsFeatureIds &mergeFeatureIds, const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage /Out/ );
%Docstring
Merge features into a single one.

:param mergeFeaturesIds: id list of features to merge
:param mergeAttributes: are the resulting attributes in the merged feature
:param unionGeometry: is the resulting geometry of the merged feature

:return: - ``True`` if the merge was successful, or ``False`` if the operation failed.
- errorMessage: will be set to a descriptive error message if any occurs

.. versionadded:: 3.30
%End

};
Expand Down
4 changes: 3 additions & 1 deletion src/app/qgisapp.cpp
Expand Up @@ -102,6 +102,7 @@
#include "qgsdockablewidgethelper.h"
#include "vertextool/qgsvertexeditor.h"
#include "qgsvectorlayerutils.h"
#include "qgsvectorlayereditutils.h"
#include "qgsadvanceddigitizingdockwidget.h"
#include "qgsabstractdatasourcewidget.h"
#include "qgsmeshlayer.h"
Expand Down Expand Up @@ -9607,7 +9608,8 @@ void QgisApp::mergeSelectedFeatures()
}

QString errorMessage;
bool success = vl->mergeSelectedFeatures( d.mergedAttributes(), unionGeom, errorMessage );
QgsVectorLayerEditUtils vectorLayerEditUtils( vl );
bool success = vectorLayerEditUtils.mergeFeatures( vl->selectedFeatureIds(), d.mergedAttributes(), unionGeom, errorMessage );

if ( !success )
{
Expand Down
114 changes: 0 additions & 114 deletions src/core/vector/qgsvectorlayer.cpp
Expand Up @@ -5863,120 +5863,6 @@ void QgsVectorLayer::setAllowCommit( bool allowCommit )
emit allowCommitChanged();
}

bool QgsVectorLayer::mergeSelectedFeatures( const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage )
{
errorMessage.clear();

if ( selectedFeatureIds().size() < 2 )
{
errorMessage = tr( "Not enough features selected, the merge tool requires at least two selected features" );
return false;
}

QgsAttributeMap newAttributes;
QgsFeatureId mergeFeatureId = FID_NULL;
for ( int i = 0; i < mergeAttributes.count(); ++i )
{
QVariant val = mergeAttributes.at( i );

bool isDefaultValue = fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
dataProvider() &&
dataProvider()->defaultValueClause( fields().fieldOriginIndex( i ) ) == val;

// Check if features can merged into an existing one
if ( mergeFeatureId == FID_NULL )
{
bool isPrimaryKey = fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
dataProvider() &&
dataProvider()->pkAttributeIndexes().contains( fields().fieldOriginIndex( i ) );

if ( isPrimaryKey && !isDefaultValue )
{
QgsFeatureRequest request;
request.setFlags( QgsFeatureRequest::Flag::NoGeometry );
// Handle multi pks
if ( dataProvider()->pkAttributeIndexes().count() > 1 && dataProvider()->pkAttributeIndexes().count() <= mergeAttributes.count() )
{
const auto pkIdxList { dataProvider()->pkAttributeIndexes() };
QStringList conditions;
QStringList fieldNames;
for ( const int &pkIdx : std::as_const( pkIdxList ) )
{
const QgsField pkField { fields().field( pkIdx ) };
conditions.push_back( QgsExpression::createFieldEqualityExpression( pkField.name(), mergeAttributes.at( pkIdx ), pkField.type( ) ) );
fieldNames.push_back( pkField.name() );
}
request.setSubsetOfAttributes( fieldNames, fields( ) );
request.setFilterExpression( conditions.join( QStringLiteral( " AND " ) ) );
}
else // single pk
{
const QgsField pkField { fields().field( i ) };
request.setSubsetOfAttributes( QStringList() << pkField.name(), fields( ) );
request.setFilterExpression( QgsExpression::createFieldEqualityExpression( pkField.name(), val, pkField.type( ) ) );
}

QgsFeature f;
QgsFeatureIterator featureIterator = getFeatures( request );
if ( featureIterator.nextFeature( f ) )
{
mergeFeatureId = f.id( );
}
}
}

// convert to destination data type
QString errorMessageConvertCompatible;
if ( !isDefaultValue && !fields().at( i ).convertCompatible( val, &errorMessageConvertCompatible ) )
{
if ( errorMessage.isEmpty() )
errorMessage = tr( "Could not store value '%1' in field of type %2: %3" ).arg( mergeAttributes.at( i ).toString(), fields().at( i ).typeName(), errorMessageConvertCompatible );
}
newAttributes[ i ] = val;
}

beginEditCommand( tr( "Merged features" ) );

QgsFeatureIds featureIdsAfter = selectedFeatureIds();

QgsFeature mergeFeature;
if ( mergeFeatureId == FID_NULL )
{
// Create new feature
mergeFeature = QgsVectorLayerUtils::createFeature( this, unionGeometry, newAttributes );
}
else
{
// Merge into existing feature
featureIdsAfter.remove( mergeFeatureId );
}

// Delete other features
QgsFeatureIds::const_iterator feature_it = featureIdsAfter.constBegin();
for ( ; feature_it != featureIdsAfter.constEnd(); ++feature_it )
{
deleteFeature( *feature_it );
}

if ( mergeFeatureId == FID_NULL )
{
// Add the new feature
addFeature( mergeFeature );
}
else
{
// Modify merge feature
changeGeometry( mergeFeatureId, unionGeometry );
changeAttributeValues( mergeFeatureId, newAttributes );
}

endEditCommand();

triggerRepaint();

return true;
}

QgsGeometryOptions *QgsVectorLayer::geometryOptions() const
{
return mGeometryOptions.get();
Expand Down
12 changes: 0 additions & 12 deletions src/core/vector/qgsvectorlayer.h
Expand Up @@ -2480,18 +2480,6 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
*/
QgsStoredExpressionManager *storedExpressionManager() { return mStoredExpressionManager; }

/**
* Merge selected features into a single one.
* \param mergeAttributes are the resulting attributes in the merged feature
* \param unionGeometry is the resulting geometry of the merged feature
* \param errorMessage will be set to a descriptive error message if any occurs
*
* \returns TRUE if the merge was successful, or FALSE if the operation failed.
*
* \since QGIS 3.30
*/
bool mergeSelectedFeatures( const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage );

public slots:

/**
Expand Down
113 changes: 113 additions & 0 deletions src/core/vector/qgsvectorlayereditutils.cpp
Expand Up @@ -721,6 +721,119 @@ int QgsVectorLayerEditUtils::addTopologicalPoints( const QgsPointXY &p )
return addTopologicalPoints( QgsPoint( p ) );
}

bool QgsVectorLayerEditUtils::mergeFeatures( const QgsFeatureIds &mergeFeatureIds, const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage )
{
errorMessage.clear();

if ( mergeFeatureIds.size() < 2 )
{
errorMessage = QObject::tr( "Not enough features to merge, the merge tool requires at least two features" );
return false;
}

QgsAttributeMap newAttributes;
QgsFeatureId mergeFeatureId = FID_NULL;
for ( int i = 0; i < mergeAttributes.count(); ++i )
{
QVariant val = mergeAttributes.at( i );

bool isDefaultValue = mLayer->fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
mLayer->dataProvider() &&
mLayer->dataProvider()->defaultValueClause( mLayer->fields().fieldOriginIndex( i ) ) == val;

// Check if features can merged into an existing one
if ( mergeFeatureId == FID_NULL )
{
bool isPrimaryKey = mLayer->fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
mLayer->dataProvider() &&
mLayer->dataProvider()->pkAttributeIndexes().contains( mLayer->fields().fieldOriginIndex( i ) );

if ( isPrimaryKey && !isDefaultValue )
{
QgsFeatureRequest request;
request.setFlags( QgsFeatureRequest::Flag::NoGeometry );
// Handle multi pks
if ( mLayer->dataProvider()->pkAttributeIndexes().count() > 1 && mLayer->dataProvider()->pkAttributeIndexes().count() <= mergeAttributes.count() )
{
const auto pkIdxList { mLayer->dataProvider()->pkAttributeIndexes() };
QStringList conditions;
QStringList fieldNames;
for ( const int &pkIdx : std::as_const( pkIdxList ) )
{
const QgsField pkField { mLayer->fields().field( pkIdx ) };
conditions.push_back( QgsExpression::createFieldEqualityExpression( pkField.name(), mergeAttributes.at( pkIdx ), pkField.type( ) ) );
fieldNames.push_back( pkField.name() );
}
request.setSubsetOfAttributes( fieldNames, mLayer->fields( ) );
request.setFilterExpression( conditions.join( QStringLiteral( " AND " ) ) );
}
else // single pk
{
const QgsField pkField { mLayer->fields().field( i ) };
request.setSubsetOfAttributes( QStringList() << pkField.name(), mLayer->fields( ) );
request.setFilterExpression( QgsExpression::createFieldEqualityExpression( pkField.name(), val, pkField.type( ) ) );
}

QgsFeature f;
QgsFeatureIterator featureIterator = mLayer->getFeatures( request );
if ( featureIterator.nextFeature( f ) )
{
mergeFeatureId = f.id( );
}
}
}

// convert to destination data type
QString errorMessageConvertCompatible;
if ( !isDefaultValue && !mLayer->fields().at( i ).convertCompatible( val, &errorMessageConvertCompatible ) )
{
if ( errorMessage.isEmpty() )
errorMessage = QObject::tr( "Could not store value '%1' in field of type %2: %3" ).arg( mergeAttributes.at( i ).toString(), mLayer->fields().at( i ).typeName(), errorMessageConvertCompatible );
}
newAttributes[ i ] = val;
}

mLayer->beginEditCommand( QObject::tr( "Merged features" ) );

QgsFeatureIds featureIdsToDelete = mergeFeatureIds;

QgsFeature mergeFeature;
if ( mergeFeatureId == FID_NULL )
{
// Create new feature
mergeFeature = QgsVectorLayerUtils::createFeature( mLayer, unionGeometry, newAttributes );
}
else
{
// Merge into existing feature
featureIdsToDelete.remove( mergeFeatureId );
}

// Delete other features
QgsFeatureIds::const_iterator feature_it = featureIdsToDelete.constBegin();
for ( ; feature_it != featureIdsToDelete.constEnd(); ++feature_it )
{
mLayer->deleteFeature( *feature_it );
}

if ( mergeFeatureId == FID_NULL )
{
// Add the new feature
mLayer->addFeature( mergeFeature );
}
else
{
// Modify merge feature
mLayer->changeGeometry( mergeFeatureId, unionGeometry );
mLayer->changeAttributeValues( mergeFeatureId, newAttributes );
}

mLayer->endEditCommand();

mLayer->triggerRepaint();

return true;
}

bool QgsVectorLayerEditUtils::boundingBoxFromPointList( const QgsPointSequence &list, double &xmin, double &ymin, double &xmax, double &ymax ) const
{
Expand Down
13 changes: 13 additions & 0 deletions src/core/vector/qgsvectorlayereditutils.h
Expand Up @@ -268,6 +268,19 @@ class CORE_EXPORT QgsVectorLayerEditUtils
*/
int addTopologicalPoints( const QgsPointSequence &ps );

/**
* Merge features into a single one.
* \param mergeFeaturesIds id list of features to merge
* \param mergeAttributes are the resulting attributes in the merged feature
* \param unionGeometry is the resulting geometry of the merged feature
* \param errorMessage will be set to a descriptive error message if any occurs
*
* \returns TRUE if the merge was successful, or FALSE if the operation failed.
*
* \since QGIS 3.30
*/
bool mergeFeatures( const QgsFeatureIds &mergeFeatureIds, const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage SIP_OUT );

private:

/**
Expand Down

0 comments on commit 9224dbe

Please sign in to comment.