Skip to content

Commit 11aaf90

Browse files
elpasonyalldawson
authored andcommitted
Function to make output features compatible
1 parent d8d32ac commit 11aaf90

File tree

2 files changed

+149
-15
lines changed

2 files changed

+149
-15
lines changed

python/plugins/processing/gui/AlgorithmExecutor.py

Lines changed: 148 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@
3434
QgsMessageLog,
3535
QgsProcessingException,
3636
QgsProcessingFeatureSourceDefinition,
37-
QgsProcessingParameters)
37+
QgsProcessingParameters,
38+
QgsProject,
39+
QgsFeatureRequest,
40+
QgsFeature,
41+
QgsExpression,
42+
QgsWkbTypes)
3843
from processing.gui.Postprocessing import handleAlgorithmResults
3944
from processing.tools import dataobjects
4045
from qgis.utils import iface
@@ -63,40 +68,168 @@ def execute(alg, parameters, context=None, feedback=None):
6368
return False, {}
6469

6570

66-
def execute_in_place(alg, parameters, context=None, feedback=None):
71+
def make_feature_compatible(new_features, input_layer):
72+
"""Try to make new features compatible with old feature by:
73+
74+
- converting single to multi part
75+
- dropping additional attributes
76+
- adding back M/Z values
77+
78+
79+
:param new_features: new features
80+
:type new_features: list of QgsFeatures
81+
:param input_layer: input layer
82+
:type input_layer: QgsVectorLayer
83+
:return: modified features
84+
:rtype: list of QgsFeatures
85+
"""
86+
87+
result_features = []
88+
for new_f in new_features:
89+
if (new_f.geometry().wkbType() != input_layer.wkbType() and
90+
QgsWkbTypes.isMultiType(input_layer.wkbType()) and not
91+
new_f.geometry().isMultipart()):
92+
new_geom = new_f.geometry()
93+
new_geom.convertToMultiType()
94+
new_f.setGeometry(new_geom)
95+
if len(new_f.fields()) > len(input_layer.fields()):
96+
f = QgsFeature(input_layer.fields())
97+
f.setGeometry(new_f.geometry())
98+
f.setAttributes(new_f.attributes()[:len(input_layer.fields())])
99+
result_features.append(new_f)
100+
return result_features
101+
102+
103+
def execute_in_place_run(alg, parameters, context=None, feedback=None):
104+
"""Executes an algorithm modifying features in-place in the input layer.
105+
106+
The input layer must be editable or an exception is raised.
107+
108+
:param alg: algorithm to run
109+
:type alg: QgsProcessingAlgorithm
110+
:param parameters: parameters of the algorithm
111+
:type parameters: dict
112+
:param context: context, defaults to None
113+
:param context: QgsProcessingContext, optional
114+
:param feedback: feedback, defaults to None
115+
:param feedback: QgsProcessingFeedback, optional
116+
:raises QgsProcessingException: raised when the layer is not editable or the layer cannot be found in the current project
117+
:return: a tuple with true if success and results
118+
:rtype: tuple
119+
"""
67120

68121
if feedback is None:
69122
feedback = QgsProcessingFeedback()
70123
if context is None:
71124
context = dataobjects.createContext(feedback)
72125

73-
parameters['INPUT'] = QgsProcessingFeatureSourceDefinition(iface.activeLayer().id(), True)
126+
# It would be nicer to get the layer from INPUT with
127+
# alg.parameterAsVectorLayer(parameters, 'INPUT', context)
128+
# but it does not work.
129+
active_layer_id, ok = parameters['INPUT'].source.value(context.expressionContext())
130+
if ok:
131+
active_layer = QgsProject.instance().mapLayer(active_layer_id)
132+
if active_layer is None or not active_layer.isEditable():
133+
raise QgsProcessingException(tr("Layer is not editable or layer with id '%s' could not be found in the current project.") % active_layer_id)
134+
else:
135+
return False, {}
74136

75137
parameters['OUTPUT'] = 'memory:'
76138

77139
try:
78-
results, ok = alg.run(parameters, context, feedback)
140+
new_feature_ids = []
79141

