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