예제 #1
0
    def __init__(self, description_file, script=None):
        super().__init__()

        self.r_templates = RTemplates()
        self.script = script
        self._name = ''
        self._display_name = ''
        self._group = ''
        self.description_file = os.path.realpath(
            description_file) if description_file else None
        self.error = None
        self.commands = list()
        self.is_user_script = False
        if description_file:
            self.is_user_script = not description_file.startswith(
                RUtils.builtin_scripts_folder())

        self.show_plots = False
        self.pass_file_names = False
        self.show_console_output = False
        self.save_output_values = False
        self.plots_filename = ''
        self.output_values_filename = ''
        self.results = {}
        if self.script is not None:
            self.load_from_string()
        if self.description_file is not None:
            self.load_from_file()
예제 #2
0
 def testString(self):  # pylint: disable=too-many-locals,too-many-statements
     """
     Test string variable
     """
     templates = RTemplates()
     self.assertEqual(templates.set_variable_string('var', 'val'),
                      'var <- "val"')
     self.assertEqual(templates.set_variable_string('var', 'va"l'),
                      'var <- "va\\"l"')
예제 #3
0
 def testStringListVariable(self):  # pylint: disable=too-many-locals,too-many-statements
     """
     Test string list variable
     """
     templates = RTemplates()
     self.assertEqual(templates.set_variable_string_list('var', []),
                      'var <- c()')
     self.assertEqual(templates.set_variable_string_list('var', ['aaaa']),
                      'var <- c("aaaa")')
     self.assertEqual(
         templates.set_variable_string_list('var', ['aaaa', 'va"l']),
         'var <- c("aaaa","va\\"l")')
예제 #4
0
 def testGithubInstall(self):
     """
     Test github install code generation.
     """
     templates = RTemplates()
     templates.install_github = True
     templates.github_dependencies = "user_1/repo_1, user_2/repo_2"
     self.assertEqual(
         templates.install_package_github(templates.github_dependencies[0]),
         'remotes::install_github("user_1/repo_1")')
     self.assertEqual(
         templates.install_package_github(templates.github_dependencies[1]),
         'remotes::install_github("user_2/repo_2")')
