QGIS/python/plugins/processing/gui/AlgorithmExecutor.py

498 lines
19 KiB
Python

"""
***************************************************************************
AlgorithmExecutor.py
---------------------
Date : August 2012
Copyright : (C) 2012 by Victor Olaya
Email : volayaf at gmail dot com
***************************************************************************
* *
* 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__ = "Victor Olaya"
__date__ = "August 2012"
__copyright__ = "(C) 2012, Victor Olaya"
import sys
from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (
Qgis,
QgsApplication,
QgsFeatureSink,
QgsProcessingFeedback,
QgsProcessingUtils,
QgsMessageLog,
QgsProcessingException,
QgsProcessingFeatureSourceDefinition,
QgsProcessingFeatureSource,
QgsProcessingParameters,
QgsProject,
QgsFeatureRequest,
QgsFeature,
QgsExpression,
QgsWkbTypes,
QgsGeometry,
QgsVectorLayerUtils,
QgsVectorLayer,
)
from processing.gui.Postprocessing import handleAlgorithmResults
from processing.tools import dataobjects
from qgis.utils import iface
def execute(alg, parameters, context=None, feedback=None, catch_exceptions=True):
"""Executes a given algorithm, showing its progress in the
progress object passed along.
Return true if everything went OK, false if the algorithm
could not be completed.
"""
if feedback is None:
feedback = QgsProcessingFeedback()
if context is None:
context = dataobjects.createContext(feedback)
if catch_exceptions:
try:
results, ok = alg.run(parameters, context, feedback)
return ok, results
except QgsProcessingException as e:
QgsMessageLog.logMessage(
str(sys.exc_info()[0]), "Processing", Qgis.MessageLevel.Critical
)
if feedback is not None:
feedback.reportError(e.msg)
return False, {}
else:
results, ok = alg.run(parameters, context, feedback, {}, False)
return ok, results
def execute_in_place_run(
alg, parameters, context=None, feedback=None, raise_exceptions=False
):
"""Executes an algorithm modifying features in-place in the input layer.
:param alg: algorithm to run
:type alg: QgsProcessingAlgorithm
:param parameters: parameters of the algorithm
:type parameters: dict
:param context: context, defaults to None
:type context: QgsProcessingContext, optional
:param feedback: feedback, defaults to None
:type feedback: QgsProcessingFeedback, optional
:param raise_exceptions: useful for testing, if True exceptions are raised, normally exceptions will be forwarded to the feedback
:type raise_exceptions: boo, default to False
:raises QgsProcessingException: raised when there is no active layer, or it cannot be made editable
:return: a tuple with true if success and results
:rtype: tuple
"""
if feedback is None:
feedback = QgsProcessingFeedback()
if context is None:
context = dataobjects.createContext(feedback)
# Only feature based algs have sourceFlags
try:
if (
alg.sourceFlags()
& QgsProcessingFeatureSource.Flag.FlagSkipGeometryValidityChecks
):
context.setInvalidGeometryCheck(
QgsFeatureRequest.InvalidGeometryCheck.GeometryNoCheck
)
except AttributeError:
pass
in_place_input_parameter_name = "INPUT"
if hasattr(alg, "inputParameterName"):
in_place_input_parameter_name = alg.inputParameterName()
active_layer = parameters[in_place_input_parameter_name]
# prepare expression context for feature iteration
alg_context = context.expressionContext()
alg_context.appendScope(active_layer.createExpressionContextScope())
context.setExpressionContext(alg_context)
# Run some checks and prepare the layer for in-place execution by:
# - getting the active layer and checking that it is a vector
# - making the layer editable if it was not already
# - selecting all features if none was selected
# - checking in-place support for the active layer/alg/parameters
# If one of the check fails and raise_exceptions is True an exception
# is raised, else the execution is aborted and the error reported in
# the feedback
try:
if active_layer is None:
raise QgsProcessingException(tr("There is no active layer."))
if not isinstance(active_layer, QgsVectorLayer):
raise QgsProcessingException(tr("Active layer is not a vector layer."))
if not active_layer.isEditable():
if not active_layer.startEditing():
raise QgsProcessingException(
tr(
"Active layer is not editable (and editing could not be turned on)."
)
)
if not alg.supportInPlaceEdit(active_layer):
raise QgsProcessingException(
tr(
"Selected algorithm and parameter configuration are not compatible with in-place modifications."
)
)
except QgsProcessingException as e:
if raise_exceptions:
raise e
QgsMessageLog.logMessage(
str(sys.exc_info()[0]), "Processing", Qgis.MessageLevel.Critical
)
if feedback is not None:
feedback.reportError(getattr(e, "msg", str(e)), fatalError=True)
return False, {}
if not active_layer.selectedFeatureIds():
active_layer.selectAll()
# Make sure we are working on selected features only
parameters[in_place_input_parameter_name] = QgsProcessingFeatureSourceDefinition(
active_layer.id(), True
)
parameters["OUTPUT"] = "memory:"
req = QgsFeatureRequest(QgsExpression(r"$id < 0"))
req.setFlags(QgsFeatureRequest.Flag.NoGeometry)
req.setSubsetOfAttributes([])
# Start the execution
# If anything goes wrong and raise_exceptions is True an exception
# is raised, else the execution is aborted and the error reported in
# the feedback
try:
new_feature_ids = []
active_layer.beginEditCommand(alg.displayName())
# Checks whether the algorithm has a processFeature method
if hasattr(alg, "processFeature"): # in-place feature editing
# Make a clone or it will crash the second time the dialog
# is opened and run
alg = alg.create({"IN_PLACE": True})
if not alg.prepare(parameters, context, feedback):
raise QgsProcessingException(
tr("Could not prepare selected algorithm.")
)
# Check again for compatibility after prepare
if not alg.supportInPlaceEdit(active_layer):
raise QgsProcessingException(
tr(
"Selected algorithm and parameter configuration are not compatible with in-place modifications."
)
)
# some algorithms have logic in outputFields/outputCrs/outputWkbType which they require to execute before
# they can start processing features
_ = alg.outputFields(active_layer.fields())
_ = alg.outputWkbType(active_layer.wkbType())
_ = alg.outputCrs(active_layer.crs())
field_idxs = range(len(active_layer.fields()))
iterator_req = QgsFeatureRequest(active_layer.selectedFeatureIds())
iterator_req.setInvalidGeometryCheck(context.invalidGeometryCheck())
feature_iterator = active_layer.getFeatures(iterator_req)
step = (
100 / len(active_layer.selectedFeatureIds())
if active_layer.selectedFeatureIds()
else 1
)
current = 0
for current, f in enumerate(feature_iterator):
if feedback.isCanceled():
break
# need a deep copy, because python processFeature implementations may return
# a shallow copy from processFeature
input_feature = QgsFeature(f)
context.expressionContext().setFeature(input_feature)
new_features = alg.processFeature(input_feature, context, feedback)
new_features = QgsVectorLayerUtils.makeFeaturesCompatible(
new_features, active_layer
)
if len(new_features) == 0:
active_layer.deleteFeature(f.id())
elif len(new_features) == 1:
new_f = new_features[0]
if not f.geometry().equals(new_f.geometry()):
active_layer.changeGeometry(f.id(), new_f.geometry())
if f.attributes() != new_f.attributes():
active_layer.changeAttributeValues(
f.id(),
dict(zip(field_idxs, new_f.attributes())),
dict(zip(field_idxs, f.attributes())),
)
new_feature_ids.append(f.id())
else:
active_layer.deleteFeature(f.id())
# Get the new ids
old_ids = {f.id() for f in active_layer.getFeatures(req)}
# If multiple new features were created, we need to pass
# them to createFeatures to manage constraints correctly
features_data = []
for f in new_features:
features_data.append(
QgsVectorLayerUtils.QgsFeatureData(
f.geometry(), dict(enumerate(f.attributes()))
)
)
new_features = QgsVectorLayerUtils.createFeatures(
active_layer, features_data, context.expressionContext()
)
if not active_layer.addFeatures(new_features):
raise QgsProcessingException(
tr("Error adding processed features back into the layer.")
)
new_ids = {f.id() for f in active_layer.getFeatures(req)}
new_feature_ids += list(new_ids - old_ids)
feedback.setProgress(int((current + 1) * step))
results, ok = {"__count": current + 1}, True
else: # Traditional 'run' with delete and add features cycle
# There is no way to know if some features have been skipped
# due to invalid geometries
if (
context.invalidGeometryCheck()
== QgsFeatureRequest.InvalidGeometryCheck.GeometrySkipInvalid
):
selected_ids = active_layer.selectedFeatureIds()
else:
selected_ids = []
results, ok = alg.run(
parameters, context, feedback, configuration={"IN_PLACE": True}
)
if ok:
result_layer = QgsProcessingUtils.mapLayerFromString(
results["OUTPUT"], context
)
# TODO: check if features have changed before delete/add cycle
new_features = []
# Check if there are any skipped features
if (
context.invalidGeometryCheck()
== QgsFeatureRequest.InvalidGeometryCheck.GeometrySkipInvalid
):
missing_ids = list(
set(selected_ids) - set(result_layer.allFeatureIds())
)
if missing_ids:
for f in active_layer.getFeatures(
QgsFeatureRequest(missing_ids)
):
if not f.geometry().isGeosValid():
new_features.append(f)
active_layer.deleteFeatures(active_layer.selectedFeatureIds())
regenerate_primary_key = result_layer.customProperty(
"OnConvertFormatRegeneratePrimaryKey", False
)
sink_flags = (
QgsFeatureSink.SinkFlags(
QgsFeatureSink.SinkFlag.RegeneratePrimaryKey
)
if regenerate_primary_key
else QgsFeatureSink.SinkFlags()
)
for f in result_layer.getFeatures():
new_features.extend(
QgsVectorLayerUtils.makeFeaturesCompatible(
[f], active_layer, sink_flags
)
)
# Get the new ids
old_ids = {f.id() for f in active_layer.getFeatures(req)}
if not active_layer.addFeatures(new_features):
raise QgsProcessingException(
tr("Error adding processed features back into the layer.")
)
new_ids = {f.id() for f in active_layer.getFeatures(req)}
new_feature_ids += list(new_ids - old_ids)
results["__count"] = len(new_feature_ids)
active_layer.endEditCommand()
if ok and new_feature_ids:
active_layer.selectByIds(new_feature_ids)
elif not ok:
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.MessageLevel.Critical
)
if feedback is not None:
feedback.reportError(getattr(e, "msg", str(e)), fatalError=True)
return False, {}
def execute_in_place(alg, parameters, context=None, feedback=None):
"""Executes an algorithm modifying features in-place, if the INPUT
parameter is not defined, the current active layer will be used as
INPUT.
:param alg: algorithm to run
:type alg: QgsProcessingAlgorithm
:param parameters: parameters of the algorithm
:type parameters: dict
:param context: context, defaults to None
:param context: QgsProcessingContext, optional
:param feedback: feedback, defaults to None
:param feedback: QgsProcessingFeedback, optional
:raises QgsProcessingException: raised when the layer is not editable or the layer cannot be found in the current project
:return: a tuple with true if success and results
:rtype: tuple
"""
if feedback is None:
feedback = QgsProcessingFeedback()
if context is None:
context = dataobjects.createContext(feedback)
in_place_input_parameter_name = "INPUT"
if hasattr(alg, "inputParameterName"):
in_place_input_parameter_name = alg.inputParameterName()
in_place_input_layer_name = "INPUT"
if hasattr(alg, "inputParameterDescription"):
in_place_input_layer_name = alg.inputParameterDescription()
if (
in_place_input_parameter_name not in parameters
or not parameters[in_place_input_parameter_name]
):
parameters[in_place_input_parameter_name] = iface.activeLayer()
ok, results = execute_in_place_run(
alg, parameters, context=context, feedback=feedback
)
if ok:
if isinstance(
parameters[in_place_input_parameter_name],
QgsProcessingFeatureSourceDefinition,
):
layer = alg.parameterAsVectorLayer(
{
in_place_input_parameter_name: parameters[
in_place_input_parameter_name
].source
},
in_place_input_layer_name,
context,
)
elif isinstance(parameters[in_place_input_parameter_name], QgsVectorLayer):
layer = parameters[in_place_input_parameter_name]
if layer:
layer.triggerRepaint()
return ok, results
def executeIterating(alg, parameters, paramToIter, context, feedback):
# Generate all single-feature layers
parameter_definition = alg.parameterDefinition(paramToIter)
if not parameter_definition:
return False
iter_source = QgsProcessingParameters.parameterAsSource(
parameter_definition, parameters, context
)
sink_list = []
if iter_source.featureCount() == 0:
return False
step = 100.0 / iter_source.featureCount()
for current, feat in enumerate(iter_source.getFeatures()):
if feedback.isCanceled():
return False
sink, sink_id = QgsProcessingUtils.createFeatureSink(
"memory:",
context,
iter_source.fields(),
iter_source.wkbType(),
iter_source.sourceCrs(),
)
sink_list.append(sink_id)
sink.addFeature(feat, QgsFeatureSink.Flag.FastInsert)
del sink
feedback.setProgress(int((current + 1) * step))
# store output values to use them later as basenames for all outputs
outputs = {}
for out in alg.destinationParameterDefinitions():
if out.name() in parameters:
outputs[out.name()] = parameters[out.name()]
# now run all the algorithms
for i, f in enumerate(sink_list):
if feedback.isCanceled():
return False
# clear any model result stored in the last iteration
context.clearModelResult()
parameters[paramToIter] = f
for out in alg.destinationParameterDefinitions():
if out.name() not in outputs:
continue
o = outputs[out.name()]
parameters[out.name()] = QgsProcessingUtils.generateIteratingDestination(
o, i, context
)
feedback.setProgressText(
QCoreApplication.translate(
"AlgorithmExecutor", "Executing iteration {0}/{1}"
).format(i + 1, len(sink_list))
)
feedback.setProgress(int((i + 1) * 100 / len(sink_list)))
ret, results = execute(alg, parameters, context, feedback)
if not ret:
return False
handleAlgorithmResults(alg, context, feedback)
return True
def tr(string, context=""):
if context == "":
context = "AlgorithmExecutor"
return QCoreApplication.translate(context, string)