Feature fixer: multi part to single part

This commit is contained in:
Alessandro Pasotti 2018-09-11 16:53:21 +02:00 committed by Nyall Dawson
parent 29fe9cca41
commit 7b162b535f
2 changed files with 212 additions and 146 deletions

View File

@ -40,7 +40,8 @@ from qgis.core import (Qgis,
QgsFeature,
QgsExpression,
QgsWkbTypes,
QgsGeometry)
QgsGeometry,
QgsVectorLayerUtils)
from processing.gui.Postprocessing import handleAlgorithmResults
from processing.tools import dataobjects
from qgis.utils import iface
@ -69,55 +70,74 @@ def execute(alg, parameters, context=None, feedback=None):
return False, {}
def make_features_compatible(new_features, input_layer, old_feature=None):
def make_features_compatible(new_features, input_layer, context):
"""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
:param context: processing context
:type context: QgsProcessingContext
:return: modified features
:rtype: list of QgsFeatures
"""
input_wkb_type = input_layer.wkbType()
result_features = []
for new_f in new_features:
if new_f.geometry().wkbType() != input_layer.wkbType():
# Single -> Multi
if (QgsWkbTypes.isMultiType(input_layer.wkbType()) 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_layer.wkbType())):
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_layer.wkbType())):
new_geom = new_f.geometry()
new_geom.get().dropMValue()
new_f.setGeometry(new_geom)
# Add Z/M back (set it to 0)
if (old_feature is not None and not new_f.geometry().constGet().is3D() and QgsWkbTypes.hasZ(input_layer.wkbType())):
new_geom = new_f.geometry()
new_geom.get().addZValue(0.0)
new_f.setGeometry(new_geom)
if (old_feature is not None and not new_f.geometry().constGet().isMeasure() and QgsWkbTypes.hasM(input_layer.wkbType())):
new_geom = new_f.geometry()
new_geom.get().addMValue(0.0)
new_f.setGeometry(new_geom)
# Fix attributes
if len(new_f.attributes()) > len(input_layer.fields()):
f = QgsFeature(input_layer.fields())
f.setGeometry(new_f.geometry())
f.setAttributes(new_f.attributes()[:len(input_layer.fields())])
new_f = f
result_features.append(new_f)
# Fix geometry
if new_f.geometry().wkbType() != input_wkb_type:
# 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
@ -176,7 +196,7 @@ def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=N
feature_iterator = active_layer.getFeatures(QgsFeatureRequest(active_layer.selectedFeatureIds())) if parameters['INPUT'].selectedFeaturesOnly else active_layer.getFeatures()
for f in feature_iterator:
new_features = alg.processFeature(f, context, feedback)
new_features = make_features_compatible(new_features, active_layer, f)
new_features = make_features_compatible(new_features, active_layer, context)
if len(new_features) == 0:
active_layer.deleteFeature(f.id())
elif len(new_features) == 1:
@ -200,30 +220,32 @@ def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=N
else: # Traditional 'run' with delete and add features cycle
results, ok = alg.run(parameters, context, feedback)
result_layer = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context)
# TODO: check if features have changed before delete/add cycle
active_layer.deleteFeatures(active_layer.selectedFeatureIds())
new_features = []
for f in result_layer.getFeatures():
new_features.append(make_features_compatible([f], active_layer))
if ok:
result_layer = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context)
# TODO: check if features have changed before delete/add cycle
active_layer.deleteFeatures(active_layer.selectedFeatureIds())
new_features = []
for f in result_layer.getFeatures():
new_features.extend(make_features_compatible([f], active_layer, context))
# Get the new ids
old_ids = set([f.id() for f in active_layer.getFeatures(req)])
active_layer.addFeatures(new_features)
new_ids = set([f.id() for f in active_layer.getFeatures(req)])
new_feature_ids += list(new_ids - old_ids)
# Get the new ids
old_ids = set([f.id() for f in active_layer.getFeatures(req)])
active_layer.addFeatures(new_features)
new_ids = set([f.id() for f in active_layer.getFeatures(req)])
new_feature_ids += list(new_ids - old_ids)
active_layer.endEditCommand()
if ok and new_feature_ids:
active_layer.selectByIds(new_feature_ids)
elif not ok:
active_layer.rollback()
active_layer.rollBack()
return ok, results
except QgsProcessingException as e:
active_layer.endEditCommand()
active_layer.rollBack()
if raise_exceptions:
raise e
QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', Qgis.Critical)

View File

