Skip to content

Commit 71e85cc

Browse files
authored
Merge pull request #8231 from elpaso/bugfix-20147-in-place-difference
[in-place][needs-docs] add buffer for polygons and fix #20147 in place difference
2 parents f00e43d + 21e685b commit 71e85cc

File tree

8 files changed

+140
-17
lines changed

8 files changed

+140
-17
lines changed

python/core/auto_generated/qgsfeaturerequest.sip.in

+1-2
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,7 @@ Sets flags that affect how features will be fetched
508508

509509
QgsFeatureRequest &setSubsetOfAttributes( const QgsAttributeList &attrs );
510510
%Docstring
511-
Set a subset of attributes that will be fetched. Empty list means that all attributes are used.
512-
To disable fetching attributes, reset the FetchAttributes flag (which is set by default)
511+
Set a subset of attributes that will be fetched.
513512
%End
514513

515514
QgsAttributeList subsetOfAttributes() const;

python/plugins/processing/core/ProcessingConfig.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,9 @@ def setValue(self, value):
293293
self.validator(value)
294294
self.value = value
295295

296-
def read(self, qsettings=QgsSettings()):
296+
def read(self, qsettings=None):
297+
if not qsettings:
298+
qsettings = QgsSettings()
297299
value = qsettings.value(self.qname, None)
298300
if value is not None:
299301
if isinstance(self.value, bool):
@@ -307,7 +309,9 @@ def read(self, qsettings=QgsSettings()):
307309
else:
308310
self.value = value
309311

310-
def save(self, qsettings=QgsSettings()):
312+
def save(self, qsettings=None):
313+
if not qsettings:
314+
qsettings = QgsSettings()
311315
if self.valuetype == self.SELECTION:
312316
qsettings.setValue(self.qname, self.options.index(self.value))
313317
else:

python/plugins/processing/gui/AlgorithmExecutor.py

