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