R/qgis-configure.R

Defines functions qgis_unconfigure qgis_env message_inspect_cache qgis_reconfigure qgis_configure

Documented in qgis_configure qgis_unconfigure

#' Configure qgisprocess
#'
#' Run `qgis_configure()` to bring the package configuration in line with
#' QGIS and to save this configuration to a persistent cache.
#' See the _Details_ section for more information about setting the path of
#' the 'qgis_process' command line tool.
#'
#' The qgisprocess package is a wrapper around the 'qgis_process' command line
#' tool distributed with QGIS (>=3.14). Several functions use heuristics to
#' detect the location of the 'qgis_process' executable.
#'
#' When loading the package, the configuration is automatically read from the
#' cache with `qgis_configure(use_cached_data = TRUE, quiet = TRUE)` in order
#' to save time.
#' Run `qgis_configure(use_cached_data = TRUE)` manually to get more details.
#'
#' Use `qgis_algorithms()`, `qgis_providers()`, `qgis_plugins()`,
#' `qgis_using_json_output()`, `qgis_path()` and `qgis_version()` to inspect cache
#' contents.
#'
#' If the configuration
#' fails or you have more than one QGIS installation, you can set
#' `options(qgisprocess.path = "path/to/qgis_process")` or the
#' `R_QGISPROCESS_PATH` environment variable (useful on CI). On Linux the
#' 'qgis_process' executable is generally available on the user's PATH,
#' on MacOS the executable is within the QGIS*.app/Contents/MacOS/bin folder,
#' and on Windows the executable is named qgis_process-qgis.bat or
#' qgis_process-qgis-dev.bat and is located in Program Files/QGIS*/bin or
#' OSGeo4W(64)/bin.
#'
#' @family topics about configuring QGIS and qgisprocess
#' @concept functions to manage and explore QGIS and qgisprocess
#' @seealso [qgis_unconfigure()]
#' @seealso [qgis_path()], [qgis_version()]
#'
#' @param use_cached_data Use the cached algorithm list and `path` found when
#'   configuring qgisprocess during the last session. This saves some time
#'   loading the package.
#' @inheritParams qgis_run
#' @inheritParams qgis_path
#'
#' @returns The result of [processx::run()].
#'
#' @examples
#' \donttest{
#' # not running in R CMD check to save time
#' qgis_configure(use_cached_data = TRUE)
#' }
#'
#' \dontrun{
#' # package reconfiguration
#' # (not run in example() as it rewrites the package cache file)
#' qgis_configure()
#' }
#'
#' @export
qgis_configure <- function(quiet = FALSE, use_cached_data = FALSE) {
  tryCatch(
    {
      qgis_unconfigure()

      cache_data_file <- qgis_pkgcache_file()

      # Practically all code of this function now consists of handling
      # use_cached_data = TRUE. This includes cases where qgis_reconfigure() must
      # be called, when a condition about the cache is not met. Note that
      # qgis_configure() by default (use_cached_data = FALSE) calls
      # qgis_reconfigure() straight away.


      # below message is usually relevant when loading the package, and serves to
      # announce some waiting time (checking and reading the cache) in case this
      # process will happen silently (as is the case when loading the pkg). Put
      # here, since packagestartupmessages in .onLoad are not good practice and
      # trigger a note during R CMD check. Same applies to the Success! message
      # further down.
      # Note: more messages have been turned into startupmessages since CRAN
      # wants to be able to suppress them.
      if (use_cached_data && quiet) {
        packageStartupMessage(
          "Attempting to load the package cache ... ",
          appendLF = FALSE
        )
      }

      if (use_cached_data && file.exists(cache_data_file)) {
        try({
          cached_data <- readRDS(cache_data_file)

          if (!quiet) message(glue("Checking configuration in cache file ({cache_data_file})"))

          # CACHE CONDITION: contains minimum required elements (i.e. objects; the
          # cache is an environment)
          if (
            !all(
              c("path", "version", "algorithms", "plugins", "use_json_output")
              %in% names(cached_data)
            )
          ) {
            if (quiet) packageStartupMessage()
            packageStartupMessage(
              "The cache does not contain all required elements.\n",
              "Will try to reconfigure qgisprocess and build new cache ..."
            )
            qgis_reconfigure(cache_data_file = cache_data_file, quiet = quiet)
            return(invisible(has_qgis()))
          }

          # CACHE CONDITION: the path element does not contradict the environment
          # variable/option for the qgis_process path
          option_path <- getOption(
            "qgisprocess.path",
            Sys.getenv("R_QGISPROCESS_PATH")
          )

          if (!identical(option_path, "") && !identical(option_path, cached_data$path)) {
            if (quiet) packageStartupMessage()
            packageStartupMessage(glue(
              "The user's qgisprocess.path option or the R_QGISPROCESS_PATH environment ",
              "variable specify a different qgis_process path ({option_path}) ",
              "than the cache did ({cached_data$path}).\n",
              "Hence rebuilding cache to reflect this change ..."
            ))
            qgis_reconfigure(cache_data_file = cache_data_file, quiet = quiet)
            return(invisible(has_qgis()))
          }

          # CACHE CONDITION: the elements checked by has_qgis() look OK

          condition <-
            is.string(cached_data$version) && nchar(cached_data$version) > 3L &&
              is.string(cached_data$path) && nchar(cached_data$path) > 10L &&
              inherits(cached_data$algorithms, "data.frame") &&
              inherits(cached_data$plugins, "data.frame")

          if (!condition) {
            if (quiet) packageStartupMessage()
            packageStartupMessage(
              "The cache does not contain all required data.\n",
              "Will try to reconfigure qgisprocess and build new cache ..."
            )
            qgis_reconfigure(cache_data_file = cache_data_file, quiet = quiet)
            return(invisible(has_qgis()))
          }

          # CACHE CONDITION: qgis_process is indeed available in the cached path

          testopt <- getOption("qgisprocess.test_skip_path_availability_check")
          outcome <- try(qgis_run(path = cached_data$path), silent = TRUE)
          if (inherits(outcome, "try-error") && !isTRUE(testopt)) {
            if (quiet) packageStartupMessage()
            packageStartupMessage(
              glue(
                "'{cached_data$path}' (cached path) is not available anymore.\n",
                "Will try to reconfigure qgisprocess and build new cache ..."
              )
            )
            qgis_reconfigure(cache_data_file = cache_data_file, quiet = quiet)
            return(invisible(has_qgis()))
          }

          # CACHE CONDITION: the path element does not contradict the
          # environment variable/option to automatically switch to a newer
          # available QGIS version
          if (is_windows() || is_macos()) {
            opt <- resolve_flag_opt(
              option_name = "qgisprocess.detect_newer_qgis",
              envvar_name = "R_QGISPROCESS_DETECT_NEWER_QGIS"
            )
            first_qgis <- qgis_detect_paths()[1]
            newer_available <- !is.na(extract_version_from_paths(first_qgis)) &&
              !identical(cached_data$path, first_qgis)

            if (opt && isTRUE(newer_available) && rlang::is_interactive()) {
              packageStartupMessage()
              packageStartupMessage(glue(
                "A newer QGIS installation seems to be available: ",
                "{extract_version_from_paths(first_qgis)}."
              ))
              answer <- ""
              while (!grepl("^[Yy](?:[Ee][Ss])?$|^[Nn](?:[Oo])?$", answer)) {
                answer <- getOption("qgisprocess.test_try_new_qgis") %||%
                  readline("Do you want to try it and rebuild the cache? (y/n) ")
              }
              if (grepl("^[Yy]", answer)) {
                newer_ok <- FALSE
                tryCatch(
                  {
                    qgis_run(path = first_qgis)
                    newer_ok <- TRUE
                  },
                  error = function(e) {
                    packageStartupMessage(
                      glue(
                        "'{first_qgis}' does not work as expected.\n",
                        "So will not try it further."
                      )
                    )
                  }
                )
                if (newer_ok) {
                  packageStartupMessage(
                    "Will try to reconfigure qgisprocess and build new cache ..."
                  )
                  qgis_reconfigure(cache_data_file = cache_data_file, quiet = quiet)
                  return(invisible(has_qgis()))
                }
              } else if (!quiet) {
                packageStartupMessage(
                  "\nNOTE: if you don't want to autodetect QGIS version updates ",
                  "in the future, unset the qgisprocess.detect_newer_qgis ",
                  "option or the R_QGISPROCESS_DETECT_NEWER_QGIS environment ",
                  "variable."
                )
              }
            }
          }

          # CACHE CONDITION: the cached QGIS version equals the one reported by
          # qgis_process

          if (!quiet) message(glue(
            "Checking cached QGIS version with version reported by '{cached_data$path}' ..."
          ))

          qgisprocess_cache$path <- cached_data$path
          qversion <- qgis_query_version(quiet = quiet)
          qgisprocess_cache$path <- NULL

          if (!identical(qversion, cached_data$version)) {
            if (quiet) packageStartupMessage()
            packageStartupMessage(glue(
              "QGIS version change detected:\n",
              "- in the qgisprocess cache it was: {cached_data$version}\n",
              "- while '{cached_data$path}' is at {qversion}\n",
              "Hence rebuilding cache to reflect this change ..."
            ))
            qgis_reconfigure(cache_data_file = cache_data_file, quiet = quiet)
            return(invisible(has_qgis()))
          }

          if (!quiet) message(glue("QGIS versions match! ({qversion})"))

          # CACHE CONDITION: the use_json_output element does not contradict the environment
          # variable/option for the output or input method (JSON vs legacy)

          # Since we want library(qgisprocess) to take into account
          # pre-existing settings for the JSON output, we must reconfigure if
          # it is not consistent with  qgis_using_json_output().

          # There is good reason to cache 'use_json_output': the value of
          # qgis_algorithms() and qgis_plugins() is different when populating
          # it with or without the --json flag.

          opt <- readopt_json_output()
          qversion_short <- strsplit(qversion, "-")[[1]][1]

          if (
            ( ## conflict with explicit json_output AND json_input setting?
              !identical(opt, "") &&
                # resolving conflicts with explicit JSON INput setting:
                !identical(
                  resolve_explicit_json_output(
                    json_output_setting = opt,
                    qgis_version = qversion_short
                  ),
                  cached_data$use_json_output
                )) ||
              ( ## conflict with explicit json_INput set as TRUE?
                identical(opt, "") &&
                  json_input_set_and_acceptable(qversion_short) &&
                  isFALSE(cached_data$use_json_output)
              )
          ) {
            if (quiet) packageStartupMessage()
            packageStartupMessage(glue(
              "The outcome of user settings for using JSON input/output ",
              "contradict the 'use_json_output' cache value ",
              "({cached_data$use_json_output}).\n",
              "Hence rebuilding cache to reflect this change ..."
            ))
            qgis_reconfigure(cache_data_file = cache_data_file, quiet = quiet)
            return(invisible(has_qgis()))
          }

          # CACHE CONDITION: the cached QGIS plugins equal the ones reported by
          # qgis_process, including their state

          if (!quiet) message(glue(
            "Checking cached QGIS plugins (and state) with those reported by '{cached_data$path}' ..."
          ))

          qgisprocess_cache$path <- cached_data$path
          qgisprocess_cache$use_json_output <- cached_data$use_json_output
          qgisprocess_cache$version <- cached_data$version
          qplugins <- qgis_query_plugins(quiet = quiet)
          qgisprocess_cache$path <- NULL
          qgisprocess_cache$use_json_output <- NULL
          qgisprocess_cache$version <- NULL

          if (!identical(qplugins, cached_data$plugins)) {
            if (quiet) packageStartupMessage()
            packageStartupMessage(
              "Change detected in (enabled) QGIS processing provider plugins!\n",
              "- in the qgisprocess cache it was:"
            )
            print(cached_data$plugins)
            packageStartupMessage(glue(
              "- while '{cached_data$path}' currently returns:"
            ))
            print(qplugins)
            packageStartupMessage(
              "Hence rebuilding cache to reflect this change ..."
            )
            qgis_reconfigure(cache_data_file = cache_data_file, quiet = quiet)
            return(invisible(has_qgis()))
          }

          if (!quiet) {
            message(glue(
              "QGIS plugins match! ({ sum(qplugins$enabled) } ",
              "processing provider plugin(s) enabled)"
            ))
            message_disabled_plugins(qplugins, prepend_newline = TRUE)
          }

          # ASSIGNING CACHE OBJECTS

          if (!quiet) message(glue(
            "\n\nRestoring configuration from '{cache_data_file}'"
          ))

          qgisprocess_cache$path <- cached_data$path
          qgisprocess_cache$version <- cached_data$version
          qgisprocess_cache$use_json_output <- cached_data$use_json_output
          qgisprocess_cache$algorithms <- cached_data$algorithms
          qgisprocess_cache$plugins <- cached_data$plugins
          qgisprocess_cache$loaded_from <- cache_data_file

          # FINAL HANDLING OF SUCCESSFUL use_cached_data = TRUE

          if (!quiet) { # triggers messages
            invisible(qgis_version(query = FALSE, quiet = FALSE))
            invisible(qgis_path(query = FALSE, quiet = FALSE))
            invisible(qgis_using_json_output(query = FALSE, quiet = FALSE))
            message(
              ifelse(qgis_using_json_input(), "Using ", "Not using "),
              "JSON for input serialization."
            )
            invisible(qgis_plugins(query = FALSE, quiet = FALSE, msg = FALSE))
            invisible(qgis_algorithms(query = FALSE, quiet = FALSE))
            message_inspect_cache()
          }

          # usually applicable only on loading the package:
          if (quiet && !is.null(qgisprocess_cache$loaded_from)) {
            (packageStartupMessage("Success!"))
          }

          return(invisible(has_qgis()))
        })
      }

      if (use_cached_data && !file.exists(cache_data_file)) {
        packageStartupMessage(
          "No cache found.\n",
          "Will try to reconfigure qgisprocess and build new cache ..."
        )
      }

      # use_cached_data = FALSE or cache is missing:

      qgis_reconfigure(cache_data_file = cache_data_file, quiet = quiet)
    },
    error = function(e) {
      qgis_unconfigure()
      if (!quiet) message(e)
    }
  )

  invisible(has_qgis())
}




