gdalcalc.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. """
  2. ***************************************************************************
  3. gdalcalc.py
  4. ---------------------
  5. Date : Janaury 2015
  6. Copyright : (C) 2015 by Giovanni Manghi
  7. Email : giovanni dot manghi at naturalgis dot pt
  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__ = 'Giovanni Manghi'
  18. __date__ = 'January 2015'
  19. __copyright__ = '(C) 2015, Giovanni Manghi'
  20. from qgis.core import (QgsProcessingException,
  21. QgsProcessingParameterDefinition,
  22. QgsProcessingParameterRasterLayer,
  23. QgsProcessingParameterBand,
  24. QgsProcessingParameterNumber,
  25. QgsProcessingParameterEnum,
  26. QgsProcessingParameterExtent,
  27. QgsProcessingParameterString,
  28. QgsProcessingParameterRasterDestination)
  29. from processing.algs.gdal.GdalAlgorithm import GdalAlgorithm
  30. from processing.algs.gdal.GdalUtils import GdalUtils
  31. from processing.tools.system import isWindows
  32. class gdalcalc(GdalAlgorithm):
  33. INPUT_A = 'INPUT_A'
  34. INPUT_B = 'INPUT_B'
  35. INPUT_C = 'INPUT_C'
  36. INPUT_D = 'INPUT_D'
  37. INPUT_E = 'INPUT_E'
  38. INPUT_F = 'INPUT_F'
  39. BAND_A = 'BAND_A'
  40. BAND_B = 'BAND_B'
  41. BAND_C = 'BAND_C'
  42. BAND_D = 'BAND_D'
  43. BAND_E = 'BAND_E'
  44. BAND_F = 'BAND_F'
  45. FORMULA = 'FORMULA'
  46. # TODO QGIS 4.0 : Rename EXTENT_OPT to EXTENT
  47. EXTENT_OPT = 'EXTENT_OPT'
  48. EXTENT_OPTIONS = ['ignore', 'fail', 'union', 'intersect']
  49. # TODO QGIS 4.0 : Rename EXTENT to PROJWIN or CUSTOM_EXTENT
  50. EXTENT = 'PROJWIN'
  51. OUTPUT = 'OUTPUT'
  52. NO_DATA = 'NO_DATA'
  53. OPTIONS = 'OPTIONS'
  54. EXTRA = 'EXTRA'
  55. RTYPE = 'RTYPE'
  56. TYPE = ['Byte', 'Int16', 'UInt16', 'UInt32', 'Int32', 'Float32', 'Float64', 'CInt16', 'CInt32', 'CFloat32', 'CFloat64', 'Int8']
  57. def __init__(self):
  58. super().__init__()
  59. def initAlgorithm(self, config=None):
  60. self.addParameter(
  61. QgsProcessingParameterRasterLayer(
  62. self.INPUT_A,
  63. self.tr('Input layer A'),
  64. optional=False))
  65. self.addParameter(
  66. QgsProcessingParameterBand(
  67. self.BAND_A,
  68. self.tr('Number of raster band for A'),
  69. parentLayerParameterName=self.INPUT_A))
  70. self.addParameter(
  71. QgsProcessingParameterRasterLayer(
  72. self.INPUT_B,
  73. self.tr('Input layer B'),
  74. optional=True))
  75. self.addParameter(
  76. QgsProcessingParameterBand(
  77. self.BAND_B,
  78. self.tr('Number of raster band for B'),
  79. parentLayerParameterName=self.INPUT_B,
  80. optional=True))
  81. self.addParameter(
  82. QgsProcessingParameterRasterLayer(
  83. self.INPUT_C,
  84. self.tr('Input layer C'),
  85. optional=True))
  86. self.addParameter(
  87. QgsProcessingParameterBand(
  88. self.BAND_C,
  89. self.tr('Number of raster band for C'),
  90. parentLayerParameterName=self.INPUT_C,
  91. optional=True))
  92. self.addParameter(
  93. QgsProcessingParameterRasterLayer(
  94. self.INPUT_D,
  95. self.tr('Input layer D'),
  96. optional=True))
  97. self.addParameter(
  98. QgsProcessingParameterBand(
  99. self.BAND_D,
  100. self.tr('Number of raster band for D'),
  101. parentLayerParameterName=self.INPUT_D,
  102. optional=True))
  103. self.addParameter(
  104. QgsProcessingParameterRasterLayer(
  105. self.INPUT_E,
  106. self.tr('Input layer E'),
  107. optional=True))
  108. self.addParameter(
  109. QgsProcessingParameterBand(
  110. self.BAND_E,
  111. self.tr('Number of raster band for E'),
  112. parentLayerParameterName=self.INPUT_E,
  113. optional=True))
  114. self.addParameter(
  115. QgsProcessingParameterRasterLayer(
  116. self.INPUT_F,
  117. self.tr('Input layer F'),
  118. optional=True))
  119. self.addParameter(
  120. QgsProcessingParameterBand(
  121. self.BAND_F,
  122. self.tr('Number of raster band for F'),
  123. parentLayerParameterName=self.INPUT_F,
  124. optional=True))
  125. self.addParameter(
  126. QgsProcessingParameterString(
  127. self.FORMULA,
  128. self.tr('Calculation in gdalnumeric syntax using +-/* or any numpy array functions (i.e. logical_and())'),
  129. 'A*2',
  130. optional=False))
  131. self.addParameter(
  132. QgsProcessingParameterNumber(
  133. self.NO_DATA,
  134. self.tr('Set output nodata value'),
  135. type=QgsProcessingParameterNumber.Double,
  136. defaultValue=None,
  137. optional=True))
  138. if GdalUtils.version() >= 3030000:
  139. extent_opt_param = QgsProcessingParameterEnum(
  140. self.EXTENT_OPT,
  141. self.tr('Handling of extent differences'),
  142. options=[o.title() for o in self.EXTENT_OPTIONS],
  143. defaultValue=0)
  144. extent_opt_param.setHelp(self.tr('This option determines how to handle rasters with different extents'))
  145. self.addParameter(extent_opt_param)
  146. if GdalUtils.version() >= 3030000:
  147. extent_param = QgsProcessingParameterExtent(self.EXTENT,
  148. self.tr('Output extent'),
  149. optional=True)
  150. extent_param.setHelp(self.tr('Custom extent of the output raster'))
  151. self.addParameter(extent_param)
  152. self.addParameter(
  153. QgsProcessingParameterEnum(
  154. self.RTYPE,
  155. self.tr('Output raster type'),
  156. options=self.TYPE,
  157. defaultValue=5))
  158. options_param = QgsProcessingParameterString(self.OPTIONS,
  159. self.tr('Additional creation options'),
  160. defaultValue='',
  161. optional=True)
  162. options_param.setFlags(options_param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
  163. options_param.setMetadata({
  164. 'widget_wrapper': {
  165. 'class': 'processing.algs.gdal.ui.RasterOptionsWidget.RasterOptionsWidgetWrapper'}})
  166. self.addParameter(options_param)
  167. extra_param = QgsProcessingParameterString(self.EXTRA,
  168. self.tr('Additional command-line parameters'),
  169. defaultValue=None,
  170. optional=True)
  171. extra_param.setFlags(extra_param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
  172. self.addParameter(extra_param)
  173. self.addParameter(
  174. QgsProcessingParameterRasterDestination(
  175. self.OUTPUT,
  176. self.tr('Calculated')))
  177. def name(self):
  178. return 'rastercalculator'
  179. def displayName(self):
  180. return self.tr('Raster calculator')
  181. def group(self):
  182. return self.tr('Raster miscellaneous')
  183. def groupId(self):
  184. return 'rastermiscellaneous'
  185. def commandName(self):
  186. return 'gdal_calc'
  187. def getConsoleCommands(self, parameters, context, feedback, executing=True):
  188. out = self.parameterAsOutputLayer(parameters, self.OUTPUT, context)
  189. self.setOutputValue(self.OUTPUT, out)
  190. formula = self.parameterAsString(parameters, self.FORMULA, context)
  191. if self.NO_DATA in parameters and parameters[self.NO_DATA] is not None:
  192. noData = self.parameterAsDouble(parameters, self.NO_DATA, context)
  193. else:
  194. noData = None
  195. arguments = [
  196. '--overwrite',
  197. f'--calc "{formula}"',
  198. '--format',
  199. GdalUtils.getFormatShortNameFromFilename(out),
  200. ]
  201. rtype = self.parameterAsEnum(parameters, self.RTYPE, context)
  202. if self.TYPE[rtype] in ['CInt16', 'CInt32', 'CFloat32', 'CFloat64'] and GdalUtils.version() < 3050300:
  203. raise QgsProcessingException(self.tr('{} data type requires GDAL version 3.5.3 or later').format(self.TYPE[rtype]))
  204. if self.TYPE[rtype] == 'Int8' and GdalUtils.version() < 3070000:
  205. raise QgsProcessingException(self.tr('Int8 data type requires GDAL version 3.7 or later'))
  206. arguments.append('--type ' + self.TYPE[rtype])
  207. if noData is not None:
  208. arguments.append('--NoDataValue')
  209. arguments.append(noData)
  210. layer_a = self.parameterAsRasterLayer(parameters, self.INPUT_A, context)
  211. if layer_a is None:
  212. raise QgsProcessingException(self.invalidRasterError(parameters, self.INPUT_A))
  213. def all_equal(iterator):
  214. iterator = iter(iterator)
  215. try:
  216. first = next(iterator)
  217. except StopIteration:
  218. return True
  219. return all(first == x for x in iterator)
  220. # Check GDAL version for projwin and extent options (GDAL 3.3 is required)
  221. if GdalUtils.version() < 3030000 and self.EXTENT in parameters.keys():
  222. raise QgsProcessingException(self.tr('The custom output extent option (--projwin) is only available on GDAL 3.3 or later'))
  223. if GdalUtils.version() < 3030000 and self.EXTENT_OPT in parameters.keys():
  224. raise QgsProcessingException(self.tr('The output extent option (--extent) is only available on GDAL 3.3 or later'))
  225. # --projwin and --extent option are mutually exclusive
  226. if (self.EXTENT in parameters.keys() and parameters[self.EXTENT] is not None) and (self.EXTENT_OPT in parameters.keys() and parameters[self.EXTENT_OPT] != 0):
  227. raise QgsProcessingException(self.tr('The custom output extent option (--projwin) and output extent option (--extent) are mutually exclusive'))
  228. # If extent option is defined, pixel size and SRS of all input raster must be the same
  229. if self.EXTENT_OPT in parameters.keys() and parameters[self.EXTENT_OPT] != 0:
  230. pixel_size_X, pixel_size_Y, srs = [], [], []
  231. for input_layer in [self.INPUT_A, self.INPUT_B, self.INPUT_C, self.INPUT_D, self.INPUT_E, self.INPUT_F]:
  232. if input_layer in parameters and parameters[input_layer] is not None:
  233. layer = self.parameterAsRasterLayer(parameters, input_layer, context)
  234. pixel_size_X.append(layer.rasterUnitsPerPixelX())
  235. pixel_size_Y.append(layer.rasterUnitsPerPixelY())
  236. srs.append(layer.crs().authid())
  237. if not (all_equal(pixel_size_X) and all_equal(pixel_size_Y) and all_equal(srs)):
  238. raise QgsProcessingException(self.tr('For all output extent options, the pixel size (resolution) and SRS (Spatial Reference System) of all the input rasters must be the same'))
  239. extent = self.EXTENT_OPTIONS[self.parameterAsEnum(parameters, self.EXTENT_OPT, context)]
  240. if extent != 'ignore':
  241. arguments.append(f'--extent={extent}')
  242. bbox = self.parameterAsExtent(parameters, self.EXTENT, context, layer_a.crs())
  243. if not bbox.isNull():
  244. arguments.append('--projwin')
  245. arguments.append(str(bbox.xMinimum()))
  246. arguments.append(str(bbox.yMaximum()))
  247. arguments.append(str(bbox.xMaximum()))
  248. arguments.append(str(bbox.yMinimum()))
  249. arguments.append('-A')
  250. arguments.append(layer_a.source())
  251. if self.parameterAsString(parameters, self.BAND_A, context):
  252. arguments.append('--A_band ' + self.parameterAsString(parameters, self.BAND_A, context))
  253. if self.INPUT_B in parameters and parameters[self.INPUT_B] is not None:
  254. layer_b = self.parameterAsRasterLayer(parameters, self.INPUT_B, context)
  255. if layer_b is None:
  256. raise QgsProcessingException(self.invalidRasterError(parameters, self.INPUT_B))
  257. arguments.append('-B')
  258. arguments.append(layer_b.source())
  259. if self.parameterAsString(parameters, self.BAND_B, context):
  260. arguments.append('--B_band ' + self.parameterAsString(parameters, self.BAND_B, context))
  261. if self.INPUT_C in parameters and parameters[self.INPUT_C] is not None:
  262. layer_c = self.parameterAsRasterLayer(parameters, self.INPUT_C, context)
  263. if layer_c is None:
  264. raise QgsProcessingException(self.invalidRasterError(parameters, self.INPUT_C))
  265. arguments.append('-C')
  266. arguments.append(layer_c.source())
  267. if self.parameterAsString(parameters, self.BAND_C, context):
  268. arguments.append('--C_band ' + self.parameterAsString(parameters, self.BAND_C, context))
  269. if self.INPUT_D in parameters and parameters[self.INPUT_D] is not None:
  270. layer_d = self.parameterAsRasterLayer(parameters, self.INPUT_D, context)
  271. if layer_d is None:
  272. raise QgsProcessingException(self.invalidRasterError(parameters, self.INPUT_D))
  273. arguments.append('-D')
  274. arguments.append(layer_d.source())
  275. if self.parameterAsString(parameters, self.BAND_D, context):
  276. arguments.append('--D_band ' + self.parameterAsString(parameters, self.BAND_D, context))
  277. if self.INPUT_E in parameters and parameters[self.INPUT_E] is not None:
  278. layer_e = self.parameterAsRasterLayer(parameters, self.INPUT_E, context)
  279. if layer_e is None:
  280. raise QgsProcessingException(self.invalidRasterError(parameters, self.INPUT_E))
  281. arguments.append('-E')
  282. arguments.append(layer_e.source())
  283. if self.parameterAsString(parameters, self.BAND_E, context):
  284. arguments.append('--E_band ' + self.parameterAsString(parameters, self.BAND_E, context))
  285. if self.INPUT_F in parameters and parameters[self.INPUT_F] is not None:
  286. layer_f = self.parameterAsRasterLayer(parameters, self.INPUT_F, context)
  287. if layer_f is None:
  288. raise QgsProcessingException(self.invalidRasterError(parameters, self.INPUT_F))
  289. arguments.append('-F')
  290. arguments.append(layer_f.source())
  291. if self.parameterAsString(parameters, self.BAND_F, context):
  292. arguments.append('--F_band ' + self.parameterAsString(parameters, self.BAND_F, context))
  293. options = self.parameterAsString(parameters, self.OPTIONS, context)
  294. if options:
  295. parts = options.split('|')
  296. for p in parts:
  297. arguments.append('--co ' + p)
  298. if self.EXTRA in parameters and parameters[self.EXTRA] not in (None, ''):
  299. extra = self.parameterAsString(parameters, self.EXTRA, context)
  300. arguments.append(extra)
  301. arguments.append('--outfile')
  302. arguments.append(out)
  303. return [self.commandName() + ('.bat' if isWindows() else '.py'), GdalUtils.escapeAndJoin(arguments)]