RasterCalculator.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. """
  2. ***************************************************************************
  3. RasterLayerCalculator.py
  4. ---------------------
  5. Date : November 2016
  6. Copyright : (C) 2016 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__ = 'November 2016'
  19. __copyright__ = '(C) 2016, Victor Olaya'
  20. import os
  21. import math
  22. from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
  23. from processing.algs.gdal.GdalUtils import GdalUtils
  24. from qgis.core import (QgsProcessing,
  25. QgsProcessingAlgorithm,
  26. QgsProcessingException,
  27. QgsProcessingUtils,
  28. QgsProcessingParameterCrs,
  29. QgsProcessingParameterMultipleLayers,
  30. QgsProcessingParameterNumber,
  31. QgsProcessingParameterExtent,
  32. QgsProcessingParameterRasterDestination,
  33. QgsProcessingParameterRasterLayer,
  34. QgsProcessingOutputRasterLayer,
  35. QgsProcessingParameterString,
  36. QgsCoordinateTransform,
  37. QgsMapLayer)
  38. from qgis.PyQt.QtCore import QObject
  39. from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry
  40. class RasterCalculator(QgisAlgorithm):
  41. LAYERS = 'LAYERS'
  42. EXTENT = 'EXTENT'
  43. CELLSIZE = 'CELLSIZE'
  44. EXPRESSION = 'EXPRESSION'
  45. CRS = 'CRS'
  46. OUTPUT = 'OUTPUT'
  47. def group(self):
  48. return self.tr('Raster analysis')
  49. def groupId(self):
  50. return 'rasteranalysis'
  51. def __init__(self):
  52. super().__init__()
  53. def initAlgorithm(self, config=None):
  54. class ParameterRasterCalculatorExpression(QgsProcessingParameterString):
  55. def __init__(self, name='', description='', multiLine=False):
  56. super().__init__(name, description, multiLine=multiLine)
  57. self.setMetadata({
  58. 'widget_wrapper': 'processing.algs.qgis.ui.RasterCalculatorWidgets.ExpressionWidgetWrapper'
  59. })
  60. def type(self):
  61. return 'raster_calc_expression'
  62. def clone(self):
  63. return ParameterRasterCalculatorExpression(self.name(), self.description(), self.multiLine())
  64. self.addParameter(ParameterRasterCalculatorExpression(self.EXPRESSION, self.tr('Expression'),
  65. multiLine=True))
  66. self.addParameter(QgsProcessingParameterMultipleLayers(self.LAYERS,
  67. self.tr('Reference layer(s) (used for automated extent, cellsize, and CRS)'),
  68. layerType=QgsProcessing.TypeRaster,
  69. optional=True))
  70. self.addParameter(QgsProcessingParameterNumber(self.CELLSIZE,
  71. self.tr('Cell size (use 0 or empty to set it automatically)'),
  72. type=QgsProcessingParameterNumber.Double,
  73. minValue=0.0, defaultValue=0.0, optional=True))
  74. self.addParameter(QgsProcessingParameterExtent(self.EXTENT,
  75. self.tr('Output extent'),
  76. optional=True))
  77. self.addParameter(QgsProcessingParameterCrs(self.CRS, 'Output CRS', optional=True))
  78. self.addParameter(QgsProcessingParameterRasterDestination(self.OUTPUT, self.tr('Output')))
  79. def flags(self):
  80. return super().flags() | QgsProcessingAlgorithm.FlagDeprecated
  81. def name(self):
  82. return 'rastercalculator'
  83. def displayName(self):
  84. return self.tr('Raster calculator')
  85. def processAlgorithm(self, parameters, context, feedback):
  86. expression = self.parameterAsString(parameters, self.EXPRESSION, context)
  87. layers = self.parameterAsLayerList(parameters, self.LAYERS, context)
  88. layersDict = {}
  89. if layers:
  90. layersDict = {lyr.source(): lyr for lyr in layers}
  91. crs = self.parameterAsCrs(parameters, self.CRS, context)
  92. if crs is None or not crs.isValid():
  93. if not layers:
  94. raise QgsProcessingException(self.tr("No reference layer selected nor CRS provided"))
  95. else:
  96. crs = list(layersDict.values())[0].crs()
  97. bbox = self.parameterAsExtent(parameters, self.EXTENT, context)
  98. if bbox.isNull() and not layers:
  99. raise QgsProcessingException(self.tr("No reference layer selected nor extent box provided"))
  100. if not bbox.isNull():
  101. bboxCrs = self.parameterAsExtentCrs(parameters, self.EXTENT, context)
  102. if bboxCrs != crs:
  103. transform = QgsCoordinateTransform(bboxCrs, crs, context.transformContext())
  104. bbox = transform.transformBoundingBox(bbox)
  105. if bbox.isNull() and layers:
  106. bbox = QgsProcessingUtils.combineLayerExtents(layers, crs, context)
  107. cellsize = self.parameterAsDouble(parameters, self.CELLSIZE, context)
  108. if cellsize == 0 and not layers:
  109. raise QgsProcessingException(self.tr("No reference layer selected nor cellsize value provided"))
  110. def _cellsize(layer):
  111. ext = layer.extent()
  112. if layer.crs() != crs:
  113. transform = QgsCoordinateTransform(layer.crs(), crs, context.transformContext())
  114. ext = transform.transformBoundingBox(ext)
  115. return (ext.xMaximum() - ext.xMinimum()) / layer.width()
  116. if cellsize == 0:
  117. cellsize = min([_cellsize(lyr) for lyr in layersDict.values()])
  118. # check for layers available in the model
  119. layersDictCopy = layersDict.copy() # need a shallow copy because next calls invalidate iterator
  120. for lyr in layersDictCopy.values():
  121. expression = self.mappedNameToLayer(lyr, expression, layersDict, context)
  122. # check for layers available in the project
  123. if context.project():
  124. for lyr in QgsProcessingUtils.compatibleRasterLayers(context.project()):
  125. expression = self.mappedNameToLayer(lyr, expression, layersDict, context)
  126. # create the list of layers to be passed as inputs to RasterCalculaltor
  127. # at this phase expression has been modified to match available layers
  128. # in the current scope
  129. entries = []
  130. for name, lyr in layersDict.items():
  131. for n in range(lyr.bandCount()):
  132. ref = f'{name:s}@{n + 1:d}'
  133. if ref in expression:
  134. entry = QgsRasterCalculatorEntry()
  135. entry.ref = ref
  136. entry.raster = lyr
  137. entry.bandNumber = n + 1
  138. entries.append(entry)
  139. # Append any missing entry from the current project
  140. for entry in QgsRasterCalculatorEntry.rasterEntries():
  141. if not [e for e in entries if e.ref == entry.ref]:
  142. entries.append(entry)
  143. output = self.parameterAsOutputLayer(parameters, self.OUTPUT, context)
  144. width = round((bbox.xMaximum() - bbox.xMinimum()) / cellsize)
  145. height = round((bbox.yMaximum() - bbox.yMinimum()) / cellsize)
  146. driverName = GdalUtils.getFormatShortNameFromFilename(output)
  147. calc = QgsRasterCalculator(expression,
  148. output,
  149. driverName,
  150. bbox,
  151. crs,
  152. width,
  153. height,
  154. entries,
  155. context.transformContext())
  156. res = calc.processCalculation(feedback)
  157. if res == QgsRasterCalculator.ParserError:
  158. raise QgsProcessingException(self.tr("Error parsing formula"))
  159. elif res == QgsRasterCalculator.CalculationError:
  160. raise QgsProcessingException(self.tr("An error occurred while performing the calculation"))
  161. return {self.OUTPUT: output}
  162. def mappedNameToLayer(self, lyr, expression, layersDict, context):
  163. '''Try to identify if a real layer is mapped in the expression with a symbolic name.'''
  164. nameToMap = lyr.source()
  165. # check if nameToMap is a file
  166. # TODO: what about URI eg for a COG?
  167. if os.path.isfile(nameToMap):
  168. # get only the name without extension and path of the file
  169. nameToMap = os.path.splitext(os.path.basename(nameToMap))[0]
  170. # check for layers directly added in the expression
  171. if (nameToMap + "@") in expression:
  172. layersDict[nameToMap] = lyr
  173. # get "algorithm_inputs" scope of the expressionContext related
  174. # with mapped variables
  175. indexOfScope = context.expressionContext().indexOfScope("algorithm_inputs")
  176. if indexOfScope >= 0:
  177. expContextAlgInputsScope = context.expressionContext().scope(indexOfScope)
  178. # check for the layers that are mapped as input in a model
  179. # to do this check in the latest scope all passed variables
  180. # to look for a variable that is a layer or a string filename
  181. # to a layer
  182. varDescription = None
  183. for varName in expContextAlgInputsScope.variableNames():
  184. layerInContext = expContextAlgInputsScope.variable(varName)
  185. if not isinstance(layerInContext, str) and not isinstance(layerInContext, QgsMapLayer):
  186. continue
  187. if isinstance(layerInContext, QgsMapLayer) and nameToMap not in layerInContext.source():
  188. continue
  189. varDescription = expContextAlgInputsScope.description(varName)
  190. # because there can be variable with None or "" description
  191. # then skip them
  192. if not varDescription:
  193. continue
  194. # check if it's description starts with Output as in:
  195. # Output 'Output' from algorithm 'calc1'
  196. # as set in https://github.com/qgis/QGIS/blob/master/src/core/processing/models/qgsprocessingmodelalgorithm.cpp#L516
  197. # but var in expression is called simply
  198. # 'Output' from algorithm 'calc1'
  199. # get the translation string to use to parse the description
  200. # HAVE to use the same translated string as in
  201. # https://github.com/qgis/QGIS/blob/master/src/core/processing/models/qgsprocessingmodelalgorithm.cpp#L516
  202. translatedDesc = self.tr("Output '%1' from algorithm '%2'")
  203. elementZero = translatedDesc.split(" ")[0] # For english the string result should be "Output"
  204. elements = varDescription.split(" ")
  205. if len(elements) > 1 and elements[0] == elementZero:
  206. # remove heading QObject.tr"Output ") string. Note adding a space at the end of elementZero!
  207. varDescription = varDescription[len(elementZero) + 1:]
  208. # check if cleaned varDescription is present in the expression
  209. # if not skip it
  210. if (varDescription + "@") not in expression:
  211. continue
  212. # !!!found!!! => substitute in expression
  213. # and add in the list of layers that will be passed to raster calculator
  214. nameToMap = varName
  215. new = f"{nameToMap}@"
  216. old = f"{varDescription}@"
  217. expression = expression.replace(old, new)
  218. layersDict[nameToMap] = lyr
  219. # need return the modified expression because it's not a reference
  220. return expression