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