Skip to content

Commit

Permalink
Merge pull request #7396 from luipir/rastercalculator_model_fix
Browse files Browse the repository at this point in the history
[processing] Rebirth RasterCalculator in Modeler. Fixes #19302
  • Loading branch information
luipir committed Sep 4, 2018
2 parents 42aea6c + 30eddf5 commit 580ecaf
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 38 deletions.
129 changes: 100 additions & 29 deletions python/plugins/processing/algs/qgis/RasterCalculator.py
Expand Up @@ -40,7 +40,9 @@
QgsProcessingParameterRasterLayer,
QgsProcessingOutputRasterLayer,
QgsProcessingParameterString,
QgsCoordinateTransform)
QgsCoordinateTransform,
QgsMapLayer)
from qgis.PyQt.QtCore import QObject
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry


Expand Down Expand Up @@ -77,23 +79,6 @@ def type(self):
def clone(self):
return ParameterRasterCalculatorExpression(self.name(), self.description(), self.multiLine())

def evaluateForModeler(self, value, model):
for i in list(model.inputs.values()):
param = i.param
if isinstance(param, QgsProcessingParameterRasterLayer):
new = "{}@".format(os.path.basename(param.value))
old = "{}@".format(param.name())
value = value.replace(old, new)

for alg in list(model.algs.values()):
for out in alg.algorithm.outputs:
if isinstance(out, QgsProcessingOutputRasterLayer):
if out.value:
new = "{}@".format(os.path.basename(out.value))
old = "{}:{}@".format(alg.modeler_name, out.name)
value = value.replace(old, new)
return value