예제 #5
0
class RAlgorithm(QgsProcessingAlgorithm):  # pylint: disable=too-many-public-methods
    """
    R Script Algorithm
    """

    R_CONSOLE_OUTPUT = 'R_CONSOLE_OUTPUT'
    RPLOTS = 'RPLOTS'

    def __init__(self, description_file, script=None):
        super().__init__()

        self.r_templates = RTemplates()
        self.script = script
        self._name = ''
        self._display_name = ''
        self._group = ''
        self.description_file = os.path.realpath(
            description_file) if description_file else None
        self.error = None
        self.commands = list()
        self.is_user_script = False
        if description_file:
            self.is_user_script = not description_file.startswith(
                RUtils.builtin_scripts_folder())

        self.show_plots = False
        self.pass_file_names = False
        self.show_console_output = False
        self.save_output_values = False
        self.plots_filename = ''
        self.output_values_filename = ''
        self.results = {}
        if self.script is not None:
            self.load_from_string()
        if self.description_file is not None:
            self.load_from_file()

    def createInstance(self):
        """
        Returns a new instance of this algorithm
        """
        if self.description_file is not None:
            return RAlgorithm(self.description_file)

        return RAlgorithm(description_file=None, script=self.script)

    def initAlgorithm(self, _=None):
        """
        Initializes the algorithm
        """
        pass  # pylint: disable=unnecessary-pass

    def icon(self):
        """
        Returns the algorithm's icon
        """
        return GuiUtils.get_icon("providerR.svg")

    def svgIconPath(self):
        """
        Returns a path to the algorithm's icon as a SVG file
        """
        return GuiUtils.get_icon_svg("providerR.svg")

    def name(self):
        """
        Internal unique id for algorithm
        """
        return self._name

    def displayName(self):
        """
        User friendly display name
        """
        return self._display_name

    def shortDescription(self):
        """
        Returns the path to the script file, for use in toolbox tooltips
        """
        return self.description_file

    def group(self):
        """
        Returns the algorithm's group
        """
        return self._group

    def groupId(self):
        """
        Returns the algorithm's group ID
        """
        return self._group

    def load_from_string(self):
        """
        Load the algorithm from a string
        """
        lines = self.script.split('\n')
        self._name = 'unnamedalgorithm'
        self._display_name = self.tr('[Unnamed algorithm]')
        self.parse_script(iter(lines))

    def load_from_file(self):
        """
        Load the algorithm from a file
        """
        filename = os.path.basename(self.description_file)
        self._display_name = self._name
        self._name = filename[:filename.rfind('.')]
        self._display_name = self._name.replace('_', ' ')
        with open(self.description_file, 'r') as f:
            lines = [line.strip() for line in f]
        self.parse_script(iter(lines))

    def parse_script(self, lines):
        """
        Parse the lines from an R script, initializing parameters and outputs as encountered
        """
        self.script = ''
        self.commands = list()
        self.error = None
        self.show_plots = False
        self.show_console_output = False
        self.pass_file_names = False
        ender = 0
        line = next(lines).strip('\n').strip('\r')
        while ender < 10:
            if line.startswith('##'):
                try:
                    self.process_metadata_line(line)
                except Exception:  # pylint: disable=broad-except
                    self.error = self.tr('This script has a syntax error.\n'
                                         'Problem with line: {0}').format(line)
            elif line.startswith('>'):
                self.commands.append(line[1:])
                if not self.show_console_output:
                    self.addParameter(
                        QgsProcessingParameterFileDestination(
                            RAlgorithm.R_CONSOLE_OUTPUT,
                            self.tr('R Console Output'),
                            self.tr('HTML files (*.html)'),
                            optional=True))
                self.show_console_output = True
            else:
                if line == '':
                    ender += 1
                else:
                    ender = 0
                self.commands.append(line)
            self.script += line + '\n'
            try:
                line = next(lines).strip('\n').strip('\r')
            except StopIteration:
                break

    def process_metadata_line(self, line):  # pylint: disable=too-many-return-statements
        """
        Processes a "metadata" (##) line
        """
        line = line.replace('#', '')

        # special commands
        # showplots is the older version, should be considere obsolete
        if line.lower().strip().startswith('output_plots_to_html') or \
           line.lower().strip().startswith('showplots'):
            self.show_plots = True
            self.addParameter(
                QgsProcessingParameterFileDestination(
                    RAlgorithm.RPLOTS,
                    self.tr('R Plots'),
                    self.tr('HTML files (*.html)'),
                    optional=True))
            return

        # dontuserasterpackage is the older version, should be considere obsolete
        if line.lower().strip().startswith('load_raster_using_rgdal') or \
           line.lower().strip().startswith('dontuserasterpackage'):
            self.r_templates.use_raster = False
            return

        if line.lower().strip().startswith('load_vector_using_rgdal'):
            self.r_templates.use_sf = False
            return

        # passfilenames is the older version, should be considere obsolete
        if line.lower().strip().startswith('pass_filenames') or\
           line.lower().strip().startswith('passfilenames'):
            self.pass_file_names = True
            return

        if line.lower().strip().startswith('dont_load_any_packages'):
            self.r_templates.auto_load_packages = False
            return

        value, type_ = self.split_tokens(line)
        if type_.lower().strip() == 'group':
            self._group = value
            return
        if type_.lower().strip() == 'name':
            self._name = self._display_name = value
            self._name = RUtils.strip_special_characters(self._name.lower())
            return
        if type_.lower().strip() == 'github_install':
            self.r_templates.install_github = True
            self.r_templates.github_dependencies = value
            return

        # process enum with values and preparing its template
        if "=enum literal" in RUtils.upgrade_parameter_line(line):
            self.r_templates.add_literal_enum(value)

        self.process_parameter_line(line)

    @staticmethod
    def split_tokens(line):
        """
        Attempts to split a line into tokens
        """
        tokens = line.split('=')
        return tokens[0], tokens[1]

    def process_parameter_line(self, line):
        """
        Processes a single script line representing a parameter
        """
        value, _ = self.split_tokens(line)
        description = RUtils.create_descriptive_name(value)

        if not RUtils.is_valid_r_variable(value):
            self.error = self.tr(
                'This script has a syntax error in variable name.\n'
                '"{1}" is not a valid variable name in R.'
                'Problem with line: {0}').format(line, value)

        output = create_output_from_string(line)
        if output is not None:
            output.setName(value)
            output.setDescription(description)
            if issubclass(output.__class__, QgsProcessingOutputDefinition):
                self.addOutput(output)
                self.save_output_values = True
            else:
                # destination type parameter
                self.addParameter(output)
        else:
            line = RUtils.upgrade_parameter_line(line)

            # this is necessary to remove the otherwise unknown keyword
            line = line.replace("enum literal", "enum")

            # this is annoying, but required to work around a bug in early 3.8.0 versions
            try:
                param = getParameterFromString(line, context="")
            except TypeError:
                param = getParameterFromString(line)

            if param is not None:
                self.addParameter(param)
            else:
                self.error = self.tr('This script has a syntax error.\n'
                                     'Problem with line: {0}').format(line)

    def canExecute(self):
        """
        Returns True if the algorithm can be executed
        """
        if self.error:
            return False, self.error

        msg = RUtils.check_r_is_installed()
        if msg is not None:
            return False, msg

        return True, ''

    def processAlgorithm(self, parameters, context, feedback):
        """
        Executes the algorithm
        """
        self.results = {}

        if RUtils.is_windows():
            path = RUtils.r_binary_folder()
            if path == '':
                raise QgsProcessingException(
                    self.tr('R folder is not configured.\nPlease configure it '
                            'before running R scripts.'))

        feedback.pushInfo(self.tr('R execution commands'))

        output = RUtils.execute_r_algorithm(self, parameters, context,
                                            feedback)

        if self.show_plots:
            html_filename = self.parameterAsFileOutput(parameters,
                                                       RAlgorithm.RPLOTS,
                                                       context)
            if html_filename:
                with open(html_filename, 'w') as f:
                    f.write('<html><img src="{}"/></html>'.format(
                        QUrl.fromLocalFile(self.plots_filename).toString()))
                self.results[RAlgorithm.RPLOTS] = html_filename
        if self.show_console_output:
            html_filename = self.parameterAsFileOutput(
                parameters, RAlgorithm.R_CONSOLE_OUTPUT, context)
            if html_filename:
                with open(html_filename, 'w') as f:
                    f.write(RUtils.html_formatted_console_output(output))
                self.results[RAlgorithm.R_CONSOLE_OUTPUT] = html_filename

        if self.save_output_values and self.output_values_filename:
            with open(self.output_values_filename, 'r') as f:
                lines = [line.strip() for line in f]
            # get output values stored into the file
            outputs = self.parse_output_values(iter(lines))
            # merge output values into results
            for k, v in outputs.items():
                if k not in self.results:
                    self.results[k] = v

        return self.results

    def parse_output_values(self, lines):
        """
        Parse the lines from an output values file and returns a dict of output values

        The lines are:
        ##output1_name
        output1_value
        ##output2_name
        output2_value
        """
        outputs = {}
        if not self.save_output_values:
            return outputs

        output = None
        ender = 0
        line = next(lines).strip('\n').strip('\r')
        while ender < 10:
            if line.startswith('##'):
                name = line.replace('#', '')
                output = self.outputDefinition(name)
            else:
                if line == '':
                    ender += 1
                else:
                    ender = 0
                if not output:
                    continue
                if output.name() in outputs:
                    outputs[output.name()] += '\n\r' + line
                else:
                    outputs[output.name()] = line
            try:
                line = next(lines).strip('\n').strip('\r')
            except StopIteration:
                break

        return outputs

    def build_r_script(self, parameters, context, feedback):
        """
        Builds up the set of R commands to run for the script
        """
        commands = []
        commands += self.build_script_header_commands(parameters, context,
                                                      feedback)
        commands += self.build_import_commands(parameters, context, feedback)
        commands += self.build_r_commands(parameters, context, feedback)
        commands += self.build_export_commands(parameters, context, feedback)

        return commands

    def build_export_commands(self, parameters, context, _):
        """
        Builds up the set of R commands for exporting results
        """
        commands = []
        for out in self.destinationParameterDefinitions():
            if isinstance(out, QgsProcessingParameterRasterDestination):
                dest = self.parameterAsOutputLayer(parameters, out.name(),
                                                   context)
                dest = dest.replace('\\', '/')
                commands.append(
                    self.r_templates.write_raster_output(out.name(), dest))
                self.results[out.name()] = dest
            elif isinstance(out, QgsProcessingParameterVectorDestination):
                dest = self.parameterAsOutputLayer(parameters, out.name(),
                                                   context)
                dest = dest.replace('\\', '/')
                filename = os.path.basename(dest)
                filename, ext = os.path.splitext(filename)
                if ext.lower() == '.csv':
                    # CSV table export
                    commands.append(
                        self.r_templates.write_csv_output(out.name(), dest))
                else:
                    commands.append(
                        self.r_templates.write_vector_output(
                            out.name(), dest, filename,
                            QgsVectorFileWriter.driverForExtension(ext)))
                self.results[out.name()] = dest

        if self.save_output_values:
            for out in self.outputDefinitions():
                name = out.name()
                # write values only if output is not already in results
                if name in (self.R_CONSOLE_OUTPUT, self.RPLOTS):
                    continue
                if name in self.results:
                    continue
                # create file path for output values only if it is necessary
                if not self.output_values_filename:
                    self.output_values_filename = QgsProcessingUtils.generateTempFilename(
                        'processing_values.txt')
                # write output name and value with cat
                commands.extend(
                    self.r_templates.write_cat_output(
                        name, self.output_values_filename))

        if self.show_plots:
            commands.append(self.r_templates.dev_off())

        return commands

    def load_vector_layer_from_parameter(self, name, parameters, context,
                                         feedback):
        """
        Creates a dedicated command to load a vector into the workspace.
        :param name: name of the parameter
        :param parameters: Parameters of the algorithm.
        :param context: Processing context
        """
        if Qgis.QGIS_VERSION_INT >= 30900 and hasattr(
                self, 'parameterAsCompatibleSourceLayerPathAndLayerName'):
            # requires qgis 3.10 or later!
            ogr_data_path, layer_name = self.parameterAsCompatibleSourceLayerPathAndLayerName(
                parameters,
                name,
                context,
                QgsVectorFileWriter.supportedFormatExtensions(),
                feedback=feedback)
            if layer_name:
                return self.r_templates.set_variable_vector(
                    name, QDir.fromNativeSeparators(ogr_data_path), layer_name)

            return self.r_templates.set_variable_vector(
                name, QDir.fromNativeSeparators(ogr_data_path))

        ogr_data_path = self.parameterAsCompatibleSourceLayerPath(
            parameters,
            name,
            context,
            QgsVectorFileWriter.supportedFormatExtensions(),
            feedback=feedback)
        ogr_layer = QgsVectorLayer(ogr_data_path, '', 'ogr')
        return self.load_vector_layer_command(name, ogr_layer, feedback)

    def build_vector_layer_import_command(self, variable_name, layer, context,
                                          feedback):
        """
        Returns an import command for the specified vector layer, storing it in a variable
        """
        if layer is None:
            return self.r_templates.set_variable_null(variable_name)

        is_ogr_disk_based_layer = layer is not None and layer.dataProvider(
        ).name() == 'ogr'
        if is_ogr_disk_based_layer:
            # we only support direct reading of disk based ogr layers -- not ogr postgres layers, etc
            source_parts = QgsProviderRegistry.instance().decodeUri(
                'ogr', layer.source())
            if not source_parts.get('path'):
                is_ogr_disk_based_layer = False
            elif source_parts.get('layerId'):
                # no support for directly reading layers by id in R
                is_ogr_disk_based_layer = False

        if not is_ogr_disk_based_layer:
            # parameter is not a vector layer or not an OGR layer - try to convert to a source compatible with
            # R OGR inputs and extract selection if required
            path = QgsProcessingUtils.convertToCompatibleFormat(
                layer,
                False,
                variable_name,
                compatibleFormats=QgsVectorFileWriter.
                supportedFormatExtensions(),
                preferredFormat='gpkg',
                context=context,
                feedback=feedback)
            ogr_layer = QgsVectorLayer(path, '', 'ogr')
            return self.load_vector_layer_command(variable_name, ogr_layer,
                                                  None)

        # already an ogr disk based layer source
        return self.load_vector_layer_command(variable_name, layer, None)

    def load_vector_layer_command(self, name, layer, _):
        """
        Creates a command to load a vector layer into the workspace
        """
        source_parts = QgsProviderRegistry.instance().decodeUri(
            'ogr', layer.source())
        file_path = source_parts.get('path')
        if self.pass_file_names:
            return self.r_templates.set_variable_string(
                name, QDir.fromNativeSeparators(file_path))

        layer_name = source_parts.get('layerName')
        if layer_name:
            return self.r_templates.set_variable_vector(
                name, QDir.fromNativeSeparators(file_path), layer=layer_name)

        # no layer name -- readOGR expects the folder, with the filename as layer
        return self.r_templates.set_variable_vector(
            name, QDir.fromNativeSeparators(file_path))

    def build_script_header_commands(self, _, __, ___):
        """
        Builds the set of script startup commands for the algorithm
        """
        return self.r_templates.build_script_header_commands(self.script)

    def build_raster_layer_import_command(self, variable_name, layer):
        """
        Returns an import command for the specified raster layer, storing it in a variable
        """
        if layer is None:
            return self.r_templates.set_variable_null(variable_name)

        if layer.dataProvider().name() != 'gdal':
            raise QgsProcessingException(
                self.
                tr("Layer {} is not a GDAL layer. Currently only GDAL based raster layers are supported."
                   ).format(variable_name))

        path = QgsProviderRegistry.instance().decodeUri(
            layer.dataProvider().name(), layer.source())['path']
        value = QDir.fromNativeSeparators(path)
        if self.pass_file_names:
            return self.r_templates.set_variable_string(variable_name, value)

        return self.r_templates.set_variable_raster(variable_name, value)

    def build_import_commands(
            self,  # pylint: disable=too-many-locals,too-many-branches,too-many-statements
            parameters,
            context,
            feedback):
        """
        Builds the set of input commands for the algorithm
        """
        commands = list()

        for param in self.parameterDefinitions():
            if param.isDestination():
                continue

            if param.name(
            ) not in parameters or parameters[param.name()] is None:
                commands.append(
                    self.r_templates.set_variable_null(param.name()))
                continue

            if isinstance(param, QgsProcessingParameterRasterLayer):
                rl = self.parameterAsRasterLayer(parameters, param.name(),
                                                 context)
                commands.append(
                    self.build_raster_layer_import_command(param.name(), rl))
            elif isinstance(param, QgsProcessingParameterVectorLayer):
                commands.append(
                    self.load_vector_layer_from_parameter(
                        param.name(), parameters, context, feedback))
            elif isinstance(param, QgsProcessingParameterFeatureSource):
                commands.append(
                    self.load_vector_layer_from_parameter(
                        param.name(), parameters, context, feedback))
            elif isinstance(param, QgsProcessingParameterExtent):
                extent = self.parameterAsExtent(parameters, param.name(),
                                                context)
                # Extent from raster package is "xmin, xmax, ymin, ymax" like in Processing
                # http://www.inside-r.org/packages/cran/raster/docs/Extent
                commands.append(
                    self.r_templates.set_variable_extent(
                        param.name(), extent.xMinimum(), extent.xMaximum(),
                        extent.yMinimum(), extent.yMaximum()))
            elif isinstance(param, QgsProcessingParameterCrs):
                crs = self.parameterAsCrs(parameters, param.name(), context)
                if crs.isValid():
                    commands.append(
                        self.r_templates.set_variable_string(
                            param.name(), crs.authid()))
                else:
                    commands.append(
                        self.r_templates.set_variable_null(param.name()))
            elif isinstance(param, QgsProcessingParameterFile):
                value = self.parameterAsString(parameters, param.name(),
                                               context)
                commands.append(
                    self.r_templates.set_variable_string(
                        param.name(), QDir.fromNativeSeparators(value)))
            elif isinstance(param, QgsProcessingParameterString):
                value = self.parameterAsString(parameters, param.name(),
                                               context)
                commands.append(
                    self.r_templates.set_variable_string(param.name(), value))
            elif isinstance(param, QgsProcessingParameterField):
                if param.allowMultiple():
                    value = self.parameterAsFields(parameters, param.name(),
                                                   context)
                    commands.append(
                        self.r_templates.set_variable_string_list(
                            param.name(), value))
                else:
                    value = self.parameterAsString(parameters, param.name(),
                                                   context)
                    commands.append(
                        self.r_templates.set_variable_string(
                            param.name(), value))
            elif isinstance(param, QgsProcessingParameterNumber):
                value = self.parameterAsDouble(parameters, param.name(),
                                               context)
                commands.append(
                    self.r_templates.set_variable_directly(
                        param.name(), value))
            elif isinstance(param, QgsProcessingParameterEnum):
                value = self.parameterAsEnum(parameters, param.name(), context)
                if self.r_templates.is_literal_enum(param.name()):
                    enum_values = self.parameterDefinition(
                        param.name()).options()
                    commands.append(
                        self.r_templates.set_variable_enum_value(
                            param.name(), value, enum_values))
                else:
                    commands.append(
                        self.r_templates.set_variable_directly(
                            param.name(), value))
            elif isinstance(param, QgsProcessingParameterBoolean):
                value = self.parameterAsBool(parameters, param.name(), context)
                value = 'TRUE' if value else 'FALSE'
                commands.append(
                    self.r_templates.set_variable_directly(
                        param.name(), value))
            elif isinstance(param, QgsProcessingParameterMultipleLayers):
                layer_idx = 0
                layers = self.parameterAsLayerList(parameters, param.name(),
                                                   context)
                if param.layerType() == QgsProcessing.TypeRaster:
                    for layer in layers:
                        variable_name = 'tempvar{}'.format(layer_idx)
                        commands.append(
                            self.build_raster_layer_import_command(
                                variable_name, layer))
                        layer_idx += 1
                else:
                    for layer in layers:
                        variable_name = 'tempvar{}'.format(layer_idx)
                        commands.append(
                            self.build_vector_layer_import_command(
                                variable_name, layer, context, feedback))
                        layer_idx += 1

                s = ''
                s += param.name()
                s += ' = c('
                layer_idx = 0
                for _ in layers:
                    if layer_idx != 0:
                        s += ','
                    s += 'tempvar{}'.format(layer_idx)
                    layer_idx += 1
                s += ')'
                commands.append(s)

        # folder, file/html output paths
        for param in self.destinationParameterDefinitions():
            if isinstance(param, QgsProcessingParameterFolderDestination):
                folder = self.parameterAsString(parameters, param.name(),
                                                context)
                Path(folder).mkdir(parents=True, exist_ok=True)
                commands.append(
                    self.r_templates.set_variable_string(
                        param.name(), QDir.fromNativeSeparators(folder)))
                self.save_output_values = True
            elif isinstance(param, QgsProcessingParameterFileDestination):
                filename = self.parameterAsFileOutput(parameters, param.name(),
                                                      context)
                Path(filename).parent.mkdir(parents=True, exist_ok=True)
                commands.append(
                    self.r_templates.set_variable_string(
                        param.name(), QDir.fromNativeSeparators(filename)))
                self.save_output_values = True

        if self.show_plots:
            html_filename = self.parameterAsFileOutput(parameters,
                                                       RAlgorithm.RPLOTS,
                                                       context)
            path, _ = os.path.splitext(html_filename)
            self.plots_filename = path + '.png'
            self.plots_filename = self.plots_filename.replace('\\', '/')
            commands.append(self.r_templates.create_png(self.plots_filename))

        return commands

    def build_r_commands(self, _, __, ___):
        """
        Returns the body of the R script
        """
        return self.commands

    def shortHelpString(self):
        """
        Returns the algorithms helper string
        """
        if self.description_file is None:
            return ''

        help_file = self.description_file + '.help'
        print(help_file)
        if os.path.exists(help_file):
            with open(help_file) as f:
                descriptions = json.load(f)

            return QgsProcessingUtils.formatHelpMapAsHtml(descriptions, self)

        return ''

    def tr(self, string, context=''):
        """
        Translates a string
        """
        if context == '':
            context = 'RAlgorithmProvider'
        return QCoreApplication.translate(context, string)