1# -*- coding: utf-8 -*-
2
3"""
4***************************************************************************
5    AlgorithmExecutor.py
6    ---------------------
7    Date                 : August 2012
8    Copyright            : (C) 2012 by Victor Olaya
9    Email                : volayaf at gmail dot com
10***************************************************************************
11*                                                                         *
12*   This program is free software; you can redistribute it and/or modify  *
13*   it under the terms of the GNU General Public License as published by  *
14*   the Free Software Foundation; either version 2 of the License, or     *
15*   (at your option) any later version.                                   *
16*                                                                         *
17***************************************************************************
18"""
19
20__author__ = 'Victor Olaya'
21__date__ = 'August 2012'
22__copyright__ = '(C) 2012, Victor Olaya'
23
24import sys
25from qgis.PyQt.QtCore import QCoreApplication
26from qgis.core import (Qgis,
27                       QgsFeatureSink,
28                       QgsProcessingFeedback,
29                       QgsProcessingUtils,
30                       QgsMessageLog,
31                       QgsProcessingException,
32                       QgsProcessingFeatureSourceDefinition,
33                       QgsProcessingFeatureSource,
34                       QgsProcessingParameters,
35                       QgsProject,
36                       QgsFeatureRequest,
37                       QgsFeature,
38                       QgsExpression,
39                       QgsWkbTypes,
40                       QgsGeometry,
41                       QgsVectorLayerUtils,
42                       QgsVectorLayer)
43from processing.gui.Postprocessing import handleAlgorithmResults
44from processing.tools import dataobjects
45from qgis.utils import iface
46
47
48def execute(alg, parameters, context=None, feedback=None, catch_exceptions=True):
49    """Executes a given algorithm, showing its progress in the
50    progress object passed along.
51
52    Return true if everything went OK, false if the algorithm
53    could not be completed.
54    """
55
56    if feedback is None:
57        feedback = QgsProcessingFeedback()
58    if context is None:
59        context = dataobjects.createContext(feedback)
60
61    if catch_exceptions:
62        try:
63            results, ok = alg.run(parameters, context, feedback)
64            return ok, results
65        except QgsProcessingException as e:
66            QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', Qgis.Critical)
67            if feedback is not None:
68                feedback.reportError(e.msg)
69            return False, {}
70    else:
71        results, ok = alg.run(parameters, context, feedback, {}, False)
72        return ok, results
73
74
75def execute_in_place_run(alg, parameters, context=None, feedback=None, raise_exceptions=False):
76    """Executes an algorithm modifying features in-place in the input layer.
77
78    :param alg: algorithm to run
79    :type alg: QgsProcessingAlgorithm
80    :param parameters: parameters of the algorithm
81    :type parameters: dict
82    :param context: context, defaults to None
83    :type context: QgsProcessingContext, optional
84    :param feedback: feedback, defaults to None
85    :type feedback: QgsProcessingFeedback, optional
86    :param raise_exceptions: useful for testing, if True exceptions are raised, normally exceptions will be forwarded to the feedback
87    :type raise_exceptions: boo, default to False
88    :raises QgsProcessingException: raised when there is no active layer, or it cannot be made editable
89    :return: a tuple with true if success and results
90    :rtype: tuple
91    """
92
93    if feedback is None:
94        feedback = QgsProcessingFeedback()
95    if context is None:
96        context = dataobjects.createContext(feedback)
97
98    # Only feature based algs have sourceFlags
99    try:
100        if alg.sourceFlags() & QgsProcessingFeatureSource.FlagSkipGeometryValidityChecks:
101            context.setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck)
102    except AttributeError:
103        pass
104
105    in_place_input_parameter_name = 'INPUT'
106    if hasattr(alg, 'inputParameterName'):
107        in_place_input_parameter_name = alg.inputParameterName()
108
109    active_layer = parameters[in_place_input_parameter_name]
110
111    # prepare expression context for feature iteration
112    alg_context = context.expressionContext()
113    alg_context.appendScope(active_layer.createExpressionContextScope())
114    context.setExpressionContext(alg_context)
115
116    # Run some checks and prepare the layer for in-place execution by:
117    # - getting the active layer and checking that it is a vector
118    # - making the layer editable if it was not already
119    # - selecting all features if none was selected
120    # - checking in-place support for the active layer/alg/parameters
121    # If one of the check fails and raise_exceptions is True an exception
122    # is raised, else the execution is aborted and the error reported in
123    # the feedback
124    try:
125        if active_layer is None:
126            raise QgsProcessingException(tr("There is not active layer."))
127
128        if not isinstance(active_layer, QgsVectorLayer):
129            raise QgsProcessingException(tr("Active layer is not a vector layer."))
130
131        if not active_layer.isEditable():
132            if not active_layer.startEditing():
133                raise QgsProcessingException(tr("Active layer is not editable (and editing could not be turned on)."))
134
135        if not alg.supportInPlaceEdit(active_layer):
136            raise QgsProcessingException(tr("Selected algorithm and parameter configuration are not compatible with in-place modifications."))
137    except QgsProcessingException as e:
138        if raise_exceptions:
139            raise e
140        QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', Qgis.Critical)
141        if feedback is not None:
142            feedback.reportError(getattr(e, 'msg', str(e)), fatalError=True)
143        return False, {}
144
145    if not active_layer.selectedFeatureIds():
146        active_layer.selectAll()
147
148    # Make sure we are working on selected features only
149    parameters[in_place_input_parameter_name] = QgsProcessingFeatureSourceDefinition(active_layer.id(), True)
150    parameters['OUTPUT'] = 'memory:'
151
152    req = QgsFeatureRequest(QgsExpression(r"$id < 0"))
153    req.setFlags(QgsFeatureRequest.NoGeometry)
154    req.setSubsetOfAttributes([])
155
156    # Start the execution
157    # If anything goes wrong and raise_exceptions is True an exception
158    # is raised, else the execution is aborted and the error reported in
159    # the feedback
160    try:
161        new_feature_ids = []
162
163        active_layer.beginEditCommand(alg.displayName())
164
165        # Checks whether the algorithm has a processFeature method
166        if hasattr(alg, 'processFeature'):  # in-place feature editing
167            # Make a clone or it will crash the second time the dialog
168            # is opened and run
169            alg = alg.create({'IN_PLACE': True})
170            if not alg.prepare(parameters, context, feedback):
171                raise QgsProcessingException(tr("Could not prepare selected algorithm."))
172            # Check again for compatibility after prepare
173            if not alg.supportInPlaceEdit(active_layer):
174                raise QgsProcessingException(tr("Selected algorithm and parameter configuration are not compatible with in-place modifications."))
175
176            # some algorithms have logic in outputFields/outputCrs/outputWkbType which they require to execute before
177            # they can start processing features
178            _ = alg.outputFields(active_layer.fields())
179            _ = alg.outputWkbType(active_layer.wkbType())
180            _ = alg.outputCrs(active_layer.crs())
181
182            field_idxs = range(len(active_layer.fields()))
183            iterator_req = QgsFeatureRequest(active_layer.selectedFeatureIds())
184            iterator_req.setInvalidGeometryCheck(context.invalidGeometryCheck())
185            feature_iterator = active_layer.getFeatures(iterator_req)
186            step = 100 / len(active_layer.selectedFeatureIds()) if active_layer.selectedFeatureIds() else 1
187            current = 0
188            for current, f in enumerate(feature_iterator):
189                if feedback.isCanceled():
190                    break
191
192                # need a deep copy, because python processFeature implementations may return
193                # a shallow copy from processFeature
194                input_feature = QgsFeature(f)
195
196                context.expressionContext().setFeature(input_feature)
197
198                new_features = alg.processFeature(input_feature, context, feedback)
199                new_features = QgsVectorLayerUtils.makeFeaturesCompatible(new_features, active_layer)
200
201                if len(new_features) == 0:
202                    active_layer.deleteFeature(f.id())
203                elif len(new_features) == 1:
204                    new_f = new_features[0]
205                    if not f.geometry().equals(new_f.geometry()):
206                        active_layer.changeGeometry(f.id(), new_f.geometry())
207                    if f.attributes() != new_f.attributes():
208                        active_layer.changeAttributeValues(f.id(), dict(zip(field_idxs, new_f.attributes())), dict(zip(field_idxs, f.attributes())))
209                    new_feature_ids.append(f.id())
210                else:
211                    active_layer.deleteFeature(f.id())
212                    # Get the new ids
213                    old_ids = set([f.id() for f in active_layer.getFeatures(req)])
214                    # If multiple new features were created, we need to pass
215                    # them to createFeatures to manage constraints correctly
216                    features_data = []
217                    for f in new_features:
218                        features_data.append(QgsVectorLayerUtils.QgsFeatureData(f.geometry(), dict(enumerate(f.attributes()))))
219                    new_features = QgsVectorLayerUtils.createFeatures(active_layer, features_data, context.expressionContext())
220                    if not active_layer.addFeatures(new_features):
221                        raise QgsProcessingException(tr("Error adding processed features back into the layer."))
222                    new_ids = set([f.id() for f in active_layer.getFeatures(req)])
223                    new_feature_ids += list(new_ids - old_ids)
224
225                feedback.setProgress(int((current + 1) * step))
226
227            results, ok = {'__count': current + 1}, True
228
229        else:  # Traditional 'run' with delete and add features cycle
230
231            # There is no way to know if some features have been skipped
232            # due to invalid geometries
233            if context.invalidGeometryCheck() == QgsFeatureRequest.GeometrySkipInvalid:
234                selected_ids = active_layer.selectedFeatureIds()
235            else:
236                selected_ids = []
237
238            results, ok = alg.run(parameters, context, feedback, configuration={'IN_PLACE': True})
239
240            if ok:
241                result_layer = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context)
242                # TODO: check if features have changed before delete/add cycle
243
244                new_features = []
245
246                # Check if there are any skipped features
247                if context.invalidGeometryCheck() == QgsFeatureRequest.GeometrySkipInvalid:
248                    missing_ids = list(set(selected_ids) - set(result_layer.allFeatureIds()))
249                    if missing_ids:
250                        for f in active_layer.getFeatures(QgsFeatureRequest(missing_ids)):
251                            if not f.geometry().isGeosValid():
252                                new_features.append(f)
253
254                active_layer.deleteFeatures(active_layer.selectedFeatureIds())
255
256                regenerate_primary_key = result_layer.customProperty('OnConvertFormatRegeneratePrimaryKey', False)
257                sink_flags = QgsFeatureSink.SinkFlags(QgsFeatureSink.RegeneratePrimaryKey) if regenerate_primary_key \
258                    else QgsFeatureSink.SinkFlags()
259
260                for f in result_layer.getFeatures():
261                    new_features.extend(QgsVectorLayerUtils.
262                                        makeFeaturesCompatible([f], active_layer, sink_flags))
263
264                # Get the new ids
265                old_ids = set([f.id() for f in active_layer.getFeatures(req)])
266                if not active_layer.addFeatures(new_features):
267                    raise QgsProcessingException(tr("Error adding processed features back into the layer."))
268                new_ids = set([f.id() for f in active_layer.getFeatures(req)])
269                new_feature_ids += list(new_ids - old_ids)
270                results['__count'] = len(new_feature_ids)
271
272        active_layer.endEditCommand()
273
274        if ok and new_feature_ids:
275            active_layer.selectByIds(new_feature_ids)
276        elif not ok:
277            active_layer.rollBack()
278
279        return ok, results
280
281    except QgsProcessingException as e:
282        active_layer.endEditCommand()
283        active_layer.rollBack()
284        if raise_exceptions:
285            raise e
286        QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', Qgis.Critical)
287        if feedback is not None:
288            feedback.reportError(getattr(e, 'msg', str(e)), fatalError=True)
289
290    return False, {}
291
292
293def execute_in_place(alg, parameters, context=None, feedback=None):
294    """Executes an algorithm modifying features in-place, if the INPUT
295    parameter is not defined, the current active layer will be used as
296    INPUT.
297
298    :param alg: algorithm to run
299    :type alg: QgsProcessingAlgorithm
300    :param parameters: parameters of the algorithm
301    :type parameters: dict
302    :param context: context, defaults to None
303    :param context: QgsProcessingContext, optional
304    :param feedback: feedback, defaults to None
305    :param feedback: QgsProcessingFeedback, optional
306    :raises QgsProcessingException: raised when the layer is not editable or the layer cannot be found in the current project
307    :return: a tuple with true if success and results
308    :rtype: tuple
309    """
310
311    if feedback is None:
312        feedback = QgsProcessingFeedback()
313    if context is None:
314        context = dataobjects.createContext(feedback)
315
316    in_place_input_parameter_name = 'INPUT'
317    if hasattr(alg, 'inputParameterName'):
318        in_place_input_parameter_name = alg.inputParameterName()
319    in_place_input_layer_name = 'INPUT'
320    if hasattr(alg, 'inputParameterDescription'):
321        in_place_input_layer_name = alg.inputParameterDescription()
322
323    if in_place_input_parameter_name not in parameters or not parameters[in_place_input_parameter_name]:
324        parameters[in_place_input_parameter_name] = iface.activeLayer()
325    ok, results = execute_in_place_run(alg, parameters, context=context, feedback=feedback)
326    if ok:
327        if isinstance(parameters[in_place_input_parameter_name], QgsProcessingFeatureSourceDefinition):
328            layer = alg.parameterAsVectorLayer({in_place_input_parameter_name: parameters[in_place_input_parameter_name].source}, in_place_input_layer_name, context)
329        elif isinstance(parameters[in_place_input_parameter_name], QgsVectorLayer):
330            layer = parameters[in_place_input_parameter_name]
331        if layer:
332            layer.triggerRepaint()
333    return ok, results
334
335
336def executeIterating(alg, parameters, paramToIter, context, feedback):
337    # Generate all single-feature layers
338    parameter_definition = alg.parameterDefinition(paramToIter)
339    if not parameter_definition:
340        return False
341
342    iter_source = QgsProcessingParameters.parameterAsSource(parameter_definition, parameters, context)
343    sink_list = []
344    if iter_source.featureCount() == 0:
345        return False
346
347    step = 100.0 / iter_source.featureCount()
348    for current, feat in enumerate(iter_source.getFeatures()):
349        if feedback.isCanceled():
350            return False
351
352        sink, sink_id = QgsProcessingUtils.createFeatureSink('memory:', context, iter_source.fields(), iter_source.wkbType(), iter_source.sourceCrs())
353        sink_list.append(sink_id)
354        sink.addFeature(feat, QgsFeatureSink.FastInsert)
355        del sink
356
357        feedback.setProgress(int((current + 1) * step))
358
359    # store output values to use them later as basenames for all outputs
360    outputs = {}
361    for out in alg.destinationParameterDefinitions():
362        if out.name() in parameters:
363            outputs[out.name()] = parameters[out.name()]
364
365    # now run all the algorithms
366    for i, f in enumerate(sink_list):
367        if feedback.isCanceled():
368            return False
369
370        parameters[paramToIter] = f
371        for out in alg.destinationParameterDefinitions():
372            if out.name() not in outputs:
373                continue
374
375            o = outputs[out.name()]
376            parameters[out.name()] = QgsProcessingUtils.generateIteratingDestination(o, i, context)
377        feedback.setProgressText(QCoreApplication.translate('AlgorithmExecutor', 'Executing iteration {0}/{1}…').format(i + 1, len(sink_list)))
378        feedback.setProgress(int((i + 1) * 100 / len(sink_list)))
379        ret, results = execute(alg, parameters, context, feedback)
380        if not ret:
381            return False
382
383    handleAlgorithmResults(alg, context, feedback, False)
384    return True
385
386
387def tr(string, context=''):
388    if context == '':
389        context = 'AlgorithmExecutor'
390    return QCoreApplication.translate(context, string)
391