#' @keywords internal
qgis_reconfigure <- function(cache_data_file, quiet = FALSE) {
  path <- qgis_path(query = TRUE, quiet = quiet)
  if (!quiet) message()

  tryCatch(
    {
      version <- qgis_version(query = TRUE, quiet = quiet)
    },
    error = function(e) {
      message(glue(
        "\n\nATTENTION: the QGIS version could not be queried. ",
        "You will loose some functionality.\n",
        "You may want to run `qgis_version(query = TRUE)` followed by ",
        "`qgis_configure()`; see its documentation.\n",
        "Error message was:\n\n{e}\n",
        ifelse("stderr" %in% names(e) && nchar(e$stderr) > 0, e$stderr, "")
      ))
    }
  )

  use_json_output <- qgis_using_json_output(query = TRUE, quiet = quiet)

  if (!quiet) message(
    ifelse(qgis_using_json_input(), "Using ", "Not using "),
    "JSON for input serialization."
  )

  tryCatch(
    {
      plugins <- qgis_plugins(query = TRUE, quiet = quiet, msg = FALSE)
    },
    error = function(e) {
      message(glue(
        "\n\nATTENTION: the QGIS plugins could not be queried. ",
        "You will loose some functionality.\n",
        "You may want to (re)run `qgis_configure()`; see its documentation.\n",
        "Error message was:\n\n{e}\n",
        ifelse("stderr" %in% names(e) && nchar(e$stderr) > 0, e$stderr, "")
      ))
    }
  )

  tryCatch(
    {
      algorithms <- qgis_algorithms(query = TRUE, quiet = quiet)
    },
    error = function(e) {
      message(glue(
        "\n\nATTENTION: the QGIS algorithms could not be queried. ",
        "You will loose some functionality.\n",
        "You may want to (re)run `qgis_configure()`; see its documentation.\n",
        "Error message was:\n\n{e}\n",
        ifelse("stderr" %in% names(e) && nchar(e$stderr) > 0, e$stderr, "")
      ))
    }
  )

  if (!quiet && exists("plugins")) message_disabled_plugins(plugins, prepend_newline = TRUE)

  if (has_qgis()) {
    if (!quiet) message(glue("\n\nSaving configuration to '{cache_data_file}'"))

    try({
      if (!dir.exists(dirname(cache_data_file))) {
        dir.create(dirname(cache_data_file), recursive = TRUE)
      }

      saveRDS(
        list(
          path = path,
          version = version,
          algorithms = algorithms,
          plugins = plugins,
          use_json_output = use_json_output
        ),
        cache_data_file
      )
    })

    if (!quiet) message_inspect_cache()
  } else if (!quiet) {
    message(config_problem_msg)
  }
}



