Skip to content

Commit

Permalink
Support multiple output file raster formats:
Browse files Browse the repository at this point in the history
* A new createopt textbox has been added to the parameters dialog for algorithms which exports to raster files.
* A new metaopt textbox has also been added to the Algorithm parameters dialog.
* Raster file format is detected from output filename extension.
* GdalUtils has been improved to correctly detect raster formats supported for creation.
* QFileDialog for output rasters now display only file filters for supported output raster file formats.
  • Loading branch information
Médéric RIBREUX committed Nov 5, 2017
1 parent aa17df1 commit d10aaf4
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 22 deletions.
36 changes: 31 additions & 5 deletions python/plugins/processing/algs/gdal/GdalUtils.py
Expand Up @@ -61,6 +61,7 @@ class GdalUtils(object):
GDAL_HELP_PATH = 'GDAL_HELP_PATH'

supportedRasters = None
supportedOutputRasters = None

@staticmethod
def runGdal(commands, feedback=None):
Expand Down Expand Up @@ -135,7 +136,10 @@ def getSupportedRasters():
gdal.AllRegister()

GdalUtils.supportedRasters = {}
GdalUtils.supportedOutputRasters = {}
GdalUtils.supportedRasters['GTiff'] = ['tif']
GdalUtils.supportedOutputRasters['GTiff'] = ['tif']

for i in range(gdal.GetDriverCount()):
driver = gdal.GetDriver(i)
if driver is None:
Expand All @@ -146,18 +150,31 @@ def getSupportedRasters():
or metadata[gdal.DCAP_RASTER] != 'YES':
continue

# ===================================================================
# if gdal.DCAP_CREATE not in metadata \
# or metadata[gdal.DCAP_CREATE] != 'YES':
# continue
# ===================================================================
if gdal.DMD_EXTENSION in metadata:
extensions = metadata[gdal.DMD_EXTENSION].split('/')
if extensions:
GdalUtils.supportedRasters[shortName] = extensions
# Only creatable rasters can be referenced in output rasters
if ((gdal.DCAP_CREATE in metadata
and metadata[gdal.DCAP_CREATE] == 'YES')
or (gdal.DCAP_CREATECOPY in metadata
and metadata[gdal.DCAP_CREATECOPY] == 'YES')):
GdalUtils.supportedOutputRasters[shortName] = extensions

return GdalUtils.supportedRasters

@staticmethod
def getSupportedOutputRasters():
if not gdalAvailable:
return {}

if GdalUtils.supportedOutputRasters is not None:
return GdalUtils.supportedOutputRasters
else:
GdalUtils.getSupportedRasters()

return GdalUtils.supportedOutputRasters

@staticmethod
def getSupportedRasterExtensions():
allexts = ['tif']
Expand All @@ -167,6 +184,15 @@ def getSupportedRasterExtensions():
allexts.append(ext)
return allexts

@staticmethod
def getSupportedOutputRasterExtensions():
allexts = ['tif']
for exts in list(GdalUtils.getSupportedOutputRasters().values()):
for ext in exts:
if ext not in allexts and ext != '':
allexts.append(ext)
return allexts

@staticmethod
def getVectorDriverFromFileName(filename):
ext = os.path.splitext(filename)[1]
Expand Down
52 changes: 45 additions & 7 deletions python/plugins/processing/algs/grass7/Grass7Algorithm.py
Expand Up @@ -86,6 +86,8 @@ class Grass7Algorithm(QgsProcessingAlgorithm):
GRASS_REGION_EXTENT_PARAMETER = 'GRASS_REGION_PARAMETER'
GRASS_REGION_CELLSIZE_PARAMETER = 'GRASS_REGION_CELLSIZE_PARAMETER'
GRASS_REGION_ALIGN_TO_RESOLUTION = 'GRASS_REGION_ALIGN_TO_RESOLUTION'
GRASS_RASTER_FORMAT_OPT = 'GRASS_RASTER_FORMAT_OPT'
GRASS_RASTER_FORMAT_META = 'GRASS_RASTER_FORMAT_META'

