├── .github ├── workflow-fail-template.md └── workflows │ ├── codestyle.yaml │ ├── mkdocs_build_deploy.yaml │ └── test_plugin.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── REQUIREMENTS_TESTING.txt ├── processing_r ├── __init__.py ├── builtin_scripts │ ├── sample.rsx │ ├── sample2.rsx │ ├── sample3.rsx │ ├── sample4.rsx │ └── sample5.rsx ├── gui │ ├── __init__.py │ ├── gui_utils.py │ └── script_editor │ │ ├── __init__.py │ │ ├── script_editor_dialog.py │ │ └── script_editor_widget.py ├── i18n │ ├── af.qm │ └── af.ts ├── icon.png ├── images │ └── providerR.svg ├── metadata.txt ├── processing │ ├── __init__.py │ ├── actions │ │ ├── __init__.py │ │ ├── create_new_script.py │ │ ├── delete_script.py │ │ └── edit_script.py │ ├── algorithm.py │ ├── exceptions.py │ ├── outputs.py │ ├── parameters.py │ ├── provider.py │ ├── r_templates.py │ └── utils.py ├── r_plugin.py └── ui │ └── DlgScriptEditor.ui ├── pylintrc ├── pyproject.toml ├── scripts ├── compile-strings.sh ├── run-env-linux.sh ├── run_docker_locally.sh └── update-strings.sh ├── tests ├── conftest.py ├── data │ ├── dem.tif │ ├── dem.tif.aux.xml │ ├── dem2.tif │ ├── dem2.tif.aux.xml │ ├── layers.gpx │ ├── lines.dbf │ ├── lines.prj │ ├── lines.shp │ ├── lines.shx │ ├── points.gml │ ├── points.xsd │ └── test_gpkg.gpkg ├── scripts │ ├── bad_algorithm.rsx │ ├── test_algorithm_1.rsx │ ├── test_algorithm_1.rsx.help │ ├── test_algorithm_2.rsx │ ├── test_algorithm_3.rsx │ ├── test_algorithm_4.rsx │ ├── test_algorithm_inline_help.rsx │ ├── test_dont_load_any_packages.rsx │ ├── test_enum_multiple.rsx │ ├── test_enums.rsx │ ├── test_field_multiple.rsx │ ├── test_field_names.rsx │ ├── test_input_color.rsx │ ├── test_input_datetime.rsx │ ├── test_input_expression.rsx │ ├── test_input_point.rsx │ ├── test_input_range.rsx │ ├── test_library_with_option.rsx │ ├── test_multiout.rsx │ ├── test_multirasterin.rsx │ ├── test_multivectorin.rsx │ ├── test_plots.rsx │ ├── test_raster_band.rsx │ ├── test_raster_in_out.rsx │ ├── test_rasterin_names.rsx │ ├── test_vectorin.rsx │ └── test_vectorout.rsx ├── test_algorithm.py ├── test_algorithm_inputs.py ├── test_algorithm_metadata.py ├── test_algorithm_outputs.py ├── test_algorithm_script_parsing.py ├── test_gui_utils.py ├── test_provider.py ├── test_r_template.py ├── test_r_utils.py └── utils.py └── website ├── mkdocs.yml └── pages ├── changelog.md ├── ex_console_output.md ├── ex_expression.md ├── ex_file_output.md ├── ex_number_string_output.md ├── ex_plot.md ├── ex_raster_output.md ├── ex_table_output.md ├── ex_vector_output.md ├── help-syntax.md ├── images ├── console_output.jpg ├── graph_location.jpg └── settings.jpg ├── index.md └── script-syntax.md /.github/workflow-fail-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Workflow {{ env.WORKFLOW }} failed for {{ env.DOCKER_TAG }} 3 | labels: bug 4 | --- 5 | 6 | Workflow {{ env.WORKFLOW }} failed for {{ env.DOCKER_TAG }} at: {{ date | date('YYYY-MM-DD HH:mm:ss') }} 7 | 8 | {{ env.TEST_RESULT }} 9 | -------------------------------------------------------------------------------- /.github/workflows/codestyle.yaml: -------------------------------------------------------------------------------- 1 | name: Check code style for plugin 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | Check-Code-Style-for-Plugin-Processing-R: 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.11' 20 | architecture: 'x64' 21 | 22 | - name: Install Black and Isort 23 | run: | 24 | pip install -r REQUIREMENTS_TESTING.txt 25 | pip install black isort pylint pycodestyle 26 | 27 | - name: Run Black 28 | run: black processing_r --check --line-length 120 29 | 30 | - name: Run Isort 31 | run: isort processing_r --check-only --diff --profile black --line-width 120 32 | 33 | - name: Pylint 34 | run: make pylint 35 | 36 | - name: Pycodestyle 37 | run: make pycodestyle -------------------------------------------------------------------------------- /.github/workflows/mkdocs_build_deploy.yaml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - processing_r/metadata.txt 9 | - ".github/workflows/mkdocs_build_deploy.yaml" 10 | 11 | jobs: 12 | 13 | build-deploy: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: '3.11' 24 | architecture: 'x64' 25 | 26 | - name: Install dependencies 27 | run: | 28 | python3 -m pip install --upgrade pip 29 | python3 -m pip install mkdocs 30 | python3 -m pip install MarkdownHighlight 31 | python3 -m pip install https://codeload.github.com/mkdocs/mkdocs-bootstrap/zip/master 32 | 33 | - name: Build 34 | run: | 35 | mkdocs build --config-file ./website/mkdocs.yml 36 | touch website/docs/.nojekyll 37 | 38 | - name: Deploy 39 | uses: peaceiris/actions-gh-pages@v3 40 | with: 41 | github_token: ${{ secrets.GITHUB_TOKEN }} 42 | publish_dir: ./website/docs 43 | publish_branch: gh-pages 44 | -------------------------------------------------------------------------------- /.github/workflows/test_plugin.yaml: -------------------------------------------------------------------------------- 1 | name: Test plugin 2 | 3 | on: 4 | push: 5 | paths: 6 | - "processing_r/**" 7 | - ".github/workflows/test_plugin.yaml" 8 | pull_request: 9 | types: [opened, synchronize, edited] 10 | 11 | env: 12 | # plugin name/directory where the code for the plugin is stored 13 | PLUGIN_NAME: processing_r 14 | # python notation to test running inside plugin 15 | TESTS_RUN_FUNCTION: processing_r.test_suite.test_package 16 | # Docker settings 17 | DOCKER_IMAGE: qgis/qgis 18 | PYTHON_SETUP: "PYTHONPATH=/usr/share/qgis/python/:/usr/share/qgis/python/plugins:/usr/lib/python3/dist-packages/qgis:/usr/share/qgis/python/qgis:/tests_directory" 19 | 20 | 21 | jobs: 22 | 23 | Test-plugin-Processing-R: 24 | 25 | runs-on: ubuntu-latest 26 | 27 | strategy: 28 | matrix: 29 | docker_tags: [release-3_28, release-3_30, release-3_32, release-3_34, latest] 30 | 31 | steps: 32 | 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | 36 | - name: Docker pull and create qgis-test-env 37 | run: | 38 | docker pull "$DOCKER_IMAGE":${{ matrix.docker_tags }} 39 | docker run -d --name qgis-test-env -v "$GITHUB_WORKSPACE":/tests_directory -e DISPLAY=:99 "$DOCKER_IMAGE":${{ matrix.docker_tags }} 40 | 41 | - name: Docker set up 42 | run: | 43 | docker exec qgis-test-env sh -c "pip3 install -r /tests_directory/REQUIREMENTS_TESTING.txt" 44 | docker exec qgis-test-env sh -c "apt-get update" 45 | docker exec qgis-test-env sh -c "apt-get install -y --no-install-recommends wget ca-certificates gnupg" 46 | 47 | - name: Add Keys and Sources for R and binary packages 48 | run: | 49 | docker exec qgis-test-env sh -c "wget -q -O- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | tee -a /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc" 50 | docker exec qgis-test-env sh -c "echo \"deb [arch=amd64] https://cloud.r-project.org/bin/linux/ubuntu jammy-cran40/\" > /etc/apt/sources.list.d/cran_r.list" 51 | docker exec qgis-test-env sh -c "apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 67C2D66C4B1D4339 51716619E084DAB9" 52 | docker exec qgis-test-env sh -c "wget -q -O- https://eddelbuettel.github.io/r2u/assets/dirk_eddelbuettel_key.asc | tee -a /etc/apt/trusted.gpg.d/cranapt_key.asc" 53 | docker exec qgis-test-env sh -c "echo \"deb [arch=amd64] https://r2u.stat.illinois.edu/ubuntu jammy main\" > /etc/apt/sources.list.d/cranapt.list" 54 | 55 | - name: Install R and necessary packages 56 | run: | 57 | docker exec qgis-test-env sh -c "apt update -qq" 58 | docker exec qgis-test-env sh -c "apt-get install -y r-base r-cran-sf r-cran-raster" 59 | 60 | - name: Docker run plugin tests 61 | run: | 62 | docker exec qgis-test-env sh -c "$PYTHON_SETUP && cd tests_directory && pytest" 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | .noseids 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | .idea 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | .Rproj.user 108 | *.Rproj 109 | 110 | processing_r.zip -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Add iso code for any locales you want to support here (space separated) 2 | # default is no locales 3 | # LOCALES = af 4 | LOCALES = af 5 | 6 | # If locales are enabled, set the name of the lrelease binary on your system. If 7 | # you have trouble compiling the translations, you may have to specify the full path to 8 | # lrelease 9 | LRELEASE ?= lrelease-qt5 10 | 11 | # QGIS3 default 12 | QGISDIR=.local/share/QGIS/QGIS3/profiles/default 13 | 14 | 15 | # translation 16 | SOURCES = \ 17 | processing_r/__init__.py \ 18 | processing_r/r_plugin.py \ 19 | processing_r/processing/algorithm.py \ 20 | processing_r/processing/provider.py \ 21 | processing_r/processing/utils.py 22 | 23 | PLUGIN_NAME = processing_r 24 | 25 | EXTRAS = metadata.txt icon.png 26 | 27 | EXTRA_DIRS = 28 | 29 | PEP8EXCLUDE=pydev,conf.py,third_party,ui 30 | 31 | default: 32 | 33 | %.qm : %.ts 34 | $(LRELEASE) $< 35 | 36 | test: transcompile 37 | @echo 38 | @echo "----------------------" 39 | @echo "Regression Test Suite" 40 | @echo "----------------------" 41 | 42 | @# Preceding dash means that make will continue in case of errors 43 | @-export PYTHONPATH=`pwd`:$(PYTHONPATH); \ 44 | export QGIS_DEBUG=0; \ 45 | export QGIS_LOG_FILE=/dev/null; \ 46 | nosetests3 -v -s --with-id --with-coverage --cover-package=. processing_r.test \ 47 | 3>&1 1>&2 2>&3 3>&- || true 48 | @echo "----------------------" 49 | @echo "If you get a 'no module named qgis.core error, try sourcing" 50 | @echo "the helper script we have provided first then run make test." 51 | @echo "e.g. source run-env-linux.sh ; make test" 52 | @echo "----------------------" 53 | 54 | 55 | deploy: 56 | @echo 57 | @echo "------------------------------------------" 58 | @echo "Deploying (symlinking) plugin to your qgis3 directory." 59 | @echo "------------------------------------------" 60 | # The deploy target only works on unix like operating system where 61 | # the Python plugin directory is located at: 62 | # $HOME/$(QGISDIR)/python/plugins 63 | ln -s `pwd`/qgis-r $(HOME)/$(QGISDIR)/python/plugins/${PWD##*/} 64 | 65 | 66 | transup: 67 | @echo 68 | @echo "------------------------------------------------" 69 | @echo "Updating translation files with any new strings." 70 | @echo "------------------------------------------------" 71 | @chmod +x scripts/update-strings.sh 72 | @scripts/update-strings.sh $(LOCALES) 73 | 74 | transcompile: 75 | @echo 76 | @echo "----------------------------------------" 77 | @echo "Compiled translation files to .qm files." 78 | @echo "----------------------------------------" 79 | @chmod +x scripts/compile-strings.sh 80 | @scripts/compile-strings.sh $(LRELEASE) $(LOCALES) 81 | 82 | transclean: 83 | @echo 84 | @echo "------------------------------------" 85 | @echo "Removing compiled translation files." 86 | @echo "------------------------------------" 87 | rm -f i18n/*.qm 88 | 89 | pylint: 90 | @echo 91 | @echo "-----------------" 92 | @echo "Pylint violations" 93 | @echo "-----------------" 94 | @pylint --reports=n --rcfile=pylintrc processing_r 95 | @echo 96 | @echo "----------------------" 97 | @echo "If you get a 'no module named qgis.core' error, try sourcing" 98 | @echo "the helper script we have provided first then run make pylint." 99 | @echo "e.g. source run-env-linux.sh ; make pylint" 100 | @echo "----------------------" 101 | 102 | 103 | # Run pep8/pycodestyle style checking 104 | #http://pypi.python.org/pypi/pep8 105 | pycodestyle: 106 | @echo 107 | @echo "-----------" 108 | @echo "pycodestyle PEP8 issues" 109 | @echo "-----------" 110 | @pycodestyle --repeat --ignore=E203,E303,E121,E122,E123,E124,E125,E126,E127,E128,E402,E501,W503,W504 --exclude $(PEP8EXCLUDE) processing_r 111 | @echo "-----------" 112 | @echo "Ignored in PEP8 check:" 113 | @echo $(PEP8EXCLUDE) 114 | 115 | # The dclean target removes compiled python files from plugin directory 116 | # also deletes any .git entry 117 | dclean: 118 | @echo 119 | @echo "-----------------------------------" 120 | @echo "Removing any compiled python files." 121 | @echo "-----------------------------------" 122 | find $(PLUGIN_NAME) -iname "*.pyc" -delete 123 | find $(PLUGIN_NAME) -iname ".git" -prune -exec rm -Rf {} \; 124 | 125 | zip: dclean 126 | @echo 127 | @echo "---------------------------" 128 | @echo "Creating plugin zip bundle." 129 | @echo "---------------------------" 130 | # The zip target deploys the plugin and creates a zip file with the deployed 131 | # content. You can then upload the zip file on http://plugins.qgis.org 132 | rm -f $(PLUGIN_NAME).zip 133 | zip -9r $(PLUGIN_NAME).zip $(PLUGIN_NAME) -x *.git* -x *__pycache__* -x *test* 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qgis-processing-r 2 | 3 | [![Test plugin](https://github.com/north-road/qgis-processing-r/actions/workflows/test_plugin.yaml/badge.svg)](https://github.com/north-road/qgis-processing-r/actions/workflows/test_plugin.yaml) 4 | 5 | Processing R Provider Plugin for QGIS 3.x 6 | 7 | # Website 8 | 9 | [Plugin website.](https://north-road.github.io/qgis-processing-r/) 10 | -------------------------------------------------------------------------------- /REQUIREMENTS_TESTING.txt: -------------------------------------------------------------------------------- 1 | # For tests execution: 2 | deepdiff 3 | mock 4 | flake8 5 | pep257 6 | pytest 7 | pytest-cov 8 | pytest-qgis -------------------------------------------------------------------------------- /processing_r/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | * * 5 | * This program is free software; you can redistribute it and/or modify * 6 | * it under the terms of the GNU General Public License as published by * 7 | * the Free Software Foundation; either version 2 of the License, or * 8 | * (at your option) any later version. * 9 | * * 10 | ***************************************************************************/ 11 | """ 12 | 13 | from .r_plugin import RProviderPlugin 14 | 15 | 16 | # noinspection PyPep8Naming 17 | def classFactory(iface): # pylint: disable=invalid-name 18 | """Load plugin. 19 | 20 | :param iface: A QGIS interface instance. 21 | :type iface: QgsInterface 22 | """ 23 | # 24 | return RProviderPlugin(iface) 25 | -------------------------------------------------------------------------------- /processing_r/builtin_scripts/sample.rsx: -------------------------------------------------------------------------------- 1 | ##Example scripts=group 2 | ##Scatterplot=name 3 | ##output_plots_to_html 4 | ##Layer=vector 5 | ##X=Field Layer 6 | ##Y=Field Layer 7 | 8 | # simple scatterplot 9 | plot(Layer[[X]], Layer[[Y]]) 10 | -------------------------------------------------------------------------------- /processing_r/builtin_scripts/sample2.rsx: -------------------------------------------------------------------------------- 1 | ##Example scripts=group 2 | ##test_sf=name 3 | ##output_plots_to_html 4 | ##Layer=vector 5 | ##output= output vector 6 | 7 | >print("--------------------------------------------------------") 8 | >str(Layer) 9 | >print("--------------------------------------------------------") 10 | >str(st_crs(Layer)) 11 | >print("--------------------------------------------------------") 12 | output <- Layer 13 | -------------------------------------------------------------------------------- /processing_r/builtin_scripts/sample3.rsx: -------------------------------------------------------------------------------- 1 | ##Example scripts=group 2 | ##test_sp=name 3 | ##Layer=vector 4 | ##output= output vector 5 | 6 | >print("--------------------------------------------------------") 7 | >head(Layer) 8 | >print("--------------------------------------------------------") 9 | >proj4string(Layer) 10 | >print("--------------------------------------------------------") 11 | output <- Layer[1,] 12 | -------------------------------------------------------------------------------- /processing_r/builtin_scripts/sample4.rsx: -------------------------------------------------------------------------------- 1 | ##Example scripts=group 2 | ##Min_Max=name 3 | ##Layer=vector 4 | ##Field=Field Layer 5 | ##Min=output number 6 | ##Max=output number 7 | ##Summary=output string 8 | 9 | Min <- min(Layer[[Field]]) 10 | Max <- max(Layer[[Field]]) 11 | Summary <- paste(Min, "to", Max, sep = " ") 12 | -------------------------------------------------------------------------------- /processing_r/builtin_scripts/sample5.rsx: -------------------------------------------------------------------------------- 1 | ##Example scripts=group 2 | ##Expressions=name 3 | ##my_r_variable=expression 1+2+3 4 | ##qgis_version=expression @qgis_version_no 5 | ##geometry=expression make_circle( make_point(0, 0), 10) 6 | ##test_date=expression make_date(2020,5,4) 7 | ##test_time=expression make_time(13,45,30.5) 8 | ##array=expression array(2, 10, 'a') 9 | >print(my_r_variable) 10 | >print(qgis_version) 11 | >print(geometry) 12 | >print(class(geometry)) 13 | >print(test_date) 14 | >print(test_time) 15 | >print(array) -------------------------------------------------------------------------------- /processing_r/gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/processing_r/gui/__init__.py -------------------------------------------------------------------------------- /processing_r/gui/gui_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """R Procesing Plugin - GUI Utilities 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | __author__ = "(C) 2018 by Nyall Dawson" 11 | __date__ = "20/04/2018" 12 | __copyright__ = "Copyright 2018, North Road" 13 | # This will get replaced with a git SHA1 when you do a git archive 14 | __revision__ = "$Format:%H$" 15 | 16 | import os 17 | 18 | from qgis.PyQt.QtGui import QIcon 19 | 20 | 21 | class GuiUtils: 22 | """ 23 | Utilities for GUI plugin components 24 | """ 25 | 26 | @staticmethod 27 | def get_icon(icon: str) -> QIcon: 28 | """ 29 | Returns a plugin icon 30 | :param icon: icon name (svg file name) 31 | :return: QIcon 32 | """ 33 | path = GuiUtils.get_icon_svg(icon) 34 | if not path: 35 | return QIcon() 36 | 37 | return QIcon(path) 38 | 39 | @staticmethod 40 | def get_icon_svg(icon: str) -> str: 41 | """ 42 | Returns a plugin icon's SVG file path 43 | :param icon: icon name (svg file name) 44 | :return: icon svg path 45 | """ 46 | path = os.path.join(os.path.dirname(__file__), "..", "images", icon) 47 | if not os.path.exists(path): 48 | return "" 49 | 50 | return path 51 | 52 | @staticmethod 53 | def get_ui_file_path(file: str) -> str: 54 | """ 55 | Returns a UI file's path 56 | :param file: file name (uifile name) 57 | :return: ui file path 58 | """ 59 | path = os.path.join(os.path.dirname(__file__), "..", "ui", file) 60 | if not os.path.exists(path): 61 | return "" 62 | 63 | return path 64 | -------------------------------------------------------------------------------- /processing_r/gui/script_editor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/processing_r/gui/script_editor/__init__.py -------------------------------------------------------------------------------- /processing_r/gui/script_editor/script_editor_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | *************************************************************************** 5 | EditScriptDialog.py 6 | --------------------- 7 | Date : December 2012 8 | Copyright : (C) 2012 by Alexander Bruy 9 | Email : alexander dot bruy 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__ = "Alexander Bruy" 21 | __date__ = "December 2012" 22 | __copyright__ = "(C) 2012, Alexander Bruy" 23 | 24 | # This will get replaced with a git SHA1 when you do a git archive 25 | 26 | __revision__ = "$Format:%H$" 27 | 28 | import codecs 29 | import inspect 30 | import os 31 | import traceback 32 | import warnings 33 | 34 | from processing.gui.AlgorithmDialog import AlgorithmDialog 35 | from processing.script import ScriptUtils 36 | from qgis.core import QgsApplication, QgsError, QgsProcessingAlgorithm, QgsProcessingFeatureBasedAlgorithm, QgsSettings 37 | from qgis.gui import QgsErrorDialog, QgsGui 38 | from qgis.PyQt import uic 39 | from qgis.PyQt.QtCore import Qt 40 | from qgis.PyQt.QtGui import QCursor 41 | from qgis.PyQt.QtWidgets import QFileDialog, QMessageBox 42 | from qgis.utils import OverrideCursor, iface 43 | 44 | from processing_r.gui.gui_utils import GuiUtils 45 | from processing_r.processing.algorithm import RAlgorithm 46 | from processing_r.processing.utils import RUtils 47 | 48 | # from qgis.processing import alg as algfactory 49 | 50 | 51 | pluginPath = os.path.split(os.path.dirname(__file__))[0] 52 | 53 | with warnings.catch_warnings(): 54 | warnings.filterwarnings("ignore", category=DeprecationWarning) 55 | WIDGET, BASE = uic.loadUiType(GuiUtils.get_ui_file_path("DlgScriptEditor.ui")) 56 | 57 | 58 | # This class is ported from the QGIS core Processing script editor. 59 | # Unfortunately generalising the core editor to allow everything we want in an R editor 60 | # isn't feasible... so lots of duplicate code here :( 61 | # Try to keep the diff between the two as small as possible, to allow porting fixes from QGIS core 62 | 63 | 64 | class ScriptEditorDialog(BASE, WIDGET): 65 | hasChanged = False 66 | 67 | def __init__(self, filePath=None, parent=None): 68 | super(ScriptEditorDialog, self).__init__(parent) 69 | self.setupUi(self) 70 | 71 | QgsGui.instance().enableAutoGeometryRestore(self) 72 | 73 | self.editor.initLexer() 74 | self.searchWidget.setVisible(False) 75 | 76 | if iface is not None: 77 | self.toolBar.setIconSize(iface.iconSize()) 78 | self.setStyleSheet(iface.mainWindow().styleSheet()) 79 | 80 | self.actionOpenScript.setIcon(QgsApplication.getThemeIcon("/mActionScriptOpen.svg")) 81 | self.actionSaveScript.setIcon(QgsApplication.getThemeIcon("/mActionFileSave.svg")) 82 | self.actionSaveScriptAs.setIcon(QgsApplication.getThemeIcon("/mActionFileSaveAs.svg")) 83 | self.actionRunScript.setIcon(QgsApplication.getThemeIcon("/mActionStart.svg")) 84 | self.actionCut.setIcon(QgsApplication.getThemeIcon("/mActionEditCut.svg")) 85 | self.actionCopy.setIcon(QgsApplication.getThemeIcon("/mActionEditCopy.svg")) 86 | self.actionPaste.setIcon(QgsApplication.getThemeIcon("/mActionEditPaste.svg")) 87 | self.actionUndo.setIcon(QgsApplication.getThemeIcon("/mActionUndo.svg")) 88 | self.actionRedo.setIcon(QgsApplication.getThemeIcon("/mActionRedo.svg")) 89 | self.actionFindReplace.setIcon(QgsApplication.getThemeIcon("/mActionFindReplace.svg")) 90 | self.actionIncreaseFontSize.setIcon(QgsApplication.getThemeIcon("/mActionIncreaseFont.svg")) 91 | self.actionDecreaseFontSize.setIcon(QgsApplication.getThemeIcon("/mActionDecreaseFont.svg")) 92 | 93 | # Connect signals and slots 94 | self.actionOpenScript.triggered.connect(self.openScript) 95 | self.actionSaveScript.triggered.connect(self.save) 96 | self.actionSaveScriptAs.triggered.connect(self.saveAs) 97 | self.actionRunScript.triggered.connect(self.runAlgorithm) 98 | self.actionCut.triggered.connect(self.editor.cut) 99 | self.actionCopy.triggered.connect(self.editor.copy) 100 | self.actionPaste.triggered.connect(self.editor.paste) 101 | self.actionUndo.triggered.connect(self.editor.undo) 102 | self.actionRedo.triggered.connect(self.editor.redo) 103 | self.actionFindReplace.toggled.connect(self.toggleSearchBox) 104 | self.actionIncreaseFontSize.triggered.connect(self.editor.zoomIn) 105 | self.actionDecreaseFontSize.triggered.connect(self.editor.zoomOut) 106 | self.editor.textChanged.connect(lambda: self.setHasChanged(True)) 107 | 108 | self.leFindText.returnPressed.connect(self.find) 109 | self.btnFind.clicked.connect(self.find) 110 | self.btnReplace.clicked.connect(self.replace) 111 | self.lastSearch = None 112 | 113 | self.filePath = None 114 | if filePath is not None: 115 | self._loadFile(filePath) 116 | 117 | self.setHasChanged(False) 118 | 119 | def update_dialog_title(self): 120 | """ 121 | Updates the script editor dialog title 122 | """ 123 | if self.filePath: 124 | path, file_name = os.path.split(self.filePath) 125 | else: 126 | file_name = self.tr("Untitled Script") 127 | 128 | if self.hasChanged: 129 | file_name = "*" + file_name 130 | 131 | self.setWindowTitle(self.tr("{} - R Script Editor").format(file_name)) 132 | 133 | def closeEvent(self, event): 134 | settings = QgsSettings() 135 | settings.setValue("/Processing/stateScriptEditor", self.saveState()) 136 | settings.setValue("/Processing/geometryScriptEditor", self.saveGeometry()) 137 | 138 | if self.hasChanged: 139 | ret = QMessageBox.question( 140 | self, 141 | self.tr("Save Script?"), 142 | self.tr("There are unsaved changes in this script. Do you want to keep those?"), 143 | QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard, 144 | QMessageBox.Cancel, 145 | ) 146 | 147 | if ret == QMessageBox.Save: 148 | self.saveScript(False) 149 | event.accept() 150 | elif ret == QMessageBox.Discard: 151 | event.accept() 152 | else: 153 | event.ignore() 154 | else: 155 | event.accept() 156 | 157 | def openScript(self): 158 | if self.hasChanged: 159 | ret = QMessageBox.warning( 160 | self, 161 | self.tr("Unsaved changes"), 162 | self.tr("There are unsaved changes in the script. Continue?"), 163 | QMessageBox.Yes | QMessageBox.No, 164 | QMessageBox.No, 165 | ) 166 | if ret == QMessageBox.No: 167 | return 168 | 169 | scriptDir = RUtils.default_scripts_folder() 170 | fileName, _ = QFileDialog.getOpenFileName( 171 | self, self.tr("Open script"), scriptDir, self.tr("R scripts (*.rsx *.RSX)") 172 | ) 173 | 174 | if fileName == "": 175 | return 176 | 177 | with OverrideCursor(Qt.WaitCursor): 178 | self._loadFile(fileName) 179 | 180 | def save(self): 181 | self.saveScript(False) 182 | 183 | def saveAs(self): 184 | self.saveScript(True) 185 | 186 | def saveScript(self, saveAs): 187 | newPath = None 188 | if self.filePath is None or saveAs: 189 | scriptDir = RUtils.default_scripts_folder() 190 | newPath, _ = QFileDialog.getSaveFileName( 191 | self, self.tr("Save script"), scriptDir, self.tr("R scripts (*.rsx *.RSX)") 192 | ) 193 | 194 | if newPath: 195 | if not newPath.lower().endswith(".rsx"): 196 | newPath += ".rsx" 197 | 198 | self.filePath = newPath 199 | 200 | if self.filePath: 201 | text = self.editor.text() 202 | try: 203 | with codecs.open(self.filePath, "w", encoding="utf-8") as f: 204 | f.write(text) 205 | except IOError as e: 206 | QMessageBox.warning(self, self.tr("I/O error"), self.tr("Unable to save edits:\n{}").format(str(e))) 207 | return 208 | 209 | self.setHasChanged(False) 210 | 211 | QgsApplication.processingRegistry().providerById("r").refreshAlgorithms() 212 | 213 | def setHasChanged(self, hasChanged): 214 | self.hasChanged = hasChanged 215 | self.actionSaveScript.setEnabled(hasChanged) 216 | self.update_dialog_title() 217 | 218 | def runAlgorithm(self): 219 | alg = RAlgorithm(description_file=None, script=self.editor.text()) 220 | if alg.error is not None: 221 | error = QgsError(alg.error, "R") 222 | QgsErrorDialog.show(error, self.tr("Execution error")) 223 | return 224 | 225 | alg.setProvider(QgsApplication.processingRegistry().providerById("r")) 226 | alg.initAlgorithm() 227 | 228 | dlg = alg.createCustomParametersWidget(iface.mainWindow()) 229 | if not dlg: 230 | dlg = AlgorithmDialog(alg, parent=iface.mainWindow()) 231 | 232 | canvas = iface.mapCanvas() 233 | prevMapTool = canvas.mapTool() 234 | 235 | dlg.show() 236 | 237 | if canvas.mapTool() != prevMapTool: 238 | if canvas.mapTool(): 239 | canvas.mapTool().reset() 240 | canvas.setMapTool(prevMapTool) 241 | 242 | def find(self): 243 | textToFind = self.leFindText.text() 244 | caseSensitive = self.chkCaseSensitive.isChecked() 245 | wholeWord = self.chkWholeWord.isChecked() 246 | if self.lastSearch is None or textToFind != self.lastSearch: 247 | self.editor.findFirst(textToFind, False, caseSensitive, wholeWord, True) 248 | else: 249 | self.editor.findNext() 250 | 251 | def replace(self): 252 | textToReplace = self.leReplaceText.text() 253 | self.editor.replaceSelectedText(textToReplace) 254 | 255 | def toggleSearchBox(self, checked): 256 | self.searchWidget.setVisible(checked) 257 | if checked: 258 | self.leFindText.setFocus() 259 | 260 | def _loadFile(self, filePath): 261 | with codecs.open(filePath, "r", encoding="utf-8") as f: 262 | txt = f.read() 263 | 264 | self.editor.setText(txt) 265 | self.hasChanged = False 266 | self.editor.setModified(False) 267 | self.editor.recolor() 268 | 269 | self.filePath = filePath 270 | self.update_dialog_title() 271 | -------------------------------------------------------------------------------- /processing_r/gui/script_editor/script_editor_widget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | *************************************************************************** 5 | script_editor_widget.py 6 | --------------------- 7 | Date : April 2013 8 | Copyright : (C) 2013 by Alexander Bruy 9 | Email : alexander dot bruy 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__ = "Alexander Bruy" 21 | __date__ = "April 2013" 22 | __copyright__ = "(C) 2013, Alexander Bruy" 23 | 24 | # This will get replaced with a git SHA1 when you do a git archive 25 | 26 | __revision__ = "$Format:%H$" 27 | 28 | import os 29 | 30 | from qgis.core import QgsApplication, QgsSettings 31 | from qgis.PyQt.Qsci import QsciAPIs, QsciLexerPython, QsciScintilla 32 | from qgis.PyQt.QtCore import Qt 33 | from qgis.PyQt.QtGui import QColor, QFont, QFontDatabase, QFontMetrics, QKeySequence 34 | from qgis.PyQt.QtWidgets import QShortcut 35 | 36 | # This class is ported from the QGIS core Processing script editor. 37 | # Unfortunately generalising the core editor to allow everything we want in an R editor 38 | # isn't feasible... so lots of duplicate code here :( 39 | # Try to keep the diff between the two as small as possible, to allow porting fixes from QGIS core 40 | 41 | 42 | class ScriptEdit(QsciScintilla): 43 | DEFAULT_COLOR = "#4d4d4c" 44 | KEYWORD_COLOR = "#8959a8" 45 | CLASS_COLOR = "#4271ae" 46 | METHOD_COLOR = "#4271ae" 47 | DECORATION_COLOR = "#3e999f" 48 | NUMBER_COLOR = "#c82829" 49 | COMMENT_COLOR = "#8e908c" 50 | COMMENT_BLOCK_COLOR = "#8e908c" 51 | BACKGROUND_COLOR = "#ffffff" 52 | CURSOR_COLOR = "#636363" 53 | CARET_LINE_COLOR = "#efefef" 54 | SINGLE_QUOTE_COLOR = "#718c00" 55 | DOUBLE_QUOTE_COLOR = "#718c00" 56 | TRIPLE_SINGLE_QUOTE_COLOR = "#eab700" 57 | TRIPLE_DOUBLE_QUOTE_COLOR = "#eab700" 58 | MARGIN_BACKGROUND_COLOR = "#efefef" 59 | MARGIN_FOREGROUND_COLOR = "#636363" 60 | SELECTION_BACKGROUND_COLOR = "#d7d7d7" 61 | SELECTION_FOREGROUND_COLOR = "#303030" 62 | MATCHED_BRACE_BACKGROUND_COLOR = "#b7f907" 63 | MATCHED_BRACE_FOREGROUND_COLOR = "#303030" 64 | EDGE_COLOR = "#efefef" 65 | FOLD_COLOR = "#efefef" 66 | 67 | def __init__(self, parent=None): 68 | super().__init__(parent) 69 | 70 | self.lexer = None 71 | self.api = None 72 | 73 | self.setCommonOptions() 74 | self.initShortcuts() 75 | 76 | def setCommonOptions(self): 77 | # Enable non-ASCII characters 78 | self.setUtf8(True) 79 | 80 | settings = QgsSettings() 81 | 82 | # Default font 83 | font = QFontDatabase.systemFont(QFontDatabase.FixedFont) 84 | self.setFont(font) 85 | self.setMarginsFont(font) 86 | 87 | self.setBraceMatching(QsciScintilla.SloppyBraceMatch) 88 | self.setMatchedBraceBackgroundColor( 89 | QColor( 90 | settings.value( 91 | "pythonConsole/matchedBraceBackgroundColorEditor", QColor(self.MATCHED_BRACE_BACKGROUND_COLOR) 92 | ) 93 | ) 94 | ) 95 | self.setMatchedBraceForegroundColor( 96 | QColor( 97 | settings.value( 98 | "pythonConsole/matchedBraceForegroundColorEditor", QColor(self.MATCHED_BRACE_FOREGROUND_COLOR) 99 | ) 100 | ) 101 | ) 102 | 103 | # self.setWrapMode(QsciScintilla.WrapWord) 104 | # self.setWrapVisualFlags(QsciScintilla.WrapFlagByText, 105 | # QsciScintilla.WrapFlagNone, 4) 106 | 107 | self.setSelectionForegroundColor( 108 | QColor( 109 | settings.value("pythonConsole/selectionForegroundColorEditor", QColor(self.SELECTION_FOREGROUND_COLOR)) 110 | ) 111 | ) 112 | self.setSelectionBackgroundColor( 113 | QColor( 114 | settings.value("pythonConsole/selectionBackgroundColorEditor", QColor(self.SELECTION_BACKGROUND_COLOR)) 115 | ) 116 | ) 117 | 118 | # Show line numbers 119 | fontmetrics = QFontMetrics(font) 120 | self.setMarginWidth(1, fontmetrics.width("0000") + 5) 121 | self.setMarginLineNumbers(1, True) 122 | self.setMarginsForegroundColor( 123 | QColor(settings.value("pythonConsole/marginForegroundColorEditor", QColor(self.MARGIN_FOREGROUND_COLOR))) 124 | ) 125 | self.setMarginsBackgroundColor( 126 | QColor(settings.value("pythonConsole/marginBackgroundColorEditor", QColor(self.MARGIN_BACKGROUND_COLOR))) 127 | ) 128 | self.setIndentationGuidesForegroundColor( 129 | QColor(settings.value("pythonConsole/marginForegroundColorEditor", QColor(self.MARGIN_FOREGROUND_COLOR))) 130 | ) 131 | self.setIndentationGuidesBackgroundColor( 132 | QColor(settings.value("pythonConsole/marginBackgroundColorEditor", QColor(self.MARGIN_BACKGROUND_COLOR))) 133 | ) 134 | 135 | # Highlight current line 136 | caretLineColorEditor = settings.value("pythonConsole/caretLineColorEditor", QColor(self.CARET_LINE_COLOR)) 137 | cursorColorEditor = settings.value("pythonConsole/cursorColorEditor", QColor(self.CURSOR_COLOR)) 138 | self.setCaretLineVisible(True) 139 | self.setCaretWidth(2) 140 | self.setCaretLineBackgroundColor(caretLineColorEditor) 141 | self.setCaretForegroundColor(cursorColorEditor) 142 | 143 | # Folding 144 | # self.setFolding(QsciScintilla.PlainFoldStyle) 145 | # foldColor = QColor(settings.value("pythonConsole/foldColorEditor", QColor(self.FOLD_COLOR))) 146 | # self.setFoldMarginColors(foldColor, foldColor) 147 | 148 | # Mark column 80 with vertical line 149 | self.setEdgeMode(QsciScintilla.EdgeLine) 150 | self.setEdgeColumn(80) 151 | self.setEdgeColor(QColor(settings.value("pythonConsole/edgeColorEditor", QColor(self.EDGE_COLOR)))) 152 | 153 | # Indentation 154 | self.setAutoIndent(False) 155 | self.setIndentationsUseTabs(False) 156 | self.setIndentationWidth(4) 157 | self.setTabIndents(True) 158 | self.setBackspaceUnindents(True) 159 | self.setTabWidth(4) 160 | self.setIndentationGuides(True) 161 | 162 | # Autocompletion 163 | # self.setAutoCompletionThreshold(2) 164 | # self.setAutoCompletionSource(QsciScintilla.AcsAPIs) 165 | 166 | self.setFonts(10) 167 | self.initLexer() 168 | 169 | def setFonts(self, size): 170 | # Load font from Python console settings 171 | settings = QgsSettings() 172 | fontName = settings.value("pythonConsole/fontfamilytext", "Monospace") 173 | fontSize = int(settings.value("pythonConsole/fontsize", size)) 174 | 175 | self.defaultFont = QFont(fontName) 176 | self.defaultFont.setFixedPitch(True) 177 | self.defaultFont.setPointSize(fontSize) 178 | self.defaultFont.setStyleHint(QFont.TypeWriter) 179 | self.defaultFont.setBold(False) 180 | 181 | self.boldFont = QFont(self.defaultFont) 182 | self.boldFont.setBold(True) 183 | 184 | self.italicFont = QFont(self.defaultFont) 185 | self.italicFont.setItalic(True) 186 | 187 | self.setFont(self.defaultFont) 188 | self.setMarginsFont(self.defaultFont) 189 | 190 | def initShortcuts(self): 191 | (ctrl, shift) = (self.SCMOD_CTRL << 16, self.SCMOD_SHIFT << 16) 192 | 193 | # Disable some shortcuts 194 | self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord("D") + ctrl) 195 | self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord("L") + ctrl) 196 | self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord("L") + ctrl + shift) 197 | self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord("T") + ctrl) 198 | 199 | # self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord("Z") + ctrl) 200 | # self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord("Y") + ctrl) 201 | 202 | # Use Ctrl+Space for autocompletion 203 | # no auto complete for R scripts! 204 | # self.shortcutAutocomplete = QShortcut(QKeySequence(Qt.CTRL + 205 | # Qt.Key_Space), self) 206 | # self.shortcutAutocomplete.setContext(Qt.WidgetShortcut) 207 | # self.shortcutAutocomplete.activated.connect(self.autoComplete) 208 | 209 | def autoComplete(self): 210 | self.autoCompleteFromAll() 211 | 212 | def initLexer(self): 213 | settings = QgsSettings() 214 | self.lexer = QsciLexerPython() 215 | 216 | font = QFontDatabase.systemFont(QFontDatabase.FixedFont) 217 | 218 | loadFont = settings.value("pythonConsole/fontfamilytextEditor") 219 | if loadFont: 220 | font.setFamily(loadFont) 221 | fontSize = settings.value("pythonConsole/fontsizeEditor", type=int) 222 | if fontSize: 223 | font.setPointSize(fontSize) 224 | 225 | self.lexer.setDefaultFont(font) 226 | self.lexer.setDefaultColor( 227 | QColor(settings.value("pythonConsole/defaultFontColorEditor", QColor(self.DEFAULT_COLOR))) 228 | ) 229 | self.lexer.setColor( 230 | QColor(settings.value("pythonConsole/commentFontColorEditor", QColor(self.COMMENT_COLOR))), 1 231 | ) 232 | self.lexer.setColor(QColor(settings.value("pythonConsole/numberFontColorEditor", QColor(self.NUMBER_COLOR))), 2) 233 | self.lexer.setColor( 234 | QColor(settings.value("pythonConsole/keywordFontColorEditor", QColor(self.KEYWORD_COLOR))), 5 235 | ) 236 | self.lexer.setColor(QColor(settings.value("pythonConsole/classFontColorEditor", QColor(self.CLASS_COLOR))), 8) 237 | self.lexer.setColor(QColor(settings.value("pythonConsole/methodFontColorEditor", QColor(self.METHOD_COLOR))), 9) 238 | self.lexer.setColor( 239 | QColor(settings.value("pythonConsole/decorFontColorEditor", QColor(self.DECORATION_COLOR))), 15 240 | ) 241 | self.lexer.setColor( 242 | QColor(settings.value("pythonConsole/commentBlockFontColorEditor", QColor(self.COMMENT_BLOCK_COLOR))), 12 243 | ) 244 | self.lexer.setColor( 245 | QColor(settings.value("pythonConsole/singleQuoteFontColorEditor", QColor(self.SINGLE_QUOTE_COLOR))), 4 246 | ) 247 | self.lexer.setColor( 248 | QColor(settings.value("pythonConsole/doubleQuoteFontColorEditor", QColor(self.DOUBLE_QUOTE_COLOR))), 3 249 | ) 250 | self.lexer.setColor( 251 | QColor( 252 | settings.value("pythonConsole/tripleSingleQuoteFontColorEditor", QColor(self.TRIPLE_SINGLE_QUOTE_COLOR)) 253 | ), 254 | 6, 255 | ) 256 | self.lexer.setColor( 257 | QColor( 258 | settings.value("pythonConsole/tripleDoubleQuoteFontColorEditor", QColor(self.TRIPLE_DOUBLE_QUOTE_COLOR)) 259 | ), 260 | 7, 261 | ) 262 | self.lexer.setColor( 263 | QColor(settings.value("pythonConsole/defaultFontColorEditor", QColor(self.DEFAULT_COLOR))), 13 264 | ) 265 | self.lexer.setFont(font, 1) 266 | self.lexer.setFont(font, 3) 267 | self.lexer.setFont(font, 4) 268 | self.lexer.setFont(font, QsciLexerPython.UnclosedString) 269 | 270 | for style in range(0, 33): 271 | paperColor = QColor( 272 | settings.value("pythonConsole/paperBackgroundColorEditor", QColor(self.BACKGROUND_COLOR)) 273 | ) 274 | self.lexer.setPaper(paperColor, style) 275 | 276 | # self.api = QsciAPIs(self.lexer) 277 | 278 | # useDefaultAPI = bool(settings.value('pythonConsole/preloadAPI', 279 | # True)) 280 | # if useDefaultAPI: 281 | # # Load QGIS API shipped with Python console 282 | # self.api.loadPrepared( 283 | # os.path.join(QgsApplication.pkgDataPath(), 284 | # 'python', 'qsci_apis', 'pyqgis.pap')) 285 | # else: 286 | # # Load user-defined API files 287 | # apiPaths = settings.value('pythonConsole/userAPI', []) 288 | # for path in apiPaths: 289 | # self.api.load(path) 290 | # self.api.prepare() 291 | # self.lexer.setAPIs(self.api) 292 | 293 | self.setLexer(self.lexer) 294 | -------------------------------------------------------------------------------- /processing_r/i18n/af.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/processing_r/i18n/af.qm -------------------------------------------------------------------------------- /processing_r/i18n/af.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @default 5 | 6 | 7 | Good morning 8 | Goeie more 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /processing_r/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/processing_r/icon.png -------------------------------------------------------------------------------- /processing_r/images/providerR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /processing_r/metadata.txt: -------------------------------------------------------------------------------- 1 | # This file contains metadata for your plugin. Since 2 | # version 2.0 of QGIS this is the proper way to supply 3 | # information about a plugin. The old method of 4 | # embedding metadata in __init__.py will 5 | # is no longer supported since version 2.0. 6 | 7 | # This file should be included when you package your plugin.# Mandatory items: 8 | 9 | [general] 10 | name=Processing R Provider 11 | qgisMinimumVersion=3.4 12 | description=A Processing provider for connecting to the R statistics framework 13 | version=4.1.0 14 | author=North Road 15 | email=nyall@north-road.com 16 | 17 | about=Processing provider for R scripts 18 | 19 | tracker=https://github.com/north-road/qgis-processing-r/issues 20 | repository=https://github.com/north-road/qgis-processing-r 21 | # End of mandatory metadata 22 | 23 | # Recommended items: 24 | 25 | # Uncomment the following line and add your changelog: 26 | changelog=4.1.0 Fix bugs with layers being converted to SHP messing up field names, version obtaining 27 | 4.0.0 Reflect retirement of rgdal R package, only support loading using SF and RASTER packages 28 | 3.1.1 Capture correctly even errors from R starting with `Error:`, fix importing `QgsProcessingParameterColor` and `QgsProcessingParameterDateTime` for older QGIS versions (where it is not available) 29 | 3.1.0 Support "range", "color", "datetime" input parameter types. Fixed list creation from multiple input layer parameters. Fix conversion of custom coordinate reference systems. 30 | 3.0.0 Added support for `QgsProcessingParameter*` strings, that can be used to define script parameters. Better handling of errors in R scripts. New script parameter `##script_title`, that can be used to define string under which the script is listed in QGIS Toolbox. 31 | 2.3.0 Introduces support for QgsProcessingParameterPoint as an input variable, set parameter help strings under QGIS 3.16 or later 32 | 2.2.2 Make sure that FolderDestination and FileDestination folders exist, otherwise that would have to be handled by R script causes issue if writing to temp directory which does not exist2.2.1 Fix file based outputs, R version number reporting 33 | 2.2.1 Fix file based outputs, R version number reporting 34 | 2.2.0 Add support for file based outputs. Check https://north-road.github.io/qgis-processing-r for details! 35 | 2.1.0 Add support for literal enums, skipping package loading, additional output types (e.g. strings and numbers). 36 | 2.0.0 Many fixes, algorithms have support for choice between sf/raster or rgdal for loading inputs 37 | 1.0.7 Fix 3.4 compatibility 38 | 1.0.6 Workaround API break in QGIS, which breaks existing scripts 39 | 1.0.5 Allow use of R development versions 40 | 1.0.4 Resurrect ability to run on selected features only 41 | 1.0.3 Remove activation setting for provider (disable plugin instead!), fix memory layer handling, simpler example script 42 | 1.0.2 Fix vector paths under windows 43 | 44 | # Tags are comma separated with spaces allowed 45 | tags=python 46 | 47 | homepage=https://north-road.github.io/qgis-processing-r 48 | category=Plugins 49 | icon=icon.png 50 | # experimental flag 51 | experimental=False 52 | 53 | # deprecated flag (applies to the whole plugin, not just a single version) 54 | deprecated=False 55 | 56 | hasProcessingProvider=yes -------------------------------------------------------------------------------- /processing_r/processing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/processing_r/processing/__init__.py -------------------------------------------------------------------------------- /processing_r/processing/actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/processing_r/processing/actions/__init__.py -------------------------------------------------------------------------------- /processing_r/processing/actions/create_new_script.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | *************************************************************************** 5 | create_new_script.py 6 | --------------------- 7 | Date : August 2012 8 | Copyright : (C) 2012 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__ = "August 2012" 22 | __copyright__ = "(C) 2012, Victor Olaya" 23 | 24 | # This will get replaced with a git SHA1 when you do a git archive 25 | 26 | __revision__ = "$Format:%H$" 27 | 28 | from processing.gui.ToolboxAction import ToolboxAction 29 | from qgis.PyQt.QtCore import QCoreApplication 30 | 31 | from processing_r.gui.script_editor.script_editor_dialog import ScriptEditorDialog 32 | 33 | 34 | class CreateNewScriptAction(ToolboxAction): 35 | """ 36 | Action for creating a new R script 37 | """ 38 | 39 | def __init__(self): 40 | super().__init__() 41 | self.name = QCoreApplication.translate("RAlgorithmProvider", "Create New R Script…") 42 | self.group = self.tr("Tools") 43 | 44 | def execute(self): 45 | """ 46 | Called whenever the action is triggered 47 | """ 48 | dlg = ScriptEditorDialog(None) 49 | dlg.show() 50 | -------------------------------------------------------------------------------- /processing_r/processing/actions/delete_script.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | *************************************************************************** 5 | delete_script.py 6 | --------------------- 7 | Date : August 2012 8 | Copyright : (C) 2012 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__ = "August 2012" 22 | __copyright__ = "(C) 2012, Victor Olaya" 23 | 24 | # This will get replaced with a git SHA1 when you do a git archive 25 | 26 | __revision__ = "$Format:%H$" 27 | 28 | import os 29 | 30 | from processing.gui.ContextAction import ContextAction 31 | from qgis.core import QgsApplication, QgsProcessingAlgorithm 32 | from qgis.PyQt.QtCore import QCoreApplication 33 | from qgis.PyQt.QtWidgets import QMessageBox 34 | 35 | 36 | class DeleteScriptAction(ContextAction): 37 | """ 38 | Toolbox context menu action for deleting an existing script 39 | """ 40 | 41 | def __init__(self): 42 | super().__init__() 43 | self.name = QCoreApplication.translate("DeleteScriptAction", "Delete Script…") 44 | 45 | def isEnabled(self): 46 | """ 47 | Returns whether the action is enabled 48 | """ 49 | return ( 50 | isinstance(self.itemData, QgsProcessingAlgorithm) 51 | and self.itemData.provider().id() == "r" 52 | and self.itemData.is_user_script 53 | ) 54 | 55 | def execute(self): 56 | """ 57 | Called whenever the action is triggered 58 | """ 59 | reply = QMessageBox.question( 60 | None, 61 | self.tr("Delete Script"), 62 | self.tr("Are you sure you want to delete this script?"), 63 | QMessageBox.Yes | QMessageBox.No, 64 | QMessageBox.No, 65 | ) 66 | if reply == QMessageBox.Yes: 67 | file_path = self.itemData.description_file 68 | if file_path is not None: 69 | os.remove(file_path) 70 | QgsApplication.processingRegistry().providerById("r").refreshAlgorithms() 71 | else: 72 | QMessageBox.warning(None, self.tr("Delete Script"), self.tr("Can not find corresponding script file.")) 73 | -------------------------------------------------------------------------------- /processing_r/processing/actions/edit_script.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | *************************************************************************** 5 | edit_script.py 6 | --------------------- 7 | Date : August 2012 8 | Copyright : (C) 2012 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__ = "August 2012" 22 | __copyright__ = "(C) 2012, Victor Olaya" 23 | 24 | # This will get replaced with a git SHA1 when you do a git archive 25 | 26 | __revision__ = "$Format:%H$" 27 | 28 | from processing.gui.ContextAction import ContextAction 29 | from qgis.core import QgsProcessingAlgorithm 30 | from qgis.PyQt.QtCore import QCoreApplication 31 | from qgis.PyQt.QtWidgets import QMessageBox 32 | from qgis.utils import iface 33 | 34 | from processing_r.gui.script_editor.script_editor_dialog import ScriptEditorDialog 35 | 36 | 37 | class EditScriptAction(ContextAction): 38 | """ 39 | Toolbox context menu action for editing an existing script 40 | """ 41 | 42 | def __init__(self): 43 | super().__init__() 44 | self.name = QCoreApplication.translate("EditScriptAction", "Edit Script…") 45 | 46 | def isEnabled(self): 47 | """ 48 | Returns whether the action is enabled 49 | """ 50 | return ( 51 | isinstance(self.itemData, QgsProcessingAlgorithm) 52 | and self.itemData.provider().id() == "r" 53 | and self.itemData.is_user_script 54 | ) 55 | 56 | def execute(self): 57 | """ 58 | Called whenever the action is triggered 59 | """ 60 | file_path = self.itemData.description_file 61 | if file_path is not None: 62 | dlg = ScriptEditorDialog(file_path, iface.mainWindow()) 63 | dlg.show() 64 | else: 65 | QMessageBox.warning(None, self.tr("Edit Script"), self.tr("Can not find corresponding script file.")) 66 | -------------------------------------------------------------------------------- /processing_r/processing/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | *************************************************************************** 5 | exceptions.py 6 | --------------------- 7 | Date : November 2018 8 | Copyright : (C) 2018 by Nyall Dawson 9 | Email : nyall dot dawson 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 | 21 | class InvalidScriptException(Exception): 22 | """ 23 | Raised on encountering an invalid script 24 | """ 25 | 26 | def __init__(self, msg): 27 | super().__init__() 28 | self.msg = msg 29 | -------------------------------------------------------------------------------- /processing_r/processing/outputs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | *************************************************************************** 5 | Output.py 6 | --------------------- 7 | Date : August 2012 8 | Copyright : (C) 2012 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 | import sys 21 | 22 | from qgis.core import ( 23 | QgsProcessing, 24 | QgsProcessingOutputFile, 25 | QgsProcessingOutputFolder, 26 | QgsProcessingOutputHtml, 27 | QgsProcessingOutputMapLayer, 28 | QgsProcessingOutputNumber, 29 | QgsProcessingOutputRasterLayer, 30 | QgsProcessingOutputString, 31 | QgsProcessingOutputVectorLayer, 32 | QgsProcessingParameterFileDestination, 33 | QgsProcessingParameterFolderDestination, 34 | QgsProcessingParameterRasterDestination, 35 | QgsProcessingParameterVectorDestination, 36 | ) 37 | 38 | 39 | def create_output_from_string(s: str): 40 | """ 41 | Tries to create an algorithm output from a line string 42 | """ 43 | try: 44 | if "|" in s and s.startswith("Output"): 45 | tokens = s.split("|") 46 | params = [t if str(t) != "None" else None for t in tokens[1:]] 47 | clazz = getattr(sys.modules[__name__], tokens[0]) 48 | return clazz(*params) 49 | 50 | tokens = s.split("=") 51 | if tokens[1].lower().strip()[: len("output")] != "output": 52 | return None 53 | 54 | name = tokens[0] 55 | description = tokens[0] 56 | 57 | token = tokens[1].strip()[len("output") + 1 :] 58 | return create_output_from_token(name, description, token) 59 | 60 | except IndexError: 61 | return None 62 | 63 | 64 | OUTPUT_FACTORY = { 65 | "layer": QgsProcessingOutputMapLayer, 66 | "folder": QgsProcessingOutputFolder, 67 | "file": QgsProcessingOutputFile, 68 | "html": QgsProcessingOutputHtml, 69 | "number": QgsProcessingOutputNumber, 70 | "string": QgsProcessingOutputString, 71 | "raster": QgsProcessingOutputRasterLayer, 72 | } 73 | 74 | 75 | def create_output_from_token(name: str, description: str, token: str): # pylint: disable=too-many-branches 76 | """ 77 | Creates an output (or destination parameter) definition from a token string 78 | """ 79 | no_prompt = False 80 | if "noprompt" in token: 81 | no_prompt = True 82 | token = token.replace(" noprompt", "") 83 | 84 | output_type = token.lower().strip() 85 | 86 | out = None 87 | if output_type.startswith("vector"): 88 | vector_type = QgsProcessing.TypeVectorAnyGeometry 89 | if output_type == "vector point": 90 | vector_type = QgsProcessing.TypeVectorPoint 91 | elif output_type == "vector line": 92 | vector_type = QgsProcessing.TypeVectorLine 93 | elif output_type == "vector polygon": 94 | vector_type = QgsProcessing.TypeVectorPolygon 95 | if no_prompt: 96 | out = QgsProcessingOutputVectorLayer(name, description, vector_type) 97 | else: 98 | out = QgsProcessingParameterVectorDestination(name, description, type=vector_type) 99 | elif output_type.startswith("table"): 100 | if no_prompt: 101 | out = QgsProcessingOutputVectorLayer(name, description, QgsProcessing.TypeVector) 102 | else: 103 | out = QgsProcessingParameterVectorDestination(name, description, type=QgsProcessing.TypeVector) 104 | elif output_type == "multilayers": 105 | # out = QgsProcessingOutputMultipleLayers(name, description) 106 | # elif token.lower().strip() == 'vector point': 107 | # out = OutputVector(datatype=[dataobjects.TYPE_VECTOR_POINT]) 108 | # elif token.lower().strip() == 'vector line': 109 | # out = OutputVector(datatype=[OutputVector.TYPE_VECTOR_LINE]) 110 | # elif token.lower().strip() == 'vector polygon': 111 | # out = OutputVector(datatype=[OutputVector.TYPE_VECTOR_POLYGON]) 112 | # elif token.lower().strip().startswith('table'): 113 | # out = OutputTable() 114 | # elif token.lower().strip().startswith('file'): 115 | # out = OutputFile() 116 | # ext = token.strip()[len('file') + 1:] 117 | # if ext: 118 | # out.ext = ext 119 | # elif token.lower().strip().startswith('extent'): 120 | # out = OutputExtent() 121 | pass 122 | elif not no_prompt: 123 | if output_type.startswith("raster"): 124 | out = QgsProcessingParameterRasterDestination(name, description) 125 | elif output_type.startswith("folder"): 126 | out = QgsProcessingParameterFolderDestination(name, description) 127 | elif output_type.startswith("html"): 128 | out = QgsProcessingParameterFileDestination(name, description, "HTML Files (*.html)") 129 | elif output_type.startswith("file"): 130 | ext = token.strip()[len("file") + 1 :] 131 | if ext: 132 | out = QgsProcessingParameterFileDestination(name, description, "%s Files (*.%s)" % (ext.upper(), ext)) 133 | else: 134 | out = QgsProcessingParameterFileDestination(name, description) 135 | if not out and output_type in OUTPUT_FACTORY: 136 | return OUTPUT_FACTORY[output_type](name, description) 137 | if not out and output_type.startswith("file"): 138 | return OUTPUT_FACTORY["file"](name, description) 139 | 140 | return out 141 | -------------------------------------------------------------------------------- /processing_r/processing/parameters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | *************************************************************************** 5 | parameters.py 6 | --------------------- 7 | Date : April 2021 8 | Copyright : (C) 2021 by René-Luc Dhont 9 | Email : rldhont 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 | from processing.core.parameters import getParameterFromString 21 | 22 | from processing_r.processing.utils import RUtils 23 | 24 | 25 | def create_parameter_from_string(s: str): 26 | """ 27 | Tries to create an algorithm parameter from a line string 28 | """ 29 | if not ("|" in s and s.startswith("QgsProcessingParameter")): 30 | s = RUtils.upgrade_parameter_line(s) 31 | 32 | # this is necessary to remove the otherwise unknown keyword 33 | s = s.replace("enum literal", "enum") 34 | 35 | # this is annoying, but required to work around a bug in early 3.8.0 versions 36 | try: 37 | param = getParameterFromString(s, context="") 38 | except TypeError: 39 | param = getParameterFromString(s) 40 | 41 | return param 42 | -------------------------------------------------------------------------------- /processing_r/processing/provider.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | *************************************************************************** 5 | provider.py 6 | --------------------- 7 | Date : August 2012 8 | Copyright : (C) 2012 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 | import os 21 | 22 | from processing.core.ProcessingConfig import ProcessingConfig, Setting 23 | from processing.gui.ProviderActions import ProviderActions, ProviderContextMenuActions 24 | from qgis.core import Qgis, QgsMessageLog, QgsProcessingProvider 25 | from qgis.PyQt.QtCore import QCoreApplication 26 | 27 | from processing_r.gui.gui_utils import GuiUtils 28 | from processing_r.processing.actions.create_new_script import CreateNewScriptAction 29 | from processing_r.processing.actions.delete_script import DeleteScriptAction 30 | from processing_r.processing.actions.edit_script import EditScriptAction 31 | from processing_r.processing.algorithm import RAlgorithm 32 | from processing_r.processing.exceptions import InvalidScriptException 33 | from processing_r.processing.utils import RUtils, plugin_version 34 | 35 | 36 | class RAlgorithmProvider(QgsProcessingProvider): 37 | """ 38 | Processing provider for executing R scripts 39 | """ 40 | 41 | def __init__(self): 42 | super().__init__() 43 | self.algs = [] 44 | self.actions = [] 45 | create_script_action = CreateNewScriptAction() 46 | self.actions.append(create_script_action) 47 | self.contextMenuActions = [EditScriptAction(), DeleteScriptAction()] 48 | 49 | self.r_version = None 50 | 51 | def load(self): 52 | """ 53 | Called when first loading provider 54 | """ 55 | ProcessingConfig.settingIcons[self.name()] = self.icon() 56 | ProcessingConfig.addSetting( 57 | Setting( 58 | self.name(), 59 | RUtils.RSCRIPTS_FOLDER, 60 | self.tr("R scripts folder"), 61 | RUtils.default_scripts_folder(), 62 | valuetype=Setting.MULTIPLE_FOLDERS, 63 | ) 64 | ) 65 | 66 | ProcessingConfig.addSetting( 67 | Setting( 68 | self.name(), RUtils.R_USE_USER_LIB, self.tr("Use user library folder instead of system libraries"), True 69 | ) 70 | ) 71 | ProcessingConfig.addSetting( 72 | Setting( 73 | self.name(), 74 | RUtils.R_LIBS_USER, 75 | self.tr("User library folder"), 76 | RUtils.r_library_folder(), 77 | valuetype=Setting.FOLDER, 78 | ) 79 | ) 80 | 81 | ProcessingConfig.addSetting( 82 | Setting( 83 | self.name(), 84 | RUtils.R_REPO, 85 | self.tr("Package repository"), 86 | "http://cran.at.r-project.org/", 87 | valuetype=Setting.STRING, 88 | ) 89 | ) 90 | 91 | ProcessingConfig.addSetting( 92 | Setting( 93 | self.name(), RUtils.R_FOLDER, self.tr("R folder"), RUtils.r_binary_folder(), valuetype=Setting.FOLDER 94 | ) 95 | ) 96 | 97 | if RUtils.is_windows(): 98 | ProcessingConfig.addSetting(Setting(self.name(), RUtils.R_USE64, self.tr("Use 64 bit version"), False)) 99 | 100 | ProviderActions.registerProviderActions(self, self.actions) 101 | ProviderContextMenuActions.registerProviderContextMenuActions(self.contextMenuActions) 102 | ProcessingConfig.readSettings() 103 | self.refreshAlgorithms() 104 | self.r_version = RUtils.get_r_version() 105 | return True 106 | 107 | def unload(self): 108 | """ 109 | Called when unloading provider 110 | """ 111 | ProcessingConfig.removeSetting(RUtils.RSCRIPTS_FOLDER) 112 | ProcessingConfig.removeSetting(RUtils.R_LIBS_USER) 113 | ProcessingConfig.removeSetting(RUtils.R_FOLDER) 114 | if RUtils.is_windows(): 115 | ProcessingConfig.removeSetting(RUtils.R_USE64) 116 | ProviderActions.deregisterProviderActions(self) 117 | ProviderContextMenuActions.deregisterProviderContextMenuActions(self.contextMenuActions) 118 | 119 | def icon(self): 120 | """ 121 | Returns the provider's icon 122 | """ 123 | return GuiUtils.get_icon("providerR.svg") 124 | 125 | def svgIconPath(self): 126 | """ 127 | Returns a path to the provider's icon as a SVG file 128 | """ 129 | return GuiUtils.get_icon_svg("providerR.svg") 130 | 131 | def name(self): 132 | """ 133 | Display name for provider 134 | """ 135 | return self.tr("R") 136 | 137 | def versionInfo(self): 138 | """ 139 | Provider plugin version 140 | """ 141 | if not self.r_version: 142 | return "QGIS R Provider version {}".format(plugin_version()) 143 | 144 | return "QGIS R Provider version {}, {}".format(plugin_version(), self.r_version) 145 | 146 | def id(self): 147 | """ 148 | Unique ID for provider 149 | """ 150 | return "r" 151 | 152 | def loadAlgorithms(self): 153 | """ 154 | Called when provider must populate its available algorithms 155 | """ 156 | algs = [] 157 | for f in RUtils.script_folders(): 158 | algs.extend(self.load_scripts_from_folder(f)) 159 | 160 | for a in algs: 161 | self.addAlgorithm(a) 162 | 163 | def load_scripts_from_folder(self, folder): 164 | """ 165 | Loads all scripts found under the specified sub-folder 166 | """ 167 | if not os.path.exists(folder): 168 | return [] 169 | 170 | algs = [] 171 | for path, _, files in os.walk(folder): 172 | for description_file in files: 173 | if description_file.lower().endswith("rsx"): 174 | try: 175 | fullpath = os.path.join(path, description_file) 176 | alg = RAlgorithm(fullpath) 177 | if alg.name().strip(): 178 | algs.append(alg) 179 | except InvalidScriptException as e: 180 | QgsMessageLog.logMessage(e.msg, self.tr("Processing"), Qgis.Critical) 181 | except Exception as e: # pylint: disable=broad-except 182 | QgsMessageLog.logMessage( 183 | self.tr("Could not load R script: {0}\n{1}").format(description_file, str(e)), 184 | self.tr("Processing"), 185 | Qgis.Critical, 186 | ) 187 | return algs 188 | 189 | def tr(self, string, context=""): 190 | """ 191 | Translates a string 192 | """ 193 | if context == "": 194 | context = "RAlgorithmProvider" 195 | return QCoreApplication.translate(context, string) 196 | 197 | def supportedOutputTableExtensions(self): 198 | """ 199 | Extensions for non-spatial vector outputs 200 | """ 201 | return ["csv"] 202 | 203 | def defaultVectorFileExtension(self, hasGeometry=True): 204 | """ 205 | Default extension -- we use Geopackage for spatial layers, CSV for non-spatial layers 206 | """ 207 | return "gpkg" if hasGeometry else "csv" 208 | 209 | def supportsNonFileBasedOutput(self): 210 | """ 211 | Provider cannot handle memory layers/db sources 212 | """ 213 | return False 214 | -------------------------------------------------------------------------------- /processing_r/processing/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | *************************************************************************** 5 | utils.py 6 | --------------------- 7 | Date : August 2012 8 | Copyright : (C) 2012 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 | import configparser 20 | import os 21 | import pathlib 22 | import platform 23 | import re 24 | import subprocess 25 | import sys 26 | from ctypes import cdll 27 | from typing import Optional 28 | 29 | from processing.core.ProcessingConfig import ProcessingConfig 30 | from processing.tools.system import mkdir, userFolder 31 | from qgis.core import Qgis, QgsMessageLog, QgsProcessingUtils 32 | from qgis.PyQt.QtCore import QCoreApplication 33 | 34 | DEBUG = True 35 | 36 | 37 | class RUtils: # pylint: disable=too-many-public-methods 38 | """ 39 | Utilities for the R Provider and Algorithm 40 | """ 41 | 42 | RSCRIPTS_FOLDER = "R_SCRIPTS_FOLDER" 43 | R_FOLDER = "R_FOLDER" 44 | R_USE64 = "R_USE64" 45 | R_LIBS_USER = "R_LIBS_USER" 46 | R_USE_USER_LIB = "R_USE_USER_LIB" 47 | R_REPO = "R_REPO" 48 | 49 | VALID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 50 | 51 | @staticmethod 52 | def is_windows() -> bool: 53 | """ 54 | Returns True if the plugin is running on Windows 55 | """ 56 | return os.name == "nt" 57 | 58 | @staticmethod 59 | def is_macos() -> bool: 60 | """ 61 | Returns True if the plugin is running on MacOS 62 | """ 63 | return platform.system() == "Darwin" 64 | 65 | @staticmethod 66 | def r_binary_folder() -> str: 67 | """ 68 | Returns the folder (hopefully) containing R binaries 69 | """ 70 | folder = ProcessingConfig.getSetting(RUtils.R_FOLDER) 71 | if not folder: 72 | folder = RUtils.guess_r_binary_folder() 73 | 74 | return os.path.abspath(folder) if folder else "" 75 | 76 | @staticmethod 77 | def guess_r_binary_folder() -> str: 78 | """ 79 | Tries to pick a reasonable path for the R binaries to be executed from 80 | """ 81 | if RUtils.is_macos(): 82 | return "/usr/local/bin" 83 | 84 | if RUtils.is_windows(): 85 | search_paths = ["ProgramW6432", "PROGRAMFILES(x86)", "PROGRAMFILES", "C:\\"] 86 | r_folder = "" 87 | for path in search_paths: 88 | if path in os.environ and os.path.isdir(os.path.join(os.environ[path], "R")): 89 | r_folder = os.path.join(os.environ[path], "R") 90 | break 91 | 92 | if r_folder: 93 | sub_folders = os.listdir(r_folder) 94 | sub_folders.sort(reverse=True) 95 | for sub_folder in sub_folders: 96 | if sub_folder.upper().startswith("R-"): 97 | return os.path.join(r_folder, sub_folder) 98 | 99 | # expect R to be in OS path 100 | return "" 101 | 102 | @staticmethod 103 | def package_repo(): 104 | """ 105 | Returns the package repo URL 106 | """ 107 | return ProcessingConfig.getSetting(RUtils.R_REPO) 108 | 109 | @staticmethod 110 | def use_user_library(): 111 | """ 112 | Returns True if user library folder should be used instead of system folder 113 | """ 114 | return ProcessingConfig.getSetting(RUtils.R_USE_USER_LIB) 115 | 116 | @staticmethod 117 | def r_library_folder(): 118 | """ 119 | Returns the user R library folder 120 | """ 121 | folder = ProcessingConfig.getSetting(RUtils.R_LIBS_USER) 122 | if folder is None: 123 | folder = str(os.path.join(userFolder(), "rlibs")) 124 | try: 125 | mkdir(folder) 126 | except FileNotFoundError: 127 | folder = str(os.path.join(userFolder(), "rlibs")) 128 | mkdir(folder) 129 | return os.path.abspath(str(folder)) 130 | 131 | @staticmethod 132 | def builtin_scripts_folder(): 133 | """ 134 | Returns the built-in scripts path 135 | """ 136 | return os.path.join(os.path.dirname(__file__), "..", "builtin_scripts") 137 | 138 | @staticmethod 139 | def default_scripts_folder(): 140 | """ 141 | Returns the default path to look for user scripts within 142 | """ 143 | folder = os.path.join(userFolder(), "rscripts") 144 | mkdir(folder) 145 | return os.path.abspath(folder) 146 | 147 | @staticmethod 148 | def script_folders(): 149 | """ 150 | Returns a list of folders to search for scripts within 151 | """ 152 | folder = ProcessingConfig.getSetting(RUtils.RSCRIPTS_FOLDER) 153 | if folder is not None: 154 | folders = folder.split(";") 155 | else: 156 | folders = [RUtils.default_scripts_folder()] 157 | 158 | folders.append(RUtils.builtin_scripts_folder()) 159 | return folders 160 | 161 | @staticmethod 162 | def create_descriptive_name(name): 163 | """ 164 | Returns a safe version of a parameter name 165 | """ 166 | return name.replace("_", " ") 167 | 168 | @staticmethod 169 | def is_valid_r_variable(variable: str) -> bool: 170 | """ 171 | Check if given string is valid R variable name. 172 | :param variable: string 173 | :return: bool 174 | """ 175 | 176 | # only letters a-z, A-Z, numbers, dot and underscore 177 | x = re.search("[a-zA-Z0-9\\._]+", variable) 178 | 179 | result = True 180 | 181 | if variable == x.group(): 182 | # cannot start with number or underscore, or start with dot followed by number 183 | x = re.search("^[0-9|_]|^\\.[0-9]", variable) 184 | if x: 185 | result = False 186 | else: 187 | result = False 188 | 189 | return result 190 | 191 | @staticmethod 192 | def strip_special_characters(name): 193 | """ 194 | Strips non-alphanumeric characters from a name 195 | """ 196 | return "".join(c for c in name if c in RUtils.VALID_CHARS) 197 | 198 | @staticmethod 199 | def create_r_script_from_commands(commands): 200 | """ 201 | Creates an R script in a temporary location consisting of the given commands. 202 | Returns the path to the temporary script file. 203 | """ 204 | script_file = QgsProcessingUtils.generateTempFilename("processing_script.r") 205 | with open(script_file, "w", encoding="utf8") as f: 206 | for command in commands: 207 | f.write(command + "\n") 208 | return script_file 209 | 210 | @staticmethod 211 | def is_error_line(line): 212 | """ 213 | Returns True if the given line looks like an error message 214 | """ 215 | return any( 216 | [error in line for error in ["Error ", "Execution halted", "Error:"]] 217 | ) # pylint: disable=use-a-generator 218 | 219 | @staticmethod 220 | def get_windows_code_page(): 221 | """ 222 | Determines MS-Windows CMD.exe shell codepage. 223 | Used into GRASS exec script under MS-Windows. 224 | """ 225 | return str(cdll.kernel32.GetACP()) 226 | 227 | @staticmethod 228 | def get_process_startup_info(): 229 | """ 230 | Returns the correct startup info to use when calling commands for different platforms 231 | """ 232 | # For MS-Windows, we need to hide the console window. 233 | si = None 234 | if RUtils.is_windows(): 235 | si = subprocess.STARTUPINFO() 236 | si.dwFlags |= subprocess.STARTF_USESHOWWINDOW 237 | si.wShowWindow = subprocess.SW_HIDE 238 | return si 239 | 240 | @staticmethod 241 | def get_process_keywords(): 242 | """ 243 | Returns the correct process keywords dict to use when calling commands for different platforms 244 | """ 245 | kw = {} 246 | if RUtils.is_windows(): 247 | kw["startupinfo"] = RUtils.get_process_startup_info() 248 | if sys.version_info >= (3, 6): 249 | kw["encoding"] = "cp{}".format(RUtils.get_windows_code_page()) 250 | return kw 251 | 252 | @staticmethod 253 | def execute_r_algorithm(alg, parameters, context, feedback): 254 | """ 255 | Runs a prepared algorithm in R, and returns a list of the output received from R 256 | """ 257 | # generate new R script file name in a temp folder 258 | 259 | script_lines = alg.build_r_script(parameters, context, feedback) 260 | for line in script_lines: 261 | feedback.pushCommandInfo(line) 262 | 263 | script_filename = RUtils.create_r_script_from_commands(script_lines) 264 | 265 | # run commands 266 | command = [RUtils.path_to_r_executable(script_executable=True), script_filename] 267 | 268 | feedback.pushInfo(RUtils.tr("R execution console output")) 269 | 270 | console_results = [] 271 | 272 | with subprocess.Popen( 273 | command, 274 | stdout=subprocess.PIPE, 275 | stdin=subprocess.DEVNULL, 276 | stderr=subprocess.STDOUT, 277 | universal_newlines=True, 278 | **RUtils.get_process_keywords() 279 | ) as proc: 280 | for line in iter(proc.stdout.readline, ""): 281 | if feedback.isCanceled(): 282 | proc.terminate() 283 | 284 | if RUtils.is_error_line(line): 285 | feedback.reportError(line.strip()) 286 | else: 287 | feedback.pushConsoleInfo(line.strip()) 288 | console_results.append(line.strip()) 289 | return console_results 290 | 291 | @staticmethod 292 | def html_formatted_console_output(output): 293 | """ 294 | Returns a HTML formatted string of the given output lines 295 | """ 296 | s = "

