# -*- coding: utf-8 -*- """QGIS Unit tests for Processing In-Place algorithms. .. note:: This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. """ __author__ = 'Alessandro Pasotti' __date__ = '2018-09' __copyright__ = 'Copyright 2018, The QGIS Project' # This will get replaced with a git SHA1 when you do a git archive __revision__ = '$Format:%H$' from qgis.PyQt.QtCore import QCoreApplication, QVariant 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.core.ProcessingConfig import ProcessingConfig from processing.tools import dataobjects 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() class ConsoleFeedBack(QgsProcessingFeedback): def reportError(self, error, fatalError=False): print(error) base_types = ['Point', 'LineString', 'Polygon'] def _add_multi(base): return base + ['Multi' + _b for _b in base] def _add_z(base): return base + [_b + 'Z' for _b in base] def _add_m(base): return base + [_b + 'M' for _b in base] def _all_true(): types = base_types types = _add_multi(types) types = _add_z(types) types = _add_m(types) types.append('NoGeometry') return {t: True for t in types} def _all_false(): return {t: False for t in _all_true().keys()} class TestQgsProcessingInPlace(unittest.TestCase): @classmethod def setUpClass(cls): """Run before all tests""" QCoreApplication.setOrganizationName("QGIS_Test") QCoreApplication.setOrganizationDomain( "QGIS_TestPyQgsProcessingInPlace.com") QCoreApplication.setApplicationName("QGIS_TestPyQgsProcessingInPlace") QgsSettings().clear() Processing.initialize() QgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms()) cls.registry = QgsApplication.instance().processingRegistry() fields = QgsFields() fields.append(QgsField('int_f', QVariant.Int)) cls.vl = QgsMemoryProviderUtils.createMemoryLayer( 'mylayer', fields, QgsWkbTypes.Point, QgsCoordinateReferenceSystem(4326)) f1 = QgsFeature(cls.vl.fields()) f1['int_f'] = 1 f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)')) f2 = QgsFeature(cls.vl.fields()) f2['int_f'] = 2 f2.setGeometry(QgsGeometry.fromWkt('Point(9.5 45.6)')) cls.vl.dataProvider().addFeatures([f1, f2]) assert cls.vl.isValid() assert cls.vl.featureCount() == 2 # Multipolygon layer cls.multipoly_vl = QgsMemoryProviderUtils.createMemoryLayer( 'mymultiplayer', fields, QgsWkbTypes.MultiPolygon, QgsCoordinateReferenceSystem(4326)) f3 = QgsFeature(cls.multipoly_vl.fields()) f3.setGeometry(QgsGeometry.fromWkt('MultiPolygon (((2.81856297539240419 41.98170998812887689, 2.81874467773035464 41.98167537995160359, 2.81879535908157752 41.98154066615795443, 2.81866433873670452 41.98144056064155905, 2.81848263699778379 41.98147516865246587, 2.81843195500470811 41.98160988234612034, 2.81856297539240419 41.98170998812887689)),((2.81898589063455907 41.9815711567298635, 2.81892080450418803 41.9816030048432367, 2.81884192631866437 41.98143737613141724, 2.8190679469505846 41.98142270931093378, 2.81898589063455907 41.9815711567298635)))')) f4 = QgsFeature(cls.multipoly_vl.fields()) f4.setGeometry(QgsGeometry.fromWkt('MultiPolygon (((2.81823679385631332 41.98133290154246566, 2.81830770255185703 41.98123540208609228, 2.81825871989355159 41.98112524362621656, 2.81813882853970243 41.98111258462271422, 2.81806791984415872 41.98121008407908761, 2.81811690250246416 41.98132024253896333, 2.81823679385631332 41.98133290154246566)),((2.81835835162010895 41.98123286963267731, 2.8183127674586852 41.98108725356146209, 2.8184520523963692 41.98115436357689134, 2.81835835162010895 41.98123286963267731)))')) cls.multipoly_vl.dataProvider().addFeatures([f3, f4]) assert cls.multipoly_vl.isValid() assert cls.multipoly_vl.featureCount() == 2 QgsProject.instance().addMapLayers([cls.vl, cls.multipoly_vl]) def _make_layer(self, layer_wkb_name): 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) return layer def _support_inplace_edit_tester(self, alg_name, expected): alg = self.registry.createAlgorithmById(alg_name) for layer_wkb_name, supported in expected.items(): layer = self._make_layer(layer_wkb_name) #print("Checking %s ( %s ) : %s" % (alg_name, layer_wkb_name, supported)) self.assertEqual(alg.supportInPlaceEdit(layer), supported, "Expected: %s - %s = supported: %s" % (alg_name, layer_wkb_name, supported)) def test_support_in_place_edit(self): ALL = _all_true() GEOMETRY_ONLY = {t: t != 'NoGeometry' for t in _all_true().keys()} NONE = _all_false() LINESTRING_ONLY = {t: t.find('LineString') >= 0 for t in _all_true().keys()} Z_ONLY = {t: t.find('Z') > 0 for t in _all_true().keys()} M_ONLY = {t: t.rfind('M') > 0 for t in _all_true().keys()} NOT_M = {t: t.rfind('M') < 1 and t != 'NoGeometry' for t in _all_true().keys()} POLYGON_ONLY = {t: t in ('Polygon', 'MultiPolygon') for t in _all_true().keys()} MULTI_ONLY = {t: t.find('Multi') == 0 for t in _all_true().keys()} SINGLE_ONLY = {t: t.find('Multi') == -1 for t in _all_true().keys()} LINESTRING_AND_POLYGON_ONLY = {t: (t.find('LineString') >= 0 or t.find('Polygon') >= 0) for t in _all_true().keys()} LINESTRING_AND_POLYGON_ONLY_NOT_M = {t: (t.rfind('M') < 1 and (t.find('LineString') >= 0 or t.find('Polygon') >= 0)) for t in _all_true().keys()} LINESTRING_AND_POLYGON_ONLY_NOT_M_NOT_Z = {t: (t.rfind('M') < 1 and t.find('Z') == -1 and (t.find('LineString') >= 0 or t.find('Polygon') >= 0)) for t in _all_true().keys()} self._support_inplace_edit_tester('native:smoothgeometry', LINESTRING_AND_POLYGON_ONLY) self._support_inplace_edit_tester('native:arrayoffsetlines', LINESTRING_ONLY) self._support_inplace_edit_tester('native:arraytranslatedfeatures', GEOMETRY_ONLY) self._support_inplace_edit_tester('native:reprojectlayer', GEOMETRY_ONLY) self._support_inplace_edit_tester('qgis:densifygeometries', LINESTRING_AND_POLYGON_ONLY) self._support_inplace_edit_tester('qgis:densifygeometriesgivenaninterval', LINESTRING_AND_POLYGON_ONLY) self._support_inplace_edit_tester('native:setzfromraster', Z_ONLY) self._support_inplace_edit_tester('native:explodelines', LINESTRING_ONLY) self._support_inplace_edit_tester('native:extendlines', LINESTRING_ONLY) self._support_inplace_edit_tester('native:fixgeometries', NOT_M) self._support_inplace_edit_tester('native:minimumenclosingcircle', POLYGON_ONLY) self._support_inplace_edit_tester('native:multiringconstantbuffer', POLYGON_ONLY) self._support_inplace_edit_tester('native:orientedminimumboundingbox', POLYGON_ONLY) self._support_inplace_edit_tester('qgis:orthogonalize', LINESTRING_AND_POLYGON_ONLY) self._support_inplace_edit_tester('native:removeduplicatevertices', GEOMETRY_ONLY) self._support_inplace_edit_tester('native:rotatefeatures', GEOMETRY_ONLY) self._support_inplace_edit_tester('native:segmentizebymaxangle', NONE) self._support_inplace_edit_tester('native:segmentizebymaxdistance', NONE) self._support_inplace_edit_tester('native:setmfromraster', M_ONLY) self._support_inplace_edit_tester('native:simplifygeometries', LINESTRING_AND_POLYGON_ONLY) self._support_inplace_edit_tester('native:snappointstogrid', GEOMETRY_ONLY) self._support_inplace_edit_tester('native:multiparttosingleparts', GEOMETRY_ONLY) self._support_inplace_edit_tester('native:promotetomulti', MULTI_ONLY) self._support_inplace_edit_tester('native:subdivide', GEOMETRY_ONLY) self._support_inplace_edit_tester('native:translategeometry', GEOMETRY_ONLY) self._support_inplace_edit_tester('native:swapxy', GEOMETRY_ONLY) self._support_inplace_edit_tester('qgis:linestopolygons', NONE) self._support_inplace_edit_tester('qgis:polygonstolines', NONE) self._support_inplace_edit_tester('native:boundary', NONE) self._support_inplace_edit_tester('native:clip', GEOMETRY_ONLY) self._support_inplace_edit_tester('native:difference', GEOMETRY_ONLY) self._support_inplace_edit_tester('native:dropgeometries', ALL) self._support_inplace_edit_tester('native:splitwithlines', LINESTRING_AND_POLYGON_ONLY) def _make_compatible_tester(self, feature_wkt, layer_wkb_name, attrs=[1]): layer = self._make_layer(layer_wkb_name) 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 = QgsVectorLayerUtils.makeFeaturesCompatible([f], layer) for new_f in new_features: self.assertEqual(new_f.geometry().wkbType(), layer.wkbType()) self.assertTrue(layer.addFeatures(new_features), "Fail: %s - %s - %s" % (feature_wkt, attrs, layer_wkb_name)) return layer, new_features def test_QgsVectorLayerUtilsmakeFeaturesCompatible(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 test_make_features_compatible_attributes(self): """Test corner cases for attributes""" # Test feature without attributes fields = QgsFields() fields.append(QgsField('int_f', QVariant.Int)) fields.append(QgsField('str_f', QVariant.String)) layer = QgsMemoryProviderUtils.createMemoryLayer( 'mkfca_layer', fields, QgsWkbTypes.Point, QgsCoordinateReferenceSystem(4326)) self.assertTrue(layer.isValid()) f1 = QgsFeature(layer.fields()) f1['int_f'] = 1 f1['str_f'] = 'str' f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)')) 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 = 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()) # Test pad with 0 without fields f1 = QgsFeature() f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)')) 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()) # Test drop extra attrs f1 = QgsFeature(layer.fields()) f1.setAttributes([1, 'foo', 'extra']) f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)')) 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') def test_make_features_compatible_geometry(self): """Test corner cases for geometries""" # Make a feature with no geometry layer = self._make_layer('Point') self.assertTrue(layer.isValid()) self.assertTrue(layer.startEditing()) f1 = QgsFeature(layer.fields()) f1.setAttributes([1]) # Check that it is accepted on a Point layer new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer) self.assertEqual(len(new_features), 1) self.assertEqual(new_features[0].geometry().asWkt(), '') # Make a geometry-less layer nogeom_layer = QgsMemoryProviderUtils.createMemoryLayer( 'nogeom_layer', layer.fields(), QgsWkbTypes.NoGeometry, QgsCoordinateReferenceSystem(4326)) # Check that a geometry-less feature is accepted new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], nogeom_layer) self.assertEqual(len(new_features), 1) self.assertEqual(new_features[0].geometry().asWkt(), '') # Make a geometry-less layer nogeom_layer = QgsMemoryProviderUtils.createMemoryLayer( '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 = QgsVectorLayerUtils.makeFeaturesCompatible([f1], nogeom_layer) self.assertEqual(len(new_features), 1) self.assertEqual(new_features[0].geometry().asWkt(), '') def _alg_tester(self, alg_name, input_layer, parameters): alg = self.registry.createAlgorithmById(alg_name) self.assertIsNotNone(alg) parameters['INPUT'] = input_layer parameters['OUTPUT'] = 'memory:' old_features = [f for f in input_layer.getFeatures()] input_layer.selectByIds([old_features[0].id()]) # Check selected self.assertEqual(input_layer.selectedFeatureIds(), [old_features[0].id()], alg_name) context = QgsProcessingContext() context.setProject(QgsProject.instance()) feedback = ConsoleFeedBack() input_layer.rollBack() ok = False ok, _ = execute_in_place_run( alg, parameters, context=context, feedback=feedback, raise_exceptions=True) new_features = [f for f in input_layer.getFeatures()] # Check ret values self.assertTrue(ok, alg_name) # Check geometry types (drop Z or M) self.assertEqual(new_features[0].geometry().wkbType(), old_features[0].geometry().wkbType()) return old_features, new_features def test_execute_in_place_run(self): """Test the execution in place""" self.vl.rollBack() old_features, new_features = self._alg_tester( 'native:translategeometry', self.vl, { 'DELTA_X': 1.1, 'DELTA_Y': 1.1, } ) # First feature was selected and modified self.assertEqual(new_features[0].id(), old_features[0].id()) self.assertAlmostEqual(new_features[0].geometry().asPoint().x(), old_features[0].geometry().asPoint().x() + 1.1, delta=0.01) self.assertAlmostEqual(new_features[0].geometry().asPoint().y(), old_features[0].geometry().asPoint().y() + 1.1, delta=0.01) # Second feature was not selected and not modified self.assertEqual(new_features[1].id(), old_features[1].id()) self.assertEqual(new_features[1].geometry().asPoint().x(), old_features[1].geometry().asPoint().x()) self.assertEqual(new_features[1].geometry().asPoint().y(), old_features[1].geometry().asPoint().y()) # Check selected self.assertEqual(self.vl.selectedFeatureIds(), [old_features[0].id()]) # Check that if the only change is Z or M then we should fail with self.assertRaises(QgsProcessingException) as cm: self._alg_tester( 'native:translategeometry', self.vl, { 'DELTA_Z': 1.1, } ) self.vl.rollBack() # Check that if the only change is Z or M then we should fail with self.assertRaises(QgsProcessingException) as cm: self._alg_tester( 'native:translategeometry', self.vl, { 'DELTA_M': 1.1, } ) self.vl.rollBack() old_features, new_features = self._alg_tester( 'native:translategeometry', self.vl, { 'DELTA_X': 1.1, 'DELTA_Z': 1.1, } ) def test_select_all_features(self): """Check that if there is no selection, the alg will run on all features""" self.vl.rollBack() self.vl.removeSelection() old_count = self.vl.featureCount() context = QgsProcessingContext() context.setProject(QgsProject.instance()) feedback = ConsoleFeedBack() alg = self.registry.createAlgorithmById('native:translategeometry') self.assertIsNotNone(alg) parameters = { 'DELTA_X': 1.1, 'DELTA_Y': 1.1, } parameters['INPUT'] = self.vl parameters['OUTPUT'] = 'memory:' old_features = [f for f in self.vl.getFeatures()] ok, _ = execute_in_place_run( alg, parameters, context=context, feedback=feedback, raise_exceptions=True) new_features = [f for f in self.vl.getFeatures()] self.assertEqual(len(new_features), old_count) # Check all are selected self.assertEqual(len(self.vl.selectedFeatureIds()), old_count) def test_multi_to_single(self): """Check that the geometry type is still multi after the alg is run""" old_features, new_features = self._alg_tester( 'native:multiparttosingleparts', self.multipoly_vl, { } ) self.assertEqual(len(new_features), 3) # Check selected self.assertEqual(len(self.multipoly_vl.selectedFeatureIds()), 2) def test_arraytranslatedfeatures(self): """Check that this runs correctly and additional attributes are dropped""" old_count = self.vl.featureCount() old_features, new_features = self._alg_tester( 'native:arraytranslatedfeatures', self.vl, { 'COUNT': 2, 'DELTA_X': 1.1, 'DELTA_Z': 1.1, } ) self.assertEqual(len(new_features), old_count + 2) # Check selected self.assertEqual(len(self.vl.selectedFeatureIds()), 3) def test_reprojectlayer(self): """Check that this runs correctly""" old_count = self.vl.featureCount() old_features, new_features = self._alg_tester( 'native:reprojectlayer', self.vl, { 'TARGET_CRS': 'EPSG:3857', } ) g = [f.geometry() for f in new_features][0] self.assertAlmostEqual(g.get().x(), 1001875.4, 1) self.assertAlmostEqual(g.get().y(), 5621521.5, 1) # Check selected self.assertEqual(self.vl.selectedFeatureIds(), [1]) def test_snappointstogrid(self): """Check that this runs correctly""" polygon_layer = self._make_layer('Polygon') f1 = QgsFeature(polygon_layer.fields()) f1.setAttributes([1]) f1.setGeometry(QgsGeometry.fromWkt('POLYGON((1.2 1.2, 1.2 2.2, 2.2 2.2, 2.2 1.2, 1.2 1.2))')) f2 = QgsFeature(polygon_layer.fields()) f2.setAttributes([2]) 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(polygon_layer.startEditing()) self.assertTrue(polygon_layer.addFeatures([f1, f2])) self.assertEqual(polygon_layer.featureCount(), 2) polygon_layer.commitChanges() self.assertEqual(polygon_layer.featureCount(), 2) QgsProject.instance().addMapLayers([polygon_layer]) polygon_layer.selectByIds([next(polygon_layer.getFeatures()).id()]) self.assertEqual(polygon_layer.selectedFeatureCount(), 1) old_features, new_features = self._alg_tester( 'native:snappointstogrid', polygon_layer, { 'HSPACING': 0.5, 'VSPACING': 0.5, } ) g = [f.geometry() for f in new_features][0] self.assertEqual(g.asWkt(), 'Polygon ((1 1, 1 2, 2 2, 2 1, 1 1))') # Check selected self.assertEqual(polygon_layer.selectedFeatureIds(), [1]) def test_clip(self): 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() 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() QgsProject.instance().addMapLayers([clip_layer, mask_layer]) old_features, new_features = self._alg_tester( 'native:clip', clip_layer, { 'OVERLAY': mask_layer.id(), } ) 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]) def test_fix_geometries(self): polygon_layer = self._make_layer('Polygon') self.assertTrue(polygon_layer.startEditing()) f = QgsFeature(polygon_layer.fields()) f.setAttributes([1]) # Flake! f.setGeometry(QgsGeometry.fromWkt('POLYGON ((0 0, 2 2, 0 2, 2 0, 0 0))')) self.assertTrue(f.isValid()) f2 = QgsFeature(polygon_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(polygon_layer.addFeatures([f, f2])) polygon_layer.commitChanges() polygon_layer.rollBack() self.assertEqual(polygon_layer.featureCount(), 2) QgsProject.instance().addMapLayers([polygon_layer]) old_features, new_features = self._alg_tester( 'native:fixgeometries', polygon_layer, { } ) self.assertEqual(polygon_layer.featureCount(), 3) wkt1, wkt2, _ = [f.geometry().asWkt() for f in new_features] self.assertEqual(wkt1, 'Polygon ((0 0, 1 1, 2 0, 0 0))') self.assertEqual(wkt2, 'Polygon ((1 1, 0 2, 2 2, 1 1))') # Test with Z (interpolated) polygonz_layer = self._make_layer('PolygonZ') self.assertTrue(polygonz_layer.startEditing()) f3 = QgsFeature(polygonz_layer.fields()) f3.setAttributes([1]) f3.setGeometry(QgsGeometry.fromWkt('POLYGON Z((0 0 1, 2 2 1, 0 2 3, 2 0 4, 0 0 1))')) self.assertTrue(f3.isValid()) self.assertTrue(polygonz_layer.addFeatures([f3])) polygonz_layer.commitChanges() polygonz_layer.rollBack() self.assertEqual(polygonz_layer.featureCount(), 1) QgsProject.instance().addMapLayers([polygonz_layer]) old_features, new_features = self._alg_tester( 'native:fixgeometries', polygonz_layer, { } ) self.assertEqual(polygonz_layer.featureCount(), 2) wkt1, wkt2 = [f.geometry().asWkt() for f in new_features] self.assertEqual(wkt1, 'PolygonZ ((0 0 1, 1 1 2.25, 2 0 4, 0 0 1))') self.assertEqual(wkt2, 'PolygonZ ((1 1 2.25, 0 2 3, 2 2 1, 1 1 2.25))') def _test_difference_on_invalid_geometries(self, geom_option): polygon_layer = self._make_layer('Polygon') self.assertTrue(polygon_layer.startEditing()) f = QgsFeature(polygon_layer.fields()) f.setAttributes([1]) # Flake! f.setGeometry(QgsGeometry.fromWkt('Polygon ((0 0, 2 2, 0 2, 2 0, 0 0))')) self.assertTrue(f.isValid()) self.assertTrue(polygon_layer.addFeatures([f])) polygon_layer.commitChanges() polygon_layer.rollBack() self.assertEqual(polygon_layer.featureCount(), 1) overlay_layer = self._make_layer('Polygon') self.assertTrue(overlay_layer.startEditing()) f = QgsFeature(overlay_layer.fields()) f.setAttributes([1]) f.setGeometry(QgsGeometry.fromWkt('Polygon ((0 0, 2 0, 2 2, 0 2, 0 0))')) self.assertTrue(f.isValid()) self.assertTrue(overlay_layer.addFeatures([f])) overlay_layer.commitChanges() overlay_layer.rollBack() self.assertEqual(overlay_layer.featureCount(), 1) QgsProject.instance().addMapLayers([polygon_layer, overlay_layer]) old_features = [f for f in polygon_layer.getFeatures()] # 'Ignore features with invalid geometries' = 1 ProcessingConfig.setSettingValue(ProcessingConfig.FILTER_INVALID_GEOMETRIES, geom_option) feedback = ConsoleFeedBack() context = dataobjects.createContext(feedback) context.setProject(QgsProject.instance()) alg = self.registry.createAlgorithmById('native:difference') self.assertIsNotNone(alg) parameters = { 'OVERLAY': overlay_layer, 'INPUT': polygon_layer, 'OUTPUT': ':memory', } old_features = [f for f in polygon_layer.getFeatures()] self.assertTrue(polygon_layer.startEditing()) polygon_layer.selectAll() ok, _ = execute_in_place_run( alg, parameters, context=context, feedback=feedback, raise_exceptions=True) new_features = [f for f in polygon_layer.getFeatures()] return old_features, new_features def test_difference_on_invalid_geometries(self): """Test #20147 difference deletes invalid geometries""" old_features, new_features = self._test_difference_on_invalid_geometries(1) self.assertEqual(len(new_features), 1) old_features, new_features = self._test_difference_on_invalid_geometries(0) self.assertEqual(len(new_features), 1) old_features, new_features = self._test_difference_on_invalid_geometries(2) self.assertEqual(len(new_features), 1) if __name__ == '__main__': unittest.main()