80-
layer = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context)
81-
iface.activeLayer().beginEditCommand('Edit features')
82-
iface.activeLayer().deleteFeatures(iface.activeLayer().selectedFeatureIds())
83-
features = []
84-
for f in layer.getFeatures():
85-
features.append(f)
86-
iface.activeLayer().addFeatures(features)
87-
new_selection = [f.id() for f in features]
88-
iface.activeLayer().endEditCommand()
89-
#iface.activeLayer().selectByIds(new_selection)
90-
iface.activeLayer().triggerRepaint()
142+
active_layer.beginEditCommand(tr('In-place editing by %s') % alg.name())
143+
144+
req = QgsFeatureRequest(QgsExpression(r"$id < 0"))
145+
req.setFlags(QgsFeatureRequest.NoGeometry)
146+
req.setSubsetOfAttributes([])
147+
148+
# Checks whether the algorithm has a processFeature method
149+
if hasattr(alg, 'processFeature'): # in-place feature editing
150+
alg.prepare(parameters, context, feedback)
151+
field_idxs = range(len(active_layer.fields()))
152+
feature_iterator = active_layer.getFeatures(QgsFeatureRequest(active_layer.selectedFeatureIds())) if parameters['INPUT'].selectedFeaturesOnly else active_layer.getFeatures()
153+
for f in feature_iterator:
154+
new_features = alg.processFeature(f, context, feedback)
155+
new_features = make_feature_compatible(new_features, active_layer)
156+
if len(new_features) == 0:
157+
active_layer.deleteFeature(f.id())
158+
elif len(new_features) == 1:
159+
new_f = new_features[0]
160+
if not f.geometry().equals(new_f.geometry()):
161+
active_layer.changeGeometry(f.id(), new_f.geometry())
162+
if f.attributes() != new_f.attributes():
163+
active_layer.changeAttributeValues(f.id(), dict(zip(field_idxs, new_f.attributes())), dict(zip(field_idxs, f.attributes())))
164+
new_feature_ids.append(f.id())
165+
else:
166+
active_layer.deleteFeature(f.id())
167+
# Get the new ids
168+
old_ids = set([f.id() for f in active_layer.getFeatures(req)])
169+
active_layer.addFeatures(new_features)
170+
new_ids = set([f.id() for f in active_layer.getFeatures(req)])
171+
new_feature_ids += list(new_ids - old_ids)
172+
173+
results, ok = {}, True
174+
175+
else: # Traditional 'run' with delete and add features cycle
176+
results, ok = alg.run(parameters, context, feedback)
177+
178+
result_layer = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context)
179+
# TODO: check if features have changed before delete/add cycle
180+
active_layer.deleteFeatures(active_layer.selectedFeatureIds())
181+
new_features = []
182+
for f in result_layer.getFeatures():
183+
new_features.append(make_feature_compatible([f], active_layer))
184+
185+
# Get the new ids
186+
old_ids = set([f.id() for f in active_layer.getFeatures(req)])
187+
active_layer.addFeatures(new_features)
188+
new_ids = set([f.id() for f in active_layer.getFeatures(req)])
189+
new_feature_ids += list(new_ids - old_ids)
190+
191+
if ok and new_feature_ids:
192+
active_layer.selectByIds(new_feature_ids)
193+
elif not ok:
194+
active_layer.rollback()
195+
196+
active_layer.endEditCommand()
91197

92198
return ok, results
199+
93200
except QgsProcessingException as e:
94201
QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', Qgis.Critical)
95202
if feedback is not None:
96203
feedback.reportError(e.msg)
204+
97205
return False, {}
98206

99207

208+
def execute_in_place(alg, parameters, context=None, feedback=None):
209+
"""Executes an algorithm modifying features in-place in the active layer.
210+
211+
The input layer must be editable or an exception is raised.
212+
213+
:param alg: algorithm to run
214+
:type alg: QgsProcessingAlgorithm
215+
:param parameters: parameters of the algorithm
216+
:type parameters: dict
217+
:param context: context, defaults to None
218+
:param context: QgsProcessingContext, optional
219+
:param feedback: feedback, defaults to None
220+
:param feedback: QgsProcessingFeedback, optional
221+
:raises QgsProcessingException: raised when the layer is not editable or the layer cannot be found in the current project
222+
:return: a tuple with true if success and results
223+
:rtype: tuple
224+
"""
225+
226+
parameters['INPUT'] = QgsProcessingFeatureSourceDefinition(iface.activeLayer().id(), True)
227+
ok, results = execute_in_place_run(alg, parameters, context=context, feedback=feedback)
228+
if ok:
229+
iface.activeLayer().triggerRepaint()
230+
return ok, results
231+
232+
100233
def executeIterating(alg, parameters, paramToIter, context, feedback):
101234
# Generate all single-feature layers
102235
parameter_definition = alg.parameterDefinition(paramToIter)

tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ ADD_PYTHON_TEST(PyQgsPointClusterRenderer test_qgspointclusterrenderer.py)
142142
ADD_PYTHON_TEST(PyQgsPointDisplacementRenderer test_qgspointdisplacementrenderer.py)
143143
ADD_PYTHON_TEST(PyQgsPostgresDomain test_qgspostgresdomain.py)
144144
ADD_PYTHON_TEST(PyQgsProcessingRecentAlgorithmLog test_qgsprocessingrecentalgorithmslog.py)
145+
ADD_PYTHON_TEST(PyQgsProcessingInPlace test_qgsprocessinginplace.py)
145146
ADD_PYTHON_TEST(PyQgsProjectionSelectionWidgets test_qgsprojectionselectionwidgets.py)
146147
ADD_PYTHON_TEST(PyQgsProjectMetadata test_qgsprojectmetadata.py)
147148
ADD_PYTHON_TEST(PyQgsRange test_qgsrange.py)

0 commit comments

Comments
 (0)