#' @keywords internal
message_inspect_cache <- function() {
  message(
    "Use qgis_algorithms(), qgis_providers(), qgis_plugins(), ",
    "qgis_path() and\nqgis_version() ",
    "to inspect the cache environment."
  )
}





#' @keywords internal
qgis_env <- function() {
  getOption(
    "qgisprocess.env",
    list(QT_QPA_PLATFORM = "offscreen")
  )
}





#' Clean the package cache
#'
#' Empties the qgisprocess cache environment.
#'
#' @family topics about programming or debugging utilities
#'
#' @returns `NULL`, invisibly.
#'
#' @examples
#' \dontrun{
#' # not running this function in example() as it clears the cache environment.
#' qgis_unconfigure()
#' }
#'
#' # undoing qgis_unconfigure() by repopulating the cache environment from file:
#' \donttest{
#' # not running in R CMD check to save time
#' qgis_configure(use_cached_data = TRUE)
#' }
#'
#' @export
qgis_unconfigure <- function() {
  qgisprocess_cache$path <- NULL
  qgisprocess_cache$version <- NULL
  qgisprocess_cache$algorithms <- NULL
  qgisprocess_cache$plugins <- NULL
  qgisprocess_cache$loaded_from <- NULL
  qgisprocess_cache$use_json_output <- NULL
  invisible(NULL)
}
paleolimbot/qgisprocess documentation built on Feb. 27, 2024, 9:52 p.m.