self.addParameter(ParameterRasterCalculatorExpression(self.EXPRESSION, self.tr('Expression'),
multiLine=True))
self.addParameter(QgsProcessingParameterMultipleLayers(self.LAYERS,
Expand Down Expand Up @@ -122,17 +107,17 @@ def processAlgorithm(self, parameters, context, feedback):

layersDict = {}
if layers:
layersDict = {os.path.basename(lyr.source().split(".")[0]): lyr for lyr in layers}
layersDict = {lyr.source(): lyr for lyr in layers}

crs = self.parameterAsCrs(parameters, self.CRS, context)
if not layers and not crs.isValid():
raise QgsProcessingException(self.tr("No reference layer selected nor CRS provided"))

if not crs.isValid() and layers:
crs = list(layersDict.values())[0].crs()
if crs is None or not crs.isValid():
if not layers:
raise QgsProcessingException(self.tr("No reference layer selected nor CRS provided"))
else:
crs = list(layersDict.values())[0].crs()

bbox = self.parameterAsExtent(parameters, self.EXTENT, context)
if not layers and bbox.isNull():
if bbox.isNull() and not layers:
raise QgsProcessingException(self.tr("No reference layer selected nor extent box provided"))

if not bbox.isNull():
Expand All @@ -145,7 +130,7 @@ def processAlgorithm(self, parameters, context, feedback):
bbox = QgsProcessingUtils.combineLayerExtents(layers, crs)

cellsize = self.parameterAsDouble(parameters, self.CELLSIZE, context)
if not layers and cellsize == 0:
if cellsize == 0 and not layers:
raise QgsProcessingException(self.tr("No reference layer selected nor cellsize value provided"))

def _cellsize(layer):
Expand All @@ -157,15 +142,23 @@ def _cellsize(layer):
if cellsize == 0:
cellsize = min([_cellsize(lyr) for lyr in layersDict.values()])

# check for layers available in the model
layersDictCopy = layersDict.copy() # need a shallow copy because next calls invalidate iterator
for lyr in layersDictCopy.values():
expression = self.mappedNameToLayer(lyr, expression, layersDict, context)

# check for layers available in the project
for lyr in QgsProcessingUtils.compatibleRasterLayers(context.project()):
name = lyr.name()
if (name + "@") in expression:
layersDict[name] = lyr
expression = self.mappedNameToLayer(lyr, expression, layersDict, context)

# create the list of layers to be passed as inputs to RasterCalculaltor
# at this phase expression has been modified to match available layers
# in the current scope
entries = []
for name, lyr in layersDict.items():
for n in range(lyr.bandCount()):
ref = '{:s}@{:d}'.format(name, n + 1)

if ref in expression:
entry = QgsRasterCalculatorEntry()
entry.ref = ref
Expand All @@ -178,6 +171,7 @@ def _cellsize(layer):
width = math.floor((bbox.xMaximum() - bbox.xMinimum()) / cellsize)
height = math.floor((bbox.yMaximum() - bbox.yMinimum()) / cellsize)
driverName = GdalUtils.getFormatShortNameFromFilename(output)

calc = QgsRasterCalculator(expression,
output,
driverName,
Expand Down Expand Up @@ -213,3 +207,80 @@ def processBeforeAddingToModeler(self, algorithm, model):
values.append(ValueFromOutput(alg.modeler_name, out.name))

algorithm.params[self.LAYERS] = values

def mappedNameToLayer(self, lyr, expression, layersDict, context):
'''Try to identify if a real layer is mapped in the expression with a symbolic name.'''

nameToMap = lyr.source()

# check if nameToMap is a file
# TODO: what about URI eg for a COG?
if os.path.isfile(nameToMap):
# get only the name without extension and path of the file
nameToMap = os.path.splitext(os.path.basename(nameToMap))[0]

# check for layers directly added in the expression
if (nameToMap + "@") in expression:
layersDict[nameToMap] = lyr

# get "algorithm_inputs" scope of the expressionContext related
# with mapped variables
indexOfScope = context.expressionContext().indexOfScope("algorithm_inputs")
if indexOfScope >= 0:
expContextAlgInputsScope = context.expressionContext().scope(indexOfScope)

# check for the layers that are mapped as input in a model
# to do this check in the latest scope all passed variables
# to look for a variable that is a layer or a string filename ç
# to a layer
varDescription = None
for varName in expContextAlgInputsScope.variableNames():

layerInContext = expContextAlgInputsScope.variable(varName)

if not isinstance(layerInContext, str) and not isinstance(layerInContext, QgsMapLayer):
continue

if isinstance(layerInContext, QgsMapLayer) and nameToMap not in layerInContext.source():
continue

varDescription = expContextAlgInputsScope.description(varName)

# because there can be variable with None or "" description
# then skip them
if not varDescription:
continue

# check if it's description starts with Output as in:
# Output 'Output' from algorithm 'calc1'
# as set in https://github.com/qgis/QGIS/blob/master/src/core/processing/models/qgsprocessingmodelalgorithm.cpp#L516
# but var in expression is called simply
# 'Output' from algorithm 'calc1'

# get the translatin string to use to parse the description
# HAVE to use the same translated string as in
# https://github.com/qgis/QGIS/blob/master/src/core/processing/models/qgsprocessingmodelalgorithm.cpp#L516
translatedDesc = self.tr("Output '%1' from algorithm '%2'")
elementZero = translatedDesc.split(" ")[0] # For english the string result should be "Output"

elements = varDescription.split(" ")
if len(elements) > 1 and elements[0] == elementZero:
# remove heading QObject.tr"Output ") string. Note adding a space at the end of elementZero!
varDescription = varDescription[len(elementZero) + 1:]

# check if cleaned varDescription is present in the expression
# if not skip it
if (varDescription + "@") not in expression:
continue

# !!!found!!! => substitute in expression
# and add in the list of layers that will be passed to raster calculator
nameToMap = varName
new = "{}@".format(nameToMap)
old = "{}@".format(varDescription)
expression = expression.replace(old, new)

layersDict[nameToMap] = lyr

# need return the modified expression because it's not a reference
return expression
Expand Up @@ -214,10 +214,11 @@ def createWidget(self):
return QLineEdit()
else:
layers = self.dialog.getAvailableValuesOfType([QgsProcessingParameterRasterLayer], [QgsProcessingOutputRasterLayer])
options = {self.dialog.resolveValueDescription(lyr): "{}@1".format(lyr) for lyr in layers}
options = {self.dialog.resolveValueDescription(lyr): "{}@1".format(self.dialog.resolveValueDescription(lyr)) for lyr in layers}
return self._panel(options)

def refresh(self):
# TODO: check if avoid code duplication with self.createWidget
layers = QgsProcessingUtils.compatibleRasterLayers(QgsProject.instance())
options = {}
for lyr in layers:
Expand Down
Binary file not shown.
5 changes: 4 additions & 1 deletion src/core/processing/models/qgsprocessingmodelalgorithm.cpp
Expand Up @@ -273,6 +273,7 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa
QgsExpressionContext expContext = baseContext;
expContext << QgsExpressionContextUtils::processingAlgorithmScope( child.algorithm(), parameters, context )
<< createExpressionContextScopeForChildAlgorithm( childId, context, parameters, childResults );
context.setExpressionContext( expContext );

QVariantMap childParams = parametersForChildAlgorithm( child, parameters, childResults, expContext );
if ( feedback )
Expand Down Expand Up @@ -530,6 +531,7 @@ QMap<QString, QgsProcessingModelAlgorithm::VariableDefinition> QgsProcessingMode
if ( !layer )
layer = QgsProcessingUtils::mapLayerFromString( value.toString(), context );

variables.insert( safeName( name ), VariableDefinition( QVariant::fromValue( layer ), source, description ) );
variables.insert( safeName( QStringLiteral( "%1_minx" ).arg( name ) ), VariableDefinition( layer ? layer->extent().xMinimum() : QVariant(), source, QObject::tr( "Minimum X of %1" ).arg( description ) ) );
variables.insert( safeName( QStringLiteral( "%1_miny" ).arg( name ) ), VariableDefinition( layer ? layer->extent().yMinimum() : QVariant(), source, QObject::tr( "Minimum Y of %1" ).arg( description ) ) );
variables.insert( safeName( QStringLiteral( "%1_maxx" ).arg( name ) ), VariableDefinition( layer ? layer->extent().xMaximum() : QVariant(), source, QObject::tr( "Maximum X of %1" ).arg( description ) ) );
Expand Down Expand Up @@ -595,6 +597,7 @@ QMap<QString, QgsProcessingModelAlgorithm::VariableDefinition> QgsProcessingMode
featureSource = vl;
}

variables.insert( safeName( name ), VariableDefinition( value, source, description ) );
variables.insert( safeName( QStringLiteral( "%1_minx" ).arg( name ) ), VariableDefinition( featureSource ? featureSource->sourceExtent().xMinimum() : QVariant(), source, QObject::tr( "Minimum X of %1" ).arg( description ) ) );
variables.insert( safeName( QStringLiteral( "%1_miny" ).arg( name ) ), VariableDefinition( featureSource ? featureSource->sourceExtent().yMinimum() : QVariant(), source, QObject::tr( "Minimum Y of %1" ).arg( description ) ) );
variables.insert( safeName( QStringLiteral( "%1_maxx" ).arg( name ) ), VariableDefinition( featureSource ? featureSource->sourceExtent().xMaximum() : QVariant(), source, QObject::tr( "Maximum X of %1" ).arg( description ) ) );
Expand All @@ -606,7 +609,7 @@ QMap<QString, QgsProcessingModelAlgorithm::VariableDefinition> QgsProcessingMode

QgsExpressionContextScope *QgsProcessingModelAlgorithm::createExpressionContextScopeForChildAlgorithm( const QString &childId, QgsProcessingContext &context, const QVariantMap &modelParameters, const QVariantMap &results ) const
{
std::unique_ptr< QgsExpressionContextScope > scope( new QgsExpressionContextScope() );
std::unique_ptr< QgsExpressionContextScope > scope( new QgsExpressionContextScope( QStringLiteral( "algorithm_inputs" ) ) );
QMap< QString, QgsProcessingModelAlgorithm::VariableDefinition> variables = variablesForChildAlgorithm( childId, context, modelParameters, results );
QMap< QString, QgsProcessingModelAlgorithm::VariableDefinition>::const_iterator varIt = variables.constBegin();
for ( ; varIt != variables.constEnd(); ++varIt )
Expand Down

0 comments on commit 580ecaf

Please sign in to comment.