Postprocessing.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. """
  2. ***************************************************************************
  3. Postprocessing.py
  4. ---------------------
  5. Date : August 2012
  6. Copyright : (C) 2012 by Victor Olaya
  7. Email : volayaf at gmail dot com
  8. ***************************************************************************
  9. * *
  10. * This program is free software; you can redistribute it and/or modify *
  11. * it under the terms of the GNU General Public License as published by *
  12. * the Free Software Foundation; either version 2 of the License, or *
  13. * (at your option) any later version. *
  14. * *
  15. ***************************************************************************
  16. """
  17. __author__ = 'Victor Olaya'
  18. __date__ = 'August 2012'
  19. __copyright__ = '(C) 2012, Victor Olaya'
  20. import traceback
  21. from typing import (
  22. Dict,
  23. List,
  24. Optional,
  25. Tuple
  26. )
  27. from qgis.PyQt.QtCore import QCoreApplication
  28. from qgis.core import (
  29. Qgis,
  30. QgsProcessingFeedback,
  31. QgsProcessingUtils,
  32. QgsMapLayer,
  33. QgsWkbTypes,
  34. QgsMessageLog,
  35. QgsProcessingContext,
  36. QgsProcessingAlgorithm,
  37. QgsLayerTreeLayer,
  38. QgsLayerTreeGroup
  39. )
  40. from processing.core.ProcessingConfig import ProcessingConfig
  41. from processing.gui.RenderingStyles import RenderingStyles
  42. SORT_ORDER_CUSTOM_PROPERTY = '_processing_sort_order'
  43. def determine_output_name(dest_id: str,
  44. details: QgsProcessingContext.LayerDetails,
  45. alg: QgsProcessingAlgorithm,
  46. context: QgsProcessingContext,
  47. parameters: Dict) -> str:
  48. """
  49. If running a model, the execution will arrive here when an
  50. algorithm that is part of that model is executed. We check if
  51. its output is a final output of the model, and adapt the output
  52. name accordingly
  53. """
  54. for out in alg.outputDefinitions():
  55. if out.name() not in parameters:
  56. continue
  57. output_value = parameters[out.name()]
  58. if hasattr(output_value, "sink"):
  59. output_value = output_value.sink.valueAsString(
  60. context.expressionContext()
  61. )[0]
  62. else:
  63. output_value = str(output_value)
  64. if output_value == dest_id:
  65. return out.name()
  66. return details.outputName
  67. def post_process_layer(output_name: str,
  68. layer: QgsMapLayer,
  69. alg: QgsProcessingAlgorithm):
  70. """
  71. Applies post-processing steps to a layer
  72. """
  73. style = None
  74. if output_name:
  75. style = RenderingStyles.getStyle(alg.id(), output_name)
  76. if style is None:
  77. if layer.type() == Qgis.LayerType.Raster:
  78. style = ProcessingConfig.getSetting(ProcessingConfig.RASTER_STYLE)
  79. elif layer.type() == Qgis.LayerType.Vector:
  80. if layer.geometryType() == QgsWkbTypes.PointGeometry:
  81. style = ProcessingConfig.getSetting(
  82. ProcessingConfig.VECTOR_POINT_STYLE)
  83. elif layer.geometryType() == QgsWkbTypes.LineGeometry:
  84. style = ProcessingConfig.getSetting(
  85. ProcessingConfig.VECTOR_LINE_STYLE)
  86. else:
  87. style = ProcessingConfig.getSetting(
  88. ProcessingConfig.VECTOR_POLYGON_STYLE)
  89. if style:
  90. layer.loadNamedStyle(style)
  91. try:
  92. from qgis._3d import QgsPointCloudLayer3DRenderer
  93. if layer.type() == Qgis.LayerType.PointCloud:
  94. if layer.renderer3D() is None:
  95. # If the layer has no 3D renderer and syncing 3D to 2D
  96. # renderer is enabled, we create a renderer and set it up
  97. # with the 2D renderer
  98. if layer.sync3DRendererTo2DRenderer():
  99. renderer_3d = QgsPointCloudLayer3DRenderer()
  100. renderer_3d.convertFrom2DRenderer(layer.renderer())
  101. layer.setRenderer3D(renderer_3d)
  102. except ImportError:
  103. QgsMessageLog.logMessage(
  104. QCoreApplication.translate(
  105. "Postprocessing",
  106. "3D library is not available, "
  107. "can't assign a 3d renderer to a layer."
  108. )
  109. )
  110. def create_layer_tree_layer(layer: QgsMapLayer,
  111. details: QgsProcessingContext.LayerDetails) \
  112. -> QgsLayerTreeLayer:
  113. """
  114. Applies post-processing steps to a QgsLayerTreeLayer created for
  115. an algorithm's output
  116. """
  117. layer_tree_layer = QgsLayerTreeLayer(layer)
  118. if ProcessingConfig.getSetting(ProcessingConfig.VECTOR_FEATURE_COUNT) and \
  119. layer.type() == Qgis.LayerType.Vector:
  120. layer_tree_layer.setCustomProperty("showFeatureCount", True)
  121. if details.layerSortKey:
  122. layer_tree_layer.setCustomProperty(SORT_ORDER_CUSTOM_PROPERTY,
  123. details.layerSortKey)
  124. return layer_tree_layer
  125. def get_layer_tree_results_group(details: QgsProcessingContext.LayerDetails,
  126. context: QgsProcessingContext) \
  127. -> QgsLayerTreeGroup:
  128. """
  129. Returns the destination layer tree group to store results in
  130. """
  131. destination_project = details.project or context.project()
  132. # default to placing results in the top level of the layer tree
  133. results_group = details.project.layerTreeRoot()
  134. # if a specific results group is specified in Processing settings,
  135. # respect it (and create if necessary)
  136. results_group_name = ProcessingConfig.getSetting(
  137. ProcessingConfig.RESULTS_GROUP_NAME)
  138. if results_group_name:
  139. results_group = destination_project.layerTreeRoot().findGroup(
  140. results_group_name)
  141. if not results_group:
  142. results_group = destination_project.layerTreeRoot().insertGroup(
  143. 0, results_group_name)
  144. results_group.setExpanded(True)
  145. # if this particular output layer has a specific output group assigned,
  146. # find or create it now
  147. if details.groupName:
  148. group = results_group.findGroup(details.groupName)
  149. if not group:
  150. group = results_group.insertGroup(
  151. 0, details.groupName)
  152. group.setExpanded(True)
  153. else:
  154. group = results_group
  155. return group
  156. def handleAlgorithmResults(alg: QgsProcessingAlgorithm,
  157. context: QgsProcessingContext,
  158. feedback: Optional[QgsProcessingFeedback] = None,
  159. parameters: Optional[Dict] = None):
  160. if not parameters:
  161. parameters = {}
  162. if feedback is None:
  163. feedback = QgsProcessingFeedback()
  164. wrong_layers = []
  165. feedback.setProgressText(
  166. QCoreApplication.translate(
  167. 'Postprocessing',
  168. 'Loading resulting layers'
  169. )
  170. )
  171. i = 0
  172. added_layers: List[Tuple[QgsLayerTreeGroup, QgsLayerTreeLayer]] = []
  173. layers_to_post_process: List[Tuple[QgsMapLayer,
  174. QgsProcessingContext.LayerDetails]] = []
  175. for dest_id, details in context.layersToLoadOnCompletion().items():
  176. if feedback.isCanceled():
  177. return False
  178. if len(context.layersToLoadOnCompletion()) > 2:
  179. # only show progress feedback if we're loading a bunch of layers
  180. feedback.setProgress(
  181. 100 * i / float(len(context.layersToLoadOnCompletion()))
  182. )
  183. try:
  184. layer = QgsProcessingUtils.mapLayerFromString(
  185. dest_id,
  186. context,
  187. typeHint=details.layerTypeHint
  188. )
  189. if layer is not None:
  190. details.setOutputLayerName(layer)
  191. output_name = determine_output_name(
  192. dest_id, details, alg, context, parameters
  193. )
  194. post_process_layer(output_name, layer, alg)
  195. # Load layer to layer tree root or to a specific group
  196. results_group = get_layer_tree_results_group(details, context)
  197. # note here that we may not retrieve an owned layer -- eg if the
  198. # output layer already exists in the destination project
  199. owned_map_layer = context.temporaryLayerStore().takeMapLayer(layer)
  200. if owned_map_layer:
  201. details.project.addMapLayer(owned_map_layer, False)
  202. # we don't add the layer to the tree yet -- that's done
  203. # later, after we've sorted all added layers
  204. layer_tree_layer = create_layer_tree_layer(owned_map_layer, details)
  205. added_layers.append((results_group, layer_tree_layer))
  206. if details.postProcessor():
  207. # we defer calling the postProcessor set in the context
  208. # until the layer has been added to the project's layer
  209. # tree, just in case the postProcessor contains logic
  210. # relating to layer tree handling
  211. layers_to_post_process.append((layer, details))
  212. else:
  213. wrong_layers.append(str(dest_id))
  214. except Exception:
  215. QgsMessageLog.logMessage(
  216. QCoreApplication.translate(
  217. 'Postprocessing',
  218. "Error loading result layer:"
  219. ) + "\n" + traceback.format_exc(),
  220. 'Processing',
  221. Qgis.Critical)
  222. wrong_layers.append(str(dest_id))
  223. i += 1
  224. # sort added layer tree layers
  225. sorted_layer_tree_layers = sorted(
  226. added_layers,
  227. key=lambda x: x[1].customProperty(SORT_ORDER_CUSTOM_PROPERTY, 0)
  228. )
  229. for group, layer_node in sorted_layer_tree_layers:
  230. layer_node.removeCustomProperty(SORT_ORDER_CUSTOM_PROPERTY)
  231. group.insertChildNode(0, layer_node)
  232. # all layers have been added to the layer tree, so safe to call
  233. # postProcessors now
  234. for layer, details in layers_to_post_process:
  235. details.postProcessor().postProcessLayer(
  236. layer,
  237. context,
  238. feedback)
  239. feedback.setProgress(100)
  240. if wrong_layers:
  241. msg = QCoreApplication.translate(
  242. 'Postprocessing',
  243. "The following layers were not correctly generated."
  244. )
  245. msg += "\n" + "\n".join([f"• {lay}" for lay in wrong_layers]) + '\n'
  246. msg += QCoreApplication.translate(
  247. 'Postprocessing',
  248. "You can check the 'Log Messages Panel' in QGIS main window "
  249. "to find more information about the execution of the algorithm.")
  250. feedback.reportError(msg)
  251. return len(wrong_layers) == 0