diff --git a/DESCRIPTION b/DESCRIPTION index c5a809f2..5f07ddf6 100755 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: rgee Title: R Bindings for Calling the 'Earth Engine' API -Version: 1.0.3 +Version: 1.0.4 Authors@R: c(person(given = "Cesar", family = "Aybar", diff --git a/NEWS.md b/NEWS.md index 8e9d7fc5..e88c8091 100755 --- a/NEWS.md +++ b/NEWS.md @@ -11,6 +11,22 @@ vignette: > %\VignetteIndexEntry{NEWS} %\VignetteEncoding{UTF-8} --- +# rgee 1.0.4 +- Add `ee_help` a new Rstudio addins that mimics the help Rstudio interface (F1). +- Fix a bug that makes that `ee_as_sf` only supports `GeoJSON` format. +- If `dsn` is not specified in `ee_as_sf`, it will create a temporary shapefile (in \tmp dir). +- Fix a bug in `ee_imagecollection_to_local` (#87 Thanks @cedlfc44) +- Fix a bug in `ee_image_local` (#88 Thanks @cedlfc44) +- Fix a bug in `ee_create_credentials_drive` (#90 #78 Thanks @cedlfc44) + +# rgee 1.0.3 +- getPass library removed from `ee_Initialize`. +- New argument `display` in `ee_Initialize` to return the authentication URI. Useful for `rgee` colab users. +- Changes in some diagnostic messages to make possible to use `rgee` in colab. +- `ee_help` returns a HTML file rather than TRUE. It also now supports characters (e.g. `ee_help("ee$Image")`). +- Fix a strange bug when `ee_Initialize` tries to connect to reticulate the first time. +- Fix small bugs in `ee_user_info` and `ee_users` + # rgee 1.0.2 - Earth Engine Python API updated to 0.1.229. - Fix a bug in `ee_Initialize`, that does not permit users to use `ee_createAssetHome` to define their *Earth Engine Assets home root folder* diff --git a/R/addins.R b/R/addins.R new file mode 100644 index 00000000..b0e0ccf6 --- /dev/null +++ b/R/addins.R @@ -0,0 +1,160 @@ +#' Return documentation of Earth Engine modules, methods and classes +#' @noRd +ee_help_addins <- function() { + context <- rstudioapi::getSourceEditorContext() + selected_content <- context$selection[[1]]$text + if (selected_content == "") { + try(ee_help(ee_get_eefunc()), silent = TRUE) + } else { + selected_content_filtered <- gsub("\n|[[:space:]]","", selected_content) + try(ee_help(selected_content_filtered), silent = TRUE) + } +} + +#' How many white space we deleted? +#' @noRd +ee_space_removed <- function(text, cursor) { + text <- strsplit(text,"")[[1]] + sum(grepl(" ",text[1:cursor])) +} + +#' Merge forward and backward +#' @noRd +ee_get_funname <- function(text, cursor) { + text <- strsplit(text,"")[[1]] + + if (length(text) < cursor) { + return(invisible(FALSE)) + } + + if (text[cursor] == "(") { + cursor <- cursor -1 + } + if (cursor == 1) { + # the last word can not be a $ + paste0(text[1:forward(text, cursor)], collapse = "") + } else { + # the last word can not be a $ + paste0(text[backward(text, cursor):forward(text, cursor)], collapse = "") + } +} + +#' Search words forward +#' @noRd +forward <- function(x, cursor) { + forward_range <- cursor:length(x) + for (index in forward_range) { + is_letter <- grepl("[a-zA-Z]", x[index]) + if (!is_letter) { + index <- index - 1 + break + } + } + if (is_letter <- grepl("\\$", x[index])) { + index - 1 + } else { + index + } +} + +#' Search words backward +#' @noRd +backward <- function(x, cursor) { + index <- cursor + repeat { + if (index == 1) { + break + } + + # Just pass the letter if is inside a () + if (x[index] == ")") { + count_par <- 1 + counter <- 0 + while (count_par != 0) { + if (x[index] == "(") { + count_par <- count_par - 1 + } else if(x[index] == ")" & counter != 0) { + count_par <- count_par + 1 + } + index <- index - 1 + counter <- counter + 1 + if (index == 1) { + break + } + # print(sprintf("%s:%s",counter,count_par)) + } + index <- index - 1 + } + + if (grepl("[a-zA-Z]|\\$|\\)", x[index])) { + index <- index - 1 + } else { + index <- index + 1 + break + } + } + index +} + +#' Aux function useful to know if a multiline (recursive) +#' Returns a logical vector. +#' @noRd +is_multilines_r <- function(context, line) { + if (is_multilines(context, line)) { + c(TRUE, is_multilines_r(context, line - 1)) + } else { + FALSE + } +} + +#' Aux function useful to know if a multiline +#' Returns a logical value. +#' @noRd +is_multilines <- function(context, line) { + if (line == 1) { + FALSE + } else { + line_of_code_1 <- context$contents[line] + text_1 <- strsplit(line_of_code_1, "")[[1]] + is_white_space <- text_1[1] == " " + + line_of_code_2 <- context$contents[line - 1] + text_2 <- strsplit(line_of_code_2, "")[[1]] + is_dolar <- text_2[length(text_2)] == "$" + if (length(is_dolar ) == 0) { + is_dolar <- FALSE + } + + if (is_dolar & is_white_space) { + TRUE + } else { + FALSE + } + } +} + +#' Returns the EE function name +#' @noRd +ee_get_eefunc <- function() { + # get rstudio context + context <- rstudioapi::getSourceEditorContext() + cursor <- context$selection[[1]]$range[[1]][2] + line <- context$selection[[1]]$range[[1]][1] + + # is a multiple line? + if (any(is_multilines_r(context, line))) { + # lines above! + number_of_extra_lineas <- sum(is_multilines_r(context, line)) + lines <- (line - number_of_extra_lineas):line + # merge lines text in one character + text_merge <- paste0(gsub(" ", "", context$contents[lines]), collapse = "") + # upgrade cursor + extra_lines <- lines[-length(lines)] + previous_len <- paste0(context$contents[extra_lines], collapse = "") + space_removed <- ee_space_removed(text = context$contents[line], cursor = cursor) + new_cursor <- nchar(previous_len) + cursor - space_removed + ee_get_funname(text = text_merge, cursor = new_cursor) + } else { + ee_get_funname(text = context$contents[line], cursor = cursor) + } +} diff --git a/R/ee_Initialize.R b/R/ee_Initialize.R index 851dae03..a63ed250 100644 --- a/R/ee_Initialize.R +++ b/R/ee_Initialize.R @@ -259,9 +259,10 @@ ee_Initialize <- function(email = NULL, } # Root folder exist? - ee_user_assetroot <- try(ee$data$getAssetRoots()[[1]]) + ee_user_assetroot <- ee$data$getAssetRoots() + assetroot_exist <- length(ee_user_assetroot) == 0 # if ee_asset_home (list) length is zero - if (length(ee_user_assetroot) == 0 | class(ee_user_assetroot) == "try-error") { + if (assetroot_exist) { root_text <- paste( "Earth Engine Assets home root folder does not exist for the current user.", "Please enter your desired root folder name below. Take into consideration", @@ -275,10 +276,10 @@ ee_Initialize <- function(email = NULL, ) message(root_text) ee_createAssetHome() - ee_user_assetroot <- ee$data$getAssetRoots()[[1]] + ee_user_assetroot <- ee$data$getAssetRoots() } - - ee_user <- ee_remove_project_chr(ee_user_assetroot$id) + ee_user_assetroot_id <- ee_user_assetroot[[1]]$id + ee_user <- ee_remove_project_chr(ee_user_assetroot_id) options(rgee.ee_user = ee_user) ee_sessioninfo( @@ -375,16 +376,17 @@ ee_create_credentials_drive <- function(email) { call. = FALSE ) } - # setting drive folder + # Set folder to save Google Drive Credentials oauth_func_path <- system.file("python/ee_utils.py", package = "rgee") utils_py <- ee_source_python(oauth_func_path) ee_path <- ee_utils_py_to_r(utils_py$ee_path()) email_clean <- gsub("@gmail.com", "", email) ee_path_user <- sprintf("%s/%s", ee_path, email_clean) - # drive_credentials + + # Load GD credentials (googledrive::drive_auth) repeat { full_credentials <- list.files(path = ee_path_user, full.names = TRUE) - drive_condition <- grepl("@gmail.com", full_credentials) + drive_condition <- grepl(".*_.*@.*", basename(full_credentials)) if (!any(drive_condition)) { suppressMessages( googledrive::drive_auth( @@ -404,8 +406,12 @@ ee_create_credentials_drive <- function(email) { break } } - # from user folder to EE folder - unlink(list.files(ee_path, "@gmail.com", full.names = TRUE)) + + # Clean previous and copy new GD credentials in ./earthengine folder + clean_drive <- list.files(ee_path, ".*_.*@.*", full.names = TRUE) %in% list.dirs(ee_path) + unlink( + list.files(ee_path, ".*_.*@.*", full.names = TRUE)[!clean_drive] + ) file.copy( from = drive_credentials, to = sprintf("%s/%s", ee_path, basename(drive_credentials)), diff --git a/R/ee_as_sf.R b/R/ee_as_sf.R index f3d3df8f..67fde796 100755 --- a/R/ee_as_sf.R +++ b/R/ee_as_sf.R @@ -3,7 +3,7 @@ #' @param x Earth Engine table (ee$FeatureCollection) to be converted into a sf #' object. #' @param dsn Character. Output filename; in case \code{dsn} is missing -#' \code{ee_as_sf} will create a temporary file. +#' \code{ee_as_sf} will create a shapefile file in tmp() directory. #' @param crs Integer or character. coordinate reference system #' for the EE table. If is NULL, \code{ee_as_sf} will take the CRS of #' the first element. @@ -101,7 +101,7 @@ ee_as_sf <- function(x, sp_eeobjects <- ee_get_spatial_objects('Table') if (missing(dsn)) { - dsn <- paste0(tempfile(),".geojson") + dsn <- paste0(tempfile(),".shp") } if (!any(class(x) %in% sp_eeobjects)) { @@ -195,12 +195,21 @@ ee_as_sf <- function(x, file_name <- paste0(table_id, "_", time_format) # table to drive + table_format <- ee_get_table_format(dsn) + if (is.na(table_format)) { + stop( + 'sf_as_ee(..., via = \"drive\"), only support the ', + 'following output format: "CSV", "GeoJSON", "KML", "KMZ", "SHP"', + '. Use ee_table_to_drive and ee_drive_to_local to save in a TFRecord format.' + ) + } + table_task <- ee_table_to_drive( collection = x_fc, description = ee_description, folder = container, fileNamePrefix = file_name, - fileFormat = "GeoJSON", + fileFormat = table_format, selectors = selectors ) @@ -227,7 +236,12 @@ ee_as_sf <- function(x, overwrite = overwrite, consider = 'all' ) - local_sf <- sf::read_sf(dsn, quiet = TRUE) + + if (table_format == "CSV") { + return(read.csv(dsn, stringsAsFactors = FALSE)) + } else { + local_sf <- sf::read_sf(dsn, quiet = TRUE) + } } else if (via == 'gcs') { # Creating name for temporal file; just for either drive or gcs time_format <- format(Sys.time(), "%Y-%m-%d-%H:%M:%S") @@ -245,13 +259,22 @@ ee_as_sf <- function(x, file_name <- paste0(table_id, "_", time_format) - # table to drive + # table to gcs + table_format <- ee_get_table_format(dsn) + if (is.na(table_format)) { + stop( + 'sf_as_ee(..., via = \"gcs\"), only support the ', + 'following output format: "CSV", "GeoJSON", "KML", "KMZ", "SHP"', + '. Use ee_table_to_drive and ee_drive_to_local to save in a TFRecord format.' + ) + } + table_task <- ee_table_to_gcs( collection = x_fc, description = ee_description, bucket = container, fileNamePrefix = file_name, - fileFormat = "GeoJSON", + fileFormat = table_format, selectors = selectors ) @@ -271,7 +294,11 @@ ee_as_sf <- function(x, stop(table_task$status()$error_message) } ee_gcs_to_local(task = table_task,dsn = dsn, overwrite = overwrite) - local_sf <- sf::read_sf(dsn, quiet = TRUE) + if (table_format == "CSV") { + return(read.csv(dsn, stringsAsFactors = FALSE)) + } else { + local_sf <- sf::read_sf(dsn, quiet = TRUE) + } } else { stop("via argument invalid.") } @@ -313,7 +340,32 @@ ee_fc_to_sf_getInfo <- function(x_fc, dsn, maxFeatures, overwrite = TRUE) { if (missing(dsn)) { x_sf } else { - sf::write_sf(x_sf, dsn, delete_dsn = overwrite, quiet = TRUE) + suppressWarnings( + sf::write_sf(x_sf, dsn, delete_dsn = overwrite, quiet = TRUE) + ) x_sf } } + +#' Sync sf and ee drivers +#' @noRd +ee_get_table_format <- function(dsn) { + table_format <- tolower(sub(".*([.*])", "\\1", basename(dsn))) + if (length(table_format) != 1) { + stop("dns must be a single-length character") + } + + if (table_format == ".shp") { + "SHP" + } else if (table_format == ".geojson") { + "GeoJSON" + } else if (table_format == ".kml") { + "KML" + } else if (table_format == ".kmz") { + "KMZ" + } else if (table_format == ".csv") { + "CSV" + } else { + NA + } +} diff --git a/R/ee_help.R b/R/ee_help.R index 378dae18..801b2d35 100644 --- a/R/ee_help.R +++ b/R/ee_help.R @@ -30,55 +30,66 @@ ee_help <- function(eeobject, browser = FALSE) { } else { if (is.character(eeobject)) { fun_name <- eeobject + # fun_name has parenthesis + fun_name_d <- strsplit(fun_name, "\\$")[[1]] + exist_parenthesis <- grepl( + pattern = "(", + x = fun_name_d[length(fun_name_d)], + fixed = TRUE + ) } else { + exist_parenthesis <- FALSE wrap_lhs <- function(x) gsub("rgee", "", ee_get_lhs()) fun_name <- wrap_lhs(eeobject) if (length(fun_name) == 0) { fun_name <- deparse(substitute(eeobject)) } + } - if (is.null(eequery_scope)) { - components <- strsplit(fun_name, "\\$")[[1]] - topic <- components[[length(components)]] - source <- paste(components[1:(length(components) - 1)], - collapse = "$") - # The name is a base function? - is_a_basefunction <- tryCatch( - expr = {eval(parse(text = sprintf("base::%s", fun_name))); TRUE}, - error = function(e) FALSE + if (is.null(eequery_scope) | exist_parenthesis) { + components <- strsplit(fun_name, "\\$")[[1]] + topic <- components[[length(components)]] + source <- paste(components[1:(length(components) - 1)], + collapse = "$") + # The name is a base function? + is_a_basefunction <- tryCatch( + expr = {eval(parse(text = sprintf("base::%s", fun_name))); TRUE}, + error = function(e) FALSE + ) + if (isTRUE(is_a_basefunction)) { + stop( + "'", fun_name, "' is not subsettable. Are you using a ", + "function name that matches the names of the R base", + " library?. If 'base::", fun_name, "' exists ee_help will not work." ) - if (isTRUE(is_a_basefunction)) { - stop( - "'", fun_name, "' is not subsettable. Are you using a ", - "function name that matches the names of the R base", - " library?. If 'base::", fun_name, "' exists ee_help will not work." - ) - } - if (topic == source) { - fun_name <- topic - } else { - # Remove just the last parenthesis - extract_parenthesis_text <- gregexpr("(?=\\().*?(?<=\\))", - topic, - perl = TRUE) - parenthesis_text <- regmatches(topic, extract_parenthesis_text)[[1]] - to_display <- gsub(parenthesis_text, "", topic, fixed = TRUE) - to_display <- gsub("\\(|\\)", "", to_display) - fun_name <- paste(source,to_display,sep = "$") - } + } + if (topic == source) { + fun_name <- topic + } else { + # Remove just the last parenthesis + extract_parenthesis_text <- gregexpr("(?=\\().*?(?<=\\))", + topic, + perl = TRUE) + parenthesis_text <- regmatches(topic, extract_parenthesis_text)[[1]] + to_display <- gsub(parenthesis_text, "", topic, fixed = TRUE) + to_display <- gsub("\\(|\\)", "", to_display) + fun_name <- paste(source,to_display,sep = "$") } } } - doc_to_display <- tryCatch( - expr = fun_name %>% - paste(collapse = '') %>% - ee_function_docs, - error = function(e) ee_real_name(fun_name) %>% - paste(collapse = '') %>% - ee_function_docs - ) - + if (fun_name == "ee") { + doc_to_display <- ee_module_help() + } else { + doc_to_display <- tryCatch( + expr = fun_name %>% + paste(collapse = '') %>% + ee_function_docs, + error = function(e) ee_real_name(fun_name) %>% + paste(collapse = '') %>% + ee_function_docs + ) + } # Creating html to display temp_file <- sprintf("%s/ee_help.html", tempdir()) @@ -515,3 +526,19 @@ ee_help_create_arg <- function(function_docs) { return(list(arg = arguments_des, signature = signature_text)) } + +#' ee module help +#' @noRd +ee_module_help <- function() { + list( + name = "", + qualified_name = "ee", + description = "Interface to main Earth Engine module. Provides access to top level classes and functions as well as sub-modules (e.g. ee$Image, ee$FeatureCollection$first, etc.).", + details = "", + signature = "ee", + parameters = "", + sections = list(), + returns = NULL, + title = "Main Earth Engine module" + ) +} diff --git a/R/ee_image.R b/R/ee_image.R index c81e62ee..a1fae11a 100644 --- a/R/ee_image.R +++ b/R/ee_image.R @@ -310,7 +310,22 @@ ee_image_local <- function(image, # Default projection on an Image prj_image <- image$projection()$getInfo() - img_crs <- as.numeric(gsub("EPSG:", "", prj_image$crs)) + + if (grepl("EPSG:", prj_image$crs)) { + img_crs <- as.numeric(gsub("EPSG:", "", prj_image$crs)) + } else { + img_crs <- NA + # getInfo only supports images with EPSG CRS + if (is.na(img_crs) & via == "getInfo") { + stop( + "ee_imagecollection_to_local(..., via = \"getInfo\") only supports a ", + "SRC linked to an EPSG code. Change the SRC or use ", + " ee_imagecollection_to_local(..., via = \"drive\") to solve.\n", + " Entered -> ", prj_image$crs, "\n", + " Expected -> EPSG:4326 (or others)" + ) + } + } # From geometry to sf sf_region <- ee_as_sf(x = region)$geometry @@ -345,9 +360,9 @@ ee_image_local <- function(image, is_geodesic <- region$geodesic()$getInfo() #is evenodd? query_params <- unlist(jsonlite::parse_json(region$serialize())$scope) - is_evenodd <- as.logical( + is_evenodd <- all(as.logical( query_params[grepl("evenOdd", names(query_params))] - ) + )) if (length(is_evenodd) == 0 | is.null(is_evenodd)) { is_evenodd <- TRUE } diff --git a/R/ee_imagecollection.R b/R/ee_imagecollection.R index a3e081a1..540eedc1 100644 --- a/R/ee_imagecollection.R +++ b/R/ee_imagecollection.R @@ -180,9 +180,9 @@ ee_geometry_message <- function(region, quiet = FALSE) { is_geodesic <- region$geodesic()$getInfo() #is evenodd? query_params <- unlist(jsonlite::parse_json(region$serialize())$scope) - is_evenodd <- as.logical( + is_evenodd <- all(as.logical( query_params[grepl("evenOdd", names(query_params))] - ) + )) if (length(is_evenodd) == 0 | is.null(is_evenodd)) { is_evenodd <- TRUE } @@ -194,7 +194,7 @@ ee_geometry_message <- function(region, quiet = FALSE) { '- region parameters\n', 'WKT :', sf::st_as_text(sf_region), "\n", 'CRS :', region_crs, "\n", - 'geodesic :', is_geodesic, "\n", + 'geodesic :', ee_utils_py_to_r(is_geodesic), "\n", 'evenOdd :', is_evenodd, "\n" ) } diff --git a/inst/rstudio/addins.dcf b/inst/rstudio/addins.dcf new file mode 100644 index 00000000..cdd79414 --- /dev/null +++ b/inst/rstudio/addins.dcf @@ -0,0 +1,4 @@ +Name: ee_help +Description: Return documentation of Earth Engine modules, methods and classes +Binding: ee_help_addins +Interactive: false diff --git a/man/ee_as_sf.Rd b/man/ee_as_sf.Rd index fc21db16..1ec4e4f8 100644 --- a/man/ee_as_sf.Rd +++ b/man/ee_as_sf.Rd @@ -21,7 +21,7 @@ ee_as_sf( object.} \item{dsn}{Character. Output filename; in case \code{dsn} is missing -\code{ee_as_sf} will create a temporary file.} +\code{ee_as_sf} will create a shapefile file in tmp() directory.} \item{overwrite}{Logical. Delete data source \code{dsn} before attempting to write?.}