Grass7Algorithm.py 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097
  1. """
  2. ***************************************************************************
  3. Grass7Algorithm.py
  4. ---------------------
  5. Date : February 2015
  6. Copyright : (C) 2014-2015 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__ = 'February 2015'
  19. __copyright__ = '(C) 2012-2015, Victor Olaya'
  20. import sys
  21. import os
  22. import re
  23. import uuid
  24. import math
  25. import importlib
  26. from pathlib import Path
  27. from qgis.PyQt.QtCore import QCoreApplication, QUrl
  28. from qgis.core import (Qgis,
  29. QgsMapLayer,
  30. QgsRasterLayer,
  31. QgsApplication,
  32. QgsMapLayerType,
  33. QgsCoordinateReferenceSystem,
  34. QgsProcessingUtils,
  35. QgsProcessing,
  36. QgsMessageLog,
  37. QgsVectorFileWriter,
  38. QgsProcessingContext,
  39. QgsProcessingAlgorithm,
  40. QgsProcessingParameterDefinition,
  41. QgsProcessingException,
  42. QgsProcessingParameterCrs,
  43. QgsProcessingParameterExtent,
  44. QgsProcessingParameterEnum,
  45. QgsProcessingParameterNumber,
  46. QgsProcessingParameterString,
  47. QgsProcessingParameterField,
  48. QgsProcessingParameterPoint,
  49. QgsProcessingParameterBoolean,
  50. QgsProcessingParameterRange,
  51. QgsProcessingParameterFeatureSource,
  52. QgsProcessingParameterVectorLayer,
  53. QgsProcessingParameterRasterLayer,
  54. QgsProcessingParameterMultipleLayers,
  55. QgsProcessingParameterVectorDestination,
  56. QgsProcessingParameterRasterDestination,
  57. QgsProcessingParameterFileDestination,
  58. QgsProcessingParameterFile,
  59. QgsProcessingParameterFolderDestination,
  60. QgsProcessingOutputHtml,
  61. QgsVectorLayer,
  62. QgsProviderRegistry)
  63. from qgis.utils import iface
  64. import warnings
  65. with warnings.catch_warnings():
  66. warnings.filterwarnings("ignore", category=DeprecationWarning)
  67. from osgeo import ogr
  68. from processing.core.ProcessingConfig import ProcessingConfig
  69. from processing.core.parameters import getParameterFromString
  70. from grassprovider.Grass7Utils import Grass7Utils
  71. from processing.tools.system import isWindows, getTempFilename
  72. pluginPath = os.path.normpath(os.path.join(
  73. os.path.split(os.path.dirname(__file__))[0], os.pardir))
  74. class Grass7Algorithm(QgsProcessingAlgorithm):
  75. GRASS_OUTPUT_TYPE_PARAMETER = 'GRASS_OUTPUT_TYPE_PARAMETER'
  76. GRASS_MIN_AREA_PARAMETER = 'GRASS_MIN_AREA_PARAMETER'
  77. GRASS_SNAP_TOLERANCE_PARAMETER = 'GRASS_SNAP_TOLERANCE_PARAMETER'
  78. GRASS_REGION_EXTENT_PARAMETER = 'GRASS_REGION_PARAMETER'
  79. GRASS_REGION_CELLSIZE_PARAMETER = 'GRASS_REGION_CELLSIZE_PARAMETER'
  80. GRASS_REGION_ALIGN_TO_RESOLUTION = 'GRASS_REGION_ALIGN_TO_RESOLUTION'
  81. GRASS_RASTER_FORMAT_OPT = 'GRASS_RASTER_FORMAT_OPT'
  82. GRASS_RASTER_FORMAT_META = 'GRASS_RASTER_FORMAT_META'
  83. GRASS_VECTOR_DSCO = 'GRASS_VECTOR_DSCO'
  84. GRASS_VECTOR_LCO = 'GRASS_VECTOR_LCO'
  85. GRASS_VECTOR_EXPORT_NOCAT = 'GRASS_VECTOR_EXPORT_NOCAT'
  86. OUTPUT_TYPES = ['auto', 'point', 'line', 'area']
  87. QGIS_OUTPUT_TYPES = {QgsProcessing.TypeVectorAnyGeometry: 'auto',
  88. QgsProcessing.TypeVectorPoint: 'point',
  89. QgsProcessing.TypeVectorLine: 'line',
  90. QgsProcessing.TypeVectorPolygon: 'area'}
  91. def __init__(self, descriptionfile):
  92. super().__init__()
  93. self._name = ''
  94. self._display_name = ''
  95. self._short_description = ''
  96. self._group = ''
  97. self._groupId = ''
  98. self.groupIdRegex = re.compile(r'^[^\s\(]+')
  99. self.grass7Name = ''
  100. self.params = []
  101. self.hardcodedStrings = []
  102. self.inputLayers = []
  103. self.commands = []
  104. self.outputCommands = []
  105. self.exportedLayers = {}
  106. self.fileOutputs = {}
  107. self.descriptionFile = descriptionfile
  108. # Default GRASS parameters
  109. self.region = None
  110. self.cellSize = None
  111. self.snapTolerance = None
  112. self.outputType = None
  113. self.minArea = None
  114. self.alignToResolution = None
  115. # destination Crs for combineLayerExtents, will be set from layer or mapSettings
  116. self.destination_crs = QgsCoordinateReferenceSystem()
  117. # Load parameters from a description file
  118. self.defineCharacteristicsFromFile()
  119. self.numExportedLayers = 0
  120. # Do we need this anymore?
  121. self.uniqueSuffix = str(uuid.uuid4()).replace('-', '')
  122. # Use the ext mechanism
  123. name = self.name().replace('.', '_')
  124. self.module = None
  125. try:
  126. extpath = self.descriptionFile.parents[1].joinpath('ext', name + '.py')
  127. # this check makes it a bit faster
  128. if extpath.exists():
  129. spec = importlib.util.spec_from_file_location('grassprovider.ext.' + name, extpath)
  130. self.module = importlib.util.module_from_spec(spec)
  131. spec.loader.exec_module(self.module)
  132. except Exception as e:
  133. QgsMessageLog.logMessage(self.tr('Failed to load: {0}\n{1}').format(extpath, e), 'Processing', Qgis.Critical)
  134. pass
  135. def createInstance(self):
  136. return self.__class__(self.descriptionFile)
  137. def name(self):
  138. return self._name
  139. def displayName(self):
  140. return self._display_name
  141. def shortDescription(self):
  142. return self._short_description
  143. def group(self):
  144. return self._group
  145. def groupId(self):
  146. return self._groupId
  147. def icon(self):
  148. return QgsApplication.getThemeIcon("/providerGrass.svg")
  149. def svgIconPath(self):
  150. return QgsApplication.iconPath("providerGrass.svg")
  151. def flags(self):
  152. # TODO - maybe it's safe to background thread this?
  153. return super().flags() | QgsProcessingAlgorithm.FlagNoThreading | QgsProcessingAlgorithm.FlagDisplayNameIsLiteral
  154. def tr(self, string, context=''):
  155. if context == '':
  156. context = self.__class__.__name__
  157. return QCoreApplication.translate(context, string)
  158. def helpUrl(self):
  159. helpPath = Grass7Utils.grassHelpPath()
  160. if helpPath == '':
  161. return None
  162. if os.path.exists(helpPath):
  163. return QUrl.fromLocalFile(os.path.join(helpPath, '{}.html'.format(self.grass7Name))).toString()
  164. else:
  165. return helpPath + '{}.html'.format(self.grass7Name)
  166. def initAlgorithm(self, config=None):
  167. """
  168. Algorithm initialization
  169. """
  170. for p in self.params:
  171. # We use createOutput argument for automatic output creation
  172. self.addParameter(p, True)
  173. def defineCharacteristicsFromFile(self):
  174. """
  175. Create algorithm parameters and outputs from a text file.
  176. """
  177. with self.descriptionFile.open() as lines:
  178. # First line of the file is the Grass algorithm name
  179. line = lines.readline().strip('\n').strip()
  180. self.grass7Name = line
  181. # Second line if the algorithm name in Processing
  182. line = lines.readline().strip('\n').strip()
  183. self._short_description = line
  184. if " - " not in line:
  185. self._name = self.grass7Name
  186. else:
  187. self._name = line[:line.find(' ')].lower()
  188. self._short_description = QCoreApplication.translate("GrassAlgorithm", line)
  189. self._display_name = self._name
  190. # Read the grass group
  191. line = lines.readline().strip('\n').strip()
  192. self._group = QCoreApplication.translate("GrassAlgorithm", line)
  193. self._groupId = self.groupIdRegex.search(line).group(0).lower()
  194. hasRasterOutput = False
  195. hasRasterInput = False
  196. hasVectorInput = False
  197. vectorOutputs = False
  198. # Then you have parameters/output definition
  199. line = lines.readline().strip('\n').strip()
  200. while line != '':
  201. try:
  202. line = line.strip('\n').strip()
  203. if line.startswith('Hardcoded'):
  204. self.hardcodedStrings.append(line[len('Hardcoded|'):])
  205. parameter = getParameterFromString(line, "GrassAlgorithm")
  206. if parameter is not None:
  207. self.params.append(parameter)
  208. if isinstance(parameter, (QgsProcessingParameterVectorLayer, QgsProcessingParameterFeatureSource)):
  209. hasVectorInput = True
  210. elif isinstance(parameter, QgsProcessingParameterRasterLayer):
  211. hasRasterInput = True
  212. elif isinstance(parameter, QgsProcessingParameterMultipleLayers):
  213. if parameter.layerType() < 3 or parameter.layerType() == 5:
  214. hasVectorInput = True
  215. elif parameter.layerType() == 3:
  216. hasRasterInput = True
  217. elif isinstance(parameter, QgsProcessingParameterVectorDestination):
  218. vectorOutputs = True
  219. elif isinstance(parameter, QgsProcessingParameterRasterDestination):
  220. hasRasterOutput = True
  221. line = lines.readline().strip('\n').strip()
  222. except Exception as e:
  223. QgsMessageLog.logMessage(self.tr('Could not open GRASS GIS 7 algorithm: {0}\n{1}').format(self.descriptionFile, line), self.tr('Processing'), Qgis.Critical)
  224. raise e
  225. param = QgsProcessingParameterExtent(
  226. self.GRASS_REGION_EXTENT_PARAMETER,
  227. self.tr('GRASS GIS 7 region extent'),
  228. optional=True
  229. )
  230. param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
  231. self.params.append(param)
  232. if hasRasterOutput or hasRasterInput:
  233. # Add a cellsize parameter
  234. param = QgsProcessingParameterNumber(
  235. self.GRASS_REGION_CELLSIZE_PARAMETER,
  236. self.tr('GRASS GIS 7 region cellsize (leave 0 for default)'),
  237. type=QgsProcessingParameterNumber.Double,
  238. minValue=0.0, maxValue=sys.float_info.max + 1, defaultValue=0.0
  239. )
  240. param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
  241. self.params.append(param)
  242. if hasRasterOutput:
  243. # Add a createopt parameter for format export
  244. param = QgsProcessingParameterString(
  245. self.GRASS_RASTER_FORMAT_OPT,
  246. self.tr('Output Rasters format options (createopt)'),
  247. multiLine=True, optional=True
  248. )
  249. param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
  250. param.setHelp(self.tr('Creation options should be comma separated'))
  251. self.params.append(param)
  252. # Add a metadata parameter for format export
  253. param = QgsProcessingParameterString(
  254. self.GRASS_RASTER_FORMAT_META,
  255. self.tr('Output Rasters format metadata options (metaopt)'),
  256. multiLine=True, optional=True
  257. )
  258. param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
  259. param.setHelp(self.tr('Metadata options should be comma separated'))
  260. self.params.append(param)
  261. if hasVectorInput:
  262. param = QgsProcessingParameterNumber(self.GRASS_SNAP_TOLERANCE_PARAMETER,
  263. self.tr('v.in.ogr snap tolerance (-1 = no snap)'),
  264. type=QgsProcessingParameterNumber.Double,
  265. minValue=-1.0, maxValue=sys.float_info.max + 1,
  266. defaultValue=-1.0)
  267. param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
  268. self.params.append(param)
  269. param = QgsProcessingParameterNumber(self.GRASS_MIN_AREA_PARAMETER,
  270. self.tr('v.in.ogr min area'),
  271. type=QgsProcessingParameterNumber.Double,
  272. minValue=0.0, maxValue=sys.float_info.max + 1,
  273. defaultValue=0.0001)
  274. param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
  275. self.params.append(param)
  276. if vectorOutputs:
  277. # Add an optional output type
  278. param = QgsProcessingParameterEnum(self.GRASS_OUTPUT_TYPE_PARAMETER,
  279. self.tr('v.out.ogr output type'),
  280. self.OUTPUT_TYPES,
  281. defaultValue=0)
  282. param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
  283. self.params.append(param)
  284. # Add a DSCO parameter for format export
  285. param = QgsProcessingParameterString(
  286. self.GRASS_VECTOR_DSCO,
  287. self.tr('v.out.ogr output data source options (dsco)'),
  288. multiLine=True, optional=True
  289. )
  290. param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
  291. self.params.append(param)
  292. # Add a LCO parameter for format export
  293. param = QgsProcessingParameterString(
  294. self.GRASS_VECTOR_LCO,
  295. self.tr('v.out.ogr output layer options (lco)'),
  296. multiLine=True, optional=True
  297. )
  298. param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
  299. self.params.append(param)
  300. # Add a -c flag for export
  301. param = QgsProcessingParameterBoolean(
  302. self.GRASS_VECTOR_EXPORT_NOCAT,
  303. self.tr('Also export features without category (not labeled). Otherwise only features with category are exported'),
  304. False
  305. )
  306. param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
  307. self.params.append(param)
  308. def getDefaultCellSize(self):
  309. """
  310. Determine a default cell size from all the raster layers.
  311. """
  312. cellsize = 0.0
  313. layers = [l for l in self.inputLayers if isinstance(l, QgsRasterLayer)]
  314. for layer in layers:
  315. cellsize = max(layer.rasterUnitsPerPixelX(), cellsize)
  316. if cellsize == 0.0:
  317. cellsize = 100.0
  318. return cellsize
  319. def grabDefaultGrassParameters(self, parameters, context):
  320. """
  321. Imports default GRASS parameters (EXTENT, etc) into
  322. object attributes for faster retrieving.
  323. """
  324. # GRASS region extent
  325. self.region = self.parameterAsExtent(parameters,
  326. self.GRASS_REGION_EXTENT_PARAMETER,
  327. context)
  328. # GRASS cell size
  329. if self.parameterDefinition(self.GRASS_REGION_CELLSIZE_PARAMETER):
  330. self.cellSize = self.parameterAsDouble(parameters,
  331. self.GRASS_REGION_CELLSIZE_PARAMETER,
  332. context)
  333. # GRASS snap tolerance
  334. self.snapTolerance = self.parameterAsDouble(parameters,
  335. self.GRASS_SNAP_TOLERANCE_PARAMETER,
  336. context)
  337. # GRASS min area
  338. self.minArea = self.parameterAsDouble(parameters,
  339. self.GRASS_MIN_AREA_PARAMETER,
  340. context)
  341. # GRASS output type
  342. self.outputType = self.parameterAsString(parameters,
  343. self.GRASS_OUTPUT_TYPE_PARAMETER,
  344. context)
  345. # GRASS align to resolution
  346. self.alignToResolution = self.parameterAsBoolean(parameters,
  347. self.GRASS_REGION_ALIGN_TO_RESOLUTION,
  348. context)
  349. def processAlgorithm(self, original_parameters, context, feedback):
  350. if isWindows():
  351. path = Grass7Utils.grassPath()
  352. if path == '':
  353. raise QgsProcessingException(
  354. self.tr('GRASS GIS 7 folder is not configured. Please '
  355. 'configure it before running GRASS GIS 7 algorithms.'))
  356. # make a copy of the original parameters dictionary - it gets modified by grass algorithms
  357. parameters = {k: v for k, v in original_parameters.items()}
  358. # Create brand new commands lists
  359. self.commands = []
  360. self.outputCommands = []
  361. self.exportedLayers = {}
  362. self.fileOutputs = {}
  363. # If GRASS session has been created outside of this algorithm then
  364. # get the list of layers loaded in GRASS otherwise start a new
  365. # session
  366. existingSession = Grass7Utils.sessionRunning
  367. if existingSession:
  368. self.exportedLayers = Grass7Utils.getSessionLayers()
  369. else:
  370. Grass7Utils.startGrassSession()
  371. # Handle default GRASS parameters
  372. self.grabDefaultGrassParameters(parameters, context)
  373. # Handle ext functions for inputs/command/outputs
  374. for fName in ['Inputs', 'Command', 'Outputs']:
  375. fullName = 'process{}'.format(fName)
  376. if self.module and hasattr(self.module, fullName):
  377. getattr(self.module, fullName)(self, parameters, context, feedback)
  378. else:
  379. getattr(self, fullName)(parameters, context, feedback)
  380. # Run GRASS
  381. loglines = [self.tr('GRASS GIS 7 execution commands')]
  382. for line in self.commands:
  383. feedback.pushCommandInfo(line)
  384. loglines.append(line)
  385. if ProcessingConfig.getSetting(Grass7Utils.GRASS_LOG_COMMANDS):
  386. QgsMessageLog.logMessage("\n".join(loglines), self.tr('Processing'), Qgis.Info)
  387. Grass7Utils.executeGrass(self.commands, feedback, self.outputCommands)
  388. # If the session has been created outside of this algorithm, add
  389. # the new GRASS GIS 7 layers to it otherwise finish the session
  390. if existingSession:
  391. Grass7Utils.addSessionLayers(self.exportedLayers)
  392. else:
  393. Grass7Utils.endGrassSession()
  394. # Return outputs map
  395. outputs = {}
  396. for out in self.outputDefinitions():
  397. outName = out.name()
  398. if outName in parameters:
  399. if outName in self.fileOutputs:
  400. outputs[outName] = self.fileOutputs[outName]
  401. else:
  402. outputs[outName] = parameters[outName]
  403. if isinstance(out, QgsProcessingOutputHtml):
  404. self.convertToHtml(self.fileOutputs[outName])
  405. return outputs
  406. def processInputs(self, parameters, context, feedback):
  407. """Prepare the GRASS import commands"""
  408. inputs = [p for p in self.parameterDefinitions()
  409. if isinstance(p, (QgsProcessingParameterVectorLayer,
  410. QgsProcessingParameterFeatureSource,
  411. QgsProcessingParameterRasterLayer,
  412. QgsProcessingParameterMultipleLayers))]
  413. for param in inputs:
  414. paramName = param.name()
  415. if paramName not in parameters:
  416. continue
  417. # Handle Null parameter
  418. if parameters[paramName] is None:
  419. continue
  420. elif isinstance(parameters[paramName], str) and len(parameters[paramName]) == 0:
  421. continue
  422. # Raster inputs needs to be imported into temp GRASS DB
  423. if isinstance(param, QgsProcessingParameterRasterLayer):
  424. if paramName not in self.exportedLayers:
  425. self.loadRasterLayerFromParameter(
  426. paramName, parameters, context)
  427. # Vector inputs needs to be imported into temp GRASS DB
  428. elif isinstance(param, (QgsProcessingParameterFeatureSource, QgsProcessingParameterVectorLayer)):
  429. if paramName not in self.exportedLayers:
  430. # Attribute tables are also vector inputs
  431. if QgsProcessing.TypeFile in param.dataTypes():
  432. self.loadAttributeTableFromParameter(
  433. paramName, parameters, context)
  434. else:
  435. self.loadVectorLayerFromParameter(
  436. paramName, parameters, context, external=None, feedback=feedback)
  437. # For multiple inputs, process each layer
  438. elif isinstance(param, QgsProcessingParameterMultipleLayers):
  439. layers = self.parameterAsLayerList(parameters, paramName, context)
  440. for idx, layer in enumerate(layers):
  441. layerName = '{}_{}'.format(paramName, idx)
  442. # Add a raster layer
  443. if layer.type() == QgsMapLayerType.RasterLayer:
  444. self.loadRasterLayer(layerName, layer, context)
  445. # Add a vector layer
  446. elif layer.type() == QgsMapLayerType.VectorLayer:
  447. self.loadVectorLayer(layerName, layer, context, external=None, feedback=feedback)
  448. self.postInputs(context)
  449. def postInputs(self, context: QgsProcessingContext):
  450. """
  451. After layer imports, we need to update some internal parameters
  452. """
  453. # If projection has not already be set, use the project
  454. self.setSessionProjectionFromProject(context)
  455. # Build GRASS region
  456. if self.region.isEmpty():
  457. self.region = QgsProcessingUtils.combineLayerExtents(self.inputLayers, self.destination_crs, context)
  458. command = 'g.region n={} s={} e={} w={}'.format(
  459. self.region.yMaximum(), self.region.yMinimum(),
  460. self.region.xMaximum(), self.region.xMinimum()
  461. )
  462. # Handle cell size
  463. if self.parameterDefinition(self.GRASS_REGION_CELLSIZE_PARAMETER):
  464. if self.cellSize:
  465. cellSize = self.cellSize
  466. else:
  467. cellSize = self.getDefaultCellSize()
  468. command += ' res={}'.format(cellSize)
  469. # Handle align to resolution
  470. if self.alignToResolution:
  471. command += ' -a'
  472. # Add the default parameters commands
  473. self.commands.append(command)
  474. QgsMessageLog.logMessage(self.tr('processInputs end. Commands: {}').format(self.commands), 'Grass7', Qgis.Info)
  475. def processCommand(self, parameters, context, feedback, delOutputs=False):
  476. """
  477. Prepare the GRASS algorithm command
  478. :param parameters:
  479. :param context:
  480. :param delOutputs: do not add outputs to commands.
  481. """
  482. noOutputs = [o for o in self.parameterDefinitions() if o not in self.destinationParameterDefinitions()]
  483. command = '{} '.format(self.grass7Name)
  484. command += '{}'.join(self.hardcodedStrings)
  485. # Add algorithm command
  486. for param in noOutputs:
  487. paramName = param.name()
  488. value = None
  489. # Exclude default GRASS parameters
  490. if paramName in [self.GRASS_REGION_CELLSIZE_PARAMETER,
  491. self.GRASS_REGION_EXTENT_PARAMETER,
  492. self.GRASS_MIN_AREA_PARAMETER,
  493. self.GRASS_SNAP_TOLERANCE_PARAMETER,
  494. self.GRASS_OUTPUT_TYPE_PARAMETER,
  495. self.GRASS_REGION_ALIGN_TO_RESOLUTION,
  496. self.GRASS_RASTER_FORMAT_OPT,
  497. self.GRASS_RASTER_FORMAT_META,
  498. self.GRASS_VECTOR_DSCO,
  499. self.GRASS_VECTOR_LCO,
  500. self.GRASS_VECTOR_EXPORT_NOCAT]:
  501. continue
  502. # Raster and vector layers
  503. if isinstance(param, (QgsProcessingParameterRasterLayer,
  504. QgsProcessingParameterVectorLayer,
  505. QgsProcessingParameterFeatureSource)):
  506. if paramName in self.exportedLayers:
  507. value = self.exportedLayers[paramName]
  508. else:
  509. value = self.parameterAsCompatibleSourceLayerPath(
  510. parameters, paramName, context,
  511. QgsVectorFileWriter.supportedFormatExtensions()
  512. )
  513. # MultipleLayers
  514. elif isinstance(param, QgsProcessingParameterMultipleLayers):
  515. layers = self.parameterAsLayerList(parameters, paramName, context)
  516. values = []
  517. for idx in range(len(layers)):
  518. layerName = '{}_{}'.format(paramName, idx)
  519. values.append(self.exportedLayers[layerName])
  520. value = ','.join(values)
  521. # For booleans, we just add the parameter name
  522. elif isinstance(param, QgsProcessingParameterBoolean):
  523. if self.parameterAsBoolean(parameters, paramName, context):
  524. command += ' {}'.format(paramName)
  525. # For Extents, remove if the value is null
  526. elif isinstance(param, QgsProcessingParameterExtent):
  527. if self.parameterAsExtent(parameters, paramName, context):
  528. value = self.parameterAsString(parameters, paramName, context)
  529. # For enumeration, we need to grab the string value
  530. elif isinstance(param, QgsProcessingParameterEnum):
  531. # Handle multiple values
  532. if param.allowMultiple():
  533. indexes = self.parameterAsEnums(parameters, paramName, context)
  534. else:
  535. indexes = [self.parameterAsEnum(parameters, paramName, context)]
  536. if indexes:
  537. value = '"{}"'.format(','.join([param.options()[i] for i in indexes]))
  538. # For strings, we just translate as string
  539. elif isinstance(param, QgsProcessingParameterString):
  540. data = self.parameterAsString(parameters, paramName, context)
  541. # if string is empty, we don't add it
  542. if len(data) > 0:
  543. value = '"{}"'.format(
  544. self.parameterAsString(parameters, paramName, context)
  545. )
  546. # For fields, we just translate as string
  547. elif isinstance(param, QgsProcessingParameterField):
  548. value = ','.join(
  549. self.parameterAsFields(parameters, paramName, context)
  550. )
  551. elif isinstance(param, QgsProcessingParameterFile):
  552. if self.parameterAsString(parameters, paramName, context):
  553. value = '"{}"'.format(
  554. self.parameterAsString(parameters, paramName, context)
  555. )
  556. elif isinstance(param, QgsProcessingParameterPoint):
  557. if self.parameterAsString(parameters, paramName, context):
  558. # parameter specified, evaluate as point
  559. # TODO - handle CRS transform
  560. point = self.parameterAsPoint(parameters, paramName, context)
  561. value = '{},{}'.format(point.x(), point.y())
  562. # For numbers, we translate as a string
  563. elif isinstance(param, (QgsProcessingParameterNumber,
  564. QgsProcessingParameterPoint)):
  565. value = self.parameterAsString(parameters, paramName, context)
  566. elif isinstance(param, QgsProcessingParameterRange):
  567. v = self.parameterAsRange(parameters, paramName, context)
  568. if (param.flags() & QgsProcessingParameterDefinition.FlagOptional) and (math.isnan(v[0]) or math.isnan(v[1])):
  569. continue
  570. else:
  571. value = '{},{}'.format(v[0], v[1])
  572. elif isinstance(param, QgsProcessingParameterCrs):
  573. if self.parameterAsCrs(parameters, paramName, context):
  574. # TODO: ideally we should be exporting to WKT here, but it seems not all grass algorithms
  575. # will accept a wkt string for a crs value (e.g. r.tileset)
  576. value = '"{}"'.format(self.parameterAsCrs(parameters, paramName, context).toProj())
  577. # For everything else, we assume that it is a string
  578. else:
  579. value = '"{}"'.format(
  580. self.parameterAsString(parameters, paramName, context)
  581. )
  582. if value:
  583. command += ' {}={}'.format(paramName.replace('~', ''), value)
  584. # Handle outputs
  585. if not delOutputs:
  586. for out in self.destinationParameterDefinitions():
  587. # We exclude hidden parameters
  588. if out.flags() & QgsProcessingParameterDefinition.FlagHidden:
  589. continue
  590. outName = out.name()
  591. # For File destination
  592. if isinstance(out, QgsProcessingParameterFileDestination):
  593. if outName in parameters and parameters[outName] is not None:
  594. outPath = self.parameterAsFileOutput(parameters, outName, context)
  595. self.fileOutputs[outName] = outPath
  596. # for HTML reports, we need to redirect stdout
  597. if out.defaultFileExtension().lower() == 'html':
  598. if outName == 'html':
  599. # for "fake" outputs redirect command stdout
  600. command += ' > "{}"'.format(outPath)
  601. else:
  602. # for real outputs only output itself should be redirected
  603. command += ' {}=- > "{}"'.format(outName, outPath)
  604. else:
  605. command += ' {}="{}"'.format(outName, outPath)
  606. # For folders destination
  607. elif isinstance(out, QgsProcessingParameterFolderDestination):
  608. # We need to add a unique temporary basename
  609. uniqueBasename = outName + self.uniqueSuffix
  610. command += ' {}={}'.format(outName, uniqueBasename)
  611. else:
  612. if outName in parameters and parameters[outName] is not None:
  613. # We add an output name to make sure it is unique if the session
  614. # uses this algorithm several times.
  615. uniqueOutputName = outName + self.uniqueSuffix
  616. command += ' {}={}'.format(outName, uniqueOutputName)
  617. # Add output file to exported layers, to indicate that
  618. # they are present in GRASS
  619. self.exportedLayers[outName] = uniqueOutputName
  620. command += ' --overwrite'
  621. self.commands.append(command)
  622. QgsMessageLog.logMessage(self.tr('processCommands end. Commands: {}').format(self.commands), 'Grass7', Qgis.Info)
  623. def vectorOutputType(self, parameters, context):
  624. """Determine vector output types for outputs"""
  625. self.outType = 'auto'
  626. if self.parameterDefinition(self.GRASS_OUTPUT_TYPE_PARAMETER):
  627. typeidx = self.parameterAsEnum(parameters,
  628. self.GRASS_OUTPUT_TYPE_PARAMETER,
  629. context)
  630. self.outType = ('auto' if typeidx
  631. is None else self.OUTPUT_TYPES[typeidx])
  632. def processOutputs(self, parameters, context, feedback):
  633. """Prepare the GRASS v.out.ogr commands"""
  634. # Determine general vector output type
  635. self.vectorOutputType(parameters, context)
  636. for out in self.destinationParameterDefinitions():
  637. outName = out.name()
  638. if outName not in parameters:
  639. # skipped output
  640. continue
  641. if isinstance(out, QgsProcessingParameterRasterDestination):
  642. self.exportRasterLayerFromParameter(outName, parameters, context)
  643. elif isinstance(out, QgsProcessingParameterVectorDestination):
  644. self.exportVectorLayerFromParameter(outName, parameters, context)
  645. elif isinstance(out, QgsProcessingParameterFolderDestination):
  646. self.exportRasterLayersIntoDirectory(outName, parameters, context)
  647. def loadRasterLayerFromParameter(self, name, parameters,
  648. context: QgsProcessingContext,
  649. external=None, band=1):
  650. """
  651. Creates a dedicated command to load a raster into
  652. the temporary GRASS DB.
  653. :param name: name of the parameter.
  654. :param parameters: algorithm parameters dict.
  655. :param context: algorithm context.
  656. :param external: use r.external if True, r.in.gdal otherwise.
  657. :param band: imports only specified band. None for all bands.
  658. """
  659. layer = self.parameterAsRasterLayer(parameters, name, context)
  660. self.loadRasterLayer(name, layer, context, external, band)
  661. def loadRasterLayer(self, name, layer, context: QgsProcessingContext,
  662. external=None, band=1, destName=None):
  663. """
  664. Creates a dedicated command to load a raster into
  665. the temporary GRASS DB.
  666. :param name: name of the parameter.
  667. :param layer: QgsMapLayer for the raster layer.
  668. :param context: Processing context
  669. :param external: use r.external if True, r.in.gdal if False.
  670. :param band: imports only specified band. None for all bands.
  671. :param destName: force the destination name of the raster.
  672. """
  673. if external is None:
  674. external = ProcessingConfig.getSetting(Grass7Utils.GRASS_USE_REXTERNAL)
  675. self.inputLayers.append(layer)
  676. self.setSessionProjectionFromLayer(layer, context)
  677. if not destName:
  678. destName = 'rast_{}'.format(os.path.basename(getTempFilename(context=context)))
  679. self.exportedLayers[name] = destName
  680. command = '{} input="{}" {}output="{}" --overwrite -o'.format(
  681. 'r.external' if external else 'r.in.gdal',
  682. os.path.normpath(layer.source()),
  683. 'band={} '.format(band) if band else '',
  684. destName)
  685. self.commands.append(command)
  686. def exportRasterLayerFromParameter(self, name, parameters, context, colorTable=True):
  687. """
  688. Creates a dedicated command to export a raster from
  689. temporary GRASS DB into a file via gdal.
  690. :param name: name of the parameter.
  691. :param parameters: Algorithm parameters dict.
  692. :param context: Algorithm context.
  693. :param colorTable: preserve color Table.
  694. """
  695. fileName = self.parameterAsOutputLayer(parameters, name, context)
  696. if not fileName:
  697. return
  698. fileName = os.path.normpath(fileName)
  699. grassName = '{}{}'.format(name, self.uniqueSuffix)
  700. outFormat = Grass7Utils.getRasterFormatFromFilename(fileName)
  701. createOpt = self.parameterAsString(parameters, self.GRASS_RASTER_FORMAT_OPT, context)
  702. metaOpt = self.parameterAsString(parameters, self.GRASS_RASTER_FORMAT_META, context)
  703. self.exportRasterLayer(grassName, fileName, colorTable, outFormat, createOpt, metaOpt)
  704. self.fileOutputs[name] = fileName
  705. def exportRasterLayer(self, grassName, fileName,
  706. colorTable=True, outFormat='GTiff',
  707. createOpt=None,
  708. metaOpt=None):
  709. """
  710. Creates a dedicated command to export a raster from
  711. temporary GRASS DB into a file via gdal.
  712. :param grassName: name of the raster to export.
  713. :param fileName: file path of raster layer.
  714. :param colorTable: preserve color Table.
  715. :param outFormat: file format for export.
  716. :param createOpt: creation options for format.
  717. :param metaOpt: metadata options for export.
  718. """
  719. createOpt = createOpt or Grass7Utils.GRASS_RASTER_FORMATS_CREATEOPTS.get(outFormat)
  720. for cmd in [self.commands, self.outputCommands]:
  721. # Adjust region to layer before exporting
  722. cmd.append('g.region raster={}'.format(grassName))
  723. cmd.append(
  724. 'r.out.gdal -t -m{} input="{}" output="{}" format="{}" {}{} --overwrite'.format(
  725. '' if colorTable else ' -c',
  726. grassName, fileName,
  727. outFormat,
  728. ' createopt="{}"'.format(createOpt) if createOpt else '',
  729. ' metaopt="{}"'.format(metaOpt) if metaOpt else ''
  730. )
  731. )
  732. def exportRasterLayersIntoDirectory(self, name, parameters, context, colorTable=True, wholeDB=False):
  733. """
  734. Creates a dedicated loop command to export rasters from
  735. temporary GRASS DB into a directory via gdal.
  736. :param name: name of the output directory parameter.
  737. :param parameters: Algorithm parameters dict.
  738. :param context: Algorithm context.
  739. :param colorTable: preserve color Table.
  740. :param wholeDB: export every raster layer from the GRASSDB
  741. """
  742. # Grab directory name and temporary basename
  743. outDir = os.path.normpath(
  744. self.parameterAsString(parameters, name, context))
  745. basename = ''
  746. if not wholeDB:
  747. basename = name + self.uniqueSuffix
  748. # Add a loop export from the basename
  749. for cmd in [self.commands, self.outputCommands]:
  750. # TODO Format/options support
  751. if isWindows():
  752. cmd.append("if not exist {0} mkdir {0}".format(outDir))
  753. cmd.append("for /F %%r IN ('g.list type^=rast pattern^=\"{}*\"') do r.out.gdal -m{} input=%%r output={}/%%r.tif {}".format(
  754. basename,
  755. ' -t' if colorTable else '',
  756. outDir,
  757. '--overwrite -c createopt="TFW=YES,COMPRESS=LZW"'
  758. ))
  759. else:
  760. cmd.append("for r in $(g.list type=rast pattern='{}*'); do".format(basename))
  761. cmd.append(" r.out.gdal -m{0} input=${{r}} output={1}/${{r}}.tif {2}".format(
  762. ' -t' if colorTable else '', outDir,
  763. '--overwrite -c createopt="TFW=YES,COMPRESS=LZW"'
  764. ))
  765. cmd.append("done")
  766. def loadVectorLayerFromParameter(self, name, parameters, context, feedback, external=False):
  767. """
  768. Creates a dedicated command to load a vector into
  769. the temporary GRASS DB.
  770. :param name: name of the parameter
  771. :param parameters: Parameters of the algorithm.
  772. :param context: Processing context
  773. :param external: use v.external (v.in.ogr if False).
  774. """
  775. layer = self.parameterAsVectorLayer(parameters, name, context)
  776. is_ogr_disk_based_layer = layer is not None and layer.dataProvider().name() == 'ogr'
  777. if is_ogr_disk_based_layer:
  778. # we only support direct reading of disk based ogr layers -- not ogr postgres layers, etc
  779. source_parts = QgsProviderRegistry.instance().decodeUri('ogr', layer.source())
  780. if not source_parts.get('path'):
  781. is_ogr_disk_based_layer = False
  782. elif source_parts.get('layerId'):
  783. # no support for directly reading layers by id in grass
  784. is_ogr_disk_based_layer = False
  785. if not is_ogr_disk_based_layer:
  786. # parameter is not a vector layer or not an OGR layer - try to convert to a source compatible with
  787. # grass OGR inputs and extract selection if required
  788. path = self.parameterAsCompatibleSourceLayerPath(parameters, name, context,
  789. QgsVectorFileWriter.supportedFormatExtensions(),
  790. feedback=feedback)
  791. ogr_layer = QgsVectorLayer(path, '', 'ogr')
  792. self.loadVectorLayer(name, ogr_layer, context, external=external, feedback=feedback)
  793. else:
  794. # already an ogr disk based layer source
  795. self.loadVectorLayer(name, layer, context, external=external, feedback=feedback)
  796. def loadVectorLayer(self, name, layer,
  797. context: QgsProcessingContext,
  798. external=False, feedback=None):
  799. """
  800. Creates a dedicated command to load a vector into
  801. temporary GRASS DB.
  802. :param name: name of the parameter
  803. :param layer: QgsMapLayer for the vector layer.
  804. :param context: Processing context
  805. :param external: use v.external (v.in.ogr if False).
  806. :param feedback: feedback object
  807. """
  808. # TODO: support multiple input formats
  809. if external is None:
  810. external = ProcessingConfig.getSetting(
  811. Grass7Utils.GRASS_USE_VEXTERNAL)
  812. source_parts = QgsProviderRegistry.instance().decodeUri('ogr', layer.source())
  813. file_path = source_parts.get('path')
  814. layer_name = source_parts.get('layerName')
  815. # safety check: we can only use external for ogr layers which support random read
  816. if external:
  817. if feedback is not None:
  818. feedback.pushInfo('Attempting to use v.external for direct layer read')
  819. ds = ogr.Open(file_path)
  820. if ds is not None:
  821. ogr_layer = ds.GetLayer()
  822. if ogr_layer is None or not ogr_layer.TestCapability(ogr.OLCRandomRead):
  823. if feedback is not None:
  824. feedback.reportError('Cannot use v.external: layer does not support random read')
  825. external = False
  826. else:
  827. if feedback is not None:
  828. feedback.reportError('Cannot use v.external: error reading layer')
  829. external = False
  830. self.inputLayers.append(layer)
  831. self.setSessionProjectionFromLayer(layer, context)
  832. destFilename = 'vector_{}'.format(os.path.basename(getTempFilename(context=context)))
  833. self.exportedLayers[name] = destFilename
  834. command = '{}{}{} input="{}"{} output="{}" --overwrite -o'.format(
  835. 'v.external' if external else 'v.in.ogr',
  836. ' min_area={}'.format(self.minArea) if not external else '',
  837. ' snap={}'.format(self.snapTolerance) if not external else '',
  838. os.path.normpath(file_path),
  839. ' layer="{}"'.format(layer_name) if layer_name else '',
  840. destFilename)
  841. self.commands.append(command)
  842. def exportVectorLayerFromParameter(self, name, parameters, context, layer=None, nocats=False):
  843. """
  844. Creates a dedicated command to export a vector from
  845. a QgsProcessingParameter.
  846. :param name: name of the parameter.
  847. :param context: parameters context.
  848. :param layer: for vector with multiples layers, exports only one layer.
  849. :param nocats: do not export GRASS categories.
  850. """
  851. fileName = os.path.normpath(
  852. self.parameterAsOutputLayer(parameters, name, context))
  853. grassName = '{}{}'.format(name, self.uniqueSuffix)
  854. # Find if there is a dataType
  855. dataType = self.outType
  856. if self.outType == 'auto':
  857. parameter = self.parameterDefinition(name)
  858. if parameter:
  859. layerType = parameter.dataType()
  860. if layerType in self.QGIS_OUTPUT_TYPES:
  861. dataType = self.QGIS_OUTPUT_TYPES[layerType]
  862. outFormat = QgsVectorFileWriter.driverForExtension(os.path.splitext(fileName)[1]).replace(' ', '_')
  863. dsco = self.parameterAsString(parameters, self.GRASS_VECTOR_DSCO, context)
  864. lco = self.parameterAsString(parameters, self.GRASS_VECTOR_LCO, context)
  865. exportnocat = self.parameterAsBoolean(parameters, self.GRASS_VECTOR_EXPORT_NOCAT, context)
  866. self.exportVectorLayer(grassName, fileName, layer, nocats, dataType, outFormat, dsco, lco, exportnocat)
  867. self.fileOutputs[name] = fileName
  868. def exportVectorLayer(self, grassName, fileName, layer=None, nocats=False, dataType='auto',
  869. outFormat=None, dsco=None, lco=None, exportnocat=False):
  870. """
  871. Creates a dedicated command to export a vector from
  872. temporary GRASS DB into a file via OGR.
  873. :param grassName: name of the vector to export.
  874. :param fileName: file path of vector layer.
  875. :param dataType: export only this type of data.
  876. :param layer: for vector with multiples layers, exports only one layer.
  877. :param nocats: do not export GRASS categories.
  878. :param outFormat: file format for export.
  879. :param dsco: datasource creation options for format.
  880. :param lco: layer creation options for format.
  881. :param exportnocat: do not export features without categories.
  882. """
  883. if outFormat is None:
  884. outFormat = QgsVectorFileWriter.driverForExtension(os.path.splitext(fileName)[1]).replace(' ', '_')
  885. for cmd in [self.commands, self.outputCommands]:
  886. cmd.append(
  887. 'v.out.ogr{} type="{}" input="{}" output="{}" format="{}" {}{}{}{} --overwrite'.format(
  888. '' if nocats else '',
  889. dataType, grassName, fileName,
  890. outFormat,
  891. 'layer={}'.format(layer) if layer else '',
  892. ' dsco="{}"'.format(dsco) if dsco else '',
  893. ' lco="{}"'.format(lco) if lco else '',
  894. ' -c' if exportnocat else ''
  895. )
  896. )
  897. def loadAttributeTableFromParameter(self, name, parameters, context):
  898. """
  899. Creates a dedicated command to load an attribute table
  900. into the temporary GRASS DB.
  901. :param name: name of the parameter
  902. :param parameters: Parameters of the algorithm.
  903. :param context: Processing context
  904. """
  905. table = self.parameterAsVectorLayer(parameters, name, context)
  906. self.loadAttributeTable(name, table, context)
  907. def loadAttributeTable(self, name, layer, context: QgsProcessingContext,
  908. destName=None):
  909. """
  910. Creates a dedicated command to load an attribute table
  911. into the temporary GRASS DB.
  912. :param name: name of the input parameter.
  913. :param layer: a layer object to import from.
  914. :param context: processing context
  915. :param destName: force the name for the table into GRASS DB.
  916. """
  917. self.inputLayers.append(layer)
  918. if not destName:
  919. destName = 'table_{}'.format(os.path.basename(getTempFilename(context=context)))
  920. self.exportedLayers[name] = destName
  921. command = 'db.in.ogr --overwrite input="{}" output="{}"'.format(
  922. os.path.normpath(layer.source()), destName)
  923. self.commands.append(command)
  924. def exportAttributeTable(self, grassName, fileName, outFormat='CSV', layer=1):
  925. """
  926. Creates a dedicated command to export an attribute
  927. table from the temporary GRASS DB into a file via ogr.
  928. :param grassName: name of the parameter.
  929. :param fileName: file path of raster layer.
  930. :param outFormat: file format for export.
  931. :param layer: In GRASS a vector can have multiple layers.
  932. """
  933. for cmd in [self.commands, self.outputCommands]:
  934. cmd.append(
  935. 'db.out.ogr input="{}" output="{}" layer={} format={} --overwrite'.format(
  936. grassName, fileName, layer, outFormat
  937. )
  938. )
  939. def setSessionProjectionFromProject(self, context: QgsProcessingContext):
  940. """
  941. Set the projection from the project.
  942. We create a WKT definition which is transmitted to Grass
  943. """
  944. if not Grass7Utils.projectionSet and iface:
  945. self.setSessionProjection(
  946. iface.mapCanvas().mapSettings().destinationCrs(),
  947. context
  948. )
  949. def setSessionProjectionFromLayer(self, layer: QgsMapLayer, context: QgsProcessingContext):
  950. """
  951. Set the projection from a QgsVectorLayer.
  952. We create a WKT definition which is transmitted to Grass
  953. """
  954. if not Grass7Utils.projectionSet:
  955. self.setSessionProjection(layer.crs(), context)
  956. def setSessionProjection(self, crs, context: QgsProcessingContext):
  957. """
  958. Set the session projection to the specified CRS
  959. """
  960. self.destination_crs = crs
  961. file_name = Grass7Utils.exportCrsWktToFile(crs, context)
  962. command = 'g.proj -c wkt="{}"'.format(file_name)
  963. self.commands.append(command)
  964. Grass7Utils.projectionSet = True
  965. def convertToHtml(self, fileName):
  966. # Read HTML contents
  967. lines = []
  968. with open(fileName, encoding='utf-8') as f:
  969. lines = f.readlines()
  970. if len(lines) > 1 and '<html>' not in lines[0]:
  971. # Then write into the HTML file
  972. with open(fileName, 'w', encoding='utf-8') as f:
  973. f.write('<html><head>')
  974. f.write('<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head>')
  975. f.write('<body><p>')
  976. for line in lines:
  977. f.write('{}</br>'.format(line))
  978. f.write('</p></body></html>')
  979. def canExecute(self):
  980. message = Grass7Utils.checkGrassIsInstalled()
  981. return not message, message
  982. def checkParameterValues(self, parameters, context):
  983. grass_parameters = {k: v for k, v in parameters.items()}
  984. if self.module:
  985. if hasattr(self.module, 'checkParameterValuesBeforeExecuting'):
  986. func = getattr(self.module, 'checkParameterValuesBeforeExecuting')
  987. return func(self, grass_parameters, context)
  988. return super().checkParameterValues(grass_parameters, context)