OUTPUT_TYPES = ['auto', 'point', 'line', 'area']
QGIS_OUTPUT_TYPES = {QgsProcessing.TypeVectorAnyGeometry: 'auto',
Expand Down Expand Up @@ -227,6 +229,7 @@ def defineCharacteristicsFromFile(self):
self.params.append(param)

if hasRasterOutput:
# Add a cellsize parameter
param = QgsProcessingParameterNumber(
self.GRASS_REGION_CELLSIZE_PARAMETER,
self.tr('GRASS GIS 7 region cellsize (leave 0 for default)'),
Expand All @@ -236,6 +239,24 @@ def defineCharacteristicsFromFile(self):
param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
self.params.append(param)

# Add a createopt parameter for format export
param = QgsProcessingParameterString(
self.GRASS_RASTER_FORMAT_OPT,
self.tr('Output Rasters format options (createopt)'),
multiLine=True, optional=True
)
param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
self.params.append(param)

# Add a metadata parameter for format export
param = QgsProcessingParameterString(
self.GRASS_RASTER_FORMAT_META,
self.tr('Output Rasters format metadata options (metaopt)'),
multiLine=True, optional=True
)
param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
self.params.append(param)

if hasVectorInput:
param = QgsProcessingParameterNumber(self.GRASS_SNAP_TOLERANCE_PARAMETER,
self.tr('v.in.ogr snap tolerance (-1 = no snap)'),
Expand Down Expand Up @@ -459,7 +480,9 @@ def processCommand(self, parameters, context, delOutputs=False):
self.GRASS_MIN_AREA_PARAMETER,
self.GRASS_SNAP_TOLERANCE_PARAMETER,
self.GRASS_OUTPUT_TYPE_PARAMETER,
self.GRASS_REGION_ALIGN_TO_RESOLUTION]:
self.GRASS_REGION_ALIGN_TO_RESOLUTION,
self.GRASS_RASTER_FORMAT_OPT,
self.GRASS_RASTER_FORMAT_META]:
continue

# Raster and vector layers
Expand Down Expand Up @@ -622,24 +645,39 @@ def exportRasterLayerFromParameter(self, name, parameters, context, colorTable=T
fileName = os.path.normpath(
self.parameterAsOutputLayer(parameters, name, context))
grassName = '{}{}'.format(name, self.uniqueSuffix)
self.exportRasterLayer(grassName, fileName, colorTable)
outFormat = Grass7Utils.getRasterFormatFromFilename(fileName)
createOpt = self.parameterAsString(parameters, self.GRASS_RASTER_FORMAT_OPT, context)
metaOpt = self.parameterAsString(parameters, self.GRASS_RASTER_FORMAT_META, context)
self.exportRasterLayer(grassName, fileName, colorTable, outFormat, createOpt, metaOpt)

def exportRasterLayer(self, grassName, fileName, colorTable=True):
def exportRasterLayer(self, grassName, fileName,
colorTable=True, outFormat='GTiff',
createOpt=None,
metaOpt=None):
"""
Creates a dedicated command to export a raster from
temporary GRASS DB into a file via gdal.
:param grassName: name of the parameter
:param fileName: file path of raster layer
:param grassName: name of the raster to export.
:param fileName: file path of raster layer.
:param colorTable: preserve color Table.
:param outFormat: file format for export.
:param createOpt: creation options for format.
:param metatOpt: metadata options for export.
"""
if not createOpt:
if outFormat in Grass7Utils.GRASS_RASTER_FORMATS_CREATEOPTS:
createOpt = Grass7Utils.GRASS_RASTER_FORMATS_CREATEOPTS[outFormat]

for cmd in [self.commands, self.outputCommands]:
# Adjust region to layer before exporting
cmd.append('g.region raster={}'.format(grassName))
cmd.append(
'r.out.gdal -m{0} {3} input="{1}" output="{2}"'.format(
'r.out.gdal -c -m{0} input="{1}" output="{2}" format="{3}" {4}{5} --overwrite'.format(
' -t' if colorTable else '',
grassName, fileName,
'--overwrite -c createopt="TFW=YES,COMPRESS=LZW"'
outFormat,
' createopt="{}"'.format(createOpt) if createOpt else '',
' metaopt="{}"'.format(metaOpt) if metaOpt else ''
)
)

Expand Down
Expand Up @@ -38,7 +38,6 @@
from .Grass7Algorithm import Grass7Algorithm
from processing.tools.system import isWindows, isMac
#from .nviz7 import nviz7
from processing.algs.gdal.GdalUtils import GdalUtils

pluginPath = os.path.normpath(os.path.join(
os.path.split(os.path.dirname(__file__))[0], os.pardir))
Expand Down Expand Up @@ -146,11 +145,7 @@ def supportedOutputVectorLayerExtensions(self):
return QgsVectorFileWriter.supportedFormatExtensions()

def supportedOutputRasterLayerExtensions(self):
# We use the same extensions than GDAL because:
# - GRASS is also using GDAL for raster imports.
# - Chances that GRASS is compiled with another version of
# GDAL than QGIS are very limited!
return GdalUtils.getSupportedRasterExtensions()
return Grass7Utils.getSupportedOutputRasterExtensions()

def canBeActivated(self):
return not bool(Grass7Utils.checkGrass7IsInstalled())
Expand Down
32 changes: 32 additions & 0 deletions python/plugins/processing/algs/grass7/Grass7Utils.py
Expand Up @@ -40,6 +40,7 @@
from processing.core.ProcessingConfig import ProcessingConfig
from processing.tools.system import userFolder, isWindows, isMac, mkdir
from processing.tests.TestData import points
from processing.algs.gdal.GdalUtils import GdalUtils


class Grass7Utils(object):
Expand All @@ -55,6 +56,13 @@ class Grass7Utils(object):
GRASS_HELP_PATH = 'GRASS_HELP_PATH'
GRASS_USE_VEXTERNAL = 'GRASS_USE_VEXTERNAL'

# TODO Review all default options formats
GRASS_RASTER_FORMATS_CREATEOPTS = {
'GTiff': 'TFW=YES,COMPRESS=LZW',
'PNG': 'ZLEVEL=9',
'WEBP': 'QUALITY=85'
}

sessionRunning = False
sessionLayers = {}
projectionSet = False
Expand Down Expand Up @@ -515,3 +523,27 @@ def grassHelpPath():
else:
# GRASS not available!
return 'https://grass.osgeo.org/grass72/manuals/'

@staticmethod
def getSupportedOutputRasterExtensions():
# We use the same extensions than GDAL because:
# - GRASS is also using GDAL for raster imports.
# - Chances that GRASS is compiled with another version of
# GDAL than QGIS are very limited!
return GdalUtils.getSupportedOutputRasterExtensions()

@staticmethod
def getRasterFormatFromFilename(filename):
"""
Returns Raster format name from a raster filename.
:param filename: The name with extension of the raster.
:return: The Gdal short format name for extension.
"""
ext = os.path.splitext(filename)[1].lower()
ext = ext.lstrip('.')
supported = GdalUtils.getSupportedRasters()
for name in list(supported.keys()):
exts = supported[name]
if ext in exts:
return name
return 'GTiff'
5 changes: 4 additions & 1 deletion python/plugins/processing/algs/grass7/TODO.md
Expand Up @@ -17,7 +17,6 @@ QGIS3 Processing Port
* TODO Improve unit tests.
* TODO Use prepareAlgorithm for algorithm preparation.
* TODO Support ParameterTable.
* TODO Support multiple output raster formats.
* TODO Support multiple output vector formats.
* TODO Try to use v.external.out on simple algorithms.
* TODO Add an optional/advanced 'format option' textbox if vector output is detected.
Expand Down Expand Up @@ -231,6 +230,10 @@ QGIS3 Processing Port
* v_net_steiner.py
* v_net_visibility.py

* DONE Support multiple output file raster formats.
* DONE Add an optional/advanced 'format option' textbox if raster output is detected.
* DONE Detext file format from extension.
* DONE Improve GdalUtils to report raster formats that can be created with GDAL.
* DONE Add GRASS 7.2 new algorithms.
* DONE Remove r.aspect => r.slope.aspect.
* DONE Remove r.median.
Expand Down
4 changes: 1 addition & 3 deletions python/plugins/processing/gui/ParameterGuiUtils.py
Expand Up @@ -59,9 +59,7 @@ def getFileFilter(param):
elif param.type() == 'raster':
return QgsProviderRegistry.instance().fileRasterFilters()
elif param.type() == 'rasterDestination':
exts = dataobjects.getSupportedOutputRasterLayerExtensions()
for i in range(len(exts)):
exts[i] = tr('{0} files (*.{1})', 'QgsProcessingParameterRasterDestination').format(exts[i].upper(), exts[i].lower())
exts = dataobjects.getSupportedOutputRasterFilters()
return ';;'.join(exts) + ';;' + tr('All files (*.*)')
elif param.type() == 'table':
exts = ['csv', 'dbf']
Expand Down
1 change: 1 addition & 0 deletions python/plugins/processing/gui/wrappers.py
Expand Up @@ -220,6 +220,7 @@ def getFileName(self, initial_value=''):
else:
path = ''

# TODO: should use selectedFilter argument for default file format
filename, selected_filter = QFileDialog.getOpenFileName(self.widget, self.tr('Select file'),
path, getFileFilter(self.param))
if filename:
Expand Down
18 changes: 18 additions & 0 deletions python/plugins/processing/tools/dataobjects.py
Expand Up @@ -117,6 +117,24 @@ def getSupportedOutputRasterLayerExtensions():
return allexts


def getSupportedOutputRasterFilters():
"""
Return a list of file filters for supported raster formats.
Supported formats come from Gdal.
:return: a list of strings for Qt file filters.
"""
allFilters = []
supported = GdalUtils.getSupportedOutputRasters()
formatList = sorted(supported.keys())
# Place GTiff as the first format
if 'GTiff' in formatList:
formatList.pop(formatList.index('GTiff'))
formatList.insert(0, 'GTiff')
for f in formatList:
allFilters.append('{0} files (*.{1})'.format(f, ' *.'.join(supported[f])))
return allFilters


def load(fileName, name=None, crs=None, style=None, isRaster=False):
"""Loads a layer/table into the current project, given its file.
"""
Expand Down

0 comments on commit d10aaf4

Please sign in to comment.