{}

\n".format(RUtils.tr("R Output")) 297 | s += "\n" 298 | for line in output: 299 | s += "{}
\n".format(line) 300 | s += "
" 301 | return s 302 | 303 | @staticmethod 304 | def path_to_r_executable(script_executable=False) -> str: 305 | """ 306 | Returns the path to the R executable 307 | """ 308 | executable = "Rscript" if script_executable else "R" 309 | bin_folder = RUtils.r_binary_folder() 310 | if bin_folder: 311 | if RUtils.is_windows(): 312 | if ProcessingConfig.getSetting(RUtils.R_USE64): 313 | exec_dir = "x64" 314 | else: 315 | exec_dir = "i386" 316 | return os.path.join(bin_folder, "bin", exec_dir, "{}.exe".format(executable)) 317 | return os.path.join(bin_folder, executable) 318 | 319 | return executable 320 | 321 | @staticmethod 322 | def check_r_is_installed() -> Optional[str]: 323 | """ 324 | Checks if R is installed and working. Returns None if R IS working, 325 | or an error string if R was not found. 326 | """ 327 | if DEBUG: 328 | QgsMessageLog.logMessage( 329 | RUtils.tr("R binary path: {}").format(RUtils.path_to_r_executable()), "R", Qgis.Info 330 | ) 331 | 332 | if RUtils.is_windows(): 333 | path = RUtils.r_binary_folder() 334 | if path == "": 335 | return RUtils.tr("R folder is not configured.\nPlease configure " "it before running R scripts.") 336 | 337 | command = [RUtils.path_to_r_executable(), "--version"] 338 | try: 339 | with subprocess.Popen( 340 | command, 341 | stdout=subprocess.PIPE, 342 | stdin=subprocess.DEVNULL, 343 | stderr=subprocess.STDOUT, 344 | universal_newlines=True, 345 | **RUtils.get_process_keywords() 346 | ) as proc: 347 | for line in proc.stdout: 348 | if ("R version" in line) or ("R Under development" in line): 349 | return None 350 | except FileNotFoundError: 351 | pass 352 | 353 | html = RUtils.tr( 354 | "

This algorithm requires R to be run. Unfortunately, it " 355 | "seems that R is not installed in your system, or it is not " 356 | "correctly configured to be used from QGIS

" 357 | '

Click here ' 358 | "to know more about how to install and configure R to be used with QGIS

" 359 | ) 360 | 361 | return html 362 | 363 | @staticmethod 364 | def get_r_version() -> Optional[str]: 365 | """ 366 | Returns the current installed R version, or None if R is not found 367 | """ 368 | if RUtils.is_windows() and not RUtils.r_binary_folder(): 369 | return None 370 | 371 | command = [RUtils.path_to_r_executable(), "--version"] 372 | try: 373 | with subprocess.Popen( 374 | command, 375 | stdout=subprocess.PIPE, 376 | stdin=subprocess.DEVNULL, 377 | stderr=subprocess.STDOUT, 378 | universal_newlines=True, 379 | **RUtils.get_process_keywords() 380 | ) as proc: 381 | for line in proc.stdout: 382 | if ("R version" in line) or ("R Under development" in line): 383 | return line 384 | except FileNotFoundError: 385 | pass 386 | 387 | return None 388 | 389 | @staticmethod 390 | def get_required_packages(code): 391 | """ 392 | Returns a list of the required packages 393 | """ 394 | regex = re.compile(r'[^#]library\("?(.*?)"?\)') 395 | return regex.findall(code) 396 | 397 | @staticmethod 398 | def upgrade_parameter_line(line: str) -> str: 399 | """ 400 | Upgrades a parameter definition line from 2.x to 3.x format 401 | """ 402 | # alias 'selection' to 'enum' 403 | if "=selection" in line: 404 | line = line.replace("=selection", "=enum") 405 | if "=vector" in line: 406 | line = line.replace("=vector", "=source") 407 | return line 408 | 409 | @staticmethod 410 | def tr(string, context=""): 411 | """ 412 | Translates a string 413 | """ 414 | if context == "": 415 | context = "RUtils" 416 | return QCoreApplication.translate(context, string) 417 | 418 | 419 | def log(message: str) -> None: 420 | """ 421 | Simple logging function, most for debuging. 422 | """ 423 | QgsMessageLog.logMessage(message, "Processing R Plugin", Qgis.Info) 424 | 425 | 426 | def _read_metadata() -> configparser.ConfigParser: 427 | """ 428 | Read metadata file. 429 | """ 430 | path = pathlib.Path(__file__).parent.parent / "metadata.txt" 431 | 432 | config = configparser.ConfigParser() 433 | config.read(path) 434 | 435 | return config 436 | 437 | 438 | def plugin_version() -> str: 439 | """ 440 | Get plugin version. 441 | """ 442 | config = _read_metadata() 443 | return config["general"]["version"] 444 | -------------------------------------------------------------------------------- /processing_r/r_plugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """QGIS Processing R Provider 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | __author__ = "(C) 2018 by Nyall Dawson" 11 | __date__ = "20/10/2018" 12 | __copyright__ = "Copyright 2018, North Road" 13 | # This will get replaced with a git SHA1 when you do a git archive 14 | __revision__ = "$Format:%H$" 15 | 16 | import os 17 | 18 | from qgis.core import QgsApplication 19 | from qgis.gui import QgisInterface 20 | from qgis.PyQt.QtCore import QCoreApplication, QTranslator 21 | 22 | from processing_r.processing.provider import RAlgorithmProvider 23 | 24 | 25 | class RProviderPlugin: 26 | """QGIS Plugin Implementation.""" 27 | 28 | def __init__(self, iface: QgisInterface): 29 | """Constructor. 30 | 31 | :param iface: An interface instance that will be passed to this class 32 | which provides the hook by which you can manipulate the QGIS 33 | application at run time. 34 | :type iface: QgsInterface 35 | """ 36 | super().__init__() 37 | # Save reference to the QGIS interface 38 | self.iface = iface 39 | # initialize plugin directory 40 | self.plugin_dir = os.path.dirname(__file__) 41 | # initialize locale 42 | locale = QgsApplication.locale() 43 | locale_path = os.path.join(self.plugin_dir, "i18n", "{}.qm".format(locale)) 44 | 45 | if os.path.exists(locale_path): 46 | self.translator = QTranslator() 47 | self.translator.load(locale_path) 48 | QCoreApplication.installTranslator(self.translator) 49 | 50 | # processing framework 51 | self.provider = RAlgorithmProvider() 52 | 53 | @staticmethod 54 | def tr(message): 55 | """Get the translation for a string using Qt translation API. 56 | 57 | We implement this ourselves since we do not inherit QObject. 58 | 59 | :param message: String for translation. 60 | :type message: str, QString 61 | 62 | :returns: Translated version of message. 63 | :rtype: QString 64 | """ 65 | # noinspection PyTypeChecker,PyArgumentList,PyCallByClass 66 | return QCoreApplication.translate("RProvider", message) 67 | 68 | def initProcessing(self): 69 | """Create the Processing provider""" 70 | QgsApplication.processingRegistry().addProvider(self.provider) 71 | 72 | def initGui(self): 73 | """Creates application GUI widgets""" 74 | self.initProcessing() 75 | 76 | def unload(self): 77 | """Removes the plugin menu item and icon from QGIS GUI.""" 78 | QgsApplication.processingRegistry().removeProvider(self.provider) 79 | -------------------------------------------------------------------------------- /processing_r/ui/DlgScriptEditor.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 721 10 | 578 11 | 12 | 13 | 14 | Processing Script Editor 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 0 26 | 27 | 28 | 5 29 | 30 | 31 | 0 32 | 33 | 34 | 5 35 | 36 | 37 | 38 | 39 | Case sensitive 40 | 41 | 42 | 43 | 44 | 45 | 46 | Whole word 47 | 48 | 49 | 50 | 51 | 52 | 53 | Replace 54 | 55 | 56 | 57 | 58 | 59 | 60 | Find what: 61 | 62 | 63 | 64 | 65 | 66 | 67 | Replace with: 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Find 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | Qt::Vertical 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Toolbar 99 | 100 | 101 | TopToolBarArea 102 | 103 | 104 | false 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | Open Script… 127 | 128 | 129 | Open Script 130 | 131 | 132 | Ctrl+O 133 | 134 | 135 | 136 | 137 | Save Script… 138 | 139 | 140 | Save Script 141 | 142 | 143 | Ctrl+S 144 | 145 | 146 | 147 | 148 | Save Script as… 149 | 150 | 151 | Save Script as 152 | 153 | 154 | Ctrl+Shift+S 155 | 156 | 157 | 158 | 159 | Run Script 160 | 161 | 162 | Run Script 163 | 164 | 165 | F5 166 | 167 | 168 | 169 | 170 | Cut 171 | 172 | 173 | Cut 174 | 175 | 176 | Ctrl+X 177 | 178 | 179 | 180 | 181 | Copy 182 | 183 | 184 | Copy 185 | 186 | 187 | Ctrl+C 188 | 189 | 190 | 191 | 192 | Paste 193 | 194 | 195 | Paste 196 | 197 | 198 | Ctrl+V 199 | 200 | 201 | 202 | 203 | Undo 204 | 205 | 206 | Undo 207 | 208 | 209 | Ctrl+Z 210 | 211 | 212 | 213 | 214 | Redo 215 | 216 | 217 | Redo 218 | 219 | 220 | Ctrl+Shift+Z 221 | 222 | 223 | 224 | 225 | Increase Font Size 226 | 227 | 228 | Increase Font Size 229 | 230 | 231 | 232 | 233 | Decrease Font Size 234 | 235 | 236 | Decrease Font Size 237 | 238 | 239 | 240 | 241 | true 242 | 243 | 244 | Ctrl+F 245 | 246 | 247 | Find && &Replace 248 | 249 | 250 | 251 | 252 | 253 | ScriptEdit 254 | QTextEdit 255 |
processing_r.gui.script_editor.script_editor_widget
256 |
257 |
258 | 259 | 260 |
261 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS,script_editor 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | 25 | [MESSAGES CONTROL] 26 | 27 | # Enable the message, report, category or checker with the given id(s). You can 28 | # either give multiple identifier separated by comma (,) or put this option 29 | # multiple time. See also the "--disable" option for examples. 30 | #enable= 31 | 32 | # Disable the message, report, category or checker with the given id(s). You 33 | # can either give multiple identifiers separated by comma (,) or put this 34 | # option multiple times (only on the command line, not in the configuration 35 | # file where it should appear only once).You can also use "--disable=all" to 36 | # disable everything first and then reenable specific checks. For example, if 37 | # you want to run only the similarities checker, you can use "--disable=all 38 | # --enable=similarities". If you want to run only the classes checker, but have 39 | # no Warning level messages displayed, use"--disable=all --enable=classes 40 | # --disable=W" 41 | # see http://stackoverflow.com/questions/21487025/pylint-locally-defined-disables-still-give-warnings-how-to-suppress-them 42 | disable=locally-disabled,C0103,no-name-in-module,duplicate-code,import-error,line-too-long,too-many-arguments,too-many-instance-attributes,no-self-use,too-few-public-methods,bad-option-value,fixme,consider-using-f-string,implicit-str-concat,use-a-generator,too-many-locals,too-many-branches,too-many-statements 43 | 44 | 45 | [REPORTS] 46 | 47 | # Set the output format. Available formats are text, parseable, colorized, msvs 48 | # (visual studio) and html. You can also give a reporter class, eg 49 | # mypackage.mymodule.MyReporterClass. 50 | output-format=text 51 | 52 | # Put messages in a separate file for each module / package specified on the 53 | # command line instead of printing them on stdout. Reports (if any) will be 54 | # written in a file name "pylint_global.[txt|html]". 55 | files-output=no 56 | 57 | # Tells whether to display a full report or only the messages 58 | reports=yes 59 | 60 | # Python expression which should return a note less than 10 (10 is the highest 61 | # note). You have access to the variables errors warning, statement which 62 | # respectively contain the number of errors / warnings messages and the total 63 | # number of statements analyzed. This is used by the global evaluation report 64 | # (RP0004). 65 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 66 | 67 | # Add a comment according to your evaluation note. This is used by the global 68 | # evaluation report (RP0004). 69 | comment=no 70 | 71 | # Template used to display messages. This is a python new-style format string 72 | # used to format the message information. See doc for all details 73 | #msg-template= 74 | 75 | 76 | [BASIC] 77 | 78 | # Required attributes for module, separated by a comma 79 | required-attributes= 80 | 81 | # List of builtins function names that should not be used, separated by a comma 82 | bad-functions=map,filter,apply,input 83 | 84 | # Regular expression which should only match correct module names 85 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 86 | 87 | # Regular expression which should only match correct module level names 88 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 89 | 90 | # Regular expression which should only match correct class names 91 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 92 | 93 | # Regular expression which should only match correct function names 94 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 95 | 96 | # Regular expression which should only match correct method names 97 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 98 | 99 | # Regular expression which should only match correct instance attribute names 100 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 101 | 102 | # Regular expression which should only match correct argument names 103 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 104 | 105 | # Regular expression which should only match correct variable names 106 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 107 | 108 | # Regular expression which should only match correct attribute names in class 109 | # bodies 110 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 111 | 112 | # Regular expression which should only match correct list comprehension / 113 | # generator expression variable names 114 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 115 | 116 | # Good variable names which should always be accepted, separated by a comma 117 | good-names=i,j,k,ex,Run,_ 118 | 119 | # Bad variable names which should always be refused, separated by a comma 120 | bad-names=foo,bar,baz,toto,tutu,tata 121 | 122 | # Regular expression which should only match function or class names that do 123 | # not require a docstring. 124 | no-docstring-rgx=__.*__ 125 | 126 | # Minimum line length for functions/classes that require docstrings, shorter 127 | # ones are exempt. 128 | docstring-min-length=-1 129 | 130 | 131 | [MISCELLANEOUS] 132 | 133 | # List of note tags to take in consideration, separated by a comma. 134 | notes=FIXME,XXX,TODO 135 | 136 | 137 | [TYPECHECK] 138 | 139 | # Tells whether missing members accessed in mixin class should be ignored. A 140 | # mixin class is detected if its name ends with "mixin" (case insensitive). 141 | ignore-mixin-members=yes 142 | 143 | # List of classes names for which member attributes should not be checked 144 | # (useful for classes with attributes dynamically set). 145 | ignored-classes=SQLObject 146 | 147 | # When zope mode is activated, add a predefined set of Zope acquired attributes 148 | # to generated-members. 149 | zope=no 150 | 151 | # List of members which are set dynamically and missed by pylint inference 152 | # system, and so shouldn't trigger E0201 when accessed. Python regular 153 | # expressions are accepted. 154 | generated-members=REQUEST,acl_users,aq_parent 155 | 156 | 157 | [VARIABLES] 158 | 159 | # Tells whether we should check for unused import in __init__ files. 160 | init-import=no 161 | 162 | # A regular expression matching the beginning of the name of dummy variables 163 | # (i.e. not used). 164 | dummy-variables-rgx=_$|dummy 165 | 166 | # List of additional names supposed to be defined in builtins. Remember that 167 | # you should avoid to define new builtins when possible. 168 | additional-builtins= 169 | 170 | 171 | [FORMAT] 172 | 173 | # Maximum number of characters on a single line. 174 | max-line-length=80 175 | 176 | # Regexp for a line that is allowed to be longer than the limit. 177 | ignore-long-lines=^\s*(# )??$ 178 | 179 | # Allow the body of an if to be on the same line as the test if there is no 180 | # else. 181 | single-line-if-stmt=no 182 | 183 | # List of optional constructs for which whitespace checking is disabled 184 | no-space-check=trailing-comma,dict-separator 185 | 186 | # Maximum number of lines in a module 187 | max-module-lines=1000 188 | 189 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 190 | # tab). 191 | indent-string=' ' 192 | 193 | 194 | [SIMILARITIES] 195 | 196 | # Minimum lines number of a similarity. 197 | min-similarity-lines=4 198 | 199 | # Ignore comments when computing similarities. 200 | ignore-comments=yes 201 | 202 | # Ignore docstrings when computing similarities. 203 | ignore-docstrings=yes 204 | 205 | # Ignore imports when computing similarities. 206 | ignore-imports=no 207 | 208 | 209 | [IMPORTS] 210 | 211 | # Deprecated modules which should not be used, separated by a comma 212 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 213 | 214 | # Create a graph of every (i.e. internal and external) dependencies in the 215 | # given file (report RP0402 must not be disabled) 216 | import-graph= 217 | 218 | # Create a graph of external dependencies in the given file (report RP0402 must 219 | # not be disabled) 220 | ext-import-graph= 221 | 222 | # Create a graph of internal dependencies in the given file (report RP0402 must 223 | # not be disabled) 224 | int-import-graph= 225 | 226 | 227 | [DESIGN] 228 | 229 | # Maximum number of arguments for function / method 230 | max-args=5 231 | 232 | # Argument names that match this expression will be ignored. Default to name 233 | # with leading underscore 234 | ignored-argument-names=_.* 235 | 236 | # Maximum number of locals for function / method body 237 | max-locals=15 238 | 239 | # Maximum number of return / yield for function / method body 240 | max-returns=6 241 | 242 | # Maximum number of branch for function / method body 243 | max-branches=12 244 | 245 | # Maximum number of statements in function / method body 246 | max-statements=50 247 | 248 | # Maximum number of parents for a class (see R0901). 249 | max-parents=7 250 | 251 | # Maximum number of attributes for a class (see R0902). 252 | max-attributes=7 253 | 254 | # Minimum number of public methods for a class (see R0903). 255 | min-public-methods=2 256 | 257 | # Maximum number of public methods for a class (see R0904). 258 | max-public-methods=21 259 | 260 | 261 | [CLASSES] 262 | 263 | # List of interface methods to ignore, separated by a comma. This is used for 264 | # instance to not check methods defines in Zope's Interface base class. 265 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 266 | 267 | # List of method names used to declare (i.e. assign) instance attributes. 268 | defining-attr-methods=__init__,__new__,setUp 269 | 270 | # List of valid names for the first argument in a class method. 271 | valid-classmethod-first-arg=cls 272 | 273 | # List of valid names for the first argument in a metaclass class method. 274 | valid-metaclass-classmethod-first-arg=mcs 275 | 276 | 277 | [EXCEPTIONS] 278 | 279 | # Exceptions that will emit a warning when being caught. Defaults to 280 | # "Exception" 281 | overgeneral-exceptions=Exception 282 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = [ 3 | ".git", 4 | "__pycache__", 5 | "tests/*" 6 | ] 7 | max-line-length = 120 8 | 9 | 10 | [tool.pytest.ini_options] 11 | testpaths = [ 12 | "tests" 13 | ] 14 | addopts = [ 15 | "--cov=processing_r", 16 | "--cov-report=term-missing:skip-covered", 17 | "-rP", 18 | "-vv", 19 | "-s" 20 | ] 21 | 22 | [tool.black] 23 | line-length = 120 24 | 25 | [tool.isort] 26 | atomic = true 27 | profile = "black" 28 | line_length = 120 29 | skip_gitignore = true 30 | 31 | [tool.pylint.'MESSAGES CONTROL'] 32 | max-line-length = 120 33 | disable = "C0103,C0209,E0611,R0904,R0912,R0914,R0915,R1729,W0611,W1404" 34 | -------------------------------------------------------------------------------- /scripts/compile-strings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LRELEASE=$1 3 | LOCALES=$2 4 | 5 | 6 | for LOCALE in ${LOCALES} 7 | do 8 | echo "Processing: ${LOCALE}.ts" 9 | # Note we don't use pylupdate with qt .pro file approach as it is flakey 10 | # about what is made available. 11 | $LRELEASE processing_r/i18n/${LOCALE}.ts 12 | done 13 | -------------------------------------------------------------------------------- /scripts/run-env-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | QGIS_PREFIX_PATH=/usr/local/qgis-3.0 4 | if [ -n "$1" ]; then 5 | QGIS_PREFIX_PATH=$1 6 | fi 7 | 8 | echo ${QGIS_PREFIX_PATH} 9 | 10 | 11 | export QGIS_PREFIX_PATH=${QGIS_PREFIX_PATH} 12 | export QGIS_PATH=${QGIS_PREFIX_PATH} 13 | export LD_LIBRARY_PATH=${QGIS_PREFIX_PATH}/lib 14 | export PYTHONPATH=${QGIS_PREFIX_PATH}/share/qgis/python:${QGIS_PREFIX_PATH}/python:${QGIS_PREFIX_PATH}/python/plugins:${PYTHONPATH} 15 | 16 | echo "QGIS PATH: $QGIS_PREFIX_PATH" 17 | export QGIS_DEBUG=0 18 | export QGIS_LOG_FILE=/tmp/redistrict/realtime/logs/qgis.log 19 | 20 | export PATH=${QGIS_PREFIX_PATH}/bin:$PATH 21 | 22 | echo "This script is intended to be sourced to set up your shell to" 23 | echo "use a QGIS 3.0 built in $QGIS_PREFIX_PATH" 24 | echo 25 | echo "To use it do:" 26 | echo "source $BASH_SOURCE /your/optional/install/path" 27 | echo 28 | echo "Then use the make file supplied here e.g. make guitest" 29 | -------------------------------------------------------------------------------- /scripts/run_docker_locally.sh: -------------------------------------------------------------------------------- 1 | DOCKER_IMAGE=qgis/qgis 2 | DOCKER_IMAGE_TAG=latest 3 | CONTAINER_NAME=qgis-testing-environment 4 | PYTHON_SETUP="PYTHONPATH=/usr/share/qgis/python/:/usr/share/qgis/python/plugins:/usr/lib/python3/dist-packages/qgis:/usr/share/qgis/python/qgis:/tests_directory" 5 | 6 | if [ ! "$(docker ps -a | grep $CONTAINER_NAME)" ]; then 7 | echo "CONTAINER NOT FOUND CREATING!" 8 | 9 | docker pull "$DOCKER_IMAGE":$DOCKER_IMAGE_TAG 10 | docker run -d --name $CONTAINER_NAME -v .:/tests_directory -e DISPLAY=:99 "$DOCKER_IMAGE":$DOCKER_IMAGE_TAG 11 | 12 | docker exec $CONTAINER_NAME sh -c "pip3 install -r /tests_directory/REQUIREMENTS_TESTING.txt" 13 | docker exec $CONTAINER_NAME sh -c "apt-get update -qq" 14 | docker exec $CONTAINER_NAME sh -c "apt-get install --yes --no-install-recommends wget ca-certificates gnupg" 15 | 16 | docker exec $CONTAINER_NAME sh -c "wget -q -O- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | tee -a /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc" 17 | docker exec $CONTAINER_NAME sh -c "echo \"deb [arch=amd64] https://cloud.r-project.org/bin/linux/ubuntu jammy-cran40/\" > /etc/apt/sources.list.d/cran_r.list" 18 | docker exec $CONTAINER_NAME sh -c "apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 67C2D66C4B1D4339 51716619E084DAB9" 19 | 20 | docker exec $CONTAINER_NAME sh -c "wget -q -O- https://eddelbuettel.github.io/r2u/assets/dirk_eddelbuettel_key.asc | tee -a /etc/apt/trusted.gpg.d/cranapt_key.asc" 21 | docker exec $CONTAINER_NAME sh -c "echo \"deb [arch=amd64] https://r2u.stat.illinois.edu/ubuntu jammy main\" > /etc/apt/sources.list.d/cranapt.list" 22 | 23 | docker exec $CONTAINER_NAME sh -c "apt update -qq" 24 | 25 | docker exec $CONTAINER_NAME sh -c "apt-get install -y r-base r-cran-sf r-cran-raster" 26 | else 27 | echo "CONTAINER FOUND, STARTING" 28 | docker start $CONTAINER_NAME 29 | fi 30 | 31 | docker exec $CONTAINER_NAME sh -c "$PYTHON_SETUP && cd tests_directory && pytest tests --cov=processing_r --cov-report=term-missing:skip-covered -rP -vv -s" 32 | 33 | docker stop $CONTAINER_NAME 34 | echo "CONTAINER STOPPED" 35 | -------------------------------------------------------------------------------- /scripts/update-strings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LOCALES=$* 3 | 4 | PYLUPDATE=${PYLUPDATE:-pylupdate5} 5 | 6 | # Get newest .py files so we don't update strings unnecessarily 7 | 8 | CHANGED_FILES=0 9 | PYTHON_FILES=`find . -regex ".*\(ui\|py\)$" -type f` 10 | for PYTHON_FILE in $PYTHON_FILES 11 | do 12 | CHANGED=$(stat -c %Y $PYTHON_FILE) 13 | if [ ${CHANGED} -gt ${CHANGED_FILES} ] 14 | then 15 | CHANGED_FILES=${CHANGED} 16 | fi 17 | done 18 | 19 | # Qt translation stuff 20 | # for .ts file 21 | UPDATE=false 22 | for LOCALE in ${LOCALES} 23 | do 24 | TRANSLATION_FILE="r/i18n/$LOCALE.ts" 25 | if [ ! -f ${TRANSLATION_FILE} ] 26 | then 27 | # Force translation string collection as we have a new language file 28 | touch ${TRANSLATION_FILE} 29 | UPDATE=true 30 | break 31 | fi 32 | 33 | MODIFICATION_TIME=$(stat -c %Y ${TRANSLATION_FILE}) 34 | if [ ${CHANGED_FILES} -gt ${MODIFICATION_TIME} ] 35 | then 36 | # Force translation string collection as a .py file has been updated 37 | UPDATE=true 38 | break 39 | fi 40 | done 41 | 42 | if [ ${UPDATE} == true ] 43 | # retrieve all python files 44 | then 45 | print ${PYTHON_FILES} 46 | # update .ts 47 | echo "Please provide translations by editing the translation files below:" 48 | for LOCALE in ${LOCALES} 49 | do 50 | echo "r/i18n/"${LOCALE}".ts" 51 | # Note we don't use pylupdate with qt .pro file approach as it is flakey 52 | # about what is made available. 53 | ${PYLUPDATE} -noobsolete ${PYTHON_FILES} -ts r/i18n/${LOCALE}.ts 54 | done 55 | else 56 | echo "No need to edit any translation files (.ts) because no python files" 57 | echo "has been updated since the last update translation. " 58 | fi 59 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | from processing.core.ProcessingConfig import ProcessingConfig, Setting 5 | from qgis.core import QgsApplication 6 | from qgis.PyQt.QtCore import QCoreApplication, QSettings 7 | 8 | from processing_r.processing.provider import RAlgorithmProvider 9 | from processing_r.processing.utils import RUtils 10 | 11 | 12 | @pytest.fixture 13 | def data_folder() -> Path: 14 | return Path(__file__).parent / "data" 15 | 16 | 17 | @pytest.fixture(autouse=True, scope="session") 18 | def setup_plugin(): 19 | QCoreApplication.setOrganizationName("North Road") 20 | QCoreApplication.setOrganizationDomain("qgis.org") 21 | QCoreApplication.setApplicationName("QGIS-R") 22 | QSettings().clear() 23 | 24 | 25 | @pytest.fixture(autouse=True, scope="session") 26 | def plugin_provider(setup_plugin, qgis_processing) -> RAlgorithmProvider: 27 | provider = RAlgorithmProvider() 28 | 29 | QgsApplication.processingRegistry().addProvider(provider) 30 | 31 | test_scripts_path = Path(__file__).parent / "scripts" 32 | scripts_paths = ProcessingConfig.getSetting(RUtils.RSCRIPTS_FOLDER) + ";" + test_scripts_path.as_posix() 33 | 34 | ProcessingConfig.setSettingValue(RUtils.RSCRIPTS_FOLDER, scripts_paths) 35 | 36 | provider.loadAlgorithms() 37 | 38 | return provider 39 | -------------------------------------------------------------------------------- /tests/data/dem.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/tests/data/dem.tif -------------------------------------------------------------------------------- /tests/data/dem.tif.aux.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 243 5 | 147.17197994967 6 | 85 7 | 43.961643261926 8 | 100 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/data/dem2.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/tests/data/dem2.tif -------------------------------------------------------------------------------- /tests/data/dem2.tif.aux.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 243 5 | 147.17197994967 6 | 85 7 | 43.961643261926 8 | 100 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/data/lines.dbf: -------------------------------------------------------------------------------- 1 | _aiNameCPValueN Highway 1.000000000000000 Highway 1.000000000000000 Arterial 2.000000000000000 Arterial 2.000000000000000 Arterial 2.000000000000000 Arterial 2.000000000000000 -------------------------------------------------------------------------------- /tests/data/lines.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]] -------------------------------------------------------------------------------- /tests/data/lines.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/tests/data/lines.shp -------------------------------------------------------------------------------- /tests/data/lines.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/tests/data/lines.shx -------------------------------------------------------------------------------- /tests/data/points.gml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 0-5 10 | 83 11 | 12 | 13 | 14 | 15 | 16 | 1,1 17 | 1 18 | 2 19 | 20 | 21 | 22 | 23 | 3,3 24 | 2 25 | 1 26 | 27 | 28 | 29 | 30 | 2,2 31 | 3 32 | 0 33 | 34 | 35 | 36 | 37 | 5,2 38 | 4 39 | 2 40 | 41 | 42 | 43 | 44 | 4,1 45 | 5 46 | 1 47 | 48 | 49 | 50 | 51 | 0,-5 52 | 6 53 | 0 54 | 55 | 56 | 57 | 58 | 8,-1 59 | 7 60 | 0 61 | 62 | 63 | 64 | 65 | 7,-1 66 | 8 67 | 0 68 | 69 | 70 | 71 | 72 | 0,-1 73 | 9 74 | 0 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /tests/data/points.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/data/test_gpkg.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/tests/data/test_gpkg.gpkg -------------------------------------------------------------------------------- /tests/scripts/bad_algorithm.rsx: -------------------------------------------------------------------------------- 1 | ##polyg=xvector 2 | library(sp) 3 | i <- 1 4 | -------------------------------------------------------------------------------- /tests/scripts/test_algorithm_1.rsx: -------------------------------------------------------------------------------- 1 | ##output_plots_to_html 2 | ##pass_filenames 3 | 4 | ##polyg=vector 5 | ##polyg_category_id=field polyg 6 | ##table_category_id=field sizes_table 7 | ##sampling_size=field sizes_table 8 | ##output=output vector 9 | library(sp) 10 | i <- 1 11 | category <- unique(polyg[[polyg_category_id]])[i] 12 | categorymap <- polyg[polyg[[polyg_category_id]] == category,] 13 | n <- sizes_table[which(sizes_table[[table_category_id]] == category), sampling_size] 14 | spdf1 <- SpatialPointsDataFrame(spsample(categorymap, n, "random"), data = data.frame(category = rep(category, n))) 15 | 16 | for (i in 2:length(unique(polyg[[polyg_category_id]]))){ 17 | category <- unique(polyg[[polyg_category_id]])[i] 18 | categorymap <- polyg[polyg[[polyg_category_id]] == category,] 19 | n <- sizes_table[which(sizes_table[[table_category_id]] == category), sampling_size] 20 | spdf1 <- rbind(spdf1, SpatialPointsDataFrame(spsample(categorymap, n, "random"), 21 | data = data.frame(category = rep(category, n))) 22 | ) 23 | } 24 | output = spdf1 25 | -------------------------------------------------------------------------------- /tests/scripts/test_algorithm_1.rsx.help: -------------------------------------------------------------------------------- 1 | {"ALG_DESC": "Test help.", "ALG_CREATOR": "Me", "polyg": "A polygon layer", "RPLOTS": "Plot output", "ALG_HELP_CREATOR": "Me2"} 2 | -------------------------------------------------------------------------------- /tests/scripts/test_algorithm_2.rsx: -------------------------------------------------------------------------------- 1 | ##my test=name 2 | ##my group=group 3 | 4 | ##in_raster=raster 5 | ##in_vector=vector 6 | ##in_field=field in_vector 7 | ##in_extent=extent 8 | ##in_crs=crs 9 | ##in_string=string 10 | ##in_file=file 11 | ##in_number=number 12 | ##in_enum=enum 13 | # for compatibility with 2.x scripts, we alias selection->enum 14 | ##in_enum2=selection normal;log10;ln;sqrt;exp 15 | ##in_bool=boolean 16 | 17 | ##param_vector_dest=output vector 18 | ##param_vector_dest2= output vector 19 | ##param_vector_point_dest=output vector point 20 | ##param_vector_line_dest=output vector line 21 | ##param_vector_polygon_dest=output vector polygon 22 | ##param_table_dest=output table 23 | ##param_raster_dest=output raster 24 | 25 | ##out_vector=output vector noprompt 26 | ##out_table=output table noprompt 27 | ##out_raster=output raster noprompt 28 | ##out_number=output number 29 | ##out_string=output string 30 | ##out_layer=output layer 31 | ##param_folder_dest=output folder 32 | ##out_folder=output folder noprompt 33 | ##param_html_dest=output html 34 | ##out_html=output html noprompt 35 | ##param_file_dest=output file 36 | ##out_file=output file noprompt 37 | ##param_csv_dest=output file csv 38 | ##out_csv=output file csv noprompt 39 | 40 | library(sp) 41 | i <- 1 42 | 43 | -------------------------------------------------------------------------------- /tests/scripts/test_algorithm_3.rsx: -------------------------------------------------------------------------------- 1 | ##thealgid=name 2 | ##the algo title=display_name 3 | ##my group=group 4 | 5 | ##in_number=number 6 | 7 | ##out_number=output number 8 | 9 | library(sp) 10 | i <- 1 11 | 12 | -------------------------------------------------------------------------------- /tests/scripts/test_algorithm_4.rsx: -------------------------------------------------------------------------------- 1 | ##my test=name 2 | ##my group=group 3 | 4 | ##QgsProcessingParameterRasterLayer|in_raster|Input raster 5 | ##QgsProcessingParameterFeatureSource|in_vector|Input vector 6 | ##QgsProcessingParameterField|in_field|Input field|None|in_vector 7 | ##QgsProcessingParameterExtent|in_extent|Input extent 8 | ##QgsProcessingParameterCrs|in_crs|Input CRS 9 | ##QgsProcessingParameterString|in_string|Input String 10 | ##QgsProcessingParameterNumber|in_number|Input number 11 | ##QgsProcessingParameterEnum|in_enum|Input enum 12 | ##QgsProcessingParameterBoolean|in_bool|Input boolean 13 | 14 | ##QgsProcessingParameterFile|in_file|Input file 15 | ##QgsProcessingParameterFile|in_folder|Input folder|1 16 | ##QgsProcessingParameterFile|in_gpkg|Input gpkg|0|gpkg 17 | ##QgsProcessingParameterFile|in_img|Input img|0|png|None|False|PNG Files (*.png);; JPG Files (*.jpg *.jpeg) 18 | 19 | ##QgsProcessingParameterVectorDestination|param_vector_dest|Vector destination 20 | ##QgsProcessingParameterVectorDestination|param_vector_point_dest|Vector destination point|0 21 | ##QgsProcessingParameterVectorDestination|param_vector_line_dest|Vector destination line|1 22 | ##QgsProcessingParameterVectorDestination|param_vector_polygon_dest|Vector destination polygon|2 23 | ##QgsProcessingParameterVectorDestination|param_table_dest|Vector destination table|5 24 | ##QgsProcessingParameterRasterDestination|param_raster_dest|Raster destination 25 | 26 | ##QgsProcessingParameterFolderDestination|param_folder_dest|Folder destination 27 | ##QgsProcessingParameterFileDestination|param_file_dest|File destination 28 | ##QgsProcessingParameterFileDestination|param_html_dest|HTML File destination|HTML Files (*.html) 29 | ##QgsProcessingParameterFileDestination|param_csv_dest|CSV File destination|CSV Files (*.csv) 30 | ##QgsProcessingParameterFileDestination|param_img_dest|Img File destination|PNG Files (*.png);; JPG Files (*.jpg *.jpeg) 31 | 32 | ##out_vector=output vector noprompt 33 | ##out_table=output table noprompt 34 | ##out_raster=output raster noprompt 35 | ##out_number=output number 36 | ##out_string=output string 37 | ##out_layer=output layer 38 | ##out_folder=output folder noprompt 39 | ##out_html=output html noprompt 40 | ##out_file=output file noprompt 41 | ##out_csv=output file csv noprompt 42 | 43 | library(sp) 44 | i <- 1 45 | 46 | -------------------------------------------------------------------------------- /tests/scripts/test_algorithm_inline_help.rsx: -------------------------------------------------------------------------------- 1 | #' ALG_DESC: Test help. 2 | #' ALG_CREATOR": Me 3 | #' polyg: A polygon layer description 4 | #' : from multi-lines 5 | #' RPLOTS: Plot output 6 | #' ALG_HELP_CREATOR: Me2} 7 | ##output_plots_to_html 8 | ##pass_filenames 9 | ##polyg=vector 10 | ##polyg_category_id=field polyg 11 | ##table_category_id=field sizes_table 12 | ##sampling_size=field sizes_table 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/scripts/test_dont_load_any_packages.rsx: -------------------------------------------------------------------------------- 1 | ##dont_load_any_packages 2 | -------------------------------------------------------------------------------- /tests/scripts/test_enum_multiple.rsx: -------------------------------------------------------------------------------- 1 | ##Test enums type multiple=name 2 | ##enum_string=enum literal multiple enum_a;enum_b;enum_c 3 | ##enum_string_optional=optional enum literal multiple enum_a;enum_b;enum_c 4 | ##enum_normal=enum multiple a;b;c 5 | ##enum_normal_optional=optional enum multiple a;b;c 6 | 7 | >print(enum_string) 8 | >print(enum_normal) -------------------------------------------------------------------------------- /tests/scripts/test_enums.rsx: -------------------------------------------------------------------------------- 1 | ##Test enums type=name 2 | ##enum_string=enum literal enum_a;enum_b;enum_c 3 | ##enum_normal=enum a;b;c -------------------------------------------------------------------------------- /tests/scripts/test_field_multiple.rsx: -------------------------------------------------------------------------------- 1 | ##Layer=vector 2 | ##MultiField=Field numeric multiple Layer 3 | -------------------------------------------------------------------------------- /tests/scripts/test_field_names.rsx: -------------------------------------------------------------------------------- 1 | ##test long fieldnames=name 2 | ##my group=group 3 | ##Layer=vector 4 | ##fieldname=output string 5 | fieldname <- fieldnames[length(fieldnames)] 6 | -------------------------------------------------------------------------------- /tests/scripts/test_input_color.rsx: -------------------------------------------------------------------------------- 1 | ##Test color input=name 2 | ##color=color #ffbb66 3 | -------------------------------------------------------------------------------- /tests/scripts/test_input_datetime.rsx: -------------------------------------------------------------------------------- 1 | ##Test datetime input=name 2 | ##datetime=datetime 3 | -------------------------------------------------------------------------------- /tests/scripts/test_input_expression.rsx: -------------------------------------------------------------------------------- 1 | ##Expressions=name 2 | ##number=expression 1+2+3 3 | ##qgis_version=expression @qgis_version_no 4 | ##geometry=expression make_circle( make_point(0, 0), 6) 5 | ##date_a=expression make_date(2020,5,4) 6 | ##time_a=expression make_time(13,45,30.5) 7 | ##array=expression array(2, 10, 'a', make_date(2020,5,4), make_time(13,45,30), datetime_from_epoch(1336128600000)) -------------------------------------------------------------------------------- /tests/scripts/test_input_point.rsx: -------------------------------------------------------------------------------- 1 | ##testpointinput=name 2 | ##Test point input=display_name 3 | ##point=point 4 | -------------------------------------------------------------------------------- /tests/scripts/test_input_range.rsx: -------------------------------------------------------------------------------- 1 | ##Test range input=name 2 | ##range=range 0,1 3 | -------------------------------------------------------------------------------- /tests/scripts/test_library_with_option.rsx: -------------------------------------------------------------------------------- 1 | ##dont_load_any_packages 2 | library(Matrix) 3 | library(MASS, quietly=True) -------------------------------------------------------------------------------- /tests/scripts/test_multiout.rsx: -------------------------------------------------------------------------------- 1 | ##Output=output vector 2 | ##OutputCSV=output table 3 | ##OutputFile=output file csv 4 | ##OutputNum=output number 5 | ##OutputStr=output string 6 | 7 | OutputNum <- 4.5 8 | OutputStr <- "value" 9 | 10 | data <- read.table(header=TRUE, text=' 11 | subject sex size 12 | 1 M 7 13 | 2 F NA 14 | 3 F 9 15 | 4 M 11 16 | ') 17 | 18 | write.csv2(data, outputFile, row.names = FALSE, na ="") -------------------------------------------------------------------------------- /tests/scripts/test_multirasterin.rsx: -------------------------------------------------------------------------------- 1 | ##Layer=multiple raster 2 | -------------------------------------------------------------------------------- /tests/scripts/test_multivectorin.rsx: -------------------------------------------------------------------------------- 1 | ##Layer=multiple vector 2 | -------------------------------------------------------------------------------- /tests/scripts/test_plots.rsx: -------------------------------------------------------------------------------- 1 | ##Basic statistics=group 2 | ##Graphs=name 3 | ##output_plots_to_html 4 | ##Layer=vector 5 | ##Field=Field Layer 6 | qqnorm(Layer[[Field]]) 7 | qqline(Layer[[Field]]) -------------------------------------------------------------------------------- /tests/scripts/test_raster_band.rsx: -------------------------------------------------------------------------------- 1 | ##Test raster band type=name 2 | ##Layer=raster 3 | ##Band=Band Layer -------------------------------------------------------------------------------- /tests/scripts/test_raster_in_out.rsx: -------------------------------------------------------------------------------- 1 | ##rasterinout=name 2 | ##Layer=raster 3 | ##out_raster=output raster 4 | out_raster = Layer -------------------------------------------------------------------------------- /tests/scripts/test_rasterin_names.rsx: -------------------------------------------------------------------------------- 1 | ##pass_filenames 2 | ##Layer=raster 3 | -------------------------------------------------------------------------------- /tests/scripts/test_vectorin.rsx: -------------------------------------------------------------------------------- 1 | ##Layer=vector 2 | ##Layer2=vector 3 | -------------------------------------------------------------------------------- /tests/scripts/test_vectorout.rsx: -------------------------------------------------------------------------------- 1 | ##Output=output vector 2 | ##OutputCSV=output table 3 | -------------------------------------------------------------------------------- /tests/test_algorithm.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import processing 4 | from utils import data_path, script_path 5 | 6 | from processing_r.processing.algorithm import RAlgorithm 7 | 8 | 9 | def test_can_run(): 10 | alg = RAlgorithm(description_file=script_path("test_algorithm_1.rsx")) 11 | alg.initAlgorithm() 12 | 13 | assert alg.canExecute() 14 | 15 | 16 | def test_run_point(): 17 | result = processing.run("r:testpointinput", {"point": "20.219926,49.138354 [EPSG:4326]"}) 18 | 19 | assert result == {} 20 | 21 | 22 | def test_run_raster_creation(): 23 | result = processing.run("r:rasterinout", {"Layer": data_path("dem.tif"), "out_raster": "TEMPORARY_OUTPUT"}) 24 | 25 | assert "out_raster" in result.keys() 26 | assert Path(result["out_raster"]).exists() 27 | 28 | 29 | def test_run_enums(): 30 | result = processing.run( 31 | "r:testenumstypemultiple", {"enum_normal": 0, "enum_string": 1, "R_CONSOLE_OUTPUT": "TEMPORARY_OUTPUT"} 32 | ) 33 | 34 | print(result.keys()) 35 | assert "R_CONSOLE_OUTPUT" in result.keys() 36 | assert Path(result["R_CONSOLE_OUTPUT"]).exists() 37 | 38 | 39 | def test_run_graphs(): 40 | result = processing.run( 41 | "r:graphs", 42 | { 43 | "RPLOTS": "TEMPORARY_OUTPUT", 44 | "Layer": data_path("points.gml"), 45 | "Field": "id", 46 | }, 47 | ) 48 | 49 | assert "RPLOTS" in result.keys() 50 | assert Path(result["RPLOTS"]).exists() 51 | -------------------------------------------------------------------------------- /tests/test_algorithm_inputs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from qgis.core import ( 3 | QgsCoordinateReferenceSystem, 4 | QgsProcessing, 5 | QgsProcessingContext, 6 | QgsProcessingFeedback, 7 | QgsVectorLayer, 8 | ) 9 | from qgis.PyQt.QtCore import QDate, QDateTime, QTime 10 | from qgis.PyQt.QtGui import QColor 11 | 12 | from processing_r.processing.algorithm import RAlgorithm 13 | from tests.utils import IS_API_BELOW_31000, IS_API_BELOW_31400, USE_API_30900, data_path, script_path 14 | 15 | 16 | def test_simple_inputs(): 17 | """ 18 | Test creation of script with algorithm inputs 19 | """ 20 | alg = RAlgorithm(description_file=script_path("test_algorithm_2.rsx")) 21 | alg.initAlgorithm() 22 | 23 | context = QgsProcessingContext() 24 | feedback = QgsProcessingFeedback() 25 | 26 | # enum evaluation 27 | script = alg.build_import_commands({"in_enum": 0}, context, feedback) 28 | assert "in_enum <- 0" in script 29 | 30 | # boolean evaluation 31 | script = alg.build_import_commands({"in_bool": True}, context, feedback) 32 | assert "in_bool <- TRUE" in script 33 | 34 | script = alg.build_import_commands({"in_bool": False}, context, feedback) 35 | assert "in_bool <- FALSE" in script 36 | 37 | # number evaluation 38 | script = alg.build_import_commands({"in_number": None}, context, feedback) 39 | assert "in_number <- NULL" in script 40 | 41 | script = alg.build_import_commands({"in_number": 5}, context, feedback) 42 | assert "in_number <- 5.0" in script 43 | 44 | script = alg.build_import_commands({"in_number": 5.5}, context, feedback) 45 | assert "in_number <- 5.5" in script 46 | 47 | script = alg.build_import_commands({"in_string": "some string"}, context, feedback) 48 | assert 'in_string <- "some string"' in script 49 | 50 | 51 | def test_folder_inputs(): 52 | """ 53 | Test creation of script with algorithm inputs 54 | """ 55 | alg = RAlgorithm(description_file=script_path("test_algorithm_2.rsx")) 56 | alg.initAlgorithm() 57 | 58 | context = QgsProcessingContext() 59 | feedback = QgsProcessingFeedback() 60 | 61 | # folder destination 62 | script = alg.build_import_commands({"param_folder_dest": "/tmp/processing/test_algorithm_2_r/"}, context, feedback) 63 | 64 | # file destination 65 | script = alg.build_import_commands( 66 | {"param_html_dest": "/tmp/processing/test_algorithm_2_r/dest.html"}, context, feedback 67 | ) 68 | assert 'param_html_dest <- "/tmp/processing/test_algorithm_2_r/dest.html"' in script 69 | 70 | script = alg.build_import_commands( 71 | {"param_file_dest": "/tmp/processing/test_algorithm_2_r/dest.file"}, context, feedback 72 | ) 73 | assert 'param_file_dest <- "/tmp/processing/test_algorithm_2_r/dest.file"' in script 74 | 75 | script = alg.build_import_commands( 76 | {"param_csv_dest": "/tmp/processing/test_algorithm_2_r/dest.csv"}, context, feedback 77 | ) 78 | assert 'param_csv_dest <- "/tmp/processing/test_algorithm_2_r/dest.csv"' in script 79 | 80 | 81 | def test_read_sf(): 82 | """ 83 | Test reading vector inputs 84 | """ 85 | alg = RAlgorithm(description_file=script_path("test_vectorin.rsx")) 86 | alg.initAlgorithm() 87 | 88 | context = QgsProcessingContext() 89 | feedback = QgsProcessingFeedback() 90 | script = alg.build_import_commands({"Layer": data_path("lines.shp")}, context, feedback) 91 | 92 | if USE_API_30900: 93 | assert script[0] == 'Layer <- st_read("{}", quiet = TRUE, stringsAsFactors = FALSE)'.format( 94 | data_path("lines.shp") 95 | ) 96 | else: 97 | assert script[0] == 'Layer <- st_read("{}", quiet = TRUE, stringsAsFactors = FALSE)'.format( 98 | data_path("lines.shp") 99 | ) 100 | 101 | script = alg.build_import_commands({"Layer": data_path("lines.shp").replace("/", "\\")}, context, feedback) 102 | if USE_API_30900: 103 | assert script[0] == 'Layer <- st_read("{}", quiet = TRUE, stringsAsFactors = FALSE)'.format( 104 | data_path("lines.shp") 105 | ) 106 | else: 107 | assert script[0] == 'Layer <- st_read("{}", quiet = TRUE, stringsAsFactors = FALSE)'.format( 108 | data_path("lines.shp") 109 | ) 110 | 111 | vl = QgsVectorLayer(data_path("test_gpkg.gpkg") + "|layername=points") 112 | assert vl.isValid() 113 | 114 | vl2 = QgsVectorLayer(data_path("test_gpkg.gpkg") + "|layername=lines") 115 | assert vl2.isValid() 116 | 117 | script = alg.build_import_commands({"Layer": vl, "Layer2": vl2}, context, feedback) 118 | 119 | if USE_API_30900: 120 | # use the newer api and avoid unnecessary layer translation 121 | assert script == [ 122 | 'Layer <- st_read("{}", layer = "points", quiet = TRUE, stringsAsFactors = FALSE)'.format( 123 | data_path("test_gpkg.gpkg") 124 | ), 125 | 'Layer2 <- st_read("{}", layer = "lines", quiet = TRUE, stringsAsFactors = FALSE)'.format( 126 | data_path("test_gpkg.gpkg") 127 | ), 128 | ] 129 | else: 130 | # older version, forced to use inefficient api 131 | assert 'Layer <- st_read("/tmp' == script[0] 132 | assert 'Layer2 <- st_read("/tmp' == script[1] 133 | 134 | 135 | def test_read_raster(): 136 | """ 137 | Test reading raster inputs 138 | """ 139 | alg = RAlgorithm(description_file=script_path("test_raster_in_out.rsx")) 140 | alg.initAlgorithm() 141 | 142 | context = QgsProcessingContext() 143 | feedback = QgsProcessingFeedback() 144 | 145 | script = alg.build_import_commands({"Layer": data_path("dem.tif")}, context, feedback) 146 | assert 'Layer <- brick("{}")'.format(data_path("dem.tif")) in script 147 | 148 | script = alg.build_import_commands({"Layer": data_path("dem.tif").replace("/", "\\")}, context, feedback) 149 | assert 'Layer <- brick("{}")'.format(data_path("dem.tif")) in script 150 | 151 | script = alg.build_import_commands({"Layer": None}, context, feedback) 152 | assert "Layer <- NULL" in script 153 | 154 | alg = RAlgorithm(description_file=script_path("test_rasterin_names.rsx")) 155 | alg.initAlgorithm() 156 | 157 | script = alg.build_import_commands({"Layer": data_path("dem.tif")}, context, feedback) 158 | assert 'Layer <- "{}"'.format(data_path("dem.tif")) in script 159 | 160 | script = alg.build_import_commands({"Layer": None}, context, feedback) 161 | assert "Layer <- NULL" in script 162 | 163 | 164 | def test_read_multi_raster(): 165 | """ 166 | Test raster multilayer input parameter 167 | """ 168 | alg = RAlgorithm(description_file=script_path("test_multirasterin.rsx")) 169 | alg.initAlgorithm() 170 | 171 | raster_param = alg.parameterDefinition("Layer") 172 | assert raster_param.type() == "multilayer" 173 | assert raster_param.layerType() == QgsProcessing.TypeRaster 174 | 175 | context = QgsProcessingContext() 176 | feedback = QgsProcessingFeedback() 177 | script = alg.build_import_commands( 178 | {"Layer": [data_path("dem.tif"), data_path("dem2.tif")]}, 179 | context, 180 | feedback, 181 | ) 182 | 183 | assert script == [ 184 | 'tempvar0 <- brick("{}")'.format(data_path("dem.tif")), 185 | 'tempvar1 <- brick("{}")'.format(data_path("dem2.tif")), 186 | "Layer = list(tempvar0,tempvar1)", 187 | ] 188 | 189 | script = alg.build_import_commands({"Layer": []}, context, feedback) 190 | assert script == ["Layer = list()"] 191 | 192 | 193 | def test_read_multi_vector(): 194 | """ 195 | Test vector multilayer input parameter 196 | """ 197 | alg = RAlgorithm(description_file=script_path("test_multivectorin.rsx")) 198 | alg.initAlgorithm() 199 | 200 | param = alg.parameterDefinition("Layer") 201 | assert param.type() == "multilayer" 202 | assert param.layerType() == QgsProcessing.TypeVectorAnyGeometry 203 | 204 | context = QgsProcessingContext() 205 | feedback = QgsProcessingFeedback() 206 | 207 | script = alg.build_import_commands( 208 | {"Layer": [data_path("lines.shp"), data_path("points.gml")]}, 209 | context, 210 | feedback, 211 | ) 212 | 213 | assert script == [ 214 | 'tempvar0 <- st_read("{}", quiet = TRUE, stringsAsFactors = FALSE)'.format(data_path("lines.shp")), 215 | 'tempvar1 <- st_read("{}", quiet = TRUE, stringsAsFactors = FALSE)'.format(data_path("points.gml")), 216 | "Layer = list(tempvar0,tempvar1)", 217 | ] 218 | 219 | script = alg.build_import_commands({"Layer": []}, context, feedback) 220 | assert script == ["Layer = list()"] 221 | 222 | 223 | def test_field(): 224 | alg = RAlgorithm(description_file=script_path("test_algorithm_2.rsx")) 225 | alg.initAlgorithm() 226 | 227 | param = alg.parameterDefinition("in_field") 228 | assert param.type() == "field" 229 | assert param.allowMultiple() is False 230 | 231 | context = QgsProcessingContext() 232 | feedback = QgsProcessingFeedback() 233 | 234 | script = alg.build_import_commands({"in_vector": data_path("lines.shp"), "in_field": "a"}, context, feedback) 235 | assert 'in_field <- "a"' in script 236 | 237 | 238 | def test_multi_field(): 239 | """ 240 | Test multiple field input parameter 241 | """ 242 | alg = RAlgorithm(description_file=script_path("test_field_multiple.rsx")) 243 | alg.initAlgorithm() 244 | 245 | param = alg.parameterDefinition("MultiField") 246 | assert param.type() == "field" 247 | assert param.allowMultiple() is True 248 | 249 | context = QgsProcessingContext() 250 | feedback = QgsProcessingFeedback() 251 | 252 | script = alg.build_import_commands({"Layer": data_path("lines.shp")}, context, feedback) 253 | 254 | assert script == [ 255 | 'Layer <- st_read("{}", quiet = TRUE, stringsAsFactors = FALSE)'.format(data_path("lines.shp")), 256 | "MultiField <- NULL", 257 | ] 258 | 259 | script = alg.build_import_commands({"Layer": data_path("lines.shp"), "MultiField": ["a"]}, context, feedback) 260 | assert script == [ 261 | 'Layer <- st_read("{}", quiet = TRUE, stringsAsFactors = FALSE)'.format(data_path("lines.shp")), 262 | 'MultiField <- c("a")', 263 | ] 264 | 265 | script = alg.build_import_commands({"Layer": data_path("lines.shp"), "MultiField": ["a", 'b"c']}, context, feedback) 266 | assert script == [ 267 | 'Layer <- st_read("{}", quiet = TRUE, stringsAsFactors = FALSE)'.format(data_path("lines.shp")), 268 | 'MultiField <- c("a","b\\"c")', 269 | ] 270 | 271 | 272 | def test_enums(): 273 | """ 274 | Test for both enum types 275 | """ 276 | alg = RAlgorithm(description_file=script_path("test_enums.rsx")) 277 | alg.initAlgorithm() 278 | 279 | context = QgsProcessingContext() 280 | feedback = QgsProcessingFeedback() 281 | 282 | script = alg.build_import_commands({"enum_normal": 0}, context, feedback) 283 | assert "enum_normal <- 0" == script[1] 284 | 285 | script = alg.build_import_commands({"enum_string": 0}, context, feedback) 286 | assert 'enum_string <- "enum_a"' == script[0] 287 | 288 | 289 | def test_enums_multiple(): 290 | """ 291 | Test for both enum multiple types 292 | """ 293 | alg = RAlgorithm(description_file=script_path("test_enum_multiple.rsx")) 294 | alg.initAlgorithm() 295 | 296 | context = QgsProcessingContext() 297 | feedback = QgsProcessingFeedback() 298 | 299 | params = {"enum_normal": [0, 1], "enum_string": [0, 1], "R_CONSOLE_OUTPUT": "TEMPORARY_OUTPUT"} 300 | script = alg.build_import_commands(params, context, feedback) 301 | 302 | assert "enum_normal <- c(0, 1)" in script 303 | assert 'enum_string <- c("enum_a","enum_b")' in script 304 | assert "enum_string_optional <- NULL" in script 305 | assert "enum_normal_optional <- NULL" in script 306 | 307 | params = { 308 | "enum_normal": [0, 1, 2], 309 | "enum_string": [0, 2], 310 | "enum_string_optional": [0], 311 | "enum_normal_optional": [1], 312 | "R_CONSOLE_OUTPUT": "TEMPORARY_OUTPUT", 313 | } 314 | script = alg.build_import_commands(params, context, feedback) 315 | 316 | assert "enum_normal <- c(0, 1, 2)" in script 317 | assert 'enum_string <- c("enum_a","enum_c")' in script 318 | assert 'enum_string_optional <- c("enum_a")' in script 319 | assert "enum_normal_optional <- c(1)" in script 320 | 321 | 322 | def test_point(): 323 | """ 324 | Test Point parameter 325 | """ 326 | 327 | context = QgsProcessingContext() 328 | feedback = QgsProcessingFeedback() 329 | 330 | alg = RAlgorithm(description_file=script_path("test_input_point.rsx")) 331 | alg.initAlgorithm() 332 | 333 | script = alg.build_r_script({"point": "20.219926,49.138354 [EPSG:4326]"}, context, feedback) 334 | 335 | assert 'library("sf")' in script 336 | assert "point <- st_sfc(st_point(c(20.219926,49.138354)), crs = point_crs)" in script 337 | 338 | 339 | def test_range(): 340 | """ 341 | Test range parameter 342 | """ 343 | 344 | context = QgsProcessingContext() 345 | feedback = QgsProcessingFeedback() 346 | 347 | alg = RAlgorithm(description_file=script_path("test_input_range.rsx")) 348 | alg.initAlgorithm() 349 | 350 | script = alg.build_r_script({"range": [0, 1]}, context, feedback) 351 | assert "range <- c(min = 0.0, max = 1.0)" in script 352 | 353 | script = alg.build_r_script({"range": [5, 10]}, context, feedback) 354 | assert "range <- c(min = 5.0, max = 10.0)" in script 355 | 356 | script = alg.build_r_script({"range": [0.5, 1.5]}, context, feedback) 357 | assert "range <- c(min = 0.5, max = 1.5)" in script 358 | 359 | 360 | def test_color(): 361 | """ 362 | Test color parameter 363 | """ 364 | 365 | if IS_API_BELOW_31000: 366 | pytest.skip("QGIS version does not support this.") 367 | 368 | context = QgsProcessingContext() 369 | feedback = QgsProcessingFeedback() 370 | 371 | alg = RAlgorithm(description_file=script_path("test_input_color.rsx")) 372 | alg.initAlgorithm() 373 | 374 | script = alg.build_r_script({"color": QColor(0, 255, 0)}, context, feedback) 375 | assert "color <- rgb(0, 255, 0, 255, maxColorValue = 255)" in script 376 | 377 | script = alg.build_r_script({"color": QColor(255, 0, 0)}, context, feedback) 378 | assert "color <- rgb(255, 0, 0, 255, maxColorValue = 255)" in script 379 | 380 | 381 | def test_date_time(): 382 | """ 383 | Test datetime parameter 384 | """ 385 | 386 | if IS_API_BELOW_31400: 387 | pytest.skip("QGIS version does not support this.") 388 | 389 | context = QgsProcessingContext() 390 | feedback = QgsProcessingFeedback() 391 | 392 | alg = RAlgorithm(description_file=script_path("test_input_datetime.rsx")) 393 | alg.initAlgorithm() 394 | 395 | script = alg.build_r_script({"datetime": QDateTime(QDate(2021, 10, 1), QTime(16, 57, 0))}, context, feedback) 396 | assert 'datetime <- as.POSIXct("2021-10-01T16:57:00", format = "%Y-%m-%dT%H:%M:%S")' in script 397 | 398 | script = alg.build_r_script({"datetime": QDateTime(QDate(2021, 10, 14), QTime(14, 48, 52))}, context, feedback) 399 | assert 'datetime <- as.POSIXct("2021-10-14T14:48:52", format = "%Y-%m-%dT%H:%M:%S")' in script 400 | 401 | 402 | def test_expressions(): 403 | """ 404 | Test Expression parameter 405 | """ 406 | 407 | context = QgsProcessingContext() 408 | feedback = QgsProcessingFeedback() 409 | 410 | alg = RAlgorithm(description_file=script_path("test_input_expression.rsx")) 411 | alg.initAlgorithm() 412 | 413 | script = alg.build_r_script({""}, context, feedback) 414 | 415 | assert "number <- 6" in script 416 | assert any(['geometry <- sf::st_as_sfc("Polygon ' in line for line in script]) # pylint: disable=use-a-generator 417 | 418 | assert 'date_a <- as.POSIXct("2020-05-04", format = "%Y-%m-%d")' in script 419 | assert 'time_a <- lubridate::hms("13:45:30")' in script 420 | 421 | 422 | def test_raster_band(): 423 | """ 424 | Test datetime parameter 425 | """ 426 | 427 | alg = RAlgorithm(description_file=script_path("test_raster_band.rsx")) 428 | alg.initAlgorithm() 429 | 430 | context = QgsProcessingContext() 431 | feedback = QgsProcessingFeedback() 432 | 433 | script = alg.build_import_commands({"Band": 1, "Layer": data_path("dem.tif")}, context, feedback) 434 | assert "Band <- 1" in script 435 | 436 | 437 | def test_plot_outputs(): 438 | """ 439 | Test plot outputs 440 | """ 441 | 442 | alg = RAlgorithm(description_file=script_path("test_algorithm_1.rsx")) 443 | alg.initAlgorithm() 444 | 445 | context = QgsProcessingContext() 446 | feedback = QgsProcessingFeedback() 447 | 448 | script = alg.build_import_commands({"RPLOTS": "/tmp/plots"}, context, feedback) 449 | assert any(["png(" in x for x in script]) 450 | 451 | 452 | def test_crs(): 453 | alg = RAlgorithm(description_file=script_path("test_algorithm_2.rsx")) 454 | alg.initAlgorithm() 455 | 456 | context = QgsProcessingContext() 457 | feedback = QgsProcessingFeedback() 458 | 459 | script = alg.build_import_commands({"in_crs": QgsCoordinateReferenceSystem("EPSG:4326")}, context, feedback) 460 | assert 'in_crs <- "EPSG:4326"' in script 461 | 462 | # invalid CRS 463 | script = alg.build_import_commands({"in_crs": QgsCoordinateReferenceSystem("user:4326")}, context, feedback) 464 | assert "in_crs <- NULL" in script 465 | 466 | 467 | def test_convert_memory_layer_to_gpkg(): 468 | """ 469 | Test reading vector inputs 470 | """ 471 | alg = RAlgorithm(description_file=script_path("test_field_names.rsx")) 472 | alg.initAlgorithm() 473 | 474 | context = QgsProcessingContext() 475 | feedback = QgsProcessingFeedback() 476 | 477 | uri = "point?crs=epsg:4326&field=very_long_fieldname_not_suitable_for_SHP:integer" 478 | layer = QgsVectorLayer(uri, "layer", "memory") 479 | 480 | script = alg.build_import_commands({"Layer": layer}, context, feedback) 481 | 482 | first_line = script[0] 483 | 484 | # test that default format is GPKG saved in tmp directory 485 | assert first_line.startswith('Layer <- st_read("/tmp') 486 | assert first_line.endswith('Layer.gpkg", quiet = TRUE, stringsAsFactors = FALSE)') 487 | -------------------------------------------------------------------------------- /tests/test_algorithm_metadata.py: -------------------------------------------------------------------------------- 1 | from qgis.core import QgsProcessingContext, QgsProcessingFeedback 2 | 3 | from processing_r.processing.algorithm import RAlgorithm 4 | from tests.utils import IS_API_ABOVE_31604, script_path 5 | 6 | 7 | def test_dont_load_any_packages(): 8 | """ 9 | Test dont_load_any_packages keyword 10 | """ 11 | alg = RAlgorithm(description_file=script_path("test_dont_load_any_packages.rsx")) 12 | alg.initAlgorithm() 13 | 14 | context = QgsProcessingContext() 15 | feedback = QgsProcessingFeedback() 16 | 17 | script = alg.build_r_script({}, context, feedback) 18 | 19 | assert not any([x == 'library("sf")' for x in script]) 20 | assert not any([x == 'library("raster")' for x in script]) 21 | assert not any(["library(" in x for x in script]) 22 | 23 | 24 | def test_library_with_options(): 25 | """ 26 | Test library with options 27 | """ 28 | 29 | alg = RAlgorithm(description_file=script_path("test_library_with_option.rsx")) 30 | alg.initAlgorithm() 31 | 32 | script = alg.r_templates.build_script_header_commands(alg.script) 33 | 34 | assert 'tryCatch(find.package("MASS"), error = function(e) install.packages("MASS", dependencies=TRUE))' in script 35 | 36 | assert ( 37 | 'tryCatch(find.package("Matrix"), error = function(e) install.packages("Matrix", dependencies=TRUE))' in script 38 | ) 39 | 40 | assert 'library("MASS", quietly=True)' in script 41 | assert 'library("Matrix")' in script 42 | 43 | 44 | def test_help(): # pylint: disable=too-many-locals,too-many-statements 45 | """ 46 | Test algorithm help 47 | """ 48 | alg = RAlgorithm(description_file=script_path("test_algorithm_1.rsx")) 49 | alg.initAlgorithm() 50 | 51 | assert "A polygon layer" in alg.shortHelpString() 52 | assert "Me2" in alg.shortHelpString() 53 | assert "Test help." in alg.shortHelpString() 54 | 55 | # param help 56 | if IS_API_ABOVE_31604: 57 | polyg_param = alg.parameterDefinition("polyg") 58 | assert polyg_param.help() == "A polygon layer" 59 | 60 | # no help file 61 | alg = RAlgorithm(description_file=script_path("test_algorithm_2.rsx")) 62 | alg.initAlgorithm() 63 | 64 | assert alg.shortHelpString() == "" 65 | 66 | alg = RAlgorithm(description_file=script_path("test_algorithm_inline_help.rsx")) 67 | alg.initAlgorithm() 68 | 69 | assert "A polygon layer" in alg.shortHelpString() 70 | assert "Me2" in alg.shortHelpString() 71 | assert "Test help." in alg.shortHelpString() 72 | 73 | # param help 74 | if IS_API_ABOVE_31604: 75 | polyg_param = alg.parameterDefinition("polyg") 76 | assert polyg_param.help() == "A polygon layer description from multi-lines" 77 | 78 | 79 | def test_unsupported_lines(): 80 | alg = RAlgorithm(None, script="##load_raster_using_rgdal") 81 | alg.initAlgorithm() 82 | assert "This command is no longer supported" in alg.error 83 | 84 | alg = RAlgorithm(None, script="##load_vector_using_rgdal") 85 | alg.initAlgorithm() 86 | assert "This command is no longer supported" in alg.error 87 | 88 | alg = RAlgorithm(None, script="##dontuserasterpackage") 89 | alg.initAlgorithm() 90 | assert "This command is no longer supported" in alg.error 91 | 92 | 93 | def test_github_install(): 94 | alg = RAlgorithm(None, script="##user/repository=github_install") 95 | alg.initAlgorithm() 96 | 97 | script = alg.r_templates.build_script_header_commands(alg.script) 98 | assert 'library("remotes")' in script 99 | assert 'remotes::install_github("user/repository")' in script 100 | -------------------------------------------------------------------------------- /tests/test_algorithm_outputs.py: -------------------------------------------------------------------------------- 1 | from qgis.core import ( 2 | QgsProcessing, 3 | QgsProcessingContext, 4 | QgsProcessingFeedback, 5 | QgsRasterLayer, 6 | QgsVectorLayer, 7 | ) 8 | 9 | from processing_r.processing.algorithm import RAlgorithm 10 | from tests.utils import USE_API_30900, data_path, script_path 11 | 12 | 13 | def test_vector_outputs(): 14 | """ 15 | Test writing vector outputs 16 | """ 17 | alg = RAlgorithm(description_file=script_path("test_vectorout.rsx")) 18 | alg.initAlgorithm() 19 | 20 | context = QgsProcessingContext() 21 | feedback = QgsProcessingFeedback() 22 | 23 | script = alg.build_export_commands( 24 | {"Output": "/home/test/lines.shp", "OutputCSV": "/home/test/tab.csv"}, context, feedback 25 | ) 26 | assert script == [ 27 | 'st_write(Output, "/home/test/lines.shp", layer = "lines", quiet = TRUE)', 28 | 'write.csv(OutputCSV, "/home/test/tab.csv", row.names = FALSE)', 29 | ] 30 | 31 | script = alg.build_export_commands( 32 | {"Output": "/home/test/lines.gpkg", "OutputCSV": "/home/test/tab.csv"}, context, feedback 33 | ) 34 | assert script == [ 35 | 'st_write(Output, "/home/test/lines.gpkg", layer = "lines", quiet = TRUE)', 36 | 'write.csv(OutputCSV, "/home/test/tab.csv", row.names = FALSE)', 37 | ] 38 | 39 | 40 | def test_multi_outputs(): 41 | """ 42 | Test writing vector outputs 43 | """ 44 | alg = RAlgorithm(description_file=script_path("test_multiout.rsx")) 45 | alg.initAlgorithm() 46 | 47 | context = QgsProcessingContext() 48 | feedback = QgsProcessingFeedback() 49 | 50 | script = alg.build_export_commands( 51 | {"Output": "/home/test/lines.shp", "OutputCSV": "/home/test/tab.csv", "OutputFile": "/home/test/file.csv"}, 52 | context, 53 | feedback, 54 | ) 55 | 56 | assert 'st_write(Output, "/home/test/lines.shp", layer = "lines", quiet = TRUE)' in script 57 | assert 'write.csv(OutputCSV, "/home/test/tab.csv", row.names = FALSE)' in script 58 | 59 | assert script[2].startswith('cat("##OutputFile", file=') 60 | assert script[3].startswith("cat(OutputFile, file=") 61 | assert script[4].startswith('cat("##OutputNum", file=') 62 | assert script[5].startswith("cat(OutputNum, file=") 63 | assert script[6].startswith('cat("##OutputStr", file=') 64 | assert script[7].startswith("cat(OutputStr, file=") 65 | 66 | 67 | def test_raster_output(): 68 | """ 69 | Test writing raster outputs 70 | """ 71 | alg = RAlgorithm(description_file=script_path("test_raster_in_out.rsx")) 72 | alg.initAlgorithm() 73 | 74 | context = QgsProcessingContext() 75 | feedback = QgsProcessingFeedback() 76 | 77 | script = alg.build_export_commands( 78 | {"Layer": data_path("dem.tif"), "out_raster": "/tmp/raster.tif"}, 79 | context, 80 | feedback, 81 | ) 82 | 83 | assert 'writeRaster(out_raster, "/tmp/raster.tif", overwrite = TRUE)' in script 84 | -------------------------------------------------------------------------------- /tests/test_algorithm_script_parsing.py: -------------------------------------------------------------------------------- 1 | from qgis.core import QgsProcessing, QgsProcessingParameterFile, QgsProcessingParameterNumber 2 | 3 | from processing_r.processing.algorithm import RAlgorithm 4 | from tests.utils import script_path 5 | 6 | 7 | def test_script_parsing_1(): 8 | """ 9 | Test script file parsing 10 | """ 11 | alg = RAlgorithm(description_file=script_path("test_algorithm_1.rsx")) 12 | alg.initAlgorithm() 13 | 14 | assert alg.error is None 15 | assert alg.name() == "test_algorithm_1" 16 | assert alg.displayName() == "test algorithm 1" 17 | assert "test_algorithm_1.rsx" in alg.description_file 18 | assert alg.show_plots is True 19 | assert alg.pass_file_names is True 20 | assert alg.show_console_output is False 21 | 22 | 23 | def test_script_parsing_2(): 24 | """ 25 | Test script file parsing 26 | """ 27 | alg = RAlgorithm(description_file=script_path("test_algorithm_2.rsx")) 28 | alg.initAlgorithm() 29 | assert alg.error is None 30 | assert alg.name() == "mytest" 31 | assert alg.displayName() == "my test" 32 | assert alg.group() == "my group" 33 | assert alg.groupId() == "my group" 34 | assert alg.show_plots is False 35 | assert alg.pass_file_names is False 36 | assert alg.show_console_output is False 37 | 38 | # test that inputs were created correctly 39 | raster_param = alg.parameterDefinition("in_raster") 40 | assert raster_param.type() == "raster" 41 | 42 | vector_param = alg.parameterDefinition("in_vector") 43 | assert vector_param.type() == "source" 44 | 45 | field_param = alg.parameterDefinition("in_field") 46 | assert field_param.type() == "field" 47 | assert field_param.parentLayerParameterName() == "in_vector" 48 | 49 | extent_param = alg.parameterDefinition("in_extent") 50 | assert extent_param.type() == "extent" 51 | 52 | string_param = alg.parameterDefinition("in_string") 53 | assert string_param.type() == "string" 54 | 55 | file_param = alg.parameterDefinition("in_file") 56 | assert file_param.type() == "file" 57 | 58 | number_param = alg.parameterDefinition("in_number") 59 | assert number_param.type() == "number" 60 | assert number_param.dataType() == QgsProcessingParameterNumber.Double 61 | 62 | enum_param = alg.parameterDefinition("in_enum") 63 | assert enum_param.type() == "enum" 64 | 65 | enum_param = alg.parameterDefinition("in_enum2") 66 | assert enum_param.type() == "enum" 67 | assert enum_param.options() == ["normal", "log10", "ln", "sqrt", "exp"] 68 | 69 | bool_param = alg.parameterDefinition("in_bool") 70 | assert bool_param.type() == "boolean" 71 | 72 | # outputs 73 | vector_output = alg.outputDefinition("out_vector") 74 | assert vector_output.type() == "outputVector" 75 | assert vector_output.dataType() == QgsProcessing.TypeVectorAnyGeometry 76 | 77 | vector_dest_param = alg.parameterDefinition("param_vector_dest") 78 | assert vector_dest_param.type() == "vectorDestination" 79 | assert vector_dest_param.dataType() == QgsProcessing.TypeVectorAnyGeometry 80 | 81 | table_output = alg.outputDefinition("out_table") 82 | assert table_output.type() == "outputVector" 83 | assert table_output.dataType() == QgsProcessing.TypeVector 84 | 85 | table_dest_param = alg.parameterDefinition("param_table_dest") 86 | assert table_dest_param.type() == "vectorDestination" 87 | assert table_dest_param.dataType() == QgsProcessing.TypeVector 88 | 89 | vector_dest_param = alg.parameterDefinition("param_vector_dest2") 90 | assert vector_dest_param.type() == "vectorDestination" 91 | assert vector_dest_param.dataType() == QgsProcessing.TypeVectorAnyGeometry 92 | 93 | vector_dest_param = alg.parameterDefinition("param_vector_point_dest") 94 | assert vector_dest_param.type() == "vectorDestination" 95 | assert vector_dest_param.dataType() == QgsProcessing.TypeVectorPoint 96 | 97 | vector_dest_param = alg.parameterDefinition("param_vector_line_dest") 98 | assert vector_dest_param.type() == "vectorDestination" 99 | assert vector_dest_param.dataType() == QgsProcessing.TypeVectorLine 100 | 101 | vector_dest_param = alg.parameterDefinition("param_vector_polygon_dest") 102 | assert vector_dest_param.type() == "vectorDestination" 103 | assert vector_dest_param.dataType() == QgsProcessing.TypeVectorPolygon 104 | 105 | raster_output = alg.outputDefinition("out_raster") 106 | assert raster_output.type() == "outputRaster" 107 | 108 | raster_dest_param = alg.parameterDefinition("param_raster_dest") 109 | assert raster_dest_param.type() == "rasterDestination" 110 | 111 | number_output = alg.outputDefinition("out_number") 112 | assert number_output.type() == "outputNumber" 113 | 114 | string_output = alg.outputDefinition("out_string") 115 | assert string_output.type() == "outputString" 116 | 117 | layer_output = alg.outputDefinition("out_layer") 118 | assert layer_output.type() == "outputLayer" 119 | 120 | folder_output = alg.outputDefinition("out_folder") 121 | assert folder_output.type() == "outputFolder" 122 | 123 | folder_dest_param = alg.parameterDefinition("param_folder_dest") 124 | assert folder_dest_param.type() == "folderDestination" 125 | 126 | html_output = alg.outputDefinition("out_html") 127 | assert html_output.type() == "outputHtml" 128 | 129 | html_dest_param = alg.parameterDefinition("param_html_dest") 130 | assert html_dest_param.type() == "fileDestination" 131 | 132 | file_output = alg.outputDefinition("out_file") 133 | assert file_output.type() == "outputFile" 134 | 135 | file_dest_param = alg.parameterDefinition("param_file_dest") 136 | assert file_dest_param.type() == "fileDestination" 137 | 138 | csv_output = alg.outputDefinition("out_csv") 139 | assert csv_output.type() == "outputFile" 140 | 141 | csv_dest_param = alg.parameterDefinition("param_csv_dest") 142 | assert csv_dest_param.type() == "fileDestination" 143 | assert csv_dest_param.defaultFileExtension() == "csv" 144 | 145 | 146 | def test_script_parsing_3(): 147 | """ 148 | Test script file parsing 149 | """ 150 | # test display_name 151 | alg = RAlgorithm(description_file=script_path("test_algorithm_3.rsx")) 152 | alg.initAlgorithm() 153 | 154 | assert alg.error is None 155 | assert alg.name() == "thealgid" 156 | assert alg.displayName() == "the algo title" 157 | assert alg.group() == "my group" 158 | assert alg.groupId() == "my group" 159 | 160 | 161 | def test_script_parsing_4(): 162 | """ 163 | Test script file parsing 164 | """ 165 | # test that inputs are defined as parameter description 166 | alg = RAlgorithm(description_file=script_path("test_algorithm_4.rsx")) 167 | alg.initAlgorithm() 168 | 169 | assert alg.error is None 170 | assert alg.name() == "mytest" 171 | assert alg.displayName() == "my test" 172 | assert alg.group() == "my group" 173 | assert alg.groupId() == "my group" 174 | 175 | raster_param = alg.parameterDefinition("in_raster") 176 | assert raster_param.type() == "raster" 177 | 178 | vector_param = alg.parameterDefinition("in_vector") 179 | assert vector_param.type() == "source" 180 | 181 | field_param = alg.parameterDefinition("in_field") 182 | assert field_param.type() == "field" 183 | assert field_param.parentLayerParameterName() == "in_vector" 184 | 185 | extent_param = alg.parameterDefinition("in_extent") 186 | assert extent_param.type() == "extent" 187 | 188 | crs_param = alg.parameterDefinition("in_crs") 189 | assert crs_param.type() == "crs" 190 | 191 | string_param = alg.parameterDefinition("in_string") 192 | assert string_param.type() == "string" 193 | 194 | number_param = alg.parameterDefinition("in_number") 195 | assert number_param.type() == "number" 196 | assert number_param.dataType() == QgsProcessingParameterNumber.Integer 197 | 198 | enum_param = alg.parameterDefinition("in_enum") 199 | assert enum_param.type() == "enum" 200 | 201 | bool_param = alg.parameterDefinition("in_bool") 202 | assert bool_param.type() == "boolean" 203 | 204 | file_param = alg.parameterDefinition("in_file") 205 | assert file_param.type() == "file" 206 | assert file_param.behavior() == QgsProcessingParameterFile.File 207 | 208 | folder_param = alg.parameterDefinition("in_folder") 209 | assert folder_param.type() == "file" 210 | assert folder_param.behavior() == QgsProcessingParameterFile.Folder 211 | 212 | gpkg_param = alg.parameterDefinition("in_gpkg") 213 | assert gpkg_param.type() == "file" 214 | assert gpkg_param.behavior() == QgsProcessingParameterFile.File 215 | assert gpkg_param.extension() == "gpkg" 216 | 217 | img_param = alg.parameterDefinition("in_img") 218 | assert img_param.type() == "file" 219 | assert img_param.behavior() == QgsProcessingParameterFile.File 220 | assert img_param.extension() == "" 221 | assert img_param.fileFilter() == "PNG Files (*.png);; JPG Files (*.jpg *.jpeg)" 222 | 223 | vector_dest_param = alg.parameterDefinition("param_vector_dest") 224 | assert vector_dest_param.type() == "vectorDestination" 225 | assert vector_dest_param.dataType() == QgsProcessing.TypeVectorAnyGeometry 226 | 227 | vector_dest_param = alg.parameterDefinition("param_vector_point_dest") 228 | assert vector_dest_param.type() == "vectorDestination" 229 | assert vector_dest_param.dataType() == QgsProcessing.TypeVectorPoint 230 | 231 | vector_dest_param = alg.parameterDefinition("param_vector_line_dest") 232 | assert vector_dest_param.type() == "vectorDestination" 233 | assert vector_dest_param.dataType() == QgsProcessing.TypeVectorLine 234 | 235 | vector_dest_param = alg.parameterDefinition("param_vector_polygon_dest") 236 | assert vector_dest_param.type() == "vectorDestination" 237 | assert vector_dest_param.dataType() == QgsProcessing.TypeVectorPolygon 238 | 239 | table_dest_param = alg.parameterDefinition("param_table_dest") 240 | assert table_dest_param.type() == "vectorDestination" 241 | assert table_dest_param.dataType() == QgsProcessing.TypeVector 242 | 243 | raster_dest_param = alg.parameterDefinition("param_raster_dest") 244 | assert raster_dest_param.type() == "rasterDestination" 245 | 246 | folder_dest_param = alg.parameterDefinition("param_folder_dest") 247 | assert folder_dest_param.type() == "folderDestination" 248 | 249 | file_dest_param = alg.parameterDefinition("param_file_dest") 250 | assert file_dest_param.type() == "fileDestination" 251 | 252 | html_dest_param = alg.parameterDefinition("param_html_dest") 253 | assert html_dest_param.type() == "fileDestination" 254 | assert html_dest_param.fileFilter() == "HTML Files (*.html)" 255 | assert html_dest_param.defaultFileExtension() == "html" 256 | 257 | csv_dest_param = alg.parameterDefinition("param_csv_dest") 258 | assert csv_dest_param.type() == "fileDestination" 259 | assert csv_dest_param.fileFilter() == "CSV Files (*.csv)" 260 | assert csv_dest_param.defaultFileExtension() == "csv" 261 | 262 | img_dest_param = alg.parameterDefinition("param_img_dest") 263 | assert img_dest_param.type() == "fileDestination" 264 | assert img_dest_param.fileFilter() == "PNG Files (*.png);; JPG Files (*.jpg *.jpeg)" 265 | assert img_dest_param.defaultFileExtension() == "png" 266 | 267 | 268 | def test_bad_syntax(): 269 | """ 270 | Test a bad script 271 | """ 272 | alg = RAlgorithm(description_file=script_path("bad_algorithm.rsx")) 273 | alg.initAlgorithm() 274 | assert alg.name() == "bad_algorithm" 275 | assert alg.displayName() == "bad algorithm" 276 | assert alg.error == "This script has a syntax error.\nProblem with line: polyg=xvector" 277 | 278 | 279 | def test_console_output(): 280 | alg = RAlgorithm(description_file=script_path("test_enum_multiple.rsx")) 281 | alg.initAlgorithm() 282 | 283 | assert alg.show_console_output is True 284 | -------------------------------------------------------------------------------- /tests/test_gui_utils.py: -------------------------------------------------------------------------------- 1 | from processing_r.gui.gui_utils import GuiUtils 2 | 3 | 4 | def test_get_icon(): 5 | """ 6 | Tests get_icon 7 | """ 8 | assert GuiUtils.get_icon("providerR.svg").isNull() is False 9 | assert GuiUtils.get_icon("not_an_icon.svg").isNull() is True 10 | 11 | 12 | def test_get_icon_svg(): 13 | """ 14 | Tests get_icon svg path 15 | """ 16 | assert GuiUtils.get_icon_svg("providerR.svg") 17 | assert "providerR.svg" in GuiUtils.get_icon_svg("providerR.svg") 18 | assert GuiUtils.get_icon_svg("not_an_icon.svg") == "" 19 | -------------------------------------------------------------------------------- /tests/test_provider.py: -------------------------------------------------------------------------------- 1 | from processing_r.processing.provider import RAlgorithmProvider 2 | 3 | 4 | def test_provider(): 5 | provider = RAlgorithmProvider() 6 | 7 | assert provider.name() == "R" 8 | assert provider.id() == "r" 9 | assert provider.versionInfo() 10 | assert "QGIS R Provider version " in provider.versionInfo() 11 | -------------------------------------------------------------------------------- /tests/test_r_template.py: -------------------------------------------------------------------------------- 1 | from processing_r.processing.r_templates import RTemplates 2 | 3 | 4 | def test_github_install(): 5 | """ 6 | Test github install code generation. 7 | """ 8 | templates = RTemplates() 9 | templates.install_github = True 10 | templates.github_dependencies = "user_1/repo_1, user_2/repo_2" 11 | assert ( 12 | templates.install_package_github(templates.github_dependencies[0]) == 'remotes::install_github("user_1/repo_1")' 13 | ) 14 | 15 | assert ( 16 | templates.install_package_github(templates.github_dependencies[1]) == 'remotes::install_github("user_2/repo_2")' 17 | ) 18 | 19 | 20 | def test_string(): # pylint: disable=too-many-locals,too-many-statements 21 | """ 22 | Test string variable 23 | """ 24 | templates = RTemplates() 25 | assert templates.set_variable_string("var", "val") == 'var <- "val"' 26 | assert templates.set_variable_string("var", 'va"l') == 'var <- "va\\"l"' 27 | 28 | 29 | def test_string_list_variable(): # pylint: disable=too-many-locals,too-many-statements 30 | """ 31 | Test string list variable 32 | """ 33 | templates = RTemplates() 34 | assert templates.set_variable_string_list("var", []) == "var <- c()" 35 | assert templates.set_variable_string_list("var", ["aaaa"]) == 'var <- c("aaaa")' 36 | assert templates.set_variable_string_list("var", ["aaaa", 'va"l']) == 'var <- c("aaaa","va\\"l")' 37 | -------------------------------------------------------------------------------- /tests/test_r_utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from processing.core.ProcessingConfig import ProcessingConfig 4 | from qgis.PyQt.QtCore import QCoreApplication, QSettings 5 | 6 | from processing_r.processing.provider import RAlgorithmProvider 7 | from processing_r.processing.utils import RUtils 8 | 9 | 10 | def test_r_is_installed(): 11 | """ 12 | Test checking that R is installed 13 | """ 14 | assert RUtils.check_r_is_installed() is None 15 | 16 | ProcessingConfig.setSettingValue(RUtils.R_FOLDER, "/home") 17 | 18 | assert isinstance(RUtils.check_r_is_installed(), str) 19 | assert "R is not installed" in RUtils.check_r_is_installed() 20 | 21 | ProcessingConfig.setSettingValue(RUtils.R_FOLDER, None) 22 | assert RUtils.check_r_is_installed() is None 23 | 24 | 25 | def test_guess_r_binary_folder(): 26 | """ 27 | Test guessing the R binary folder -- not much to do here, all the logic is Windows specific 28 | """ 29 | assert RUtils.guess_r_binary_folder() == "" 30 | 31 | 32 | def test_r_binary_folder(): 33 | """ 34 | Test retrieving R binary folder 35 | """ 36 | assert RUtils.r_binary_folder() == "" 37 | 38 | ProcessingConfig.setSettingValue(RUtils.R_FOLDER, "/usr/local/bin") 39 | assert RUtils.r_binary_folder() == "/usr/local/bin" 40 | 41 | ProcessingConfig.setSettingValue(RUtils.R_FOLDER, None) 42 | assert RUtils.r_binary_folder() == "" 43 | 44 | 45 | def test_r_executable(): 46 | """ 47 | Test retrieving R executable 48 | """ 49 | assert RUtils.path_to_r_executable() == "R" 50 | assert RUtils.path_to_r_executable(script_executable=True) == "Rscript" 51 | 52 | ProcessingConfig.setSettingValue(RUtils.R_FOLDER, "/usr/local/bin") 53 | assert RUtils.path_to_r_executable() == "/usr/local/bin/R" 54 | assert RUtils.path_to_r_executable(script_executable=True) == "/usr/local/bin/Rscript" 55 | 56 | ProcessingConfig.setSettingValue(RUtils.R_FOLDER, None) 57 | assert RUtils.path_to_r_executable() == "R" 58 | assert RUtils.path_to_r_executable(script_executable=True) == "Rscript" 59 | 60 | 61 | def test_package_repo(): 62 | """ 63 | Test retrieving/setting the package repo 64 | """ 65 | assert RUtils.package_repo() == "http://cran.at.r-project.org/" 66 | 67 | ProcessingConfig.setSettingValue(RUtils.R_REPO, "http://mirror.at.r-project.org/") 68 | assert RUtils.package_repo() == "http://mirror.at.r-project.org/" 69 | 70 | ProcessingConfig.setSettingValue(RUtils.R_REPO, "http://cran.at.r-project.org/") 71 | assert RUtils.package_repo() == "http://cran.at.r-project.org/" 72 | 73 | 74 | def test_use_user_library(): 75 | """ 76 | Test retrieving/setting the user library setting 77 | """ 78 | assert RUtils.use_user_library() is True 79 | 80 | ProcessingConfig.setSettingValue(RUtils.R_USE_USER_LIB, False) 81 | assert RUtils.use_user_library() is False 82 | 83 | ProcessingConfig.setSettingValue(RUtils.R_USE_USER_LIB, True) 84 | assert RUtils.use_user_library() is True 85 | 86 | 87 | def test_library_folder(): 88 | """ 89 | Test retrieving/setting the library folder 90 | """ 91 | assert "/profiles/default/processing/rlibs" in RUtils.r_library_folder() 92 | 93 | ProcessingConfig.setSettingValue(RUtils.R_LIBS_USER, "/usr/local") 94 | assert RUtils.r_library_folder() == "/usr/local" 95 | 96 | ProcessingConfig.setSettingValue(RUtils.R_LIBS_USER, None) 97 | assert "/profiles/default/processing/rlibs" in RUtils.r_library_folder() 98 | 99 | 100 | def test_is_error_line(): 101 | """ 102 | Test is_error_line 103 | """ 104 | assert RUtils.is_error_line("xxx yyy") is False 105 | assert RUtils.is_error_line("Error something went wrong") is True 106 | assert RUtils.is_error_line("Execution halted") is True 107 | 108 | 109 | def test_is_valid_r_variable(): 110 | """ 111 | Test for strings to check if they are valid R variables. 112 | """ 113 | assert RUtils.is_valid_r_variable("var_name%") is False 114 | assert RUtils.is_valid_r_variable("2var_name") is False 115 | assert RUtils.is_valid_r_variable(".2var_name") is False 116 | assert RUtils.is_valid_r_variable("_var_name") is False 117 | 118 | assert RUtils.is_valid_r_variable("var_name2.") is True 119 | assert RUtils.is_valid_r_variable(".var_name") is True 120 | assert RUtils.is_valid_r_variable("var.name") is True 121 | 122 | 123 | def test_scripts_folders(): 124 | """ 125 | Test script folders 126 | """ 127 | assert RUtils.script_folders() 128 | assert RUtils.default_scripts_folder() in RUtils.script_folders() 129 | assert RUtils.builtin_scripts_folder() in RUtils.script_folders() 130 | 131 | 132 | def test_descriptive_name(): 133 | """ 134 | Tests creating descriptive name 135 | """ 136 | assert RUtils.create_descriptive_name("a B_4324_asd") == "a B 4324 asd" 137 | 138 | 139 | def test_strip_special_characters(): 140 | """ 141 | Tests stripping special characters from a name 142 | """ 143 | assert RUtils.strip_special_characters("aB 43 24a:sd") == "aB4324asd" 144 | 145 | 146 | def test_is_windows(): 147 | """ 148 | Test is_windows 149 | """ 150 | assert RUtils.is_windows() is False # suck it, Windows users! 151 | 152 | 153 | def test_is_macos(): 154 | """ 155 | Test is_macos 156 | """ 157 | assert RUtils.is_macos() is False # suck it even more, MacOS users! 158 | 159 | 160 | def test_built_in_path(): 161 | """ 162 | Tests built in scripts path 163 | """ 164 | assert RUtils.builtin_scripts_folder() 165 | assert "builtin_scripts" in RUtils.builtin_scripts_folder() 166 | assert Path(RUtils.builtin_scripts_folder()).exists() 167 | 168 | 169 | def test_default_scripts_folder(): 170 | """ 171 | Tests default user scripts folder 172 | """ 173 | assert RUtils.default_scripts_folder() 174 | assert "rscripts" in RUtils.default_scripts_folder() 175 | assert Path(RUtils.default_scripts_folder()).exists() 176 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from qgis.core import Qgis, QgsProcessingAlgorithm 4 | 5 | 6 | def script_path(filename: str) -> str: 7 | path = Path(__file__).parent / "scripts" / filename 8 | return path.as_posix() 9 | 10 | 11 | def data_path(filename: str) -> str: 12 | path = Path(__file__).parent / "data" / filename 13 | return path.as_posix() 14 | 15 | 16 | USE_API_30900 = Qgis.QGIS_VERSION_INT >= 30900 and hasattr( 17 | QgsProcessingAlgorithm, "parameterAsCompatibleSourceLayerPathAndLayerName" 18 | ) 19 | 20 | IS_API_BELOW_31000 = Qgis.QGIS_VERSION_INT < 31000 21 | 22 | IS_API_BELOW_31400 = Qgis.QGIS_VERSION_INT < 31400 23 | 24 | IS_API_ABOVE_31604 = Qgis.QGIS_VERSION_INT >= 31604 25 | -------------------------------------------------------------------------------- /website/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Processing R Provider 2 | site_url: https://north-road.github.io/qgis-processing-r/ 3 | site_description: Plugin for QGIS for processing using R language 4 | site_author: Jan Caha 5 | 6 | 7 | copyright: Copyright © 2019 North Road and Jan Caha. 8 | 9 | docs_dir: pages 10 | site_dir: docs 11 | 12 | lang: en 13 | 14 | theme: 15 | name: bootstrap 16 | 17 | nav: 18 | - Home: index.md 19 | - Syntax: 20 | - Script Syntax: script-syntax.md 21 | - Help Syntax: help-syntax.md 22 | - Examples: 23 | - Example with vector output: ex_vector_output.md 24 | - Example with raster output: ex_raster_output.md 25 | - Example with table output: ex_table_output.md 26 | - Example with file output: ex_file_output.md 27 | - Example with number and string outputs: ex_number_string_output.md 28 | - Example with tool console log: ex_console_output.md 29 | - Example of graphs output: ex_plot.md 30 | - Example of using QGIS expressions: ex_expression.md 31 | - Changelog: changelog.md 32 | - Github: https://github.com/north-road/qgis-processing-r 33 | 34 | plugins: 35 | - search -------------------------------------------------------------------------------- /website/pages/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - 4.0.0 Reflect retirement of `rgdal` R package, only support loading using `sf` and `raster` packages 4 | 5 | - 3.1.1 Capture correctly even errors from R starting with `Error:`, fix importing `QgsProcessingParameterColor` and `QgsProcessingParameterDateTime` for older QGIS versions (where it is not available) 6 | 7 | - 3.0.0 Added support for `QgsProcessingParameter*` strings, that can be used to define script parameters. Better handling of errors in R scripts. New script parameter `##script_title`, that can be used to define string under which the script is listed in QGIS Toolbox. 8 | 9 | - 2.3.0 Introduces support for QgsProcessingParameterPoint as an input variable, set parameter help strings under QGIS 3.16 or later 10 | 11 | - 2.2.2 Make sure that FolderDestination and FileDestination folders exist, otherwise that would have to be handled by R script 12 | causes issue if writing to temp directory which does not exist 13 | 14 | - 2.2.1 Fix file based outputs, R version number reporting 15 | 16 | - 2.2.0 Add support for file based outputs. 17 | 18 | - 2.1.0 Added support for literal enums, skipping package loading, additional output types (e.g. strings and numbers). 19 | 20 | - 2.0.0 Many fixes, algorithms have support for choice between sf/raster or rgdal for loading inputs 21 | 22 | - 1.0.7 Fix 3.4 compatibility 23 | 24 | - 1.0.6 Workaround API break in QGIS, which breaks existing scripts 25 | 26 | - 1.0.5 Allow use of R development versions 27 | 28 | - 1.0.4 Resurrect ability to run on selected features only 29 | 30 | - 1.0.3 Remove activation setting for provider (disable plugin instead!), fix memory layer handling, simpler example script 31 | 32 | - 1.0.2 Fix vector paths under windows -------------------------------------------------------------------------------- /website/pages/ex_console_output.md: -------------------------------------------------------------------------------- 1 | # Example with tool console log 2 | 3 | This script takes input vector data and one of its fields. The result is a tool console log with summary statistics for a given field of the layer. 4 | 5 | ## Script 6 | 7 | ``` 8 | ##Basic statistics=group 9 | ##Statistic summary console=name 10 | ##Layer=vector 11 | ##Field=Field Layer 12 | Summary_statistics <- data.frame(rbind( 13 | sum(Layer[[Field]]), 14 | length(Layer[[Field]]), 15 | length(unique(Layer[[Field]])), 16 | min(Layer[[Field]]), 17 | max(Layer[[Field]]), 18 | max(Layer[[Field]])-min(Layer[[Field]]), 19 | mean(Layer[[Field]]), 20 | median(Layer[[Field]]), 21 | sd(Layer[[Field]])), 22 | row.names=c("Sum:","Count:","Unique values:","Minimum value:","Maximum value:","Range:","Mean value:","Median value:","Standard deviation:")) 23 | colnames(Summary_statistics) <- c(Field) 24 | >Summary_statistics 25 | ``` 26 | 27 | ## Script lines description 28 | 29 | 1. **Basic statistics** is the group of the algorithm. 30 | 2. **Statistic summary console** is the name of the algorithm. 31 | 3. **Layer** is the input vector layer. 32 | 4. **Field** is the name of a field from _Layer_. 33 | 5. Create data.frame with calculated statistics and assign correct row names. 34 | 6. Give the data.frame correct colname, the name of the field. 35 | 7. Print the created data.frame to the tool console log. 36 | 37 | The output will be shown in the tool console log. 38 | 39 | ![](./images/console_output.jpg) 40 | -------------------------------------------------------------------------------- /website/pages/ex_expression.md: -------------------------------------------------------------------------------- 1 | # Example with QGIS expression as inputs 2 | 3 | This script takes several QGIS expression and prints their values from R. 4 | 5 | ## Script 6 | 7 | ``` 8 | ##Expression examples=group 9 | ##Expressions=name 10 | ##qgis_version_info=expression @qgis_version_no 11 | ##example_value=expression 1+2+3 12 | ##example_geometry=expression make_circle( make_point(0, 0), 10) 13 | ##example_date=expression make_date(2020,5,4) 14 | ##example_time=expression make_time(13,45,30.5) 15 | ##example_array=expression array(2, 10, 'a') 16 | >print(qgis_version_info) 17 | >print(example_value) 18 | >print(example_geometry) 19 | >print(example_date) 20 | >print(example_time) 21 | >print(example_array) 22 | ``` 23 | 24 | ## Script lines description 25 | 26 | 1. **Expression examples** is the group of the algorithm. 27 | 2. **Expressions** is the name of the algorithm. 28 | 3. **qgis_version_info** will be **string** after the expression is evaluated. 29 | 4. **example_value** will be number (**integer**) after the expression is evaluated. 30 | 5. **example_geometry** will be **sfg** (from **sf** package) after the expression is evaluated. 31 | 6. **example_date** will be **POSIXct** after the expression is evaluated. 32 | 7. **example_time** will be **Period** (from **lubridate** package) after the expression is evaluated. 33 | 8. **example_array** will be **list** of values after the expression is evaluated. 34 | 35 | The rest of the lines prints the outputs in R. 36 | 37 | ## Script output 38 | 39 | The result may look like this (depending on QGIS version): 40 | 41 | ``` 42 | 1] 32200 43 | [1] 6 44 | POLYGON ((3.06e-15 10, 1.736482 9.848078, 3.420201 9.396926, 5 8.660254, 6.427876 7.660444, 7.660444 6.427876, 8.660254 5, 9.396926 3.420201, 9.848078 1.736482, 10 -2.45e-15, 9.848078 -1.736482, 9.396926 -3.420201, 8.660254 -5, 7.660444 -6.427876, 6.427876 -7.660444, 5 -8.660254, 3.420201 -9.396926, 1.736482 -9.848078, -1.84e-15 -10, -1.736482 -9.848078, -3.420201 -9.396926, -5 -8.660254, -6.427876 -7.660444, -7.660444 -6.427876, -8.660254 -5, -9.396926 -3.420201, -9.848078 -1.736482, -10 1.22e-15, -9.848078 1.736482, -9.396926 3.420201, -8.660254 5, -7.660444 6.427876, -6.427876 7.660444, -5 8.660254, -3.420201 9.396926, -1.736482 9.848078, 3.06e-15 10)) 45 | [1] "2020-05-04 CEST" 46 | [1] "13H 45M 30S" 47 | [[1]] 48 | [1] 2 49 | 50 | [[2]] 51 | [1] 10 52 | 53 | [[3]] 54 | [1] "a" 55 | ``` 56 | 57 | ## Generated R script 58 | 59 | The code that was actually generated from these expressions and passed to R looks like this: 60 | 61 | ``` 62 | qgis_version_info <- 32200 63 | example_value <- 6 64 | example_geometry <- sf::st_as_sfc("Polygon ((0.00000000000000306 10, 1.73648177666930459 9.84807753012207954, 3.42020143325668657 9.39692620785908517, 5.00000000000000533 8.66025403784438375, 6.42787609686539696 7.66044443118977814, 7.6604444311897808 6.42787609686539163, 8.66025403784438552 5, 9.39692620785908517 3.42020143325668124, 9.84807753012208131 1.73648177666929904, 10 -0.00000000000000245, 9.84807753012207954 -1.73648177666930392, 9.39692620785908517 -3.42020143325668569, 8.66025403784438907 -4.99999999999999734, 7.66044443118977902 -6.42787609686539607, 6.42787609686539163 -7.66044443118977991, 4.99999999999999645 -8.66025403784438907, 3.42020143325668613 -9.39692620785908517, 1.73648177666930414 -9.84807753012207954, -0.00000000000000184 -10, -1.73648177666930348 -9.84807753012207954, -3.42020143325668924 -9.39692620785908339, -5 -8.6602540378443873, -6.42787609686539607 -7.66044443118977902, -7.66044443118977991 -6.42787609686539252, -8.66025403784438552 -5.00000000000000089, -9.39692620785908161 -3.42020143325669057, -9.84807753012208131 -1.73648177666930015, -10 0.00000000000000122, -9.84807753012208131 1.73648177666930281, -9.39692620785908517 3.42020143325668435, -8.66025403784438552 5.00000000000000444, -7.66044443118977902 6.42787609686539607, -6.42787609686539252 7.66044443118977991, -5.00000000000000089 8.66025403784438552, -3.42020143325669101 9.39692620785908161, -1.73648177666930059 9.84807753012208131, 0.00000000000000306 10))")[[1]] 65 | example_date <- as.POSIXct("2020-05-04", format = "%Y-%m-%d") 66 | example_time <- lubridate::hms("13:45:30") 67 | example_array <- list(2, 10, "a") 68 | ``` 69 | -------------------------------------------------------------------------------- /website/pages/ex_file_output.md: -------------------------------------------------------------------------------- 1 | # Example with table output 2 | 3 | This script takes input vector data and one of its fields. The result is a table with summary statistics for a given field of the layer. 4 | 5 | ## Script 6 | 7 | ``` 8 | ##Basic statistics=group 9 | ##Statistic table=name 10 | ##Layer=vector 11 | ##Field=Field Layer 12 | ##Stat_file=Output file csv 13 | ##Stat=Output table noprompt 14 | Summary_statistics <- data.frame(rbind( 15 | sum(Layer[[Field]]), 16 | length(Layer[[Field]]), 17 | length(unique(Layer[[Field]])), 18 | min(Layer[[Field]]), 19 | max(Layer[[Field]]), 20 | max(Layer[[Field]])-min(Layer[[Field]]), 21 | mean(Layer[[Field]]), 22 | median(Layer[[Field]]), 23 | sd(Layer[[Field]])), 24 | row.names=c("Sum:","Count:","Unique values:","Minimum value:","Maximum value:","Range:","Mean value:","Median value:","Standard deviation:")) 25 | colnames(Summary_statistics) <- c(Field) 26 | write.csv2(Summary_statistics, Stat_file, row.names = FALSE, na ="") 27 | Stat <- Stat_file 28 | ``` 29 | 30 | ## Script lines description 31 | 32 | 1. **Basic statistics** is the group of the algorithm. 33 | 2. **Statistic table** is the name of the algorithm. 34 | 3. **Layer** is the input vector layer. 35 | 4. **Field** is the name of a field from _Layer_. 36 | 5. **Stat_file** is the name of output that will be selected by user to store result. 37 | 6. **Stat** is the name of output that will be created and returned to QGIS. 38 | 7. Create data.frame with calculated statistics and assign correct row names. 39 | 8. Give the data.frame correct colname, the name of the field. 40 | 9. Write the created data.frame to output file (**Stat_file**) with a controlled formatting. 41 | 10. Assign the output file path to table output (**Stat**) to be presented in QGIS 42 | 43 | The output will be presented in QGIS as a table with a controlled formatting. -------------------------------------------------------------------------------- /website/pages/ex_number_string_output.md: -------------------------------------------------------------------------------- 1 | # Example with number and string outputs 2 | 3 | This script takes input vector data and one of its fields. The result is the minimum and maximum for a given field of the layer. 4 | 5 | ## Script 6 | 7 | ``` 8 | ##Basic statistics=group 9 | ##Min_Max=name 10 | ##Layer=vector 11 | ##Field=Field Layer 12 | ##Min=output number 13 | ##Max=output number 14 | ##Summary=output string 15 | 16 | Min <- min(Layer[[Field]]) 17 | Max <- max(Layer[[Field]]) 18 | Summary <- paste(Min, "to", Max, sep = " ") 19 | ``` 20 | 21 | ## Script lines description 22 | 23 | 1. **Basic statistics** is the group of the algorithm. 24 | 2. **Min_Max** is the name of the algorithm. 25 | 3. **Layer** is the input vector layer. 26 | 4. **Field** is the name of a field from _Layer_. 27 | 5. **Min** is the name of the minimum output that will be created and returned to QGIS. 28 | 6. **Max** is the name of the maximum output that will be created and returned to QGIS. 29 | 7. **Summary** is the name of the string output that will be created and returned to QGIS. 30 | 8. Calculate minimum 31 | 9. Calculate maximum 32 | 10. Create the string 33 | 34 | The outputs will be presented in QGIS as a dictionnary. -------------------------------------------------------------------------------- /website/pages/ex_plot.md: -------------------------------------------------------------------------------- 1 | # Example of graphs output 2 | 3 | This scripts takes input vector data and randomly samples _Size_ points over it. The result is returned as a vector layer. 4 | s 5 | ## Script 6 | 7 | ``` 8 | ##Basic statistics=group 9 | ##Graphs=name 10 | ##output_plots_to_html 11 | ##Layer=vector 12 | ##Field=Field Layer 13 | qqnorm(Layer[[Field]]) 14 | qqline(Layer[[Field]]) 15 | ``` 16 | 17 | ## Script lines description 18 | 19 | 1. **Basic statistics** is the group of the algorithm. 20 | 2. **Graphs** is the name of the algorithm. 21 | 3. The script output plot or plots. 22 | 4. **Layer** is the input vector layer. 23 | 5. **Field** is the name of the field from _Layer_ whose values will be plotted. 24 | 6. Produce a standard QQ plot of the values from _Field_ of _Layer_. 25 | 7. Add a line to a “theoretical”, by default normal, quantile-quantile plot which passes through the probs quantiles, by default the first and third quartiles. 26 | 27 | The plot is automatically added to the Result Viewer of Processing, and it can be open the location shown in the image. 28 | 29 | ![](./images/graph_location.jpg) -------------------------------------------------------------------------------- /website/pages/ex_raster_output.md: -------------------------------------------------------------------------------- 1 | # Example with raster output 2 | 3 | This scripts takes point layer and a field name as an input and performs automatic kriging. The returned value is a raster layer of interpolated values. 4 | 5 | ## Script 6 | 7 | ``` 8 | ##Basic statistics=group 9 | ##Krige value=name 10 | ##Layer=vector 11 | ##Field=Field Layer 12 | ##Output=output raster 13 | library("automap") 14 | library("sp") 15 | Layer_sp = as_Spatial(Layer) 16 | table = as.data.frame(Layer_sp) 17 | coordinates(table)= ~coords.x1+coords.x2 18 | c = Layer[[Field]] 19 | kriging_result = autoKrige(c~1, table) 20 | prediction = raster(kriging_result$krige_output) 21 | Output = prediction 22 | ``` 23 | -------------------------------------------------------------------------------- /website/pages/ex_table_output.md: -------------------------------------------------------------------------------- 1 | # Example with table output 2 | 3 | This script takes input vector data and one of its fields. The result is a table with summary statistics for a given field of the layer. 4 | 5 | ## Script 6 | 7 | ``` 8 | ##Basic statistics=group 9 | ##Statistic table=name 10 | ##Layer=vector 11 | ##Field=Field Layer 12 | ##Stat=Output table 13 | Summary_statistics <- data.frame(rbind( 14 | sum(Layer[[Field]]), 15 | length(Layer[[Field]]), 16 | length(unique(Layer[[Field]])), 17 | min(Layer[[Field]]), 18 | max(Layer[[Field]]), 19 | max(Layer[[Field]])-min(Layer[[Field]]), 20 | mean(Layer[[Field]]), 21 | median(Layer[[Field]]), 22 | sd(Layer[[Field]])), 23 | row.names=c("Sum:","Count:","Unique values:","Minimum value:","Maximum value:","Range:","Mean value:","Median value:","Standard deviation:")) 24 | colnames(Summary_statistics) <- c(Field) 25 | Stat <- Summary_statistics 26 | ``` 27 | 28 | ## Script lines description 29 | 30 | 1. **Basic statistics** is the group of the algorithm. 31 | 2. **Statistic table** is the name of the algorithm. 32 | 3. **Layer** is the input vector layer. 33 | 4. **Field** is the name of a field from _Layer_. 34 | 5. **Stat** is the name of output that will be created and returned to QGIS. 35 | 6. Create data.frame with calculated statistics and assign correct row names. 36 | 7. Give the data.frame correct colname, the name of the field. 37 | 8. Assign the created data.frame to output variable (**Stat**). 38 | 39 | The output will be presented in QGIS as a table. -------------------------------------------------------------------------------- /website/pages/ex_vector_output.md: -------------------------------------------------------------------------------- 1 | # Example with vector output 2 | 3 | This scripts takes input vector data and randomly samples _Size_ points over it. The result is returned as a vector layer. 4 | 5 | ## Script 6 | 7 | ``` 8 | ##Point pattern analysis=group 9 | ##Sample random points=name 10 | ##Layer=vector 11 | ##Size=number 10 12 | ##Output= output vector 13 | Layer_sp = as_Spatial(Layer) 14 | pts = spsample(Layer_sp,Size,type="random") 15 | Output = SpatialPointsDataFrame(pts, as.data.frame(pts)) 16 | ``` 17 | 18 | ## Script lines description 19 | 20 | 1. **Point pattern analysis** is the group of the algorithm. 21 | 2. **Sample random points** is the name of the algorithm. 22 | 3. **Layer** is the input vector layer. 23 | 4. **Size** is the numerical parameter with a default value of 10. 24 | 5. **Output** is the vector layer that will be created by the algorithm. 25 | 6. Convert from sf format to older sp format, for which `spsample()` works 26 | 7. Call the spsample function of the sp library and pass it to all the input defined above. 27 | 8. Create the output vector with the SpatialPointsDataFrame function. 28 | 29 | That’s it! Just run the algorithm with a vector layer you have in the QGIS Legend, choose a number of the random point, and you will get them in the QGIS Map Canvas. -------------------------------------------------------------------------------- /website/pages/help-syntax.md: -------------------------------------------------------------------------------- 1 | # Help syntax 2 | 3 | The R scripts for the plugin contain lines of metadata and R commands that perform a procedure. However, it is also possible to add help documentation for the user of that script. 4 | 5 | ## Help files 6 | 7 | The documentation of the script functionalities and its parameters requires an extra file containing the descriptive texts of each input or output parameter. The help file must be located in the same directory and have the name of the script with extension **.rsx.help**. 8 | 9 | The content of this file is a JSON object with the parameter descriptions. For example, suppose we have added a script `"simple_scatterplot.rsx"` with the following content: 10 | 11 | ```r 12 | ##Example scripts=group 13 | ##Scatterplot=name 14 | ##output_plots_to_html 15 | ##Layer=vector 16 | ##X=Field Layer 17 | ##Y=Field Layer 18 | 19 | # simple scatterplot 20 | plot(Layer[[X]], Layer[[Y]]) 21 | ``` 22 | 23 | The help file should be named `"simple_scatterplot.rsx.help"` and its content should be a JSON object as follows: 24 | 25 | ```json 26 | { 27 | "Layer": "Vector Layer", 28 | "X": "Field from Layer to be used as x-axis variable", 29 | "Y": "Field from Layer to be used as y-axis variable" 30 | } 31 | ``` 32 | 33 | This file can already be used in the script. Parameters that are not described in the help file will be omitted from the help section. Note that each parameter is composed of a pair of `key:value` strings and are separated from other parameters by a comma (,). 34 | 35 | There are special parameters that do not have a user-defined name. These are: 36 | 37 | - `"RPLOTS"`, 38 | - `"R_CONSOLE_OUTPUT"`, 39 | - `"ALG_DESC"`, 40 | - `"ALG_VERSION"`, 41 | - `"ALG_CREATOR"`, 42 | - `"ALG_HELP_CREATOR"` 43 | 44 | If added to the help file, it will allow for more descriptive information to be appended to the script. Continuing with the previous example we can add to the JSON the following information: 45 | 46 | ```json 47 | { 48 | "Layer": "Vector Layer", 49 | "X": "Field from Layer to be used as x-axis variable", 50 | "Y": "Field from Layer to be used as y-axis variable", 51 | "RPLOTS": "Output path for html file with the scatterplot", 52 | "ALG_DESC": "This file creates a simple scatterplot from two fields in a vector layer", 53 | "ALG_CREATOR": "Name of algorithm creator", 54 | "ALG_HELP_CREATOR": "Name of help creator", 55 | "ALG_VERSION": "0.0.1" 56 | } 57 | ``` 58 | 59 | ## In-line help 60 | 61 | As of version 3.2.0 of the plugin, it is also possible to enter the documentation as lines in the script itself. This makes it possible to dispense with the **.rsx.help** file. For this purpose, lines with the structure `#' Parameter: Description` must be written. Let's see how the above example should be written. 62 | ```r 63 | ##Example scripts=group 64 | ##Scatterplot=name 65 | ##output_plots_to_html 66 | ##Layer=vector 67 | ##X=Field Layer 68 | ##Y=Field Layer 69 | 70 | # simple scatterplot 71 | plot(Layer[[X]], Layer[[Y]]) 72 | 73 | #' Layer: Vector Layer 74 | #' X: Field from Layer to be used as x-axis variable 75 | #' Y: Field from Layer to be used as y-axis variable 76 | #' RPLOTS: Output path for html file with the scatterplot 77 | #' ALG_DESC: This file creates a simple scatterplot from 78 | #' : two fields in a vector layer 79 | #' ALG_CREATOR: Name of algorithm creator 80 | #' ALG_HELP_CREATOR: Name of help creator 81 | #' ALG_VERSION: 0.0.1 82 | ``` 83 | 84 | Note that in this way it is also possible to enter the description of a parameter on several lines. To do this, it is necessary to continue the subsequent lines without entering the parameter name, like this: `#' : description append`. 85 | -------------------------------------------------------------------------------- /website/pages/images/console_output.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/website/pages/images/console_output.jpg -------------------------------------------------------------------------------- /website/pages/images/graph_location.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/website/pages/images/graph_location.jpg -------------------------------------------------------------------------------- /website/pages/images/settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/qgis-processing-r/e4f83a13109eb9dd7989cbfd38a37ab16daa0788/website/pages/images/settings.jpg -------------------------------------------------------------------------------- /website/pages/index.md: -------------------------------------------------------------------------------- 1 | # Processing R Provider 2 | 3 | Processing provider for R scripts plugin for [QGIS](https://www.qgis.org/en/site/). The plugin allows execution of R scripts directly from QGIS on data loaded in QGIS. 4 | 5 | ## Important note regarding R packages 6 | 7 | As of November 2023 R package `rgdal` was retired and removed from CRAN, which means it is no longer easily installable. To reflect this the plugin moves from using this package to only supporting `sf` for vector data and `raster` for raster data. If you need to convert to vector format of `sp` package, which was previously done by loading data using `rgdal`, you need to load data using `sf` and then convert to `sp`. Example how to do that is in [this script](ex_vector_output.md). The same applies for loading raster data using `rgdal`, these are now loaded using `raster` package. 8 | 9 | Options `load_raster_using_rgdal`, `dontuserasterpackage` and `load_vector_using_rgdal` are removed from plugin version 4.0.0 and will have no effects on scripts (effectively those lines will be skipped). 10 | 11 | ## Installation 12 | 13 | The plugin is available QGIS Python Plugins Repository [here](https://plugins.qgis.org/plugins/processing_r/). It can also be installed directly from QGIS via the **Plugins** tool. 14 | 15 | Building from source for offline install can be done by downloading the [source code](https://github.com/north-road/qgis-processing-r) and running command: 16 | ``` 17 | make zip 18 | ``` 19 | in the main directory. The produced zip file can then be installed in QGIS from **Plugins** tool in the **Install from zip** window. 20 | 21 | ## Contributors 22 | 23 | [List of project contributors on GitHub.](https://github.com/north-road/qgis-processing-r/graphs/contributors) 24 | 25 | ## R 26 | 27 | [R](https://www.r-project.org) is a free and open-source software environment for statistical computing and graphics. 28 | 29 | It has to be installed separately ([Download and Install R](https://cran.r-project.org/)), together with a few necessary libraries. 30 | 31 | The beauty of Processing implementation is that you can add your scripts, simple or complex ones, and they may then be used as any other module, piped into more complex workflows, etc. 32 | 33 | Test some of the preinstalled examples, if you have [R](https://www.r-project.org) already installed. 34 | 35 | ## Settings for the plugin 36 | 37 | ### Linux 38 | 39 | On Linux, the toolbox should find system installation of R on its own. 40 | 41 | ### Windows 42 | 43 | If R is installed in the expected path (e.g. "C:\PROGRAM FILES\R\"), the plugin tries to detect the R installation automatically. Otherwise, it is necessary to set the setting "R folder" to the correct folder, as seen on the image. The correct folder is the one under which folders "bin", "doc", "etc" and others exist. Generally, it is a folder that has R's version in its name. 44 | 45 | ![](./images/settings.jpg) 46 | -------------------------------------------------------------------------------- /website/pages/script-syntax.md: -------------------------------------------------------------------------------- 1 | # Script syntax 2 | 3 | The R scripts for the plugin use extension **.rsx**. These are classic R scripts with several metadata lines (starting with `##`) that define the interaction between QGIS and R. Metadata amongst other things specify how UI of the tool will look like. 4 | 5 | The script loads necessary packages by itself. Combination of _sp_ and _rgdal_ or _sf_ for vector data and _sp_ and _rgdal_ or _raster_ for raster data. Any other packages need to be directly loaded using `library()` command in the script body. 6 | 7 | ## Metadata 8 | 9 | The R script's file name is used to define the script name and id in the R processing provider. You can override these 10 | default metadata with these lines. 11 | 12 | `##script_name=name` _script_name_ is the name of the script; under this name, it will be listed in processing toolbox. 13 | It is also used to define the script id in the R processing provider. 14 | 15 | `##script_title=display_name` _script_title_ is the title of the script; under this title, it will be listed in processing toolbox. 16 | It overrides the _name_ if it is defined after. 17 | 18 | `##group_name=group` _group_name_ is the name of the group of the script, which allows sorting of scripts into groups in processing toolbox. 19 | 20 | ### Script behaviour 21 | 22 | Several metadata lines define the general behaviour of the script. 23 | 24 | `##output_plots_to_html` (older version of this metadata keyword is `##showplots`) defines that there will be graphical output from the script that will be presented as an HTML page with images. 25 | 26 | `##pass_filenames` (legacy alias `##passfilenames`) specifies that data are not passed directly. Instead only their file names are passed. 27 | 28 | `##dont_load_any_packages` specifies that no packages, besides what is directly specified in script, should be loaded. This means that neither of **sf**, **raster**, **sp** or **rgdal** packages is loaded automatically. If spatial data (either raster or vector) should be passed to this script, the metadata `##pass_filenames` should be used as well. 29 | 30 | `##user1/repo1,user2/repo2=github_install` allows instalation of **R packages** from GitHub using [remotes](https://CRAN.R-project.org/package=remotes). Multiple repos can be specified and divided by coma, white spaces around are stripped. The formats for repository specification are listed on [remotes website](https://remotes.r-lib.org/#usage). 31 | 32 | ### Inputs 33 | 34 | #### Simple specification 35 | 36 | The inputs to R script are specified as: `variable_name=variable_type [default_value/from_variable]`. This metadata line also specifies how tool UI will look in QGIS, as inputs are one section of the tool UI. In this specification _variable_name_ is the name of the variable used in R script, _variable_type_ is a type of input variable from possible input types (vector, raster, table, number, string, boolean, Field). 37 | This metadata line is based on the QGIS parameter `asScriptCode` / `fromScriptCode` string definition. 38 | 39 | The _default_value_ is applicable to number, string, boolean, range, color folder and file inputs. 40 | 41 | The _from_ variable_ applies to Field and must point to _variable_name_ of vector input. 42 | 43 | So the inputs can look like this: 44 | 45 | `##Layer=vector` specifies that there will be variable `Layer` that will be a vector. 46 | 47 | `##X=Field Layer` specifies that variable `X` will be field name taken from `Layer`. 48 | 49 | `##Raster_Layer=raster` specifies that there will be variable `Layer` that will be a raster. 50 | 51 | `##X=Band Raster_Layer` specifies that variable `X` will be raster band index taken from `Raster_Layer`. 52 | 53 | `##Size=number 10` specifies that there will be variable `Size` that will be numeric, and a default value for `Size` will be `10`. 54 | 55 | `##Extent=extent` specifies that there will be variable `Extent` that will be numeric of length `4` (_xmin_, _xmax_, _ymin_ and _ymax_ values). 56 | 57 | `##CRS=crs` specifies that there will be variable `CRS` that will be `EPSG:____` string. 58 | 59 | `##Range=range 0,1` specifies that the `Range` variable will be a two numeric values vector with names (min/max values). The parameter accepts default values, e.g. `0,1`. A range widget will be displayed for setting the values. 60 | 61 | `##Color=color withopacity #FF0000CC` specifies that the `Color` variable will be a text string of the chosen color in hexadecimal format. This parameter will display a color selection widget. The parameter accepts default values, for example `#FF0000`. The opacity value depends on the `withopacity` option. 62 | 63 | `##Date_Time=datetime` specifies that the `Date_Time` variable will be a `POSIXct` vector of length `1` of the date and time choosing from the `datetime` widget . The parameter does not accept default values, instead, the current date and time will set by default. 64 | 65 | ##### Enum 66 | 67 | The basic enum syntax is `##var_enum=enum a;b;c` to select from values `a`, `b` or `c`. The value of `var_enum` in this case will be integer indicated position of the selected item in a list. So for example, if `a` is selected the value of `var_enum` will be `0`. 68 | 69 | The approach described above works well for a wide range of applications but for **R** it is often not ideal. That is a reason why a new type of enum is available in script syntax. 70 | 71 | The syntax is `##var_enum_string=enum literal a;b;c`. The important part here is the keyword `literal` (or more precisely `enum literal`) which specifies that the value from the select box to `var_enum_string` should be passed as a string. So if `b` is selected, then the value of `var_enum_string` will be `"b"`. 72 | 73 | Enum also accept multiples choices in both, literal or numeric setups, i.e. `##var_enum_string=enum literal multiple a;b;c` or `##var_enum_string=enum multiple a;b;c`. The variables in R will behave in the same way as non-multiple setup. 74 | 75 | #### Advanced specification 76 | 77 | The inputs to R script are specified as: `QgsProcessingParameter|name|description|other_parameters_separated_by_pipe`. 78 | The _other_ parameter_ separated_ by_ pipe_ can contain the `QgsProcessingParameter` specific parameters. 79 | This metadata line is based on the QGIS definitions used in description file used by GRASS7, SAGA and others providers. 80 | 81 | So the inputs can look like this: 82 | 83 | `##QgsProcessingParameterFeatureSource|INPUT|Vector layer` specifies that there will be variable `INPUT` that will be a vector. 84 | 85 | `##QgsProcessingParameterField|FIELDS|Attributes|None|INPUT|-1|False|False` specifies that there will be variable `FIELDS` that will be a field index. 86 | 87 | `##QgsProcessingParameterRasterLayer|INPUT|Grid` specifies that there will be variable `INPUT` that will be a raster. 88 | 89 | `##QgsProcessingParameterNumber|SIZE|Aggregation Size|QgsProcessingParameterNumber.Integer|10` specifies that there will be variable `Size` that will be numeric, and a default value for `Size` will be `10`. 90 | 91 | `##QgsProcessingParameterEnum|METHOD|Method|[0] Sum;[1] Min;[2] Max` specifies that there will be variable `METHOD` that will be a value provided under `[]`. 92 | 93 | `##QgsProcessingParameterVectorDestination|OUTPUT|Result` specifies that there will be variable `OUTPUT` that will be the destination file. 94 | 95 | 96 | ##### Feature source 97 | 98 | `##QgsProcessingParameterFeatureSource|name|description|geometry type|default value|optional` 99 | * _name_ is mandatory 100 | * _description_ is mandatory 101 | * _geometry type_ is optional and the default value is `-1` for any geometry, the available values are `0` for point, `1` for line, `2` for polygon, `5` for table 102 | * _default value_ is optional and the default value is `None` 103 | * _optional_ is optional and the default value is `False` 104 | 105 | `##QgsProcessingParameterFeatureSource|INPUT|Points|0` specifies that there will be variable `INPUT` that will be a vector point layer. 106 | 107 | `##QgsProcessingParameterFeatureSource|INPUT|Lines|1` specifies that there will be variable `INPUT` that will be a vector line layer. 108 | 109 | `##QgsProcessingParameterFeatureSource|INPUT|Polygons|2` specifies that there will be variable `INPUT` that will be a vector polygon layer. 110 | 111 | `##QgsProcessingParameterFeatureSource|INPUT|Table|5` specifies that there will be variable `INPUT` that will be a vector layer to provide table. 112 | 113 | `##QgsProcessingParameterFeatureSource|INPUT|Optional layer|-1|None|True` specifies that there will be variable `INPUT` that will be an optional vector layer. 114 | 115 | ##### Raster layer 116 | 117 | `##QgsProcessingParameterRasterLayer|name|description|default value|optional` 118 | * _name_ is mandatory 119 | * _description_ is mandatory 120 | * _default value_ is optional and the default value is `None` 121 | * _optional_ is optional and the default value is `False` 122 | 123 | ##### File or folder parameter 124 | 125 | `##QgsProcessingParameterFile|name|description|behavior|extension|default value|optional|file filter` 126 | * _name_ is mandatory 127 | * _description_ is mandatory 128 | * _behavior_ is optional and the default value is `0` for file, `1` is for folder input parameter 129 | * _extension_ is optional and specifies a file extension associated with the parameter (e.g. `html`). Use _file filter_ for a more flexible approach which allows for multiple file extensions. if _file filter_ is specified it takes precedence and _extension_ is emptied. 130 | * _default value_ is optional and the default value is `None` 131 | * _optional_ is optional and the default value is `False` 132 | * _file filter_ is optional and specifies the available file extensions associted sith the parameter (e.g. `PNG Files (*.png);; JPG Files (*.jpg *.jpeg)`) 133 | 134 | `##QgsProcessingParameterFile|in_file|Input file` specifies that there will be variable `in_file` that will be any file. 135 | 136 | `##QgsProcessingParameterFile|in_folder|Input folder|1` specifies that there will be variable `in_folder` that will be a folder. 137 | 138 | `##QgsProcessingParameterFile|in_gpkg|Input gpkg|0|gpkg` specifies that there will be variable `in_gpkg` that will be gpkg file. 139 | 140 | `##QgsProcessingParameterFile|in_img|Input img|0|png|None|False|PNG Files (*.png);; JPG Files (*.jpg *.jpeg)` specifies that there will be variable `in_img` that will be an image PNG or JPG. 141 | 142 | #### QGIS Expression 143 | 144 | A QGIS expression can be used as input for the script. The syntax is `r_variable_name=expression` followed by space and any valid QGIS expression. The expression is evaluated in QGIS before passing the result value script to R. This type of input is hidden from user of the script and can only be modified by editing the R script itself. 145 | 146 | The following code: 147 | 148 | `##qgis_expression=expression @qgis_version` 149 | 150 | creates variable `qgis_expression` that will hold information about QGIS version on which the script was run (i.e. __3.22.0-Białowieża__). 151 | 152 | `##example_geometry=expression make_circle( make_point(0, 0), 10)` creates R variable with name `example_geometry` that holds polygon created by the expression. 153 | 154 | The variable type for R is determined from type of expression output in QGIS. So far these types are supported - string, integer, float, date, time, datetime, geometry and lists (arrays) of these types. 155 | 156 | ### Outputs 157 | 158 | The outputs of R script are specified as `##variable_name=output output_type`. This line also specifies how tool UI will look in QGIS, as outputs are one section of the tool UI. In this specification _variable_name_ specifies variable from the script that will be exported back to QGIS, _output_type_ is one of the allowed types that can be returned from R script (layer, raster, folder, file, HTML, number, string, table). 159 | 160 | So the outputs can look like this: 161 | 162 | `##New_string=output string` specifies that a variable `New_string` will be set in the script. 163 | 164 | `##New_number=output number` specifies that a variable `New_number` will be set in the script. 165 | 166 | #### Layer outputs 167 | 168 | For layer outputs, the line definition can end with the `noprompt` keyword. In this case, the `variable_name` has to be defined in the script. Otherwise, the UI will display a widget to let the user defined the output destination. 169 | 170 | `##New_layer=output vector` specifies that the variable `New_layer` will be imported to QGIS as a vector layer. 171 | 172 | `##New_raster=output raster` specifies that variable `New_raster` will be imported to QGIS as a raster layer. 173 | 174 | `##New_table=output table` specifies that the variable `New_table` will be imported to QGIS as a vector layer without geometry (CSV file). 175 | 176 | #### Folder and files outputs 177 | 178 | Like layer outputs, the folder and files outputs the line definition can end with the `noprompt` keyword. 179 | 180 | `##New_file=output file` specifies that there will be variable `New_file` that will be a file path with `.file` extension 181 | 182 | `##New_csv=output file csv` specifies that there will be variable `New_csv` that will be a file path with `.csv` extension 183 | 184 | `##New_folder=output folder` specifies that there will be variable `New_folder` that will be a folder path 185 | 186 | ### Printing from R to tool log 187 | 188 | If any output of any line in R script should be outputted to tool log, it needs to be preceded by `>`. So, for example, the following code will print the number of rows in `Layer`. 189 | 190 | ``` 191 | >nrow(Layer) 192 | ``` 193 | --------------------------------------------------------------------------------