+36-7
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,14 @@ def execute_in_place_run(alg, parameters, context=None, feedback=None, raise_exc
128128
if not active_layer.selectedFeatureIds():
129129
active_layer.selectAll()
130130

131+
# Make sure we are working on selected features only
132+
parameters['INPUT'] = QgsProcessingFeatureSourceDefinition(active_layer.id(), True)
131133
parameters['OUTPUT'] = 'memory:'
132134

135+
req = QgsFeatureRequest(QgsExpression(r"$id < 0"))
136+
req.setFlags(QgsFeatureRequest.NoGeometry)
137+
req.setSubsetOfAttributes([])
138+
133139
# Start the execution
134140
# If anything goes wrong and raise_exceptions is True an exception
135141
# is raised, else the execution is aborted and the error reported in
@@ -139,10 +145,6 @@ def execute_in_place_run(alg, parameters, context=None, feedback=None, raise_exc
139145

140146
active_layer.beginEditCommand(alg.displayName())
141147

142-
req = QgsFeatureRequest(QgsExpression(r"$id < 0"))
143-
req.setFlags(QgsFeatureRequest.NoGeometry)
144-
req.setSubsetOfAttributes([])
145-
146148
# Checks whether the algorithm has a processFeature method
147149
if hasattr(alg, 'processFeature'): # in-place feature editing
148150
# Make a clone or it will crash the second time the dialog
@@ -154,7 +156,9 @@ def execute_in_place_run(alg, parameters, context=None, feedback=None, raise_exc
154156
if not alg.supportInPlaceEdit(active_layer):
155157
raise QgsProcessingException(tr("Selected algorithm and parameter configuration are not compatible with in-place modifications."))
156158
field_idxs = range(len(active_layer.fields()))
157-
feature_iterator = active_layer.getFeatures(QgsFeatureRequest(active_layer.selectedFeatureIds()))
159+
iterator_req = QgsFeatureRequest(active_layer.selectedFeatureIds())
160+
iterator_req.setInvalidGeometryCheck(context.invalidGeometryCheck())
161+
feature_iterator = active_layer.getFeatures(iterator_req)
158162
step = 100 / len(active_layer.selectedFeatureIds()) if active_layer.selectedFeatureIds() else 1
159163
for current, f in enumerate(feature_iterator):
160164
feedback.setProgress(current * step)
@@ -166,6 +170,7 @@ def execute_in_place_run(alg, parameters, context=None, feedback=None, raise_exc
166170
input_feature = QgsFeature(f)
167171
new_features = alg.processFeature(input_feature, context, feedback)
168172
new_features = QgsVectorLayerUtils.makeFeaturesCompatible(new_features, active_layer)
173+
169174
if len(new_features) == 0:
170175
active_layer.deleteFeature(f.id())
171176
elif len(new_features) == 1:
@@ -187,13 +192,32 @@ def execute_in_place_run(alg, parameters, context=None, feedback=None, raise_exc
187192
results, ok = {}, True
188193

189194
else: # Traditional 'run' with delete and add features cycle
195+
196+
# There is no way to know if some features have been skipped
197+
# due to invalid geometries
198+
if context.invalidGeometryCheck() == QgsFeatureRequest.GeometrySkipInvalid:
199+
selected_ids = active_layer.selectedFeatureIds()
200+
else:
201+
selected_ids = []
202+
190203
results, ok = alg.run(parameters, context, feedback)
191204

192205
if ok:
193206
result_layer = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context)
194207
# TODO: check if features have changed before delete/add cycle
195-
active_layer.deleteFeatures(active_layer.selectedFeatureIds())
208+
196209
new_features = []
210+
211+
# Check if there are any skipped features
212+
if context.invalidGeometryCheck() == QgsFeatureRequest.GeometrySkipInvalid:
213+
missing_ids = list(set(selected_ids) - set(result_layer.allFeatureIds()))
214+
if missing_ids:
215+
for f in active_layer.getFeatures(QgsFeatureRequest(missing_ids)):
216+
if not f.geometry().isGeosValid():
217+
new_features.append(f)
218+
219+
active_layer.deleteFeatures(active_layer.selectedFeatureIds())
220+
197221
for f in result_layer.getFeatures():
198222
new_features.extend(QgsVectorLayerUtils.
199223
makeFeaturesCompatible([f], active_layer))
@@ -248,7 +272,12 @@ def execute_in_place(alg, parameters, context=None, feedback=None):
248272
parameters['INPUT'] = iface.activeLayer()
249273
ok, results = execute_in_place_run(alg, parameters, context=context, feedback=feedback)
250274
if ok:
251-
parameters['INPUT'].triggerRepaint()
275+
if isinstance(parameters['INPUT'], QgsProcessingFeatureSourceDefinition):
276+
layer = alg.parameterAsVectorLayer({'INPUT': parameters['INPUT'].source}, 'INPUT', context)
277+
elif isinstance(parameters['INPUT'], QgsVectorLayer):
278+
layer = parameters['INPUT']
279+
if layer:
280+
layer.triggerRepaint()
252281
return ok, results
253282

254283

src/analysis/processing/qgsalgorithmbuffer.cpp

+18
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
***************************************************************************/
1717

1818
#include "qgsalgorithmbuffer.h"
19+
#include "qgswkbtypes.h"
20+
#include "qgsvectorlayer.h"
1921

2022
///@cond PRIVATE
2123

@@ -165,4 +167,20 @@ QVariantMap QgsBufferAlgorithm::processAlgorithm( const QVariantMap &parameters,
165167
return outputs;
166168
}
167169

170+
QgsProcessingAlgorithm::Flags QgsBufferAlgorithm::flags() const
171+
{
172+
Flags f = QgsProcessingAlgorithm::flags();
173+
f |= QgsProcessingAlgorithm::FlagSupportsInPlaceEdits;
174+
return f;
175+
}
176+
177+
bool QgsBufferAlgorithm::supportInPlaceEdit( const QgsMapLayer *layer ) const
178+
{
179+
const QgsVectorLayer *vlayer = qobject_cast< const QgsVectorLayer * >( layer );
180+
if ( !vlayer )
181+
return false;
182+
//Only Polygons
183+
return vlayer->wkbType() == QgsWkbTypes::Type::Polygon || vlayer->wkbType() == QgsWkbTypes::Type::MultiPolygon;
184+
}
185+
168186
///@endcond

src/analysis/processing/qgsalgorithmbuffer.h

+4
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,16 @@ class QgsBufferAlgorithm : public QgsProcessingAlgorithm
4444
QString groupId() const override;
4545
QString shortHelpString() const override;
4646
QgsBufferAlgorithm *createInstance() const override SIP_FACTORY;
47+
bool supportInPlaceEdit( const QgsMapLayer *layer ) const override;
48+
QgsProcessingAlgorithm::Flags flags() const override;
49+
4750

4851
protected:
4952

5053
QVariantMap processAlgorithm( const QVariantMap &parameters,
5154
QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override;
5255

56+
5357
};
5458

5559
///@endcond PRIVATE

src/analysis/processing/qgsoverlayutils.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ void QgsOverlayUtils::difference( const QgsFeatureSource &sourceA, const QgsFeat
9292

9393
QgsFeature featA;
9494
QgsFeatureRequest requestA;
95+
requestA.setInvalidGeometryCheck( context.invalidGeometryCheck() );
9596
if ( outputAttrs == OutputBA )
9697
requestA.setDestinationCrs( sourceB.sourceCrs(), context.transformContext() );
9798
QgsFeatureIterator fitA = sourceA.getFeatures( requestA );

src/core/qgsfeaturerequest.h

+1-2
Original file line numberDiff line numberDiff line change
@@ -493,8 +493,7 @@ class CORE_EXPORT QgsFeatureRequest
493493
const Flags &flags() const { return mFlags; }
494494

495495
/**
496-
* Set a subset of attributes that will be fetched. Empty list means that all attributes are used.
497-
* To disable fetching attributes, reset the FetchAttributes flag (which is set by default)
496+
* Set a subset of attributes that will be fetched.
498497
*/
499498
QgsFeatureRequest &setSubsetOfAttributes( const QgsAttributeList &attrs );
500499

tests/src/python/test_qgsprocessinginplace.py

+73-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
QgsFeature, QgsGeometry, QgsSettings, QgsApplication, QgsMemoryProviderUtils, QgsWkbTypes, QgsField, QgsFields, QgsProcessingFeatureSourceDefinition, QgsProcessingContext, QgsProcessingFeedback, QgsCoordinateReferenceSystem, QgsProject, QgsProcessingException
1818
)
1919
from processing.core.Processing import Processing
20+
from processing.core.ProcessingConfig import ProcessingConfig
21+
from processing.tools import dataobjects
2022
from processing.gui.AlgorithmExecutor import execute_in_place_run
2123
from qgis.testing import start_app, unittest
2224
from qgis.PyQt.QtTest import QSignalSpy
@@ -132,7 +134,8 @@ def test_support_in_place_edit(self):
132134
Z_ONLY = {t: t.find('Z') > 0 for t in _all_true().keys()}
133135
M_ONLY = {t: t.rfind('M') > 0 for t in _all_true().keys()}
134136
NOT_M = {t: t.rfind('M') < 1 and t != 'NoGeometry' for t in _all_true().keys()}
135-
POLYGON_ONLY = {t: t in ('Polygon', 'MultiPolygon') for t in _all_true().keys()}
137+
POLYGON_ONLY = {t: t.find('Polygon') for t in _all_true().keys()}
138+
POLYGON_ONLY_NOT_M_NOT_Z = {t: t in ('Polygon', 'MultiPolygon') for t in _all_true().keys()}
136139
MULTI_ONLY = {t: t.find('Multi') == 0 for t in _all_true().keys()}
137140
SINGLE_ONLY = {t: t.find('Multi') == -1 for t in _all_true().keys()}
138141
LINESTRING_AND_POLYGON_ONLY = {t: (t.find('LineString') >= 0 or t.find('Polygon') >= 0) for t in _all_true().keys()}
@@ -149,9 +152,9 @@ def test_support_in_place_edit(self):
149152
self._support_inplace_edit_tester('native:explodelines', LINESTRING_ONLY)
150153
self._support_inplace_edit_tester('native:extendlines', LINESTRING_ONLY)
151154
self._support_inplace_edit_tester('native:fixgeometries', NOT_M)
152-
self._support_inplace_edit_tester('native:minimumenclosingcircle', POLYGON_ONLY)
153-
self._support_inplace_edit_tester('native:multiringconstantbuffer', POLYGON_ONLY)
154-
self._support_inplace_edit_tester('native:orientedminimumboundingbox', POLYGON_ONLY)
155+
self._support_inplace_edit_tester('native:minimumenclosingcircle', POLYGON_ONLY_NOT_M_NOT_Z)
156+
self._support_inplace_edit_tester('native:multiringconstantbuffer', POLYGON_ONLY_NOT_M_NOT_Z)
157+
self._support_inplace_edit_tester('native:orientedminimumboundingbox', POLYGON_ONLY_NOT_M_NOT_Z)
155158
self._support_inplace_edit_tester('qgis:orthogonalize', LINESTRING_AND_POLYGON_ONLY)
156159
self._support_inplace_edit_tester('native:removeduplicatevertices', GEOMETRY_ONLY)
157160
self._support_inplace_edit_tester('native:rotatefeatures', GEOMETRY_ONLY)
@@ -172,6 +175,7 @@ def test_support_in_place_edit(self):
172175
self._support_inplace_edit_tester('native:difference', GEOMETRY_ONLY)
173176
self._support_inplace_edit_tester('native:dropgeometries', ALL)
174177
self._support_inplace_edit_tester('native:splitwithlines', LINESTRING_AND_POLYGON_ONLY)
178+
self._support_inplace_edit_tester('native:buffer', POLYGON_ONLY_NOT_M_NOT_Z)
175179

176180
def _make_compatible_tester(self, feature_wkt, layer_wkb_name, attrs=[1]):
177181
layer = self._make_layer(layer_wkb_name)
@@ -659,6 +663,71 @@ def test_fix_geometries(self):
659663
self.assertEqual(wkt1, 'PolygonZ ((0 0 1, 1 1 2.25, 2 0 4, 0 0 1))')
660664
self.assertEqual(wkt2, 'PolygonZ ((1 1 2.25, 0 2 3, 2 2 1, 1 1 2.25))')
661665

666+
def _test_difference_on_invalid_geometries(self, geom_option):
667+
polygon_layer = self._make_layer('Polygon')
668+
self.assertTrue(polygon_layer.startEditing())
669+
f = QgsFeature(polygon_layer.fields())
670+
f.setAttributes([1])
671+
# Flake!
672+
f.setGeometry(QgsGeometry.fromWkt('Polygon ((0 0, 2 2, 0 2, 2 0, 0 0))'))
673+
self.assertTrue(f.isValid())
674+
self.assertTrue(polygon_layer.addFeatures([f]))
675+
polygon_layer.commitChanges()
676+
polygon_layer.rollBack()
677+
self.assertEqual(polygon_layer.featureCount(), 1)
678+
679+
overlay_layer = self._make_layer('Polygon')
680+
self.assertTrue(overlay_layer.startEditing())
681+
f = QgsFeature(overlay_layer.fields())
682+
f.setAttributes([1])
683+
f.setGeometry(QgsGeometry.fromWkt('Polygon ((0 0, 2 0, 2 2, 0 2, 0 0))'))
684+
self.assertTrue(f.isValid())
685+
self.assertTrue(overlay_layer.addFeatures([f]))
686+
overlay_layer.commitChanges()
687+
overlay_layer.rollBack()
688+
self.assertEqual(overlay_layer.featureCount(), 1)
689+
690+
QgsProject.instance().addMapLayers([polygon_layer, overlay_layer])
691+
692+
old_features = [f for f in polygon_layer.getFeatures()]
693+
694+
# 'Ignore features with invalid geometries' = 1
695+
ProcessingConfig.setSettingValue(ProcessingConfig.FILTER_INVALID_GEOMETRIES, geom_option)
696+
697+
feedback = ConsoleFeedBack()
698+
context = dataobjects.createContext(feedback)
699+
context.setProject(QgsProject.instance())
700+
701+
alg = self.registry.createAlgorithmById('native:difference')
702+
self.assertIsNotNone(alg)
703+
704+
parameters = {
705+
'OVERLAY': overlay_layer,
706+
'INPUT': polygon_layer,
707+
'OUTPUT': ':memory',
708+
}
709+
710+
old_features = [f for f in polygon_layer.getFeatures()]
711+
712+
self.assertTrue(polygon_layer.startEditing())
713+
polygon_layer.selectAll()
714+
ok, _ = execute_in_place_run(
715+
alg, parameters, context=context, feedback=feedback, raise_exceptions=True)
716+
717+
new_features = [f for f in polygon_layer.getFeatures()]
718+
719+
return old_features, new_features
720+
721+
def test_difference_on_invalid_geometries(self):
722+
"""Test #20147 difference deletes invalid geometries"""
723+
724+
old_features, new_features = self._test_difference_on_invalid_geometries(1)
725+
self.assertEqual(len(new_features), 1)
726+
old_features, new_features = self._test_difference_on_invalid_geometries(0)
727+
self.assertEqual(len(new_features), 1)
728+
old_features, new_features = self._test_difference_on_invalid_geometries(2)
729+
self.assertEqual(len(new_features), 1)
730+
662731

663732
if __name__ == '__main__':
664733
unittest.main()

0 commit comments

Comments
 (0)