mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-18 00:03:05 -04:00
Port makeFeaturesCompatible to C++
as: QgsVectorLayerUtils::makeFeaturesCompatible With tests.
This commit is contained in:
parent
5173744818
commit
930c3f8e45
@ -133,7 +133,7 @@ Returns true if the attribute value is valid for the field. Any constraint failu
|
||||
If the strength or origin parameter is set then only constraints with a matching strength/origin will be checked.
|
||||
%End
|
||||
|
||||
static QgsFeature createFeature( QgsVectorLayer *layer,
|
||||
static QgsFeature createFeature( const QgsVectorLayer *layer,
|
||||
const QgsGeometry &geometry = QgsGeometry(),
|
||||
const QgsAttributeMap &attributes = QgsAttributeMap(),
|
||||
QgsExpressionContext *context = 0 );
|
||||
@ -176,6 +176,28 @@ are padded with NULL values to match the required length).
|
||||
|
||||
.. versionadded:: 3.4
|
||||
%End
|
||||
|
||||
|
||||
static QgsFeatureList makeFeaturesCompatible( const QgsFeatureList &features, QgsVectorLayer &layer );
|
||||
%Docstring
|
||||
Converts input ``features`` to be compatible with the given ``layer``.
|
||||
|
||||
This function returns a new list of transformed features compatible with the input
|
||||
layer, note that the number of features returned might be greater than the number
|
||||
of input featurers.
|
||||
|
||||
The following operations will be performed to convert the input features:
|
||||
- convert single geometries to multi part
|
||||
- drop additional attributes
|
||||
- drop geometry if layer is geometry-less
|
||||
- add missing attribute fields
|
||||
- add back M/Z values (initialized to 0)
|
||||
- drop Z/M
|
||||
- convert multi part geometries to single part
|
||||
|
||||
.. versionadded:: 3.4
|
||||
%End
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
@ -70,85 +70,6 @@ def execute(alg, parameters, context=None, feedback=None):
|
||||
return False, {}
|
||||
|
||||
|
||||
def make_features_compatible(new_features, input_layer):
|
||||
"""Try to make the new features compatible with old features by:
|
||||
|
||||
- converting single to multi part
|
||||
- dropping additional attributes
|
||||
- adding back M/Z values
|
||||
- drop Z/M
|
||||
- convert multi part to single part
|
||||
|
||||
:param new_features: new features
|
||||
:type new_features: list of QgsFeatures
|
||||
:param input_layer: input layer
|
||||
:type input_layer: QgsVectorLayer
|
||||
:return: modified features
|
||||
:rtype: list of QgsFeatures
|
||||
"""
|
||||
|
||||
input_wkb_type = input_layer.wkbType()
|
||||
result_features = []
|
||||
for new_f in new_features:
|
||||
# Fix attributes
|
||||
QgsVectorLayerUtils.matchAttributesToFields(new_f, input_layer.fields())
|
||||
|
||||
# Check if we need geometry manipulation
|
||||
new_f_geom_type = QgsWkbTypes.geometryType(new_f.geometry().wkbType())
|
||||
new_f_has_geom = new_f_geom_type not in (QgsWkbTypes.UnknownGeometry, QgsWkbTypes.NullGeometry)
|
||||
input_layer_has_geom = input_wkb_type not in (QgsWkbTypes.NoGeometry, QgsWkbTypes.Unknown)
|
||||
|
||||
# Drop geometry if layer is geometry-less
|
||||
if not input_layer_has_geom and new_f_has_geom:
|
||||
f = QgsFeature(input_layer.fields())
|
||||
f.setAttributes(new_f.attributes())
|
||||
new_f = f
|
||||
result_features.append(new_f)
|
||||
continue # skip the rest
|
||||
|
||||
if input_layer_has_geom and new_f_has_geom and \
|
||||
new_f.geometry().wkbType() != input_wkb_type: # Fix geometry
|
||||
# Single -> Multi
|
||||
if (QgsWkbTypes.isMultiType(input_wkb_type) and not
|
||||
new_f.geometry().isMultipart()):
|
||||
new_geom = new_f.geometry()
|
||||
new_geom.convertToMultiType()
|
||||
new_f.setGeometry(new_geom)
|
||||
# Drop Z/M
|
||||
if (new_f.geometry().constGet().is3D() and not QgsWkbTypes.hasZ(input_wkb_type)):
|
||||
new_geom = new_f.geometry()
|
||||
new_geom.get().dropZValue()
|
||||
new_f.setGeometry(new_geom)
|
||||
if (new_f.geometry().constGet().isMeasure() and not QgsWkbTypes.hasM(input_wkb_type)):
|
||||
new_geom = new_f.geometry()
|
||||
new_geom.get().dropMValue()
|
||||
new_f.setGeometry(new_geom)
|
||||
# Add Z/M back (set it to 0)
|
||||
if (not new_f.geometry().constGet().is3D() and QgsWkbTypes.hasZ(input_wkb_type)):
|
||||
new_geom = new_f.geometry()
|
||||
new_geom.get().addZValue(0.0)
|
||||
new_f.setGeometry(new_geom)
|
||||
if (not new_f.geometry().constGet().isMeasure() and QgsWkbTypes.hasM(input_wkb_type)):
|
||||
new_geom = new_f.geometry()
|
||||
new_geom.get().addMValue(0.0)
|
||||
new_f.setGeometry(new_geom)
|
||||
# Multi -> Single
|
||||
if (not QgsWkbTypes.isMultiType(input_wkb_type) and
|
||||
new_f.geometry().isMultipart()):
|
||||
g = new_f.geometry()
|
||||
g2 = g.constGet()
|
||||
for i in range(g2.partCount()):
|
||||
# Clone or crash!
|
||||
g4 = QgsGeometry(g2.geometryN(i).clone())
|
||||
f = QgsVectorLayerUtils.createFeature(input_layer, g4, {i: new_f.attribute(i) for i in range(new_f.fields().count())})
|
||||
result_features.append(f)
|
||||
else:
|
||||
result_features.append(new_f)
|
||||
else:
|
||||
result_features.append(new_f)
|
||||
return result_features
|
||||
|
||||
|
||||
def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=None, raise_exceptions=False):
|
||||
"""Executes an algorithm modifying features in-place in the input layer.
|
||||
|
||||
@ -208,7 +129,7 @@ def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=N
|
||||
# a shallow copy from processFeature
|
||||
input_feature = QgsFeature(f)
|
||||
new_features = alg.processFeature(input_feature, context, feedback)
|
||||
new_features = make_features_compatible(new_features, active_layer)
|
||||
new_features = QgsVectorLayerUtils.makeFeaturesCompatible(new_features, active_layer)
|
||||
if len(new_features) == 0:
|
||||
active_layer.deleteFeature(f.id())
|
||||
elif len(new_features) == 1:
|
||||
@ -238,7 +159,8 @@ def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=N
|
||||
active_layer.deleteFeatures(active_layer.selectedFeatureIds())
|
||||
new_features = []
|
||||
for f in result_layer.getFeatures():
|
||||
new_features.extend(make_features_compatible([f], active_layer))
|
||||
new_features.extend(QgsVectorLayerUtils.
|
||||
makeFeaturesCompatible([f], active_layer))
|
||||
|
||||
# Get the new ids
|
||||
old_ids = set([f.id() for f in active_layer.getFeatures(req)])
|
||||
|
@ -25,6 +25,7 @@
|
||||
#include "qgsfeedback.h"
|
||||
#include "qgsvectorlayer.h"
|
||||
#include "qgsthreadingutils.h"
|
||||
#include "qgsgeometrycollection.h"
|
||||
|
||||
QgsFeatureIterator QgsVectorLayerUtils::getValuesIterator( const QgsVectorLayer *layer, const QString &fieldOrExpression, bool &ok, bool selectedOnly )
|
||||
{
|
||||
@ -348,7 +349,7 @@ bool QgsVectorLayerUtils::validateAttribute( const QgsVectorLayer *layer, const
|
||||
return valid;
|
||||
}
|
||||
|
||||
QgsFeature QgsVectorLayerUtils::createFeature( QgsVectorLayer *layer, const QgsGeometry &geometry,
|
||||
QgsFeature QgsVectorLayerUtils::createFeature( const QgsVectorLayer *layer, const QgsGeometry &geometry,
|
||||
const QgsAttributeMap &attributes, QgsExpressionContext *context )
|
||||
{
|
||||
if ( !layer )
|
||||
@ -560,6 +561,97 @@ void QgsVectorLayerUtils::matchAttributesToFields( QgsFeature &feature, const Qg
|
||||
}
|
||||
}
|
||||
|
||||
QgsFeatureList QgsVectorLayerUtils::makeFeaturesCompatible( const QgsFeatureList &features, QgsVectorLayer &layer )
|
||||
{
|
||||
QgsWkbTypes::Type inputWkbType( layer.wkbType( ) );
|
||||
QgsFeatureList resultFeatures;
|
||||
for ( const QgsFeature &f : features )
|
||||
{
|
||||
QgsFeature newF( f );
|
||||
// Fix attributes
|
||||
QgsVectorLayerUtils::matchAttributesToFields( newF, layer.fields( ) );
|
||||
// Does geometry need tranformations?
|
||||
QgsWkbTypes::GeometryType newFGeomType( QgsWkbTypes::geometryType( newF.geometry().wkbType() ) );
|
||||
bool newFHasGeom = newFGeomType !=
|
||||
QgsWkbTypes::GeometryType::UnknownGeometry &&
|
||||
newFGeomType != QgsWkbTypes::GeometryType::NullGeometry;
|
||||
bool layerHasGeom = inputWkbType !=
|
||||
QgsWkbTypes::Type::NoGeometry &&
|
||||
inputWkbType != QgsWkbTypes::Type::Unknown;
|
||||
// Drop geometry if layer is geometry-less
|
||||
if ( newFHasGeom && ! layerHasGeom )
|
||||
{
|
||||
QgsFeature _f = QgsFeature( layer.fields() );
|
||||
_f.setAttributes( newF.attributes() );
|
||||
resultFeatures.append( _f );
|
||||
continue; // Skip the rest
|
||||
}
|
||||
// Geometry need fixing
|
||||
if ( newFHasGeom && layerHasGeom && newF.geometry().wkbType() != inputWkbType )
|
||||
{
|
||||
// Single -> multi
|
||||
if ( QgsWkbTypes::isMultiType( inputWkbType ) && ! newF.geometry().isMultipart( ) )
|
||||
{
|
||||
QgsGeometry newGeom( newF.geometry( ) );
|
||||
newGeom.convertToMultiType();
|
||||
newF.setGeometry( newGeom );
|
||||
}
|
||||
// Drop Z/M
|
||||
if ( newF.geometry().constGet()->is3D() && ! QgsWkbTypes::hasZ( inputWkbType ) )
|
||||
{
|
||||
QgsGeometry newGeom( newF.geometry( ) );
|
||||
newGeom.get()->dropZValue();
|
||||
newF.setGeometry( newGeom );
|
||||
}
|
||||
if ( newF.geometry().constGet()->isMeasure() && ! QgsWkbTypes::hasM( inputWkbType ) )
|
||||
{
|
||||
QgsGeometry newGeom( newF.geometry( ) );
|
||||
newGeom.get()->dropMValue();
|
||||
newF.setGeometry( newGeom );
|
||||
}
|
||||
// Add Z/M back, set to 0
|
||||
if ( ! newF.geometry().constGet()->is3D() && QgsWkbTypes::hasZ( inputWkbType ) )
|
||||
{
|
||||
QgsGeometry newGeom( newF.geometry( ) );
|
||||
newGeom.get()->addZValue( 0.0 );
|
||||
newF.setGeometry( newGeom );
|
||||
}
|
||||
if ( ! newF.geometry().constGet()->isMeasure() && QgsWkbTypes::hasM( inputWkbType ) )
|
||||
{
|
||||
QgsGeometry newGeom( newF.geometry( ) );
|
||||
newGeom.get()->addMValue( 0.0 );
|
||||
newF.setGeometry( newGeom );
|
||||
}
|
||||
// Multi -> single
|
||||
if ( ! QgsWkbTypes::isMultiType( inputWkbType ) && newF.geometry().isMultipart( ) )
|
||||
{
|
||||
QgsGeometry newGeom( newF.geometry( ) );
|
||||
const QgsGeometryCollection *parts( static_cast< const QgsGeometryCollection * >( newGeom.constGet() ) );
|
||||
for ( int i = 0; i < parts->partCount( ); i++ )
|
||||
{
|
||||
QgsGeometry g( parts->geometryN( i )->clone() );
|
||||
QgsAttributeMap attrMap;
|
||||
for ( int j = 0; j < newF.fields().count(); j++ )
|
||||
{
|
||||
attrMap[j] = newF.attribute( j );
|
||||
}
|
||||
QgsFeature _f( QgsVectorLayerUtils::createFeature( &layer, g, attrMap ) );
|
||||
resultFeatures.append( _f );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resultFeatures.append( newF );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resultFeatures.append( newF );
|
||||
}
|
||||
}
|
||||
return resultFeatures;
|
||||
}
|
||||
|
||||
QList<QgsVectorLayer *> QgsVectorLayerUtils::QgsDuplicateFeatureContext::layers() const
|
||||
{
|
||||
QList<QgsVectorLayer *> layers;
|
||||
|
@ -140,7 +140,7 @@ class CORE_EXPORT QgsVectorLayerUtils
|
||||
* assuming that they respect the layer's constraints. Note that the created feature is not
|
||||
* automatically inserted into the layer.
|
||||
*/
|
||||
static QgsFeature createFeature( QgsVectorLayer *layer,
|
||||
static QgsFeature createFeature( const QgsVectorLayer *layer,
|
||||
const QgsGeometry &geometry = QgsGeometry(),
|
||||
const QgsAttributeMap &attributes = QgsAttributeMap(),
|
||||
QgsExpressionContext *context = nullptr );
|
||||
@ -187,6 +187,28 @@ class CORE_EXPORT QgsVectorLayerUtils
|
||||
* \since QGIS 3.4
|
||||
*/
|
||||
static void matchAttributesToFields( QgsFeature &feature, const QgsFields &fields );
|
||||
|
||||
|
||||
/**
|
||||
* Converts input \a features to be compatible with the given \a layer.
|
||||
*
|
||||
* This function returns a new list of transformed features compatible with the input
|
||||
* layer, note that the number of features returned might be greater than the number
|
||||
* of input featurers.
|
||||
*
|
||||
* The following operations will be performed to convert the input features:
|
||||
* - convert single geometries to multi part
|
||||
* - drop additional attributes
|
||||
* - drop geometry if layer is geometry-less
|
||||
* - add missing attribute fields
|
||||
* - add back M/Z values (initialized to 0)
|
||||
* - drop Z/M
|
||||
* - convert multi part geometries to single part
|
||||
*
|
||||
* \since QGIS 3.4
|
||||
*/
|
||||
static QgsFeatureList makeFeaturesCompatible( const QgsFeatureList &features, QgsVectorLayer &layer );
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
@ -17,10 +17,11 @@ from qgis.core import (
|
||||
QgsFeature, QgsGeometry, QgsSettings, QgsApplication, QgsMemoryProviderUtils, QgsWkbTypes, QgsField, QgsFields, QgsProcessingFeatureSourceDefinition, QgsProcessingContext, QgsProcessingFeedback, QgsCoordinateReferenceSystem, QgsProject, QgsProcessingException
|
||||
)
|
||||
from processing.core.Processing import Processing
|
||||
from processing.gui.AlgorithmExecutor import execute_in_place_run, make_features_compatible
|
||||
from processing.gui.AlgorithmExecutor import execute_in_place_run
|
||||
from qgis.testing import start_app, unittest
|
||||
from qgis.PyQt.QtTest import QSignalSpy
|
||||
from qgis.analysis import QgsNativeAlgorithms
|
||||
from qgis.core import QgsVectorLayerUtils
|
||||
|
||||
start_app()
|
||||
|
||||
@ -185,7 +186,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
|
||||
context.setProject(QgsProject.instance())
|
||||
|
||||
# Fix it!
|
||||
new_features = make_features_compatible([f], layer)
|
||||
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f], layer)
|
||||
|
||||
for new_f in new_features:
|
||||
self.assertEqual(new_f.geometry().wkbType(), layer.wkbType())
|
||||
@ -193,7 +194,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
|
||||
self.assertTrue(layer.addFeatures(new_features), "Fail: %s - %s - %s" % (feature_wkt, attrs, layer_wkb_name))
|
||||
return layer, new_features
|
||||
|
||||
def test_make_features_compatible(self):
|
||||
def test_QgsVectorLayerUtilsmakeFeaturesCompatible(self):
|
||||
"""Test fixer function"""
|
||||
# Test failure
|
||||
with self.assertRaises(AssertionError):
|
||||
@ -283,13 +284,13 @@ class TestQgsProcessingInPlace(unittest.TestCase):
|
||||
f1['int_f'] = 1
|
||||
f1['str_f'] = 'str'
|
||||
f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)'))
|
||||
new_features = make_features_compatible([f1], layer)
|
||||
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
|
||||
self.assertEqual(new_features[0].attributes(), f1.attributes())
|
||||
self.assertTrue(new_features[0].geometry().asWkt(), f1.geometry().asWkt())
|
||||
|
||||
# Test pad with 0 with fields
|
||||
f1.setAttributes([])
|
||||
new_features = make_features_compatible([f1], layer)
|
||||
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
|
||||
self.assertEqual(len(new_features[0].attributes()), 2)
|
||||
self.assertEqual(new_features[0].attributes()[0], QVariant())
|
||||
self.assertEqual(new_features[0].attributes()[1], QVariant())
|
||||
@ -297,7 +298,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
|
||||
# Test pad with 0 without fields
|
||||
f1 = QgsFeature()
|
||||
f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)'))
|
||||
new_features = make_features_compatible([f1], layer)
|
||||
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
|
||||
self.assertEqual(len(new_features[0].attributes()), 2)
|
||||
self.assertEqual(new_features[0].attributes()[0], QVariant())
|
||||
self.assertEqual(new_features[0].attributes()[1], QVariant())
|
||||
@ -306,7 +307,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
|
||||
f1 = QgsFeature(layer.fields())
|
||||
f1.setAttributes([1, 'foo', 'extra'])
|
||||
f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)'))
|
||||
new_features = make_features_compatible([f1], layer)
|
||||
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
|
||||
self.assertEqual(len(new_features[0].attributes()), 2)
|
||||
self.assertEqual(new_features[0].attributes()[0], 1)
|
||||
self.assertEqual(new_features[0].attributes()[1], 'foo')
|
||||
@ -322,7 +323,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
|
||||
f1.setAttributes([1])
|
||||
|
||||
# Check that it is accepted on a Point layer
|
||||
new_features = make_features_compatible([f1], layer)
|
||||
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
|
||||
self.assertEqual(len(new_features), 1)
|
||||
self.assertEqual(new_features[0].geometry().asWkt(), '')
|
||||
|
||||
@ -330,7 +331,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
|
||||
nogeom_layer = QgsMemoryProviderUtils.createMemoryLayer(
|
||||
'nogeom_layer', layer.fields(), QgsWkbTypes.NoGeometry, QgsCoordinateReferenceSystem(4326))
|
||||
# Check that a geometry-less feature is accepted
|
||||
new_features = make_features_compatible([f1], nogeom_layer)
|
||||
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], nogeom_layer)
|
||||
self.assertEqual(len(new_features), 1)
|
||||
self.assertEqual(new_features[0].geometry().asWkt(), '')
|
||||
|
||||
@ -339,7 +340,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
|
||||
'nogeom_layer', layer.fields(), QgsWkbTypes.NoGeometry, QgsCoordinateReferenceSystem(4326))
|
||||
# Check that a Point feature is accepted but geometry was dropped
|
||||
f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)'))
|
||||
new_features = make_features_compatible([f1], nogeom_layer)
|
||||
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], nogeom_layer)
|
||||
self.assertEqual(len(new_features), 1)
|
||||
self.assertEqual(new_features[0].geometry().asWkt(), '')
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user