@ -25,6 +25,11 @@ from qgis.analysis import QgsNativeAlgorithms
start_app()
class ConsoleFeedBack(QgsProcessingFeedback):
def reportError(self, error, fatalError=False):
print(error)
class TestQgsProcessingInPlace(unittest.TestCase):
@classmethod
@ -70,6 +75,109 @@ class TestQgsProcessingInPlace(unittest.TestCase):
QgsProject.instance().addMapLayers([cls.vl, cls.multipoly_vl])
def _make_compatible_tester(self, feature_wkt, layer_wkb_name, attrs=[1]):
fields = QgsFields()
wkb_type = getattr(QgsWkbTypes, layer_wkb_name)
fields.append(QgsField('int_f', QVariant.Int))
layer = QgsMemoryProviderUtils.createMemoryLayer(
'%s_layer' % layer_wkb_name, fields, wkb_type, QgsCoordinateReferenceSystem(4326))
self.assertTrue(layer.isValid())
self.assertEqual(layer.wkbType(), wkb_type)
layer.startEditing()
f = QgsFeature(layer.fields())
f.setAttributes(attrs)
f.setGeometry(QgsGeometry.fromWkt(feature_wkt))
self.assertTrue(f.isValid())
context = QgsProcessingContext()
context.setProject(QgsProject.instance())
# Fix it!
new_features = make_features_compatible([f], layer, context)
for new_f in new_features:
self.assertEqual(new_f.geometry().wkbType(), wkb_type)
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):
"""Test fixer function"""
# Test failure
with self.assertRaises(AssertionError):
self._make_compatible_tester('LineString (1 1, 2 2, 3 3)', 'Point')
self._make_compatible_tester('Point(1 1)', 'Point')
self._make_compatible_tester('Point(1 1)', 'Point', [1, 'nope'])
self._make_compatible_tester('Point z (1 1 3)', 'Point')
self._make_compatible_tester('Point z (1 1 3)', 'PointZ')
# Adding Z back
l, f = self._make_compatible_tester('Point (1 1)', 'PointZ')
self.assertEqual(f[0].geometry().get().z(), 0)
# Adding M back
l, f = self._make_compatible_tester('Point (1 1)', 'PointM')
self.assertEqual(f[0].geometry().get().m(), 0)
self._make_compatible_tester('Point m (1 1 3)', 'Point')
self._make_compatible_tester('Point(1 3)', 'MultiPoint')
self._make_compatible_tester('MultiPoint((1 3), (2 2))', 'MultiPoint')
self._make_compatible_tester('Polygon((1 1, 2 2, 3 3, 1 1))', 'Polygon')
self._make_compatible_tester('Polygon((1 1, 2 2, 3 3, 1 1)', 'Polygon', [1, 'nope'])
self._make_compatible_tester('Polygon z ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'Polygon')
self._make_compatible_tester('Polygon z ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'PolygonZ')
# Adding Z back
l, f = self._make_compatible_tester('Polygon ((1 1, 2 2, 3 3, 1 1))', 'PolygonZ')
g = f[0].geometry()
g2 = g.get()
for v in g2.vertices():
self.assertEqual(v.z(), 0)
# Adding M back
l, f = self._make_compatible_tester('Polygon ((1 1, 2 2, 3 3, 1 1))', 'PolygonM')
g = f[0].geometry()
g2 = g.get()
for v in g2.vertices():
self.assertEqual(v.m(), 0)
self._make_compatible_tester('Polygon m ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'Polygon')
self._make_compatible_tester('Polygon m ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'PolygonM')
self._make_compatible_tester('Polygon((1 1, 2 2, 3 3, 1 1))', 'MultiPolygon')
self._make_compatible_tester('MultiPolygon(((1 1, 2 2, 3 3, 1 1)), ((1 1, 2 2, 3 3, 1 1)))', 'MultiPolygon')
self._make_compatible_tester('LineString((1 1, 2 2, 3 3, 1 1))', 'LineString')
self._make_compatible_tester('LineString((1 1, 2 2, 3 3, 1 1)', 'LineString', [1, 'nope'])
self._make_compatible_tester('LineString z ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'LineString')
self._make_compatible_tester('LineString z ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'LineStringZ')
self._make_compatible_tester('LineString m ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'LineString')
self._make_compatible_tester('LineString m ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'LineStringM')
# Adding Z back
l, f = self._make_compatible_tester('LineString (1 1, 2 2, 3 3, 1 1))', 'LineStringZ')
g = f[0].geometry()
g2 = g.get()
for v in g2.vertices():
self.assertEqual(v.z(), 0)
# Adding M back
l, f = self._make_compatible_tester('LineString (1 1, 2 2, 3 3, 1 1))', 'LineStringM')
g = f[0].geometry()
g2 = g.get()
for v in g2.vertices():
self.assertEqual(v.m(), 0)
self._make_compatible_tester('LineString(1 1, 2 2, 3 3, 1 1)', 'MultiLineString')
self._make_compatible_tester('MultiLineString((1 1, 2 2, 3 3, 1 1), (1 1, 2 2, 3 3, 1 1))', 'MultiLineString')
# Test Multi -> Single
l, f = self._make_compatible_tester('MultiLineString((1 1, 2 2, 3 3, 1 1), (10 1, 20 2, 30 3, 10 1))', 'LineString')
self.assertEqual(len(f), 2)
self.assertEqual(f[0].geometry().asWkt(), 'LineString (1 1, 2 2, 3 3, 1 1)')
self.assertEqual(f[1].geometry().asWkt(), 'LineString (10 1, 20 2, 30 3, 10 1)')
def _alg_tester(self, alg_name, input_layer, parameters):
alg = self.registry.createAlgorithmById(alg_name)
@ -86,7 +194,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
context = QgsProcessingContext()
context.setProject(QgsProject.instance())
feedback = QgsProcessingFeedback()
feedback = ConsoleFeedBack()
input_layer.rollBack()
with self.assertRaises(QgsProcessingException) as cm:
@ -220,115 +328,51 @@ class TestQgsProcessingInPlace(unittest.TestCase):
# Check selected
self.assertEqual(self.vl.selectedFeatureIds(), [1])
def _make_compatible_tester(self, feature_wkt, layer_wkb_name, attrs=[1], old_feature=None):
fields = QgsFields()
wkb_type = getattr(QgsWkbTypes, layer_wkb_name)
fields.append(QgsField('int_f', QVariant.Int))
layer = QgsMemoryProviderUtils.createMemoryLayer(
'%s_layer' % layer_wkb_name, fields, wkb_type, QgsCoordinateReferenceSystem(4326))
self.assertTrue(layer.isValid())
self.assertEqual(layer.wkbType(), wkb_type)
layer.startEditing()
def test_clip(self):
f = QgsFeature(layer.fields())
f.setAttributes(attrs)
f.setGeometry(QgsGeometry.fromWkt(feature_wkt))
mask_layer = QgsMemoryProviderUtils.createMemoryLayer(
'mask_layer', self.vl.fields(), QgsWkbTypes.Polygon, QgsCoordinateReferenceSystem(4326))
self.assertTrue(mask_layer.isValid())
self.assertTrue(mask_layer.startEditing())
f = QgsFeature(mask_layer.fields())
f.setAttributes([1])
f.setGeometry(QgsGeometry.fromWkt('POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))'))
self.assertTrue(f.isValid())
f2 = QgsFeature(mask_layer.fields())
f2.setAttributes([1])
f2.setGeometry(QgsGeometry.fromWkt('POLYGON((1.1 1.1, 1.1 2.1, 2.1 2.1, 2.1 1.1, 1.1 1.1))'))
self.assertTrue(f2.isValid())
self.assertTrue(mask_layer.addFeatures([f, f2]))
mask_layer.commitChanges()
mask_layer.rollBack()
# Fix it!
new_features = make_features_compatible([f], layer, old_feature)
clip_layer = QgsMemoryProviderUtils.createMemoryLayer(
'clip_layer', self.vl.fields(), QgsWkbTypes.LineString, QgsCoordinateReferenceSystem(4326))
self.assertTrue(clip_layer.isValid())
self.assertTrue(clip_layer.startEditing())
f = QgsFeature(clip_layer.fields())
f.setAttributes([1])
f.setGeometry(QgsGeometry.fromWkt('LINESTRING(-1 -1, 3 3)'))
self.assertTrue(f.isValid())
self.assertTrue(clip_layer.addFeatures([f]))
self.assertEqual(clip_layer.featureCount(), 1)
clip_layer.commitChanges()
clip_layer.selectAll()
clip_layer.rollBack()
self.assertEqual(f.geometry().wkbType(), wkb_type)
self.assertTrue(layer.addFeatures(new_features), "Fail: %s - %s - %s" % (feature_wkt, attrs, layer_wkb_name))
return layer, new_features[0]
QgsProject.instance().addMapLayers([clip_layer, mask_layer])
def test_make_features_compatible(self):
"""Test fixer function"""
# Test failure
with self.assertRaises(AssertionError):
self._make_compatible_tester('LineString (1 1, 2 2, 3 3)', 'Point')
self._make_compatible_tester('Point(1 1)', 'Point')
self._make_compatible_tester('Point(1 1)', 'Point', [1, 'nope'])
self._make_compatible_tester('Point z (1 1 3)', 'Point')
self._make_compatible_tester('Point z (1 1 3)', 'PointZ')
old_features, new_features = self._alg_tester(
'native:clip',
clip_layer,
{
'OVERLAY': mask_layer.id(),
}
)
# Adding Z back
old_feature = QgsFeature(self.vl.fields())
old_feature.setAttributes([1])
old_feature.setGeometry(QgsGeometry.fromWkt('Point z (1 1 3)'))
l, f = self._make_compatible_tester('Point (1 1)', 'PointZ', old_feature=old_feature)
self.assertEqual(f.geometry().get().z(), 0)
# Adding M back
old_feature = QgsFeature(self.vl.fields())
old_feature.setAttributes([1])
old_feature.setGeometry(QgsGeometry.fromWkt('Point m (1 1 3)'))
l, f = self._make_compatible_tester('Point (1 1)', 'PointM', old_feature=old_feature)
self.assertEqual(f.geometry().get().m(), 0)
self._make_compatible_tester('Point m (1 1 3)', 'Point')
self._make_compatible_tester('Point(1 3)', 'MultiPoint')
self._make_compatible_tester('MultiPoint((1 3), (2 2))', 'MultiPoint')
self._make_compatible_tester('Polygon((1 1, 2 2, 3 3, 1 1))', 'Polygon')
self._make_compatible_tester('Polygon((1 1, 2 2, 3 3, 1 1)', 'Polygon', [1, 'nope'])
self._make_compatible_tester('Polygon z ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'Polygon')
self._make_compatible_tester('Polygon z ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'PolygonZ')
# Adding Z back
old_feature = QgsFeature(self.vl.fields())
old_feature.setAttributes([1])
old_feature.setGeometry(QgsGeometry.fromWkt('Polygon z ((1 1 1, 2 2 2, 3 3 3 , 1 1 1))'))
l, f = self._make_compatible_tester('Polygon ((1 1, 2 2, 3 3, 1 1))', 'PolygonZ', old_feature=old_feature)
g = f.geometry()
g2 = g.get()
for v in g2.vertices():
self.assertEqual(v.z(), 0)
# Adding M back
old_feature = QgsFeature(self.vl.fields())
old_feature.setAttributes([1])
old_feature.setGeometry(QgsGeometry.fromWkt('Polygon m ((1 1 1, 2 2 2, 3 3 3 , 1 1 1))'))
l, f = self._make_compatible_tester('Polygon ((1 1, 2 2, 3 3, 1 1))', 'PolygonM', old_feature=old_feature)
g = f.geometry()
g2 = g.get()
for v in g2.vertices():
self.assertEqual(v.m(), 0)
self._make_compatible_tester('Polygon m ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'Polygon')
self._make_compatible_tester('Polygon m ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'PolygonM')
self._make_compatible_tester('Polygon((1 1, 2 2, 3 3, 1 1))', 'MultiPolygon')
self._make_compatible_tester('MultiPolygon(((1 1, 2 2, 3 3, 1 1)), ((1 1, 2 2, 3 3, 1 1)))', 'MultiPolygon')
self._make_compatible_tester('LineString((1 1, 2 2, 3 3, 1 1))', 'LineString')
self._make_compatible_tester('LineString((1 1, 2 2, 3 3, 1 1)', 'LineString', [1, 'nope'])
self._make_compatible_tester('LineString z ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'LineString')
self._make_compatible_tester('LineString z ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'LineStringZ')
self._make_compatible_tester('LineString m ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'LineString')
self._make_compatible_tester('LineString m ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', 'LineStringM')
# Adding Z back
old_feature = QgsFeature(self.vl.fields())
old_feature.setAttributes([1])
old_feature.setGeometry(QgsGeometry.fromWkt('LineString z ((1 1 1, 2 2 2, 3 3 3 , 1 1 1))'))
l, f = self._make_compatible_tester('LineString ((1 1, 2 2, 3 3, 1 1))', 'LineStringZ', old_feature=old_feature)
g = f.geometry()
g2 = g.get()
for v in g2.vertices():
self.assertEqual(v.z(), 0)
# Adding M back
old_feature = QgsFeature(self.vl.fields())
old_feature.setAttributes([1])
old_feature.setGeometry(QgsGeometry.fromWkt('LineString m ((1 1 1, 2 2 2, 3 3 3 , 1 1 1))'))
l, f = self._make_compatible_tester('LineString ((1 1, 2 2, 3 3, 1 1))', 'LineStringM', old_feature=old_feature)
g = f.geometry()
g2 = g.get()
for v in g2.vertices():
self.assertEqual(v.m(), 0)
self._make_compatible_tester('LineString((1 1, 2 2, 3 3, 1 1))', 'MultiLineString')
self._make_compatible_tester('MultiLineString(((1 1, 2 2, 3 3, 1 1)), ((1 1, 2 2, 3 3, 1 1)))', 'MultiLineString')
self.assertEqual(len(new_features), 2)
self.assertEqual(new_features[0].geometry().asWkt(), 'LineString (0 0, 1 1)')
self.assertEqual(new_features[0].attributes(), [1])
if __name__ == '__main__':