From 2ec452ed40b915a665bbb32246688e7e7fb35d1d Mon Sep 17 00:00:00 2001 From: Martin Jung Date: Sat, 19 Oct 2024 00:29:55 +0200 Subject: [PATCH] Typos fix and export tests --- DESCRIPTION | 1 + NEWS.md | 1 + R/app_server.R | 4 +-- R/misc.R | 56 +++++++++++++++++++++++++++++++++++++++ R/mod_Export.R | 50 +++++++++++++++++++++++++++++++--- R/mod_Overview.R | 23 +++++++++------- R/utils_format_protocol.R | 40 ++++++++++++++++++---------- inst/01_protocol.yaml | 44 +++++++++++++++++++++++------- 8 files changed, 181 insertions(+), 38 deletions(-) create mode 100644 R/misc.R diff --git a/DESCRIPTION b/DESCRIPTION index fa3025c..cebc51f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -37,6 +37,7 @@ Imports: tools, utils, waiter, + zip, yaml Suggests: spelling, diff --git a/NEWS.md b/NEWS.md index d5a496f..7ccb446 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ ## Version 0.4 +* Fixed several typos ✍️. * Updated with further description and suggested edits by experts. * Updated and shortened preface description page. diff --git a/R/app_server.R b/R/app_server.R index 6fce55b..003860e 100644 --- a/R/app_server.R +++ b/R/app_server.R @@ -14,8 +14,8 @@ app_server <- function(input, output, session) { shiny::observeEvent(input$bookmark, { # session$doBookmark() # Use manuall bookmarking instead owing to the complexity - shiny::showNotification("Export the current protocol as yaml. Then import later...", - duration = 5,closeButton = TRUE, type = "message") + shiny::showNotification("Save the current protocol as csv or yaml. Then import later...", + duration = 5, closeButton = TRUE, type = "message") bs4Dash::updateTabItems(session, inputId = "sidebarmenu", selected = "Export") }) diff --git a/R/misc.R b/R/misc.R new file mode 100644 index 0000000..97e2328 --- /dev/null +++ b/R/misc.R @@ -0,0 +1,56 @@ +#' Parse spatial data and convert to [`sf`] format +#' +#' @description +#' This is a small helper function that converts any type of spatial +#' format to a [`sf`] object. +#' @param file A [`character`] file path +#' @param make_valid A [`logical`] on whether \code{'file'} should be ensured to +#' be a valid geometry (Default: \code{FALSE}). +#' @keywords internal +#' @noRd +spatial_to_sf <- function(file, make_valid = FALSE){ + assertthat::assert_that(is.character(file), + is.logical(make_valid)) + + # Get file extension + ext <- tolower( tools::file_ext(file) ) + + # Found vector + if(ext %in% c("shp","gpkg")){ + out <- sf::st_read(file, quiet = TRUE) + } else if( ext %in% c("tif","geotiff")){ + out <- terra::rast(file) + out[out>0] <- 1 # Replace all with 1 + # If there are 0 assume those should be NA + out[out==0] <- NA + out <- out |> terra::as.polygons() |> sf::st_as_sf() + } else { + return( NULL ) + } + # --- # + # Check for empty crs + if(is.na(sf::st_crs(out))){ + # Assume long-lat + out <- sf::st_set_crs(out, value = sf::st_crs(4326)) + } + + # Check for empty geometries + if(all( sf::st_is_empty(out) )) return( NULL ) + + if(make_valid){ + if(!all( sf::st_is_valid(out) )){ + out <- sf::st_make_valid(out) + } + } + + # Convert to MULTIPOLYGON + out <- out |> sf::st_cast("MULTIPOLYGON") + + # Transform + out <- out |> sf::st_transform(crs = sf::st_crs(4326)) + + # Rename geometry name to be sure + sf::st_geometry(out) <- "geometry" # rename + + return(out) +} diff --git a/R/mod_Export.R b/R/mod_Export.R index b7c62e2..269296f 100644 --- a/R/mod_Export.R +++ b/R/mod_Export.R @@ -44,7 +44,7 @@ mod_Export_ui <- function(id){ size = "lg", status = "info", choices = c('docx', 'pdf', 'csv', 'yaml'), - selected = 'yaml', + selected = 'csv', checkIcon = list( yes = shiny::icon("circle-down"), no = NULL @@ -95,7 +95,13 @@ mod_Export_ui <- function(id){ shiny::textOutput(outputId = ns("missingtext"))), shiny::br(), # Button - shiny::downloadButton(ns("downloadData"), "Download the protocol") + shiny::p( + shiny::downloadButton(ns("downloadData"), "Download the selection option", + class = "btn-primary"), + shiny::downloadButton(ns("downloadEverything"), "Download everything", + icon = shiny::icon("file-zipper"), + class = "btn-secondary") + ) ) ), shiny::tabPanel( @@ -176,7 +182,7 @@ mod_Export_server <- function(id, results){ } else if(oftype() == "docx"){ # Create document from results, everything handled by function protocol <- format_protocol(results, format = "list") - # saveRDS(protocol, "test.rds") + # saveRDS(results, "test.rds") protocol_to_document(protocol, file = file, format = "docx") } else if(oftype() == "pdf"){ # Create document from results @@ -186,6 +192,44 @@ mod_Export_server <- function(id, results){ } ) + # Download everything button + output$downloadEverything <- shiny::downloadHandler( + filename = function() { + # Compose output file + paste0( + "ODPSCP__", + format(Sys.Date(), "%Y_%m_%d"), + ".zip" + ) + }, + content = function(file) { + # Create outputs from results + shiny::showNotification("Preparing zipped outputs which can take a little while...", + duration = 3, type = "message") + # First csv + protocol <- format_protocol(results, format = "data.frame") + ofname1 <- file.path(tempdir(), "ODPSCP_protocol.csv") + readr::write_csv(protocol, file = ofname1) + + # Create document from results, everything handled by function + protocol <- format_protocol(results, format = "list") + ofname2 <- file.path(tempdir(), "ODPSCP_protocol.docx") + protocol_to_document(protocol, file = ofname2, + format = "docx") + + # Create PDF document from results + protocol <- format_protocol(results, format = "list") + ofname3 <- file.path(tempdir(), "ODPSCP_protocol.pdf") + protocol_to_document(protocol, file = ofname3, + format = "pdf") + + # Zip everything together + zip::zip(file, + files = c(ofname1, ofname2, ofname3), + mode = "cherry-pick") + } + ) + }) } diff --git a/R/mod_Overview.R b/R/mod_Overview.R index 970f344..d5ab034 100644 --- a/R/mod_Overview.R +++ b/R/mod_Overview.R @@ -63,6 +63,8 @@ mod_Overview_ui <- function(id){ collapsible = TRUE, shiny::p("Add each author of the study to the table below. If a ORCID is not known or available, leave blank."), + shiny::helpText("If the number of authors is extensive it might also be ok to + simply add the lead author's name."), DT::DTOutput(outputId = ns("authors_table")), shiny::actionButton(inputId = ns("add_author"), label = "Add a new author row", icon = shiny::icon("plus")), @@ -507,16 +509,17 @@ mod_Overview_server <- function(id, results, parentsession){ shiny::req(file) if(!is.null(input$studyregion)){ - # Found vector - if(tolower( tools::file_ext(file)) %in% c("shp","gpkg")){ - out <- sf::st_read(file, quiet = TRUE) |> - sf::st_transform(crs = sf::st_crs(4326)) - } else if(tolower( tools::file_ext(file)) %in% c("tif","geotiff")){ - out <- terra::rast(file) - out[out>0] <- 1 # Replace all with 1 - out <- out |> terra::as.polygons() |> sf::st_as_sf() |> - sf::st_cast("MULTIPOLYGON") |> - sf::st_transform(crs = sf::st_crs(4326)) + # Check file size in MB + ss <- (file.size(file) / 1048576) + if(ss > 3){ + shiny::showNotification("Uploaded file over 3 MB. Loading can take a while...", + duration = 5, type = "message") + } + # Load spatial file + out <- spatial_to_sf(file, make_valid = FALSE) + if(is.null(out)){ + shiny::showNotification("Layer could not be loaded!", + duration = 2, type = "warning") } return(out) } else { diff --git a/R/utils_format_protocol.R b/R/utils_format_protocol.R index 90ee5bb..ff8c695 100644 --- a/R/utils_format_protocol.R +++ b/R/utils_format_protocol.R @@ -80,22 +80,22 @@ format_protocol <- function(results, format = "data.frame", #' Small helper for spatial conversion to wkt #' @param val A [`list`] with the datapath for the spatial file +#' @return A [`character`] with a WKT. #' @noRd format_studyregion_to_text <- function(val){ assertthat::assert_that(utils::hasName(val, "datapath")) - # Assume as sf and load as such + # Load from data path val <- try({ - sf::st_as_sfc( - sf::st_read(val$datapath,quiet = TRUE) - ) - },silent = TRUE) + spatial_to_sf(val$datapath, make_valid = FALSE) + }, silent = TRUE) if(inherits(val,"try-error")){ val <- "Studyregion could not be loaded?" } else { + # Convert to sfc + val <- val |> sf::st_as_sfc() val <- paste0( # Also append SRID in front - sf::st_crs(val) |> sf::st_as_text(),";", - sf::st_as_text(val) + sf::st_crs(val) |> sf::st_as_text(),";", sf::st_as_text(val) ) } return(val) @@ -230,13 +230,25 @@ protocol_to_document <- function(results, file, format = "docx", path_protocol = if(el == "studyregion"){ # Render studyregion sp <- strsplit(res,";") # Split SRID off - sp <- sp[[1]][2] |> sf::st_as_sfc() |> sf::st_sf(crs = sp[[1]][1]) - gg <- ggplot2::ggplot() + - ggplot2::geom_sf(data = sp) + - ggplot2::labs(title = "Outline of studyregion") - # Add to document - doc <- doc |> officer::body_add_gg(value = gg) - try({ rm(gg, sp) },silent = TRUE) + # Catch error in case region could not be loaded + if(is.na(sp[[1]][2])){ + # Add to body + fpar <- officer::fpar( + officer::ftext(text = sp[[1]][1], + prop = officer::fp_text(font.size = 12,italic = FALSE)) + ) + doc <- doc |> officer::body_add_fpar(value = fpar) + } else { + # Correctly parsed geometry + sp <- sp[[1]][2] |> sf::st_as_sfc() |> sf::st_sf(crs = sp[[1]][1]) + gg <- ggplot2::ggplot() + + ggplot2::geom_sf(data = sp) + + ggplot2::labs(title = "Outline of studyregion") + # Add to document + doc <- doc |> officer::body_add_gg(value = gg) + try({ rm(gg)},silent = TRUE) + } + try({ rm(sp) },silent = TRUE) } else if(el %in% c("authors_table","featurelist", "evalidentification","specificzones")) { diff --git a/inst/01_protocol.yaml b/inst/01_protocol.yaml index 5e93bcf..035028f 100644 --- a/inst/01_protocol.yaml +++ b/inst/01_protocol.yaml @@ -73,6 +73,7 @@ overview: fieldtype: 'fileupload' fieldtype_conditional_render-id: 'studymap' fieldtype_conditional: 'leaflet' + fieldtype_conditional_description: '' mandatory: false popexample: "Upload for example a shp file of the study area." @@ -96,6 +97,7 @@ overview: fieldtype: 'slider' fieldtype_conditional_render-id: 'otherstudytime' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Additional detail related to the temporal range of the study' mandatory: true popexample: "The study uses data from the period 2015-2020 in order to make recommendations for potential protected area network in 2030. The time period is thus 2015-2030." @@ -127,6 +129,7 @@ overview: fieldtype: 'radio' fieldtype_conditional_render-id: 'inputdata' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Link to the data storage repository.' mandatory: true popexample: "Data are available from the original data providers here:" @@ -139,10 +142,11 @@ overview: fieldtype: 'radio' fieldtype_conditional_render-id: 'outputdata' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Link to the data storage repository.' mandatory: true popexample: "A link to a permanent data repository, such as Zenodo, Figshare or similar." - reproducibility: + code_availability: render-nr: 2 render-id: 'codeavailability' render-group: 'data_availability' @@ -151,6 +155,7 @@ overview: fieldtype: 'radio' fieldtype_conditional_render-id: 'outputcode' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Link to the code storage repository.' mandatory: true popexample: "The analytical code for preparing input data and running the prioritization has been made available as supporting material and under the folling link: " @@ -161,7 +166,7 @@ design: render-id: 'studyaim' render-group: 'study_design' question: 'What is the aim of the study?' - description: 'Desribe in 1-2 sentences what the study aims to achieve?' + description: 'Describe in 1-2 sentences what the study aims to achieve.' fieldtype: 'textbox' mandatory: true popexample: "Identification of joint priorities, location of new protected areas or management actions." @@ -179,6 +184,7 @@ design: - Reference fieldtype_conditional_render-id: 'frameworkreference' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Clarification and link to the used framework.' mandatory: false popexample: "A framework outlining the decision making process and theory is described in the publication." @@ -191,6 +197,7 @@ design: fieldtype: 'radio' fieldtype_conditional_render-id: 'theoryofchange_text' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Clarification with regards to the theory of change.' mandatory: false popexample: "Highlight an initial policy or stakeholder demand, continuous engagement and clear process towards implementation." @@ -223,6 +230,7 @@ design: fieldtype: 'radio' fieldtype_conditional_render-id: 'multobj' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Clarification how multiple objectives were considered.' mandatory: true popexample: "Joint priorities for both climate mitigation and biodiversity preservation were identified." @@ -235,6 +243,7 @@ design: fieldtype: 'checkbox' fieldtype_conditional_render-id: 'planningscenarios' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Description of the used scenarios.' mandatory: false popexample: "One planning outcome weighted towards biodiversity and another towards ecosystem services. Or SSP1-2.6 and SSP5-8.5, or similar" @@ -247,6 +256,7 @@ design: fieldtype: 'radio' fieldtype_conditional_render-id: 'stakeholderint' fieldtype_conditional: 'dropdown' + fieldtype_conditional_description: 'Multiple choice selection how exactly stakeholder were engaged.' conditional_options: - Informed - Consulted @@ -305,6 +315,7 @@ specification: - Other fieldtype_conditional_render-id: 'othertypes' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Description of any other type of planning unit.' mandatory: true popexample: "Regular gridded planning units were used." @@ -315,16 +326,17 @@ specification: question: 'What was the spatial grain of planning?' description: 'Describe the grain of planning units if applicable.' fieldtype: 'numeric' - fieldtype_conditional_render-id: 'pu_grainunit' + fieldtype_conditional_render-id: 'pu_grainother' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Any other description of how the grain was described?' mandatory: false popexample: "Each planning unit had a size of 1000 m²." - grainspace_other: + grainspace_unit: render-nr: 3 - render-id: 'pu_grainother' + render-id: 'pu_grainunit' render-group: 'planningunits' - question: 'Any other description with regards to spatial grain of PU?' + question: 'Select or enter unit used for the spatial grain.' description: 'Leave empty if not applicable.' fieldtype: 'selectizedropdown' options: @@ -343,7 +355,7 @@ specification: render-id: 'pu_checkcosts' render-group: 'planningunits' question: 'Where there any costs of selecting a planning unit? Select one or multiple.' - description: 'Describe the grain of planning units if applicable.' + description: 'Where there any costs or penalities for selecting a planning unit? Select one or multiple.' fieldtype: 'multiplechoice' options: - Area only @@ -358,6 +370,7 @@ specification: - Other fieldtype_conditional_render-id: 'pu_costs' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Describe the process of costs were created or used.' mandatory: true popexample: "No specific cost estimates were used and all PU had equal cost (Area only)." @@ -370,6 +383,7 @@ specification: fieldtype: 'radio' fieldtype_conditional_render-id: 'specificecosystem' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Describe exactly how specific ecosystems were used.' mandatory: false popexample: "Study was conducted not on the whole landscape, but rather prioritizing only forest and features patches within them." @@ -382,6 +396,7 @@ specification: fieldtype: 'radio' fieldtype_conditional_render-id: 'specificzones' fieldtype_conditional: 'multifieldselection' + fieldtype_conditional_description: 'Describe exactly how (management) zones were used, including contributions or exclusions.' mandatory: false popexample: "Identify a set of no-take and partial-take areas to prevent overfishing, but also ensure that there still remain plenty of areas for fishing activities." @@ -398,6 +413,7 @@ specification: - Areas or Action excluded fieldtype_conditional_render-id: 'defaultareas' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Describe which areas were included/excluded and why.' mandatory: true popexample: "Existing protected areas were considered to be part of every solution (included)." @@ -424,6 +440,7 @@ specification: - Other fieldtype_conditional_render-id: 'otherthreattype' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Describe the use of any other threat type.' mandatory: true popexample: "Select any type of threat considered in this work." @@ -446,6 +463,7 @@ specification: - Other fieldtype_conditional_render-id: 'threatdetail' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Describe the use of other ways threats were considered in the planning.' mandatory: false popexample: "Anthropogenic land-uses were considered as a selection penalty for avoiding high impact regions." @@ -472,6 +490,7 @@ specification: - Other fieldtype_conditional_render-id: 'otherfeaturetype' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Describe any other not listed feature types.' mandatory: true popexample: "The study used exclusively distribution estimates of species." @@ -484,6 +503,7 @@ specification: fieldtype: 'checkbox' fieldtype_conditional_render-id: 'featureaggregated' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Describe which features were aggregated, why and how.' mandatory: false popexample: "Species distributions were aggregated to a species richness layer, that was then included as a feature." @@ -494,7 +514,7 @@ specification: question: 'List the features' description: 'Add the features to the list.' fieldtype: 'multifieldselection' - mandatory: false + mandatory: true popexample: "Threatened plant species, Species (distributions), 260" featureorigin: @@ -549,6 +569,7 @@ context: fieldtype: 'radio' fieldtype_conditional_render-id: 'connectivityplan' fieldtype_conditional: 'selectizedropdown' + fieldtype_conditional_description: 'Select what type of connectivity was considered specifically.' conditional_options: - Boundary penalty - Neighbour constraint @@ -594,6 +615,7 @@ context: - Other fieldtype_conditional_render-id: 'targetdetail' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Explain how targets for features were estimated.' mandatory: true popexample: "A flat target of at least 10% of each feature was considered necessary to be in the solution." @@ -606,6 +628,7 @@ context: fieldtype: 'checkbox' fieldtype_conditional_render-id: 'featureweightsdetails' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Explain how feature weights have been defined or set.' mandatory: true popexample: "Threatened species received a weight five-times as high as non-threatend species." @@ -632,6 +655,7 @@ prioritization: - Other fieldtype_conditional_render-id: 'othersoftware' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Specify details for any other software used for planning.' mandatory: true popexample: "The software prioritizr was used for identifying a solution." @@ -681,6 +705,7 @@ prioritization: - Other fieldtype_conditional_render-id: 'otheridentification' fieldtype_conditional: 'textbox' + fieldtype_conditional_description: 'Explain how final solutions were obtained.' mandatory: true popexample: "The final priorities are identified by overlaying individual optimization results with priority areas identified by stakeholders." @@ -688,12 +713,13 @@ prioritization: render-nr: 1 render-id: 'checkperformance' render-group: 'perfidenticators' - question: 'Was the performance of the study in anyway evaluated?' + question: 'Was the performance of the study in any way evaluated?' description: 'Most studies describe their outputs in terms of what is gained by the solutions.' fieldtype: 'radio' mandatory: true fieldtype_conditional_render-id: 'evalidentification' fieldtype_conditional: 'multifieldselection' + fieldtype_conditional_description: 'Explain how the goodness of the planning was evaluated.' popexample: "Common examples are the compactness of the solutions, average number of targets achieved, avoided costs or impacts." otherperformance: