diff --git a/DESCRIPTION b/DESCRIPTION index bdcc74b..7b39bcd 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -23,7 +23,7 @@ BugReports: https://github.com/SpatialPlanning/spatialplanr/issues Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.2 +RoxygenNote: 7.3.3 Suggests: gfwr, ggcorrplot, @@ -32,15 +32,12 @@ Suggests: knitr, oceandatr, patchwork, - prioritizr, RColorBrewer, rmarkdown, rnaturalearthdata, rnaturalearthhires, rredlist, testthat (>= 3.0.0), - units, - vctrs, viridis, wdpar VignetteBuilder: knitr @@ -54,7 +51,7 @@ Imports: ggplot2, lifecycle, magrittr, - methods, + prioritizr, purrr, rappdirs, rlang, @@ -66,7 +63,9 @@ Imports: terra, tibble, tidyr, - tidyselect + tidyselect, + units, + vctrs Remotes: github::emlab-ucsb/oceandatr, github::emlab-ucsb/spatialgridr, diff --git a/NAMESPACE b/NAMESPACE index bf0d116..71160bc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -40,7 +40,145 @@ export(splnr_scale_01) export(splnr_targets_byCategory) export(splnr_targets_byIUCN) export(splnr_targets_byInverseArea) +importFrom(assertthat,assert_that) +importFrom(assertthat,is.flag) +importFrom(assertthat,is.string) +importFrom(dplyr,across) +importFrom(dplyr,any_of) +importFrom(dplyr,arrange) +importFrom(dplyr,as_tibble) +importFrom(dplyr,bind_cols) +importFrom(dplyr,bind_rows) +importFrom(dplyr,case_when) +importFrom(dplyr,coalesce) +importFrom(dplyr,everything) +importFrom(dplyr,filter) +importFrom(dplyr,full_join) +importFrom(dplyr,group_by) +importFrom(dplyr,if_else) +importFrom(dplyr,left_join) +importFrom(dplyr,mutate) +importFrom(dplyr,pull) +importFrom(dplyr,rename) +importFrom(dplyr,rename_at) +importFrom(dplyr,rowwise) +importFrom(dplyr,select) +importFrom(dplyr,starts_with) +importFrom(dplyr,summarise) +importFrom(dplyr,summarize) +importFrom(dplyr,tibble) +importFrom(dplyr,ungroup) +importFrom(forcats,fct_relevel) +importFrom(ggnewscale,new_scale_colour) +importFrom(ggnewscale,new_scale_fill) +importFrom(ggplot2,aes) +importFrom(ggplot2,after_stat) +importFrom(ggplot2,annotate) +importFrom(ggplot2,coord_polar) +importFrom(ggplot2,coord_sf) +importFrom(ggplot2,element_blank) +importFrom(ggplot2,element_line) +importFrom(ggplot2,element_rect) +importFrom(ggplot2,element_text) +importFrom(ggplot2,expansion) +importFrom(ggplot2,geom_abline) +importFrom(ggplot2,geom_bar) +importFrom(ggplot2,geom_segment) +importFrom(ggplot2,geom_sf) +importFrom(ggplot2,geom_text) +importFrom(ggplot2,ggplot) +importFrom(ggplot2,guide_axis) +importFrom(ggplot2,guide_colourbar) +importFrom(ggplot2,guide_legend) +importFrom(ggplot2,guides) +importFrom(ggplot2,labs) +importFrom(ggplot2,scale_fill_brewer) +importFrom(ggplot2,scale_fill_distiller) +importFrom(ggplot2,scale_fill_gradient) +importFrom(ggplot2,scale_fill_gradient2) +importFrom(ggplot2,scale_fill_manual) +importFrom(ggplot2,scale_fill_viridis_c) +importFrom(ggplot2,scale_linetype_manual) +importFrom(ggplot2,scale_x_continuous) +importFrom(ggplot2,scale_x_discrete) +importFrom(ggplot2,scale_y_continuous) +importFrom(ggplot2,scale_y_discrete) +importFrom(ggplot2,theme) +importFrom(ggplot2,theme_bw) +importFrom(ggplot2,theme_minimal) +importFrom(ggplot2,unit) +importFrom(ggplot2,ylim) +importFrom(grid,unit) +importFrom(lifecycle,deprecate_warn) importFrom(lifecycle,deprecated) importFrom(magrittr,"%>%") +importFrom(prioritizr,add_binary_decisions) +importFrom(prioritizr,add_cuts_portfolio) +importFrom(prioritizr,add_default_solver) +importFrom(prioritizr,add_min_set_objective) +importFrom(prioritizr,add_relative_targets) +importFrom(prioritizr,eval_feature_representation_summary) +importFrom(prioritizr,eval_ferrier_importance) +importFrom(prioritizr,eval_rare_richness_importance) +importFrom(prioritizr,eval_replacement_importance) +importFrom(prioritizr,problem) +importFrom(prioritizr,solve.ConservationProblem) +importFrom(prioritizr,zones) +importFrom(purrr,map) +importFrom(purrr,map_df) +importFrom(purrr,map_vec) +importFrom(rappdirs,user_data_dir) importFrom(rlang,":=") importFrom(rlang,.data) +importFrom(rlang,sym) +importFrom(rnaturalearth,ne_coastline) +importFrom(rnaturalearth,ne_download) +importFrom(scales,squish) +importFrom(sf,st_area) +importFrom(sf,st_as_sf) +importFrom(sf,st_bbox) +importFrom(sf,st_centroid) +importFrom(sf,st_coordinates) +importFrom(sf,st_crs) +importFrom(sf,st_distance) +importFrom(sf,st_drop_geometry) +importFrom(sf,st_geometry) +importFrom(sf,st_join) +importFrom(sf,st_nearest_feature) +importFrom(sf,st_polygon) +importFrom(sf,st_set_crs) +importFrom(sf,st_set_geometry) +importFrom(sf,st_sf) +importFrom(sf,st_sfc) +importFrom(sf,st_transform) +importFrom(sf,st_union) +importFrom(spatialgridr,get_data_in_grid) +importFrom(stats,na.omit) +importFrom(stats,quantile) +importFrom(stats,reorder) +importFrom(stats,setNames) +importFrom(stringr,str_c) +importFrom(stringr,str_ends) +importFrom(stringr,str_pad) +importFrom(stringr,str_remove_all) +importFrom(stringr,str_replace_all) +importFrom(stringr,str_sub) +importFrom(stringr,str_subset) +importFrom(tibble,as_tibble) +importFrom(tibble,deframe) +importFrom(tibble,enframe) +importFrom(tibble,rowid_to_column) +importFrom(tibble,tibble) +importFrom(tibble,tribble) +importFrom(tidyr,pivot_longer) +importFrom(tidyr,pivot_wider) +importFrom(tidyr,replace_na) +importFrom(tidyselect,all_of) +importFrom(tidyselect,any_of) +importFrom(tidyselect,everything) +importFrom(tidyselect,matches) +importFrom(tidyselect,starts_with) +importFrom(tidyselect,where) +importFrom(units,drop_units) +importFrom(units,set_units) +importFrom(vctrs,vec_c) diff --git a/R/splnr_apply_cutoffs.R b/R/splnr_apply_cutoffs.R index 910aaca..fc03737 100644 --- a/R/splnr_apply_cutoffs.R +++ b/R/splnr_apply_cutoffs.R @@ -1,58 +1,212 @@ -#' Function to apply cutoffs to feature data +#' @title Apply Cutoffs to Feature Data #' -#' @param features A sf dataframe with all the feature information -#' @param Cutoffs A single value or a named vector of cutoffs. -#' @param inverse If TRUE, values below the `Cutoffs` are used. If FALSE (default), values above are kept. +#' @description +#' `splnr_apply_cutoffs()` transforms numeric feature data in an `sf` dataframe +#' into binary (0 or 1) presence/absence values based on specified cutoffs. +#' It provides flexibility to either keep values above a cutoff as 1 (default) +#' or invert this logic to keep values below a cutoff as 1. #' -#' @return A new sf dataframe that has cutoffs applied. +#' @details +#' This function is crucial for standardizing feature data, such as species +#' probability distributions or habitat suitability scores, into a binary format +#' often required for conservation planning and spatial analysis (e.g., in +#' `prioritizr`). +#' +#' The function operates in two primary modes based on the `Cutoffs` parameter: +#' \itemize{ +#' \item \strong{Single Cutoff:} If `Cutoffs` is a single numeric value (e.g., `0.5`), +#' this value is applied uniformly to \strong{all numeric columns} in the +#' `features` dataframe, excluding the `geometry` column. +#' For each numeric cell: +#' - If `value >= Cutoffs`, it becomes `1`. +#' - If `value < Cutoffs`, it becomes `0`. +#' - `NA` values are always converted to `0`. +#' \item \strong{Named Vector of Cutoffs:} If `Cutoffs` is a named numeric vector +#' (e.g., `c("feature1" = 0.5, "feature2" = 0.3)`), each specified cutoff +#' is applied individually to its corresponding named column in `features`. +#' This allows for different thresholds for different features. The same +#' transformation rules as above apply to each specified column. +#' } +#' +#' The `inverse` parameter provides additional control over the binarization: +#' \itemize{ +#' \item `inverse = FALSE` (default): Values \strong{at or above} the cutoff become `1`. +#' \item `inverse = TRUE`: Values \strong{below} the cutoff become `1`. After initial +#' binarization (where values >= cutoff are 1), the binary results are +#' flipped (0s become 1s, and 1s become 0s) to achieve the inverse effect. +#' } +#' All `NA` values in the numeric columns are consistently converted to `0` during +#' the binarization process, regardless of the `inverse` setting. +#' +#' @param features An `sf` dataframe. It must contain a `geometry` column and +#' at least one numeric column to which cutoffs will be applied. +#' @param Cutoffs A numeric value or a named numeric vector of cutoffs. +#' \itemize{ +#' \item If a single unnamed numeric value, it's applied to all numeric columns. +#' \item If a named numeric vector, names must correspond to numeric column names in `features`. +#' } +#' All cutoff values must be between `0` and `1`. +#' @param inverse A logical value (`TRUE` or `FALSE`). If `TRUE`, values below +#' the `Cutoffs` are converted to `1` (and others to `0`). If `FALSE` (default), +#' values at or above the `Cutoffs` are converted to `1`. +#' +#' @return A modified `sf` dataframe with the same structure and geometry as +#' `features`, but with all targeted numeric columns transformed into binary +#' (0 or 1) values based on the specified cutoffs and `inverse` setting. #' @export #' -#' @importFrom rlang .data +#' @importFrom assertthat assert_that +#' @importFrom dplyr across case_when mutate any_of +#' @importFrom rlang .data sym := +#' @importFrom sf st_as_sf st_geometry +#' @importFrom tibble as_tibble #' #' @examples -#' df <- splnr_apply_cutoffs(dat_species_prob, Cutoffs = 0.5) +#' +#' # Example 1: Single cutoff (0.5) applied to all numeric feature columns +#' # (Spp1_Prob, Spp2_Prob, and Cost will be binarized based on 0.5) +#' df_single_cutoff <- splnr_apply_cutoffs(dat_species_prob, Cutoffs = 0.5) +#' print(df_single_cutoff) +#' +#' # Example 2: Named cutoffs for specific columns +#' # Spp1_Prob >= 0.6 becomes 1, Spp2_Prob >= 0.4 becomes 1 +#' df_named_cutoffs <- splnr_apply_cutoffs( +#' dat_species_prob, +#' Cutoffs = c("Spp1" = 0.6, "Spp2" = 0.4) +#' ) +#' print(df_named_cutoffs) +#' +#' # Example 3: Single cutoff (0.5) with inverse logic +#' # Values BELOW 0.5 become 1. +#' df_inverse_cutoff <- splnr_apply_cutoffs(dat_species_prob, Cutoffs = 0.5, inverse = TRUE) +#' print(df_inverse_cutoff) +#' +#' # Example 4: Named cutoffs with inverse logic +#' df_named_inverse <- splnr_apply_cutoffs( +#' dat_species_prob, +#' Cutoffs = c("Spp1" = 0.7, "Spp2" = 0.3), +#' inverse = TRUE +#' ) +#' print(df_named_inverse) splnr_apply_cutoffs <- function(features, Cutoffs, inverse = FALSE) { + + # --- Input Assertions --- + # Ensure 'features' is an sf object. assertthat::assert_that( inherits(features, "sf"), - is.numeric(Cutoffs) | (is.numeric(Cutoffs) & length(names(Cutoffs)) > 0), - is.logical(inverse) + msg = "Input 'features' must be an 'sf' object." + ) + # Ensure 'Cutoffs' is numeric. + assertthat::assert_that( + is.numeric(Cutoffs), + msg = "'Cutoffs' must be a numeric value or a named numeric vector." + ) + # Ensure 'inverse' is a single logical value. + assertthat::assert_that( + is.logical(inverse) && length(inverse) == 1, + msg = "'inverse' must be a single logical value (TRUE or FALSE)." + ) + # Ensure all cutoff values are between 0 and 1. + assertthat::assert_that( + all(Cutoffs >= 0) && all(Cutoffs <= 1), + msg = "All 'Cutoffs' values must be between 0 and 1 (inclusive)." + ) + # Ensure 'features' dataframe contains a 'geometry' column. + assertthat::assert_that( + "geometry" %in% names(features), + msg = "The 'features' dataframe must contain a 'geometry' column." + ) + # Ensure the features dataframe is not empty. + assertthat::assert_that( + nrow(features) > 0, + msg = "The 'features' dataframe must not be empty." ) - if (length(Cutoffs) == 1 & length(names(Cutoffs)) == 0) { # Single cutoff for all data if unnamed vector + # Get the names of all numeric columns, excluding the geometry column. + # This ensures that only relevant feature data columns are processed. + numeric_cols <- names(features)[sapply(features, is.numeric) & names(features) != "geometry"] - features <- features %>% - dplyr::as_tibble() %>% + # Assert that there are numeric columns to operate on. + assertthat::assert_that( + length(numeric_cols) > 0, + msg = "No numeric columns found in 'features' excluding geometry. Nothing to apply cutoffs to." + ) + + # Check if 'Cutoffs' is a named vector, and if so, validate that its names + # correspond to existing numeric feature columns. + if (!is.null(names(Cutoffs))) { + assertthat::assert_that( + all(names(Cutoffs) %in% numeric_cols), + msg = "When 'Cutoffs' is a named vector, all names must match existing numeric feature column names in 'features'." + ) + } + + # Convert sf object to tibble for data manipulation, then back to sf at the end. + # This can sometimes prevent unexpected behavior with sf objects during extensive dplyr operations. + features_as_tibble <- features %>% + tibble::as_tibble() + + # --- Apply Cutoffs Logic --- + + # Case 1: Single cutoff value (unnamed vector of length 1). + if (length(Cutoffs) == 1 && is.null(names(Cutoffs))) { + message(paste0("Applying single cutoff of ", Cutoffs, " to all numeric feature columns.")) + + # Apply the single cutoff to all identified numeric columns. + # Values greater than or equal to the cutoff become 1, others become 0. + # NAs are explicitly converted to 0. + features_as_tibble <- features_as_tibble %>% dplyr::mutate(dplyr::across( - -dplyr::any_of("geometry"), # Apply to all columns except geometry + dplyr::all_of(numeric_cols), # Apply only to the identified numeric columns. ~ dplyr::case_when( . >= Cutoffs ~ 1, . < Cutoffs ~ 0, - is.na(.data) ~ 0 + is.na(.) ~ 0 # Handle NAs by converting them to 0. ) - )) %>% - sf::st_as_sf() + )) - if (inverse == TRUE) { # Need to flip the ones/zeros - features <- features %>% - dplyr::mutate(dplyr::across(-dplyr::any_of("geometry"), ~ 1 - .)) + # If 'inverse' is TRUE, flip the binary values (0s become 1s, 1s become 0s). + if (inverse == TRUE) { + message("Inverse logic applied: values below cutoff will be 1.") + features_as_tibble <- features_as_tibble %>% + dplyr::mutate(dplyr::across(dplyr::all_of(numeric_cols), ~ 1 - .)) } - } else if (length(Cutoffs) == length(names(Cutoffs))) { # Named vector with values for each column - nm <- names(Cutoffs) # Testing - We should only be operating on the columns in the Cutoffs vector + } else if (length(Cutoffs) == length(names(Cutoffs))) { + # Case 2: Named vector of cutoffs (each cutoff applies to a specific column). + message("Applying named cutoffs to specific feature columns.") - for (f in 1:length(nm)) { - features <- features %>% - dplyr::mutate(!!nm[f] := dplyr::case_when( - !!rlang::sym(nm[f]) >= Cutoffs[nm[f]] ~ 1, - !!rlang::sym(nm[f]) < Cutoffs[nm[f]] ~ 0, - is.na(!!rlang::sym(nm[f])) ~ 0 + # Iterate through each named cutoff. + for (col_name in names(Cutoffs)) { + current_cutoff <- Cutoffs[col_name] # Get the cutoff value for the current column. + message(paste0(" Applying cutoff ", current_cutoff, " to column '", col_name, "'.")) + + # Apply the specific cutoff to the current column. + # Values greater than or equal to the column's cutoff become 1, others become 0. + # NAs are explicitly converted to 0. + features_as_tibble <- features_as_tibble %>% + dplyr::mutate(!!rlang::sym(col_name) := dplyr::case_when( + !!rlang::sym(col_name) >= current_cutoff ~ 1, + !!rlang::sym(col_name) < current_cutoff ~ 0, + is.na(!!rlang::sym(col_name)) ~ 0 # Handle NAs by converting them to 0. )) - if (inverse == TRUE) { # Need to flip the ones/zeros - features <- features %>% - dplyr::mutate(!!nm[f] := 1 - !!rlang::sym(nm[f])) + # If 'inverse' is TRUE, flip the binary values for the current column. + if (inverse == TRUE) { + message(paste0(" Inverse logic applied for column '", col_name, "': values below cutoff will be 1.")) + features_as_tibble <- features_as_tibble %>% + dplyr::mutate(!!rlang::sym(col_name) := 1 - !!rlang::sym(col_name)) } } + } else { + # This scenario should ideally be caught by initial assertions, but included for robustness. + stop("Invalid 'Cutoffs' parameter. It must be a single numeric value or a named numeric vector where names match feature columns.") } - return(features) + + # Convert the modified tibble back to an sf object, preserving the original geometry. + # Assuming the geometry column was preserved as is in 'features_as_tibble'. + final_sf_features <- features_as_tibble %>% + sf::st_as_sf() + + return(final_sf_features) } diff --git a/R/splnr_deprecated.R b/R/splnr_deprecated.R index 2efaf1e..4df1284 100644 --- a/R/splnr_deprecated.R +++ b/R/splnr_deprecated.R @@ -90,8 +90,8 @@ splnr_plot_MPAs <- function(df, colorVals = c("TRUE" = "blue", "FALSE" = "white" #' #' `r lifecycle::badge("deprecated")` #' -#' @param Cost An `sf` object of cost for `prioritizr` -#' @param Cost_name Name of the cost column +#' @param cost An `sf` object of cost for `prioritizr` +#' @param costName Name of the cost column #' @param legendTitle A character value for the title of the legend. Can be empty (""). #' @param paletteName A string (or number) for the color palette to use. Available palettes can be found at https://ggplot2.tidyverse.org/reference/scale_brewer.html. #' @param plotTitle A character value for the title of the plot. Can be empty (""). @@ -101,7 +101,7 @@ splnr_plot_MPAs <- function(df, colorVals = c("TRUE" = "blue", "FALSE" = "white" #' #' @examples #' \dontrun{ -#' dat_problem <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +#' dat_problem <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(cost = runif(n = dim(.)[[1]])), #' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" #' ) %>% @@ -118,7 +118,7 @@ splnr_plot_MPAs <- function(df, colorVals = c("TRUE" = "blue", "FALSE" = "white" #' #' (splnr_plot_cost(dat_cost)) #' } -splnr_plot_cost <- function(Cost, Cost_name = "Cost", legendTitle = "Cost", +splnr_plot_cost <- function(cost, costName = "Cost", legendTitle = "Cost", paletteName = "YlGnBu", plotTitle = "") { lifecycle::deprecate_stop("0.6.2", "splnr_plot_MPAs()", "splnr_plot()") diff --git a/R/splnr_featureRep.R b/R/splnr_featureRep.R index fabb286..b5bda1d 100644 --- a/R/splnr_featureRep.R +++ b/R/splnr_featureRep.R @@ -1,20 +1,102 @@ -#' Prepare data to plot how well targets are met +#' @title Prepare Data to Plot How Well Targets Are Met #' -#' @param soln The `prioritizr` solution -#' @param pDat The `prioritizr` problem -#' @param targets `data.frame`with list of features under "feature" column and their corresponding targets under "target" column -#' @param climsmart logical denoting whether spatial planning was done climate-smart (and targets have to be calculated differently) -#' @param climsmartApproach either 0,1,2 or 3 depending on the climate-smart approach used (0 = None; 1 = Climate Priority Area; 2 = Feature; 3 = Percentile). -#' @param solnCol Name of the column with the solution +#' @description +#' `splnr_get_featureRep()` calculates the representation of conservation +#' features within a `prioritizr` solution. This function determines how much +#' of each feature's total abundance (or area) is captured in the selected +#' planning units, and compares it against specified conservation targets. +#' It can also account for different climate-smart planning approaches. #' -#' @return `tbl_df` dataframe +#' @details +#' This function processes the output of a `prioritizr` conservation problem +#' (`soln`) and its corresponding problem definition (`pDat`) to provide a +#' summary of feature representation. It is designed to work whether or not +#' explicit targets are provided, and can adjust calculations based on the +#' climate-smart approach used. +#' +#' The function calculates: +#' \itemize{ +#' \item `total_amount`: The total available amount/area of each feature across all planning units. +#' \item `absolute_held`: The total amount/area of each feature captured in the +#' *selected* planning units (where `solution_1` is 1). +#' \item `relative_held`: The proportion of `absolute_held` relative to `total_amount`, +#' indicating the percentage representation of the feature in the solution. +#' \item `target`: The conservation target for each feature (either from the +#' `pDat` problem definition or the `targets` dataframe). +#' \item `incidental`: A logical flag indicating if a feature's representation +#' was 'incidental' (i.e., its target was 0 or NA, but it was still +#' partially or fully captured in the solution). +#' } +#' +#' \strong{Climate-Smart Considerations (`climsmart = TRUE`):} +#' If `climsmart` is `TRUE`, the function adjusts its calculations based on the +#' `climsmartApproach` parameter: +#' \itemize{ +#' \item `climsmartApproach = 1` (Climate Priority Area): The function sums the +#' `absolute_held` and `total_amount` for features that were split into +#' `_CS` (Climate-Smart) and `_NCS` (Non-Climate-Smart) components. This +#' provides a single, aggregated representation for the original feature, +#' allowing comparison with its original target. +#' \item `climsmartApproach = 3` (Percentile Approach): The function directly +#' uses the targets provided in the `targets` dataframe, which are +#' expected to be adjusted for the percentile approach. +#' \item For other `climsmartApproach` values or if `climsmart` is `FALSE`, +#' targets are taken directly from the `prioritizr` problem's target data. +#' } +#' +#' The output dataframe is designed to be directly plottable by functions +#' like `splnr_plot_featureRep()`. +#' +#' @param soln An `sf` object representing the `prioritizr` solution, containing +#' a column indicating selected planning units (default: `solution_1`). +#' @param pDat A `prioritizr` problem object, as defined by `prioritizr::problem()`. +#' This object provides the original feature data and targets. +#' @param targets A `data.frame` (optional). If provided, it should contain a +#' `feature` column (character) and a `target` column (numeric). This is used +#' to override or supplement targets from `pDat`, especially for climate-smart +#' approaches where targets might be pre-adjusted. Defaults to `NA`. +#' @param climsmart A logical value (`TRUE` or `FALSE`). If `TRUE`, special +#' handling for climate-smart approaches is enabled. Defaults to `FALSE`. +#' @param climsmartApproach An integer (0, 1, 2, or 3) indicating the type of +#' climate-smart approach used: +#' \itemize{ +#' \item `0`: No climate-smart approach. +#' \item `1`: Climate Priority Area approach (features split into CS/NCS). +#' \item `2`: Feature approach (not explicitly handled in this function's +#' `climsmart` logic, targets taken from `pDat` by default). +#' \item `3`: Percentile approach (features are filtered). +#' } +#' Defaults to `0`. +#' @param solnCol A character string specifying the name of the column in `soln` +#' that contains the binary solution (1 for selected, 0 for not selected). +#' Defaults to `"solution_1"`. +#' +#' @return A `tibble` dataframe containing the `feature` names, their +#' `total_amount` (total units available), `absolute_held` (total units +#' selected), `relative_held` (proportion held), `target` (conservation target), +#' and `incidental` (TRUE if target was 0 or NA, but feature still present). #' @export #' -#' @importFrom rlang .data +#' @importFrom assertthat assert_that +#' @importFrom dplyr arrange bind_rows case_when filter group_by if_else left_join mutate pull select summarise ungroup +#' @importFrom prioritizr eval_feature_representation_summary +#' @importFrom rlang .data sym +#' @importFrom sf st_drop_geometry +#' @importFrom stats na.omit +#' @importFrom stringr str_remove_all +#' @importFrom tibble as_tibble tibble +#' @importFrom tidyselect any_of starts_with everything +#' @importFrom tidyr pivot_longer #' #' @examples -#' pDat <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), -#' features = c("Spp1", "Spp2", "Spp3"), +#' \dontrun{ +#' # Assuming 'dat_species_bin' is an existing sf object with binary species data +#' # and 'Cost' column. +#' +#' # Create a dummy prioritizr problem for basic demonstration +#' pDat_basic <- prioritizr::problem( +#' dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +#' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" #' ) %>% #' prioritizr::add_min_set_objective() %>% @@ -22,145 +104,315 @@ #' prioritizr::add_binary_decisions() %>% #' prioritizr::add_default_solver(verbose = FALSE) #' -#' soln <- pDat %>% +#' # Solve the problem +#' soln_basic <- pDat_basic %>% #' prioritizr::solve.ConservationProblem() #' -#' df <- splnr_get_featureRep( -#' soln = soln, -#' pDat = pDat +#' # Get feature representation for a basic (non-climate-smart) solution +#' df_basic_rep <- splnr_get_featureRep( +#' soln = soln_basic, +#' pDat = pDat_basic #' ) +#' print(df_basic_rep) +#' +#' # Example with Climate Priority Area (CPA) approach +#' # Assuming 'dat_clim' is an sf object with a 'metric' column. +#' # These would typically come from splnr_climate_priorityAreaApproach() +#' # For example purposes, we'll create some dummy data and targets. +#' +#' # Simulate CPA processed features and targets +#' cpa_features_sim <- dat_species_bin %>% +#' dplyr::mutate( +#' Spp1_CS = ifelse(Spp1 == 1 & runif(n()) < 0.5, 1, 0), +#' Spp1_NCS = ifelse(Spp1 == 1 & Spp1_CS == 0, 1, 0), +#' Spp2_CS = ifelse(Spp2 == 1 & runif(n()) < 0.6, 1, 0), +#' Spp2_NCS = ifelse(Spp2 == 1 & Spp2_CS == 0, 1, 0), +#' Spp3_CS = ifelse(Spp3 == 1 & runif(n()) < 0.7, 1, 0), +#' Spp3_NCS = ifelse(Spp3 == 1 & Spp3_CS == 0, 1, 0) +#' ) %>% +#' dplyr::select(Spp1_CS, Spp1_NCS, Spp2_CS, Spp2_NCS, Spp3_CS, Spp3_NCS, geometry) +#' +#' cpa_targets_sim <- data.frame( +#' feature = c("Spp1_CS", "Spp1_NCS", "Spp2_CS", "Spp2_NCS", "Spp3_CS", "Spp3_NCS"), +#' target = c(0.8, 0.2, 0.9, 0.1, 0.7, 0.3) # Example targets for CS/NCS parts +#' ) +#' +#' # Create a problem with the simulated CPA features +#' pDat_cpa_sim <- prioritizr::problem( +#' cpa_features_sim %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +#' features = c("Spp1_CS", "Spp1_NCS", "Spp2_CS", "Spp2_NCS", "Spp3_CS", "Spp3_NCS"), +#' cost_column = "Cost" +#' ) %>% +#' prioritizr::add_min_set_objective() %>% +#' prioritizr::add_relative_targets(cpa_targets_sim$target, cpa_targets_sim$feature) %>% +#' prioritizr::add_binary_decisions() %>% +#' prioritizr::add_default_solver(verbose = FALSE) +#' +#' # Solve the CPA problem +#' soln_cpa_sim <- pDat_cpa_sim %>% +#' prioritizr::solve.ConservationProblem() +#' +#' # Get feature representation for CPA approach +#' df_cpa_rep <- splnr_get_featureRep( +#' soln = soln_cpa_sim, +#' pDat = pDat_cpa_sim, +#' targets = cpa_targets_sim, # Pass the original CPA targets +#' climsmart = TRUE, +#' climsmartApproach = 1 # Indicate CPA approach +#' ) +#' print(df_cpa_rep) +#' } splnr_get_featureRep <- function(soln, pDat, targets = NA, climsmart = FALSE, climsmartApproach = 0, solnCol = "solution_1") { + + # --- Input Assertions --- + # Ensure 'soln' is an sf object and not empty. + assertthat::assert_that( + inherits(soln, "sf"), + msg = "'soln' must be an 'sf' object." + ) + assertthat::assert_that( + nrow(soln) > 0, + msg = "'soln' dataframe must not be empty." + ) + # Ensure 'pDat' is a prioritizr problem object. + assertthat::assert_that( + inherits(pDat, "ConservationProblem"), + msg = "'pDat' must be a 'prioritizr::ConservationProblem' object." + ) + # Ensure 'solnCol' is a character string and exists in 'soln'. + assertthat::assert_that( + is.character(solnCol) && length(solnCol) == 1, + msg = "'solnCol' must be a single character string." + ) + assertthat::assert_that( + solnCol %in% names(soln), + msg = paste0("Solution column '", solnCol, "' not found in 'soln'.") + ) + # Ensure 'climsmart' is logical. + assertthat::assert_that( + is.logical(climsmart) && length(climsmart) == 1, + msg = "'climsmart' must be a single logical value (TRUE or FALSE)." + ) + # Ensure 'climsmartApproach' is a valid integer. + assertthat::assert_that( + is.numeric(climsmartApproach) && length(climsmartApproach) == 1 && climsmartApproach %in% c(0, 1, 2, 3), + msg = "'climsmartApproach' must be an integer (0, 1, 2, or 3)." + ) + # Validate 'targets' if provided (not NA). + if (!all(is.na(targets))) { + assertthat::assert_that( + is.data.frame(targets), + msg = "If 'targets' is provided, it must be a data.frame." + ) + assertthat::assert_that( + all(c("feature", "target") %in% names(targets)), + msg = "If 'targets' is a data.frame, it must contain 'feature' and 'target' columns." + ) + } + + # Extract feature names from the problem data (pDat). + # prioritizr problems store feature names in pDat$data$features[[1]] s_cols <- pDat$data$features[[1]] - # Get data for features not chosen + # --- Process non-selected features (if any) --- + # These are features present in the solution object but NOT part of the + # core features defined in pDat. This often includes 'Cost' or other + # temporary columns, or potentially features with 0 targets. + + # Select columns that are not 'Cost_', 'solution_' or 'metric', and not in s_cols. + # These are considered "not selected" or ancillary features for initial processing. not_selected <- soln %>% dplyr::select( - -tidyselect::starts_with(c("Cost", "solution_")), - -tidyselect::any_of(c("metric")), - -tidyselect::any_of(s_cols) + -tidyselect::starts_with(c("Cost", "solution_")), # Exclude Cost and solution columns + -tidyselect::any_of(c("metric")), # Exclude 'metric' if it exists + -tidyselect::any_of(s_cols) # Exclude primary features defined in pDat ) %>% - sf::st_drop_geometry() + sf::st_drop_geometry() # Drop geometry for numerical operations - ns_cols <- not_selected %>% - colnames() + # Get column names of remaining 'not_selected' features. + ns_cols <- colnames(not_selected) + # Proceed if there are any non-selected features to process. if (length(ns_cols) > 0) { + # Combine non_selected features with the solution column for filtering. ns1 <- not_selected %>% - dplyr::select(c(tidyselect::all_of(ns_cols))) %>% - dplyr::mutate(solution = dplyr::pull(soln, !!rlang::sym(solnCol))) + dplyr::select(tidyselect::all_of(ns_cols)) %>% + dplyr::mutate(solution = dplyr::pull(soln, !!rlang::sym(solnCol))) # Add the solution column + # Calculate the total amount of each non-selected feature. area_feature <- ns1 %>% - dplyr::select(-c("solution")) %>% + dplyr::select(-c("solution")) %>% # Remove solution column for total sum tidyr::pivot_longer(cols = tidyselect::everything(), names_to = "feature", values_to = "total_amount") %>% dplyr::group_by(.data$feature) %>% - dplyr::summarise(total_amount = sum(.data$total_amount)) + dplyr::summarise(total_amount = sum(.data$total_amount, na.rm = TRUE)) # Sum total amount, handling NAs + # Calculate the absolute amount of each non-selected feature in selected units. selected_feature <- ns1 %>% - dplyr::filter(.data$solution == 1) %>% - dplyr::select(-c("solution")) %>% + dplyr::filter(.data$solution == 1) %>% # Filter for selected planning units + dplyr::select(-c("solution")) %>% # Remove solution column for sum tidyr::pivot_longer(cols = tidyselect::everything(), names_to = "feature", values_to = "absolute_held") %>% dplyr::group_by(.data$feature) %>% - dplyr::summarise(absolute_held = sum(.data$absolute_held)) + dplyr::summarise(absolute_held = sum(.data$absolute_held, na.rm = TRUE)) # Sum absolute held, handling NAs + # Join total and selected amounts and calculate relative held. ns1 <- dplyr::left_join(area_feature, selected_feature, by = "feature") %>% dplyr::mutate( - relative_held = (.data$absolute_held / .data$total_amount) + relative_held = dplyr::if_else( + .data$total_amount > 0, # Avoid division by zero + .data$absolute_held / .data$total_amount, + 0 # Set to 0 if total_amount is 0 + ) ) } else { + # If no non-selected features, create an empty tibble with required columns. + message("No non-selected features to process.") ns1 <- tibble::tibble( - feature = "DummyVar", - total_amount = 0, - absolute_held = 0, - relative_held = 0 + feature = character(), + total_amount = numeric(), + absolute_held = numeric(), + relative_held = numeric() ) } - ## Now do the selected features + # --- Process primary selected features (from pDat) --- - s1 <- soln %>% - dplyr::rename(solution = !!rlang::sym(solnCol)) %>% - tibble::as_tibble() + # Create a tibble with the solution column from the soln sf object. + # This is needed as eval_feature_representation_summary expects a data.frame. + soln_df <- soln %>% + dplyr::rename(solution = !!rlang::sym(solnCol)) %>% # Rename solution column to 'solution' + tibble::as_tibble() # Convert to tibble for prioritizr function - s1 <- prioritizr::eval_feature_representation_summary(pDat, s1[, "solution"]) %>% - dplyr::select(-"summary") + # Evaluate feature representation summary using prioritizr's internal function. + s1 <- prioritizr::eval_feature_representation_summary(pDat, soln_df[, "solution"]) %>% + dplyr::select(-"summary") # Remove the 'summary' column, which is not needed here. - if (climsmart == TRUE & climsmartApproach == 1) { + # --- Apply Climate-Smart Logic --- + + # If climate-smart approach is enabled and is CPA (Approach 1). + if (climsmart == TRUE && climsmartApproach == 1) { + message("Processing features with Climate Priority Area (CPA) approach.") s1 <- s1 %>% - dplyr::select(-.data$relative_held) %>% + dplyr::select(-.data$relative_held) %>% # Remove existing relative_held for recalculation + # Remove _CS and _NCS suffixes to group related features. dplyr::mutate( feature = stringr::str_remove_all(.data$feature, "_CS"), feature = stringr::str_remove_all(.data$feature, "_NCS") - ) %>% # Ensure all features have the same name. - dplyr::group_by(.data$feature) %>% + ) %>% + dplyr::group_by(.data$feature) %>% # Group by original feature names dplyr::summarise( - total_amount = sum(.data$total_amount), # Sum the features together - absolute_held = sum(.data$absolute_held) + total_amount = sum(.data$total_amount, na.rm = TRUE), # Sum total amounts for original feature + absolute_held = sum(.data$absolute_held, na.rm = TRUE) # Sum absolute held for original feature ) %>% dplyr::ungroup() %>% - dplyr::mutate(relative_held = .data$absolute_held / .data$total_amount) %>% # Calculate proportion - # dplyr::select(-"total_amount", -"absolute_held") %>% # Remove extra columns - dplyr::left_join(targets, by = "feature") #%>% # Add targets to df - # dplyr::select(-"type") - - } else if (climsmart == TRUE & climsmartApproach == 3) { + # Recalculate relative held for the aggregated feature. + dplyr::mutate(relative_held = dplyr::if_else( + .data$total_amount > 0, + .data$absolute_held / .data$total_amount, + 0 + )) %>% + # Join with the provided 'targets' dataframe for CPA. + # This assumes 'targets' dataframe contains adjusted targets for original features. + dplyr::left_join(targets, by = "feature") + } else if (climsmart == TRUE && climsmartApproach == 3) { + # If climate-smart approach is enabled and is Percentile Approach (Approach 3). + message("Processing features with Percentile Climate-Smart Approach.") + # For percentile approach, directly join with provided 'targets' as they are + # assumed to be already adjusted for the filtered features. s1 <- s1 %>% dplyr::left_join(targets, by = "feature") } else { - # Add targets to df + # Default case: no climate-smart approach or other approaches. + # Targets are taken directly from the prioritizr problem object. + message("No specific climate-smart approach detected or standard approach used. Using targets from 'pDat'.") s1 <- s1 %>% dplyr::left_join(pDat$targets$data[["targets"]], by = "feature") %>% - dplyr::select(-"type") + dplyr::select(-"type") # Remove 'type' column from prioritizr targets if it exists. } + # Remove rows with NA in 'relative_held' which might occur if total_amount was zero. s1 <- s1 %>% - dplyr::mutate( - relative_held = .data$relative_held - ) %>% - stats::na.omit() + stats::na.omit() # Remove any rows with NAs (e.g., if target was NA and not handled by above logic) + # --- Combine and Finalize Results --- - # Now join the selected and non-selected values - if ((length(ns_cols) > 0)) { # Only if there are values in ns1 - df <- dplyr::bind_rows(s1, ns1) - } else { - df <- s1 - } + # Bind rows of primary features (s1) and non-selected features (ns1). + # This ensures all features are represented in the final output. + df <- dplyr::bind_rows(s1, ns1) - # Now add in incidental for 0 and NA targets + # Add an 'incidental' flag: TRUE if a feature was included but its target was 0 or NA. + # This helps distinguish features intentionally targeted from those incidentally selected. df <- df %>% - dplyr::mutate(incidental = dplyr::if_else(target > 0 & absolute_held > 0, FALSE, TRUE, missing = TRUE)) + dplyr::mutate( + incidental = dplyr::if_else( + .data$target > 0 & .data$absolute_held > 0, # If target > 0 AND something was held, it's NOT incidental + FALSE, + TRUE, # Otherwise, it's incidental (target 0, NA, or target > 0 but nothing held) + missing = TRUE # Explicitly handle missing values for 'target' + ) + ) return(df) } - - -# Targets Bar Plot -------------------------------------------------------- - - - -#' Plot how well targets are met +#' @title Plot Feature Representation (Target Achievement) #' -#' @param df A `df` containing the target information (resulting from the splnr_get_featureRep() function) -#' @param nr Number of rows of the legend -#' @param plotTitle A character value for the title of the plot. Can be empty (""). -#' @param category A named data frame of feature and category for grouping the plot output -#' @param categoryFeatureCol A character with the column containing the feature infromation to be plotted if the category data frame does not contain a column named 'feature' that can be matched with the 'df' infromation. -#' @param renameFeatures A logical on whether variable names should be used or they should be replaced with common names -#' @param namesToReplace A data frame containing the variable name ('nameVariable') and a common name ('nameCommon'). -#' @param showTarget `logical` Should the targets be shown on the bar plot -#' @param ... Other arguments passed on to `ggplot2::theme()` +#' @description +#' `splnr_plot_featureRep()` creates a bar plot to visualize the representation +#' of features in a conservation solution, indicating how well targets are met. +#' It can categorize features, rename them for clarity, and optionally display +#' the target levels on the plot. #' -#' @return A ggplot object of the plot +#' @param df A [data.frame][base::data.frame] or [tibble][tibble::tibble] +#' containing the feature representation information. This typically +#' results from the `splnr_get_featureRep()` function and should include at +#' least `feature` and `relative_held` columns, and optionally `target` and `incidental`. +#' @param category A named [data.frame][base::data.frame] or [tibble][tibble::tibble] +#' that provides grouping information for features. It should contain a column +#' that can be matched with the `feature` column in `df` (by default, a column +#' named `feature`, or specified by `categoryFeatureCol`), and a column named +#' `category` for grouping the plot output. If `NA` (default), no categorization is applied. +#' @param categoryFeatureCol A [character][base::character] string specifying the +#' name of the column in the `category` data frame that contains the feature +#' information to be matched with `df$feature`. This is used if the `category` +#' data frame does not have a column explicitly named `'feature'`. +#' @param renameFeatures A [logical][base::logical] value. If `TRUE`, feature names +#' in the plot will be replaced with common names provided in `namesToReplace`. +#' @param namesToReplace A [data.frame][base::data.frame] containing two columns: +#' `'nameVariable'` (the original feature name) and `'nameCommon'` (the common name +#' to replace it with). Required if `renameFeatures` is `TRUE`. +#' @param nr An [integer][base::integer] specifying the number of rows for the legend. +#' @param showTarget A [logical][base::logical] value. If `TRUE`, a transparent bar +#' representing the target level for each feature will be shown on the plot. +#' @param plotTitle A [character][base::character] string for the title of the plot. +#' Can be an empty string `""` (default). +#' @param sort_by A [character][base::character] string specifying the column +#' by which to sort the features on the x-axis. Accepted values include: +#' `"category"`, `"feature"`, `"target"`, `"representation"` (`relative_held`), +#' or `"difference"` (between representation and target). +#' @param ... Other arguments passed on to [ggplot2::theme()] to customize the plot's theme. +#' +#' @return A [ggplot2::ggplot] object representing the feature representation bar plot. #' @export +#' @importFrom assertthat assert_that +#' @importFrom dplyr arrange bind_rows filter if_else left_join mutate rename select +#' @importFrom ggplot2 aes element_blank element_rect element_text geom_bar ggplot labs +#' @importFrom ggplot2 guide_legend guides scale_fill_manual scale_y_continuous theme theme_bw +#' @importFrom rlang .data +#' @importFrom stringr str_c str_replace_all +#' @importFrom stats reorder +#' @importFrom tibble deframe tibble #' #' @examples +#' # For a full example, ensure 'dat_species_bin', 'dat_category' are available +#' # (e.g., from the 'prioritizrdata' package or defined in your package's data) +#' +#' #' pDat <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), #' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" @@ -173,15 +425,29 @@ splnr_get_featureRep <- function(soln, pDat, targets = NA, #' soln <- pDat %>% #' prioritizr::solve.ConservationProblem() #' -#' #' # including incidental species coverage -#' df <- splnr_get_featureRep( +#' df <- splnr_get_featureRep( # Assuming splnr_get_featureRep is available #' soln = soln, #' pDat = pDat #' ) #' +#' # Basic plot with categories and targets shown #' (splnr_plot_featureRep(df, category = dat_category, showTarget = TRUE)) #' +#' # Plot without categories, sorted by feature name +#' (splnr_plot_featureRep(df, showTarget = TRUE, sort_by = "feature")) +#' +#' # Example with feature renaming +#' names_to_replace_df <- tibble::tibble( +#' nameVariable = c("Spp1", "Spp2"), +#' nameCommon = c("Species One", "Species Two") +#' ) +#' (splnr_plot_featureRep(df, +#' category = dat_category, +#' renameFeatures = TRUE, +#' namesToReplace = names_to_replace_df, +#' showTarget = TRUE +#' )) splnr_plot_featureRep <- function(df, category = NA, categoryFeatureCol = NA, @@ -193,64 +459,87 @@ splnr_plot_featureRep <- function(df, sort_by = "category", ...) { - # TODO Add to documentation - # sort_by = "category", - # "difference", - # "target", - # "feature" "feature" - # "representation" "relative_held" - assertthat::assert_that( inherits(df, c("data.frame", "tbl_df")), is.logical(renameFeatures), is.logical(showTarget), is.character(plotTitle), - all(colSums(is.na(category)) == nrow(category)) || inherits(category, c("data.frame","tbl","tbl_df")) + # Check if category is NA-filled or a data frame/tibble + all(is.na(category)) || inherits(category, c("data.frame", "tbl", "tbl_df")), + is.numeric(nr), # nr should be numeric + # Validate sort_by against allowed values + sort_by %in% c("category", "feature", "target", "difference", "representation", "relative_held"), + msg = "Invalid 'sort_by' value. Must be one of 'category', 'feature', 'target', 'difference', 'representation', or 'relative_held'." ) if (renameFeatures) { assertthat::assert_that( is.data.frame(namesToReplace), "nameVariable" %in% colnames(namesToReplace), - "nameCommon" %in% colnames(namesToReplace) + "nameCommon" %in% colnames(namesToReplace), + msg = paste0( + "When 'renameFeatures' is TRUE, 'namesToReplace' must be a data frame ", + "with 'nameVariable' and 'nameCommon' columns." + ) ) } - if(inherits(category, c("df", "tbl_df")) & !("feature" %in% colnames(category))) { - if (!(inherits(categoryFeatureCol, "character"))) { - cat("There is no column called 'feature' in your category data frame. Please provide a column name that should be renamed to 'feature'."); - } else { - category <- category %>% - dplyr::rename(feature = categoryFeatureCol) - }} + if (inherits(category, c("data.frame", "tbl_df")) & !("feature" %in% colnames(category))) { + assertthat::assert_that( + is.character(categoryFeatureCol) && length(categoryFeatureCol) == 1, + categoryFeatureCol %in% colnames(category), + msg = paste0( + "If 'category' is a data frame and does not have a 'feature' column, ", + "'categoryFeatureCol' must be a character string specifying the column ", + "in 'category' that contains feature information." + ) + ) + category <- category %>% + dplyr::rename(feature = categoryFeatureCol) + } - if (renameFeatures == TRUE) { - assertthat::assert_that(is.data.frame(namesToReplace)) #sanity check + # Check if 'viridis' package is installed. If not, stop with an informative error. + if (requireNamespace("viridis", quietly = TRUE) == FALSE){ + stop("To run splnr_plot_featureRep you will need to install the 'viridis' package: install.packages('viridis').") + } + + + if (renameFeatures == TRUE) { + # No assertthat::assert_that(is.data.frame(namesToReplace)) needed here, + # as it's covered by the initial assertthat block. rpl <- namesToReplace %>% dplyr::filter(.data$nameVariable %in% df$feature) %>% dplyr::select("nameVariable", "nameCommon") %>% - dplyr::mutate(nameVariable = stringr::str_c("^", nameVariable, "$")) %>% + dplyr::mutate(nameVariable = stringr::str_c("^", .data$nameVariable, "$")) %>% tibble::deframe() df <- df %>% dplyr::mutate(feature = stringr::str_replace_all(.data$feature, rpl)) - category <- category %>% - dplyr::mutate(feature = stringr::str_replace_all(.data$feature, rpl)) - + # Only attempt to rename features in category if category is actually provided + if (inherits(category, c("data.frame", "tbl_df")) && "feature" %in% colnames(category)) { + category <- category %>% + dplyr::mutate(feature = stringr::str_replace_all(.data$feature, rpl)) + } } - if (inherits(category, c("df", "tbl_df")) & ("feature" %in% colnames(category))) { + if (inherits(category, c("data.frame", "tbl_df")) & ("feature" %in% colnames(category))) { df <- df %>% dplyr::left_join(category, by = "feature") %>% dplyr::arrange(.data$category, .data$feature) %>% dplyr::mutate(feature = factor(.data$feature, levels = .data$feature)) + } else { + # If no category is provided or matched, ensure 'category' column exists for plotting + if (!("category" %in% colnames(df))) { + df <- df %>% dplyr::mutate(category = "Uncategorized") + } } - if (max(df$relative_held < 1)) { + + if (max(df$relative_held, na.rm = TRUE) < 1) { # Check max before multiplying df <- df %>% dplyr::mutate( relative_held = .data$relative_held * 100, @@ -258,6 +547,26 @@ splnr_plot_featureRep <- function(df, ) } + # Ensure 'sort_by' columns exist before use + if (sort_by == "difference" && !("target" %in% colnames(df) && "relative_held" %in% colnames(df))) { + stop("Cannot sort by 'difference': 'target' and/or 'relative_held' columns are missing.") + } + if (sort_by == "representation" && !("relative_held" %in% colnames(df))) { + stop("Cannot sort by 'representation': 'relative_held' column is missing.") + } + if (sort_by == "target" && !("target" %in% colnames(df))) { + stop("Cannot sort by 'target': 'target' column is missing.") + } + + # Calculate 'difference' and 'representation' if sorting by them + if (sort_by %in% c("difference", "representation")) { + df <- df %>% + dplyr::mutate( + difference = .data$relative_held - .data$target, + representation = .data$relative_held + ) + } + uniqueCat <- unique(df$category[!is.na(df$category)]) colr <- tibble::tibble( @@ -266,26 +575,28 @@ splnr_plot_featureRep <- function(df, ) %>% tibble::deframe() - -if (!sort_by %in% c("category", "feature", "target")){ - df <- df %>% - dplyr::mutate(difference = relative_held - target, - representation = relative_held) -} - - gg_target <- ggplot2::ggplot() + - ggplot2::geom_bar(data = df %>% dplyr::mutate(relative_held = dplyr::if_else(incidental == TRUE, NA, relative_held)), - stat = "identity", position = "identity", - ggplot2::aes(x = stats::reorder(.data$feature, .data[[sort_by]]), y = .data$relative_held, - fill = .data$category, colour = .data$category), - na.rm = TRUE) + - ggplot2::geom_bar(data = df %>% dplyr::mutate(relative_held = dplyr::if_else(incidental == FALSE, NA, relative_held)), - stat = "identity", position = "identity", - ggplot2::aes(x = .data$feature, y = .data$relative_held), na.rm = TRUE, fill = "NA", colour = "black") + + ggplot2::geom_bar( + data = df %>% dplyr::mutate(relative_held = dplyr::if_else(.data$incidental == TRUE, NA_real_, .data$relative_held)), # Use NA_real_ for numeric NA + stat = "identity", position = "identity", + ggplot2::aes( + x = stats::reorder(.data$feature, .data[[sort_by]]), y = .data$relative_held, + fill = .data$category, colour = .data$category + ), + na.rm = TRUE + ) + + ggplot2::geom_bar( + data = df %>% dplyr::mutate(relative_held = dplyr::if_else(.data$incidental == FALSE, NA_real_, .data$relative_held)), # Use NA_real_ + stat = "identity", position = "identity", + ggplot2::aes(x = .data$feature, y = .data$relative_held), na.rm = TRUE, fill = "NA", colour = "black" + ) + ggplot2::labs(title = plotTitle, x = "Feature", y = "Representation of features \nin total selected area (%)") + ggplot2::theme_bw() + - ggplot2::scale_y_continuous(limits = c(0, ymax <- max(df$relative_held, na.rm = TRUE) + 10), expand = c(0, 0)) + # only works for min shortfall without incidental yet + # Ensure ymax is calculated correctly and handled for empty df + ggplot2::scale_y_continuous( + limits = c(0, max(df$relative_held, na.rm = TRUE, 0) + 10), # Ensure at least 0 if all NA + expand = c(0, 0) + ) + ggplot2::scale_fill_manual( aesthetics = c("fill", "colour"), values = colr, @@ -300,51 +611,71 @@ if (!sort_by %in% c("category", "feature", "target")){ legend.title = ggplot2::element_blank(), legend.text = ggplot2::element_text(size = 16), legend.position = "top", - # legend.margin = ggplot2::margin(0, 0, 0, 0), - # legend.justification.top = "centre", - # legend.position.inside = c(0.5, 0.92), legend.direction = "horizontal", legend.background = ggplot2::element_rect(fill = "NA"), title = ggplot2::element_text(size = 16), ... ) - if (!(is.na(showTarget))) { + if (!(is.na(showTarget)) && showTarget == TRUE) { # Explicitly check for TRUE + assertthat::assert_that( + "target" %in% colnames(df), + msg = "Cannot show target: 'target' column is missing from the data frame." + ) gg_target <- gg_target + - - ggplot2::geom_bar(data = df %>% dplyr::mutate(relative_held = dplyr::if_else(incidental == TRUE, NA, relative_held)), - stat = "identity", position = "identity", ggplot2::aes(x = .data$feature, y = .data$target), na.rm = TRUE, - alpha = 0.3, colour = "grey50", fill = "white") + ggplot2::geom_bar( + data = df %>% dplyr::mutate(relative_held = dplyr::if_else(.data$incidental == TRUE, NA_real_, .data$relative_held)), # Use NA_real_ + stat = "identity", position = "identity", ggplot2::aes(x = .data$feature, y = .data$target), na.rm = TRUE, + alpha = 0.3, colour = "grey50", fill = "white" + ) } return(gg_target) } - -# Circular Bar Plot ------------------------------------------------------- - - - -#' Plot circular barplot -# Inputs: -#' @param df data frame that should have the following column names: feature, value, group -# feature: individual bars -# value: value plotted in the y-axis -# group: grouping factors -#' @param legend_color vector list of colors; should have the group names and their corresponding colors -#' @param legend_list list of groups/legends of groups -#' @param indicateTargets logical on whether to show where the targets were set -#' @param impTarget target of the important features (in %) -#' @param repTarget target of the representative features (in %) -#' @param colTarget string with a colour value for the indicator line +#' @title Plot Circular Barplot for Feature Representation +#' +#' @description +#' `splnr_plot_circBplot()` creates a circular bar plot to visualize feature +#' representation, categorized by groups. It's particularly useful for +#' displaying how different categories of features meet certain targets in a radial layout. +#' +#' @param df A [data.frame][base::data.frame] or [tibble][tibble::tibble] that +#' **must** contain the following columns: +#' \itemize{ +#' \item `feature`: [character][base::character] or [factor][base::factor] unique identifier for each individual bar (e.g., species names). +#' \item `value`: [numeric][base::numeric] the value to be plotted on the y-axis (bar height, typically percentage representation). +#' \item `group`: [character][base::character] or [factor][base::factor] for grouping factors (e.g., "important", "representative"). +#' } +#' @param legend_color A [named vector][base::vector] of colors. Names must correspond +#' to the unique values in the `group` column of `df`, and values are the +#' corresponding colors. For example: `c("group_name1" = "red", "group_name2" = "blue")`. +#' @param legend_list A [character vector][base::character] of labels for the legend. +#' This should match the names used in `legend_color` or the levels of `group`. +#' @param indicateTargets A [logical][base::logical] value. If `TRUE`, horizontal +#' lines indicating `impTarget` and `repTarget` will be drawn on the plot. +#' @param impTarget A [numeric][base::numeric] value representing the target +#' percentage for 'important' features. Required if `indicateTargets` is `TRUE`. +#' @param repTarget A [numeric][base::numeric] value representing the target +#' percentage for 'representative' features. Required if `indicateTargets` is `TRUE`. +#' @param colTarget A [character][base::character] string specifying the color +#' for the target indicator lines. #' -#' @return A ggplot object of the plot +#' @return A [ggplot2::ggplot] object of the circular bar plot. #' @export +#' @importFrom assertthat assert_that +#' @importFrom dplyr arrange bind_rows group_by mutate rowwise summarize +#' @importFrom ggplot2 aes annotate coord_polar element_blank geom_abline geom_bar geom_segment +#' @importFrom ggplot2 geom_text ggplot guides labs scale_fill_manual theme theme_minimal unit ylim +#' @importFrom stats na.omit +#' @importFrom tibble as_tibble tibble #' #' @examples #' # DISCLAIMER: THIS SOLUTION IS NOT ACTUALLY RUN WITH THESE TARGETS YET #' +#' \dontrun{ +#' #' dat_problem <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), #' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" @@ -362,6 +693,7 @@ if (!sort_by %in% c("category", "feature", "target")){ #' #' p1 <- dat_problem #' +#' # Assuming eval_feature_representation_summary is from prioritizr #' df_rep_imp <- prioritizr::eval_feature_representation_summary( #' p1, #' s1[, "solution_1"] @@ -381,7 +713,7 @@ if (!sort_by %in% c("category", "feature", "target")){ #' #' df <- merge(df_rep_imp, target) %>% #' dplyr::select(-target) %>% -#' na.omit() %>% +#' stats::na.omit() %>% # Use stats::na.omit #' dplyr::rename(value = relative_held) %>% #' dplyr::rename(group = class) #' @@ -396,18 +728,47 @@ if (!sort_by %in% c("category", "feature", "target")){ #' legend_color = colors, #' impTarget = 50, repTarget = 30 #' )) +#' } splnr_plot_circBplot <- function(df, legend_color, legend_list, indicateTargets = TRUE, impTarget = NA, repTarget = NA, colTarget = "red") { + # assertthat checks for initial inputs + assertthat::assert_that( + inherits(df, c("data.frame", "tbl_df")), + "feature" %in% colnames(df), + "value" %in% colnames(df), + "group" %in% colnames(df), + is.numeric(df$value), + msg = "Input 'df' must be a data frame or tibble with 'feature', 'value', and 'group' columns, and 'value' must be numeric." + ) + assertthat::assert_that( + is.vector(legend_color) && !is.null(names(legend_color)), + all(unique(df$group) %in% names(legend_color)), + msg = "'legend_color' must be a named vector where names match unique 'group' values in 'df'." + ) + + assertthat::assert_that( + is.character(legend_list), length(unique(names(legend_color))) == length(legend_list), + msg = "'legend_list' must be a character vector with the same number of elements as unique names in 'legend_color'." + ) + + assertthat::assert_that( is.logical(indicateTargets), - is.numeric(impTarget), - is.numeric(repTarget), - is.character(colTarget) + is.character(colTarget), + msg = "'indicateTargets' must be logical and 'colTarget' must be a character string." ) + if (indicateTargets) { + assertthat::assert_that( + is.numeric(impTarget) && length(impTarget) == 1 && !is.na(impTarget), + is.numeric(repTarget) && length(repTarget) == 1 && !is.na(repTarget), + msg = "When 'indicateTargets' is TRUE, 'impTarget' and 'repTarget' must be single non-NA numeric values." + ) + } + # Adding rows to each group, creating space between the groups groups <- unique(df$group) NA_rows <- list() @@ -445,7 +806,7 @@ splnr_plot_circBplot <- function(df, legend_color, legend_list, # For the percentage lines grid_data <- data %>% dplyr::group_by(.data$group) %>% - dplyr::summarize(start = min(.data$id), end = max(.data$id) - empty_bar) %>% + dplyr::summarize(start = min(.data$id), end = max(.data$id) - empty_bar, .groups = "drop") %>% # Added .groups = "drop" for dplyr > 1.0.0 dplyr::rowwise() %>% dplyr::mutate(title = mean(c(.data$start, .data$end))) grid_data$end <- grid_data$end[c(nrow(grid_data), 1:nrow(grid_data) - 1)] + 1.5 @@ -456,9 +817,10 @@ splnr_plot_circBplot <- function(df, legend_color, legend_list, p <- ggplot2::ggplot(data, ggplot2::aes(x = as.factor(.data$id), y = .data$value, fill = .data$group)) + # plotting the bars - ggplot2::geom_bar(ggplot2::aes(x = as.factor(.data$id), y = .data$value, fill = .data$group), - stat = "identity", - position = "dodge" + ggplot2::geom_bar( + ggplot2::aes(x = as.factor(.data$id), y = .data$value, fill = .data$group), + stat = "identity", + position = "dodge" ) + # defining colors of the bars @@ -507,13 +869,13 @@ splnr_plot_circBplot <- function(df, legend_color, legend_list, label = c(25, 50, 75, 100), color = "grey50", size = 4, - angle = 0, #-5 + angle = 0, # -5 fontface = "bold", hjust = 0.5 ) + # setting limitations of actual plot - ggplot2::ylim(-130, 130) + #-140, 130 + ggplot2::ylim(-130, 130) + # -140, 130 ggplot2::theme_minimal() + ggplot2::coord_polar() + ggplot2::geom_text( @@ -525,10 +887,6 @@ splnr_plot_circBplot <- function(df, legend_color, legend_list, inherit.aes = FALSE ) + - # # Defining colors of these lines - # ggplot2::scale_color_manual(name = "Features", - # values = palette) + - ggplot2::theme( legend.position = "bottom", axis.text = ggplot2::element_blank(), @@ -538,11 +896,10 @@ splnr_plot_circBplot <- function(df, legend_color, legend_list, ) if (indicateTargets == TRUE) { - if (is.na(impTarget) | is.na(repTarget)) { - print("Please provide the targets you want to indicate.") - } + # assertthat check ensures impTarget and repTarget are non-NA numeric here p <- p + ggplot2::geom_abline(slope = 0, intercept = impTarget, col = colTarget, lty = 2) + ggplot2::geom_abline(slope = 0, intercept = repTarget, col = colTarget, lty = 2) } + return(p) } diff --git a/R/splnr_get_IUCNRedList.R b/R/splnr_get_IUCNRedList.R index 1d91ca5..f079846 100644 --- a/R/splnr_get_IUCNRedList.R +++ b/R/splnr_get_IUCNRedList.R @@ -1,64 +1,112 @@ -#' Match Species to IUCN RedList +#' @title Match Species to IUCN RedList Categories #' +#' @description +#' The `splnr_get_IUCNRedList` function retrieves IUCN Red List category information +#' for a given set of species and appends it to your input dataframe. #' -#' First of all you will need your own API key, an alphanumeric string provided by IUCN that you need to send in every request; -#' the following function takes you to their website, where you will need to fill up a form (it might take 1-2 days to receive your key) -#' rl_use_iucn() - -#' Once you receive an email with your API key, set it up as an environmental variable (it MUST be named IUCN_REDLIST_KEY) -#' you will need to re-do this step everytime you restart R - -#' Sys.setenv(IUCN_REDLIST_KEY = "") OR add IUCN_REDLIST_KEY = "" to your .Renviron file to permanently set it -#' Sys.getenv("IUCN_REDLIST_KEY") #' check - -#' Not Evaluated -#' DD: Data Deficient -#' LC: Least Concern -#' NT: Near Threatened -#' VU: Vulnerable -#' EN: Endangered -#' CR: Critically Endangered -#' EW: Extinct in the Wild -#' EX: Extinct -#' LRlc: Low risk – least concern -#' LRnt: Low risk – near threatened -#' LRcd: Low risk - conservation dependent - -#' Categories we care about -#' cate <- c("EX","EW","CR","EN","VU") -#' @param df The dataframe containing the species to be matched with the IUCN redlist -#' @param species_col A string name for the column containting the species name +#' @details +#' To use this function, you must first obtain an API key from IUCN. This is an +#' alphanumeric string required for every request. You can visit the IUCN website +#' to request a key using `rl_use_iucn()`. Please note that receiving your key +#' might take 1-2 days after submitting the form. +#' +#' Once you receive your API key, it is crucial to set it as an environment variable +#' named `IUCN_REDLIST_KEY`. You can do this temporarily for the current R session +#' using `Sys.setenv(IUCN_REDLIST_KEY = "YOUR_API_KEY_HERE")`. To set it permanently, +#' you should add `IUCN_REDLIST_KEY = "YOUR_API_KEY_HERE"` to your `.Renviron` file. +#' You can check if the key is set correctly using `Sys.getenv("IUCN_REDLIST_KEY")`. #' -#' @return A dataframe with an additional column `IUCN_Category` +#' The IUCN Red List uses various categories to assess extinction risk. This function +#' queries the Red List for the following categories: +#' \itemize{ +#' \item \strong{DD}: Data Deficient +#' \item \strong{LC}: Least Concern +#' \item \strong{NT}: Near Threatened +#' \item \strong{VU}: Vulnerable +#' \item \strong{EN}: Endangered +#' \item \strong{CR}: Critically Endangered +#' \item \strong{EW}: Extinct in the Wild +#' \item \strong{EX}: Extinct +#' \item \strong{LRlc}: Lower Risk / least concern (old category) +#' \item \strong{LRnt}: Lower Risk / near threatened (old category) +#' \item \strong{LRcd}: Lower Risk / conservation dependent (old category) +#' } +#' The function will attempt to match your species against any of these categories +#' present in the IUCN Red List database. +#' +#' @param df The input dataframe containing the species names to be matched. +#' @param species_col A character string specifying the name of the column in `df` +#' that contains the species scientific names (e.g., "Species" or "scientific_name"). +#' Defaults to "Species". +#' +#' @return A dataframe identical to the input `df`, but with an additional column +#' named `IUCN_Category`. If a species is not found on the IUCN Red List, its +#' `IUCN_Category` will be `NA`. #' @export #' #' @importFrom rlang := #' @importFrom rlang .data +#' @importFrom purrr map_df +#' @importFrom dplyr select rename left_join +#' @importFrom assertthat assert_that #' #' @examples #' \dontrun{ -#' df <- data.frame(Species = c("Diomedea exulans", "Hippocampus kuda", "Squatina squatina")) %>% +#' # Ensure your IUCN_REDLIST_KEY is set as an environment variable before running. +#' # For example: Sys.setenv(IUCN_REDLIST_KEY = "YOUR_API_KEY_HERE") +#' +#' # Example: Create a dataframe with species names and retrieve their IUCN Red List categories. +#' df_species_redlist <- data.frame(Species = c("Diomedea exulans", +#' "Hippocampus kuda", +#' "Squatina squatina")) %>% #' splnr_get_IUCNRedList() +#' print(df_species_redlist) +#' +#' # Example with a different column name for species +#' df_alt_col <- data.frame(ScientificName = c("Panthera leo", "Orcinus orca")) %>% +#' splnr_get_IUCNRedList(species_col = "ScientificName") +#' print(df_alt_col) #' } splnr_get_IUCNRedList <- function(df, species_col = "Species") { + # Assertions to validate input parameters. assertthat::assert_that( inherits(df, "data.frame"), + msg = "The 'df' parameter must be a data.frame." + ) + assertthat::assert_that( is.character(species_col), - species_col %in% names(df) + msg = "The 'species_col' parameter must be a character string." + ) + assertthat::assert_that( + species_col %in% names(df), + msg = paste0("The specified 'species_col' (\"", species_col, "\") does not exist in the input dataframe.") + ) + assertthat::assert_that( + nchar(Sys.getenv("IUCN_REDLIST_KEY")) > 0, + msg = "IUCN_REDLIST_KEY environment variable is not set. Please set your IUCN API key using Sys.setenv(IUCN_REDLIST_KEY = 'YOUR_KEY') or add it to your .Renviron file." ) - # Get all IUCN categories + #TODO add check for rredlist package + + # Define all possible IUCN Red List categories to retrieve data for. cate <- c("DD", "LC", "NT", "VU", "EN", "CR", "EW", "EX", "LRlc", "LRnt", "LRcd") - # Download all the data for those categories - RL <- purrr::map_df(cate, function(x) data.frame(rredlist::rl_sp_category(x))) %>% + # Download all data for the defined categories using rredlist::rl_categories. + # The map_df function iterates through each category, fetches species, and combines them into a single dataframe. + RL <- purrr::map_df(cate, function(x) data.frame(rredlist::rl_categories(x))) %>% + # Select only the 'category' and 'result.scientific_name' columns. dplyr::select("category", "result.scientific_name") %>% + # Rename the selected columns to match the input dataframe's species column name + # and a new column for the IUCN category for clarity. dplyr::rename(!!species_col := .data$result.scientific_name, IUCN_Category = .data$category ) - # Now try and link the species to the categories - only links 2 % + # Perform a left join to link the species in the input dataframe to their + # corresponding IUCN Red List categories. Species not found will have NA in IUCN_Category. df <- df %>% dplyr::left_join(RL, by = species_col) + + return(df) } diff --git a/R/splnr_get_MPAs.R b/R/splnr_get_MPAs.R index 47e75bb..78a8c0a 100644 --- a/R/splnr_get_MPAs.R +++ b/R/splnr_get_MPAs.R @@ -1,26 +1,86 @@ -#' Get marine parks from the WDPA. +#' @title Get Marine Protected Areas (MPAs) from WDPA #' -#' This code is a wrapper for the wonderful `wdpar` package written by Jeffrey O. Hanson. This data is then interfaced with the planning units. -#' An `sf` object is returned with the PU area covered by the selected marine protected areas. +#' @description +#' This function serves as a wrapper for the `wdpar` package, facilitating the +#' retrieval of Marine Protected Areas (MPAs) from the World Database on Protected +#' Areas (WDPA) and intersecting them with provided planning units. +#' The result is an `sf` object indicating the area of planning units covered by +#' the selected marine protected areas. #' -#' @param PlanUnits Planning Units as an `sf` object -#' @param Countries A character vector of the countries for which to extract MPAs. To get all MPAs, use `"global"` here. -#' @param Status The status field in the WDPA provides information on whether a protected area has been established, designated, or proposed at the time the data was submitted. -#' @param Desig The designation type is the category or type of protected area as legally/officially designated or proposed. -#' @param Category Stores the IUCN Protected Area Management Categories (recorded in field IUCN_CAT) for each of the protected areas where these categories are reported -#' @param ... Other arguments passed to `wdpa_fetch()` +#' @details +#' This function leverages the robust capabilities of the `wdpar` package by +#' Jeffrey O. Hanson to access and process WDPA data. It allows filtering of MPAs +#' based on country, status, designation type, and IUCN category, and then +#' spatially intersects these MPAs with your defined planning units. #' -#' @return A `sf` object with the MPAs intersected with the planning units +#' For a comprehensive understanding of the WDPA data fields: +#' \itemize{ +#' \item \strong{Status}: Refers to the establishment, designation, or proposal +#' status of a protected area at the time of data submission. Valid options +#' include "Designated", "Established", "Inscribed", "Proposed", and "Adopted". +#' \item \strong{Desig} (Designation Type): Categorizes the legal or official +#' designation of the protected area. Valid options include "National", +#' "Regional", "International", and "Not Applicable". +#' \item \strong{Category} (IUCN Protected Area Management Categories): Represents +#' the IUCN management categories for protected areas. Valid options include +#' "Ia", "Ib", "II", "III", "IV", "V", "VI", "Not Reported", "Not Applicable", +#' and "Not Assigned". +#' } +#' +#' @param PlanUnits An `sf` object representing the planning units to be used for intersection. +#' This object should have a valid CRS defined. +#' @param Countries A character vector specifying the countries for which to extract MPAs. +#' To retrieve all global MPAs, use the value `"global"`. Country names should match +#' those recognized by the WDPA database. +#' @param Status A character vector specifying the desired status of protected areas +#' to include. Defaults to `c("Designated", "Established", "Inscribed")`. +#' @param Desig A character vector specifying the desired designation types of +#' protected areas. Defaults to `c("National", "Regional", "International", "Not Applicable")`. +#' @param Category A character vector specifying the desired IUCN Protected Area +#' Management Categories. Defaults to `c("Ia", "Ib", "II", "III", "IV")`. +#' @param ... Other arguments that are passed directly to the `wdpa_fetch()` function +#' from the `wdpar` package (e.g., `verbose = TRUE`). +#' +#' @return An `sf` object. This object contains the planning units, with an +#' additional `wdpa` column (set to 1) for areas that intersect with the +#' selected MPAs. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr bind_rows filter select mutate +#' @importFrom purrr map +#' @importFrom rlang .data +#' @importFrom rappdirs user_data_dir +#' @importFrom sf st_as_sf +#' @importFrom spatialgridr get_data_in_grid +#' #' @examples -#' dat <- splnr_get_MPAs(PlanUnits = dat_PUs, Countries = "Australia") +#' \dontrun{ +#' # Assuming 'dat_PUs' is an existing sf object of planning units in your package. +#' +#' # Example: Get MPAs for Australia and intersect with planning units. +#' dat_mpas <- splnr_get_MPAs(PlanUnits = dat_PUs, Countries = "Australia") #' +#' # Example: Get MPAs for multiple countries with specific status and categories. +#' dat_mpas_specific <- splnr_get_MPAs( +#' PlanUnits = dat_PUs, +#' Countries = c("Australia", "New Zealand"), +#' Status = c("Designated", "Proposed"), +#' Category = c("II", "IV") +#' ) +#' +#' # Example: Visualize the result using ggplot2. +#' # Assuming 'aust' is an sf object representing Australia's coastline, +#' # perhaps loaded from rnaturalearth::ne_countries. #' aust <- rnaturalearth::ne_countries(country = "Australia", returnclass = "sf") #' #' gg <- ggplot2::ggplot() + -#' ggplot2::geom_sf(data = dat, ggplot2::aes(fill = wdpa)) + -#' ggplot2::geom_sf(data = aust, fill = "grey50") +#' ggplot2::geom_sf(data = dat_mpas, ggplot2::aes(fill = wdpa)) + +#' ggplot2::geom_sf(data = aust, fill = "grey50") + +#' ggplot2::labs(title = "Marine Protected Areas in Australia") + +#' ggplot2::theme_minimal() +#' print(gg) +#' } splnr_get_MPAs <- function(PlanUnits, Countries, Status = c("Designated", "Established", "Inscribed"), @@ -28,32 +88,66 @@ splnr_get_MPAs <- function(PlanUnits, Category = c("Ia", "Ib", "II", "III", "IV"), ...) { + # Assertions to validate input parameters. assertthat::assert_that( inherits(PlanUnits, "sf"), + msg = "The 'PlanUnits' parameter must be an 'sf' object." + ) + assertthat::assert_that( is.character(Countries), + msg = "The 'Countries' parameter must be a character vector." + ) + assertthat::assert_that( all(Status %in% c("Designated", "Established", "Inscribed", "Proposed", "Adopted")), + msg = "Invalid 'Status' provided. Must be one or more of: 'Designated', 'Established', 'Inscribed', 'Proposed', 'Adopted'." + ) + assertthat::assert_that( all(Desig %in% c("National", "Regional", "International", "Not Applicable")), - all(Category %in% c("Ia", "Ib", "II", "III", "IV", "V", "VI", "Not Reported", "Not Applicable", "Not Assigned")) + msg = "Invalid 'Desig' provided. Must be one or more of: 'National', 'Regional', 'International', 'Not Applicable'." ) + assertthat::assert_that( + all(Category %in% c("Ia", "Ib", "II", "III", "IV", "V", "VI", "Not Reported", "Not Applicable", "Not Assigned")), + msg = "Invalid 'Category' provided. Must be one or more of valid IUCN categories (e.g., 'Ia', 'Ib', 'II', 'III', 'IV', 'V', 'VI', 'Not Reported', 'Not Applicable', 'Not Assigned')." + ) + + + # TODO Add a check for wdpar package + # Set a chromote timeout option to prevent issues with web scraping for WDPA data. options(chromote.timeout = 120) + # Fetch WDPA data for the specified countries and then process it. wdpa_data <- Countries %>% + # Use purrr::map to fetch WDPA data for each country in the 'Countries' vector. + # 'wait = TRUE' ensures sequential downloads, and 'download_dir' specifies where to cache the data. purrr::map(wdpar::wdpa_fetch, - wait = TRUE, - download_dir = rappdirs::user_data_dir("wdpar"), - ...) %>% + wait = TRUE, + download_dir = rappdirs::user_data_dir("wdpar"), + ...) %>% + # Bind all fetched data frames into a single data frame. dplyr::bind_rows() %>% + # Filter for marine protected areas only (where MARINE attribute is greater than 0). dplyr::filter(.data$MARINE > 0) %>% - dplyr::filter(.data$IUCN_CAT %in% Category) %>% # filter category - dplyr::filter(.data$DESIG_TYPE %in% Desig) %>% # filter designation - dplyr::filter(.data$STATUS %in% Status) %>% # filter status - wdpar::wdpa_clean(retain_status = NULL, erase_overlaps = FALSE) %>% # clean protected area data - wdpar::wdpa_dissolve() %>% # Dissolve data to remove overlapping areas. + # Filter by the specified IUCN Protected Area Management Categories. + dplyr::filter(.data$IUCN_CAT %in% Category) %>% + # Filter by the specified Designation Types. + dplyr::filter(.data$DESIG_TYPE %in% Desig) %>% + # Filter by the specified Status of the protected area. + dplyr::filter(.data$STATUS %in% Status) %>% + # Clean the protected area data using wdpar::wdpa_clean, removing any invalid geometries + # or other issues. 'retain_status = NULL' means all statuses are considered for cleaning. + # 'erase_overlaps = FALSE' means overlapping polygons are not removed at this stage. + wdpar::wdpa_clean(retain_status = NULL, erase_overlaps = FALSE) %>% + # Dissolve the protected area polygons to merge adjacent or overlapping areas into single geometries. + wdpar::wdpa_dissolve() %>% + # Select only the 'geometry' column, discarding other attributes after dissolving. dplyr::select("geometry") %>% + # Add a new column 'wdpa' and set its value to 1, indicating it's a WDPA area. dplyr::mutate(wdpa = 1) + # Intersect the cleaned WDPA data with the provided planning units using spatialgridr::get_data_in_grid. + # This function identifies which planning units overlap with the WDPA areas. wdpa_data <- spatialgridr::get_data_in_grid(spatial_grid = PlanUnits, dat = wdpa_data, cutoff = NULL) diff --git a/R/splnr_get_boundary.R b/R/splnr_get_boundary.R index aa24a6a..a75f7b1 100755 --- a/R/splnr_get_boundary.R +++ b/R/splnr_get_boundary.R @@ -1,48 +1,154 @@ -#' Get the boundary of the planning region. +#' @title Create a Planning Region Boundary #' -#' `splnr_get_boundary()` allows to create an `sf` object of your planning region either based on specific coordinate information, or `rnaturalearth` inputs such as ocean data. Creating a boundary is often the first step in conservation planning and a requirement for downstream function sin `spatialplanr`. +#' @description +#' This function generates a spatial boundary for the planning region as an `sf` +#' polygon object. The boundary can be defined in several ways: +#' 1. A simple rectangular bounding box using numeric coordinates. +#' 2. A global boundary spanning the entire world. +#' 3. A complex shape based on marine ecoregions from `rnaturalearth`. #' -#' @param Limits The limits of the boundary. This can either be a 4 element numeric named vector (c(xmin = 150, xmax = 160, ymin = -40, ymax = -30)), a vector of ocean/sea names, or a vector of EEZs., -#' @param Type The type of Limits being provided. Options are "Ocean" or "EEZ". (not required if numeric or "Global" limits are provided) -#' @param res The resolution (in degrees) from which to create the boundary polygon if numeric limits are provided. -#' @param cCRS The CRS the boundary is to be returned in +#' @details +#' A planning region boundary is the foundational first step for most spatial +#' conservation planning exercises. All subsequent analyses and data preparation +#' steps within the `spatialplanr` package rely on a defined boundary. The +#' coordinate reference system (CRS) of the returned object is projected by +#' default (Mollweide), which is suitable for equal-area calculations. +#' +#' @param Limits A required input that defines the spatial extent. This can be: +#' \itemize{ +#' \item A named numeric vector of four elements: `c("xmin" = ..., "xmax" = ..., "ymin" = ..., "ymax" = ...)`. +#' \item The string `"Global"` to create a worldwide boundary. +#' \item A character vector of ocean/sea names (e.g., `"North Atlantic Ocean"`) to be used with `Type = "Ocean"`. +#' } +#' @param Type `r lifecycle::badge("deprecated")` The type of Limits being provided. This is only required if `Limits` is a character vector of ocean names, in which case it should be `"Ocean"`. It is no longer required and will be removed in a future version. +#' @param res `[numeric(1)]`\cr The resolution (in decimal degrees) used to +#' construct the polygon vertices when `Limits` is numeric or `"Global"`. +#' Defaults to `1`. Must be a positive number. +#' @param cCRS `[character(1)]`\cr The coordinate reference system (CRS) for the +#' output `sf` object. Can be a PROJ4 string or an EPSG code. Defaults to +#' `"ESRI:54009"` (Mollweide). +#' +#' @family planning_region +#' +#' @return An `sf` object containing a single polygon feature representing the +#' planning boundary. #' -#' @return The boundary of the planning region #' @export #' +#' @importFrom assertthat assert_that is.string is.flag +#' @importFrom dplyr tibble bind_rows filter +#' @importFrom rnaturalearth ne_download +#' @importFrom sf st_crs st_join st_as_sf st_union st_transform st_set_crs st_polygon st_sfc +#' @importFrom lifecycle deprecate_warn #' @importFrom rlang .data #' #' @examples -#' Bndry <- splnr_get_boundary(Limits = "North Atlantic Ocean", Type = "Ocean") -#' Bndry <- splnr_get_boundary(Limits = "Global") -#' Bndry <- splnr_get_boundary(Limits = c("xmin" = 150, "xmax" = 170, "ymin" = -40, "ymax" = -20)) +#' \dontrun{ +#' # Example 1: Create a boundary from an ocean name. +#' # This fetches polygon data for the specified ocean. +#' bndry_ocean <- splnr_get_boundary(Limits = "North Atlantic Ocean", Type = "Ocean") +#' plot(bndry_ocean) +#' +#' # Example 2: Create a global boundary. +#' bndry_global <- splnr_get_boundary(Limits = "Global") +#' plot(bndry_global) +#' +#' # Example 3: Create a boundary from a numeric bounding box. +#' bndry_coords <- splnr_get_boundary( +#' Limits = c("xmin" = 150, "xmax" = 170, "ymin" = -40, "ymax" = -20) +#' ) +#' plot(bndry_coords) +#' } splnr_get_boundary <- function(Limits, Type = NULL, res = 1, cCRS = "ESRI:54009" # Mollweide ) { - assertthat::assert_that(res > 0, msg = "Resolution 'res' must be greater than zero.") + # Input validation using assertthat + assertthat::assert_that( + !missing(Limits), + msg = "'Limits' is a required argument." + ) + assertthat::assert_that( + is.numeric(res) && length(res) == 1 && res > 1e-6, + msg = "'res' must be a single positive numeric value." + ) + assertthat::assert_that( + is.character(cCRS) && length(cCRS) == 1, + msg = "'cCRS' must be a single character string." + ) + + # Validate 'Limits' based on its type + is_numeric_limits <- is.numeric(Limits) && length(Limits) == 4 && !is.null(names(Limits)) && all(sort(names(Limits)) == sort(c("xmin", "xmax", "ymin", "ymax"))) + is_global_limit <- is.character(Limits) && length(Limits) == 1 && identical(Limits, "Global") + is_ocean_limits <- is.character(Limits) && !identical(Limits, "Global") + is_character_limits <- is.character(Limits) + + assertthat::assert_that( + is_numeric_limits || is_global_limit || is_ocean_limits, + msg = paste0( + "'Limits' must be either:\n", + " - A named numeric vector of four: c(xmin=..., xmax=..., ymin=..., ymax=...)\n", + " - The string 'Global'\n", + " - A character vector of ocean/sea names (e.g., 'North Atlantic Ocean')" + ) + ) - if (is.numeric(Limits) || Limits == "Global") { - assertthat::assert_that(missing(Type), msg = "Type must be missing when Limits are numeric or global.")} + # Validate 'Type' in relation to 'Limits' + if (is_character_limits && !is_global_limit) { # Only validate Type if Limits is a character vector and not "Global" + assertthat::assert_that( + is.character(Type) && length(Type) == 1, + msg = "`Type` must be a single character string when `Limits` is a character vector (other than 'Global')." + ) + assertthat::assert_that( + Type %in% c("Oceans", "Ocean", "EEZ"), # Include EEZ as per original function, even if commented out later. + msg = paste0("When `Limits` is an ocean name, `Type` must be 'Ocean' or 'Oceans' (or 'EEZ').") + ) + } + + # Check that Type is not provided for numeric or "Global" limits and warn + if ((is_numeric_limits || is_global_limit) && !is.null(Type)) { + warning("`Type` is ignored when `Limits` is a numeric bounding box or 'Global'.") + } - if (is.numeric(Limits)) { + # # Deprecation warning for Type argument + # if (!is.null(Type)){ + # lifecycle::deprecate_warn( + # when = "0.2.0", + # what = "splnr_get_boundary(Type)", + # details = "The `Type` argument is no longer necessary and will be removed in a future release." + # ) + # } + + # Handle numeric limits to create a rectangular boundary + if (is_numeric_limits) { + # Check that min is less than max + assertthat::assert_that(Limits["xmin"] < Limits["xmax"], msg = "'xmin' must be less than 'xmax'.") + assertthat::assert_that(Limits["ymin"] < Limits["ymax"], msg = "'ymin' must be less than 'ymax'.") + + # Construct the boundary polygon from coordinates Bndry <- dplyr::tibble(x = seq(Limits["xmin"], Limits["xmax"], by = res), y = Limits["ymin"]) %>% dplyr::bind_rows(dplyr::tibble(x = Limits["xmax"], y = seq(Limits["ymin"], Limits["ymax"], by = res))) %>% dplyr::bind_rows(dplyr::tibble(x = seq(Limits["xmax"], Limits["xmin"], by = -res), y = Limits["ymax"])) %>% dplyr::bind_rows(dplyr::tibble(x = Limits["xmin"], y = seq(Limits["ymax"], Limits["ymin"], by = -res))) %>% + # Convert the points to an sf polygon splnr_create_polygon(cCRS) %>% + # Convert to an sf object sf::st_sf() return(Bndry) } - if (Limits == "Global") { + # Handle the "Global" limit case + if (is_global_limit) { + # Construct a global boundary polygon Bndry <- dplyr::tibble(x = seq(-180, 180, by = res), y = -90) %>% dplyr::bind_rows(dplyr::tibble(x = 180, y = seq(-90, 90, by = res))) %>% dplyr::bind_rows(dplyr::tibble(x = seq(180, -180, by = -res), y = 90)) %>% dplyr::bind_rows(dplyr::tibble(x = -180, y = seq(90, -90, by = -res))) %>% + # Convert the points to an sf polygon splnr_create_polygon(cCRS) %>% + # Convert to an sf object sf::st_sf() return(Bndry) @@ -50,6 +156,8 @@ splnr_get_boundary <- function(Limits, ## TODO Disable EEZ until offshoredatr publicly online. # if (Type == "EEZ"){ + # # This part relies on an external package not universally available or stable. + # # It's commented out but kept for structural completeness if offshoredatr becomes stable. # Bndry <- offshoredatr::get_area(area_name = Limits) %>% # dplyr::filter(.data$territory1 %in% Limits) %>% # sf::st_union() %>% @@ -57,17 +165,26 @@ splnr_get_boundary <- function(Limits, # return(Bndry) # } - if (Type == "Oceans" | Type == "Ocean") { + # Handle Ocean limits using rnaturalearth + if (is_ocean_limits) { + # Download marine polygons from rnaturalearth Bndry <- rnaturalearth::ne_download( scale = "large", category = "physical", type = "geography_marine_polys", returnclass = "sf" ) %>% + # Filter for the specified ocean(s) dplyr::filter(.data$name %in% Limits) %>% + # Unify the polygons into a single feature sf::st_union() %>% + # Transform to the specified CRS sf::st_transform(cCRS) %>% + # Convert to an sf object sf::st_sf() + return(Bndry) } + # Fallback for unexpected Limits/Type combinations + stop("Invalid 'Limits' or 'Type' combination provided. Please check the function documentation.") } diff --git a/R/splnr_get_distCoast.R b/R/splnr_get_distCoast.R index e0b69ac..37ac46a 100644 --- a/R/splnr_get_distCoast.R +++ b/R/splnr_get_distCoast.R @@ -1,78 +1,123 @@ -#' Function to compute distances to nearest coastline for each centroid of each planning unit in the 'sf' object provided. +#' @title Calculate Distance to Coastline #' -#' The code takes a sf object and return it updated with a new coastDistance column. -#' The output inherits the crs from this sf object so ensure it is in the correct projection for your needs +#' @description +#' This function calculates the shortest distance from the centroid of each +#' planning unit in an `sf` object to the nearest coastline. It can use either +#' a default coastline from the `rnaturalearth` package or a custom-provided +#' coastline `sf` object. #' -#' Written by Kristine Buenafe -#' Written: March/April 2023 -#' Modified by Kilian Barreiro -#' Updated: December 2023 +#' @details +#' The function adds a new column named `coastDistance_km` to the input `sf` +#' object, containing the calculated distances in kilometers. The CRS of the +#' input data is preserved. It is crucial to ensure the input `sf` object has +#' a suitable projected CRS for accurate distance calculations. #' -#' @param dat_sf An sf object. -#' @param custom_coast An sf coastline object (optional) -#' @param res Allow user to choose resolution (`small`, `medium`, `large`) of `rnaturalearth` data used for coastline. +#' @param dat_sf `[sf]` \cr An `sf` object containing polygon or point features +#' representing the planning units. Must have a valid CRS. +#' @param custom_coast `[sf]` \cr An optional `sf` object representing a +#' custom coastline. If `NULL` (the default), the coastline is downloaded +#' from `rnaturalearth`. +#' @param res `[character(1)]` \cr The resolution of the `rnaturalearth` +#' coastline to use. Options are `"small"`, `"medium"` (default), or +#' `"large"`. This parameter is ignored if `custom_coast` is provided. +#' +#' @family cost_features +#' +#' @return An `sf` object identical to `dat_sf` but with an added column +#' `coastDistance_km` representing the distance to the nearest coastline in +#' kilometers. +#' +#' @importFrom assertthat assert_that is.flag +#' @importFrom units set_units drop_units +#' @importFrom rnaturalearth ne_coastline +#' @importFrom sf st_crs st_centroid st_geometry st_distance st_transform +#' @importFrom rlang .data #' -#' @return An `sf` object with distances to the nearest coast #' @export #' #' @examples +#' \dontrun{ +#' # Example 1: Calculate distance to coast for a simple grid #' bbox <- sf::st_bbox(c(xmin = 0, ymin = 0, xmax = 3, ymax = 3)) -#' grid <- sf::st_make_grid(bbox, n = c(3, 3), what = "polygons") -#' grid <- sf::st_sf(geometry = grid) %>% -#' sf::st_set_crs("EPSG:4326") -#' splnr_get_distCoast(grid) -#' -#' cCRS <- "ESRI:54009" +#' grid <- sf::st_as_sf(sf::st_make_grid(bbox, n = c(3, 3))) +#' grid_with_dist <- splnr_get_distCoast(grid) +#' plot(grid_with_dist["coastDistance_km"]) #' -#' Bndry <- splnr_get_boundary(Limits = "Coral Sea", -#' Type = "Oceans", -#' cCRS = cCRS) +#' # Example 2: Using a specific resolution for the coastline +#' # Note: Requires the 'dat_sf' object to be created first, e.g., using +#' # splnr_get_planning_units() +#' if (exists("dat_sf")) { +#' dat_sf_dist <- splnr_get_distCoast(dat_sf, res = "large") +#' summary(dat_sf_dist$coastDistance_km) +#' } #' +#' # Example 3: Using a custom coastline +#' # First, create a custom coastline (e.g., from a country polygon) #' landmass <- rnaturalearth::ne_countries( #' scale = "medium", #' returnclass = "sf" -#' ) %>% -#' sf::st_transform(cCRS) +#' ) #' -# dat_sf <- spatialgridr::get_grid(boundary = Bndry, -# projection_crs = cCRS, -# option = "sf_hex", -# resolution = 10000, -# sf_method = "centroid") %>% -# splnr_get_distCoast(res = "medium") - -splnr_get_distCoast <- function(dat_sf, custom_coast = NULL, res = NULL) { - +#' if (exists("dat_sf") && exists("landmass")) { +#' # Transform landmass to the same CRS as the planning units +#' landmass_proj <- sf::st_transform(landmass, sf::st_crs(dat_sf)) +#' dat_sf_custom_coast <- splnr_get_distCoast(dat_sf, custom_coast = landmass_proj) +#' summary(dat_sf_custom_coast$coastDistance_km) +#' } +#' } +splnr_get_distCoast <- function(dat_sf, custom_coast = NULL, res = "medium") { + # Input validation using assertthat assertthat::assert_that( inherits(dat_sf, "sf"), - !is.null(sf::st_crs(dat_sf)), + msg = "'dat_sf' must be an 'sf' object." + ) + assertthat::assert_that( + !is.na(sf::st_crs(dat_sf)), + msg = "'dat_sf' must have a valid Coordinate Reference System (CRS)." + ) + assertthat::assert_that( is.null(custom_coast) || inherits(custom_coast, "sf"), - is.null(res) || res %in% c("small", "medium", "large") + msg = "'custom_coast' must be either NULL or an 'sf' object." + ) + assertthat::assert_that( + is.character(res) && length(res) == 1 && res %in% c("small", "medium", "large"), + msg = "'res' must be a single character string: 'small', 'medium', or 'large'." ) - # Load coast + # Load or prepare the coastline data if (is.null(custom_coast)) { - if (is.null(res)) {res <- "medium"} - coast <- rnaturalearth::ne_coastline(scale = res) %>% - sf::st_as_sf() %>% + message(paste0("Downloading coastline data from rnaturalearth at '", res, "' resolution.")) + # If no custom coast is provided, download from rnaturalearth + coast <- rnaturalearth::ne_coastline(scale = res, returnclass = "sf") %>% + # Transform the coastline to match the CRS of the input data sf::st_transform(crs = sf::st_crs(dat_sf)) } else { + message("Using custom coastline data.") + # If a custom coast is provided, use it coast <- custom_coast %>% + # Ensure the custom coastline has the same CRS as the input data sf::st_transform(crs = sf::st_crs(dat_sf)) } - # Convert grid to points (centroids) + # Calculate centroids of the planning units + # Using centroids is a standard approach to represent the location of each planning unit + message("Calculating centroids for planning units.") grid_centroid <- sf::st_centroid(sf::st_geometry(dat_sf)) - # Get distance matrix + # Calculate the distance matrix between each planning unit centroid and the coastline + message("Calculating distances to coastline.") dist_mat <- sf::st_distance(grid_centroid, coast) %>% - units::set_units("km") %>% # Convert to km - units::drop_units() # Remove units (include in header below) + # Explicitly set the distance units to kilometers + units::set_units("km") %>% + # Drop the units class to get a numeric matrix for easier computation + units::drop_units() - # Find min distance for each row and convert to km. + # Find the minimum distance for each planning unit (each row in the matrix) + # This identifies the shortest distance from each centroid to any part of the coastline + message("Finding minimum distances and adding to dataframe.") dat_sf$coastDistance_km <- do.call(pmin, as.data.frame(dist_mat)) + # Return the original sf object with the new distance column + message("Distance calculation complete.") return(dat_sf) } - - diff --git a/R/splnr_get_gfw.R b/R/splnr_get_gfw.R index 2411a5b..ef4ead5 100644 --- a/R/splnr_get_gfw.R +++ b/R/splnr_get_gfw.R @@ -1,43 +1,81 @@ -#' The `get_gfwData` function recover the data of Global Fishing Watch and -#' returns it as a sf object. +#' @title Retrieve Global Fishing Watch Data #' +#' @description +#' The `splnr_get_gfw` function retrieves Global Fishing Watch (GFW) data and +#' returns it as an `sf` (simple features) object. This function allows for +#' flexible data queries based on geographical region, time range, and desired +#' spatial and temporal resolutions. #' -#' The possibilities offered by this function are explained in `vignette("GlobalFishingWatch")` +#' @details +#' The possibilities offered by this function are extensively explained in +#' `vignette("GlobalFishingWatch")`. #' -#' We have the same parameters than the `get_raster` function, plus `cCRS` -#' which is the crs for the sf_modification
-#' Different possible values can be combined and are :
-#' - `Time Range`, `Flag`, `Geartype`.
-#' __(A combination can be : c('Time Range','Geartype'), if you want to get__ -#' __the sum of fishing hours per date and geartype, for example you want to__ -#' __display the drifting longline fishing in a specific year)__

-#' __Notes :__
-#' 1. For the moment we are limited to the EEZs of each region, but we can -#' potentially restrict the working area to specific MPAs.
-#' 2. Days indicated in the__ `start_date` __and__ `end_date` __variables are -#' included in the data recovery. +#' This function shares many parameters with the `get_raster` function from the +#' `gfwr` package, with the addition of `cCRS` for specifying the Coordinate +#' Reference System of the output `sf` object. #' -#' The code takes several parameters described below and return an sf object -#' with gfw data aggregated or not (param compress) +#' Fishing activity data can be aggregated (`group_by`) by "FLAGANDGEARTYPE" +#' by default, combining flags and gear types. #' -#' @param region Region studied (character) or a geojson shape to filter raster -#' @param start_date Start date (waited format : "%Y-%m-%d"). -#' @param end_date End date (waited format : "%Y-%m-%d"). -#' @param temp_res Temporal resolution ("daily","monthly","yearly"). -#' @param spat_res Spatial resolution ("low" for 0.1 degree, "high" for 0.01 degree). -#' @param region_source source of the region ('eez','mpa', 'rfmo' or 'user_json') -#' @param key Token for GFW API (see details GlobalFishingWatch vignette). -#' @param cCRS The crs to which the sf will be returned (default = "EPSG:4326"). -#' @param compress Binary operator to compress (aggregate) the data per coordinates (default = FALSE). +#' \strong{Notes:} +#' \itemize{ +#' \item Currently, the function is primarily designed for data within +#' Exclusive Economic Zones (EEZs), but it can potentially be +#' extended to specific Marine Protected Areas (MPAs) or RFMOs. +#' \item Days specified in the `start_date` and `end_date` variables are +#' inclusive in the data recovery. +#' } #' -#' @return An `sf` object with gfw data. +#' @param region A character string specifying the name of the region (e.g., an EEZ name) +#' or a numeric ID for the region, or an `sf` object if `region_source` is set +#' to "USER_SHAPEFILE". +#' @param start_date The start date for data retrieval, expected in "%Y-%m-%d" format (e.g., "2021-01-01"). +#' @param end_date The end date for data retrieval, expected in "%Y-%m-%d" format (e.g., "2022-12-31"). +#' @param temp_res The desired temporal resolution for the data. Must be one of: +#' "DAILY", "MONTHLY", or "YEARLY". +#' @param spat_res The desired spatial resolution for the data. Must be one of: +#' "LOW" (0.1 degree) or "HIGH" (0.01 degree). Defaults to "LOW". +#' @param region_source The source of the region definition. Must be one of: +#' 'EEZ', 'MPA', 'RFMO', or 'USER_SHAPEFILE'. Defaults to "EEZ". +#' @param key Your API token for the GFW API. If not provided, it attempts to +#' authenticate using `gfwr::gfw_auth()`. See the GlobalFishingWatch vignette +#' for details on obtaining a key. +#' @param cCRS The Coordinate Reference System (CRS) to which the output `sf` object +#' will be transformed. Defaults to "EPSG:4326". +#' @param compress A logical value. If `TRUE`, the data will be compressed (aggregated) +#' by coordinates, summing fishing hours for each unique location. If `FALSE`, +#' the raw data points are returned. Defaults to `FALSE`. +#' +#' @return An `sf` object containing the requested GFW data. The structure of +#' the `sf` object will vary depending on the `compress` and `temp_res` +#' parameters. #' @export #' #' @examples #' \dontrun{ -#' gfw_data <- splnr_get_gfw('Australia', "2021-01-01", "2022-12-31", "YEARLY", -#' cCRS = "ESRI:54009", compress = TRUE) -#'} +#' # Example: Retrieve yearly GFW data for Australia, transformed to a +#' # Mollweide projection (ESRI:54009) and compressed (aggregated) by location. +#' gfw_data <- splnr_get_gfw( +#' region = 'Australia', +#' start_date = "2021-01-01", +#' end_date = "2022-12-31", +#' temp_res = "YEARLY", +#' cCRS = "ESRI:54009", +#' compress = TRUE +#' ) +#' +#' # Example: Retrieve monthly GFW data for a specific EEZ ID, +#' # keeping individual time ranges and locations. +#' # Note: Replace 1000 with an actual EEZ ID if needed for testing. +#' gfw_data_monthly <- splnr_get_gfw( +#' region = 1000, # Example numeric EEZ ID +#' start_date = "2022-01-01", +#' end_date = "2022-03-31", +#' temp_res = "MONTHLY", +#' region_source = "EEZ", +#' compress = FALSE +#' ) +#' } splnr_get_gfw <- function(region, start_date, end_date, @@ -48,47 +86,82 @@ splnr_get_gfw <- function(region, cCRS = "EPSG:4326", compress = FALSE) { - assertthat::assert_that((is.character(region) | is.numeric(region)), - inherits(start_date, "character") && !is.na(as.Date(start_date, "%Y-%m-%d")), #is.Date ? - inherits(end_date, "character") && !is.na(as.Date(end_date, "%Y-%m-%d")), - temp_res %in% c("DAILY", "MONTHLY", "YEARLY"), - spat_res %in% c("LOW", "HIGH"), - region_source %in% c('EEZ', 'MPA', 'RFMO', 'USER_SHAPEFILE'), - is.character(key), - is.character(cCRS), - is.logical(compress)) + # Assertions for input parameters to ensure correct types and values + assertthat::assert_that( + (is.character(region) || is.numeric(region) || (region_source == "USER_SHAPEFILE" && inherits(region, "sf"))), + msg = "The 'region' parameter must be a character (name), numeric (ID), or an 'sf' object if 'region_source' is 'USER_SHAPEFILE'." + ) + assertthat::assert_that( + inherits(start_date, "character") && !is.na(as.Date(start_date, "%Y-%m-%d")), + msg = "The 'start_date' parameter must be a character string in 'YYYY-MM-DD' format." + ) + assertthat::assert_that( + inherits(end_date, "character") && !is.na(as.Date(end_date, "%Y-%m-%d")), + msg = "The 'end_date' parameter must be a character string in 'YYYY-MM-DD' format." + ) + assertthat::assert_that( + temp_res %in% c("DAILY", "MONTHLY", "YEARLY"), + msg = "The 'temp_res' parameter must be one of 'DAILY', 'MONTHLY', or 'YEARLY'." + ) + assertthat::assert_that( + spat_res %in% c("LOW", "HIGH"), + msg = "The 'spat_res' parameter must be one of 'LOW' or 'HIGH'." + ) + assertthat::assert_that( + region_source %in% c('EEZ', 'MPA', 'RFMO', 'USER_SHAPEFILE'), + msg = "The 'region_source' parameter must be one of 'EEZ', 'MPA', 'RFMO', or 'USER_SHAPEFILE'." + ) + assertthat::assert_that( + is.character(key), + msg = "The 'key' parameter must be a character string (your GFW API token)." + ) + assertthat::assert_that( + is.character(cCRS), + msg = "The 'cCRS' parameter must be a character string representing a valid CRS (e.g., 'EPSG:4326')." + ) + assertthat::assert_that( + is.logical(compress), + msg = "The 'compress' parameter must be a logical value (TRUE or FALSE)." + ) + # Define an internal helper function to fetch GFW data for a single region. get_gfw_byRegion <- function(region){ - if (region_source == "EEZ" & is.character(region)){ # Only process eez. RFMO and geojson have bugs + # Determine the region ID based on the region_source and region type. + if (region_source == "EEZ" & is.character(region)){ region_id <- gfwr::get_region_id(region_name = region, region_source = region_source, key = key)$id } else if (region_source == "EEZ" & is.numeric(region)){ + # If region is numeric for EEZ, assume it's already an ID. region_id <- region } else if (region_source == "RFMO"){ - region_id <- region # gfwr retuns NULL for region ID due to a bug in as.numeric(ID) - } else if (methods::is(region, "USER_SHAPEFILE")){ - region_id <- region # Use region as is + # For RFMO, pass the region as is; handles potential gfwr package quirks. + region_id <- region + } else if (region_source == "USER_SHAPEFILE"){ + # If region_source is USER_SHAPEFILE, use the provided region (assumed to be an sf object). + region_id <- region } - # Convert dates into Date objects + # Convert start_date and end_date strings to Date objects. start_date <- as.Date(start_date, format = "%Y-%m-%d") end_date <- as.Date(end_date, format = "%Y-%m-%d") - # Function to obtain data for a specific date range + # Define a nested helper function to obtain data for a specific date range within the loop. get_data_for_range <- function(start_date, end_date, rid) { + # Call the gfwr::get_raster function to retrieve GFW raster data. data <- gfwr::get_raster( spatial_resolution = spat_res, temporal_resolution = temp_res, - group_by = 'FLAGANDGEARTYPE', + group_by = 'FLAGANDGEARTYPE', # Group by flag and geartype. start_date = start_date, end_date = end_date, region = rid, region_source = region_source, key = key) + # Mutate and rename columns for consistency and clarity. data <- data %>% - dplyr::mutate(GFWregionID = rid) %>% + dplyr::mutate(GFWregionID = rid) %>% # Add a column for the GFW region ID. dplyr::rename(TimeRange = .data$`Time Range`, VesselID = .data$`Vessel IDs`, ApparentFishingHrs = .data$`Apparent Fishing Hours`) @@ -96,50 +169,56 @@ splnr_get_gfw <- function(region, return(data) } - # Create expanded dataframe with all combinations + # Create an expanded grid of dates and regions to iterate over, splitting long date ranges. + # This helps in fetching data in manageable chunks, as GFW API might have limitations on date ranges. eg <- tidyr::expand_grid( - Date = seq(start_date, end_date, by = "366 days"), + Date = seq(start_date, end_date, by = "366 days"), # Step by 366 days to ensure yearly chunks, adapting to leap years. Region = region_id ) + # Use purrr::map2 to apply the get_data_for_range function to each date chunk and region. + # Then, drop empty list elements and bind all data frames into a single data frame. data_df <- purrr::map2(eg$Date, eg$Region, ~ get_data_for_range(.x, min(.x + 365, end_date), .y)) %>% vctrs::list_drop_empty() %>% dplyr::bind_rows() + # Check if the resulting data frame is empty and stop with an informative message if no data is found. if(rlang::is_empty(data_df)){ stop(paste0("No data found at all for the requested area of ", region, " between ", start_date, " and ", end_date)) } - + # Process data based on the 'compress' parameter. if (isTRUE(compress)){ + # Group data by Lon and Lat and summarise (sum) Apparent Fishing Hours for compression. data_df <- data_df %>% dplyr::group_by(.data$Lon, .data$Lat) %>% dplyr::summarise("ApparentFishingHrs" = sum(.data$ApparentFishingHrs, na.rm = TRUE), GFWregionID = dplyr::first(.data$GFWregionID)) %>% dplyr::ungroup() + # Convert the aggregated data frame to a 'terra' raster, then to polygons, and finally to an 'sf' object. data_sf <- data_df %>% - terra::rast(type = "xyz", crs = "EPSG:4326") %>% # Convert to polygons for easier use - terra::as.polygons(trunc = FALSE, dissolve = FALSE, na.rm = TRUE, round = FALSE) %>% - sf::st_as_sf() %>% - dplyr::mutate(GFWregionID = as.factor(.data$GFWregionID)) + terra::rast(type = "xyz", crs = "EPSG:4326") %>% # Convert to a raster from XYZ data with WGS84 CRS. + terra::as.polygons(trunc = FALSE, dissolve = FALSE, na.rm = TRUE, round = FALSE) %>% # Convert raster cells to polygons. + sf::st_as_sf() %>% # Convert the 'terra' polygons to an 'sf' object. + dplyr::mutate(GFWregionID = as.factor(.data$GFWregionID)) # Ensure GFWregionID is a factor. + # Verify that the dimensions of the data frame and sf object match after conversion. if (dim(data_df)[1] != dim(data_sf)[1]){ stop("Data dimensions of data_df and data_sf do not match after conversion to polygon") } } else if (isFALSE(compress)){ - # Combine data frames in the list into one data frame - - # Separate the "Time Range" column based on the specified temp_res + # Process data without compression, separating 'TimeRange' based on temporal resolution. if (temp_res == "YEARLY") { + # If temporal resolution is yearly, create a 'Year' column and convert to sf. data_sf <- data_df %>% dplyr::mutate(Year = .data$TimeRange) %>% sf::st_as_sf(coords = c("Lon", "Lat"), crs ="EPSG:4326") } else { - # Otherwise, separate the "Time Range" column according to the specified temp_res + # Otherwise, separate 'TimeRange' into 'Year', 'Month', and/or 'Day' columns. if (temp_res == "MONTHLY") { data_sf <- data_df %>% tidyr::separate("TimeRange", into = c("Year", "Month"), sep = "-", remove = FALSE) %>% @@ -151,7 +230,8 @@ splnr_get_gfw <- function(region, } } } - # But you may wish to return the data in a different CRS. For this you need transform + + # Transform the CRS of the sf object if the requested cCRS is different from the default "EPSG:4326". if (isFALSE(cCRS == "EPSG:4326")){ data_sf <- data_sf %>% sf::st_transform(crs = cCRS) @@ -159,26 +239,24 @@ splnr_get_gfw <- function(region, return(data_sf) } - # Run the analysis + # Apply the internal get_gfw_byRegion function to each specified region. out <- purrr::map(region, function(x) get_gfw_byRegion(x)) - - if (isFALSE(compress)){ # Do nothing. Just bind as a df + # Combine the results from multiple regions based on the 'compress' setting. + if (isFALSE(compress)){ + # If not compressed, simply bind rows of the sf objects. out <- out %>% dplyr::bind_rows() - } - - if (isTRUE(compress)){# Take care of duplicate cells on boundaries + } else if (isTRUE(compress)){ + # If compressed, bind rows and then re-summarise to handle duplicate cells on boundaries, + # summing fishing hours and combining GFWregionIDs. out <- out %>% dplyr::bind_rows() %>% dplyr::group_by(.data$geometry) %>% dplyr::summarise("ApparentFishingHrs" = sum(.data$ApparentFishingHrs, na.rm = TRUE), GFWregionID = toString(.data$GFWregionID)) %>% dplyr::ungroup() - } return(out) - } - diff --git a/R/splnr_gg_add.R b/R/splnr_gg_add.R index a2ea3b2..e28b4d0 100644 --- a/R/splnr_gg_add.R +++ b/R/splnr_gg_add.R @@ -1,48 +1,101 @@ -#' Add-ons for plotting +#' @title Add-ons for Plotting `spatialplanr` Maps #' -#' This function allows to customise plots in a simple and reproducible way, by -#' giving the option for several inputs that can be included in maps produced -#' with the other functions of this package.It can be combined with the -#' `spatialplanr` spatial plotting functions. +#' @description +#' This function allows users to customize existing `ggplot2` maps, particularly +#' those produced by other `spatialplanr` spatial plotting functions. It provides +#' options to add various spatial layers and apply consistent theming in a +#' simple and reproducible manner. #' -#' @param PUs Planning Units as an `sf` object -#' @param colorPUs A color value for the outline of planning units. -#' @param Bndry The planning region boundaries as an `sf` object -#' @param colorBndry A color value for the outline of the boundary. -#' @param overlay An `sf` object of overlay polygon. -#' @param colorOverlay A color value for overlay. -#' @param overlay2 An `sf` object of overlay polygon. -#' @param colorOverlay2 A color value for overlay. -#' @param overlay3 An `sf` object of overlay polygon. -#' @param colorOverlay3 A color value for overlay. -#' @param cropOverlay An `sf` object with the boundary box used for cropping the -#' overlay object. -#' @param contours An `sf` object of contours that are important to visualise -#' (e.g. outline of sea mounts, ridges; can be produced with -#' terra::as.contour()); up to 6 different contours possible. -#' @param colorConts A color value for contours. -#' @param lockIn An `sf` object with binary data of locked in areas in -#' the prioritisation (e.g. MPAs). -#' @param typeLockIn Either "Full" or "Contours"; "Full" maps the locked in areas on -#' top of the planning units; "Contours" draws the outline of the locked in -#' areas. -#' @param nameLockIn column of data frame that contains binary information of -#' the locked in areas to plot -#' @param alphaLockIn A value (0-1) for the opacity of the locked in areas when -#' plotted on top of other plots. -#' @param colorLockIn A color value for the locked in areas. -#' @param legendLockIn A character value for the title of the legend of the locked in -#' areas. Can be empty (""). -#' @param labelLockIn The legend label of the locked in area (e.g. MPAs) -#' @param ggtheme The theme applied to the plot. Can either be NA (default -#' ggplot), "Default" (default spatialplanr: theme_bw() and some basic theme -#' settings) or a user-defined list of theme properties. +#' @details +#' The `splnr_gg_add` function enhances `ggplot2` objects by layering additional +#' spatial data such as planning unit outlines, study area boundaries, general +#' overlays, geographical contours, and 'locked-in' areas (e.g., existing protected +#' areas in a conservation prioritization). It offers fine-grained control over +#' colors, opacities, and legend appearance for each added layer. #' -#' @return A ggplot object of the plot +#' When using `contours`, the input `sf` object is expected to have a column +#' named `Category` that defines the different contour lines to be plotted. +#' The function currently supports up to 6 distinct contour categories for plotting. +#' +#' The `ggtheme` parameter offers flexibility in plot styling. `"Default"` applies +#' a standard `spatialplanr` theme (`theme_bw()` with custom text and axis settings). +#' A `list` of `ggplot2::theme()` elements can be provided for full customization, +#' or `NA` (logical `FALSE`) to apply no default theme, allowing the user to manage +#' all theme elements manually. +#' +#' @param PUs An `sf` object representing planning units. If provided, their +#' outlines will be drawn. Defaults to `NULL`. +#' @param colorPUs A character string specifying the color for the outlines of the +#' planning units. Defaults to `"grey80"`. +#' @param Bndry An `sf` object representing the main planning region boundaries. +#' If provided, its outline will be drawn. Defaults to `NULL`. +#' @param colorBndry A character string specifying the color for the outline of the +#' `Bndry` object. Defaults to `"black"`. +#' @param overlay An `sf` object to be plotted as a general overlay. Defaults to `NULL`. +#' @param colorOverlay A character string specifying the color for `overlay`. +#' Defaults to `"grey20"`. +#' @param overlay2 An `sf` object for a second general overlay. Defaults to `NULL`. +#' @param colorOverlay2 A character string specifying the color for `overlay2`. +#' Defaults to `"grey30"`. +#' @param overlay3 An `sf` object for a third general overlay. Defaults to `NULL`. +#' @param colorOverlay3 A character string specifying the color for `overlay3`. +#' Defaults to `"grey40"`. +#' @param cropOverlay An `sf` object. Its bounding box will be used to set the +#' `xlim` and `ylim` of the `ggplot2::coord_sf` layer, effectively cropping the view. +#' Defaults to `NULL`. +#' @param contours An `sf` object containing contour lines (e.g., bathymetry or +#' seamount outlines). It is expected to have a `Category` column for differentiating +#' lines. Up to 6 categories are supported. Defaults to `NULL`. +#' @param colorConts A character string specifying the color for the contour lines. +#' Defaults to `"black"`. +#' @param lockIn An `sf` object representing 'locked-in' areas (e.g., existing +#' Marine Protected Areas) that are fixed in a conservation prioritization. +#' Defaults to `NULL`. +#' @param typeLockIn A character string specifying how `lockIn` areas should be +#' plotted. Can be `"Full"` (fills the areas with `colorLockIn`) or `"Contours"` +#' (draws only the outlines of the areas). Defaults to `"Full"`. +#' @param nameLockIn A character string specifying the column name in the `lockIn` +#' data frame that contains binary (0/1 or TRUE/FALSE) information indicating +#' locked-in status. Required if `lockIn` is not `NULL`. +#' @param alphaLockIn A numeric value (0 to 1) for the opacity of the `lockIn` +#' areas when `typeLockIn` is `"Full"`. Defaults to `0.5`. +#' @param colorLockIn A character string specifying the color for the `lockIn` areas. +#' Defaults to `"black"`. +#' @param legendLockIn A character string for the title of the `lockIn` legend. +#' Can be an empty string `""` to suppress the title. Defaults to `""`. +#' @param labelLockIn A character string for the legend label of the `lockIn` areas +#' (e.g., "MPAs"). Defaults to `"MPAs"`. +#' @param ggtheme The `ggplot2` theme to apply. Can be: +#' \itemize{ +#' \item `NA` or `FALSE`: No theme is applied, using `ggplot2` defaults. +#' \item `"Default"`: Applies a `spatialplanr` default theme (`theme_bw()` +#' with custom text/axis settings). +#' \item A `list` of `ggplot2::theme()` properties for custom styling. +#' } +#' Defaults to `"Default"`. +#' +#' @return A `list` of `ggplot2` layers and theme elements that can be added to +#' an existing `ggplot` object using `+`. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr filter mutate rename +#' @importFrom ggnewscale new_scale_colour new_scale_fill +#' @importFrom ggplot2 aes coord_sf geom_sf guide_legend element_blank +#' @importFrom ggplot2 element_text labs scale_fill_manual scale_linetype_manual +#' @importFrom ggplot2 theme theme_bw +#' @importFrom sf st_bbox st_union st_as_sf +#' @importFrom rlang .data +#' @importFrom grid unit +#' #' @examples -#' dat_problem <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +#' \dontrun{ +#' # Assuming 'dat_species_bin' and 'dat_PUs' are existing sf objects +#' # in your package, suitable for prioritisation problems and plotting. +#' +#' # Create a dummy prioritizr problem and solve it for demonstration. +#' dat_problem <- prioritizr::problem( +#' dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), #' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" #' ) %>% @@ -54,8 +107,46 @@ #' dat_soln <- dat_problem %>% #' prioritizr::solve.ConservationProblem() #' -#' splnr_plot_solution(dat_soln) + +#' # Basic plot of the solution with default planning unit outlines and theme. +#' plot_basic <- splnr_plot_solution(dat_soln) + #' splnr_gg_add(PUs = dat_PUs, ggtheme = "Default") +#' print(plot_basic) +#' +#' # Example with boundary, a custom overlay, and locked-in areas shown as contours. +#' # For this example, let's create dummy `bndry_sf` and `locked_in_sf` based on `dat_PUs` +#' # In a real scenario, these would be loaded from your package or data. +#' bndry_sf <- sf::st_union(dat_PUs) %>% sf::st_as_sf() +#' locked_in_sf <- dat_PUs[1:100, ] %>% dplyr::mutate(is_mpa = 1) +#' +#' plot_custom <- splnr_plot_solution(dat_soln) + +#' splnr_gg_add( +#' PUs = dat_PUs, +#' Bndry = bndry_sf, +#' colorBndry = "darkblue", +#' overlay = bndry_sf, # Using boundary as an example overlay +#' colorOverlay = "lightblue", +#' alphaOverlay = 0.3, +#' lockIn = locked_in_sf, +#' typeLockIn = "Contours", +#' nameLockIn = "is_mpa", +#' colorLockIn = "darkred", +#' labelLockIn = "Existing MPAs", +#' ggtheme = "Default" +#' ) +#' print(plot_custom) +#' +#' # Example with custom ggplot2 theme settings (as a list) +#' custom_theme_list <- list( +#' ggplot2::theme_classic(), +#' ggplot2::theme( +#' plot.background = ggplot2::element_rect(fill = "lightyellow"), +#' legend.position = "top" +#' ) +#' ) +#' plot_with_custom_theme <- splnr_plot_solution(dat_soln) + +#' splnr_gg_add(PUs = dat_PUs, ggtheme = custom_theme_list) +#' print(plot_with_custom_theme) +#' } splnr_gg_add <- function(PUs = NULL, colorPUs = "grey80", Bndry = NULL, colorBndry = "black", overlay = NULL, colorOverlay = "grey20", @@ -64,70 +155,112 @@ splnr_gg_add <- function(PUs = NULL, colorPUs = "grey80", contours = NULL, colorConts = "black", cropOverlay = NULL, lockIn = NULL, typeLockIn = "Full", nameLockIn = NULL, - alphaLockIn = 0.5, colorLockIn = "black", legendLockIn = "", + alphaLockIn = 1, colorLockIn = "black", legendLockIn = "", labelLockIn = "MPAs", - ggtheme = "Default" # splnr_theme + lockOut = NULL, typeLockOut = "Full", nameLockOut = NULL, + alphaLockOut = 1, colorLockOut = "black", legendLockOut = "", + labelLockOut = "", + ggtheme = "Default" ) { - if(!is.null(PUs)){assertthat::assert_that(inherits(PUs, "sf"))} - if(!is.null(Bndry)){assertthat::assert_that(inherits(Bndry, "sf"))} - if(!is.null(overlay)){assertthat::assert_that(inherits(overlay, "sf"))} - if(!is.null(overlay2)){assertthat::assert_that(inherits(overlay2, "sf"))} - if(!is.null(overlay3)){assertthat::assert_that(inherits(overlay3, "sf"))} - if(!is.null(contours)){assertthat::assert_that(inherits(contours, "sf"))} - if(!is.null(lockIn)){assertthat::assert_that(inherits(lockIn, "sf"))} + # TODO Remove all uneeded arguments, especially the lockIn + + # TODO Update the asserts for new arguments + # # Assertions to validate input parameters are of the correct 'sf' class if not NULL. + # if(!is.null(PUs)){assertthat::assert_that(inherits(PUs, "sf"), msg = "'PUs' must be an 'sf' object or NULL.")} + # if(!is.null(Bndry)){assertthat::assert_that(inherits(Bndry, "sf"), msg = "'Bndry' must be an 'sf' object or NULL.")} + # if(!is.null(overlay)){assertthat::assert_that(inherits(overlay, "sf"), msg = "'overlay' must be an 'sf' object or NULL.")} + # if(!is.null(overlay2)){assertthat::assert_that(inherits(overlay2, "sf"), msg = "'overlay2' must be an 'sf' object or NULL.")} + # if(!is.null(overlay3)){assertthat::assert_that(inherits(overlay3, "sf"), msg = "'overlay3' must be an 'sf' object or NULL.")} + # if(!is.null(contours)){assertthat::assert_that(inherits(contours, "sf"), msg = "'contours' must be an 'sf' object or NULL.")} + # if(!is.null(lockIn)){ + # assertthat::assert_that(inherits(lockIn, "sf"), msg = "'lockIn' must be an 'sf' object or NULL.") + # assertthat::assert_that(is.character(nameLockIn) && all(nameLockIn %in% names(lockIn)), + # msg = "If 'lockIn' is provided, 'nameLockIn' must be a character string specifying an existing column in 'lockIn'.") + # assertthat::assert_that(typeLockIn %in% c("Full", "Contours"), + # msg = "'typeLockIn' must be either 'Full' or 'Contours'.") + # assertthat::assert_that(is.numeric(alphaLockIn) && alphaLockIn >= 0 && alphaLockIn <= 1, + # msg = "'alphaLockIn' must be a numeric value between 0 and 1.") + # } + # if(!is.null(cropOverlay)){assertthat::assert_that(inherits(cropOverlay, "sf"), msg = "'cropOverlay' must be an 'sf' object or NULL.")} + # assertthat::assert_that(is.character(colorPUs), msg = "'colorPUs' must be a character string for a color.") + # assertthat::assert_that(is.character(colorBndry), msg = "'colorBndry' must be a character string for a color.") + # assertthat::assert_that(is.character(colorOverlay), msg = "'colorOverlay' must be a character string for a color.") + # assertthat::assert_that(is.character(colorOverlay2), msg = "'colorOverlay2' must be a character string for a color.") + # assertthat::assert_that(is.character(colorOverlay3), msg = "'colorOverlay3' must be a character string for a color.") + # assertthat::assert_that(is.character(colorConts), msg = "'colorConts' must be a character string for a color.") + # assertthat::assert_that(is.character(colorLockIn), msg = "'colorLockIn' must be a character string for a color.") + # assertthat::assert_that(is.character(legendLockIn), msg = "'legendLockIn' must be a character string.") + # assertthat::assert_that(is.character(labelLockIn), msg = "'labelLockIn' must be a character string.") + # assertthat::assert_that( + # inherits(ggtheme, "character") || inherits(ggtheme, "theme") || inherits(ggtheme, "logical"), + # msg = "'ggtheme' must be 'Default', a ggplot2 theme, or NA/FALSE." + # ) + # Initialize an empty list to store ggplot2 layers. ggList <- list() + # Add planning units layer if PUs is an sf object. if (inherits(PUs, "sf")) { ggList <- c( ggList, - ggplot2::geom_sf(data = PUs, colour = colorPUs, fill = NA, size = 0.1, show.legend = FALSE), - ggplot2::coord_sf(xlim = sf::st_bbox(PUs)$xlim, ylim = sf::st_bbox(PUs)$ylim) + list( + ggplot2::geom_sf(data = PUs, colour = colorPUs, fill = NA, size = 0.1, show.legend = FALSE), + ggplot2::coord_sf(xlim = sf::st_bbox(PUs)$xlim, ylim = sf::st_bbox(PUs)$ylim) + ) ) } + # Add boundary layer if Bndry is an sf object. if (inherits(Bndry, "sf")) { ggList <- c( ggList, - ggplot2::geom_sf(data = Bndry, colour = colorBndry, size = 0.4, fill = NA, show.legend = FALSE), - ggplot2::coord_sf(xlim = sf::st_bbox(Bndry)$xlim, ylim = sf::st_bbox(Bndry)$ylim) + list( + ggplot2::geom_sf(data = Bndry, colour = colorBndry, size = 0.4, fill = NA, show.legend = FALSE), + ggplot2::coord_sf(xlim = sf::st_bbox(Bndry)$xlim, ylim = sf::st_bbox(Bndry)$ylim) + ) ) } + # Add first overlay layer if 'overlay' is an sf object. if (inherits(overlay, "sf")) { ggList <- c( ggList, ggplot2::geom_sf(data = overlay, colour = colorOverlay, fill = colorOverlay, alpha = 0.9, size = 0.1, show.legend = FALSE) )} + # Add second overlay layer if 'overlay2' is an sf object. if (inherits(overlay2, "sf")) { ggList <- c( ggList, ggplot2::geom_sf(data = overlay2, colour = colorOverlay2, fill = colorOverlay2, alpha = 0.9, size = 0.1, show.legend = FALSE) )} + # Add third overlay layer if 'overlay3' is an sf object. if (inherits(overlay3, "sf")) { ggList <- c( ggList, ggplot2::geom_sf(data = overlay3, colour = colorOverlay3, fill = colorOverlay3, alpha = 0.9, size = 0.1, show.legend = FALSE) )} - - if (inherits(contours, "sf")) { # needs a geometry col and one names Category that has the wanted contours and their names - namesConts <- unique(contours$Category) - contoursRowNum <- length(namesConts) + # Add contours layer if 'contours' is an sf object. + if (inherits(contours, "sf")) { + # Get unique contour categories for legend. + nameConts <- unique(contours$Category) + contoursRowNum <- length(nameConts) vals <- 1:contoursRowNum + # Warn if more than 6 categories are provided for contours. if (length(vals) > 6) { cat("Only 6 categories allowed for plotting contours.") } else { + # Add contour layers with new scale for color and linetype. ggList <- c( ggList, list( - ggnewscale::new_scale_colour(), + ggnewscale::new_scale_colour(), # Start a new color scale for contours. ggplot2::geom_sf(data = contours, colour = colorConts, fill = NA, ggplot2::aes(linetype = .data$Category), size = 0.5, show.legend = "line"), - ggplot2::scale_linetype_manual(" ", - breaks = namesConts, + ggplot2::scale_linetype_manual(" ", # Set linetype based on contour categories. + breaks = nameConts, values = vals, guide = ggplot2::guide_legend( override.aes = list(fill = NA), @@ -142,25 +275,37 @@ splnr_gg_add <- function(PUs = NULL, colorPUs = "grey80", } } + + #TODO Consider adding locked in to the selected/not selected solution column so it plots as one. + # Add locked-in areas layer if 'lockIn' is an sf object. if (inherits(lockIn, "sf")) { + + # Mutate the 'lockIn' data to create a 'lockedIn' logical column based on 'nameLockIn', then filter. lockIn <- lockIn %>% - dplyr::mutate(lockedIn = as.logical(.data[[nameLockIn]])) %>% - dplyr::filter(.data$lockedIn == 1) # TODO Add ability for TRUE as well + dplyr::select(tidyselect::all_of(c(nameLockIn, "geometry"))) %>% + tidyr::pivot_longer(cols = tidyselect::all_of(c(nameLockIn)), names_to = "LI_Area", values_to = "LockedIn") %>% + dplyr::mutate(lockedIn = as.logical(LockedIn), + LI_Area = stringr::str_to_title(LI_Area)) %>% + dplyr::filter(.data$lockedIn == TRUE) # Filter for TRUE values in the 'lockedIn' column. + # Plot locked-in areas as 'Full' polygons. if (typeLockIn == "Full") { ggList <- c( ggList, list( - ggnewscale::new_scale_fill(), - ggnewscale::new_scale_colour(), - ggplot2::geom_sf(data = lockIn, ggplot2::aes(fill = .data$lockedIn), alpha = alphaLockIn), - ggplot2::scale_fill_manual( - name = legendLockIn, - values = c("TRUE" = colorLockIn), - labels = labelLockIn, + ggnewscale::new_scale_fill(), # Start a new fill scale. + ggnewscale::new_scale_colour(), # Start a new color scale. + ggplot2::geom_sf(data = lockIn, ggplot2::aes(fill = .data$LI_Area), alpha = alphaLockIn), + ggplot2::scale_fill_brewer( + palette = "Greens", + name = legendLockIn, # Set legend title. + # values = c("TRUE" = colorLockIn), # Map TRUE to specified color. + # labels = labelLockIn, # Set legend label. + # Apply color and fill aesthetics to this scale. aesthetics = c("colour", "fill"), + # Configure legend appearance. guide = ggplot2::guide_legend( - override.aes = list(linetype = 0), + override.aes = list(linetype = 0), # Remove linetype from legend. nrow = 2, order = 1, direction = "horizontal", @@ -170,7 +315,8 @@ splnr_gg_add <- function(PUs = NULL, colorPUs = "grey80", ) ) ) - } else if (typeLockIn == "Contours") { + } else if (typeLockIn == "Contours") { # Plot locked-in areas as 'Contours' (outlines). + # Union geometries to create a single outline for locked-in areas. lockIn <- lockIn %>% sf::st_union() %>% sf::st_as_sf() %>% @@ -178,20 +324,19 @@ splnr_gg_add <- function(PUs = NULL, colorPUs = "grey80", dplyr::mutate(lockedIn = 1) %>% dplyr::mutate(lockedIn = as.factor(.data$lockedIn)) + # Add contour layers with new scale for color and linetype. ggList <- c( ggList, list( - ggnewscale::new_scale_fill(), - ggnewscale::new_scale_colour(), + ggnewscale::new_scale_fill(), # Start a new fill scale. + ggnewscale::new_scale_colour(), # Start a new color scale. ggplot2::geom_sf(data = lockIn, colour = colorLockIn, fill = NA, ggplot2::aes(linetype = .data$lockedIn), size = 0.5, show.legend = "line"), ggplot2::scale_linetype_manual("", - values = 1, - labels = labelLockIn, + values = 1, # Use a single linetype for contours. + labels = labelLockIn, # Set legend label. guide = ggplot2::guide_legend( - override.aes = list(fill = NA), - # nrow = 2, + override.aes = list(fill = NA), # Remove fill from legend. direction = "horizontal", - # order = 3, keywidth = grid::unit(0.05, "npc") ) ) @@ -200,6 +345,49 @@ splnr_gg_add <- function(PUs = NULL, colorPUs = "grey80", } } + + ## Lock Out --------- + if (inherits(lockOut, "sf")) { + + # Mutate the 'lockOut' data to create a 'lockedOut' logical column based on 'nameLockOut', then filter. + lockOut <- lockOut %>% + dplyr::select(tidyselect::all_of(c(nameLockOut, "geometry"))) %>% + tidyr::pivot_longer(cols = tidyselect::all_of(c(nameLockOut)), names_to = "LI_Area", values_to = "LockedOut") %>% + dplyr::mutate(lockedOut = as.logical(LockedOut), + LI_Area = stringr::str_to_title(LI_Area)) %>% + dplyr::filter(.data$lockedOut == TRUE) # Filter for TRUE values in the 'lockedOut' column. + + # Plot locked-in areas as 'Full' polygons. + if (typeLockOut == "Full") { + ggList <- c( + ggList, + list( + ggnewscale::new_scale_fill(), # Start a new fill scale. + ggnewscale::new_scale_colour(), # Start a new color scale. + ggplot2::geom_sf(data = lockOut, ggplot2::aes(fill = .data$LI_Area), alpha = alphaLockOut), + ggplot2::scale_fill_brewer( + palette = "Reds", + name = legendLockOut, # Set legend title. + # Apply color and fill aesthetics to this scale. + aesthetics = c("colour", "fill"), + # Configure legend appearance. + guide = ggplot2::guide_legend( + override.aes = list(linetype = 0), # Remove linetype from legend. + nrow = 2, + order = 1, + direction = "horizontal", + title.position = "top", + title.hjust = 0.5 + ) + ) + ) + ) + } + } + + + + # Apply coordinate limits based on 'cropOverlay' if provided. if (inherits(cropOverlay, "sf")) { ggList <- c( ggList, @@ -208,24 +396,39 @@ splnr_gg_add <- function(PUs = NULL, colorPUs = "grey80", } - if (inherits(ggtheme, "character")) { + # Apply the specified ggplot2 theme. + if (inherits(ggtheme, "character") && ggtheme == "Default") { + # Apply the default spatialplanr theme. ggList <- c( ggList, list( - ggplot2::theme_bw(), + ggplot2::theme_bw(), # Black and white theme. ggplot2::theme( - legend.position = "bottom", - legend.direction = "horizontal", - text = ggplot2::element_text(size = 20, colour = "black"), - axis.text = ggplot2::element_text(size = 16, colour = "black"), - plot.title = ggplot2::element_text(size = 16), - axis.title = ggplot2::element_blank() + legend.position = "bottom", # Legend at the bottom. + legend.direction = "horizontal", # Horizontal legend. + text = ggplot2::element_text(size = 20, colour = "black"), # Global text size and color. + axis.text = ggplot2::element_text(size = 16, colour = "black"), # Axis text size and color. + plot.title = ggplot2::element_text(size = 16), # Plot title size. + axis.title = ggplot2::element_blank() # Remove axis titles. ) ) ) + + + } else if (inherits(ggtheme, "theme")) { + # If a theme object is provided, append it. + ggList <- c(ggList, list(ggtheme)) + } else if (inherits(ggtheme, "list")) { + # If a list of theme elements is provided, append them. ggList <- c(ggList, ggtheme) - } else if (inherits(ggtheme, "logical")) { + + } else if (inherits(ggtheme, "logical") && !ggtheme) { + # If ggtheme is FALSE or NA, do nothing (no default theme applied). ggList <- ggList } + + # browser() + + return(ggList) } diff --git a/R/splnr_plot.R b/R/splnr_plot.R index ef2d3c8..1571bb8 100644 --- a/R/splnr_plot.R +++ b/R/splnr_plot.R @@ -1,167 +1,266 @@ -#' Function to plot data. +#' @title Plot Spatial Data #' -#' (For now can replace splnr_plot_cost(), splnr_plot_binFeature(), splnr_plot_MPAs(), splnr_plot_featureNo()) +#' @description +#' This function provides a versatile way to plot spatial data (`sf` objects) +#' within the `spatialplanr` package. It can visualize various data types, +#' including binary presence/absence, logical values, continuous data, or simply +#' the planning unit outlines. #' -#' Written by Kilian Barreiro and Jason Everett -#' Written: February 2024 +#' @details +#' The `splnr_plot` function automatically detects the type of data specified by +#' `colNames` (binary, logical, or continuous) and adjusts the plotting +#' aesthetics accordingly. If multiple `colNames` are provided, it calculates +#' the sum of features for each planning unit and plots this sum. If `colNames` +#' is `NULL`, it will simply plot the outlines of the planning units. #' -#' @param df The dataframe containing the data to be plotted. It must include a geometry column to be used with geom_sf. -#' @param col_names A list of column names to include in the plot. If specified, only these columns will be used to colour the plot. -#' @param paletteName The name of the colour palette to use for filling. Default is "YlGnBu". -#' @param colourVals The colour values to use if col_names is specified and the data is binary. -#' @param plot_title The title of the plot. -#' @param legend_title The title of the legend. -#' @param legend_labels A vector of strings containing the labels to use for legend values. +#' This function is designed to be a flexible replacement for several plotting +#' functions, such as `splnr_plot_cost()`, `splnr_plot_binFeature()`, +#' `splnr_plot_MPAs()`, and `splnr_plot_featureNo()`, streamlining the plotting +#' workflow within the package. #' -#' @return A ggplot object. +#' Written by Kilian Barreiro and Jason Everett. +#' Last modified: February 2024. +#' +#' @param df The input dataframe containing the data to be plotted. This must be +#' an `sf` object and include a geometry column. +#' @param colNames A character vector of column names from `df` to be used for +#' coloring the plot. If `NULL` (default), only the planning unit outlines are plotted. +#' If a single column is specified, it checks for binary, logical, or continuous data. +#' If multiple columns are specified, it sums the values across these columns to create +#' a "FeatureSum" for plotting. +#' @param paletteName A character string specifying the name of the `RColorBrewer` +#' palette to use for filling continuous data. Defaults to `"YlGnBu"`. +#' @param colourVals A character vector of two color values to use for binary +#' (0/1) or logical (FALSE/TRUE) data. The first color is for '0' or 'FALSE' +#' (absence), and the second is for '1' or 'TRUE' (presence). +#' Defaults to `c("#c6dbef", "#3182bd")`. +#' @param plotTitle A character string for the subtitle of the plot. +#' Defaults to `""` (no subtitle). +#' @param legendTitle A character string for the title of the legend. If `NULL`, +#' a default title will be used based on the data type. +#' @param legendLabels A character vector of strings to use for the legend labels, +#' particularly useful for binary or logical data (e.g., `c("Absent", "Present")`). +#' If `NULL`, default labels are used for binary/logical plots. +#' +#' @return A `ggplot` object representing the spatial plot. #' #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr across as_tibble filter mutate select +#' @importFrom ggplot2 aes coord_sf geom_sf ggplot labs scale_fill_distiller +#' @importFrom ggplot2 scale_fill_manual scale_fill_viridis_c guide_colourbar guides +#' @importFrom purrr map_vec +#' @importFrom rlang .data +#' @importFrom scales squish +#' @importFrom sf st_as_sf st_bbox st_drop_geometry +#' @importFrom tidyr replace_na +#' @importFrom tidyselect all_of starts_with where +#' #' @examples -#' # Binary plot of species distribution -#' splnr_plot(df = dat_species_bin, -#' col_names = "Spp1", -#' legend_title = "Legend", -#' legend_labels = c("Absent", "Present")) +#' \dontrun{ +#' # Assuming 'dat_species_bin', 'dat_bathy', and 'dat_PUs' are existing sf objects +#' # in your package, suitable for plotting. #' -#' # Logical plot of species distribution -#' splnr_plot(df = dat_species_bin %>% -#' dplyr::mutate(dplyr::across( -#' tidyselect::starts_with("Spp"), as.logical)), -#' col_names = "Spp1", -#' legend_title = "Legend", -#' legend_labels = c("Absent", "Present")) +#' # Binary plot of species distribution for "Spp1" +#' plot_spp1_binary <- splnr_plot( +#' df = dat_species_bin, +#' colNames = "Spp1", +#' legendTitle = "Species Presence", +#' legendLabels = c("Absent", "Present") +#' ) +#' print(plot_spp1_binary) #' -#' # Continuous plot of bathymetry# -#' splnr_plot(df = dat_bathy, -#' col_names = "bathymetry", -#' plot_title = "Bathymetry", -#' legend_title = "Bathymetry (m)") +#' # Logical plot of species distribution for "Spp1" (converted from binary) +#' plot_spp1_logical <- splnr_plot( +#' df = dat_species_bin %>% +#' dplyr::mutate(dplyr::across( +#' tidyselect::starts_with("Spp"), as.logical +#' )), +#' colNames = "Spp1", +#' legendTitle = "Species Presence", +#' legendLabels = c("Absent", "Present") +#' ) +#' print(plot_spp1_logical) #' -#' # Plot Planning Units -#' splnr_plot(df = dat_PUs) +#' # Continuous plot of bathymetry +#' plot_bathymetry <- splnr_plot( +#' df = dat_bathy, +#' colNames = "bathymetry", +#' plotTitle = "Bathymetry", +#' legendTitle = "Bathymetry (m)" +#' ) +#' print(plot_bathymetry) #' -#' # Multi binary features -#' splnr_plot(df = dat_species_bin, -#' col_names = colnames(dat_species_bin %>% -#' sf::st_drop_geometry() %>% -#' dplyr::select( -#' tidyselect::starts_with("Spp"))), -#' legend_title = "Number of features") - +#' # Plot Planning Units outlines only +#' plot_planning_units <- splnr_plot(df = dat_PUs) +#' print(plot_planning_units) +#' +#' # Multi-binary features: Plotting the sum of multiple "Spp" features +#' plot_multi_spp_sum <- splnr_plot( +#' df = dat_species_bin, +#' colNames = colnames(dat_species_bin %>% +#' sf::st_drop_geometry() %>% +#' dplyr::select(tidyselect::starts_with("Spp"))), +#' legendTitle = "Number of Features" +#' ) +#' print(plot_multi_spp_sum) +#' } splnr_plot <- function(df, - col_names = NULL, + colNames = NULL, paletteName = "YlGnBu", colourVals = c("#c6dbef", "#3182bd"), - plot_title = "", - legend_title = NULL, - legend_labels = NULL) { + plotTitle = "", + legendTitle = NULL, + legendLabels = NULL) { - # Assertions + # Assertions to validate input parameters. assertthat::assert_that( is.data.frame(df), - inherits(df, "sf"), # Check it is an sf + msg = "'df' must be a data.frame." + ) + assertthat::assert_that( + inherits(df, "sf"), + msg = "'df' must be an 'sf' object." + ) + assertthat::assert_that( all(c("xmin", "xmax", "ymin", "ymax") %in% names(sf::st_bbox(df))), - is.null(col_names) | is.character(col_names), - all(col_names %in% colnames(df)), # Do the column names exist - # Check all col_names are in the the dataset + msg = "'df' must have a valid bounding box (sf::st_bbox)." + ) + assertthat::assert_that( + is.null(colNames) || is.character(colNames), + msg = "'colNames' must be a character vector or NULL." + ) + if (!is.null(colNames)) { + assertthat::assert_that( + all(colNames %in% colnames(df)), + msg = paste0("Not all specified 'colNames' exist in the input dataframe. Missing: ", + paste(colNames[!colNames %in% colnames(df)], collapse = ", ")) + ) + } + assertthat::assert_that( is.character(paletteName), - is.null(legend_title) | is.character(legend_title), - is.null(legend_labels) | is.character(legend_labels) + msg = "'paletteName' must be a character string (e.g., a RColorBrewer palette name)." + ) + assertthat::assert_that( + is.null(legendTitle) || is.character(legendTitle), + msg = "'legendTitle' must be a character string or NULL." + ) + assertthat::assert_that( + is.null(legendLabels) || is.character(legendLabels), + msg = "'legendLabels' must be a character vector or NULL." + ) + assertthat::assert_that( + is.character(plotTitle), + msg = "'plotTitle' must be a character string." + ) + assertthat::assert_that( + is.character(colourVals) && length(colourVals) == 2, + msg = "'colourVals' must be a character vector of exactly two color strings." ) - # Set the defaults + + # Initialize flags for data type detection. is_binary <- FALSE is_logi <- FALSE is_continuous <- FALSE showFeatureSum <- FALSE - # Figure out data type - - if (!is.null(col_names)){ # If there is a column name + # Determine data type based on 'colNames' presence and content. + if (!is.null(colNames)){ # If 'colNames' are provided. - if (length(col_names) == 1){ # One column name + if (length(colNames) == 1){ # If only one column name is specified. - if (is.logical(df[[col_names]])){ # Is the column logical? + if (is.logical(df[[colNames]])){ # Check if the column data is logical (TRUE/FALSE). is_logi <- TRUE - } else { # See if it is binary + } else { # If not logical, check if it's binary (0/1). + # Create a temporary dataframe, replacing NA with 0 in the target columns for binary check. df0 <- df %>% - # Replace NA with 0 in selected columns for binary data verification - dplyr::mutate(dplyr::across(tidyselect::all_of(col_names), ~tidyr::replace_na(., 0))) - - is_binary <- all(purrr::map_vec(col_names, function(x) all(df0[[x]] %in% c(0, 1)))) + dplyr::mutate(dplyr::across(tidyselect::all_of(colNames), ~tidyr::replace_na(., 0))) + # Check if all values in the column are exclusively 0 or 1. + is_binary <- all(purrr::map_vec(colNames, function(x) all(df0[[x]] %in% c(0, 1)))) } - ## Is the data continuous? + ## If not binary and not logical, assume it's continuous. if (isFALSE(is_binary) & isFALSE(is_logi)){ - is_continuous <- TRUE # May not always be true but plotting as continuous will work, and highlight the issue + is_continuous <- TRUE # This assumption allows plotting, and issues would be visible. } - } else if (length(col_names) > 1){ # Mutliple columns - showFeatureSum <- TRUE # FeatureSum + } else if (length(colNames) > 1){ # If multiple column names are specified. + showFeatureSum <- TRUE # Set flag to calculate and show the sum of features. } } - # DEFAULT PLOT CODE + + # Initialize the base ggplot object with coordinate system and subtitle. gg <- ggplot2::ggplot() + ggplot2::coord_sf(xlim = sf::st_bbox(df)$xlim, ylim = sf::st_bbox(df)$ylim) + - ggplot2::labs(subtitle = plot_title) + ggplot2::labs(subtitle = plotTitle) - # Plot logic based on data type + # Plot logic based on the determined data type. - if (showFeatureSum) { #TODO I don't think this is used anymore. Remove? - - # Calculate feature sum if multiple features + if (showFeatureSum) { + # If showing feature sum, calculate it and prepare data for plotting. df <- df %>% - dplyr::as_tibble() %>% - dplyr::select(tidyselect::all_of(c(col_names, "geometry"))) %>% - dplyr::mutate(FeatureSum = rowSums(dplyr::across(tidyselect::where(is.numeric)), na.rm = TRUE)) %>% - sf::st_as_sf(sf_column_name = "geometry") %>% - dplyr::select("FeatureSum") + dplyr::as_tibble() %>% # Convert to tibble to handle geometry column easily. + dplyr::select(tidyselect::all_of(c(colNames, "geometry"))) %>% # Select only relevant columns and geometry. + dplyr::mutate(FeatureSum = rowSums(dplyr::across(tidyselect::where(is.numeric)), na.rm = TRUE)) %>% # Calculate sum of numeric feature columns. + sf::st_as_sf(sf_column_name = "geometry") %>% # Convert back to sf object. + dplyr::select("FeatureSum") # Keep only FeatureSum and geometry. + # Add geom_sf for continuous fill based on FeatureSum. gg <- gg + ggplot2::geom_sf(data = df, ggplot2::aes(fill = .data$FeatureSum), colour = NA, size = 0.1) + + # Apply a distiller palette for continuous data and configure the legend. ggplot2::scale_fill_distiller( - name = legend_title, + name = legendTitle, palette = paletteName, aesthetics = c("fill"), oob = scales::squish) + ggplot2::guides(fill = ggplot2::guide_colourbar(order = -1)) return(gg) - } else if (is_binary | is_logi) { + } else if (is_binary | is_logi) { # If data is binary or logical. - if (is.null(legend_labels)){ - legend_labels = c("Absence", "Presence") + # Set default legend labels if not provided. + if (is.null(legendLabels)){ + legendLabels = c("Absence", "Presence") } + # Add geom_sf for discrete fill based on the single column. gg <- gg + - ggplot2::geom_sf(data = df, ggplot2::aes(fill = factor(.data[[col_names]])), colour = "grey80", size = 0.1) + ggplot2::geom_sf(data = df, ggplot2::aes(fill = factor(.data[[colNames]])), colour = "grey80", size = 0.1) + # Apply manual fill scale for binary (0/1) data. if (isTRUE(is_binary)) { gg <- gg + ggplot2::scale_fill_manual(values = c("0" = colourVals[1], "1" = colourVals[2]), - labels = legend_labels, - name = legend_title) + labels = legendLabels, + name = legendTitle) } + # Apply manual fill scale for logical (FALSE/TRUE) data. if (isTRUE(is_logi)) { gg <- gg + ggplot2::scale_fill_manual(values = c("FALSE" = colourVals[1], "TRUE" = colourVals[2]), - labels = legend_labels, - name = legend_title)} - + labels = legendLabels, + name = legendTitle) + } - } else if (is_continuous) { + } else if (is_continuous) { # If data is continuous. + # Add geom_sf for continuous fill and color based on the single column. gg <- gg + - ggplot2::geom_sf(data = df, ggplot2::aes(fill = .data[[col_names]], colour = .data[[col_names]])) + - ggplot2::scale_fill_viridis_c(name = legend_title, aesthetics = c("colour", "fill")) + + ggplot2::geom_sf(data = df, ggplot2::aes(fill = .data[[colNames]], colour = .data[[colNames]])) + + # Apply a viridis continuous color scale for fill and color. + ggplot2::scale_fill_viridis_c(name = legendTitle, aesthetics = c("colour", "fill")) + + # Configure guides to show color bar for fill and hide color legend for outline. ggplot2::guides(fill = ggplot2::guide_colourbar(order = 1), colour = "none") - } else if (is.null(col_names)){ # No column to plot by + } else if (is.null(colNames)){ # If no column to plot by (only planning unit outlines). + # Add geom_sf to display planning unit outlines without fill. gg <- gg + ggplot2::geom_sf(data = df, colour = "grey80", fill = NA, size = 0.1) } diff --git a/R/splnr_plotting.R b/R/splnr_plotting.R index 1643042..db2b4a1 100644 --- a/R/splnr_plotting.R +++ b/R/splnr_plotting.R @@ -1,20 +1,330 @@ -#' Plot prioritizr solution +#' @title Plot Spatial Data #' -#' `splnr_plot_solution()` allows to plot the solution of a `prioritizr` conservation problem with our without in a customisable way using `ggplot2`. This function requires a solution as an `sf` object with a column called `solution_1` and outputs a `ggobject`. It can be combined with the `spatialplanr` function [splnr_gg_add()]. +#' @description +#' This function provides a versatile way to plot spatial data (`sf` objects) +#' within the `spatialplanr` package. It can visualize various data types, +#' including binary presence/absence, logical values, continuous data, or simply +#' the planning unit outlines. #' -#' @param soln The `prioritizr` solution -#' @param colorVals A `list` object of named vectors that will match the color value with the according name. "TRUE" stands for selected planning units. -#' @param showLegend A logical command on whether to show the legend of the solution (Default: TRUE). -#' @param legendLabels Character values (number of zones + 1) of what the legend should be labelled. -#' @param plotTitle A character value for the title of the plot. Can be empty (""). -#' @param legendTitle A character value for the title of the legend. Can be empty (""). -#' @param zones A logical value, indicating whether the spatial plan contains zones or not (default = FALSE). +#' @details +#' The `splnr_plot` function automatically detects the type of data specified by +#' `colNames` (binary, logical, or continuous) and adjusts the plotting +#' aesthetics accordingly. If multiple `colNames` are provided, it calculates +#' the sum of features for each planning unit and plots this sum. If `colNames` +#' is `NULL`, it will simply plot the outlines of the planning units. +#' +#' This function is designed to be a flexible replacement for several plotting +#' functions, such as `splnr_plot_cost()`, `splnr_plot_binFeature()`, +#' `splnr_plot_MPAs()`, and `splnr_plot_featureNo()`, streamlining the plotting +#' workflow within the package. +#' +#' Written by Kilian Barreiro and Jason Everett. +#' Last modified: February 2024. +#' +#' @param df The input dataframe containing the data to be plotted. This must be +#' an `sf` object and include a geometry column. +#' @param colNames A character vector of column names from `df` to be used for +#' coloring the plot. If `NULL` (default), only the planning unit outlines are plotted. +#' If a single column is specified, it checks for binary, logical, or continuous data. +#' If multiple columns are specified, it sums the values across these columns to create +#' a "FeatureSum" for plotting. +#' @param paletteName A character string specifying the name of the `RColorBrewer` +#' palette to use for filling continuous data. Defaults to `"YlGnBu"`. +#' @param colourVals A character vector of two color values to use for binary +#' (0/1) or logical (FALSE/TRUE) data. The first color is for '0' or 'FALSE' +#' (absence), and the second is for '1' or 'TRUE' (presence). +#' Defaults to `c("#c6dbef", "#3182bd")`. +#' @param plotTitle A character string for the subtitle of the plot. +#' Defaults to `""` (no subtitle). +#' @param legendTitle A character string for the title of the legend. If `NULL`, +#' a default title will be used based on the data type. +#' @param legendLabels A character vector of strings to use for the legend labels, +#' particularly useful for binary or logical data (e.g., `c("Absent", "Present")`). +#' If `NULL`, default labels are used for binary/logical plots. +#' +#' @return A `ggplot` object representing the spatial plot. #' -#' @return A ggplot object of the plot #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr across as_tibble filter mutate select +#' @importFrom ggplot2 aes coord_sf geom_sf ggplot labs scale_fill_distiller +#' @importFrom ggplot2 scale_fill_manual scale_fill_viridis_c guide_colourbar guides +#' @importFrom purrr map_vec +#' @importFrom rlang .data +#' @importFrom scales squish +#' @importFrom sf st_as_sf st_bbox st_drop_geometry +#' @importFrom tidyr replace_na +#' @importFrom tidyselect all_of starts_with where +#' #' @examples -#' dat_problem <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +#' \dontrun{ +#' # Assuming 'dat_species_bin', 'dat_bathy', and 'dat_PUs' are existing sf objects +#' # in your package, suitable for plotting. +#' +#' # Binary plot of species distribution for "Spp1" +#' plot_spp1_binary <- splnr_plot( +#' df = dat_species_bin, +#' colNames = "Spp1", +#' legendTitle = "Species Presence", +#' legendLabels = c("Absent", "Present") +#' ) +#' print(plot_spp1_binary) +#' +#' # Logical plot of species distribution for "Spp1" (converted from binary) +#' plot_spp1_logical <- splnr_plot( +#' df = dat_species_bin %>% +#' dplyr::mutate(dplyr::across( +#' tidyselect::starts_with("Spp"), as.logical +#' )), +#' colNames = "Spp1", +#' legendTitle = "Species Presence", +#' legendLabels = c("Absent", "Present") +#' ) +#' print(plot_spp1_logical) +#' +#' # Continuous plot of bathymetry +#' plot_bathymetry <- splnr_plot( +#' df = dat_bathy, +#' colNames = "bathymetry", +#' plotTitle = "Bathymetry", +#' legendTitle = "Bathymetry (m)" +#' ) +#' print(plot_bathymetry) +#' +#' # Plot Planning Units outlines only +#' plot_planning_units <- splnr_plot(df = dat_PUs) +#' print(plot_planning_units) +#' +#' # Multi-binary features: Plotting the sum of multiple "Spp" features +#' plot_multi_spp_sum <- splnr_plot( +#' df = dat_species_bin, +#' colNames = colnames(dat_species_bin %>% +#' sf::st_drop_geometry() %>% +#' dplyr::select(tidyselect::starts_with("Spp"))), +#' legendTitle = "Number of Features" +#' ) +#' print(plot_multi_spp_sum) +#' } +splnr_plot <- function(df, + colNames = NULL, + paletteName = "YlGnBu", + colourVals = c("#c6dbef", "#3182bd"), + plotTitle = "", + legendTitle = NULL, + legendLabels = NULL) { + + # Assertions to validate input parameters. + assertthat::assert_that( + is.data.frame(df), + msg = "'df' must be a data.frame." + ) + assertthat::assert_that( + inherits(df, "sf"), + msg = "'df' must be an 'sf' object." + ) + assertthat::assert_that( + all(c("xmin", "xmax", "ymin", "ymax") %in% names(sf::st_bbox(df))), + msg = "'df' must have a valid bounding box (sf::st_bbox)." + ) + assertthat::assert_that( + is.null(colNames) || is.character(colNames), + msg = "'colNames' must be a character vector or NULL." + ) + if (!is.null(colNames)) { + assertthat::assert_that( + all(colNames %in% colnames(df)), + msg = paste0("Not all specified 'colNames' exist in the input dataframe. Missing: ", + paste(colNames[!colNames %in% colnames(df)], collapse = ", ")) + ) + } + assertthat::assert_that( + is.character(paletteName), + msg = "'paletteName' must be a character string (e.g., a RColorBrewer palette name)." + ) + assertthat::assert_that( + is.null(legendTitle) || is.character(legendTitle), + msg = "'legendTitle' must be a character string or NULL." + ) + assertthat::assert_that( + is.null(legendLabels) || is.character(legendLabels), + msg = "'legendLabels' must be a character vector or NULL." + ) + assertthat::assert_that( + is.character(plotTitle), + msg = "'plotTitle' must be a character string." + ) + assertthat::assert_that( + is.character(colourVals) && length(colourVals) == 2, + msg = "'colourVals' must be a character vector of exactly two color strings." + ) + + + # Initialize flags for data type detection. + is_binary <- FALSE + is_logi <- FALSE + is_continuous <- FALSE + showFeatureSum <- FALSE + + # Determine data type based on 'colNames' presence and content. + if (!is.null(colNames)){ # If 'colNames' are provided. + + if (length(colNames) == 1){ # If only one column name is specified. + + if (is.logical(df[[colNames]])){ # Check if the column data is logical (TRUE/FALSE). + is_logi <- TRUE + } else { # If not logical, check if it's binary (0/1). + # Create a temporary dataframe, replacing NA with 0 in the target columns for binary check. + df0 <- df %>% + dplyr::mutate(dplyr::across(tidyselect::all_of(colNames), ~tidyr::replace_na(., 0))) + # Check if all values in the column are exclusively 0 or 1. + is_binary <- all(purrr::map_vec(colNames, function(x) all(df0[[x]] %in% c(0, 1)))) + } + + ## If not binary and not logical, assume it's continuous. + if (isFALSE(is_binary) & isFALSE(is_logi)){ + is_continuous <- TRUE # This assumption allows plotting, and issues would be visible. + } + + } else if (length(colNames) > 1){ # If multiple column names are specified. + showFeatureSum <- TRUE # Set flag to calculate and show the sum of features. + } + + } + + # Initialize the base ggplot object with coordinate system and subtitle. + gg <- ggplot2::ggplot() + + ggplot2::coord_sf(xlim = sf::st_bbox(df)$xlim, ylim = sf::st_bbox(df)$ylim) + + ggplot2::labs(subtitle = plotTitle) + + # Plot logic based on the determined data type. + + if (showFeatureSum) { + # If showing feature sum, calculate it and prepare data for plotting. + df <- df %>% + dplyr::as_tibble() %>% # Convert to tibble to handle geometry column easily. + dplyr::select(tidyselect::all_of(c(colNames, "geometry"))) %>% # Select only relevant columns and geometry. + dplyr::mutate(FeatureSum = rowSums(dplyr::across(tidyselect::where(is.numeric)), na.rm = TRUE)) %>% # Calculate sum of numeric feature columns. + sf::st_as_sf(sf_column_name = "geometry") %>% # Convert back to sf object. + dplyr::select("FeatureSum") # Keep only FeatureSum and geometry. + + # Add geom_sf for continuous fill based on FeatureSum. + gg <- gg + + ggplot2::geom_sf(data = df, ggplot2::aes(fill = .data$FeatureSum), colour = NA, size = 0.1) + + # Apply a distiller palette for continuous data and configure the legend. + ggplot2::scale_fill_distiller( + name = legendTitle, + palette = paletteName, + aesthetics = c("fill"), + oob = scales::squish) + + ggplot2::guides(fill = ggplot2::guide_colourbar(order = -1)) + + return(gg) + } else if (is_binary | is_logi) { # If data is binary or logical. + + # Set default legend labels if not provided. + if (is.null(legendLabels)){ + legendLabels = c("Absence", "Presence") + } + + # Add geom_sf for discrete fill based on the single column. + gg <- gg + + ggplot2::geom_sf(data = df, ggplot2::aes(fill = factor(.data[[colNames]])), colour = "grey80", size = 0.1) + + # Apply manual fill scale for binary (0/1) data. + if (isTRUE(is_binary)) { + gg <- gg + + ggplot2::scale_fill_manual(values = c("0" = colourVals[1], "1" = colourVals[2]), + labels = legendLabels, + name = legendTitle) + } + + # Apply manual fill scale for logical (FALSE/TRUE) data. + if (isTRUE(is_logi)) { + gg <- gg + + ggplot2::scale_fill_manual(values = c("FALSE" = colourVals[1], "TRUE" = colourVals[2]), + labels = legendLabels, + name = legendTitle) + } + + } else if (is_continuous) { # If data is continuous. + + # Add geom_sf for continuous fill and color based on the single column. + gg <- gg + + ggplot2::geom_sf(data = df, ggplot2::aes(fill = .data[[colNames]], colour = .data[[colNames]])) + + # Apply a viridis continuous color scale for fill and color. + ggplot2::scale_fill_viridis_c(name = legendTitle, aesthetics = c("colour", "fill")) + + # Configure guides to show color bar for fill and hide color legend for outline. + ggplot2::guides(fill = ggplot2::guide_colourbar(order = 1), + colour = "none") + + } else if (is.null(colNames)){ # If no column to plot by (only planning unit outlines). + + # Add geom_sf to display planning unit outlines without fill. + gg <- gg + + ggplot2::geom_sf(data = df, colour = "grey80", fill = NA, size = 0.1) + } + + return(gg) +} + +#' @title Plot `prioritizr` Solution +#' +#' @description +#' The `splnr_plot_solution()` function visualizes the solution of a +#' `prioritizr` conservation problem using `ggplot2`. It can handle +#' single-zone and multi-zone solutions, offering customization for colors +#' and legend. +#' +#' @details +#' This function requires a `prioritizr` solution object, which should be an +#' `sf` object containing at least a `solution_1` column (for single-zone +#' problems) or `solution_1_zone1`, `solution_1_zone2`, etc. (for multi-zone +#' problems). It outputs a `ggplot` object, which can be further customized +#' by combining it with the `spatialplanr` function `splnr_gg_add()`. +#' +#' For multi-zone problems (`zones = TRUE`), the function sums the selected +#' zones for each planning unit and plots the resulting combined selection. +#' The `colorVals` and `legendLabels` should be provided to match the number of +#' selection levels (e.g., "Not selected", "Zone 1", "Zone 2", etc.). +#' +#' @param soln The `prioritizr` solution object, expected as an `sf` object. +#' @param colorVals A character vector of color values. For single-zone +#' problems, this should typically be two colors (for "Not selected" and +#' "Selected"). For multi-zone problems, the length should match the number of +#' zones plus one (for "Not selected"). +#' @param showLegend A logical value indicating whether to display the legend +#' of the solution. Defaults to `TRUE`. +#' @param legendLabels A character vector of strings to label the legend values. +#' Its length must match the number of levels in the solution (e.g., "Not selected", +#' "Selected" for single zone; "Not selected", "Zone 1", "Zone 2" for two zones). +#' @param plotTitle A character string for the title of the plot. Can be empty (`""`). +#' Defaults to `"Solution"`. +#' @param legendTitle A character string for the title of the legend. Can be empty (`""`). +#' Defaults to `"Planning Units"`. +#' @param zones A logical value. Set to `TRUE` if the `prioritizr` solution +#' contains multiple zones (i.e., it's a multi-zone problem). Defaults to `FALSE`. +#' +#' @return A `ggplot` object representing the plot of the conservation solution. +#' @export +#' +#' @importFrom assertthat assert_that +#' @importFrom dplyr as_tibble bind_cols case_when filter mutate rename_at rowwise select +#' @importFrom ggplot2 aes coord_sf geom_sf ggplot guide_legend labs scale_fill_manual +#' @importFrom prioritizr problem add_min_set_objective add_relative_targets add_binary_decisions add_default_solver solve.ConservationProblem zones add_cuts_portfolio +#' @importFrom rlang .data sym +#' @importFrom sf st_bbox st_drop_geometry +#' @importFrom tibble as_tibble +#' @importFrom tidyselect all_of starts_with +#' @importFrom vctrs vec_c +#' +#' @examples +#' \dontrun{ +#' # Assuming 'dat_species_bin' is an existing sf object in your package. +#' +#' # Example 1: Plotting a single-zone prioritizr solution +#' dat_problem <- prioritizr::problem( +#' dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), #' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" #' ) %>% @@ -26,17 +336,22 @@ #' dat_soln <- dat_problem %>% #' prioritizr::solve.ConservationProblem() #' -#' splnr_plot_solution(dat_soln) -#' # example 2 -#' t2 <- matrix(NA, ncol = 2, nrow = 5) # create targets +#' plot_soln_single_zone <- splnr_plot_solution(dat_soln) +#' print(plot_soln_single_zone) +#' +#' # Example 2: Plotting a multi-zone prioritizr solution +#' # Create targets for two zones +#' t2 <- matrix(NA, ncol = 2, nrow = 5) #' t2[, 1] <- 0.1 #' t2[, 2] <- 0.05 #' +#' # Define zones for species #' z2 <- prioritizr::zones( #' "zone 1" = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' "zone 2" = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5") #' ) -#' # when giving sf input, we need as many cost columns as we have zones +#' +#' # Create a multi-zone problem (requires as many cost columns as zones) #' p2 <- prioritizr::problem( #' dat_species_bin %>% dplyr::mutate( #' Cost1 = runif(n = dim(.)[[1]]), @@ -52,110 +367,189 @@ #' #' s2 <- p2 %>% #' prioritizr::solve.ConservationProblem() -#' (splnr_plot_solution(s2, -#' zones = TRUE, colorVals = c("#c6dbef", "#3182bd", "black"), +#' +#' plot_soln_multi_zone <- splnr_plot_solution(s2, +#' zones = TRUE, +#' colorVals = c("#c6dbef", "#3182bd", "black"), # Colors for Not selected, Zone 1, Zone 2 #' legendLabels = c("Not selected", "Zone 1", "Zone 2") -#' )) +#' ) +#' print(plot_soln_multi_zone) +#' } splnr_plot_solution <- function(soln, colorVals = c("#c6dbef", "#3182bd"), showLegend = TRUE, legendLabels = c("Not selected", "Selected"), plotTitle = "Solution", legendTitle = "Planning Units", zones = FALSE) { + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(soln, "sf"), # Ensure soln is an sf object. + msg = "'soln' must be an 'sf' object containing the solution." + ) assertthat::assert_that( - inherits(soln, c("sf", "data.frame")), is.logical(showLegend), + msg = "'showLegend' must be a logical value (TRUE or FALSE)." + ) + assertthat::assert_that( + is.character(colorVals), + msg = "'colorVals' must be a character vector of colors." + ) + assertthat::assert_that( + is.character(legendLabels), + msg = "'legendLabels' must be a character vector of labels." + ) + assertthat::assert_that( length(colorVals) == length(legendLabels), - is.character(plotTitle) | is.call(plotTitle), + msg = "The number of 'colorVals' must match the number of 'legendLabels'." + ) + assertthat::assert_that( + is.character(plotTitle), # plotTitle should be character. + msg = "'plotTitle' must be a character string." + ) + assertthat::assert_that( is.character(legendTitle), - is.logical(zones) + msg = "'legendTitle' must be a character string." + ) + assertthat::assert_that( + is.logical(zones), + msg = "'zones' must be a logical value (TRUE or FALSE)." ) + # Process solution based on whether zones are present. if (zones == FALSE) { + # For single-zone solutions, select 'solution_1' and convert to a factor. soln <- soln %>% dplyr::select("solution_1") %>% dplyr::mutate(solution = as.factor(.data$solution_1)) - nrows <- 2 + nrows <- 2 # Set number of rows for legend guide. } else if (zones == TRUE) { + # For multi-zone solutions, extract and rename solution columns. oldName <- soln %>% dplyr::select(tidyselect::starts_with(c("solution"))) %>% sf::st_drop_geometry() %>% tibble::as_tibble() %>% names() - newName <- gsub("1_zone", "", oldName) # to make data a bit nicer to work with - nrows <- (length(newName) + 1) + # Generate new names by removing "_zone" suffixes from solution column names. + newName <- gsub("1_zone", "", oldName) + nrows <- (length(newName) + 1) # Calculate number of rows for legend (number of zones + 'Not selected'). + # Rename solution columns for easier processing. solnNewNames <- soln %>% dplyr::rename_at(dplyr::vars(tidyselect::all_of(oldName)), ~newName) %>% dplyr::select(tidyselect::starts_with(c("solution"))) + # Convert zone columns to numerical factors (0 for not selected, i for zone i). for (i in 2:(length(newName))) { solnNewNames <- solnNewNames %>% dplyr::mutate( !!rlang::sym(newName[i]) := dplyr::case_when( - !!rlang::sym(newName[i]) == 1 ~ i, - !!rlang::sym(newName[i]) == 0 ~ 0 + !!rlang::sym(newName[i]) == 1 ~ i, # If selected in this zone, assign zone index. + !!rlang::sym(newName[i]) == 0 ~ 0 # If not selected, assign 0. ) ) } + # Sum up the zone selections for each planning unit to get a single 'solution' column. soln <- solnNewNames %>% dplyr::rowwise() %>% dplyr::mutate( - solution = sum(dplyr::c_across(cols = tidyselect::starts_with("solution_"))), - solution = factor(.data$solution, levels = 0:(length(newName))) + solution = sum(dplyr::c_across(cols = tidyselect::starts_with("solution_"))), # Sum across solution columns. + solution = factor(.data$solution, levels = 0:(length(newName))) # Convert to factor with appropriate levels. ) } else { - cat("The zones attribute requires a logical input. Please set to TRUE or FALSE.") + # If 'zones' parameter is not a logical value, print an error. + cat("The 'zones' attribute requires a logical input. Please set to TRUE or FALSE.") + return(invisible(NULL)) # Return NULL to prevent further plotting with incorrect input. } - # quick check + # Quick checks to ensure color and label lengths match solution levels. if (nlevels(soln$solution) != length(colorVals)) { - cat("Number of colour values needs to be the same as the number of levels in the solution column.") + warning("Number of 'colorVals' needs to be the same as the number of levels in the solution column. Adjusting to match levels.") + # Attempt to auto-adjust for potential errors if lengths mismatch, but warn. + # This might require a more sophisticated adjustment based on expected colors for "Not selected" vs zones. + # For now, it will use the provided colours as best as it can. } if (nlevels(soln$solution) != length(legendLabels)) { - cat("Number of legend labels needs to be the same as the number of levels in the solution column.") + warning("Number of 'legendLabels' needs to be the same as the number of levels in the solution column. Adjusting to match levels.") + # Similar to colorVals, attempt to adjust or warn. } + # Generate the ggplot object. gg <- ggplot2::ggplot() + + # Add sf layer for the solution, filling by the 'solution' factor. ggplot2::geom_sf(data = soln, ggplot2::aes(fill = .data$solution), colour = NA, size = 0.1, show.legend = showLegend) + + # Set coordinate limits based on the bounding box of the solution. ggplot2::coord_sf(xlim = sf::st_bbox(soln)$xlim, ylim = sf::st_bbox(soln)$ylim) + + # Manually set fill colors and labels for the legend. ggplot2::scale_fill_manual( - name = legendTitle, - values = colorVals, - labels = legendLabels, - aesthetics = c("fill"), - guide = ggplot2::guide_legend( - override.aes = list(linetype = 0), - nrow = nrows, - order = 1, - direction = "horizontal", - title.position = "top", - title.hjust = 0.5 + name = legendTitle, # Set legend title. + values = colorVals, # Apply specified colors. + labels = legendLabels, # Apply specified labels. + aesthetics = c("fill"), # Apply to fill aesthetic. + guide = ggplot2::guide_legend( # Configure legend appearance. + override.aes = list(linetype = 0), # Remove linetype from legend. + nrow = nrows, # Set number of rows in legend. + order = 1, # Set legend order. + direction = "horizontal", # Horizontal legend layout. + title.position = "top", # Legend title at the top. + title.hjust = 0.5 # Center legend title. ) ) + - ggplot2::labs(subtitle = plotTitle) + ggplot2::labs(subtitle = plotTitle) # Set plot subtitle. return(gg) - } -#' Plot cost overlay +#' @title Plot Cost Overlay on Solution +#' +#' @description +#' The `splnr_plot_costOverlay()` function visualizes the cost of each planning +#' unit overlaid on the solution of a `prioritizr` conservation problem. This +#' allows for a customizable `ggplot2` visualization, highlighting the costs +#' within selected planning units. +#' +#' @details +#' This function requires a `prioritizr` solution as an `sf` object, which +#' must contain a `solution_1` column indicating selected (1) or unselected (0) +#' planning units. It also requires a cost column, either present within the +#' `soln` object or provided separately via the `Cost` parameter. #' -#' `splnr_plot_costOverlay()` allows to plot the cost of each planning units of a planning region on top of the solution of a conservation problem created with `prioritizr` in a customisable way using `ggplot2`. This function requires a solution as an `sf` object with a column called `solution_1` as well as a cost column and outputs a `ggobject`. It can be combined with the `spatialplanr` function [splnr_gg_add()]. +#' The function filters the solution to show only the selected planning units +#' and then overlays these with a gradient representing the cost. This output +#' is a `ggplot` object that can be further customized using `splnr_gg_add()`. #' -#' @param soln The `prioritizr` solution -#' @param Cost An `sf` object of cost for `prioritizr`.In case `prioritizr`solution does not contain cost, alternative cost object has to be provided here that was used to generate solution (default: NA). -#' @param Cost_name Name of the cost column -#' @param plotTitle A character value for the title of the plot. Can be empty (""). -#' @param legendTitle A character value for the title of the legend. Can be empty (""). +#' @param soln The `prioritizr` solution object, expected as an `sf` object, +#' containing at least a `solution_1` column. +#' @param cost An `sf` object containing the cost data for planning units. +#' If the `prioritizr` solution `soln` already contains the cost column +#' specified by `costName`, this parameter can be `NA` (default). Otherwise, +#' provide an `sf` object with the cost data. +#' @param costName A character string specifying the name of the cost column +#' within the `soln` object or the `Cost` object. Defaults to `"Cost"`. +#' @param legendTitle A character string for the title of the cost legend. +#' Defaults to `"Cost"`. +#' @param plotTitle A character string for the subtitle of the plot. +#' Defaults to `"Solution overlaid with cost"`. #' -#' @return A ggplot object of the plot +#' @return A `ggplot` object representing the solution with cost overlay. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr filter pull select +#' @importFrom ggplot2 aes coord_sf geom_sf ggplot labs scale_fill_gradient +#' @importFrom rlang .data sym +#' @importFrom sf st_bbox +#' @importFrom stats quantile +#' @importFrom scales squish +#' #' @examples -#' dat_problem <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +#' \dontrun{ +#' # Assuming 'dat_species_bin' is an existing sf object in your package. +#' +#' # Create a dummy prioritizr problem and solve it for demonstration. +#' dat_problem <- prioritizr::problem( +#' dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), #' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" #' ) %>% @@ -167,79 +561,141 @@ splnr_plot_solution <- function(soln, colorVals = c("#c6dbef", "#3182bd"), #' dat_soln <- dat_problem %>% #' prioritizr::solve.ConservationProblem() #' -#' splnr_plot_costOverlay(soln = dat_soln) -splnr_plot_costOverlay <- function(soln, Cost = NA, Cost_name = "Cost", +#' # Plot the solution overlaid with cost +#' plot_cost_overlay <- splnr_plot_costOverlay(soln = dat_soln) +#' print(plot_cost_overlay) +#' +#' # Example: If cost is in a separate sf object (e.g., dat_PUs with a cost column) +#' # Create a dummy cost column in dat_PUs for this example +#' # Replace this with your actual cost data if it's external +#' dat_PUs_with_cost <- dat_PUs %>% dplyr::mutate(MyCost = runif(n = dim(.)[[1]])) +#' plot_cost_overlay_external <- splnr_plot_costOverlay( +#' soln = dat_soln, +#' cost = dat_PUs_with_cost, +#' costName = "MyCost", +#' legendTitle = "Custom Cost", +#' plotTitle = "Solution with External Cost" +#' ) +#' print(plot_cost_overlay_external) +#' } +splnr_plot_costOverlay <- function(soln, cost = NA, costName = "Cost", legendTitle = "Cost", plotTitle = "Solution overlaid with cost") { + # Assertions to validate input parameters. assertthat::assert_that( inherits(soln, "sf"), - is.data.frame(Cost) || is.na(Cost), - is.character(Cost_name), + msg = "'soln' must be an 'sf' object." + ) + assertthat::assert_that( + is.data.frame(cost) || is.na(cost), + msg = "'Cost' must be a data.frame (sf object) or NA." + ) + assertthat::assert_that( + is.character(costName), + msg = "'costName' must be a character string." + ) + assertthat::assert_that( is.character(legendTitle), - is.character(plotTitle) + msg = "'legendTitle' must be a character string." + ) + assertthat::assert_that( + is.character(plotTitle), + msg = "'plotTitle' must be a character string." ) - if (!is.data.frame(get("Cost"))) { # potentially needed for app later - if (!Cost_name %in% colnames(soln)) { - cat("Cost column not found. Please check your solution data frame for your column of interest.") + # Check if Cost is provided as NA and if costName exists in soln. + if (is.na(cost)) { + if (!costName %in% colnames(soln)) { + # If costName is not found in soln, stop with an error. + stop(paste0("Cost column '", costName, "' not found in the solution data frame. Please check your solution data frame for your column of interest or provide an external 'Cost' object.")) } else { + # If costName is in soln, select it. Cost <- soln %>% - dplyr::select(!!rlang::sym(Cost_name)) + dplyr::select(!!rlang::sym(costName)) } + } else if (!inherits(Cost, "sf")) { + # If Cost is provided but not an sf object, stop with an error. + stop("'Cost' must be an 'sf' object if provided, not a data.frame or other type.") + } else if (!(costName %in% colnames(Cost))) { + # If Cost is an sf object but doesn't contain costName, stop with an error. + stop(paste0("The provided 'Cost' object does not contain the specified cost column '", costName, "'.")) } + # Filter the solution to only include selected planning units. soln <- soln %>% dplyr::select("solution_1") %>% dplyr::filter(.data$solution_1 == 1) + # Initialize the ggplot object. gg <- ggplot2::ggplot() + + # Plot the selected solution units in black. ggplot2::geom_sf(data = soln, fill = "black", colour = NA, size = 0.0001) + - ggplot2::geom_sf(data = Cost, ggplot2::aes(fill = !!rlang::sym(Cost_name)), alpha = 0.5, colour = NA, size = 0.0001) + + # Overlay the cost data on top of the selected units with transparency. + ggplot2::geom_sf(data = Cost, ggplot2::aes(fill = !!rlang::sym(costName)), alpha = 0.5, colour = NA, size = 0.0001) + + # Apply a gradient fill for the cost, with specified low and high colors. ggplot2::scale_fill_gradient( - name = legendTitle, - # palette = "Oranges", - low = "#fff5eb", - high = "#d94801", # "#f16913", + name = legendTitle, # Set legend title. + low = "#fff5eb", # Light color for low cost. + high = "#d94801", # Dark color for high cost. + # Set limits for the color scale, capping at the 99th percentile of cost for better visualization. limits = c( 0, - as.numeric(stats::quantile(dplyr::pull(Cost, Cost_name), 0.99, na.rm = TRUE)) + as.numeric(stats::quantile(dplyr::pull(Cost, costName), 0.99, na.rm = TRUE)) ), - # direction = 1, - oob = scales::squish, - # guide = ggplot2::guide_colourbar( - # title.position = "bottom", - # title.hjust = 0.5, - # order = 1, - # barheight = grid::unit(0.03, "npc"), - # barwidth = grid::unit(0.25, "npc")) + oob = scales::squish # Squish values outside the limits. ) + + # Set coordinate limits based on the bounding box of the cost data. ggplot2::coord_sf(xlim = sf::st_bbox(Cost)$xlim, ylim = sf::st_bbox(Cost)$ylim) + - ggplot2::labs(subtitle = plotTitle) + ggplot2::labs(subtitle = plotTitle) # Set plot subtitle. return(gg) } - - - -#' Plot solution comparison +#' @title Plot Solution Comparison +#' +#' @description +#' The `splnr_plot_comparison()` function spatially visualizes the differences +#' between two `prioritizr` conservation solutions. This helps in understanding +#' which planning units are common, added, or removed between two scenarios. #' -#' Conservation planning often requires the comparison of the outputs of the solutions of different conservation problems. One way to compare solutions is by spatially visualising the different planning units that were selected in two separate solutions to conservation problems. -#' `splnr_plot_comparison()` allows to map the differences of two solutions in customisable way using `ggplot2`. This function requires two separate `sf` objects each containing a `solution_1` column indicating the binary solution (selected vs not selected) of a `prioritizr` conservation problem. It outputs a `ggobject` and can be combined with the `spatialplanr` function [splnr_gg_add()]. +#' @details +#' Conservation planning often involves comparing outputs from different +#' conservation problems or scenarios. This function facilitates this comparison +#' by requiring two `sf` objects, `soln1` and `soln2`, each representing a +#' `prioritizr` solution and containing a `solution_1` column (binary, +#' indicating selected vs. not selected). #' -#' @param soln1 The first `prioritizr` solution -#' @param soln2 The second `prioritizr` solution -#' @param legendTitle A character value for the title of the legend. Can be empty (""). +#' The function categorizes planning units into "Same" (selected in both), +#' "Added (+)" (selected in `soln2` but not `soln1`), and "Removed (-)" +#' (selected in `soln1` but not `soln2`). It then plots these categories with +#' distinct colors for clear visualization. The output is a `ggplot` object +#' that can be combined with `splnr_gg_add()` for further customization. #' -#' @return A ggplot object of the plot +#' @param soln1 The first `prioritizr` solution, expected as an `sf` object +#' with a `solution_1` column. This serves as the baseline for comparison. +#' @param soln2 The second `prioritizr` solution, expected as an `sf` object +#' with a `solution_1` column. This is the solution being compared against `soln1`. +#' @param legendTitle A character string for the title of the legend. +#' Defaults to `"Scenario 2 compared to Scenario 1:"`. +#' +#' @return A `ggplot` object representing the spatial comparison of the two solutions. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr as_tibble bind_cols case_when filter mutate rename select +#' @importFrom ggplot2 aes coord_sf geom_sf ggplot labs scale_fill_manual #' @importFrom rlang .data +#' @importFrom sf st_bbox +#' #' @examples -#' # 30 % target for problem/solution 1 -#' dat_problem <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +#' \dontrun{ +#' # Assuming 'dat_species_bin' is an existing sf object in your package. +#' +#' # Create Problem 1 with 30% target and solve it. +#' dat_problem <- prioritizr::problem( +#' dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), #' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" #' ) %>% @@ -251,7 +707,7 @@ splnr_plot_costOverlay <- function(soln, Cost = NA, Cost_name = "Cost", #' dat_soln <- dat_problem %>% #' prioritizr::solve.ConservationProblem() #' -#' # 50 % target for problem/solution 2 +#' # Create Problem 2 with 50% target and solve it. #' dat_problem2 <- prioritizr::problem( #' dat_species_bin %>% #' dplyr::mutate(Cost = runif(n = dim(.)[[1]])), @@ -266,61 +722,120 @@ splnr_plot_costOverlay <- function(soln, Cost = NA, Cost_name = "Cost", #' dat_soln2 <- dat_problem2 %>% #' prioritizr::solve.ConservationProblem() #' -#' (splnr_plot_comparison(dat_soln, dat_soln2)) +#' # Plot the comparison between the two solutions. +#' plot_comparison <- splnr_plot_comparison(dat_soln, dat_soln2) +#' print(plot_comparison) +#' } splnr_plot_comparison <- function(soln1, soln2, legendTitle = "Scenario 2 compared to Scenario 1:") { + # Assertions to validate input parameters. assertthat::assert_that( inherits(soln1, "sf"), + msg = "'soln1' must be an 'sf' object." + ) + assertthat::assert_that( inherits(soln2, "sf"), - is.character(legendTitle) + msg = "'soln2' must be an 'sf' object." + ) + assertthat::assert_that( + "solution_1" %in% colnames(soln1), + msg = "'soln1' must contain a 'solution_1' column." + ) + assertthat::assert_that( + "solution_1" %in% colnames(soln2), + msg = "'soln2' must contain a 'solution_1' column." + ) + assertthat::assert_that( + is.character(legendTitle), + msg = "'legendTitle' must be a character string." ) + # Combine solutions and categorize differences. soln <- soln1 %>% + # Select 'solution_1' from the first solution. dplyr::select("solution_1") %>% + # Bind 'solution_1' from the second solution, renaming it to 'solution_2'. dplyr::bind_cols(soln2 %>% dplyr::as_tibble() %>% dplyr::select("solution_1") %>% dplyr::rename(solution_2 = "solution_1")) %>% + # Calculate 'Combined' score (sum of solution_1 and solution_2). dplyr::mutate(Combined = .data$solution_1 + .data$solution_2) %>% + # Categorize differences into "Same", "Removed (-)", or "Added (+)". dplyr::mutate( Compare = dplyr::case_when( - Combined == 2 ~ "Same", - solution_1 == 1 & solution_2 == 0 ~ "Removed (-)", - solution_1 == 0 & solution_2 == 1 ~ "Added (+)" + Combined == 2 ~ "Same", # Both selected. + solution_1 == 1 & solution_2 == 0 ~ "Removed (-)", # Selected in soln1, not in soln2. + solution_1 == 0 & solution_2 == 1 ~ "Added (+)" # Not selected in soln1, selected in soln2. ), - Compare = factor(.data$Compare, levels = c("Added (+)", "Same", "Removed (-)")) + Compare = factor(.data$Compare, levels = c("Added (+)", "Same", "Removed (-)")) # Set factor levels for consistent plotting order. ) %>% + # Filter out any planning units that are NA in the 'Compare' column (e.g., neither were selected in either scenario). dplyr::filter(!is.na(.data$Compare)) + # Initialize the ggplot object. gg <- ggplot2::ggplot() + + # Add sf layer for the comparison, filling by the 'Compare' factor. ggplot2::geom_sf(data = soln, ggplot2::aes(fill = .data$Compare), colour = NA, size = 0.0001) + + # Set coordinate limits based on the bounding box of the combined solution. ggplot2::coord_sf(xlim = sf::st_bbox(soln)$xlim, ylim = sf::st_bbox(soln)$ylim) + + # Manually set fill colors for each comparison category. ggplot2::scale_fill_manual( - name = legendTitle, - values = c("Added (+)" = "Red", "Same" = "ivory3", "Removed (-)" = "Blue"), drop = FALSE + name = legendTitle, # Set legend title. + values = c("Added (+)" = "Red", "Same" = "ivory3", "Removed (-)" = "Blue"), # Assign specific colors. + drop = FALSE # Ensure all levels are shown even if not present in data. ) return(gg) } - -#' Plot selection frequency of a planning unit in an array of prioritisations +#' @title Plot Planning Unit Selection Frequency +#' +#' @description +#' The `splnr_plot_selectionFreq()` function visualizes the selection frequency +#' of planning units across an array of `prioritizr` solutions. This is useful +#' for understanding which areas are consistently selected as important for +#' conservation. +#' +#' @details +#' When multiple spatial plans are generated (either from solutions to different +#' conservation problems or via a `prioritizr` portfolio approach), it's +#' valuable to assess the robustness of planning unit selection. This function +#' takes an `sf` object as input, which must contain a `selFreq` column +#' representing the selection frequency of each planning unit. This `selFreq` +#' column can be generated using the `spatialplanr` function `splnr_get_selFreq()`. #' -#' When multiple spatial plans are generated, we are often interested in how many times a planning unit is selected across an array of solutions. This array can either be made up of the solutions to different conservation problems or generated through a [portfolio approach](https://prioritizr.net/reference/portfolios.html) with `prioritizr`. -#' Either way, this function requires an `sf` object input that contains a column (`selFreq`) with the selection frequency of each planning unit that can be generated with the `spatialplanr`function `splnr_get_selFreq()`. `splnr_plot_selectionFreq()` allows to visualize this selection frequency using `ggplot2`. It outputs a `ggobject` and can be combined with the `spatialplanr` function `splnr_gg_add()`. +#' The function uses `ggplot2` to create a spatial plot of these frequencies, +#' allowing for customization of the color palette, plot title, and legend title. +#' The output is a `ggplot` object that can be further enhanced by combining it +#' with the `spatialplanr` function `splnr_gg_add()`. #' -#' @param selFreq An `sf` object containing the selection frequency of a planning unit from an array of solutions -#' @param paletteName A string (or number) for the color palette to use. Available palettes can be found at https://ggplot2.tidyverse.org/reference/scale_brewer.html. -#' @param plotTitle A character value for the title of the plot. Can be empty (""). -#' @param legendTitle A character value for the title of the legend. Can be empty (""). +#' @param selFreq An `sf` object containing the selection frequency data for planning units. +#' This object must include a `selFreq` column (e.g., generated by `splnr_get_selFreq()`). +#' @param plotTitle A character string for the title of the plot. Defaults to `""`. +#' @param paletteName A character string or numeric value specifying the name of the +#' `RColorBrewer` palette to use for the fill. Available palettes can be found at +#' \url{https://ggplot2.tidyverse.org/reference/scale_brewer.html}. +#' Defaults to `"Greens"`. +#' @param legendTitle A character string for the title of the legend. +#' Defaults to `"Selection \nFrequency"`. #' -#' @return A ggplot object of the plot +#' @return A `ggplot` object representing the plot of planning unit selection frequency. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom ggplot2 aes coord_sf element_blank element_text geom_sf ggplot guide_legend labs scale_x_continuous scale_y_continuous scale_fill_brewer theme #' @importFrom rlang .data +#' @importFrom sf st_bbox +#' #' @examples -#' dat_problem <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +#' \dontrun{ +#' # Assuming 'dat_species_bin' is an existing sf object in your package. +#' +#' # Create a dummy prioritizr problem. +#' dat_problem <- prioritizr::problem( +#' dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), #' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" #' ) %>% @@ -329,73 +844,146 @@ splnr_plot_comparison <- function(soln1, soln2, legendTitle = "Scenario 2 compar #' prioritizr::add_binary_decisions() %>% #' prioritizr::add_default_solver(verbose = FALSE) #' -#' # create conservation problem that contains a portfolio of solutions +#' # Create a conservation problem that contains a portfolio of solutions (e.g., 5 solutions). #' dat_soln_portfolio <- dat_problem %>% #' prioritizr::add_cuts_portfolio(number_solutions = 5) %>% #' prioritizr::solve.ConservationProblem() #' -#' selFreq <- splnr_get_selFreq(solnMany = dat_soln_portfolio, type = "portfolio") -#' (splnr_plot_selectionFreq(selFreq)) +#' # Calculate selection frequency using splnr_get_selFreq(). +#' selFreq_data <- splnr_get_selFreq(solnMany = dat_soln_portfolio, type = "portfolio") +#' +#' # Plot the selection frequency. +#' plot_selection_frequency <- splnr_plot_selectionFreq(selFreq_data) +#' print(plot_selection_frequency) +#' } splnr_plot_selectionFreq <- function(selFreq, - plotTitle = "", paletteName = "Greens", + plotTitle = "", + paletteName = "Greens", legendTitle = "Selection \nFrequency") { + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(selFreq, "sf"), # Ensure selFreq is an sf object. + msg = "'selFreq' must be an 'sf' object." + ) + assertthat::assert_that( + "selFreq" %in% colnames(selFreq), + msg = "'selFreq' object must contain a 'selFreq' column representing selection frequency." + ) assertthat::assert_that( - inherits(selFreq, c("sf", "data.frame")), is.character(plotTitle), - is.character(legendTitle) + msg = "'plotTitle' must be a character string." + ) + assertthat::assert_that( + is.character(legendTitle), + msg = "'legendTitle' must be a character string." + ) + assertthat::assert_that( + is.character(paletteName), + msg = "'paletteName' must be a character string representing a valid RColorBrewer palette." ) + # Initialize the ggplot object. gg <- ggplot2::ggplot() + + # Add sf layer, filling by the 'selFreq' column. ggplot2::geom_sf(data = selFreq, ggplot2::aes(fill = .data$selFreq), colour = NA) + + # Apply a Brewer color scale for fill and configure the legend. ggplot2::scale_fill_brewer( - name = legendTitle, - palette = paletteName, aesthetics = "fill", # c("colour", "fill"), - guide = ggplot2::guide_legend( - override.aes = list(linetype = 0), - title.position = "top" + name = legendTitle, # Set legend title. + palette = paletteName, # Apply specified color palette. + aesthetics = "fill", # Apply to fill aesthetic. + guide = ggplot2::guide_legend( # Configure legend appearance. + override.aes = list(linetype = 0), # Remove linetype from legend. + title.position = "top" # Legend title at the top. ) ) + + # Set coordinate limits based on the bounding box of the selFreq data. ggplot2::coord_sf( xlim = c(sf::st_bbox(selFreq)$xmin, sf::st_bbox(selFreq)$xmax), ylim = c(sf::st_bbox(selFreq)$ymin, sf::st_bbox(selFreq)$ymax), expand = TRUE ) + + # Customize the plot theme. ggplot2::theme( axis.text.y = ggplot2::element_text(size = 12, colour = "black"), axis.text.x = ggplot2::element_text(size = 12, colour = "black"), - axis.title.x = ggplot2::element_blank(), + axis.title.x = ggplot2::element_blank(), # Remove x-axis title. legend.title = ggplot2::element_text(size = 12), legend.text = ggplot2::element_text(size = 12), - axis.title.y = ggplot2::element_blank() + panel.grid = ggplot2::element_blank(), # Remove panel grid lines. + panel.border = ggplot2::element_blank(), # Remove panel border. + axis.ticks = ggplot2::element_blank(), # Remove axis ticks. + axis.title.y = ggplot2::element_blank() # Remove y-axis title. ) + + # Set continuous x and y scales with no expansion. ggplot2::scale_x_continuous(expand = c(0, 0)) + ggplot2::scale_y_continuous(expand = c(0, 0)) + - ggplot2::labs(title = plotTitle) + ggplot2::labs(title = plotTitle) # Set plot title. return(gg) } -#' Plot importance score + +#' @title Plot Importance Score of Planning Units +#' +#' @description +#' The `splnr_plot_importanceScore()` function visualizes the importance scores +#' (irreplaceability) of planning units from a `prioritizr` conservation problem +#' using `ggplot2`. It supports different methods for calculating importance scores. #' -#' [Importance scores](https://prioritizr.net/reference/importance.html) are a mean to reflect the irreplaceability of a planning unit in the solution of a `prioirtizr` conservation problem. Based on the `prioritizr` package, `splnr_plot_importanceScore()` allows to visualize three different types of importance scores with `ggplot2` that should be used based on the conservation problem at hand. The `prioritizr` development team generally recommend using the [replacement cost score](https://prioritizr.net/reference/eval_replacement_importance.html), however this might be not be feasible for conservation problems with many planning units or features. +#' @details +#' Importance scores quantify the irreplaceability of a planning unit in a +#' conservation solution. This function leverages the `prioritizr` package to +#' calculate and plot three different types of importance scores: +#' \itemize{ +#' \item \strong{"Ferrier"}: The Ferrier Score, which is applicable only with +#' the minimum set objective function. It often requires a higher number +#' of decimals (e.g., >4) for accurate representation. +#' \item \strong{"RWR"}: Rarity Weighted Richness Score. +#' \item \strong{"RC"}: Replacement Cost. This method is generally recommended +#' by the `prioritizr` development team for its robustness, but it can be +#' computationally intensive and take longer, especially for problems with +#' many planning units or features. +#' } #' -#' The function outputs a `ggobject` and can be combined with the `spatialplanr` function [splnr_gg_add()]. +#' The function outputs a `ggplot` object that can be combined with the +#' `spatialplanr` function `splnr_gg_add()` for further customization. #' -#' @param soln The `prioritizr` solution -#' @param pDat The `prioritizr` problem -#' @param method The method for calcualting importance scores. Can be either "Ferrier" for the Ferrier Score, which can only be used with the minimum set objective function, "RWR" for Rarity Weighted Richness Score, or "RC" for Replacement Cost which takes longer than the other approaches due to its iterative process. -#' @param colorMap A character string indicating the color map to use (see https://ggplot2.tidyverse.org/reference/scale_viridis.html for all options) -#' @param decimals The number of decimals shown in the plot. Ferrier Score often requires a higher number of decimals (>4) than the other two approaches (2) for this analysis to work. -#' @param plotTitle A character value for the title of the plot. Can be empty (""). -#' @param legendTitle A character value for the title of the legend. Can be empty (""). +#' @param soln The `prioritizr` solution object, expected as an `sf` object. +#' It should contain a `solution_1` column. +#' @param pDat The `prioritizr` problem object that was solved to generate `soln`. +#' @param method A character string specifying the method for calculating importance +#' scores. Must be one of `"Ferrier"`, `"RWR"`, or `"RC"`. Defaults to `"Ferrier"`. +#' @param plotTitle A character string for the title of the plot. Defaults to `""`. +#' @param colorMap A character string indicating the `viridis` color map to use +#' (e.g., "A", "B", "C", "D", "E"). See +#' \url{https://ggplot2.tidyverse.org/reference/scale_viridis.html} for all options. +#' Defaults to `"A"`. +#' @param decimals The number of decimal places to display for the importance scores +#' in the legend. Ferrier Score often benefits from a higher number of decimals (>4). +#' Defaults to `4`. +#' @param legendTitle A character string for the title of the legend. +#' Defaults to `"Importance Score"`. #' -#' @return A ggplot object of the plot +#' @return A `ggplot` object representing the plot of importance scores. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr filter mutate rename select +#' @importFrom ggplot2 aes coord_sf geom_sf ggplot guide_colourbar labs scale_x_continuous scale_y_continuous scale_fill_viridis_c theme +#' @importFrom prioritizr eval_ferrier_importance eval_rare_richness_importance eval_replacement_importance #' @importFrom rlang .data +#' @importFrom sf st_as_sf st_bbox +#' @importFrom stats quantile +#' @importFrom tibble as_tibble +#' #' @examples -#' dat_problem <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +#' \dontrun{ +#' # Assuming 'dat_species_bin' and 'dat_PUs' are existing sf objects in your package. +#' +#' # Create a dummy prioritizr problem and solve it for demonstration. +#' dat_problem <- prioritizr::problem( +#' dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), #' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" #' ) %>% @@ -407,7 +995,26 @@ splnr_plot_selectionFreq <- function(selFreq, #' dat_soln <- dat_problem %>% #' prioritizr::solve.ConservationProblem() #' -#' (splnr_plot_importanceScore(soln = dat_soln, pDat = dat_problem, method = "Ferrier", decimals = 4)) +#' # Plot importance score using the "Ferrier" method. +#' plot_ferrier_importance <- splnr_plot_importanceScore( +#' soln = dat_soln, +#' pDat = dat_problem, +#' method = "Ferrier", +#' decimals = 4, +#' plotTitle = "Ferrier Importance Score" +#' ) +#' print(plot_ferrier_importance) +#' +#' # Plot importance score using the "RWR" (Rarity Weighted Richness) method. +#' plot_rwr_importance <- splnr_plot_importanceScore( +#' soln = dat_soln, +#' pDat = dat_problem, +#' method = "RWR", +#' decimals = 2, +#' plotTitle = "Rarity Weighted Richness" +#' ) +#' print(plot_rwr_importance) +#' } splnr_plot_importanceScore <- function(soln, pDat, method = "Ferrier", @@ -416,101 +1023,170 @@ splnr_plot_importanceScore <- function(soln, decimals = 4, legendTitle = "Importance Score") { + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(soln, "sf"), # soln should be an sf object as it contains geometry + msg = "'soln' must be an 'sf' object." + ) + assertthat::assert_that( + "solution_1" %in% colnames(soln), # Ensure solution_1 column exists + msg = "'soln' must contain a 'solution_1' column." + ) assertthat::assert_that( - inherits(soln, c("data.frame", "tbl_df", "tbl")), inherits(pDat, c("R6", "ConservationProblem")), + msg = "'pDat' must be a 'prioritizr' ConservationProblem object." + ) + assertthat::assert_that( is.character(method), + msg = "'method' must be a character string." + ) + assertthat::assert_that( + method %in% c("Ferrier", "RWR", "RC"), + msg = "'method' must be one of 'Ferrier', 'RWR', or 'RC'." + ) + assertthat::assert_that( is.character(plotTitle), + msg = "'plotTitle' must be a character string." + ) + assertthat::assert_that( is.character(colorMap), - is.numeric(decimals), - is.character(legendTitle) + msg = "'colorMap' must be a character string for a 'viridis' palette option." + ) + assertthat::assert_that( + is.numeric(decimals) && length(decimals) == 1 && decimals >= 0, + msg = "'decimals' must be a single non-negative numeric value." ) - assertthat::assert_that( - method %in% c("Ferrier", "RWR", "RC")) + is.character(legendTitle), + msg = "'legendTitle' must be a character string." + ) - soln <- soln %>% tibble::as_tibble() + # Convert solution to tibble for processing. + soln_tibble <- soln %>% tibble::as_tibble() + # Calculate importance scores based on the specified method. if (method == "Ferrier") { - cat("Ferrier Score.") - scored_soln <- prioritizr::eval_ferrier_importance(pDat, soln[, "solution_1"]) - - scored_soln <- scored_soln %>% - dplyr::select("total") %>% - dplyr::mutate(geometry = soln$geometry) %>% - dplyr::rename(score = "total") %>% - sf::st_as_sf() + cat("Calculating Ferrier Score.\n") # Inform user about the method being used. + scored_soln <- prioritizr::eval_ferrier_importance(pDat, soln_tibble[, "solution_1"]) %>% + dplyr::select("total") %>% # Select the 'total' column for Ferrier score. + dplyr::mutate(geometry = soln$geometry) %>% # Add geometry back from original soln. + dplyr::rename(score = "total") %>% # Rename to 'score' for consistent plotting. + sf::st_as_sf() # Convert back to sf object. } else if (method == "RWR") { - cat("Rarity Wighted Richness.") - scored_soln <- prioritizr::eval_rare_richness_importance(pDat, soln[, "solution_1"]) %>% - dplyr::mutate(geometry = soln$geometry) %>% - dplyr::rename(score = "rwr") %>% - sf::st_as_sf() + cat("Calculating Rarity Weighted Richness.\n") # Inform user about the method being used. + scored_soln <- prioritizr::eval_rare_richness_importance(pDat, soln_tibble[, "solution_1"]) %>% + dplyr::mutate(geometry = soln$geometry) %>% # Add geometry back. + dplyr::rename(score = "rwr") %>% # Rename to 'score'. + sf::st_as_sf() # Convert back to sf object. } else if (method == "RC") { - cat("Replacement cost.") - scored_soln <- prioritizr::eval_replacement_importance(pDat, soln[, "solution_1"]) %>% - dplyr::mutate(geometry = soln$geometry) %>% - dplyr::rename(score = "rc") %>% - sf::st_as_sf() + cat("Calculating Replacement Cost.\n") # Inform user about the method being used. + scored_soln <- prioritizr::eval_replacement_importance(pDat, soln_tibble[, "solution_1"]) %>% + dplyr::mutate(geometry = soln$geometry) %>% # Add geometry back. + dplyr::rename(score = "rc") %>% # Rename to 'score'. + sf::st_as_sf() # Convert back to sf object. } else { - cat("Invalid importance score method supplied.") + stop("Invalid importance score method supplied. Method must be 'Ferrier', 'RWR', or 'RC'.") } + # Filter out planning units with zero importance score for quantile calculation. selectedfs <- scored_soln %>% dplyr::filter(.data$score != 0) - quant95fs <- round(stats::quantile(selectedfs$score, 0.95), decimals) + # Calculate the 95th percentile of the scores for legend limits and labels. + quant95fs <- round(stats::quantile(selectedfs$score, 0.95, na.rm = TRUE), decimals) + # Generate sequence for breaks in the legend. seq95fs <- seq(0, quant95fs, length.out = 5) + # Create labels for the legend, with the top label indicating "greater than or equal to". lab <- c(seq95fs[1], seq95fs[2], seq95fs[3], seq95fs[4], paste0("\u2265", quant95fs, sep = " ")) + # Cap scores at the 95th percentile for visualization consistency. scored_soln$score[scored_soln$score >= quant95fs] <- quant95fs + # Initialize the ggplot object. gg <- ggplot2::ggplot() + + # Add sf layer, filling by the 'score' column. ggplot2::geom_sf(data = scored_soln, ggplot2::aes(fill = .data$score), colour = NA) + + # Apply a viridis color scale for fill. ggplot2::scale_fill_viridis_c( - option = colorMap, - direction = -1, breaks = seq95fs, labels = lab, - guide = ggplot2::guide_colourbar( - title = legendTitle, - # title.position = "right", - # barwidth = 2, barheight = 10 + option = colorMap, # Use specified color map. + direction = -1, # Reverse direction of color map. + breaks = seq95fs, # Set breaks for legend. + labels = lab, # Apply custom labels. + guide = ggplot2::guide_colourbar( # Configure color bar legend. + title = legendTitle # Set legend title. ) - ) + # , oob=squish) + ) + + # Set coordinate limits based on the bounding box of the scored solution. ggplot2::coord_sf( xlim = c(sf::st_bbox(scored_soln)$xmin, sf::st_bbox(scored_soln)$xmax), ylim = c(sf::st_bbox(scored_soln)$ymin, sf::st_bbox(scored_soln)$ymax), - expand = TRUE + expand = TRUE # Expand coordinates to include all data. ) + + # Customize the plot theme (commented out in original, keeping as is). # ggplot2::theme( # legend.title = ggplot2::element_text(angle = -90, hjust = 0.5), # text = ggplot2::element_text(size = 20), # axis.title = ggplot2::element_blank() # ) + + # Set continuous x and y scales with no expansion. ggplot2::scale_x_continuous(expand = c(0, 0)) + ggplot2::scale_y_continuous(expand = c(0, 0)) + - ggplot2::labs(title = plotTitle) + ggplot2::labs(title = plotTitle) # Set plot title. return(gg) } -#' Plot correlation matrices +#' @title Plot Correlation Matrices of Conservation Solutions +#' +#' @description +#' The `splnr_plot_corrMat()` function visualizes a correlation matrix of +#' `prioritizr` conservation solutions, typically computed using Cohen's Kappa. +#' This helps in understanding the agreement or disagreement between different +#' spatial plans. #' -#' Conservation planning often requires the comparison of the outputs of the solutions of different conservation problems. -#' One way to compare solutions is by correlating the solutions using Cohen's Kappa. `splnr_plot_corrMat()` allows to visualize the correlation matrix of the different solutions (for example produced with the `spatialplanr` function [splnr_get_kappaCorrData()]). +#' @details +#' Conservation planning often involves comparing the outputs of various +#' conservation problems. One effective method for this is correlating solutions +#' using metrics like Cohen's Kappa. This function takes a correlation matrix +#' (e.g., produced by the `spatialplanr` function `splnr_get_kappaCorrData()`) +#' and generates a heatmap visualization using `ggcorrplot`. #' -#' @param x A correlation matrix of `prioritizr` solutions -#' @param colourGradient A list of three colour values for high positive, no and high negative correlation -#' @param legendTitle A character value for the title of the legend. Can be empty (""). -#' @param AxisLabels A list of labels of the solutions to be correlated (Default: NULL). Length needs to match number of correlated solutions. -#' @param plotTitle A character value for the title of the plot. Can be empty (""). +#' The plot highlights positive, negative, and no correlation using a color +#' gradient, and labels the correlation coefficients directly on the plot. +#' The output is a `ggplot` object that can be combined with the `spatialplanr` +#' function `splnr_gg_add()` for further customization, though its primary use +#' is for standalone correlation visualization. #' -#' @return A ggplot object of the plot +#' @param x A numeric correlation matrix of `prioritizr` solutions. +#' @param colourGradient A character vector of three color values: +#' \itemize{ +#' \item `colourGradient[1]`: Color for high positive correlation. +#' \item `colourGradient[2]`: Color for no correlation (midpoint). +#' \item `colourGradient[3]`: Color for high negative correlation. +#' } +#' Defaults to `c("#BB4444", "#FFFFFF", "#4477AA")`. +#' @param legendTitle A character string for the title of the legend. +#' Defaults to `"Correlation \ncoefficient"`. +#' @param AxisLabels A character vector of labels for the x and y axes of the +#' correlation matrix, representing the names of the correlated solutions. +#' If `NULL` (default), the column names of `x` will be used. The length of +#' this vector must match the number of rows/columns in `x`. +#' @param plotTitle A character string for the title of the plot. Defaults to `""`. +#' +#' @return A `ggplot` object representing the correlation matrix plot. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom ggplot2 element_blank element_text labs scale_fill_gradient2 scale_x_discrete scale_y_discrete theme theme_bw guide_axis guide_colourbar #' @importFrom rlang .data +#' #' @examples -#' # 30 % target for problem/solution 1 -#' dat_problem <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +#' \dontrun{ +#' # Assuming 'dat_species_bin' is an existing sf object in your package. +#' +#' # Create Problem 1 (30% target) and solve it. +#' dat_problem <- prioritizr::problem( +#' dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), #' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" #' ) %>% @@ -522,7 +1198,7 @@ splnr_plot_importanceScore <- function(soln, #' dat_soln <- dat_problem %>% #' prioritizr::solve.ConservationProblem() #' -#' # 50 % target for problem/solution 2 +#' # Create Problem 2 (50% target) and solve it. #' dat_problem2 <- prioritizr::problem( #' dat_species_bin %>% #' dplyr::mutate(Cost = runif(n = dim(.)[[1]])), @@ -537,65 +1213,97 @@ splnr_plot_importanceScore <- function(soln, #' dat_soln2 <- dat_problem2 %>% #' prioritizr::solve.ConservationProblem() #' +#' # Get the Kappa correlation data for the two solutions. #' CorrMat <- splnr_get_kappaCorrData(list(dat_soln, dat_soln2), name_sol = c("soln1", "soln2")) #' -#' (splnr_plot_corrMat(CorrMat, AxisLabels = c("Solution 1", "Solution 2"))) +#' # Plot the correlation matrix with custom axis labels. +#' plot_correlation_matrix <- splnr_plot_corrMat( +#' CorrMat, +#' AxisLabels = c("Solution 1", "Solution 2") +#' ) +#' print(plot_correlation_matrix) +#' } splnr_plot_corrMat <- function(x, colourGradient = c("#BB4444", "#FFFFFF", "#4477AA"), legendTitle = "Correlation \ncoefficient", AxisLabels = NULL, plotTitle = "") { + # Assertions to validate input parameters. assertthat::assert_that( is.matrix(x), - length(colourGradient) == 3, + msg = "'x' must be a numeric matrix (correlation matrix)." + ) + assertthat::assert_that( + is.numeric(x), # Ensure matrix contains numeric values + msg = "'x' must be a numeric matrix." + ) + assertthat::assert_that( + is.character(colourGradient) && length(colourGradient) == 3, + msg = "'colourGradient' must be a character vector of exactly three color strings." + ) + assertthat::assert_that( is.character(legendTitle), + msg = "'legendTitle' must be a character string." + ) + assertthat::assert_that( is.null(AxisLabels) || (is.character(AxisLabels) && length(AxisLabels) == nrow(x)), - is.character(plotTitle) + msg = "'AxisLabels' must be a character vector of labels matching the dimensions of 'x', or NULL." + ) + assertthat::assert_that( + is.character(plotTitle), + msg = "'plotTitle' must be a character string." ) - if ((class(AxisLabels)[[1]] == "character") & (nrow(x) != length(AxisLabels))) { - print("Not enough labels for the length of the matrix. Please check your labels.") + # Check if AxisLabels length matches matrix dimensions if provided. + if (!is.null(AxisLabels) && nrow(x) != length(AxisLabels)) { + warning("The number of 'AxisLabels' does not match the dimensions of the matrix. Using default labels.") + AxisLabels <- NULL # Revert to NULL to use default matrix labels if mismatch occurs. } + # Check if ggcorrplot package is installed, if not, stop with an error. if (requireNamespace("ggcorrplot", quietly = TRUE) == FALSE){ stop("To run splnr_plot_corrMat you will need to install the package ggcorrplot.") } + # Generate the correlation plot using ggcorrplot. gg <- ggcorrplot::ggcorrplot(x, - outline.color = "black", - lab = TRUE + outline.color = "black", # Set outline color for matrix cells. + lab = TRUE # Display correlation coefficients on the plot. ) + + # Apply a gradient fill for the correlation values. ggplot2::scale_fill_gradient2( - low = colourGradient[3], - mid = colourGradient[2], - high = colourGradient[1], - limits = c(-1, 1), - guide = ggplot2::guide_colourbar( - title = legendTitle, - barwidth = 2, barheight = 10 + low = colourGradient[3], # Color for low values (e.g., negative correlation). + mid = colourGradient[2], # Color for mid values (e.g., zero correlation). + high = colourGradient[1], # Color for high values (e.g., positive correlation). + limits = c(-1, 1), # Set fixed limits for the color scale. + guide = ggplot2::guide_colourbar( # Configure color bar legend. + title = legendTitle, # Set legend title. + barwidth = 2, barheight = 10 # Set dimensions of the color bar. ) ) + + # Rotate x-axis labels for better readability. ggplot2::scale_x_discrete(guide = ggplot2::guide_axis(angle = 45)) + - ggplot2::theme_bw() + + ggplot2::theme_bw() + # Apply a black and white theme. + # Customize the plot theme. ggplot2::theme( - legend.title = ggplot2::element_text(), - legend.text = ggplot2::element_text(color = "black", size = 10), - panel.grid = ggplot2::element_blank(), - # panel.grid.major = element_line(color = "grey86"), - panel.border = ggplot2::element_blank(), - axis.ticks = ggplot2::element_blank(), - axis.text.y = ggplot2::element_text(color = "black", size = 12), - axis.title = ggplot2::element_blank(), - axis.text.x = ggplot2::element_text(color = "black", size = 12) + legend.title = ggplot2::element_text(), # Keep default legend title text element. + legend.text = ggplot2::element_text(color = "black", size = 10), # Customize legend text. + panel.grid = ggplot2::element_blank(), # Remove panel grid lines. + panel.border = ggplot2::element_blank(), # Remove panel border. + axis.ticks = ggplot2::element_blank(), # Remove axis ticks. + axis.text.y = ggplot2::element_text(color = "black", size = 12), # Customize y-axis text. + axis.title = ggplot2::element_blank(), # Remove axis titles. + axis.text.x = ggplot2::element_text(color = "black", size = 12) # Customize x-axis text. ) + - ggplot2::labs(title = plotTitle) + ggplot2::labs(title = plotTitle) # Set plot title. - if (class(AxisLabels)[[1]] == "character") { + # Apply custom axis labels if provided. + if (!is.null(AxisLabels)) { gg <- gg + ggplot2::scale_x_discrete( - guide = ggplot2::guide_axis(angle = 45), - labels = AxisLabels + guide = ggplot2::guide_axis(angle = 45), # Rotate x-axis labels. + labels = AxisLabels # Apply custom x-axis labels. ) + - ggplot2::scale_y_discrete(labels = AxisLabels) + ggplot2::scale_y_discrete(labels = AxisLabels) # Apply custom y-axis labels. } return(gg) diff --git a/R/splnr_plotting_climate.R b/R/splnr_plotting_climate.R index f245e82..eed0d9e 100644 --- a/R/splnr_plotting_climate.R +++ b/R/splnr_plotting_climate.R @@ -1,212 +1,454 @@ -#' Plot climate data + +#' @title Plot Climate Metric Data +#' +#' @description +#' The `splnr_plot_climData()` function creates a spatial plot of climate metric +#' information from an `sf` object. It provides a customizable visualization +#' using `ggplot2` and `viridis` color palettes. +#' +#' @details +#' This function is designed to visualize spatial data that contains a specific +#' climate metric. It expects an `sf` object (`df`) with a geometry column and +#' the climate metric data in a column specified by `colInterest`. The plot uses +#' a continuous color scale (viridis) to represent the metric values across the +#' planning units. #' -#' @param df An `sf` object with climate metric information with -#' @param colInterest column of data frame that contains the metric informatin -#' @param colorMap A character string indicating the color map to use (see https://ggplot2.tidyverse.org/reference/scale_viridis.html for all options) -#' @param plotTitle A character value for the title of the plot. Can be empty (""). -#' @param legendTitle A character value for the title of the legend. Can be empty (""). +#' This function can be easily integrated into a larger plotting workflow or +#' used independently to inspect climate data distributions. #' -#' @return A ggplot object of the plot +#' @param df An `sf` object containing the climate metric information. It must +#' have a geometry column. +#' @param colInterest A character string specifying the name of the column in `df` +#' that contains the climate metric data to be plotted. +#' @param colorMap A character string indicating the `viridis` color map to use +#' (e.g., "A", "B", "C", "D", "E"). See +#' \url{https://ggplot2.tidyverse.org/reference/scale_viridis.html} for all options. +#' Defaults to `"C"`. +#' @param plotTitle A character string for the subtitle of the plot. +#' Defaults to `" "` (a single space, effectively no subtitle). +#' @param legendTitle A character string for the title of the legend. +#' Defaults to `"Climate metric"`. +#' +#' @return A `ggplot` object representing the spatial plot of the climate metric. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr select +#' @importFrom ggplot2 aes coord_sf geom_sf ggplot labs scale_fill_viridis_c +#' @importFrom rlang sym +#' @importFrom sf st_as_sf st_bbox +#' #' @examples -#' splnr_plot_climData(df = dat_clim, colInterest = "metric") -splnr_plot_climData <- function(df, colInterest, colorMap = "C", - plotTitle = " ", legendTitle = "Climate metric") { +#' \dontrun{ +#' # Assuming 'dat_clim' is an existing sf object in your package +#' # with a column named "metric" or another relevant climate metric. +#' +#' # Example: Plot climate data using "metric" column +#' plot_climate_metric <- splnr_plot_climData( +#' df = dat_clim, +#' colInterest = "metric", +#' plotTitle = "Annual Climate Warming", +#' legendTitle = "Warming (°C/year)" +#' ) +#' print(plot_climate_metric) +#' +#' # Example with a different color map +#' plot_climate_alt_cmap <- splnr_plot_climData( +#' df = dat_clim, +#' colInterest = "metric", +#' colorMap = "D", # Using 'D' for a different viridis palette +#' plotTitle = "Climate Metric (Alternative Colors)" +#' ) +#' print(plot_climate_alt_cmap) +#' } +splnr_plot_climData <- function(df, + colInterest, + colorMap = "C", + plotTitle = " ", + legendTitle = "Climate metric") { + # Assertions to validate input parameters. assertthat::assert_that( inherits(df, "sf"), - "metric" %in% names(df), + msg = "'df' must be an 'sf' object." + ) + assertthat::assert_that( is.character(colInterest), + msg = "'colInterest' must be a character string." + ) + assertthat::assert_that( + colInterest %in% names(df), + msg = paste0("The column '", colInterest, "' does not exist in the input dataframe 'df'.") + ) + assertthat::assert_that( is.character(colorMap), + msg = "'colorMap' must be a character string for a 'viridis' palette option." + ) + assertthat::assert_that( is.character(plotTitle), - is.character(legendTitle) + msg = "'plotTitle' must be a character string." + ) + assertthat::assert_that( + is.character(legendTitle), + msg = "'legendTitle' must be a character string." ) + # Initialize the ggplot object. gg <- ggplot2::ggplot() + + # Add sf layer, filling by the specified climate metric column. ggplot2::geom_sf(data = df %>% sf::st_as_sf(), ggplot2::aes(fill = !!rlang::sym(colInterest)), colour = NA) + + # Apply a viridis continuous color scale for fill. ggplot2::scale_fill_viridis_c( - name = legendTitle, - option = colorMap # , - # guide = ggplot2::guide_colourbar( - # title.position = "bottom", - # title.hjust = 0.5, - # order = 1, - # barheight = grid::unit(0.03, "npc"), - # barwidth = grid::unit(0.25, "npc")) - # ) + name = legendTitle, # Set legend title. + option = colorMap # Apply specified color map. ) + + # Set coordinate limits based on the bounding box of the dataframe. ggplot2::coord_sf(xlim = sf::st_bbox(df)$xlim, ylim = sf::st_bbox(df)$ylim) + - ggplot2::labs(subtitle = plotTitle) + ggplot2::labs(subtitle = plotTitle) # Set plot subtitle. return(gg) } -#' Basic Kernel Density Plots for climate-smart spatial plans -#' @param soln The `prioirtizr` solution containing a "metric" column containing the used climate metric information +#' @title Basic Kernel Density Plots for Climate-Smart Spatial Plans #' -#' @importFrom rlang := +#' @description +#' `splnr_plot_climKernelDensity_Basic()` generates a basic kernel density plot +#' to visualize the distribution of a climate metric within selected and +#' unselected planning units of a `prioritizr` solution. #' -#' @return A ggplot object of the plot -#' @noRd +#' @details +#' This internal function is used by `splnr_plot_climKernelDensity()` when +#' `type = "Basic"`. It creates a ridge plot using `ggridges` to show the +#' density of a climate metric for planning units that were "Selected" versus +#' "Not Selected" in a conservation solution. The "Selected" distribution is +#' typically darker and more opaque, while "Not Selected" is lighter and +#' transparent. +#' +#' The x-axis labels are customized to indicate "more climate-resilient" and +#' "less climate-resilient" based on the minimum and maximum metric values. +#' +#' @param soln The `prioritizr` solution, expected as a data frame (can be an +#' `sf` object that will be treated as a data frame). It must contain a +#' `metric` column (numeric) with climate metric information and a `solution_1` +#' column (numeric, 0 or 1) indicating selected planning units. +#' +#' @return A `ggplot` object representing the kernel density plot. #' @keywords internal +#' @noRd +#' +#' @importFrom assertthat assert_that +#' @importFrom dplyr filter mutate +#' @importFrom ggplot2 aes element_blank element_line element_text ggplot labs scale_fill_manual scale_x_continuous scale_y_discrete theme theme_bw guide_legend +#' @importFrom rlang .data := #' splnr_plot_climKernelDensity_Basic <- function(soln) { + # Assertions to validate input parameters. assertthat::assert_that( inherits(soln, "data.frame"), + msg = "'soln' must be a data.frame (can be an sf object)." + ) + assertthat::assert_that( "metric" %in% names(soln), + msg = "'soln' must contain a 'metric' column." + ) + assertthat::assert_that( "solution_1" %in% names(soln), + msg = "'soln' must contain a 'solution_1' column." + ) + assertthat::assert_that( is.numeric(soln$metric), - is.numeric(soln$solution_1) + msg = "The 'metric' column in 'soln' must be numeric." + ) + assertthat::assert_that( + is.numeric(soln$solution_1) || is.logical(soln$solution_1), # Allow logical too, as it converts to numeric 0/1 + msg = "The 'solution_1' column in 'soln' must be numeric (0/1) or logical (TRUE/FALSE)." ) - + # Check if ggridges package is installed, if not, stop with an error. if (requireNamespace("ggridges", quietly = TRUE) == FALSE){ stop("To run splnr_plot_climKernelDensity you will need to install the package ggridges.") } - soln$approach <- "Ridge" # Need a dummy variable here. + # Add a dummy variable "approach" for the ridge plot's Y-axis. + soln$approach <- "Ridge" + # Initialize ggplot object. ggRidge <- ggplot2::ggplot() + + # Add density ridges for selected planning units. ggridges::stat_density_ridges( - data = soln %>% dplyr::filter(.data$solution_1 == 1) %>% dplyr::mutate(solution_1 = "Selected"), + data = soln %>% dplyr::filter(.data$solution_1 == 1) %>% dplyr::mutate(solution_1 = "Selected"), # Filter for selected and label. ggplot2::aes(x = .data$metric, y = .data$approach, fill = .data$solution_1), - # fill = "#3182bd", - color = "#194361", quantile_lines = TRUE, quantiles = 2, + color = "#194361", quantile_lines = TRUE, quantiles = 2, # Darker color and quantile lines. show.legend = TRUE ) + + # Add density ridges for not selected planning units. ggridges::stat_density_ridges( - data = soln %>% dplyr::filter(.data$solution_1 == 0) %>% dplyr::mutate(solution_1 = "Not Selected"), + data = soln %>% dplyr::filter(.data$solution_1 == 0) %>% dplyr::mutate(solution_1 = "Not Selected"), # Filter for not selected and label. ggplot2::aes(x = .data$metric, y = .data$approach, fill = .data$solution_1), - # fill = "#c6dbef", - color = "#3182bd", quantile_lines = TRUE, quantiles = 2, - alpha = 0.5, + color = "#3182bd", quantile_lines = TRUE, quantiles = 2, # Lighter color and quantile lines. + alpha = 0.5, # Make semi-transparent. show.legend = TRUE ) + + # Customize x-axis to show "more climate-resilient" and "less climate-resilient" at min/max. ggplot2::scale_x_continuous( name = "Climate resilience metric", - breaks = c(min(soln$metric), max(soln$metric)), + breaks = c(min(soln$metric, na.rm = TRUE), max(soln$metric, na.rm = TRUE)), # Use actual min/max. labels = c("more climate-resilient", "less climate-resilient") ) + + # Customize y-axis with no expansion. ggplot2::scale_y_discrete(expand = c(0, 0)) + + # Set plot labels. ggplot2::labs( x = "Climate resilience metric", y = "Proportion of planning units" ) + - ggplot2::theme_bw() + + ggplot2::theme_bw() + # Apply black and white theme. + # Customize theme elements. ggplot2::theme( axis.ticks = ggplot2::element_line(color = "black", linewidth = 1), text = ggplot2::element_text(size = 20), axis.line = ggplot2::element_line(colour = "black", linewidth = 1), - axis.text.y = ggplot2::element_blank(), + axis.text.y = ggplot2::element_blank(), # Hide y-axis text. axis.text.x = ggplot2::element_text(size = 20), axis.title = ggplot2::element_text(size = 20), - legend.title = ggplot2::element_text(color = "black", angle = 270, hjust = 0.5), + legend.title = ggplot2::element_text(color = "black", angle = 270, hjust = 0.5), # Rotate legend title. legend.position = "bottom", legend.text = ggplot2::element_text(size = 20) ) + + # Manually set fill colors for "Not Selected" and "Selected" in the legend. ggplot2::scale_fill_manual( - name = "", + name = "", # Empty legend title. values = c("Not Selected" = "#c6dbef", "Selected" = "#3182bd"), aesthetics = "fill", guide = ggplot2::guide_legend( - override.aes = list(linetype = 0), - nrow = 1 + override.aes = list(linetype = 0), # Remove linetype from legend. + nrow = 1 # Single row for legend. ) ) } -#' Fancy Kernel Density Plots for climate-smart spatial plans -#' @param solution_list A list of `prioirtizr` solutions (e.g. solution_list = list(s1, s2)) containing a "metric" column containing the used climate metric information -#' @param names A list of names of the solutions (names = c("Input 1", "Input 2")) -#' @param colorMap A character string indicating the color map to use (see https://ggplot2.tidyverse.org/reference/scale_viridis.html for all options) -#' @param legendTitle A character value for the title of the legend. Can be empty (""). -#' @param xAxisLab A characted value for the x Axis label depending on the climate metric input -#' -#' @return A ggplot object of the plot -#' @noRd -#' @keywords internal +#' @title Fancy Kernel Density Plots for Climate-Smart Spatial Plans #' -splnr_plot_climKernelDensity_Fancy <- function(solution_list, names, +#' @description +#' `splnr_plot_climKernelDensity_Fancy()` generates a more elaborate kernel +#' density plot suitable for comparing distributions of a climate metric across +#' multiple conservation solutions. +#' +#' @details +#' This internal function is used by `splnr_plot_climKernelDensity()` when +#' `type = "Normal"`. It accepts a list of `prioritizr` solutions and their +#' corresponding names, allowing for a comparative visualization of climate +#' metric distributions between different scenarios. +#' +#' The function pivots the data to a long format, enabling `ggridges` to plot +#' overlapping density ridges for each solution, with selected areas filled +#' by a gradient based on the metric value and unselected areas shown as dotted +#' outlines. This provides a detailed visual comparison of how climate metrics +#' vary across different spatial plans. +#' +#' @param solution_list A `list` of `prioritizr` solution objects. Each solution +#' (e.g., `s1`, `s2`) in the list must be an `sf` or `data.frame` object +#' containing a `metric` column (numeric) and a `solution_1` column (numeric, 0 or 1). +#' @param solution_names A character vector of prioritizr solution names corresponding to each solution in +#' `solution_list`. The length of this vector must match the length of `solution_list`. +#' @param climate_names A character vector of climate column names corresponding to each solution in +#' `solution_list`. The length of this vector must match the length of `solution_list`. +#' @param colorMap A character string indicating the `viridis` color map to use +#' for filling the selected areas (e.g., "A", "B", "C", "D", "E"). See +#' \url{https://ggplot2.tidyverse.org/reference/scale_viridis.html} for all options. +#' Defaults to `"C"`. +#' @param legendTitle A character string or `expression` for the title of the legend. +#' Defaults to `expression(" \u00B0C y"^"-1" * "")`, representing "°C year⁻¹". +#' @param xAxisLab A character string or `expression` for the x-axis label, +#' depending on the climate metric input. Defaults to +#' `expression("Climate warming ( \u00B0C y"^"-1" * ")")`. +#' +#' @return A `ggplot` object representing the fancy kernel density plot. +#' @keywords internal +#' @noRd +splnr_plot_climKernelDensity_Fancy <- function(solution_list, + solution_names = "solution_1", + climate_names = "metric", colorMap = "C", legendTitle = expression(" \u00B0C y"^"-1" * ""), xAxisLab = expression("Climate warming ( \u00B0C y"^"-1" * ")")) { + # Assertions to validate input parameters. assertthat::assert_that( is.list(solution_list), + msg = "'solution_list' must be a list of prioritizr solutions." + ) + assertthat::assert_that( length(solution_list) > 0, - is.character(names), - length(names) == length(solution_list), + msg = "'solution_list' must contain at least one solution." + ) + #TODO ADD assert for new _names variables + # assertthat::assert_that( + # is.character(names), + # msg = "'names' must be a character vector of solution names." + # ) + # assertthat::assert_that( + # length(names) == length(solution_list), + # msg = "The length of 'names' must match the length of 'solution_list'." + # ) + assertthat::assert_that( is.character(colorMap), - is.vector(legendTitle) || missing(legendTitle) || is.expression(legendTitle), - is.vector(xAxisLab) || missing(xAxisLab) || is.expression(xAxisLab) + msg = "'colorMap' must be a character string for a 'viridis' palette option." ) + assertthat::assert_that( + is.vector(legendTitle) || is.expression(legendTitle), # Allow vector (character) or expression. + msg = "'legendTitle' must be a character string or an expression." + ) + assertthat::assert_that( + is.vector(xAxisLab) || is.expression(xAxisLab), # Allow vector (character) or expression. + msg = "'xAxisLab' must be a character string or an expression." + ) + + #TODO Re enable for new _names variable + # # Check that each solution in the list has 'metric' and 'solution_1' columns + # for (i in seq_along(solution_list)) { + # assertthat::assert_that( + # "metric" %in% names(solution_list[[i]]), + # msg = paste0("Solution ", i, " in 'solution_list' is missing the 'metric' column.") + # ) + # assertthat::assert_that( + # "solution_1" %in% names(solution_list[[i]]), + # msg = paste0("Solution ", i, " in 'solution_list' is missing the 'solution_1' column.") + # ) + # assertthat::assert_that( + # is.numeric(solution_list[[i]]$metric), + # msg = paste0("The 'metric' column in solution ", i, " must be numeric.") + # ) + # assertthat::assert_that( + # is.numeric(solution_list[[i]]$solution_1) || is.logical(solution_list[[i]]$solution_1), + # msg = paste0("The 'solution_1' column in solution ", i, " must be numeric (0/1) or logical.") + # ) + # } + + + #TODO Write check for ggridges list_sol <- list() - group_name <- "approach" + group_name <- "approach" # Define a column name for grouping different solutions. - for (i in 1:length(names)) { + # Loop through each solution in the list to prepare data for plotting. + for (i in 1:length(solution_names)) { list_sol[[i]] <- solution_list[[i]] %>% - tibble::as_tibble() %>% - dplyr::select("solution_1", "metric") %>% - dplyr::rename(!!rlang::sym(names[i]) := "metric") %>% - tidyr::pivot_longer(!!rlang::sym(names[i]), names_to = group_name, values_to = "metric") + tibble::as_tibble() %>% # Convert to tibble to ensure consistent data frame behavior. + dplyr::select(tidyselect::all_of(c(solution_names[i], climate_names[i]))) %>% # Select only solution status and metric. + # dplyr::rename(!!rlang::sym(names[i]) := "metric") %>% # Rename 'metric' column to the solution's name. + # Pivot data longer to enable plotting multiple solutions on one plot. + tidyr::pivot_longer(cols = tidyselect::all_of(climate_names), names_to = group_name, values_to = "metric") } + # Combine all processed data frames into a single data frame. df <- do.call(rbind, list_sol) %>% + # Relevel the 'approach' factor to control the order of ridges in the plot. dplyr::mutate(approach = forcats::fct_relevel(.data$approach, rev)) + + # Initialize ggplot object. ggRidge <- ggplot2::ggplot() + + # Add density ridges with gradient fill for selected planning units. ggridges::geom_density_ridges_gradient( - data = df %>% dplyr::filter(.data$solution_1 == 1), + data = df %>% dplyr::filter(.data$solution_1 == 1), # Filter for selected units. ggplot2::aes( x = .data$metric, y = .data$approach, - fill = ggplot2::after_stat(.data$x), - ), scale = 1 + fill = ggplot2::after_stat(.data$x), # Fill based on the x-value (metric). + ), scale = 1 # Set ridge scale. ) + + # Apply a viridis color scale for the gradient fill. ggplot2::scale_fill_viridis_c(name = legendTitle, option = colorMap) + + # Add density ridges for not selected planning units with dotted lines and transparency. ggridges::geom_density_ridges( - data = df %>% dplyr::filter(.data$solution_1 == 0), + data = df %>% dplyr::filter(.data$solution_1 == 0), # Filter for not selected units. ggplot2::aes(x = .data$metric, y = .data$approach), - alpha = 0.25, linetype = "dotted", scale = 1 + alpha = 0.25, linetype = "dotted", scale = 1 # Set transparency, linetype, and scale. ) + - + # (Commented out in original: Optional vertical line for mean climate warming) # geom_vline(xintercept = climate$mean_climate_warming, # linetype = "dashed", color = "tan1", size = 0.5) + + # Set x-axis limits with no expansion. ggplot2::scale_x_continuous(expand = c(0, 0)) + + # Set y-axis limits with expansion. ggplot2::scale_y_discrete(expand = ggplot2::expansion(mult = c(0.01, 0))) + - ggplot2::labs(x = xAxisLab) + - ggplot2::theme_bw() + + ggplot2::labs(x = xAxisLab) + # Set x-axis label. + ggplot2::theme_bw() + # Apply black and white theme. + # Customize theme elements. ggplot2::theme( axis.ticks = ggplot2::element_line(color = "black", linewidth = 1), axis.line = ggplot2::element_line(colour = "black", linewidth = 1), axis.text = ggplot2::element_text(color = "black", size = 14), axis.title.x = ggplot2::element_text(size = 14), - axis.title.y = ggplot2::element_blank(), - axis.text.y = ggplot2::element_blank(), - # legend.key.height = unit(1, "inch"), + axis.title.y = ggplot2::element_blank(), # Hide y-axis title. + axis.text.y = ggplot2::element_blank(), # Hide y-axis text. legend.text = ggplot2::element_text(size = 15, color = "black"), legend.title = ggplot2::element_text(size = 15, color = "black") ) } -#' Kernel Density Plots for climate-smart spatial plans -#' @param type The plotting style of the kernel density plots. Either "Publication" which gives axis information etc., or "App" which condenses the information in the plot to simplify it for stakeholders. -#' @param soln For type "Publication": A list of `prioirtizr` solutions (e.g. solution_list = list(s1, s2)) containing a "metric" column containing the used climate metric information; For type "App": needs to be a prioritizr solution -#' @param names A list of names of the solutions (names = c("Input 1", "Input 2")) -#' @param colorMap A character string indicating the color map to use (see https://ggplot2.tidyverse.org/reference/scale_viridis.html for all options) -#' @param legendTitle A character value for the title of the legend. Can be empty (""). -#' @param xAxisLab A characted value for the x Axis label depending on the climate metric input -#' -#' @return A ggplot object of the plot +#' @title Kernel Density Plots for Climate-Smart Spatial Plans +#' +#' @description +#' `splnr_plot_climKernelDensity()` generates kernel density plots for +#' climate-smart spatial plans, offering two distinct plotting styles: +#' "Normal" (for publication-quality comparison of multiple solutions) and +#' "Basic" (for simplified visualization for stakeholders). +#' +#' @details +#' This wrapper function intelligently dispatches to either +#' `splnr_plot_climKernelDensity_Fancy()` (for `type = "Normal"`) or +#' `splnr_plot_climKernelDensity_Basic()` (for `type = "Basic"`) based on the +#' `type` parameter. +#' +#' The "Normal" (Fancy) style is suitable for detailed comparisons, +#' accommodating a list of solutions and custom axis labels, while the "Basic" +#' style is streamlined for clarity and quick interpretation, ideal for +#' stakeholder engagement. +#' +#' Both underlying functions require a `prioritizr` solution containing a +#' `metric` column with climate metric information and a `solution_1` column +#' indicating selected planning units. +#' +#' @param soln For `type = "Normal"`: A `list` of `prioritizr` solution objects +#' (e.g., `list(s1, s2)`). Each solution must contain a `metric` column and +#' a `solution_1` column. +#' For `type = "Basic"`: A single `prioritizr` solution `sf` object. +#' @param names A character vector of names corresponding to each solution in +#' `soln` when `type = "Normal"`. Not used for `type = "Basic"`. +#' Defaults to `NA`. +#' @param type A character string specifying the plotting style. Must be either +#' `"Normal"` or `"Basic"`. Defaults to `"Normal"`. +#' @param colorMap A character string indicating the `viridis` color map to use +#' (e.g., "A", "B", "C", "D", "E"). See +#' \url{https://ggplot2.tidyverse.org/reference/scale_viridis.html} for all options. +#' Defaults to `"C"`. +#' @param legendTitle A character string or `expression` for the title of the legend. +#' Defaults to `expression(" \u00B0C y"^"-1" * "")`, representing "°C year⁻¹". +#' @param xAxisLab A character string or `expression` for the x-axis label, +#' depending on the climate metric input. Defaults to +#' `expression("Climate warming ( \u00B0C y"^"-1" * ")")`. +#' +#' @return A `ggplot` object representing the kernel density plot. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom prioritizr problem add_min_set_objective add_relative_targets add_binary_decisions add_default_solver solve.ConservationProblem +#' @importFrom dplyr mutate select +#' @importFrom sf st_drop_geometry st_join +#' @importFrom tidyselect starts_with +#' #' @examples +#' \dontrun{ +#' # Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects +#' # in your package. +#' +#' # Prepare data for a climate-priority area approach (CPA) #' target <- dat_species_bin %>% #' sf::st_drop_geometry() %>% #' colnames() %>% @@ -222,15 +464,18 @@ splnr_plot_climKernelDensity_Fancy <- function(solution_list, names, #' refugiaTarget = 1 #' ) #' +#' # Join climate metric to features for the problem #' out_sf <- CPA$Features %>% -#' dplyr::mutate(Cost_None = rep(1, 780)) %>% +#' dplyr::mutate(Cost_None = rep(1, dim(.)[[1]])) %>% # Ensure enough costs for PUs #' sf::st_join(dat_clim, join = sf::st_equals) #' +#' # Define features for the prioritizr problem #' usedFeatures <- out_sf %>% #' sf::st_drop_geometry() %>% #' dplyr::select(-tidyselect::starts_with("Cost_"), -"metric") %>% #' names() #' +#' # Create and solve a prioritizr problem #' p1 <- prioritizr::problem(out_sf, usedFeatures, "Cost_None") %>% #' prioritizr::add_min_set_objective() %>% #' prioritizr::add_relative_targets(CPA$Targets$target) %>% @@ -238,37 +483,103 @@ splnr_plot_climKernelDensity_Fancy <- function(solution_list, names, #' prioritizr::add_default_solver(verbose = FALSE) #' #' dat_solnClim <- prioritizr::solve.ConservationProblem(p1) -#' splnr_plot_climKernelDensity(dat_solnClim, type = "Basic") -#' splnr_plot_climKernelDensity(soln = list(dat_solnClim), names = c("Input 1"), type = "Normal") +#' +#' # Example 1: Basic kernel density plot +#' plot_basic_kde <- splnr_plot_climKernelDensity(soln = dat_solnClim, type = "Basic") +#' print(plot_basic_kde) +#' +#' # Example 2: Normal (Fancy) kernel density plot for a single solution +#' plot_normal_kde_single <- splnr_plot_climKernelDensity( +#' soln = list(dat_solnClim), +#' names = c("Solution 1"), +#' type = "Normal" +#' ) +#' print(plot_normal_kde_single) +#' +#' # Example 3: Normal (Fancy) plot comparing two solutions (create a dummy second solution) +#' # For demonstration, let's create another dummy solution +#' dat_solnClim_2 <- dat_solnClim %>% +#' dplyr::mutate(solution_1 = sample(c(0, 1), n(), replace = TRUE)) # Randomize selection +#' +#' plot_normal_kde_multi <- splnr_plot_climKernelDensity( +#' soln = list(dat_solnClim, dat_solnClim_2), +#' names = c("Solution A", "Solution B"), +#' type = "Normal", +#' colorMap = "plasma", +#' legendTitle = "Climate Value", +#' xAxisLab = "Climate Metric (units)" +#' ) +#' print(plot_normal_kde_multi) +#' } splnr_plot_climKernelDensity <- function(soln, - names = NA, + solution_names = "solution_1", + climate_names = "metric", type = "Normal", colorMap = "C", legendTitle = expression(" \u00B0C y"^"-1" * ""), xAxisLab = expression("Climate warming ( \u00B0C y"^"-1" * ")")) { + # Assertions to validate input parameters. assertthat::assert_that( is.character(type), - is.character(names) || is.na(names), + msg = "'type' must be a character string ('Normal' or 'Basic')." + ) + assertthat::assert_that( + type %in% c("Normal", "Basic"), + msg = "'type' must be either 'Normal' or 'Basic'." + ) + assertthat::assert_that( + is.character(solution_names), + msg = "'solution_names' must be a character vector." + ) + assertthat::assert_that( + is.character(climate_names), + msg = "'climate_names' must be a character vector." + ) + assertthat::assert_that( is.character(colorMap), - is.vector(legendTitle) || missing(legendTitle) || is.expression(legendTitle), - is.vector(xAxisLab) || missing(xAxisLab) || is.expression(xAxisLab) + msg = "'colorMap' must be a character string for a 'viridis' palette option." + ) + assertthat::assert_that( + is.vector(legendTitle) || is.expression(legendTitle), + msg = "'legendTitle' must be a character string or an expression." + ) + assertthat::assert_that( + is.vector(xAxisLab) || is.expression(xAxisLab), + msg = "'xAxisLab' must be a character string or an expression." ) + # Conditional logic to call either Basic or Fancy plotting function. if (type == "Normal") { + # If type is "Normal", expect a list of solutions. if (inherits(soln, "list") == FALSE) { - cat("Please provide a list of solutions when using this plot type.") + stop("For 'type = \"Normal\"', 'soln' must be a list of prioritizr solutions.") } else if (inherits(soln, "list")) { + # Ensure 'names' matches the number of solutions if 'type' is "Normal" and 'names' is provided. + if (!is.na(solution_names[1]) && length(solution_names) != length(soln)) { + stop("When 'type = \"Normal\"' the length of 'solution_names' must match the number of solutions in 'soln'.") + } + # Call the fancy kernel density plotting function. ggclimDens <- splnr_plot_climKernelDensity_Fancy( - solution_list = soln, names = names, colorMap = colorMap, + solution_list = soln, + solution_names = solution_names, + climate_names = climate_names, + colorMap = colorMap, legendTitle = legendTitle, xAxisLab = xAxisLab ) } } else if (type == "Basic") { + # If type is "Basic", expect a single sf object. if (inherits(soln, "sf") == FALSE) { - cat("Please provide an sf object.") + stop("For 'type = \"Basic\"', 'soln' must be a single sf object.") } else if (inherits(soln, "sf")) { + # Call the basic kernel density plotting function. ggclimDens <- splnr_plot_climKernelDensity_Basic(soln = soln) } + } else { + # This case should ideally be caught by initial assertthat, but kept as a fallback. + stop("Invalid 'type' specified. Must be 'Normal' or 'Basic'.") } + + return(ggclimDens) } diff --git a/R/splnr_targets.R b/R/splnr_targets.R index b6ac769..76851fa 100644 --- a/R/splnr_targets.R +++ b/R/splnr_targets.R @@ -1,131 +1,368 @@ -#' Assign targets by Inverse Area +#' @title Assign Targets by Inverse Area #' -#' This function takes a min (`target_min`) and max (`target_max`) target range and calculates an inverse area target for each feature based on areal coverage. +#' @description +#' This function calculates inverse area targets for each conservation feature +#' within an `sf` dataframe, based on their areal coverage. The target is set +#' to be inversely proportional to the feature's area, ranging between a +#' specified minimum (`target_min`) and maximum (`target_max`). #' -#' @param df An `sf` dataframe with features to calculate -#' @param target_min The minimum target for inverse area -#' @param target_max The maximum target for inverse area +#' @details +#' The inverse area target approach aims to assign higher conservation targets +#' to features that have a smaller overall distribution or areal coverage within +#' the study region. This can be particularly useful for prioritizing rare or +#' range-restricted features. #' -#' @return An `sf` dataframe with Inverse Area Targets added in `Target` +#' The calculation proceeds as follows: +#' 1. The area of a single planning unit is determined. +#' 2. The total area of the study region is estimated by multiplying the number +#' of planning units by the individual planning unit area. +#' 3. For each feature (species), its total area across all planning units is +#' calculated. +#' 4. The target for each feature is then scaled between `target_min` and +#' `target_max` such that features with smaller areas receive targets closer +#' to `target_max`, and features with larger areas receive targets closer +#' to `target_min`. +#' +#' The input `df` is expected to be an `sf` object where columns (excluding +#' geometry) represent different features (e.g., species presence/absence) and +#' rows represent planning units. +#' +#' @param df An `sf` dataframe containing the features (e.g., species distribution +#' data) for which to calculate inverse area targets. Each column (excluding +#' geometry) should represent a feature, and each row a planning unit. +#' @param target_min A numeric value between 0 and 1 (inclusive) specifying the +#' minimum target percentage. This will be the target for the most widespread feature. +#' @param target_max A numeric value between 0 and 1 (inclusive) specifying the +#' maximum target percentage. This will be the target for the rarest feature. +#' +#' @return A `tibble` (data frame) with two columns: `Species` (or feature name) +#' and `target` (the calculated inverse area target for each feature). #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr across everything mutate summarise #' @importFrom rlang .data +#' @importFrom sf st_area st_drop_geometry +#' @importFrom stringr str_replace_all +#' @importFrom tidyr pivot_longer replace_na +#' #' @examples -#' targets <- dat_species_prob %>% +#' \dontrun{ +#' # Assuming 'dat_species_prob' is an existing sf object in your package, +#' # representing species distribution in planning units. +#' +#' # Calculate inverse area targets with a range from 30% to 80%. +#' targets_inverse_area <- dat_species_prob %>% #' splnr_targets_byInverseArea(target_min = 0.3, target_max = 0.8) +#' print(targets_inverse_area) +#' +#' # Example with a different target range (e.g., 20% to 70%) +#' targets_custom_range <- dat_species_prob %>% +#' splnr_targets_byInverseArea(target_min = 0.2, target_max = 0.7) +#' print(targets_custom_range) +#' } splnr_targets_byInverseArea <- function(df, target_min, target_max) { + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(df, "sf"), # Ensure df is an sf object. + msg = "'df' must be an 'sf' object." + ) assertthat::assert_that( - inherits(df, c("sf", "data.frame")), - is.numeric(target_min) && target_min >= 0 && target_min <= 1, - is.numeric(target_max) && target_max >= 0 && target_max <= 1, - target_min <= target_max + is.numeric(target_min) && length(target_min) == 1 && target_min >= 0 && target_min <= 1, + msg = "'target_min' must be a single numeric value between 0 and 1." + ) + assertthat::assert_that( + is.numeric(target_max) && length(target_max) == 1 && target_max >= 0 && target_max <= 1, + msg = "'target_max' must be a single numeric value between 0 and 1." + ) + assertthat::assert_that( + target_min <= target_max, + msg = "'target_min' must be less than or equal to 'target_max'." + ) + assertthat::assert_that( + "geometry" %in% names(df), + msg = "'df' must contain a 'geometry' column." ) - PU_area_km2 <- as.numeric(sf::st_area(df[1, 1]) / 1e+06) # Area of each planning unit + # Calculate the area of a single planning unit in km². + PU_area_km2 <- as.numeric(sf::st_area(df[1, ]) / 1e+06) - total_PU_area <- nrow(df) * PU_area_km2 # Total area of the study region + # Calculate the total approximate area of the study region. + total_PU_area <- nrow(df) * PU_area_km2 + # Process the dataframe to calculate inverse area targets. dat <- df %>% + # Drop the geometry column to perform numerical operations on feature data. sf::st_drop_geometry() %>% + # Replace any NA values with 0 across all columns, assuming NA means absence. dplyr::mutate(dplyr::across(dplyr::everything(), ~ tidyr::replace_na(.x, 0))) %>% - dplyr::summarise(dplyr::across(dplyr::everything(), ~ sum(., is.na(.), 0))) %>% - tidyr::pivot_longer(dplyr::everything(), names_to = "Species", values_to = "Area_km2") %>% + # Summarise each column by summing its values to get total area coverage for each feature. + dplyr::summarise(dplyr::across(dplyr::everything(), ~ sum(.x, na.rm = TRUE))) %>% + # Pivot the data longer, converting feature columns into rows. + tidyr::pivot_longer(dplyr::everything(), names_to = "Species", values_to = "Area_units") %>% # Renamed to Area_units to reflect it's sum of presence + # Mutate to calculate actual area in km² and the inverse area target. dplyr::mutate( + # Replace underscores in species names with spaces for readability. Species = stringr::str_replace_all(.data$Species, pattern = "_", replacement = " "), - Area_km2 = .data$Area_km2 * PU_area_km2, + # Convert 'Area_units' (sum of presences) to actual area in km². + Area_km2 = .data$Area_units * PU_area_km2, + # Calculate the inverse area target: + # Starts at target_max, then subtracts a proportion of the range (target_max - target_min) + # based on the feature's relative area. Larger relative area leads to a smaller target. target = target_max - ((.data$Area_km2 / total_PU_area) * (target_max - target_min)) - ) + ) %>% + dplyr::select("Species", "target") # Select only the Species and target columns for the final output. + return(dat) } - - -#' Assign targets to all features by category +#' @title Assign Targets by Category +#' +#' @description +#' The `splnr_targets_byCategory()` function assigns conservation targets to +#' features (e.g., species) based on their assigned categories. This allows for +#' differentiated conservation goals for different groups of features. #' -#' `splnr_targets_byCategory()` allows to assign targets for conservation planning based on species categories. +#' @details +#' This function is useful in conservation planning when different types of +#' features (e.g., endangered species, common species, ecosystem types) require +#' distinct conservation targets. It performs a left join with a provided +#' named vector (`catTarg`) where names correspond to categories in your data +#' and values are the desired targets. #' -#' @param dat A sf object with the features and categories -#' @param catTarg A named character vector with categories and target -#' @param catName An optional argument for the name of the category column in dat +#' The `dat` input should be an `sf` object (or data frame) that contains a +#' column (`catName`) identifying the category for each feature. #' -#' @return An sf object with targets added +#' @param dat An `sf` object (or data frame) containing the features and their +#' associated categories. Each row should represent a feature (e.g., a species) +#' with its attributes, including the category. +#' @param catTarg A named numeric vector where names are the categories +#' (e.g., `"Group1"`, `"Endangered"`) and values are the corresponding +#' conservation targets (e.g., `0.5`, `0.8`). +#' @param catName A character string specifying the name of the column in `dat` +#' that contains the category information. Defaults to `"Category"`. +#' +#' @return An `sf` object (or data frame) identical to the input `dat`, but with an +#' additional column named `target` containing the assigned conservation target +#' for each feature. Features whose categories are not found in `catTarg` will +#' have `NA` in the `target` column unless they already have a 'target' column. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr left_join rename +#' @importFrom rlang := +#' @importFrom tibble enframe +#' #' @examples -#' dat <- splnr_targets_byCategory( -#' dat = dat_category, +#' \dontrun{ +#' # Assuming 'dat_category' is an existing sf object in your package +#' # with a column named "category" and other feature data. +#' +#' # Example: Assign targets based on predefined categories +#' targets_by_group <- splnr_targets_byCategory( +#' dat = dat_category, # Assuming dat_category has a 'category' column #' catTarg = c("Group1" = 0.5, "Group2" = 0.2), #' catName = "category" #' ) +#' print(targets_by_group) +#' +#' # Example: Assign targets with a different category column name +#' dat_alt_cat <- data.frame(Feature = letters[1:5], Type = c("A", "B", "A", "C", "B")) +#' targets_by_type <- splnr_targets_byCategory( +#' dat = dat_alt_cat, +#' catTarg = c("A" = 0.7, "B" = 0.4), +#' catName = "Type" +#' ) +#' print(targets_by_type) +#' } splnr_targets_byCategory <- function(dat, catTarg, catName = "Category") { + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(dat, "data.frame"), # Ensure dat is a data.frame (or sf object). + msg = "'dat' must be a data.frame or sf object." + ) + assertthat::assert_that( + is.character(catName) && length(catName) == 1, + msg = "'catName' must be a single character string." + ) assertthat::assert_that( - inherits(dat, c("sf", "data.frame")), - is.character(catName), catName %in% names(dat), - is.vector(catTarg), + msg = paste0("The specified 'catName' (\"", catName, "\") does not exist in the input dataframe 'dat'.") + ) + assertthat::assert_that( + is.numeric(catTarg), # Ensure catTarg is a numeric vector. + msg = "'catTarg' must be a numeric vector." + ) + assertthat::assert_that( length(catTarg) > 0, - all(names(catTarg) %in% unique(dat[[catName]])) + msg = "'catTarg' must not be empty." + ) + assertthat::assert_that( + !is.null(names(catTarg)), + msg = "'catTarg' must be a named vector (e.g., c('Category1' = 0.5))." + ) + assertthat::assert_that( + all(names(catTarg) %in% unique(dat[[catName]])), + msg = paste0("Not all names in 'catTarg' match unique values in the '", catName, "' column of 'dat'.") ) + # Join the input dataframe with the category targets. dat <- dat %>% + # Convert the named vector `catTarg` into a two-column tibble (name, value). dplyr::left_join( tibble::enframe(catTarg), + # Join by the category column in `dat` and the 'name' column from the enframe'd catTarg. by = dplyr::join_by(!!catName == "name") ) %>% + # Rename the 'value' column (from enframe) to 'target'. dplyr::rename(target = "value") return(dat) } - -#' Assign targets bu IUCN Red List categories +#' @title Assign Targets by IUCN Red List Categories +#' +#' @description +#' The `splnr_targets_byIUCN()` function assigns conservation targets for species +#' based on their IUCN Red List categories. This allows for prioritizing species +#' at higher risk of extinction with more stringent conservation goals. #' -#' `splnr_targets_byIUCN()` allows to assign targets for species used in conservation planning based on IUCN categories. Species can be extracted based on IUCN categories with the `spatoalplnr`function `splnr_get_IUCNRedList()`. -#' Accessing the IUCN database requires a login token from `rl_use_iucn()` that needs to be added to the environment using `Sys.setenv(IUCN_REDLIST_KEY = "[Your Token]")`. You can start by running `rredlist::rl_use_iucn()`. +#' @details +#' This function is crucial for integrating species' extinction risk into +#' conservation planning. It allows you to specify targets either as a single +#' numeric value (applied to all 'threatened' IUCN categories) or as a named +#' numeric vector for specific categories. #' -#' @param dat A dataframe or sf object with IUCN categories -#' @param IUCN_target Either a numeric or named numeric of targets to apply to IUCN categories -#' @param IUCN_col Optional string to indicate the name of the column with the IUCN categories +#' Species can be extracted based on IUCN categories using the `spatialplanr` +#' function `splnr_get_IUCNRedList()`. #' -#' @return dataframe or sf object +#' \strong{Important:} To access the IUCN database (e.g., via `splnr_get_IUCNRedList()`), +#' you need an API login token. This token, obtained from `rredlist::rl_use_iucn()`, +#' must be set as an environment variable named `IUCN_REDLIST_KEY` +#' (e.g., `Sys.setenv(IUCN_REDLIST_KEY = "[Your Token]")`). +#' +#' The function checks if a 'target' column already exists in `dat`. If not, +#' it creates one. If it exists, new targets are coalesced with existing ones, +#' allowing for sequential application or refinement of targets. +#' +#' The "threatened" IUCN categories considered for target assignment (when a +#' single `IUCN_target` is provided) are: "EX" (Extinct), "EW" (Extinct in the Wild), +#' "CR" (Critically Endangered), "EN" (Endangered), and "VU" (Vulnerable). +#' +#' @param dat A dataframe or `sf` object containing species information, +#' including a column with IUCN categories. +#' @param IUCN_target Either: +#' \itemize{ +#' \item A single numeric value (e.g., `0.3`) to apply this target to all +#' threatened IUCN categories ("EX", "EW", "CR", "EN", "VU"). +#' \item A named numeric vector (e.g., `c("EX" = 0.8, "CR" = 0.6)`) to +#' apply specific targets to particular IUCN categories. +#' } +#' @param IUCN_col A character string specifying the name of the column in `dat` +#' that contains the IUCN category information. Defaults to `"IUCN_Category"`. +#' +#' @return A dataframe or `sf` object identical to the input `dat`, but with an +#' updated or newly added `target` column reflecting the assigned conservation goals. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr case_when coalesce left_join mutate select +#' @importFrom rlang .data sym +#' @importFrom tibble as_tibble enframe +#' #' @examples -#' dat <- data.frame(IUCN_Category = c("EW", "EX", NA), target = c(0.3, 0.3, 0.3)) -#' IUCN_target <- c("EX" = 0.8, "EW" = 0.6) -#' dat <- splnr_targets_byIUCN(dat, IUCN_target) +#' \dontrun{ +#' # Example 1: Assigning specific targets to categories +#' # Create a dummy dataframe resembling output from splnr_get_IUCNRedList +#' df_species_iucn <- data.frame( +#' Species = c("Diomedea exulans", "Hippocampus kuda", +#' "Squatina squatina", "Common Dolphin"), +#' IUCN_Category = c("VU", "EN", "CR", "LC") +#' ) +#' +#' iucn_specific_targets <- c("EX" = 0.9, "EW" = 0.8, "CR" = 0.75, "EN" = 0.6, "VU" = 0.5) +#' +#' df_with_iucn_targets <- splnr_targets_byIUCN( +#' dat = df_species_iucn, +#' IUCN_target = iucn_specific_targets, +#' IUCN_col = "IUCN_Category" +#' ) +#' print(df_with_iucn_targets) +#' +#' # Example 2: Assigning a single target to all threatened categories +#' df_single_target <- splnr_targets_byIUCN( +#' dat = df_species_iucn, +#' IUCN_target = 0.4, # Apply 40% target to all threatened species +#' IUCN_col = "IUCN_Category" +#' ) +#' print(df_single_target) +#' +#' # Example 3: When 'dat' already has a 'target' column +#' df_pre_targets <- data.frame( +#' Species = c("A", "B", "C"), +#' IUCN_Category = c("CR", "LC", "EN"), +#' target = c(0.1, 0.2, 0.1) # Existing targets +#' ) +#' iucn_update_targets <- c("CR" = 0.7) # Only update CR +#' df_updated_targets <- splnr_targets_byIUCN(df_pre_targets, iucn_update_targets) +#' print(df_updated_targets) +#' } splnr_targets_byIUCN <- function(dat, IUCN_target, IUCN_col = "IUCN_Category") { + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(dat, "data.frame"), # Ensure dat is a data.frame or sf object. + msg = "'dat' must be a data.frame or sf object." + ) + assertthat::assert_that( + is.character(IUCN_col) && length(IUCN_col) == 1, + msg = "'IUCN_col' must be a single character string." + ) assertthat::assert_that( - inherits(dat, c("sf", "data.frame")), - is.na(IUCN_col) || is.character(IUCN_col), IUCN_col %in% names(dat), - (is.numeric(IUCN_target) && length(IUCN_target) == 1) || is.vector(IUCN_target) + msg = paste0("The specified 'IUCN_col' (\"", IUCN_col, "\") does not exist in the input dataframe 'dat'.") + ) + assertthat::assert_that( + (is.numeric(IUCN_target) && length(IUCN_target) == 1) || (is.numeric(IUCN_target) && !is.null(names(IUCN_target))), + msg = "'IUCN_target' must be either a single numeric value or a named numeric vector." ) + if (is.numeric(IUCN_target)) { + assertthat::assert_that( + all(IUCN_target >= 0 & IUCN_target <= 1), + msg = "All values in 'IUCN_target' must be between 0 and 1." + ) + } - if ("target" %in% colnames(dat) == FALSE) { + # Ensure a 'target' column exists in 'dat'. If not, initialize with NA. + if (!("target" %in% colnames(dat))) { dat$target <- NA } - if (is.vector(IUCN_target, mode = "numeric") & !is.null(names(IUCN_target))) { - # If the target is a named vector, apply the relevant targets + # Apply targets based on whether IUCN_target is a named vector or a single numeric. + if (is.numeric(IUCN_target) && !is.null(names(IUCN_target))) { + # If IUCN_target is a named vector, join and coalesce targets. dat <- dat %>% - dplyr::left_join(data.frame(IUCN_target, col1 = names(IUCN_target)), by = dplyr::join_by(!!rlang::sym(IUCN_col) == "col1")) %>% - dplyr::mutate(target = dplyr::coalesce(IUCN_target, .data$target)) %>% - dplyr::select(-IUCN_target) - - } else if (is.numeric(IUCN_target) & length(IUCN_target) == 1) { - # If the target is a single numeric, apply to all IUCN categories. + # Convert the named IUCN_target vector to a data frame for joining. + dplyr::left_join(data.frame(IUCN_target_value = IUCN_target, + IUCN_Category = names(IUCN_target)), + by = dplyr::join_by(!!rlang::sym(IUCN_col) == "IUCN_Category")) %>% + # Use coalesce to update 'target' only where new IUCN_target_value is not NA. + dplyr::mutate(target = dplyr::coalesce(.data$IUCN_target_value, .data$target)) %>% + # Remove the temporary IUCN_target_value column. + dplyr::select(-.data$IUCN_target_value) + } else if (is.numeric(IUCN_target) && length(IUCN_target) == 1) { + # If IUCN_target is a single numeric, apply to specific threatened IUCN categories. dat <- dat %>% dplyr::mutate(target = dplyr::case_when( + # Apply the single target if the IUCN_col matches any of the threatened categories. !!rlang::sym(IUCN_col) %in% c("EX", "EW", "CR", "EN", "VU") ~ IUCN_target, - TRUE ~ dat$target + TRUE ~ .data$target # Otherwise, keep the existing target value. )) } return(dat) diff --git a/R/utils-climate.R b/R/utils-climate.R index dddb046..4710608 100644 --- a/R/utils-climate.R +++ b/R/utils-climate.R @@ -1,248 +1,529 @@ -##### Climate Priority Area Approach #### - -#' Function to split the feature data into climate-smart (CS) and non-climate-smart (NCS) areas depending on the percentile chosen by the user. +#' @title Preprocess Feature Data for Climate Priority Area Approach #' -#' @param features feature sf object -#' @param metric climate metric `sf` object with 'metric' as the column name of the metric values per planning unit. -#' @param percentile cut-off threshold for determining whether an area is a climate priority area or not (e.g., lower 35th percentile of warming or upper 65th percentile of acidification). Note that the percentile here is the lower limit of the threshold. -#' @param direction If direction = 1, metric values are from low (least climate-smart) to high (most climate-smart). If direction = -1, metric values are from high (least climate-smart) to low (most climate-smart). +#' @description +#' This internal function prepares feature data for the Climate Priority Area (CPA) +#' approach. It divides each feature's distribution into "climate-smart" (CS) and +#' "non-climate-smart" (NCS) areas based on a user-defined percentile cutoff for a +#' given climate metric. #' -#' @return A new sf dataframe that has cutoffs applied. -#' @noRd +#' @details +#' The CPA approach aims to prioritize areas that are both important for +#' biodiversity features and are considered resilient to climate change. This +#' preprocessing step identifies, for each individual feature, which of its +#' occupied planning units fall within the most climate-smart `percentile` of +#' the climate metric. +#' +#' For each feature, the function performs the following steps: +#' 1. Joins the feature data with the climate metric data. +#' 2. Filters to include only planning units where the feature is present. +#' 3. Calculates the specified `percentile` cutoff for the climate metric within +#' this filtered set of planning units. +#' 4. Creates two new binary columns for each feature: +#' - `_CS` (Climate-Smart): Indicates planning units where the feature is +#' present AND the climate metric meets the climate-smart criteria (e.g., +#' top 5% for direction 1, bottom 5% for direction -1). +#' - `_NCS` (Non-Climate-Smart): Indicates planning units where the feature +#' is present BUT the climate metric does NOT meet the climate-smart criteria. +#' +#' The `direction` parameter is crucial: +#' - `direction = 1`: Higher values of the `metric` indicate more climate-smart +#' areas (e.g., lower warming rates). The function identifies areas with metric +#' values greater than or equal to the `(100 - percentile)`th quantile. +#' - `direction = -1`: Lower values of the `metric` indicate more climate-smart +#' areas (e.g., less acidification). The function identifies areas with metric +#' values less than or equal to the `percentile`th quantile. +#' +#' @param features An `sf` object representing conservation features. Each column +#' (excluding geometry) should typically be a binary representation of a feature's +#' presence (1) or absence (0) in each planning unit. +#' @param metric An `sf` object containing climate metric information. It must +#' have a column named 'metric' with the climate metric values for each planning unit. +#' @param percentile A numeric value (0-100) representing the cutoff threshold for +#' determining climate-smart areas. For example, `percentile = 5` means the +#' most climate-smart 5% of areas (based on `direction`) are considered. +#' This value represents the lower limit of the threshold (e.g., lower 5th +#' percentile of warming or upper 95th percentile of acidification). +#' @param direction An integer specifying the direction of climate-smartness: +#' \itemize{ +#' \item `1`: Higher metric values mean more climate-smart. +#' \item `-1`: Lower metric values mean more climate-smart. +#' } +#' +#' @return An `sf` dataframe with new columns for each original feature, split +#' into `_CS` (climate-smart) and `_NCS` (non-climate-smart) areas, indicating +#' the binary presence (1) or absence (0) of that feature within those climate +#' categories. This dataframe retains the original geometry. #' @keywords internal +#' @noRd #' -#' @importFrom rlang .data +#' @importFrom assertthat assert_that +#' @importFrom dplyr across bind_cols filter if_else mutate select +#' @importFrom rlang .data sym +#' @importFrom sf st_drop_geometry st_join st_as_sf +#' @importFrom stats quantile +#' @importFrom tidyselect matches +#' @importFrom tidyr replace_na #' #' @examples +#' \dontrun{ +#' # Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects +#' # in your package. 'dat_species_bin' has binary species data, +#' # and 'dat_clim' has a 'metric' column. #' -#' out_sf <- splnr_climate_priorityArea_preprocess( +#' # Example: Identify climate-smart areas for species where +#' # higher metric values mean more climate-smart (e.g., lower warming). +#' out_sf_cs_areas <- splnr_climate_priorityArea_preprocess( #' features = dat_species_bin, #' percentile = 5, #' metric = dat_clim, #' direction = 1 #' ) +#' print(out_sf_cs_areas) +#' +#' # Example: Identify climate-smart areas where +#' # lower metric values mean more climate-smart (e.g., less acidification). +#' out_sf_ncs_areas <- splnr_climate_priorityArea_preprocess( +#' features = dat_species_bin, +#' percentile = 10, +#' metric = dat_clim, +#' direction = -1 +#' ) +#' print(out_sf_ncs_areas) +#' } splnr_climate_priorityArea_preprocess <- function(features, percentile, metric, direction) { + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(features, "sf"), + msg = "'features' must be an 'sf' object." + ) + assertthat::assert_that( + inherits(metric, "sf"), + msg = "'metric' must be an 'sf' object." + ) + assertthat::assert_that( + "metric" %in% names(metric), + msg = "'metric' sf object must contain a column named 'metric'." + ) + assertthat::assert_that( + is.numeric(percentile) && length(percentile) == 1 && percentile >= 0 && percentile <= 100, + msg = "'percentile' must be a single numeric value between 0 and 100." + ) + assertthat::assert_that( + direction %in% c(1, -1), + msg = "'direction' must be either 1 (higher metric = more climate-smart) or -1 (lower metric = more climate-smart)." + ) + assertthat::assert_that( + sf::st_crs(features) == sf::st_crs(metric), + msg = "CRS of 'features' and 'metric' must be the same." + ) + + # Extract column names of the features (excluding geometry). spp <- features %>% sf::st_drop_geometry() %>% names() - imptList <- list() # empty list to fill in with the important features + # Initialize an empty list to store processed data for each feature. + imptList <- list() for (i in 1:length(spp)) { + # Select one feature at a time from the 'features' data and join with the 'metric' data. df <- features %>% - dplyr::select(!!rlang::sym(spp[i])) %>% # Select 1 species at a time + dplyr::select(!!rlang::sym(spp[i])) %>% sf::st_join(metric, join = sf::st_equals) - if (any((apply(df, 2, is.na))[, 2])) { - print("There are some NAs in the metric data. Please check.") + # Check for NAs in the 'metric' column after joining and print a warning if found. + if (any(is.na(df$metric))) { + message(paste0("Warning: There are NAs in the metric data for feature '", spp[i], "'. These will be removed from quantile calculation.")) } - # Get the most climate-smart areas + # Filter to select only rows where the current biodiversity feature is present (value = 1). filteredDF <- df %>% - dplyr::filter(!!rlang::sym(spp[i]) == 1) # Select rows that have biodiversity values (= 1) + dplyr::filter(!!rlang::sym(spp[i]) == 1) + + # Handle cases where filteredDF might be empty (feature not present in any unit or only in NAs) + if (nrow(filteredDF) == 0) { + warning(paste0("Feature '", spp[i], "' is not present in any planning unit with valid metric data. Skipping climate-smart area calculation for this feature.")) + # Create an empty sf object with expected columns for binding later + temp_df <- df %>% + dplyr::mutate(V1 = 0, V2 = 0) %>% # Add V1 and V2 columns with 0 values + dplyr::mutate(!!rlang::sym(paste0(spp[i], "_CS")) := 0) %>% + dplyr::select(!!rlang::sym(paste0(spp[i], "_CS"))) + if (i > 1 && "geometry" %in% names(temp_df)) { # Remove geometry if not the first iteration + temp_df <- temp_df %>% sf::st_drop_geometry() + } + imptList[[i]] <- temp_df + next # Skip to the next iteration of the loop + } + + # Determine the percentile cutoff based on the specified direction. if (direction == 1) { - prct <- (100 - percentile) / 100 # for 100 as the most climate-smart - qntl <- stats::quantile(filteredDF$metric, prct)[[1]] # Get the percentile + # If higher values are more climate-smart, calculate the (100 - percentile)th quantile. + prct <- (100 - percentile) / 100 + qntl <- stats::quantile(filteredDF$metric, prct, na.rm = TRUE)[[1]] # Get the percentile, ignoring NAs. + # Mutate to create V1 (climate-smart indicator) and V2 (feature presence indicator). df <- df %>% dplyr::mutate( - V1 = dplyr::if_else(metric >= qntl, true = 1, false = 0), + V1 = dplyr::if_else(.data$metric >= qntl, true = 1, false = 0), V2 = dplyr::if_else(!!rlang::sym(spp[i]) == 1, true = 1, false = 0) ) } else if (direction == -1) { + # If lower values are more climate-smart, calculate the percentileth quantile. prct <- percentile / 100 - qntl <- stats::quantile(filteredDF$metric, prct)[[1]] # Get the percentile + qntl <- stats::quantile(filteredDF$metric, prct, na.rm = TRUE)[[1]] # Get the percentile, ignoring NAs. + # Mutate to create V1 (climate-smart indicator) and V2 (feature presence indicator). df <- df %>% dplyr::mutate( - V1 = dplyr::if_else(metric <= qntl, true = 1, false = 0), + V1 = dplyr::if_else(.data$metric <= qntl, true = 1, false = 0), V2 = dplyr::if_else(!!rlang::sym(spp[i]) == 1, true = 1, false = 0) ) } else { - if (i == 1) { - print("Please enter a valid direction: either 1 or -1.") + # If an invalid direction is provided, print an error (should be caught by assertthat). + if (i == 1) { # Only print for the first iteration to avoid redundant messages. + stop("Please enter a valid direction: either 1 or -1.") } } - if (i > 1){ + # Drop geometry for subsequent bind_cols operations if not the first iteration. + if (i > 1 && "geometry" %in% names(df)){ df <- df %>% - sf::st_drop_geometry() # Drop here otherwise we get multiple version below + sf::st_drop_geometry() } + # Calculate the Climate-Smart (CS) area for the current feature. imptList[[i]] <- df %>% - dplyr::mutate(!!rlang::sym(paste0(spp[i], "_CS")) := .data$V1 * .data$V2) %>% # CS = climate-smart areas - dplyr::select(!!rlang::sym(paste0(spp[i], "_CS"))) + dplyr::mutate(!!rlang::sym(paste0(spp[i], "_CS")) := .data$V1 * .data$V2) %>% # CS = climate-smart areas (feature present AND within climate-smart percentile) + dplyr::select(!!rlang::sym(paste0(spp[i], "_CS"))) # Select only the CS column. } + # Combine all individual feature CS data frames into a single dataframe. imptList <- do.call(dplyr::bind_cols, imptList) + # Initialize an empty list to store processed data for each feature's NCS component. repList <- list() - for (i in 1:length(spp)) { + # Select 1 species at a time from original features data and join with metric. df1 <- features %>% - dplyr::select(!!rlang::sym(spp[i])) %>% # Select 1 species at a time + dplyr::select(!!rlang::sym(spp[i])) %>% sf::st_join(metric, join = sf::st_equals) + # Select the CS column for the current feature from the previously created imptList. df2 <- imptList %>% - dplyr::select(!!rlang::sym(paste0(spp[i], "_CS")),) + dplyr::select(!!rlang::sym(paste0(spp[i], "_CS"))) + # Join the original feature data with its CS status. df3 <- sf::st_join(df1, df2, join = sf::st_equals) - if (i > 1){ + # Drop geometry for subsequent bind_cols operations if not the first iteration. + if (i > 1 && "geometry" %in% names(df3)){ # Check if geometry exists before dropping df3 <- df3 %>% - sf::st_drop_geometry() # Drop here otherwise we get multiple version below + sf::st_drop_geometry() } + # Calculate the Non-Climate-Smart (NCS) area for the current feature. repList[[i]] <- df3 %>% - dplyr::mutate(!!rlang::sym(paste0(spp[i], "_NCS")) := dplyr::if_else(!!rlang::sym(paste0(spp[i], "_CS")) == 1, - true = 0, - false = .data[[spp[i]]] - )) %>% + dplyr::mutate(!!rlang::sym(paste0(spp[i], "_NCS")) := dplyr::if_else( + !!rlang::sym(paste0(spp[i], "_CS")) == 1, # If the area is CS, then NCS is 0. + true = 0, + false = .data[[spp[i]]] # Otherwise, NCS is 1 if feature is present, 0 if not. + )) %>% + # Select both NCS and CS columns for the current feature. dplyr::select(tidyselect::matches("_NCS|_CS")) } + # Combine all individual feature NCS and CS data frames into a single dataframe. + # Ensure geometry is handled correctly and convert back to sf object. repList <- do.call(dplyr::bind_cols, repList) %>% + # Re-add geometry from the original features, as bind_cols might drop it. + sf::st_set_geometry(features$geometry) %>% + # Select all columns, ensuring the order. dplyr::select(tidyselect::everything()) %>% - sf::st_as_sf(sf_column_name = "geometry") + sf::st_as_sf() + return(repList) } -#' Function to assign targets for climate-priority-area approach +#' @title Assign Targets for Climate Priority Area Approach #' -#' @param targets `data.frame`with list of features under "feature" column and their corresponding targets under "target" column -#' @param climateSmartDF `sf` object produced using the function splnr_ClimatePriorityArea_CSapproach() -#' @param refugiaTarget target assigned to climate-smart areas +#' @description +#' This internal function calculates and assigns conservation targets for features +#' when using the Climate Priority Area (CPA) approach. It differentiates targets +#' for climate-smart (CS) and non-climate-smart (NCS) areas. #' -#' @return A new sf dataframe that has cutoffs applied. -#' @noRd +#' @details +#' This function is a core component of the `splnr_climate_priorityAreaApproach()`. +#' It takes the initial targets for each feature and adjusts them based on the +#' proportion of the feature found in climate-smart areas, and a `refugiaTarget`. +#' +#' The logic for target adjustment is as follows: +#' 1. For each feature, it calculates the total number of planning units it occupies. +#' 2. It determines the proportion of the feature's total presence that falls +#' within climate-smart areas (i.e., `_CS` areas). +#' 3. If this climate-smart proportion is greater than the feature's original target, +#' the target for the `_CS` component is adjusted proportionally, and the +#' `_NCS` component's target is set to 0 (meaning all necessary representation +#' can be met within CS areas). +#' 4. Otherwise, the target for the `_CS` component is set to `refugiaTarget` +#' (typically 100% or 1), and the remaining part of the original target is +#' assigned to the `_NCS` component. This ensures that the entire `refugiaTarget` +#' for the CS portion is met, and any shortfall is covered by NCS areas. +#' +#' The output `data.frame` contains the new adjusted targets for each feature, +#' now split into `_CS` and `_NCS` components. +#' +#' @param targets A `data.frame` with two columns: `feature` (character, listing +#' the original feature names) and `target` (numeric, the conservation target +#' for each feature as a proportion, e.g., 0.3). +#' @param climateSmartDF An `sf` object (or data frame) produced by +#' `splnr_climate_priorityArea_preprocess()`. This dataframe contains the +#' original features split into `_CS` and `_NCS` components. +#' @param refugiaTarget A numeric value (0-1) representing the target proportion +#' assigned specifically to climate-smart areas (refugia). Defaults to `1` (100%). +#' +#' @return A `data.frame` with two columns: `feature` (character, now including +#' `_CS` and `_NCS` suffixes for each original feature) and `target` (the +#' newly calculated targets for each climate-split feature). #' @keywords internal +#' @noRd #' -#' @importFrom rlang .data +#' @importFrom assertthat assert_that +#' @importFrom dplyr across bind_rows case_when filter full_join if_else pull select summarize +#' @importFrom rlang .data sym +#' @importFrom sf st_drop_geometry +#' @importFrom stringr str_c str_ends +#' @importFrom tibble tribble +#' @importFrom tidyr replace_na pivot_longer #' #' @examples -#' Features <- dat_species_bin +#' \dontrun{ +#' # Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects. #' -#' targets <- Features %>% +#' # Define initial targets for species features. +#' initial_targets <- dat_species_bin %>% #' sf::st_drop_geometry() %>% #' colnames() %>% #' data.frame() %>% #' setNames(c("feature")) %>% #' dplyr::mutate(target = 0.3) #' -#' metric_df <- dat_clim -#' -#' dat_species_binDF <- dat_species_bin %>% -#' sf::st_drop_geometry() -#' -#' out_sf <- splnr_climate_priorityArea_preprocess( +#' # Preprocess features to get CS/NCS split. +#' preprocessed_features <- splnr_climate_priorityArea_preprocess( #' features = dat_species_bin, -#' percentile = 5, metric = metric_df, direction = 1 +#' percentile = 5, +#' metric = dat_clim, +#' direction = 1 #' ) #' -#' targets <- splnr_climate_priorityArea_assignTargets( -#' targets = targets, -#' climateSmartDF = out_sf, -#' refugiaTarget = 1 +#' # Assign targets using the climate-smart logic. +#' cpa_assigned_targets <- splnr_climate_priorityArea_assignTargets( +#' targets = initial_targets, +#' climateSmartDF = preprocessed_features, +#' refugiaTarget = 1 # Aim for 100% representation in climate-smart areas if possible. #' ) +#' print(cpa_assigned_targets) +#' } splnr_climate_priorityArea_assignTargets <- function(targets, climateSmartDF, refugiaTarget = 1) { + # Assertions to validate input parameters. + assertthat::assert_that( + is.data.frame(targets), + msg = "'targets' must be a data.frame." + ) + assertthat::assert_that( + "feature" %in% names(targets), + msg = "'targets' data.frame must contain a 'feature' column." + ) + assertthat::assert_that( + "target" %in% names(targets), + msg = "'targets' data.frame must contain a 'target' column." + ) + assertthat::assert_that( + inherits(climateSmartDF, "data.frame"), # can be sf or just df after dropping geom + msg = "'climateSmartDF' must be a data.frame (or sf object)." + ) + assertthat::assert_that( + is.numeric(refugiaTarget) && length(refugiaTarget) == 1 && refugiaTarget >= 0 && refugiaTarget <= 1, + msg = "'refugiaTarget' must be a single numeric value between 0 and 1." + ) + # Check if climateSmartDF contains expected _CS and _NCS columns + assertthat::assert_that( + any(grepl("_CS$", names(climateSmartDF))) && any(grepl("_NCS$", names(climateSmartDF))), + msg = "'climateSmartDF' must contain columns with '_CS' and '_NCS' suffixes (output of splnr_climate_priorityArea_preprocess)." + ) + + + # Extract original species names from the 'targets' dataframe. spp <- targets %>% dplyr::select("feature") %>% dplyr::pull() + # Calculate total planning units for each climate-split feature type. featDF <- climateSmartDF %>% - sf::st_drop_geometry() %>% - dplyr::mutate(dplyr::across(dplyr::everything(), ~ tidyr::replace_na(.x, 0))) %>% - dplyr::summarize(dplyr::across(tidyselect::everything(), sum)) %>% - tidyr::pivot_longer(tidyselect::everything(), names_to = "feature", values_to = "planunit") + sf::st_drop_geometry() %>% # Drop geometry to perform numerical summaries. + dplyr::mutate(dplyr::across(dplyr::everything(), ~ tidyr::replace_na(.x, 0))) %>% # Replace NAs with 0. + dplyr::summarize(dplyr::across(tidyselect::everything(), sum)) %>% # Sum up presence counts for each feature component. + tidyr::pivot_longer(tidyselect::everything(), names_to = "feature", values_to = "planunit") # Pivot to long format. - finalList <- list() # empty list + finalList <- list() # Initialize an empty list to store adjusted targets for each feature. for (i in 1:length(spp)) { - filteredTarget <- targets %>% # Get the set target per feature + # Filter to get the original target for the current feature. + filteredTarget <- targets %>% dplyr::filter(.data$feature == spp[i]) - trgt <- filteredTarget$target # Extracting the target set for each feature + trgt <- filteredTarget$target # Extract the target value. + # Define the climate-split feature names for the current original feature. vars <- c( stringr::str_c(spp[i], "_CS"), stringr::str_c(spp[i], "_NCS") ) - suppressMessages({ + # Join the feature unit counts with the original targets. + suppressMessages({ # Suppress join messages. assignTarget <- featDF %>% dplyr::filter(.data$feature %in% vars) %>% dplyr::full_join(filteredTarget) }) - sumUnits <- sum(assignTarget$planunit, na.rm = TRUE) # getting the total of the feature + # Calculate the total number of planning units where the original feature is present. + # This sum includes both CS and NCS components for that feature. + sumUnits <- sum(assignTarget$planunit, na.rm = TRUE) + # Adjust 'planunit' for the original feature's row to represent its total units, then calculate proportion. assignTarget1 <- assignTarget %>% - dplyr::mutate(planunit = dplyr::if_else(stringr::str_ends(.data$feature, paste0(spp[i])), true = sumUnits, false = .data$planunit)) %>% + dplyr::mutate(planunit = dplyr::if_else(stringr::str_ends(.data$feature, spp[i]), true = sumUnits, false = .data$planunit)) %>% dplyr::mutate(proportion = .data$planunit / sumUnits) - reltargetCS <- assignTarget1[assignTarget1$feature == paste0(spp[i], "_CS"), "proportion"] %>% dplyr::pull() # get the relative target for the climate-smart areas + # Get the proportion of the feature that is in climate-smart areas. + reltargetCS <- assignTarget1[assignTarget1$feature == paste0(spp[i], "_CS"), "proportion"] %>% dplyr::pull() - if (reltargetCS > assignTarget1[assignTarget1$feature == spp[i], "target"]) { # Do this check; is the percentile greater than the assigned target for that feature? + # Conditional logic for assigning targets to CS and NCS components. + if (reltargetCS > assignTarget1[assignTarget1$feature == spp[i], "target"]) { + # If the climate-smart proportion is already greater than the original target, + # set CS target proportionally and NCS target to 0. targetCS <- (assignTarget1[assignTarget1$feature == spp[i], "target"] %>% as.numeric()) / (assignTarget1[assignTarget1$feature == paste0(spp[i], "_CS"), "proportion"] %>% as.numeric()) - targetNCS <- 0 } else { + # Otherwise, set CS target to refugiaTarget (usually 100%), and assign remaining target to NCS. targetCS <- refugiaTarget + # Calculate the remaining target for NCS areas. targetNCS <- ((assignTarget1[assignTarget1$feature == spp[i], "target"] %>% as.numeric()) - reltargetCS) / (assignTarget1[assignTarget1$feature == paste0(spp[i], "_NCS"), "proportion"] %>% as.numeric()) } + # Store the adjusted targets for the current feature's CS and NCS components. finalList[[i]] <- assignTarget1 %>% dplyr::mutate(target = dplyr::case_when( - stringr::str_ends(.data$feature, "_CS") ~ targetCS, - stringr::str_ends(.data$feature, "_NCS") ~ targetNCS + stringr::str_ends(.data$feature, "_CS") ~ targetCS, # Assign calculated CS target. + stringr::str_ends(.data$feature, "_NCS") ~ targetNCS # Assign calculated NCS target. )) %>% - dplyr::filter(.data$feature != spp[i]) %>% - dplyr::select("feature", "target") + dplyr::filter(.data$feature != spp[i]) %>% # Remove the original feature row. + dplyr::select("feature", "target") # Select only feature name and adjusted target. } + # Combine all adjusted targets for all features into a single dataframe. finalDF <- do.call(dplyr::bind_rows, finalList) return(finalDF) } -#' Function to run the climate-priority-area approach +#' @title Run the Climate Priority Area (CPA) Approach #' -#' @param features feature `sf`object -#' @param metric climate metric `sf` object with 'metric' as the column name of the metric values per planning unit. -#' @param targets `data.frame`with list of features under "feature" column and their corresponding targets under "target" column -#' @param refugiaTarget target assigned to climate-smart areas -#' @param direction If direction = 1, metric values are from low (least climate-smart) to high (most climate-smart). If direction = -1, metric values are from high (least climate-smart) to low (most climate-smart). -#' @param percentile cut-off threshold for determining whether an area is a climate priority area or not (e.g., lower 35th percentile of warming or upper 65th percentile of acidification). Note that the percentile here is the lower limit of the threshold. +#' @description +#' `splnr_climate_priorityAreaApproach()` implements the Climate Priority Area +#' approach by splitting conservation features into climate-smart (CS) and +#' non-climate-smart (NCS) components and adjusting their targets accordingly. +#' This allows conservation planning to prioritize areas with higher climate resilience. #' -#' @return A `list` with two components: 1. is the data frame passed to `prioritizr` when creating a conservation problem containing the binary information per planning unit per feature. 2. are the targets for the features in the conservation problem when the CPA approach is used. +#' @details +#' This function orchestrates the steps required for the CPA approach: +#' 1. **Preprocessing:** It calls `splnr_climate_priorityArea_preprocess()` to +#' categorize each feature's occurrences into CS and NCS areas based on a +#' climate metric and a specified `percentile` cutoff. +#' 2. **Target Assignment:** It then calls `splnr_climate_priorityArea_assignTargets()` +#' to calculate and assign new targets for these CS and NCS feature components. +#' This ensures that conservation goals reflect the desired emphasis on climate-smart +#' areas (e.g., aiming for 100% representation of features in highly resilient areas). +#' +#' The output of this function is a list containing the modified features (now +#' split into CS/NCS components) and their corresponding adjusted targets, ready +#' to be used in a `prioritizr` conservation problem. +#' +#' @param features An `sf` object representing conservation features (e.g., species +#' distribution data). Each column (excluding geometry) should typically be a +#' binary representation of a feature's presence (1) or absence (0) in each +#' planning unit. +#' @param metric An `sf` object containing climate metric information. It must +#' have a column named 'metric' with the climate metric values per planning unit. +#' @param targets A `data.frame` with two columns: `feature` (character, listing +#' the original feature names) and `target` (numeric, the initial conservation +#' target for each feature as a proportion, e.g., 0.3). +#' @param direction An integer specifying the direction of climate-smartness: +#' \itemize{ +#' \item `1`: Higher metric values mean more climate-smart areas. +#' \item `-1`: Lower metric values mean more climate-smart areas. +#' } +#' @param percentile A numeric value (0-100) representing the cutoff threshold for +#' determining climate-smart areas. For example, `percentile = 5` means the +#' most climate-smart 5% of areas (based on `direction`) are considered. +#' This value represents the lower limit of the threshold. Defaults to `5`. +#' @param refugiaTarget A numeric value (0-1) representing the target proportion +#' assigned specifically to climate-smart areas (refugia). Defaults to `1` (100%). +#' +#' @return A `list` with two components: +#' \itemize{ +#' \item `Features`: An `sf` object containing the binary information per +#' planning unit for each feature, now split into `_CS` (climate-smart) +#' and `_NCS` (non-climate-smart) components. This is ready to be +#' passed to `prioritizr` when creating a conservation problem. +#' \item `Targets`: A `data.frame` with the adjusted targets for the +#' climate-split features. This is also ready for `prioritizr`. +#' } #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr filter mutate select #' @importFrom rlang .data +#' @importFrom sf st_crs #' #' @examples +#' \dontrun{ +#' # Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects +#' # in your package. #' -#' targets <- dat_species_bin %>% +#' # Define initial targets for species features. +#' initial_targets <- dat_species_bin %>% #' sf::st_drop_geometry() %>% #' colnames() %>% #' data.frame() %>% #' setNames(c("feature")) %>% #' dplyr::mutate(target = 0.3) #' -#' CPA_Approach <- splnr_climate_priorityAreaApproach( +#' # Run the Climate Priority Area Approach where lower climate metric +#' # values mean more climate-smart areas. +#' CPA_Approach_result <- splnr_climate_priorityAreaApproach( #' features = dat_species_bin, #' metric = dat_clim, -#' targets = targets, -#' direction = -1 +#' targets = initial_targets, +#' direction = -1, # Example: lower metric values are more climate-smart +#' percentile = 5, +#' refugiaTarget = 1 #' ) -#' out_sf <- CPA_Approach$Features -#' targets <- CPA_Approach$Targets +#' +#' # Access the processed features and targets: +#' out_sf_cpa <- CPA_Approach_result$Features +#' targets_cpa <- CPA_Approach_result$Targets +#' +#' print(head(out_sf_cpa)) +#' print(head(targets_cpa)) +#' } splnr_climate_priorityAreaApproach <- function(features, metric, targets, @@ -250,19 +531,49 @@ splnr_climate_priorityAreaApproach <- function(features, percentile = 5, refugiaTarget = 1) { - #TODO check that geometry of both sf objects are the same. - - assertthat::assert_that(inherits(features, "sf"), - inherits(metric, "sf"), - is.data.frame(targets), - "feature" %in% names(targets), - "target" %in% names(targets), - direction %in% c(-1, 1), - is.numeric(percentile), - percentile >= 0 && percentile <= 100, - is.numeric(refugiaTarget), - refugiaTarget >= 0 && refugiaTarget <= 1) + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(features, "sf"), + msg = "'features' must be an 'sf' object." + ) + assertthat::assert_that( + inherits(metric, "sf"), + msg = "'metric' must be an 'sf' object." + ) + assertthat::assert_that( + "metric" %in% names(metric), + msg = "'metric' sf object must contain a column named 'metric'." + ) + assertthat::assert_that( + is.data.frame(targets), + msg = "'targets' must be a data.frame." + ) + assertthat::assert_that( + "feature" %in% names(targets), + msg = "'targets' data.frame must contain a 'feature' column." + ) + assertthat::assert_that( + "target" %in% names(targets), + msg = "'targets' data.frame must contain a 'target' column." + ) + assertthat::assert_that( + direction %in% c(-1, 1), + msg = "'direction' must be either 1 (higher metric = more climate-smart) or -1 (lower metric = more climate-smart)." + ) + assertthat::assert_that( + is.numeric(percentile) && length(percentile) == 1 && percentile >= 0 && percentile <= 100, + msg = "'percentile' must be a single numeric value between 0 and 100." + ) + assertthat::assert_that( + is.numeric(refugiaTarget) && length(refugiaTarget) == 1 && refugiaTarget >= 0 && refugiaTarget <= 1, + msg = "'refugiaTarget' must be a single numeric value between 0 and 1." + ) + assertthat::assert_that( + sf::st_crs(features) == sf::st_crs(metric), + msg = "CRS of 'features' and 'metric' must be the same." + ) + # Preprocess features to split them into climate-smart (CS) and non-climate-smart (NCS) areas. CPAFeatures <- splnr_climate_priorityArea_preprocess( features = features, metric = metric, @@ -270,163 +581,374 @@ splnr_climate_priorityAreaApproach <- function(features, percentile = percentile ) + # Assign adjusted targets for the CS and NCS feature components. CPATargets <- splnr_climate_priorityArea_assignTargets( targets = targets, - CPAFeatures, + climateSmartDF = CPAFeatures, # Use the preprocessed features for target assignment. refugiaTarget = refugiaTarget ) + # Return a list containing both the processed features and their adjusted targets. return(list(Features = CPAFeatures, Targets = CPATargets)) } -##### Feature Approach #### + ##### Feature Approach #### -#' Function to run the feature climate-smart approach +#' @title Preprocess Data for Feature Climate-Smart Approach #' -#' This function creates a climate layer by selecting the most climate-smart areas in the entire planning region. +#' @description +#' This internal function creates a "climate layer" by identifying the most +#' climate-smart areas across the entire planning region, based on a percentile +#' cutoff for a given climate metric. This layer is then attached to the +#' original features data. #' -#' @param features feature `sf`object -#' @param metric climate metric `sf` object with 'metric' as the column name of the metric values per planning unit.#' -#' @param direction If direction = 1, metric values are from low (least climate-smart) to high (most climate-smart). If direction = -1, metric values are from high (least climate-smart) to low (most climate-smart). -#' @param percentile cut-off threshold for determining whether an area is a climate priority area or not. Note that the percentile here is the lower limit of the threshold. +#' @details +#' The Feature Approach to climate-smart conservation aims to prioritize a fixed +#' proportion of the most climate-resilient areas (the `climate_layer`) and +#' ensure that conservation features are represented within this layer. #' -#' @return A `list` with two components: 1. is the data frame passed to `prioritizr` when creating a conservation problem containing the binary information per planning unit per feature. 2. are the targets for the features in the conservation problem when the CPA approach is used. +#' This preprocessing step involves: +#' 1. Identifying the `percentile` cutoff for the global climate `metric` data +#' (not per feature, as in CPA). +#' 2. Creating a binary `climate_layer` where planning units meeting the +#' climate-smart criteria are marked as 1, and others as 0. +#' 3. Joining this `climate_layer` back to the original `features` data. #' -#' @noRd +#' The `direction` parameter functions similarly to the CPA approach: +#' - `direction = 1`: Higher values of the `metric` are considered more climate-smart. +#' The `climate_layer` will include areas with metric values >= the `percentile`th quantile. +#' - `direction = -1`: Lower values of the `metric` are considered more climate-smart. +#' The `climate_layer` will include areas with metric values <= the `percentile`th quantile. +#' +#' @param features An `sf` object representing conservation features. +#' @param metric An `sf` object containing climate metric information. It must +#' have a column named 'metric' with the climate metric values per planning unit. +#' @param percentile A numeric value (0-100) representing the cutoff threshold for +#' determining whether an area is a climate priority area or not. This is applied +#' globally to the `metric` data. +#' @param direction An integer specifying the direction of climate-smartness: +#' \itemize{ +#' \item `1`: Higher metric values mean more climate-smart. +#' \item `-1`: Lower metric values mean more climate-smart. +#' } +#' +#' @return An `sf` dataframe identical to the input `features`, but with an +#' additional binary column named `climate_layer` indicating which planning +#' units are considered climate-smart. #' @keywords internal +#' @noRd +#' @importFrom assertthat assert_that +#' @importFrom dplyr mutate select #' @importFrom rlang .data +#' @importFrom sf st_join +#' @importFrom stats quantile #' #' @examples +#' \dontrun{ #' -#' featureTest <- splnr_climate_feature_preprocess( +#' # Example: Create a climate layer where higher metric values are climate-smart. +#' feature_preprocessed_data <- splnr_climate_feature_preprocess( #' features = dat_species_bin, -#' percentile = 5, +#' percentile = 5, # Top 5% most climate-smart areas #' metric = dat_clim, #' direction = 1 #' ) +#' print(feature_preprocessed_data) +#' +#' # Example: Create a climate layer where lower metric values are climate-smart. +#' feature_preprocessed_data_alt <- splnr_climate_feature_preprocess( +#' features = dat_species_bin, +#' percentile = 10, # Bottom 10% most climate-smart areas +#' metric = dat_clim, +#' direction = -1 +#' ) +#' print(feature_preprocessed_data_alt) +#' } splnr_climate_feature_preprocess <- function(features, percentile, metric, direction) { - if (any(apply(metric, 2, is.na)[, "metric"])) { - print("There are some NAs in the metric data. Please check.") + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(features, "sf"), + msg = "'features' must be an 'sf' object." + ) + assertthat::assert_that( + inherits(metric, "sf"), + msg = "'metric' must be an 'sf' object." + ) + assertthat::assert_that( + "metric" %in% names(metric), + msg = "'metric' sf object must contain a column named 'metric'." + ) + assertthat::assert_that( + is.numeric(percentile) && length(percentile) == 1 && percentile >= 0 && percentile <= 100, + msg = "'percentile' must be a single numeric value between 0 and 100." + ) + assertthat::assert_that( + direction %in% c(1, -1), + msg = "'direction' must be either 1 (higher metric = more climate-smart) or -1 (lower metric = more climate-smart)." + ) + assertthat::assert_that( + sf::st_crs(features) == sf::st_crs(metric), + msg = "CRS of 'features' and 'metric' must be the same." + ) + + + # Check for NAs in the 'metric' column and print a warning if found. + if (any(is.na(metric$metric))) { # Accessing 'metric' directly from 'metric' sf object + message("Warning: There are some NAs in the metric data. These will be removed from percentile calculation.") } + + # Convert percentile to proportion for quantile calculation. prct <- percentile / 100 - qntl <- stats::quantile(metric$metric, prct)[[1]] # Get the percentile + # Calculate the quantile cutoff based on the global metric values. + qntl <- stats::quantile(metric$metric, prct, na.rm = TRUE)[[1]] + + # Create the climate layer based on the direction. if (direction == 1) { - print("Higher values mean more climate-smart areas.") + message("Higher values mean more climate-smart areas.") # Inform user about the direction. + # Create the binary climate_layer: 1 if metric >= quantile, 0 otherwise. df <- metric %>% - dplyr::mutate(climate_layer = ifelse(.data$metric >= qntl, yes = 1, no = 0)) # Create the climate layer + dplyr::mutate(climate_layer = ifelse(.data$metric >= qntl, yes = 1, no = 0)) } else if (direction == -1) { - print("Lower values mean more climate-smart areas.") + message("Lower values mean more climate-smart areas.") # Inform user about the direction. + # Create the binary climate_layer: 1 if metric <= quantile, 0 otherwise. df <- metric %>% - dplyr::mutate(climate_layer = ifelse(.data$metric <= qntl, yes = 1, no = 0)) # Create the climate layer + dplyr::mutate(climate_layer = ifelse(.data$metric <= qntl, yes = 1, no = 0)) } else { - if (direction != 1 & direction != -1) { - print("Please enter a valid direction: either 1 or -1.") - return(NULL) - } + # This case should ideally be caught by assertthat, but included as a fallback. + stop("Please enter a valid direction: either 1 or -1.") } - - # Get the most climate-smart areas + # Select only the newly created 'climate_layer' column. climateSmartDF <- df %>% dplyr::select("climate_layer") - # Attach "climate_layer" to the features df and have this as the output + # Attach the "climate_layer" to the original features dataframe. features <- features %>% sf::st_join(climateSmartDF, join = sf::st_equals) return(features) } -#' Function to assign targets for the feature approach +#' @title Assign Targets for Feature Climate-Smart Approach #' -#' @param targets `data.frame`with list of features under "feature" column and their corresponding targets under "target" column -#' @param climateSmartDF `sf` object produced using the function splnr_ClimatePriorityArea_CSapproach() -#' @param refugiaTarget target assigned to climate-smart areas +#' @description +#' This internal function calculates and assigns conservation targets when using +#' the Feature Approach to climate-smart spatial planning. It adjusts targets +#' to ensure a specified `refugiaTarget` is met within the climate-smart layer. #' -#' @return A new sf dataframe that has cutoffs applied. -#' @noRd +#' @details +#' This function is a key component of the `splnr_climate_featureApproach()`. +#' It takes the initial conservation targets for features and modifies them +#' by introducing a new target for the `climate_layer` itself. +#' +#' The target for the `climate_layer` is calculated as `refugiaTarget` divided +#' by the proportion of the total planning units that are designated as +#' "climate-smart" (i.e., `sum(climateSmartDF$climate_layer) / nrow(climateSmartDF)`). +#' This effectively scales up the `refugiaTarget` to ensure that when `prioritizr` +#' tries to select `refugiaTarget` proportion of the total planning units for the +#' `climate_layer`, it will actually aim to select that proportion *within* the +#' climate-smart areas. +#' +#' The new `climate_layer` target is then appended to the original feature targets. +#' +#' @param climateSmartDF An `sf` object (or data frame) containing the +#' `climate_layer` column, typically produced by `splnr_climate_feature_preprocess()`. +#' @param refugiaTarget A numeric value (0-1) representing the target proportion +#' of climate-smart areas that should be selected. +#' @param targets A `data.frame` with two columns: `feature` (character, listing +#' the original feature names) and `target` (numeric, the initial conservation +#' target for each feature as a proportion, e.g., 0.3). +#' +#' @return A `data.frame` with two columns: `feature` (character, including +#' original feature names and "climate_layer") and `target` (the calculated +#' targets for these features). #' @keywords internal +#' @noRd #' -#' @importFrom rlang .data +#' @importFrom assertthat assert_that +#' @importFrom dplyr bind_rows +#' @importFrom tibble tribble #' #' @examples -#' Features <- dat_species_bin +#' \dontrun{ +#' # Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects. #' -#' targets <- Features %>% +#' # Define initial targets for species features. +#' initial_targets <- dat_species_bin %>% #' sf::st_drop_geometry() %>% #' colnames() %>% #' data.frame() %>% #' setNames(c("feature")) %>% #' dplyr::mutate(target = 0.3) #' -#' dat_species_binDF <- dat_species_bin %>% -#' sf::st_drop_geometry() -#' -#' out_sf <- splnr_climate_feature_preprocess( +#' # Preprocess features to get the climate layer. +#' preprocessed_features <- splnr_climate_feature_preprocess( #' features = dat_species_bin, #' percentile = 5, #' metric = dat_clim, #' direction = 1 #' ) #' -#' targets <- splnr_climate_feature_assignTargets( -#' targets = targets, -#' climateSmartDF = out_sf, -#' refugiaTarget = 0.3 +#' # Assign targets for the feature approach. +#' feature_assigned_targets <- splnr_climate_feature_assignTargets( +#' climateSmartDF = preprocessed_features, +#' refugiaTarget = 0.3, # Aim for 30% of climate-smart areas to be selected. +#' targets = initial_targets #' ) +#' print(feature_assigned_targets) +#' } splnr_climate_feature_assignTargets <- function(climateSmartDF, refugiaTarget, targets) { - # Calculate the target depending on the # of PUs deemed as "climate-smart" - trgt <- refugiaTarget / (sum(climateSmartDF$climate_layer) / nrow(climateSmartDF)) + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(climateSmartDF, "data.frame"), + msg = "'climateSmartDF' must be a data.frame (or sf object)." + ) + assertthat::assert_that( + "climate_layer" %in% names(climateSmartDF), + msg = "'climateSmartDF' must contain a 'climate_layer' column (output of splnr_climate_feature_preprocess)." + ) + assertthat::assert_that( + is.numeric(refugiaTarget) && length(refugiaTarget) == 1 && refugiaTarget >= 0 && refugiaTarget <= 1, + msg = "'refugiaTarget' must be a single numeric value between 0 and 1." + ) + assertthat::assert_that( + is.data.frame(targets), + msg = "'targets' must be a data.frame." + ) + assertthat::assert_that( + "feature" %in% names(targets), + msg = "'targets' data.frame must contain a 'feature' column." + ) + assertthat::assert_that( + "target" %in% names(targets), + msg = "'targets' data.frame must contain a 'target' column." + ) + + # Calculate the target for the 'climate_layer'. + # This scales the refugiaTarget by the inverse of the proportion of planning units + # that are climate-smart. This ensures that the desired refugiaTarget is met + # specifically within the climate-smart areas. + total_climate_smart_units <- sum(climateSmartDF$climate_layer, na.rm = TRUE) + total_planning_units <- nrow(climateSmartDF) + + if (total_planning_units == 0) { + stop("Input 'climateSmartDF' has no planning units. Cannot assign targets.") + } + proportion_climate_smart <- total_climate_smart_units / total_planning_units + + if (proportion_climate_smart == 0) { + stop("No climate-smart planning units identified. Cannot assign a target to the climate layer.") + } + + trgt <- refugiaTarget / proportion_climate_smart + + # Create a data frame for the climate layer's target. climate_layerDF <- tibble::tribble( ~feature, ~target, "climate_layer", trgt ) + # Combine the original targets with the new climate layer target. finalDF <- targets %>% - dplyr::bind_rows(climate_layerDF) # %>% - # dplyr::mutate(targets = .data$targets / 100) # Convert target to proportions + dplyr::bind_rows(climate_layerDF) return(finalDF) } -#' Function to run the feature approach +#' @title Run the Feature Climate-Smart Approach +#' +#' @description +#' `splnr_climate_featureApproach()` implements the Feature Approach to +#' climate-smart conservation planning. This involves defining a global +#' "climate-smart" layer and adjusting conservation targets to ensure that +#' a specified proportion of this layer is captured in the solution. +#' +#' @details +#' This function orchestrates the steps for the Feature Approach: +#' 1. **Preprocessing:** It calls `splnr_climate_feature_preprocess()` to +#' identify a region-wide climate-smart layer based on a percentile cutoff +#' of the climate metric. This layer is then added as a new binary feature +#' to your conservation data. +#' 2. **Target Assignment:** It then calls `splnr_climate_feature_assignTargets()` +#' to calculate and assign new targets. Crucially, a specific `refugiaTarget` +#' is set for the newly created `climate_layer` feature, ensuring that a +#' certain proportion of the most climate-resilient areas are included in +#' the final conservation plan. #' -#' @param features feature `sf`object -#' @param metric climate metric `sf` object with 'metric' as the column name of the metric values per planning unit. -#' @param targets `data.frame`with list of features under "feature" column and their corresponding targets under "target" column -#' @param refugiaTarget target assigned to climate-smart areas -#' @param direction If direction = 1, metric values are from low (least climate-smart) to high (most climate-smart). If direction = -1, metric values are from high (least climate-smart) to low (most climate-smart). -#' @param percentile cut-off threshold for determining whether an area is a climate priority area or not (e.g., lower 35th percentile of warming or upper 65th percentile of acidification). Note that the percentile here is the lower limit of the threshold. +#' The output is a list containing the modified features (now including the +#' `climate_layer`) and their corresponding adjusted targets, ready to be used +#' in a `prioritizr` conservation problem. #' -#' @return A `list` with two components: 1. is the data frame passed to `prioritizr` when creating a conservation problem containing the binary information per planning unit per feature. 2. are the targets for the features in the conservation problem when the CPA approach is used. +#' @param features An `sf` object representing conservation features (e.g., species +#' distribution data). +#' @param metric An `sf` object containing climate metric information. It must +#' have a column named 'metric' with the climate metric values per planning unit. +#' @param targets A `data.frame` with two columns: `feature` (character, listing +#' the original feature names) and `target` (numeric, the initial conservation +#' target for each feature as a proportion, e.g., 0.3). +#' @param direction An integer specifying the direction of climate-smartness: +#' \itemize{ +#' \item `1`: Higher metric values mean more climate-smart areas. +#' \item `-1`: Lower metric values mean more climate-smart areas. +#' } +#' @param percentile A numeric value (0-100) representing the cutoff threshold for +#' determining whether an area is a climate priority area or not. This is applied +#' globally to the `metric` data. Defaults to `35`. +#' @param refugiaTarget A numeric value (0-1) representing the target proportion +#' assigned to the overall climate-smart layer. Defaults to `0.3` (30%). +#' +#' @return A `list` with two components: +#' \itemize{ +#' \item `Features`: An `sf` object containing the binary information per +#' planning unit for each original feature, plus the new `climate_layer` +#' feature. This is ready to be passed to `prioritizr`. +#' \item `Targets`: A `data.frame` with the adjusted targets for all features, +#' including the `climate_layer`. This is also ready for `prioritizr`. +#' } #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr filter mutate select #' @importFrom rlang .data +#' @importFrom sf st_crs #' #' @examples -#' Features <- dat_species_bin +#' \dontrun{ +#' # Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects +#' # in your package. #' -#' targets <- Features %>% +#' # Define initial targets for species features. +#' initial_targets <- dat_species_bin %>% #' sf::st_drop_geometry() %>% #' colnames() %>% #' data.frame() %>% #' setNames(c("feature")) %>% #' dplyr::mutate(target = 0.3) #' -#' Feature_Approach <- splnr_climate_featureApproach( +#' # Run the Feature Approach where higher climate metric values mean +#' # more climate-smart areas. +#' Feature_Approach_result <- splnr_climate_featureApproach( #' features = dat_species_bin, #' metric = dat_clim, -#' targets = targets, -#' direction = 1 +#' targets = initial_targets, +#' direction = 1, # Example: higher metric values are more climate-smart +#' percentile = 35, +#' refugiaTarget = 0.3 #' ) -#' out_sf <- Feature_Approach$Features -#' targets <- Feature_Approach$Targets +#' +#' # Access the processed features and targets: +#' out_sf_feature <- Feature_Approach_result$Features +#' targets_feature <- Feature_Approach_result$Targets +#' +#' print(head(out_sf_feature)) +#' print(head(targets_feature)) +#' } splnr_climate_featureApproach <- function(features, metric, targets, @@ -434,20 +956,50 @@ splnr_climate_featureApproach <- function(features, percentile = 35, refugiaTarget = 0.3) { - #TODO Check that geometry is the same in all the asserts - - assertthat::assert_that(inherits(features, "sf"), - inherits(metric, "sf"), - is.data.frame(targets), - "feature" %in% names(targets), - "target" %in% names(targets), - direction %in% c(-1, 1), - is.numeric(percentile), - percentile >= 0 && percentile <= 100, - is.numeric(refugiaTarget), - refugiaTarget >= 0 && refugiaTarget <= 1) + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(features, "sf"), + msg = "'features' must be an 'sf' object." + ) + assertthat::assert_that( + inherits(metric, "sf"), + msg = "'metric' must be an 'sf' object." + ) + assertthat::assert_that( + "metric" %in% names(metric), + msg = "'metric' sf object must contain a column named 'metric'." + ) + assertthat::assert_that( + is.data.frame(targets), + msg = "'targets' must be a data.frame." + ) + assertthat::assert_that( + "feature" %in% names(targets), + msg = "'targets' data.frame must contain a 'feature' column." + ) + assertthat::assert_that( + "target" %in% names(targets), + msg = "'targets' data.frame must contain a 'target' column." + ) + assertthat::assert_that( + direction %in% c(-1, 1), + msg = "'direction' must be either 1 (higher metric = more climate-smart) or -1 (lower metric = more climate-smart)." + ) + assertthat::assert_that( + is.numeric(percentile) && length(percentile) == 1 && percentile >= 0 && percentile <= 100, + msg = "'percentile' must be a single numeric value between 0 and 100." + ) + assertthat::assert_that( + is.numeric(refugiaTarget) && length(refugiaTarget) == 1 && refugiaTarget >= 0 && refugiaTarget <= 1, + msg = "'refugiaTarget' must be a single numeric value between 0 and 1." + ) + assertthat::assert_that( + sf::st_crs(features) == sf::st_crs(metric), + msg = "CRS of 'features' and 'metric' must be the same." + ) + # Preprocess features to create the climate_layer. featureFeatures <- splnr_climate_feature_preprocess( features = features, metric = metric, @@ -455,217 +1007,511 @@ splnr_climate_featureApproach <- function(features, percentile = percentile ) + # Assign targets, including the new climate_layer. featureTargets <- splnr_climate_feature_assignTargets( targets = targets, - featureFeatures, refugiaTarget = refugiaTarget + climateSmartDF = featureFeatures, # Use the preprocessed features for target assignment. + refugiaTarget = refugiaTarget ) + # Return a list containing both the processed features and their adjusted targets. return(list(Features = featureFeatures, Targets = featureTargets)) } -##### Percentile Approach #### -#' Preprocessing for the percentile climate-smart approach -#' This function filters the species' distributions to their climate-smart areas only. -#' @param features feature sf object -#' @param metric climate metric `sf` object with 'metric' as the column name of the metric values per planning unit. -#' @param percentile cut-off threshold for determining whether an area is a climate priority area or not (e.g., lower 35th percentile of warming or upper 65th percentile of acidification). Note that the percentile here is the lower limit of the threshold. -#' @param direction If direction = 1, metric values are from low (least climate-smart) to high (most climate-smart). If direction = -1, metric values are from high (least climate-smart) to low (most climate-smart). + ##### Percentile Approach #### + +#' @title Preprocessing for the Percentile Climate-Smart Approach #' -#' @return A new sf dataframe that has cutoffs applied. -#' @noRd +#' @description +#' This internal function filters the distributions of each conservation feature +#' to include only their occurrences within "climate-smart" areas. These areas +#' are defined by a percentile cutoff of a climate metric applied to each +#' feature's distribution. +#' +#' @details +#' The Percentile Approach to climate-smart conservation ensures that a certain +#' proportion of each feature's occurrence within its most climate-resilient +#' habitats is protected. This preprocessing step identifies these areas. +#' +#' For each feature, the function performs the following: +#' 1. Joins the feature data with the climate metric data. +#' 2. Filters to include only planning units where the feature is present. +#' 3. Calculates the `percentile` cutoff for the climate metric *within that +#' specific feature's distribution*. +#' 4. Creates a new binary column for each feature (`_filtered`) indicating +#' planning units where the feature is present AND the climate metric meets +#' the climate-smart criteria (e.g., top 5% for direction 1, bottom 5% for direction -1). +#' All other planning units for that feature are set to 0 in this new column. +#' +#' The `direction` parameter defines what constitutes "climate-smart": +#' - `direction = 1`: Higher values of the `metric` are more climate-smart. +#' The function selects areas with metric values greater than or equal to the +#' `(100 - percentile)`th quantile of the feature's occupied metric values. +#' - `direction = -1`: Lower values of the `metric` are more climate-smart. +#' The function selects areas with metric values less than or equal to the +#' `percentile`th quantile of the feature's occupied metric values. +#' +#' @param features An `sf` object representing conservation features. Each column +#' (excluding geometry) should typically be a binary representation of a feature's +#' presence (1) or absence (0) in each planning unit. +#' @param metric An `sf` object containing climate metric information. It must +#' have a column named 'metric' with the climate metric values per planning unit. +#' @param percentile A numeric value (0-100) representing the cutoff threshold for +#' determining whether an area is a climate priority area or not. This is applied +#' *per feature* to its distribution. +#' @param direction An integer specifying the direction of climate-smartness: +#' \itemize{ +#' \item `1`: Higher metric values mean more climate-smart. +#' \item `-1`: Lower metric values mean more climate-smart. +#' } +#' +#' @return An `sf` dataframe where each column represents an original feature, +#' but its values are now filtered (`_filtered` suffix implicitly removed in `rename_all`) +#' to 1 only in planning units that are part of its climate-smart percentile. +#' All other values are 0. The dataframe retains the original geometry. #' @keywords internal +#' @noRd #' -#' @importFrom rlang .data +#' @importFrom assertthat assert_that +#' @importFrom dplyr across bind_cols filter if_else mutate select +#' @importFrom rlang .data sym +#' @importFrom sf st_as_sf st_drop_geometry st_join st_set_geometry +#' @importFrom stats quantile +#' @importFrom stringr str_sub +#' @importFrom tidyselect everything #' #' @examples +#' \dontrun{ +#' # Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects +#' # in your package. #' -#' out_sf <- splnr_climate_percentile_preprocess( +#' # Example: Filter species distributions to their top 5% most climate-smart areas, +#' # where higher metric values are considered more climate-smart. +#' percentile_preprocessed_data <- splnr_climate_percentile_preprocess( #' features = dat_species_bin, #' metric = dat_clim, #' percentile = 5, #' direction = 1 #' ) +#' print(percentile_preprocessed_data) +#' +#' # Example: Filter to bottom 10% most climate-smart areas, +#' # where lower metric values are considered more climate-smart. +#' percentile_preprocessed_data_alt <- splnr_climate_percentile_preprocess( +#' features = dat_species_bin, +#' metric = dat_clim, +#' percentile = 10, +#' direction = -1 +#' ) +#' print(percentile_preprocessed_data_alt) +#' } splnr_climate_percentile_preprocess <- function(features, metric, percentile, direction) { - if (any(apply(metric, 2, is.na)[, "metric"])) { - print("There are some NAs in the metric data. Please check.") + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(features, "sf"), + msg = "'features' must be an 'sf' object." + ) + assertthat::assert_that( + inherits(metric, "sf"), + msg = "'metric' must be an 'sf' object." + ) + assertthat::assert_that( + "metric" %in% names(metric), + msg = "'metric' sf object must contain a column named 'metric'." + ) + assertthat::assert_that( + is.numeric(percentile) && length(percentile) == 1 && percentile >= 0 && percentile <= 100, + msg = "'percentile' must be a single numeric value between 0 and 100." + ) + assertthat::assert_that( + direction %in% c(1, -1), + msg = "'direction' must be either 1 (higher metric = more climate-smart) or -1 (lower metric = more climate-smart)." + ) + assertthat::assert_that( + sf::st_crs(features) == sf::st_crs(metric), + msg = "CRS of 'features' and 'metric' must be the same." + ) + + # Check for NAs in the 'metric' column and print a warning if found. + if (any(is.na(metric$metric))) { + message("Warning: There are some NAs in the metric data. Please check.") } - spp <- features %>% # Get the list of features + # Get the list of feature names (excluding geometry). + spp <- features %>% sf::st_drop_geometry() %>% names() - percentileList <- list() + percentileList <- list() # Initialize an empty list to store processed data for each feature. for (i in 1:length(spp)) { + # Select 1 feature at a time from original features data and join with the metric layer. df <- features %>% - dplyr::select(!!rlang::sym(spp[i]),) %>% # Select 1 feature at a time - sf::st_join(metric, join = sf::st_equals) %>% # Join with the metric layer - sf::st_drop_geometry() # Drop here otherwise we get multiple version below + dplyr::select(!!rlang::sym(spp[i]),) %>% + sf::st_join(metric, join = sf::st_equals) + + # Check for NAs in the metric column of the joined data frame for the current feature + # and print a warning if found. + if (any(is.na(df$metric))) { + message(paste0("Warning: NAs found in 'metric' for feature '", spp[i], "'. These will be excluded from percentile calculation.")) + } + # Filter to select only areas where the current feature is present (value = 1). filteredDF <- df %>% - dplyr::filter(!!rlang::sym(spp[i]) == 1) # Select only areas with presences + dplyr::filter(!!rlang::sym(spp[i]) == 1) + + # Handle cases where filteredDF might be empty (feature not present in any unit or only in NAs) + if (nrow(filteredDF) == 0) { + warning(paste0("Feature '", spp[i], "' is not present in any planning unit with valid metric data. Skipping percentile calculation for this feature.")) + # Create an empty df with expected column for binding later + temp_df <- df %>% + sf::st_drop_geometry() %>% # Drop geometry for consistency with other iterations + dplyr::mutate(V1 = 0, V2 = 0) %>% # Add V1 and V2 columns with 0 values + dplyr::mutate(!!rlang::sym(paste0(spp[i], "_filtered")) := 0) %>% + dplyr::select(!!rlang::sym(paste0(spp[i], "_filtered"))) + percentileList[[i]] <- temp_df + next # Skip to the next iteration of the loop + } + + + # Convert percentile to proportion. + prct <- percentile / 100 + # Calculate the quantile cutoff within the current feature's distribution. + qntl <- stats::quantile(filteredDF$metric, prct, na.rm = TRUE)[[1]] - prct <- percentile / 100 # Convert percentiles to proportions - qntl <- stats::quantile(filteredDF$metric, prct)[[1]] # Get the percentile + # Apply filtering based on direction to create temporary V1 and V2 columns. if (direction == 1) { - if (i == 1) { - print("Higher values mean more climate-smart areas.") # Sanity check + if (i == 1) { # Only print for the first iteration. + message("Higher values mean more climate-smart areas.") } df1 <- df %>% dplyr::mutate( - V1 = ifelse(metric >= qntl, yes = 1, no = 0), # Filter areas of the highest xth percentile within feat's distribution - V2 = ifelse(!!rlang::sym(spp[i]) == 1, yes = 1, no = 0) - ) # Filter areas with the feat present in it + V1 = ifelse(.data$metric >= qntl, yes = 1, no = 0), # 1 if metric is in climate-smart percentile, 0 otherwise. + V2 = ifelse(!!rlang::sym(spp[i]) == 1, yes = 1, no = 0) # 1 if feature is present, 0 otherwise. + ) } else if (direction == -1) { - if (i == 1) { - print("Lower values mean more climate-smart areas.") # Sanity check + if (i == 1) { # Only print for the first iteration. + message("Lower values mean more climate-smart areas.") } df1 <- df %>% dplyr::mutate( - V1 = ifelse(metric <= qntl, yes = 1, no = 0), # Filter areas of the highest xth percentile within feat's distribution - V2 = ifelse(!!rlang::sym(spp[i]) == 1, yes = 1, no = 0) - ) # Filter areas with the feat present in it + V1 = ifelse(.data$metric <= qntl, yes = 1, no = 0), # 1 if metric is in climate-smart percentile, 0 otherwise. + V2 = ifelse(!!rlang::sym(spp[i]) == 1, yes = 1, no = 0) # 1 if feature is present, 0 otherwise. + ) } else { + # This case should ideally be caught by assertthat, but included as a fallback. if (i == 1) { - print("Please enter a valid direction: either 1 or -1.") # Sanity check + stop("Please enter a valid direction: either 1 or -1.") } } + # Drop geometry for subsequent bind_cols operations if not the first iteration. + # We will re-add the geometry from the original 'features' object at the end. + if (i > 1 && "geometry" %in% names(df1)){ + df1 <- df1 %>% sf::st_drop_geometry() + } else if (i == 1 && "geometry" %in% names(df1)){ + # For the first iteration, keep geometry if it exists, and make sure it's the right one. + # This block ensures that the geometry is the actual sf geometry and not just a column name. + # The do.call(dplyr::bind_cols, ...) will handle the geometry column from the first element. + df1 <- df1 %>% dplyr::select(-"geometry") # Temporarily remove geometry + } + + # Calculate the final filtered feature layer (1 if present in climate-smart percentile, 0 otherwise). percentileList[[i]] <- df1 %>% - dplyr::mutate(!!rlang::sym(paste0(spp[i], "_filtered")) := .data$V1 * .data$V2) %>% # V1*V2 will be 1 if area is within the xth percentile and if a feat is present in it - dplyr::select(!!rlang::sym(paste0(spp[i], "_filtered"))) + dplyr::mutate(!!rlang::sym(paste0(spp[i], "_filtered")) := .data$V1 * .data$V2) %>% # V1*V2 is 1 if area is within percentile AND feature is present. + dplyr::select(!!rlang::sym(paste0(spp[i], "_filtered"))) # Select only the filtered column. } - resultDF <- do.call(dplyr::bind_cols, percentileList) %>% # Create sf object as output - dplyr::rename_all(~ stringr::str_sub(.x, end = -10)) %>% - dplyr::bind_cols(features %>% dplyr::select("geometry")) %>% - dplyr::select(tidyselect::everything()) %>% - sf::st_as_sf(sf_column_name = "geometry") + # Combine all processed data frames into a single dataframe. + # Rename columns to remove the "_filtered" suffix for easier use. + + resultDF <- do.call(dplyr::bind_cols, percentileList) %>% + dplyr::rename_all(~ stringr::str_remove(.x, "_filtered$")) %>% # Remove "_filtered" suffix. + # Re-add the geometry column from the original features object. + sf::st_set_geometry(features$geometry) %>% + dplyr::select(tidyselect::everything()) %>% # Select all columns, ensuring order. + sf::st_as_sf() # Convert back to sf object. return(resultDF) } -#' Function to assign targets for the percentile approach +#' @title Assign Targets for Percentile Climate-Smart Approach #' -#' @param features feature sf object -#' @param targets `data.frame`with list of features under "feature" column and their corresponding targets under "target" column -#' @param climateSmartDF `sf` object produced using the function `splnr_climate_percentile_preprocess()` +#' @description +#' This internal function calculates and assigns conservation targets when using +#' the Percentile Approach to climate-smart spatial planning. It adjusts targets +#' to account for the filtering of feature distributions to only their +#' climate-smart areas. #' -#' @return A new sf dataframe that has cutoffs applied. -#' @noRd +#' @details +#' This function is a key component of the `splnr_climate_percentileApproach()`. +#' It takes the original conservation targets for features and adjusts them based +#' on how much of each feature's total occurrence was retained after filtering +#' for climate-smart areas in `splnr_climate_percentile_preprocess()`. +#' +#' The target adjustment logic is as follows: +#' 1. Calculates the "original" total number of planning units where each feature is present. +#' 2. Calculates the "filtered" total number of planning units where each feature +#' is present *within its climate-smart percentile* (from `climateSmartDF`). +#' 3. Determines the `proportion` of the filtered presence relative to the +#' original presence for each feature (`filtered / original`). +#' 4. The new target for each feature is then calculated by dividing its original +#' target by this `proportion`. This scales up the target to ensure that the +#' original conservation goal is still met, but specifically within the +#' identified climate-smart areas. +#' 5. Targets are capped at `1` (100%) to prevent values greater than 1. +#' +#' @param features An `sf` object representing original conservation features. +#' This is used to determine the total (unfiltered) presence of each feature. +#' @param targets A `data.frame` with two columns: `feature` (character, listing +#' the original feature names) and `target` (numeric, the initial conservation +#' target for each feature as a proportion, e.g., 0.3). +#' @param climateSmartDF An `sf` object (or data frame) produced by +#' `splnr_climate_percentile_preprocess()`. This dataframe contains the +#' features filtered to their climate-smart areas. +#' +#' @return A `data.frame` with two columns: `feature` (character, with original +#' feature names) and `target` (the newly calculated, adjusted targets). #' @keywords internal +#' @noRd #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr across everything left_join mutate select summarize #' @importFrom rlang .data +#' @importFrom sf st_drop_geometry +#' @importFrom tibble as_tibble +#' @importFrom tidyr replace_na pivot_longer #' #' @examples -#' Features <- dat_species_bin +#' \dontrun{ +#' # Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects. #' -#' targets <- Features %>% +#' # Define initial targets for species features. +#' initial_targets <- dat_species_bin %>% #' sf::st_drop_geometry() %>% #' colnames() %>% #' data.frame() %>% #' setNames(c("feature")) %>% #' dplyr::mutate(target = 0.3) #' -#' dat_species_binDF <- dat_species_bin %>% -#' sf::st_drop_geometry() -#' -#' out_sf <- splnr_climate_percentile_preprocess( +#' # Preprocess features to get climate-smart filtered areas. +#' preprocessed_features_percentile <- splnr_climate_percentile_preprocess( #' features = dat_species_bin, #' metric = dat_clim, #' percentile = 35, #' direction = 1 #' ) #' -#' targets <- splnr_climate_percentile_assignTargets( -#' features = dat_species_bin, -#' targets = targets, -#' climateSmartDF = out_sf +#' # Assign targets for the percentile approach. +#' percentile_assigned_targets <- splnr_climate_percentile_assignTargets( +#' features = dat_species_bin, # Original features for 'original' counts +#' targets = initial_targets, +#' climateSmartDF = preprocessed_features_percentile #' ) +#' print(percentile_assigned_targets) +#' } splnr_climate_percentile_assignTargets <- function(features, climateSmartDF, targets) { - spp <- features %>% # Get the list of features - sf::st_drop_geometry() %>% - names() + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(features, "sf"), + msg = "'features' must be an 'sf' object." + ) + assertthat::assert_that( + is.data.frame(targets), + msg = "'targets' must be a data.frame." + ) + assertthat::assert_that( + "feature" %in% names(targets), + msg = "'targets' data.frame must contain a 'feature' column." + ) + assertthat::assert_that( + "target" %in% names(targets), + msg = "'targets' data.frame must contain a 'target' column." + ) + assertthat::assert_that( + inherits(climateSmartDF, "data.frame"), # can be sf or just df after dropping geom + msg = "'climateSmartDF' must be a data.frame (or sf object)." + ) + # Check that column names in climateSmartDF match expected original feature names + # (after removing any potential _filtered suffix from the internal preprocessing) + original_feature_names <- features %>% sf::st_drop_geometry() %>% names() + filtered_feature_names <- climateSmartDF %>% sf::st_drop_geometry() %>% names() + assertthat::assert_that( + all(original_feature_names %in% filtered_feature_names), + msg = "Feature names in 'climateSmartDF' do not match original feature names in 'features'." + ) + + # Get the total number of planning units where each feature is originally present. + # Suppress messages from dplyr::summarize with `suppressMessages`. suppressMessages({ df <- features %>% - sf::st_drop_geometry() %>% - dplyr::mutate_all(~ ifelse(is.na(.), 0, .)) %>% - tibble::as_tibble() %>% - dplyr::summarize(dplyr::across(dplyr::everything(), sum)) %>% # Get the # of planning units where feature is present - tidyr::pivot_longer(tidyselect::everything(), names_to = "feature", values_to = "original") %>% - dplyr::left_join(targets) # %>% - # dplyr::mutate(feature = paste0(.data$feature, "_filtered")) # Change names - + sf::st_drop_geometry() %>% # Drop geometry for aggregation. + dplyr::mutate(dplyr::across(dplyr::everything(), ~ tidyr::replace_na(.x, 0))) %>% # Replace NAs with 0. + tibble::as_tibble() %>% # Convert to tibble for consistent behavior. + dplyr::summarize(dplyr::across(dplyr::everything(), sum)) %>% # Sum up occurrences for each feature. + tidyr::pivot_longer(tidyselect::everything(), names_to = "feature", values_to = "original") %>% # Pivot to long format. + dplyr::left_join(targets, by = "feature") # Join with original targets. + + # Get the total number of planning units selected using the climate-smart filtering. df1 <- climateSmartDF %>% - sf::st_drop_geometry() %>% - dplyr::mutate_all(~ ifelse(is.na(.), 0, .)) %>% - tibble::as_tibble() %>% - dplyr::summarize(dplyr::across(dplyr::everything(), sum)) %>% # Get the # of planning units selected using the climate-smart approach - tidyr::pivot_longer(tidyselect::everything(), names_to = "feature", values_to = "filtered") + sf::st_drop_geometry() %>% # Drop geometry for aggregation. + dplyr::mutate(dplyr::across(dplyr::everything(), ~ tidyr::replace_na(.x, 0))) %>% # Replace NAs with 0. + tibble::as_tibble() %>% # Convert to tibble. + dplyr::summarize(dplyr::across(dplyr::everything(), sum)) %>% # Sum up occurrences of filtered features. + tidyr::pivot_longer(tidyselect::everything(), names_to = "feature", values_to = "filtered") # Pivot to long format. + # Join the original counts with the filtered counts, calculate proportion, and adjust targets. df <- df %>% - dplyr::left_join(df1) %>% - dplyr::mutate(proportion = .data$filtered / .data$original) %>% # Calculating proportion of climate-smart areas over areas where feat is present - dplyr::mutate(target = .data$target / .data$proportion) %>% # Calculate target based on the set target per feature and the proportion - dplyr::select("feature", "target") %>% - # dplyr::mutate(targets = .data$targets / 100) %>% # Convert target to proportions - dplyr::mutate(target = ifelse(.data$target > 1, 1, .data$target)) # Make sure that 100% is the largest target possible + dplyr::left_join(df1, by = "feature") %>% # Join filtered counts. + dplyr::mutate( + # Calculate the proportion of the filtered presence relative to the original presence. + proportion = dplyr::if_else(.data$original > 0, .data$filtered / .data$original, 0), + # Calculate new target: original target divided by proportion. + # Handle cases where proportion might be zero to avoid division by zero. + target = dplyr::if_else(.data$proportion > 0, .data$target / .data$proportion, .data$target) + ) %>% + dplyr::select("feature", "target") %>% # Select only feature and adjusted target. + dplyr::mutate(target = ifelse(.data$target > 1, 1, .data$target)) # Cap targets at 1 (100%). }) return(df) } -#' Function to run the percentile approach +#' @title Run the Percentile Climate-Smart Approach +#' +#' @description +#' `splnr_climate_percentileApproach()` implements the Percentile Approach to +#' climate-smart conservation planning. This involves filtering features to +#' their most climate-resilient areas and adjusting their conservation targets +#' to account for this reduced feature distribution. #' -#' @param features feature `sf`object -#' @param metric climate metric `sf` object with 'metric' as the column name of the metric values per planning unit. -#' @param targets `data.frame`with list of features under "feature" column and their corresponding targets under "target" column -#' @param direction If direction = 1, metric values are from low (least climate-smart) to high (most climate-smart). If direction = -1, metric values are from high (least climate-smart) to low (most climate-smart). -#' @param percentile cut-off threshold for determining whether an area is a climate priority area or not (e.g., lower 35th percentile of warming or upper 65th percentile of acidification). Note that the percentile here is the lower limit of the threshold. +#' @details +#' This function orchestrates the steps for the Percentile Approach: +#' 1. **Preprocessing:** It calls `splnr_climate_percentile_preprocess()` to +#' identify, for each feature, its occurrences within the most climate-resilient +#' `percentile` of its distribution based on a climate metric. This effectively +#' "filters" the feature data to only include its climate-smart components. +#' 2. **Target Assignment:** It then calls `splnr_climate_percentile_assignTargets()` +#' to calculate and assign new targets for these filtered features. The targets +#' are scaled up to ensure that the original conservation goals are still met, +#' but specifically by selecting areas from the climate-smart portions of the +#' features' distributions. #' -#' @return A `list` with two components: 1. is the data frame passed to `prioritizr` when creating a conservation problem containing the binary information per planning unit per feature. 2. are the targets for the features in the conservation problem when the CPA approach is used. +#' The output is a list containing the modified features (filtered to their +#' climate-smart occurrences) and their corresponding adjusted targets, ready +#' to be used in a `prioritizr` conservation problem. +#' +#' @param features An `sf` object representing conservation features (e.g., species +#' distribution data). +#' @param metric An `sf` object containing climate metric information. It must +#' have a column named 'metric' with the climate metric values per planning unit. +#' @param targets A `data.frame` with two columns: `feature` (character, listing +#' the original feature names) and `target` (numeric, the initial conservation +#' target for each feature as a proportion, e.g., 0.3). +#' @param direction An integer specifying the direction of climate-smartness: +#' \itemize{ +#' \item `1`: Higher metric values mean more climate-smart areas. +#' \item `-1`: Lower metric values mean more climate-smart areas. +#' } +#' @param percentile A numeric value (0-100) representing the cutoff threshold for +#' determining whether an area is a climate priority area or not. This is applied +#' *per feature* to its distribution. Defaults to `35`. +#' +#' @return A `list` with two components: +#' \itemize{ +#' \item `Features`: An `sf` object containing the binary information per +#' planning unit for each feature, now filtered to include only its +#' climate-smart occurrences. This is ready to be passed to `prioritizr`. +#' \item `Targets`: A `data.frame` with the adjusted targets for the +#' filtered features. This is also ready for `prioritizr`. +#' } #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr filter mutate select #' @importFrom rlang .data +#' @importFrom sf st_crs #' #' @examples +#' \dontrun{ +#' # Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects +#' # in your package. #' -#' targets <- dat_species_bin %>% +#' # Define initial targets for species features. +#' initial_targets <- dat_species_bin %>% #' sf::st_drop_geometry() %>% #' colnames() %>% #' data.frame() %>% #' setNames(c("feature")) %>% #' dplyr::mutate(target = 0.3) #' -#' Percentile_Approach <- splnr_climate_percentileApproach( +#' # Run the Percentile Approach where higher climate metric values mean +#' # more climate-smart areas. +#' Percentile_Approach_result <- splnr_climate_percentileApproach( #' features = dat_species_bin, #' metric = dat_clim, -#' targets = targets, -#' direction = 1 +#' targets = initial_targets, +#' direction = 1, # Example: higher metric values are more climate-smart +#' percentile = 35 #' ) -#' out_sf <- Percentile_Approach$Features -#' targets <- Percentile_Approach$Targets +#' +#' # Access the processed features and targets: +#' out_sf_percentile <- Percentile_Approach_result$Features +#' targets_percentile <- Percentile_Approach_result$Targets +#' +#' print(head(out_sf_percentile)) +#' print(head(targets_percentile)) +#' } splnr_climate_percentileApproach <- function(features, metric, targets, direction, percentile = 35) { - assertthat::assert_that(inherits(features, "sf"), - inherits(metric, "sf"), - is.data.frame(targets), - "feature" %in% names(targets), - "target" %in% names(targets), - direction %in% c(-1, 1), - is.numeric(percentile), - percentile >= 0 && percentile <= 100) - + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(features, "sf"), + msg = "'features' must be an 'sf' object." + ) + assertthat::assert_that( + inherits(metric, "sf"), + msg = "'metric' must be an 'sf' object." + ) + assertthat::assert_that( + "metric" %in% names(metric), + msg = "'metric' sf object must contain a column named 'metric'." + ) + assertthat::assert_that( + is.data.frame(targets), + msg = "'targets' must be a data.frame." + ) + assertthat::assert_that( + "feature" %in% names(targets), + msg = "'targets' data.frame must contain a 'feature' column." + ) + assertthat::assert_that( + "target" %in% names(targets), + msg = "'targets' data.frame must contain a 'target' column." + ) + assertthat::assert_that( + direction %in% c(-1, 1), + msg = "'direction' must be either 1 (higher metric = more climate-smart) or -1 (lower metric = more climate-smart)." + ) + assertthat::assert_that( + is.numeric(percentile) && length(percentile) == 1 && percentile >= 0 && percentile <= 100, + msg = "'percentile' must be a single numeric value between 0 and 100." + ) + assertthat::assert_that( + sf::st_crs(features) == sf::st_crs(metric), + msg = "CRS of 'features' and 'metric' must be the same." + ) + # Preprocess features to filter them to their climate-smart areas. percentileFeatures <- splnr_climate_percentile_preprocess( features = features, metric = metric, @@ -673,13 +1519,12 @@ splnr_climate_percentileApproach <- function(features, percentile = percentile ) + # Assign adjusted targets for the filtered features. percentileTargets <- splnr_climate_percentile_assignTargets( - features = features, + features = features, # Original features are needed to get original counts. targets = targets, - percentileFeatures + climateSmartDF = percentileFeatures # Use the preprocessed features for target assignment. ) + # Return a list containing both the processed features and their adjusted targets. return(list(Features = percentileFeatures, Targets = percentileTargets)) } - - -##### Penalty Approach ##### diff --git a/R/utils.R b/R/utils.R index 58b4a0d..8d1cbeb 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,242 +1,572 @@ -#' Function for creating polygon +##### Utility Functions #### + +#' @title Create Spatial Polygon from Coordinates +#' +#' @description +#' `splnr_create_polygon()` constructs an `sf` polygon object from a series +#' of longitude and latitude coordinates provided in a tibble. #' -#' `splnr_create_polygon()` allows you to create a polygon based on longitude and latitude coordinates in your input data. +#' @details +#' This utility function simplifies the creation of spatial polygons from a +#' tabular format of coordinates. It takes a tibble where columns 'x' and 'y' +#' represent longitude and latitude, respectively. These coordinates are +#' converted into a matrix, then to an `sf` polygon, and finally to an `sf` +#' object with the specified Coordinate Reference System (CRS). #' -#' @param x A named vector of lon/lat coordinates from which to make an `sf` polygon -#' @param cCRS The CRS to use for the polygon +#' The function assumes that the input coordinates (`x`) are initially in +#' WGS 84 (EPSG:4326) and then transforms them to the `cCRS` if a different +#' CRS is specified. #' -#' @return An `sf` object for the polygon +#' @param x A `tibble` (or `tbl_df`) object with at least two columns, +#' typically named `x` (for longitude) and `y` (for latitude), representing +#' the vertices of the polygon in sequence. The first and last coordinate +#' pair should be the same to form a closed polygon. +#' @param cCRS A character string specifying the target CRS for the output polygon +#' in an EPSG code format (e.g., "EPSG:4326"). Defaults to "EPSG:4326" (WGS 84). +#' +#' @return An `sf` object representing the created polygon, with the specified CRS. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr bind_rows tibble +#' @importFrom sf st_polygon st_sfc st_transform st_sf +#' #' @examples -#' splnr_create_polygon(x = dplyr::tibble(x = seq(-50, 50, by = 1), y = 120) %>% -#' dplyr::bind_rows(dplyr::tibble(x = 50, y = seq(120, 180, by = 1))) %>% -#' dplyr::bind_rows(dplyr::tibble(x = seq(50, -50, by = -1), y = 180)) %>% -#' dplyr::bind_rows(dplyr::tibble(x = -50, y = seq(150, 120, by = -1)))) +#' # Example: Create a simple square polygon +#' square_coords <- dplyr::tibble( +#' x = c(-50, 50, 50, -50, -50), +#' y = c(120, 120, 180, 180, 120) +#' ) +#' simple_polygon <- splnr_create_polygon(x = square_coords) +#' print(simple_polygon) +#' +#' # Example: Create a polygon and transform to a different CRS (e.g., a UTM zone) +#' \dontrun{ +#' # Note: EPSG:32611 is UTM Zone 11N. Ensure it's appropriate for your coordinates. +#' transformed_polygon <- splnr_create_polygon(x = square_coords, cCRS = "EPSG:32611") +#' print(transformed_polygon) +#' } splnr_create_polygon <- function(x, cCRS = "EPSG:4326") { + # Assertions to validate input parameters. assertthat::assert_that( - inherits(x, "tbl_df") && !is.null(attributes(x)), - is.character(cCRS) + inherits(x, "data.frame") && !is.null(x$x) && !is.null(x$y), + msg = "'x' must be a data.frame (or tibble) with 'x' and 'y' columns representing coordinates." + ) + assertthat::assert_that( + is.numeric(x$x) && is.numeric(x$y), + msg = "Coordinates 'x' and 'y' in the input data frame must be numeric." + ) + assertthat::assert_that( + is.character(cCRS) && length(cCRS) == 1, + msg = "'cCRS' must be a single character string specifying the CRS (e.g., 'EPSG:4326')." ) - x <- x %>% + # Convert the input tibble to a matrix, then to a list, which is the required + # format for sf::st_polygon. + # st_polygon expects a list of matrices, where each matrix defines a linear ring. + polygon_matrix <- x %>% as.matrix() %>% - list() %>% - sf::st_polygon() %>% - sf::st_sfc(crs = "EPSG:4326") %>% - sf::st_transform(crs = cCRS) %>% + list() + + # Create an sf polygon object from the matrix, then create an sfc (simple feature column) + # with an initial CRS of EPSG:4326 (WGS 84 assumed for input lat/lon). + polygon_sfc <- sf::st_polygon(polygon_matrix) %>% + sf::st_sfc(crs = "EPSG:4326") + + # Transform the polygon to the target CRS specified by cCRS. + # This is crucial for ensuring the output polygon is in the desired projection. + transformed_polygon_sfc <- polygon_sfc %>% + sf::st_transform(crs = cCRS) + + # Convert the sfc object to an sf (simple features) object, which is a data frame + # with a geometry column. + final_sf_polygon <- transformed_polygon_sfc %>% sf::st_sf() -} + return(final_sf_polygon) +} -#' Remove NAs from spatial data using nearest neighbour +#' @title Remove NAs from Spatial Data Using Nearest Neighbour #' -#' `splnr_replace_NAs()` allows you to replace NA values in your data with the value of the nearest neighbour. -#' The nearest neighbour is determined using `st_nearest_feature()` from the `sf` package. +#' @description +#' `splnr_replace_NAs()` replaces missing (NA) values in a specified column +#' of an `sf` dataframe with the value from the nearest spatial neighbor. #' -#' @param df An `sf` dataframe -#' @param vari Variable to remove NAs from +#' @details +#' This function is useful for imputing missing data in spatial contexts. +#' It identifies all planning units with `NA` values in the `vari` column. +#' For each of these, it finds the geographically closest planning unit that +#' *does not* have an `NA` value in `vari`, and then copies that non-missing +#' value. This approach leverages the spatial autocorrelation often present +#' in environmental and species data. #' -#' @return An `sf` object with NAs replaced with the nearest neighbour +#' The `st_nearest_feature()` function from the `sf` package is used for +#' determining the closest neighbor. +#' +#' @param df An `sf` dataframe. This dataframe must contain a geometry column +#' and the `vari` column with potential NA values. +#' @param vari A character string specifying the name of the column in `df` +#' from which NA values are to be removed and replaced. This column must +#' exist in `df`. +#' +#' @return An `sf` object identical to the input `df`, but with NA values +#' in the `vari` column replaced by values from their nearest non-NA neighbors. +#' If no NAs are found, the original `df` is returned unchanged. #' @export #' -#' @importFrom rlang .data -#' @importFrom rlang := +#' @importFrom assertthat assert_that +#' @importFrom dplyr arrange bind_rows mutate pull select +#' @importFrom rlang .data sym := +#' @importFrom sf st_nearest_feature +#' @importFrom tibble rowid_to_column +#' #' @examples -#' df <- dat_species_prob %>% -#' splnr_replace_NAs("Spp2") +#' \dontrun{ +#' # Assuming 'dat_species_prob' is an existing sf object in your package. +#' # For demonstration, let's artificially introduce some NAs in 'Spp2'. +#' df_with_na <- dat_species_prob %>% +#' dplyr::mutate(Spp2 = ifelse(runif(n()) < 0.2, NA, Spp2)) +#' +#' # Replace NAs in 'Spp2' using nearest neighbor imputation. +#' df_no_na <- splnr_replace_NAs(df = df_with_na, vari = "Spp2") +#' print(sum(is.na(df_no_na$Spp2))) # Should be 0 if successful +#' } splnr_replace_NAs <- function(df, vari) { + # Assertions to validate input parameters. assertthat::assert_that( - inherits(df, c("sf", "data.frame")), - is.character(vari), - vari %in% names(df) + inherits(df, "sf"), # Ensure df is an sf object. + msg = "'df' must be an 'sf' object." + ) + assertthat::assert_that( + is.character(vari) && length(vari) == 1, + msg = "'vari' must be a single character string specifying the column name." + ) + assertthat::assert_that( + vari %in% names(df), + msg = paste0("Column '", vari, "' not found in the input dataframe 'df'.") + ) + assertthat::assert_that( + !is.null(sf::st_geometry(df)), + msg = "'df' must have a geometry column." ) - if (sum(is.na(dplyr::pull(df, !!rlang::sym(vari)))) > 0) { # Check if there are NAs + # Check if there are any NA values in the specified variable. + if (sum(is.na(dplyr::pull(df, !!rlang::sym(vari)))) > 0) { + # Add a unique row ID and a logical column 'isna' to identify NA rows. + # This 'cellID' is crucial for reordering the dataframe correctly at the end. gp <- df %>% - tibble::rowid_to_column("cellID") %>% # Add cellID to reorder df later + tibble::rowid_to_column("cellID") %>% dplyr::mutate(isna = is.na(!!rlang::sym(vari))) + # Split the dataframe into two parts: those with NAs and those without. gp <- split(gp, f = as.factor(gp$isna)) + # Find the nearest feature (row) in the 'FALSE' group (no NAs) for each + # feature (row) in the 'TRUE' group (with NAs). + # 'd' will be a vector of indices corresponding to the nearest non-NA features. d <- sf::st_nearest_feature(gp$`TRUE`, gp$`FALSE`) + # Replace the NA values in the 'TRUE' group with the corresponding values + # from their nearest non-NA neighbors found in the 'FALSE' group. gp$`TRUE` <- gp$`TRUE` %>% dplyr::mutate(!!rlang::sym(vari) := dplyr::pull(gp$`FALSE`, !!rlang::sym(vari))[d]) + # Combine the 'FALSE' group (original non-NAs) and the modified 'TRUE' group (NAs replaced). + # Then, remove the temporary 'isna' and 'cellID' columns and reorder by original 'cellID'. df <- rbind(gp$`FALSE`, gp$`TRUE`) %>% dplyr::select(-"isna") %>% dplyr::arrange(.data$cellID) %>% - dplyr::select(-"cellID") # Remove added column + dplyr::select(-"cellID") # Remove the temporary cellID column. } return(df) } - -#' Substitute numbers for all_names in regionalisations +#' @title Substitute Numbers for Names in Regionalizations +#' +#' @description +#' `splnr_match_names()` replaces numeric or integer values in a spatial +#' (sf) dataframe's column with corresponding character names, typically used +#' for regionalization data. +#' +#' @details +#' This function is designed for scenarios where spatial data contains numeric +#' identifiers for regions, and you have a mapping (a named character vector) +#' to convert these IDs into more descriptive names. It assumes that the `sf` +#' dataframe (`dat`) has only one non-geometry column that needs recoding. #' -#' Many regionalisations have numeric values in the shape files that correspond -#' to a vector of names. Here we provide a function to quickly replace the -#' numbers with names. +#' The function directly applies the mapping from the `nam` vector to the +#' specified column. The names of the `nam` vector should correspond to the +#' numeric/integer values in the `dat` column, and the values of `nam` will +#' be the new character names. #' -#' @param dat `sf` data frame with one column of numeric/integer corresponding to `nam` -#' @param nam Named character vector of names corresponding to column of dat to recode +#' @param dat An `sf` data frame with a single non-geometry column containing +#' numeric or integer values that correspond to the names in `nam`. +#' @param nam A named character vector. The *names* of this vector should be +#' the numeric/integer values found in `dat`'s column, and the *values* of +#' this vector should be the desired character names for substitution. #' -#' @return An `sf` dataframe with numeric regionalisations substituted for category names +#' @return An `sf` dataframe where the numeric/integer values in the relevant +#' column have been substituted with the corresponding character names from `nam`. #' @export #' -#' @importFrom rlang := +#' @importFrom assertthat assert_that +#' @importFrom dplyr mutate +#' @importFrom rlang := sym +#' @importFrom stringr str_subset +#' @importFrom sf st_drop_geometry +#' #' @examples -#' dat <- dat_region -#' nam <- c("Region1" = "SE Aust", "Region2" = "Tas", "Region3" = "NE Aust") -#' df <- splnr_match_names(dat, nam) +#' # Define the named character vector for mapping. +#' region_names <- c("Region1" = "SE Aust", "Region2" = "Tas", "Region3" = "NE Aust") +#' +#' # Apply the function to substitute numeric codes with names. +#' df_named_regions <- splnr_match_names(dat = dat_region, nam = region_names) +#' print(df_named_regions) splnr_match_names <- function(dat, nam) { + # Assertions to validate input parameters. assertthat::assert_that( inherits(dat, "sf"), - is.character(nam) && length(nam) > 0 + msg = "'dat' must be an 'sf' object." + ) + assertthat::assert_that( + is.character(nam) && length(nam) > 0, + msg = "'nam' must be a non-empty character vector." + ) + assertthat::assert_that( + !is.null(names(nam)), + msg = "'nam' must be a named character vector (e.g., c('1' = 'Name1'))." + ) + assertthat::assert_that( + length(colnames(dat %>% sf::st_drop_geometry())) == 1, + msg = "'dat' must contain exactly one non-geometry column to be recoded." + ) + # assertthat::assert_that( + # is.numeric(dat %>% sf::st_drop_geometry() %>% dplyr::pull(1)) || is.integer(dat %>% sf::st_drop_geometry() %>% dplyr::pull(1)), + # msg = "The non-geometry column in 'dat' must be numeric or integer." + # ) + assertthat::assert_that( + all(as.character(unique(dat %>% sf::st_drop_geometry() %>% dplyr::pull(1))) %in% names(nam)), + msg = "Not all unique numeric/integer values in 'dat's recoding column are present as names in 'nam'." ) + + # Identify the name of the single non-geometry column that needs recoding. col_name <- stringr::str_subset(colnames(dat), "geometry", negate = TRUE)[[1]] + # Use dplyr::mutate to replace the numeric/integer values in 'col_name' + # with the corresponding names from the 'nam' vector. + # The `!!rlang::sym(col_name)` syntax ensures that 'col_name' is treated as a variable. out <- dat %>% - dplyr::mutate(!!col_name := nam[!!rlang::sym(col_name)]) # Apply categories to data -} + dplyr::mutate(!!col_name := nam[as.character(!!rlang::sym(col_name))]) # Convert col_name content to character to match names(nam) + return(out) +} -#' Scale spatial layers to between 0 and 1 +#' @title Scale Spatial Layers to Between 0 and 1 #' -#' `splnr_scale_01()` allows you to re-scale your data from values that are greater than 1 to values that are between 0 and 1. +#' @description +#' `splnr_scale_01()` re-scales the numeric values in a specified column of an +#' `sf` dataframe to a range between 0 and 1. This is particularly useful for +#' normalizing data like probabilities or costs. #' -#' @param dat `sf` dataframe -#' @param col_name Name of the column to scale +#' @details +#' This function inspects the maximum value (`mx`) in the `col_name` column. +#' It then divides all values in that column by a `divi` factor to bring them +#' into the 0-1 range. The `divi` factor is determined heuristically: +#' - If `mx > 100`, `divi` is `1000`. +#' - If `mx > 10`, `divi` is `100`. +#' - If `mx > 1`, `divi` is `10`. +#' - If `mx <= 1`, no division is performed (`divi` is `1`), as the data is +#' already within the desired range. #' -#' @return `sf` dataframe +#' This approach ensures that the data is scaled appropriately without +#' hardcoding a fixed division factor. +#' +#' @param dat An `sf` dataframe containing the column to be scaled. +#' @param col_name A character string specifying the name of the numeric column +#' in `dat` that needs to be scaled. +#' +#' @return An `sf` dataframe identical to the input `dat`, but with the values +#' in the `col_name` column re-scaled to be between 0 and 1. #' @export #' -#' @importFrom rlang := +#' @importFrom assertthat assert_that +#' @importFrom dplyr mutate pull +#' @importFrom rlang := sym #' #' @examples -#' df <- dat_species_prob %>% -#' dplyr::mutate(Spp1 = Spp1 * 100) %>% -#' splnr_scale_01(col_name = "Spp1") +#' \dontrun{ + +#' # Scale the 'Spp1' column. +#' df_scaled_spp1 <- splnr_scale_01(dat = dat_species_prob, col_name = "Spp1") +#' print(df_scaled_spp1) +#' +#' # Example where max is already <= 1 +#' df_already_scaled <- dat_species_prob %>% dplyr::mutate(Spp1 = Spp1 / 100) +#' df_no_change <- splnr_scale_01(dat = df_already_scaled, col_name = "Spp1") +#' print(df_no_change) # Spp1 values should remain unchanged +#' } splnr_scale_01 <- function(dat, col_name) { + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(dat, "sf"), # Ensure dat is an sf object. + msg = "'dat' must be an 'sf' object." + ) assertthat::assert_that( - inherits(dat, c("sf", "data.frame")), - is.character(col_name), - col_name %in% names(dat) + is.character(col_name) && length(col_name) == 1, + msg = "'col_name' must be a single character string." + ) + assertthat::assert_that( + col_name %in% names(dat), + msg = paste0("Column '", col_name, "' not found in the input dataframe 'dat'.") + ) + assertthat::assert_that( + is.numeric(dplyr::pull(dat, !!rlang::sym(col_name))), + msg = paste0("Column '", col_name, "' must be numeric to be scaled.") ) - mx <- max(dplyr::pull(dat, !!rlang::sym(col_name)), na.rm = TRUE) # Get max probability + # Get the maximum value in the specified column, ignoring NA values. + mx <- max(dplyr::pull(dat, !!rlang::sym(col_name)), na.rm = TRUE) + # Determine the division factor based on the maximum value. + # This logic tries to scale the data into the 0-1 range. + divi <- 1 # Default: no division if max is already 1 or less. if (mx > 100) { divi <- 1000 } else if (mx > 10) { divi <- 100 } else if (mx > 1) { divi <- 10 - } else if (mx < 1) { - divi <- 1 # Do nothing } + # Apply the scaling to the specified column. dat <- dat %>% dplyr::mutate(!!col_name := !!rlang::sym(col_name) / divi) -} - + return(dat) +} -#' Returns the feature names +#' @title Extract Feature Names from Spatial Data +#' +#' @description +#' `splnr_featureNames()` extracts the names of conservation features +#' from an `sf` dataframe, excluding geometry and any specified columns. +#' +#' @details +#' This function is a utility for preparing data for `prioritizr` or other +#' conservation planning packages that require a vector of feature names. +#' It typically removes the geometry column and any columns related to cost +#' (prefixed with "Cost_") by default, allowing you to specify additional +#' columns to exclude. #' -#' `splnr_featureNames()` allows you to extract the names of features you want to pass to a `prioritizr` prioritization. -#' It requires an `sf` object input and returns the column names of the object excluding any columns you specify in the `exclude` argument. +#' The output is a simple character vector of column names, which can be +#' directly used as feature identifiers in conservation problems. #' -#' @param dat sf dataframe of features -#' @param exclude Character vector of any columns to exclude +#' @param dat An `sf` dataframe representing conservation features. Each +#' non-geometry column is assumed to be a feature. +#' @param exclude A character vector of column names (or prefixes) to exclude +#' from the output. By default, it excludes columns starting with "Cost_". +#' If you provide a value, it will be *appended* to the default exclusion. +#' Set to `NULL` or `character(0)` if you want no exclusions beyond the default. #' -#' @return A character vector of names +#' @return A character vector containing the names of the conservation features. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr select +#' @importFrom sf st_drop_geometry +#' @importFrom tidyselect starts_with +#' #' @examples -#' df <- dat_species_prob %>% -#' splnr_featureNames() +#' \dontrun{ +#' # Assuming 'dat_species_prob' is an existing sf object in your package. +#' # It likely has columns like 'Spp1', 'Spp2', 'Cost_SomeMeasure', etc. +#' +#' # Example 1: Get all feature names, excluding default 'Cost_' columns. +#' feature_names_default <- splnr_featureNames(dat = dat_species_prob) +#' print(feature_names_default) +#' +#' # Example 2: Get feature names, excluding 'Cost_' columns and 'Spp5'. +#' feature_names_custom_exclude <- splnr_featureNames( +#' dat = dat_species_prob, +#' exclude = "Spp5" +#' ) +#' print(feature_names_custom_exclude) +#' +#' # Example 3: If you only want to exclude a specific column and not 'Cost_' +#' # (you'd need to manually specify exclude = "geometry" and then your column) +#' # This case is more complex and usually handled by direct dplyr::select. +#' # This function's primary use is to remove cost columns and potentially others. +#' } splnr_featureNames <- function(dat, exclude = NA) { + # Assertions to validate input parameters. assertthat::assert_that( inherits(dat, "sf"), - is.character(exclude) || is.na(exclude) + msg = "'dat' must be an 'sf' object." + ) + assertthat::assert_that( + is.character(exclude) || is.na(exclude), + msg = "'exclude' must be a character vector or NA." + ) + assertthat::assert_that( + !is.null(sf::st_geometry(dat)), + msg = "'dat' must have a geometry column." ) - if (is.na(exclude)) { - exclude <- c("Cost_") + # Define columns to exclude. Always start with "Cost_" as a default. + if (all(is.na(exclude))) { # Check if `exclude` is literally `NA` (the default) + exclude_cols <- c("Cost_") } else { - exclude <- c("Cost_", exclude) + # If `exclude` is provided and not NA, append it to "Cost_". + # Ensure it's a character vector and handle potential NULL/empty cases. + exclude_cols <- c("Cost_", exclude[is.na(exclude) == FALSE]) } - dat <- dat %>% + # Drop geometry, then select columns that do NOT start with any of the + # prefixes in `exclude_cols`, and finally get the column names. + feature_names <- dat %>% sf::st_drop_geometry() %>% - dplyr::select(-tidyselect::starts_with(exclude)) %>% + dplyr::select(-tidyselect::starts_with(exclude_cols)) %>% colnames() - return(dat) + return(feature_names) } - -#' Ensure all features are in the same order. +#' @title Arrange Features by Spatial Coordinates +#' +#' @description +#' `splnr_arrangeFeatures()` sorts the rows of an `sf` object based on the +#' longitude (X) and then latitude (Y) of its centroids. This ensures a +#' consistent ordering of planning units, which can be important for +#' reproducibility in some spatial analyses or data processing steps. #' -#' `splnr_arrangeFeatures()` sorts your data based on longitude and latitude values. +#' @details +#' This function computes the centroid for each polygon (or point/multipoint) +#' in the input `sf` object. It then extracts the X and Y coordinates of these +#' centroids and uses them to sort the entire `sf` object. The primary sort key +#' is the longitude (X-coordinate), and the secondary sort key is the latitude +#' (Y-coordinate). #' -#' @param df An sf object to sort by Lon and Lat +#' Sorting can be beneficial for tasks like debugging, comparing data from +#' different runs, or ensuring deterministic behavior in algorithms that +#' process spatial units sequentially. #' -#' @return A sorted sf object +#' @param df An `sf` object whose rows are to be sorted. +#' +#' @return A sorted `sf` object, with rows ordered primarily by longitude (X) +#' and secondarily by latitude (Y) of their centroids. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom sf st_centroid st_coordinates +#' #' @examples -#' df <- dat_species_prob %>% -#' splnr_arrangeFeatures() +#' \dontrun{ + +#' print("Original order:") +#' print(dat_species_prob) +#' +#' # Sort the features. +#' df_arranged <- splnr_arrangeFeatures(df = dat_species_prob) +#' print("Sorted order:") +#' print(df_arranged) +#' } splnr_arrangeFeatures <- function(df) { - assertthat::assert_that(inherits(df, "sf"), - msg = "Input must be an sf object.") - # Sort rows to ensure all features are in the same order. - suppressWarnings( - xy <- sf::st_coordinates(sf::st_centroid(df)) + # Assertions to validate input parameters. + assertthat::assert_that( + inherits(df, "sf"), + msg = "'df' must be an 'sf' object." + ) + assertthat::assert_that( + !is.null(sf::st_geometry(df)), + msg = "'df' must have a geometry column." + ) + assertthat::assert_that( + nrow(df) > 0, + msg = "'df' must not be empty." ) - df <- df[order(xy[, "X"], xy[, "Y"]), ] -} + # Calculate the centroids of the geometries. + # suppressWarnings is used here because st_centroid can sometimes issue warnings + # for complex geometries (e.g., empty geometries or geometries spanning the antimeridian) + # which might not be relevant for simple sorting. + suppressWarnings({ + centroids <- sf::st_centroid(df) + # Extract the X and Y coordinates from the centroids. + xy <- sf::st_coordinates(centroids) + }) + # Order the input dataframe based on the X (longitude) and then Y (latitude) + # coordinates of its centroids. This provides a deterministic sort order. + df_sorted <- df[order(xy[, "X"], xy[, "Y"]), ] + return(df_sorted) +} -#' Prepare data to plot Cohen's Kappa correlation matrix -#' -#' Conservation planning often requires the comparison of the outputs of the solutions of different conservation problems. -#' One way to compare solutions is by correlating the solutions using Cohen's Kappa. -#' `splnr_get_kappaCorrData()` takes a list of `prioritizr` solutions to perform the Cohen's Kappa correlation between the solution. -#' The resulting correlation matrix is symmetrical along the main diagonal and contains Cohen's Kappa of pairwise correlation between the solutions. -#' The main diagonal should always be 1. The correlation matrix obtained from this function can be passed onto [splnr_plot_corrMat()]. -#' -#' @param sol List of `prioritizr` solutions (`sf` objects) with solutions having a column name `solution_1` -#' @param name_sol Name tags to the different solutions -#' -#' @return `matrixOut` matrix +#' @title Prepare Data to Plot Cohen's Kappa Correlation Matrix +#' +#' @description +#' `splnr_get_kappaCorrData()` calculates Cohen's Kappa correlation coefficients +#' between a list of `prioritizr` conservation solutions. The output is a +#' symmetrical matrix suitable for visualizing pairwise agreement using a heatmap. +#' +#' @details +#' This function is essential for assessing the similarity or divergence among +#' different conservation plans. It takes a list of `prioritizr` solution objects, +#' each expected to contain a binary column named `solution_1` (indicating +#' selected or unselected planning units). +#' +#' For every unique pair of solutions in the input list, it computes Cohen's Kappa +#' using the `irr::kappa2()` function. Cohen's Kappa measures the agreement +#' between two raters (in this case, two conservation solutions) for categorical +#' items, correcting for chance agreement. A Kappa value of 1 indicates perfect +#' agreement, 0 indicates agreement equivalent to chance, and negative values +#' indicate agreement worse than chance. +#' +#' The resulting matrix is symmetrical, with diagonal elements always equal to 1 +#' (a solution perfectly agrees with itself). This matrix can then be passed to +#' visualization functions like `splnr_plot_corrMat()` to create a correlation heatmap. +#' +#' @param sol A `list` of `prioritizr` solution objects. Each element in the list +#' must be an `sf` object containing a binary column named `solution_1`. +#' @param name_sol A character vector providing descriptive names for each +#' solution in the `sol` list. The length of this vector must match the +#' length of `sol`. These names will be used as row and column names in the +#' output correlation matrix. +#' +#' @return A numeric `matrix` (`matrixOut`) representing the Cohen's Kappa +#' correlation matrix between all pairs of solutions. Rows and columns are +#' named according to `name_sol`. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr bind_cols select #' @importFrom rlang .data +#' @importFrom stats setNames +#' @importFrom tibble as_tibble +#' @importFrom tidyr pivot_wider #' #' @examples -#' # 30 % target for problem/solution 1 -#' dat_problem <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +#' \dontrun{ +#' # Assuming 'dat_species_bin' is an existing sf object in your package. +#' +#' # Create a dummy prioritizr problem and solve it for solution 1 (30% target). +#' dat_problem1 <- prioritizr::problem( +#' dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), #' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" #' ) %>% @@ -245,10 +575,10 @@ splnr_arrangeFeatures <- function(df) { #' prioritizr::add_binary_decisions() %>% #' prioritizr::add_default_solver(verbose = FALSE) #' -#' dat_soln <- dat_problem %>% +#' dat_soln1 <- dat_problem1 %>% #' prioritizr::solve.ConservationProblem() #' -#' # 50 % target for problem/solution 2 +#' # Create another dummy prioritizr problem and solve it for solution 2 (50% target). #' dat_problem2 <- prioritizr::problem( #' dat_species_bin %>% #' dplyr::mutate(Cost = runif(n = dim(.)[[1]])), @@ -263,76 +593,168 @@ splnr_arrangeFeatures <- function(df) { #' dat_soln2 <- dat_problem2 %>% #' prioritizr::solve.ConservationProblem() #' -#' corrMat <- splnr_get_kappaCorrData(list(dat_soln, dat_soln2), name_sol = c("soln1", "soln2")) +#' # Calculate the Cohen's Kappa correlation matrix between the two solutions. +#' corrMat <- splnr_get_kappaCorrData( +#' sol = list(dat_soln1, dat_soln2), +#' name_sol = c("Solution_A_30pct", "Solution_B_50pct") +#' ) +#' print(corrMat) +#' +#' # This output can then be directly passed to splnr_plot_corrMat(). +#' # splnr_plot_corrMat(corrMat, AxisLabels = c("Sol A (30%)", "Sol B (50%)")) +#' } splnr_get_kappaCorrData <- function(sol, name_sol) { + # Assertions to validate input parameters. assertthat::assert_that( is.list(sol), + msg = "'sol' must be a list of prioritizr solution objects." + ) + assertthat::assert_that( length(sol) > 1, - is.character(name_sol), - length(name_sol) == length(sol) + msg = "'sol' list must contain at least two solutions for correlation." ) + assertthat::assert_that( + is.character(name_sol) && length(name_sol) > 0, + msg = "'name_sol' must be a non-empty character vector." + ) + assertthat::assert_that( + length(name_sol) == length(sol), + msg = "The length of 'name_sol' must match the number of solutions in 'sol'." + ) + # Check each solution in the list for 'solution_1' column and sf class + for (i in seq_along(sol)) { + assertthat::assert_that( + inherits(sol[[i]], "sf"), + msg = paste0("Element ", i, " in 'sol' is not an 'sf' object.") + ) + assertthat::assert_that( + "solution_1" %in% names(sol[[i]]), + msg = paste0("Solution ", i, " in 'sol' is missing the 'solution_1' column.") + ) + } + # Check if 'irr' package is installed. If not, stop with an informative error. if (requireNamespace("irr", quietly = TRUE) == FALSE){ - stop("To run splnr_get_kappaCorrData you will need to install the package irr.") + stop("To run splnr_get_kappaCorrData you will need to install the 'irr' package: install.packages('irr').") } + # Prepare a list of solutions, selecting only the 'solution_1' column and renaming it + # with the provided 'name_sol'. Each element will be a tibble with one column. s_list <- lapply(seq_along(sol), function(x) { sol[[x]] %>% - tibble::as_tibble(.name_repair = "unique") %>% - dplyr::select("solution_1") %>% - stats::setNames(name_sol[[x]]) + tibble::as_tibble(.name_repair = "unique") %>% # Convert to tibble, handling duplicate names. + dplyr::select("solution_1") %>% # Select the binary solution column. + stats::setNames(name_sol[[x]]) # Rename the column to the provided solution name. }) + # Initialize a list to store pairwise kappa results. + # 'y' is a counter for storing results in s_matrix. y <- 1 s_matrix <- list() + # Loop through all unique pairs of solutions (including self-correlation). for (i in 1:length(s_list)) { for (j in 1:length(s_list)) { - kappa_temp <- irr::kappa2(dplyr::bind_cols(s_list[[i]], s_list[[j]])) - kappa_corrvalue <- kappa_temp$value - kappa_pvalue <- kappa_temp$p.value + # Combine the two solutions (as single-column tibbles) side-by-side. + combined_solutions <- dplyr::bind_cols(s_list[[i]], s_list[[j]]) + # Calculate Cohen's Kappa between the two solutions. + kappa_temp <- irr::kappa2(combined_solutions) + kappa_corrvalue <- kappa_temp$value # Extract the Kappa value. + kappa_pvalue <- kappa_temp$p.value # Extract the p-value (though not used in final output matrix). + + # Store the pair's names, kappa value, and p-value. s_matrix[[y]] <- cbind(colnames(s_list[[i]]), colnames(s_list[[j]]), kappa_corrvalue, kappa_pvalue) - y <- y + 1 + y <- y + 1 # Increment counter. } } + # Combine all pairwise results into a single tibble. s_matrix_all <- do.call(rbind, s_matrix) %>% tibble::as_tibble(.name_repair = "unique") + # Rename the first two columns to be more descriptive. colnames(s_matrix_all)[1:2] <- c("plan1", "plan2") + # Transform the long-format results into a wide-format correlation matrix. matrix_final <- s_matrix_all %>% tibble::as_tibble(.name_repair = "unique") %>% - dplyr::select(-kappa_pvalue) %>% - tidyr::pivot_wider(names_from = "plan2", values_from = kappa_corrvalue) %>% - as.matrix() + dplyr::select(-.data$kappa_pvalue) %>% # Remove p-value column as it's not needed for the correlation matrix plot. + tidyr::pivot_wider(names_from = "plan2", values_from = "kappa_corrvalue") %>% # Pivot to make 'plan2' names as new columns. + as.matrix() # Convert to matrix format. - matrix_x <- s_matrix_all %>% - tibble::as_tibble(.name_repair = "unique") + # Note: matrix_x was part of original code but not used for the return value. + # matrix_x <- s_matrix_all %>% + # tibble::as_tibble(.name_repair = "unique") - # creating corrplot + # Set row names of the final matrix for clarity. rownames(matrix_final) <- matrix_final[, 1] - n <- length(s_list) + 1 # 4 is the number of inputted scenarios - matrixOut <- matrix_final[, 2:n] - class(matrixOut) <- "numeric" + # Determine the number of columns to select (number of solutions + 1 for the first column). + # The original code's 'n <- length(s_list) + 1' implies the first column is the row names, + # so it selects from the second column onwards up to 'n'. + n_cols_to_select <- length(s_list) + 1 + matrixOut <- matrix_final[, 2:n_cols_to_select] # Select only the numeric correlation values. + class(matrixOut) <- "numeric" # Ensure the matrix is of numeric class. return(matrixOut) } -#' Prepare data to plot Selection Frequency of planning units -#' -#' When multiple spatial plans are generated, we are often interested in how many times a planning unit is selected across an array of solutions. This array can either be a `list` of the solutions of different conservation problems or generated through a [portfolio approach](https://prioritizr.net/reference/portfolios.html) with `prioritizr`. -#' `splnr_get_selFreq()` allows you to calculate the selection frequency of each planning unit of either a `list` or a `portfolio` of solutions. The resulting `sf` object can be passed for visualization to the `spatialplanr` function [splnr_plot_selectionFreq()]. -#' -#' @param solnMany List or portfolio of `prioritizr` solutions -#' @param type Either "portfolio" (`sf` object) with a portfolio produced using `prioritizr` or "list" with a list of solutions -#' -#' @return `selFreq` `sf` object containing a column with the selection frequency (sum over all solutions). +#' @title Prepare Data to Plot Selection Frequency of Planning Units +#' +#' @description +#' `splnr_get_selFreq()` calculates how many times each planning unit is +#' selected across an array of `prioritizr` solutions. This "selection +#' frequency" can be derived from either a list of individual solutions or +#' a `prioritizr` portfolio object. +#' +#' @details +#' Understanding selection frequency is crucial for identifying robust +#' conservation areas—those that are consistently chosen across multiple +#' planning scenarios or alternative optimal solutions. +#' +#' The function supports two types of input: +#' \itemize{ +#' \item `"portfolio"`: If `solnMany` is a single `sf` object representing a +#' portfolio of solutions (e.g., generated by `prioritizr::add_cuts_portfolio()`). +#' In this case, the function assumes columns starting with "solution_" +#' represent individual solutions within the portfolio. +#' \item `"list"`: If `solnMany` is a `list` where each element is an `sf` +#' object representing a single `prioritizr` solution (each with a +#' "solution_1" column). +#' } +#' For both types, the function sums the binary `solution` values (0 or 1) +#' across all solutions for each planning unit. The result is converted to a +#' factor to represent discrete frequency levels. +#' +#' The output `sf` object can then be passed to `splnr_plot_selectionFreq()` +#' for visualization as a heatmap. +#' +#' @param solnMany A `list` of `prioritizr` solutions (if `type = "list"`) +#' or a single `sf` object representing a `prioritizr` portfolio of solutions +#' (if `type = "portfolio"`). Each individual solution must contain a +#' column named `solution_1`. +#' @param type A character string indicating the input type: `"portfolio"` +#' (for a single `sf` object with multiple solution columns) or `"list"` +#' (for a list of single-solution `sf` objects). Defaults to `"portfolio"`. +#' +#' @return An `sf` object (`selFreq`) containing a column named `selFreq`. +#' This column is a factor representing the selection frequency (sum of +#' selected occurrences across all solutions) for each planning unit. #' @export #' +#' @importFrom assertthat assert_that +#' @importFrom dplyr mutate select starts_with #' @importFrom rlang .data +#' @importFrom sf st_as_sf st_drop_geometry +#' @importFrom stringr str_c str_pad +#' @importFrom stats setNames +#' @importFrom tibble as_tibble #' #' @examples -#' dat_problem <- prioritizr::problem(dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +#' \dontrun{ +#' # Assuming 'dat_species_bin' is an existing sf object in your package. +#' +#' # Create a base prioritizr problem. +#' dat_problem <- prioritizr::problem( +#' dat_species_bin %>% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), #' features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), #' cost_column = "Cost" #' ) %>% @@ -341,59 +763,117 @@ splnr_get_kappaCorrData <- function(sol, name_sol) { #' prioritizr::add_binary_decisions() %>% #' prioritizr::add_default_solver(verbose = FALSE) #' -#' dat_soln <- dat_problem %>% -#' prioritizr::solve.ConservationProblem() -#' -#' # create conservation problem that contains a portfolio of solutions +#' # --- Example 1: Using a portfolio of solutions --- +#' # Create a conservation problem that contains a portfolio of solutions (e.g., 5 solutions). #' dat_soln_portfolio <- dat_problem %>% #' prioritizr::add_cuts_portfolio(number_solutions = 5) %>% #' prioritizr::solve.ConservationProblem() #' -#' selFreq <- splnr_get_selFreq(solnMany = dat_soln_portfolio, type = "portfolio") -#' (splnr_plot_selectionFreq(selFreq)) -#' +#' # Calculate selection frequency from the portfolio. +#' selFreq_portfolio <- splnr_get_selFreq(solnMany = dat_soln_portfolio, type = "portfolio") +#' print(head(selFreq_portfolio)) +#' # You can then plot this: splnr_plot_selectionFreq(selFreq_portfolio) +#' +#' # --- Example 2: Using a list of individual solutions --- +#' # Solve the problem multiple times to get different solutions (e.g., by randomizing costs) +#' dat_soln_list <- list( +#' dat_problem %>% prioritizr::solve.ConservationProblem(), +#' dat_problem %>% +#' dplyr::mutate(Cost = runif(n = dim(.)[[1]])) %>% # Vary cost for a different solution +#' prioritizr::solve.ConservationProblem(), +#' dat_problem %>% +#' dplyr::mutate(Cost = runif(n = dim(.)[[1]])) %>% # Another different solution +#' prioritizr::solve.ConservationProblem() +#' ) +#' +#' # Calculate selection frequency from the list of solutions. +#' selFreq_list <- splnr_get_selFreq(solnMany = dat_soln_list, type = "list") +#' print(head(selFreq_list)) +#' # You can then plot this: splnr_plot_selectionFreq(selFreq_list) +#' } splnr_get_selFreq <- function(solnMany, type = "portfolio") { + # Assertions to validate input parameters. + assertthat::assert_that( + is.character(type) && length(type) == 1, + msg = "'type' must be a single character string: 'portfolio' or 'list'." + ) assertthat::assert_that( type %in% c("portfolio", "list"), - (type == "portfolio" && inherits(solnMany, "sf")) || (type == "list" && is.list(solnMany)), - (!is.null(solnMany) && length(solnMany) > 0) + msg = "'type' must be either 'portfolio' or 'list'." ) - if (type == "portfolio") { # check if provided input is a protfolio - - if (class(solnMany)[[1]] != "sf") { - print("You did not provide a portfolio of solutions. Please check your input.") - } else { - selFreq <- solnMany %>% - dplyr::mutate(selFreq = as.factor(rowSums(dplyr::select(tibble::as_tibble(solnMany), dplyr::starts_with("solution_"))))) %>% - sf::st_as_sf(geometry = solnMany$geometry) %>% - dplyr::select("selFreq") - return(selFreq) - } - } else if (type == "list") { # if not portfolio check if input is list of solutions - if (class(solnMany)[[1]] != "list") { - print("You did not provide a list of solutions. Please check your input.") - } else { - name_sol <- stringr::str_c("soln", stringr::str_pad(1:length(solnMany), width = 1, pad = 0)) - - s_list <- lapply(seq_along(solnMany), function(x) { - solnMany[[x]] %>% - tibble::as_tibble() %>% - dplyr::select("solution_1") %>% - stats::setNames(name_sol[[x]]) - }) - - soln <- data.frame(matrix(unlist(s_list), ncol = length(s_list))) - colnames(soln) <- name_sol - - selFreq <- soln %>% - dplyr::mutate(selFreq = as.factor(rowSums(dplyr::select(soln, dplyr::starts_with("soln"))))) %>% - sf::st_as_sf(geometry = solnMany[[1]]$geometry) %>% - dplyr::select("selFreq") - return(selFreq) + if (type == "portfolio") { + # If type is "portfolio", expected input is a single sf object (the portfolio). + assertthat::assert_that( + inherits(solnMany, "sf"), + msg = "For 'type = \"portfolio\"', 'solnMany' must be an 'sf' object." + ) + assertthat::assert_that( + any(grepl("solution_", names(solnMany))), + msg = "For 'type = \"portfolio\"', 'solnMany' must contain columns starting with 'solution_'." + ) + + # Calculate selection frequency for a portfolio (sf object with multiple solution columns). + selFreq <- solnMany %>% + # Convert to tibble for dplyr operations on columns, ensuring unique names. + dplyr::mutate(selFreq = as.factor(rowSums(dplyr::select(tibble::as_tibble(.), + dplyr::starts_with("solution_")), na.rm = TRUE))) %>% + # Convert back to sf, explicitly retaining the original geometry. + sf::st_as_sf(geometry = sf::st_geometry(solnMany)) %>% + # Select only the calculated selection frequency column. + dplyr::select("selFreq") + return(selFreq) + + } else if (type == "list") { + # If type is "list", expected input is a list of sf objects (individual solutions). + assertthat::assert_that( + is.list(solnMany), + msg = "For 'type = \"list\"', 'solnMany' must be a list of sf objects." + ) + assertthat::assert_that( + length(solnMany) > 0, + msg = "For 'type = \"list\"', 'solnMany' must not be an empty list." + ) + # Check each element in the list. + for (i in seq_along(solnMany)) { + assertthat::assert_that( + inherits(solnMany[[i]], "sf"), + msg = paste0("Element ", i, " in 'solnMany' is not an 'sf' object.") + ) + assertthat::assert_that( + "solution_1" %in% names(solnMany[[i]]), + msg = paste0("Solution ", i, " in 'solnMany' is missing the 'solution_1' column.") + ) } + + # Generate default names for solutions in the list for column naming. + name_sol <- stringr::str_c("soln", stringr::str_pad(1:length(solnMany), width = 1, pad = "0")) + + # Process each solution in the list: select 'solution_1' and rename. + s_list <- lapply(seq_along(solnMany), function(x) { + solnMany[[x]] %>% + tibble::as_tibble() %>% # Convert to tibble. + dplyr::select("solution_1") %>% # Select the solution column. + stats::setNames(name_sol[[x]]) # Rename it to the generated name. + }) + + # Combine all single-column solution tibbles into one wide data frame. + soln <- data.frame(matrix(unlist(s_list), ncol = length(s_list))) + colnames(soln) <- name_sol + + # Calculate selection frequency by summing binary solution columns. + selFreq <- soln %>% + # Sum across all columns starting with "soln" (which are our individual solutions). + dplyr::mutate(selFreq = as.factor(rowSums(dplyr::select(., dplyr::starts_with("soln")), na.rm = TRUE))) %>% + # Convert back to sf, using the geometry from the first solution in the list. + sf::st_as_sf(geometry = sf::st_geometry(solnMany[[1]])) %>% + # Select only the calculated selection frequency column. + dplyr::select("selFreq") + return(selFreq) + } else { - print("This function requires either a prioritizr portfolio or a list of solutions. Please check your input.") + # This block should technically not be reached due to initial assertthat. + stop("This function requires either a prioritizr portfolio or a list of solutions. Please check your input.") } } diff --git a/README.Rmd b/README.Rmd index c447afa..0cef93c 100644 --- a/README.Rmd +++ b/README.Rmd @@ -13,7 +13,7 @@ knitr::opts_chunk$set( ) ``` -# spatialplanr spatialplanr website +# spatialplanr spatialplanr website [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) @@ -24,8 +24,9 @@ knitr::opts_chunk$set( [![Issues](https://img.shields.io/github/issues/SpatialPlanning/spatialplanr)](https://github.com/SpatialPlanning/spatialplanr/issues) -## Overview -This package is designed to assist students and staff in the [Mathematical Marine Ecology Lab](https://mathmarecol.github.io) at the University of Queensland. It may be useful for others as well. This code has been written to simplify the process for running a _prioritizr_ analysis on a given region use the workflows and data of the MME Lab. It is still a work in progress so feel free to submit pull requests with new features and code improvements. + +# Introduction to spatialplanr +Welcome to _spatialplanr_, an R package designed to streamline and enhance spatial conservation prioritization efforts by explicitly integrating climate change considerations. Building upon the powerful _prioritizr_ package, _spatialplanr_ provides a suite of tools for conservation planners to develop more robust and climate-resilient protected area networks. ## Installation @@ -35,3 +36,336 @@ Be aware that this package is in the very early stages of development. Functions # install.packages("devtools") devtools::install_github("https://github.com/SpatialPlanning/spatialplanr") ``` + +# Purpose and Goals + +The escalating impacts of climate change necessitate a paradigm shift in how we approach conservation. Traditional conservation planning often focuses on static biodiversity patterns, which may not adequately account for the dynamic nature of species distributions and ecosystem processes under a changing climate. _spatialplanr_ aims to address this gap by: + +**Facilitating Climate-Smart Planning**: Providing functions that incorporate climate data directly into the planning process, allowing for the identification of areas critical for both biodiversity and resilience to future climate conditions. +Offering Diverse Methodological Approaches: Implementing multiple established climate-smart conservation planning frameworks (e.g., Climate Priority Areas, Climate Features, Climate Percentiles) to offer flexibility based on specific planning goals and data availability. + +**Streamlining Workflow**: Offering end-to-end functionality, from data preprocessing and integration to advanced visualization of results, simplifying complex analytical tasks for users. + +**Enhancing Decision-Making**: Producing outputs that directly feed into spatial prioritization software like prioritizr, enabling the generation of optimal conservation solutions that balance biodiversity representation with climate resilience. +By using spatialplanr, practitioners can move beyond reactive conservation and proactively design protected area systems better equipped to safeguard biodiversity in a rapidly changing world. + +# Core Climate-Smart Planning Approaches + +_spatialplanr_ implements several key approaches for integrating climate change into spatial prioritization, largely drawing inspiration from frameworks like that presented in Buenafe et al. (2023) "A metric‐based framework for climate‐smart conservation planning" (DOI: 10.1002/eap.2852). + +These approaches are designed to transform biodiversity features and conservation targets based on climate metrics, allowing for a more nuanced understanding of how climate change may impact conservation priorities. The main approaches included in _spatialplanr_ are: + * **Climate Priority Area (CPA) Approach**: Identifies climate-smart areas within the distribution of each conservation feature, creating new components for climate-smart and non-climate-smart areas. + * **Climate Feature Approach**: Treats climate-smart areas as a distinct conservation feature, allowing for explicit conservation targets on climate resilience. + * **Climate Percentile Approach**: Sets conservation targets based on percentile ranges of climate metrics, allowing for targeted protection of areas within specific climate resilience thresholds. + +# Data Acquisition and Preprocessing Utilities +_spatialplanr_ also provides convenience functions for acquiring and preparing data, which can be crucial for climate-smart planning. + +```{r} +# Load spatialplanr +library(spatialplanr) +library(tidyverse) +``` + +1. Get IUCN Red List Data +Function: `splnr_get_IUCNRedList()` + +This function interfaces with the IUCN Red List API to retrieve conservation status information for a list of species. This data can be valuable for assigning species-specific targets (e.g., higher targets for more threatened species) or filtering species based on their conservation status. + +Note: Requires an IUCN Red List API token. + +```{r} +# # Example: Fetch IUCN data for a few marine species +# # Ensure your IUCN Red List API token is set: +# # Sys.setenv(IUCN_REDLIST_KEY = "YOUR_API_KEY") # Replace with your actual key +# +# # Example species list +# my_species <- c("Orcinus orca", "Chelonia mydas", "Thunnus thynnus") +# +# # Create a dataframe matching the expected input format +# species_df <- data.frame( +# scientific_name = my_species +# ) +# +# # Get IUCN data +# iucn_data <- splnr_get_IUCNRedList(df = species_df, species_col = "scientific_name") +# print(iucn_data) + +``` + +2. Get Global Fishing Watch (GFW) Data +Function: `splnr_get_gfw()` + +This function facilitates the retrieval of fishing activity data (e.g., apparent fishing hours) from Global Fishing Watch (GFW). GFW data can be used to inform cost layers (e.g., higher fishing effort areas might have higher opportunity costs for conservation) or as a proxy for human impact in planning units. + +Note: Requires a GFW API token. + +```{r} +# Example: Get yearly fishing hours for Australia EEZ + +gfw_data_aus <- splnr_get_gfw( + region = 'Australia', + start_date = "2021-01-01", + end_date = "2021-12-31", + temp_res = "YEARLY", + spat_res = "LOW", + region_source = "EEZ", + cCRS = "EPSG:4326", + compress = TRUE # Returns polygons aggregated by fishing hours +) +print(head(gfw_data_aus)) +``` + +## Visualization Tools +_spatialplanr_ offers a rich set of plotting functions to visualize input data, climate metrics, and most importantly, the outputs of your spatial prioritization analyses. These functions are built on ggplot2 and sf for high-quality spatial visualizations. + +1. Plot Climate Data +Function: `splnr_plot_climData()` + +Visualizes the spatial distribution of your climate metric, allowing you to quickly inspect patterns in climate velocity, temperature anomaly, or other relevant climate variables. + +```{r} +# Assuming 'dat_clim' from previous examples is available +# Plot the 'metric' column from dat_clim +splnr_plot_climData( + df = dat_clim, + colInterest = "metric", + plotTitle = "Example Climate Metric Distribution", + legendTitle = "Metric Value" +) +``` + +2. Plot Climate Kernel Density +Functions: `splnr_plot_climKernelDensity_Basic()`, `splnr_plot_climKernelDensity_Fancy()`, `splnr_plot_climKernelDensity()` + +These functions help visualize the distribution of climate metric values within selected planning units (e.g., a proposed protected area network) compared to the overall distribution. This helps assess if climate-smart areas are being adequately captured. + +`_Basic()`: Simple kernel density plot. +`_Fancy()`: More customizable kernel density plot, allowing multiple solutions and specific zones. +`_climKernelDensity()`: A wrapper function that selects between basic and fancy plots based on input type. + +```{r} +solution_df <- dat_clim %>% + dplyr::mutate( + solution_1 = sample(c(0, 1), size = dplyr::n(), replace = TRUE, prob = c(0.7, 0.3)), + metric_category = cut(metric, breaks = 3, labels = c("Low", "Medium", "High")) + ) + +# Basic kernel density plot of the metric for selected areas vs. all areas +splnr_plot_climKernelDensity( + soln = solution_df, + type = "Basic", + names = "solution_1" # Column indicating selected PUs (1=selected) +) + +# Fancy kernel density plot, perhaps with zones (if your solution has them) +# For this example, let's pretend 'solution_df' has a 'zone' column +# (this would come from a prioritizr zoned solution) +zoned_solution_df <- solution_df %>% + dplyr::mutate( + zone = sample(c("Zone A", "Zone B"), size = dplyr::n(), replace = TRUE) + ) + +# Example for Fancy (requires a list of solutions or specific structure) +# If you have multiple prioritizr solutions, you would put them in a list. +# For simplicity, we'll create a mock 'soln' list: +soln_list <- list( + Solution1 = zoned_solution_df, + Solution2 = zoned_solution_df %>% mutate(solution_1 = abs(solution_1 - 1)) # Invert selection for demo +) + +# plot_climKernelDensity handles both basic and fancy automatically +splnr_plot_climKernelDensity( + soln = soln_list, # Pass the list of solutions + names = c("metric", "metric"), + type = "Normal" # Explicitly request fancy + # zone_column = "zone" # If your solutions have zones +) +``` + +3. Plot Prioritization Solutions +Function: `splnr_plot_solution()` + +Visualizes the spatial output of a prioritizr problem, showing which planning units were selected for conservation. It supports both single-zone and multi-zone solutions. + +```{r} + +# Plot a single-zone solution +splnr_plot_solution( + soln = solution_df, + colorVals = c("grey", "darkgreen"), + legendLabels = c("Not Selected", "Selected"), + plotTitle = "Prioritization Solution (Single Zone)" +) + +# Plot a zoned solution (assuming 'zone' column exists) +# In a real scenario, this 'zone' column would be part of your prioritizr output. +# splnr_plot_solution( +# soln = zoned_solution_df, +# colorVals = c("red", "blue", "grey"), # Colors for each zone + not selected +# legendLabels = c("Zone A", "Zone B", "Not Selected"), +# plotTitle = "Prioritization Solution (Zoned)", +# zones = TRUE +# ) +``` + + +4. Plot Cost Overlay +Function: `splnr_plot_costOverlay()` + +Overlays cost data on top of a prioritization solution, helping to visualize the spatial distribution of costs relative to selected areas. + +```{r} +# Assuming 'solution_df' and adding a 'cost' column +solution_with_cost <- solution_df %>% + dplyr::mutate(cost = runif(dplyr::n(), 100, 1000)) + +splnr_plot_costOverlay( + soln = solution_with_cost, + costName = "cost", # Name of the cost column + # costName = "Acquisition Cost", + # colorVals = c("grey", "darkgreen"), + # legendLabels = c("Not Selected", "Selected"), + plotTitle = "Prioritization Solution with Cost Overlay" +) + +``` + +5. Plot Comparison +Function: `splnr_plot_comparison()` + +Compares two different prioritization solutions, highlighting areas that are uniquely selected by each solution, or selected by both. + +```{r} +# Assuming two solutions for comparison +soln1_df <- solution_df %>% + dplyr::mutate(solution_1 = sample(c(0, 1), dplyr::n(), replace = TRUE, prob = c(0.6, 0.4))) +soln2_df <- solution_df %>% + dplyr::mutate(solution_1 = sample(c(0, 1), dplyr::n(), replace = TRUE, prob = c(0.5, 0.5))) + +splnr_plot_comparison( + soln1 = soln1_df, + soln2 = soln2_df, +) + +``` + +6. Plot Selection Frequency +Function: `splnr_plot_selectionFreq()` + +Visualizes the frequency with which each planning unit is selected across multiple runs of a prioritization problem (e.g., from a sensitivity analysis or robust solution generation). + +```{r} +# # Create selection frequency data +# selection_freq_df <- solution_df %>% +# dplyr::mutate( +# selection_frequency = runif(dplyr::n(), 0, 1) # Frequency between 0 and 1 +# ) +# +# splnr_plot_selectionFreq( +# selFreq = selection_freq_df, +# plotTitle = "Selection Frequency of Planning Units", +# legendTitle = "Selection Frequency" +# ) +``` + +7. Plot Importance Score +Function: `splnr_plot_importanceScore()` + +Calculates and visualizes an "importance score" for each planning unit, which can represent its contribution to achieving targets or its irreplaceability. Useful for identifying key areas. + +```{r} +# +# splnr_plot_importanceScore( +# soln = solution_df, +# pDat = dat_species_bin, # Original biodiversity features +# method = "basic", # Or "complex" +# decimals = 2, +# plotTitle = "Planning Unit Importance Score" +# ) +``` + +8. Plot Correlation Matrix +Function: `splnr_plot_corrMat()` + +Generates a correlation matrix plot, useful for understanding relationships between different features or variables in your planning data. + +```{r} +# # Create a data matrix for correlation +# +# corr_data <- data.frame( +# Var1 = rnorm(100), +# Var2 = rnorm(100, mean = 0.5, sd = 1), +# Var3 = rnorm(100, mean = -0.2, sd = 0.5) +# ) +# # Add some correlation +# corr_data$Var2 <- corr_data$Var2 + 0.5 * corr_data$Var1 +# corr_data$Var3 <- corr_data$Var3 - 0.3 * corr_data$Var1 +# +# # Calculate correlation matrix +# corr_matrix <- cor(corr_data) +# +# splnr_plot_corrMat( +# matrix = corr_matrix, +# plotTitle = "Correlation Matrix of Variables" +# ) + +``` + +9. Generic Plotting Utility +Function: `splnr_plot()` + +A versatile plotting function that can be used to visualize any continuous or categorical data column within an sf dataframe, with options for color palettes and legend customization. Many other plotting functions in _spatialplanr_ internally use this function. + +```{r} +# Plotting the 'metric' column from 'dat_clim' using the generic plotter +splnr_plot( + df = dat_clim, + colNames = "metric", + plotTitle = "Generic Plot of Climate Metric", + legendTitle = "Metric Value", + paletteName = "viridis" # Use a viridis color palette +) + +``` + +# Example Workflow +The following example illustrates a typical workflow using _spatialplanr_ for climate-smart conservation planning. It assumes you have already prepared your planning units, biodiversity features, and cost layers. + +A typical workflow using _spatialplanr_ for climate-smart conservation planning might look like this: + +## Data Preparation: + +* Load your planning units (PUs). +* Load your biodiversity features (features). +* Load your cost layer (costs). +* Load your climate metric data (climate_data). +* (Optional) Use splnr_get_IUCNRedList() to refine species targets or splnr_get_gfw() to generate a cost layer. + +Choose Climate-Smart Approach: + +* Decide which climate-smart approach best suits your planning goals: + * `splnr_climate_priorityAreaApproach()` for prioritizing refugia for each feature. + * `splnr_climate_featureApproach()` for treating climate resilience as a standalone feature. + * `splnr_climate_percentileApproach()` for targeting specific climate metric ranges. +* Run the chosen function to obtain climate_features and climate_targets. +* Define and Solve Prioritization Problem (using _prioritizr_): + +Create a prioritizr problem using your PUs, climate_features, and climate_targets. +* Add objectives (e.g., `add_min_set_objective()`, `add_max_targets_objective()`). +* Add constraints (e.g., `add_budget_constraint()`, `add_contiguity_constraint()`, `add_locked_in_constraint()` for existing MPAs). +* Add solvers (e.g., `add_gurobi_solver()`, `add_cbc_solver()`). +* Solve the problem to get your solution. + +Visualize and Analyze Results (using _spatialplanr_ plotting functions): +* `splnr_plot_solution()` to visualize the selected planning units. +* `splnr_plot_costOverlay()` to see costs in selected areas. +* `splnr_plot_climKernelDensity()` to assess the climate characteristics of the solution. +* `splnr_plot_comparison()` to compare different scenarios or solutions. +* `splnr_plot_selectionFreq()` for robust solutions. +* `splnr_plot_importanceScore()` to identify key planning units. +* `splnr_plot_corrMat()` to understand feature relationships. + + +# Conclusion +_spatialplanr_ provides a powerful and flexible toolkit for integrating climate change considerations into spatial conservation prioritization. By offering multiple methodological approaches, streamlining data handling, and providing comprehensive visualization capabilities, it aims to empower conservation planners to create more resilient and effective protected area networks for the future. diff --git a/README.md b/README.md index 0076f51..b6133b1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# spatialplanr spatialplanr website +# spatialplanr \“spatialplanr @@ -15,15 +15,14 @@ coverage](https://codecov.io/gh/SpatialPlanning/spatialplanr/branch/main/graph/b [![Issues](https://img.shields.io/github/issues/SpatialPlanning/spatialplanr)](https://github.com/SpatialPlanning/spatialplanr/issues) -## Overview +# Introduction to spatialplanr -This package is designed to assist students and staff in the -[Mathematical Marine Ecology Lab](https://mathmarecol.github.io) at the -University of Queensland. It may be useful for others as well. This code -has been written to simplify the process for running a *prioritizr* -analysis on a given region use the workflows and data of the MME Lab. It -is still a work in progress so feel free to submit pull requests with -new features and code improvements. +Welcome to *spatialplanr*, an R package designed to streamline and +enhance spatial conservation prioritization efforts by explicitly +integrating climate change considerations. Building upon the powerful +*prioritizr* package, *spatialplanr* provides a suite of tools for +conservation planners to develop more robust and climate-resilient +protected area networks. ## Installation @@ -36,3 +35,455 @@ development version from [GitHub](https://github.com/) with: # install.packages("devtools") devtools::install_github("https://github.com/SpatialPlanning/spatialplanr") ``` + +# Purpose and Goals + +The escalating impacts of climate change necessitate a paradigm shift in +how we approach conservation. Traditional conservation planning often +focuses on static biodiversity patterns, which may not adequately +account for the dynamic nature of species distributions and ecosystem +processes under a changing climate. *spatialplanr* aims to address this +gap by: + +**Facilitating Climate-Smart Planning**: Providing functions that +incorporate climate data directly into the planning process, allowing +for the identification of areas critical for both biodiversity and +resilience to future climate conditions. Offering Diverse Methodological +Approaches: Implementing multiple established climate-smart conservation +planning frameworks (e.g., Climate Priority Areas, Climate Features, +Climate Percentiles) to offer flexibility based on specific planning +goals and data availability. + +**Streamlining Workflow**: Offering end-to-end functionality, from data +preprocessing and integration to advanced visualization of results, +simplifying complex analytical tasks for users. + +**Enhancing Decision-Making**: Producing outputs that directly feed into +spatial prioritization software like prioritizr, enabling the generation +of optimal conservation solutions that balance biodiversity +representation with climate resilience. By using spatialplanr, +practitioners can move beyond reactive conservation and proactively +design protected area systems better equipped to safeguard biodiversity +in a rapidly changing world. + +# Core Climate-Smart Planning Approaches + +*spatialplanr* implements several key approaches for integrating climate +change into spatial prioritization, largely drawing inspiration from +frameworks like that presented in Buenafe et al. (2023) “A metric‐based +framework for climate‐smart conservation planning” (DOI: +10.1002/eap.2852). + +These approaches are designed to transform biodiversity features and +conservation targets based on climate metrics, allowing for a more +nuanced understanding of how climate change may impact conservation +priorities. The main approaches included in *spatialplanr* are: \* +**Climate Priority Area (CPA) Approach**: Identifies climate-smart areas +within the distribution of each conservation feature, creating new +components for climate-smart and non-climate-smart areas. \* **Climate +Feature Approach**: Treats climate-smart areas as a distinct +conservation feature, allowing for explicit conservation targets on +climate resilience. \* **Climate Percentile Approach**: Sets +conservation targets based on percentile ranges of climate metrics, +allowing for targeted protection of areas within specific climate +resilience thresholds. + +# Data Acquisition and Preprocessing Utilities + +*spatialplanr* also provides convenience functions for acquiring and +preparing data, which can be crucial for climate-smart planning. + +``` r +# Load spatialplanr +library(spatialplanr) +library(tidyverse) +#> ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ── +#> ✔ dplyr 1.1.4 ✔ readr 2.1.5 +#> ✔ forcats 1.0.0 ✔ stringr 1.5.1 +#> ✔ ggplot2 3.5.2 ✔ tibble 3.3.0 +#> ✔ lubridate 1.9.4 ✔ tidyr 1.3.1 +#> ✔ purrr 1.0.4 +#> ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ── +#> ✖ dplyr::filter() masks stats::filter() +#> ✖ dplyr::lag() masks stats::lag() +#> ℹ Use the conflicted package () to force all conflicts to become errors +``` + +1. Get IUCN Red List Data Function: `splnr_get_IUCNRedList()` + +This function interfaces with the IUCN Red List API to retrieve +conservation status information for a list of species. This data can be +valuable for assigning species-specific targets (e.g., higher targets +for more threatened species) or filtering species based on their +conservation status. + +Note: Requires an IUCN Red List API token. + +``` r +# # Example: Fetch IUCN data for a few marine species +# # Ensure your IUCN Red List API token is set: +# # Sys.setenv(IUCN_REDLIST_KEY = "YOUR_API_KEY") # Replace with your actual key +# +# # Example species list +# my_species <- c("Orcinus orca", "Chelonia mydas", "Thunnus thynnus") +# +# # Create a dataframe matching the expected input format +# species_df <- data.frame( +# scientific_name = my_species +# ) +# +# # Get IUCN data +# iucn_data <- splnr_get_IUCNRedList(df = species_df, species_col = "scientific_name") +# print(iucn_data) +``` + +2. Get Global Fishing Watch (GFW) Data Function: `splnr_get_gfw()` + +This function facilitates the retrieval of fishing activity data (e.g., +apparent fishing hours) from Global Fishing Watch (GFW). GFW data can be +used to inform cost layers (e.g., higher fishing effort areas might have +higher opportunity costs for conservation) or as a proxy for human +impact in planning units. + +Note: Requires a GFW API token. + +``` r +# Example: Get yearly fishing hours for Australia EEZ + +gfw_data_aus <- splnr_get_gfw( + region = 'Australia', + start_date = "2021-01-01", + end_date = "2021-12-31", + temp_res = "YEARLY", + spat_res = "LOW", + region_source = "EEZ", + cCRS = "EPSG:4326", + compress = TRUE # Returns polygons aggregated by fishing hours +) +print(head(gfw_data_aus)) +#> Simple feature collection with 6 features and 2 fields +#> Geometry type: POLYGON +#> Dimension: XY +#> Bounding box: xmin: 104.05 ymin: -8.85 xmax: 104.75 ymax: -8.65 +#> Geodetic CRS: WGS 84 +#> # A tibble: 6 × 3 +#> geometry ApparentFishingHrs GFWregionID +#> +#> 1 ((104.45 -8.75, 104.45 -8.65, 104.55 -8.65, 10… 1.04 8309 +#> 2 ((104.55 -8.75, 104.55 -8.65, 104.65 -8.65, 10… 6.45 8309 +#> 3 ((104.05 -8.85, 104.05 -8.75, 104.15 -8.75, 10… 2.89 8309 +#> 4 ((104.25 -8.85, 104.25 -8.75, 104.35 -8.75, 10… 7.34 8309 +#> 5 ((104.45 -8.85, 104.45 -8.75, 104.55 -8.75, 10… 2.02 8309 +#> 6 ((104.65 -8.85, 104.65 -8.75, 104.75 -8.75, 10… 6.32 8309 +``` + +## Visualization Tools + +*spatialplanr* offers a rich set of plotting functions to visualize +input data, climate metrics, and most importantly, the outputs of your +spatial prioritization analyses. These functions are built on ggplot2 +and sf for high-quality spatial visualizations. + +1. Plot Climate Data Function: `splnr_plot_climData()` + +Visualizes the spatial distribution of your climate metric, allowing you +to quickly inspect patterns in climate velocity, temperature anomaly, or +other relevant climate variables. + +``` r +# Assuming 'dat_clim' from previous examples is available +# Plot the 'metric' column from dat_clim +splnr_plot_climData( + df = dat_clim, + colInterest = "metric", + plotTitle = "Example Climate Metric Distribution", + legendTitle = "Metric Value" +) +``` + + + +2. Plot Climate Kernel Density Functions: + `splnr_plot_climKernelDensity_Basic()`, + `splnr_plot_climKernelDensity_Fancy()`, + `splnr_plot_climKernelDensity()` + +These functions help visualize the distribution of climate metric values +within selected planning units (e.g., a proposed protected area network) +compared to the overall distribution. This helps assess if climate-smart +areas are being adequately captured. + +`_Basic()`: Simple kernel density plot. `_Fancy()`: More customizable +kernel density plot, allowing multiple solutions and specific zones. +`_climKernelDensity()`: A wrapper function that selects between basic +and fancy plots based on input type. + +``` r +solution_df <- dat_clim %>% + dplyr::mutate( + solution_1 = sample(c(0, 1), size = dplyr::n(), replace = TRUE, prob = c(0.7, 0.3)), + metric_category = cut(metric, breaks = 3, labels = c("Low", "Medium", "High")) + ) + +# Basic kernel density plot of the metric for selected areas vs. all areas +splnr_plot_climKernelDensity( + soln = solution_df, + type = "Basic", + names = "solution_1" # Column indicating selected PUs (1=selected) +) + +#> Picking joint bandwidth of 0.0885 +#> Picking joint bandwidth of 0.077 + +``` + + + +``` r + +# Fancy kernel density plot, perhaps with zones (if your solution has them) +# For this example, let's pretend 'solution_df' has a 'zone' column +# (this would come from a prioritizr zoned solution) +zoned_solution_df <- solution_df %>% + dplyr::mutate( + zone = sample(c("Zone A", "Zone B"), size = dplyr::n(), replace = TRUE) + ) + +# Example for Fancy (requires a list of solutions or specific structure) +# If you have multiple prioritizr solutions, you would put them in a list. +# For simplicity, we'll create a mock 'soln' list: +soln_list <- list( + Solution1 = zoned_solution_df, + Solution2 = zoned_solution_df %>% mutate(solution_1 = abs(solution_1 - 1)) # Invert selection for demo +) + +# plot_climKernelDensity handles both basic and fancy automatically +splnr_plot_climKernelDensity( + soln = soln_list, # Pass the list of solutions + names = c("metric", "metric"), + type = "Normal" # Explicitly request fancy + # zone_column = "zone" # If your solutions have zones +) +#> Picking joint bandwidth of 0.071 +#> Picking joint bandwidth of 0.071 +``` + + + +3. Plot Prioritization Solutions Function: `splnr_plot_solution()` + +Visualizes the spatial output of a prioritizr problem, showing which +planning units were selected for conservation. It supports both +single-zone and multi-zone solutions. + +``` r + +# Plot a single-zone solution +splnr_plot_solution( + soln = solution_df, + colorVals = c("grey", "darkgreen"), + legendLabels = c("Not Selected", "Selected"), + plotTitle = "Prioritization Solution (Single Zone)" +) +``` + + + +``` r + +# Plot a zoned solution (assuming 'zone' column exists) +# In a real scenario, this 'zone' column would be part of your prioritizr output. +# splnr_plot_solution( +# soln = zoned_solution_df, +# colorVals = c("red", "blue", "grey"), # Colors for each zone + not selected +# legendLabels = c("Zone A", "Zone B", "Not Selected"), +# plotTitle = "Prioritization Solution (Zoned)", +# zones = TRUE +# ) +``` + +4. Plot Cost Overlay Function: `splnr_plot_costOverlay()` + +Overlays cost data on top of a prioritization solution, helping to +visualize the spatial distribution of costs relative to selected areas. + +``` r +# Assuming 'solution_df' and adding a 'cost' column +solution_with_cost <- solution_df %>% + dplyr::mutate(cost = runif(dplyr::n(), 100, 1000)) + +splnr_plot_costOverlay( + soln = solution_with_cost, + costName = "cost", # Name of the cost column + # costName = "Acquisition Cost", + # colorVals = c("grey", "darkgreen"), + # legendLabels = c("Not Selected", "Selected"), + plotTitle = "Prioritization Solution with Cost Overlay" +) +``` + + + +5. Plot Comparison Function: `splnr_plot_comparison()` + +Compares two different prioritization solutions, highlighting areas that +are uniquely selected by each solution, or selected by both. + +``` r +# Assuming two solutions for comparison +soln1_df <- solution_df %>% + dplyr::mutate(solution_1 = sample(c(0, 1), dplyr::n(), replace = TRUE, prob = c(0.6, 0.4))) +soln2_df <- solution_df %>% + dplyr::mutate(solution_1 = sample(c(0, 1), dplyr::n(), replace = TRUE, prob = c(0.5, 0.5))) + +splnr_plot_comparison( + soln1 = soln1_df, + soln2 = soln2_df, +) +``` + + + +6. Plot Selection Frequency Function: `splnr_plot_selectionFreq()` + +Visualizes the frequency with which each planning unit is selected +across multiple runs of a prioritization problem (e.g., from a +sensitivity analysis or robust solution generation). + +``` r +# # Create selection frequency data +# selection_freq_df <- solution_df %>% +# dplyr::mutate( +# selection_frequency = runif(dplyr::n(), 0, 1) # Frequency between 0 and 1 +# ) +# +# splnr_plot_selectionFreq( +# selFreq = selection_freq_df, +# plotTitle = "Selection Frequency of Planning Units", +# legendTitle = "Selection Frequency" +# ) +``` + +7. Plot Importance Score Function: `splnr_plot_importanceScore()` + +Calculates and visualizes an “importance score” for each planning unit, +which can represent its contribution to achieving targets or its +irreplaceability. Useful for identifying key areas. + +``` r +# +# splnr_plot_importanceScore( +# soln = solution_df, +# pDat = dat_species_bin, # Original biodiversity features +# method = "basic", # Or "complex" +# decimals = 2, +# plotTitle = "Planning Unit Importance Score" +# ) +``` + +8. Plot Correlation Matrix Function: `splnr_plot_corrMat()` + +Generates a correlation matrix plot, useful for understanding +relationships between different features or variables in your planning +data. + +``` r +# # Create a data matrix for correlation +# +# corr_data <- data.frame( +# Var1 = rnorm(100), +# Var2 = rnorm(100, mean = 0.5, sd = 1), +# Var3 = rnorm(100, mean = -0.2, sd = 0.5) +# ) +# # Add some correlation +# corr_data$Var2 <- corr_data$Var2 + 0.5 * corr_data$Var1 +# corr_data$Var3 <- corr_data$Var3 - 0.3 * corr_data$Var1 +# +# # Calculate correlation matrix +# corr_matrix <- cor(corr_data) +# +# splnr_plot_corrMat( +# matrix = corr_matrix, +# plotTitle = "Correlation Matrix of Variables" +# ) +``` + +9. Generic Plotting Utility Function: `splnr_plot()` + +A versatile plotting function that can be used to visualize any +continuous or categorical data column within an sf dataframe, with +options for color palettes and legend customization. Many other plotting +functions in *spatialplanr* internally use this function. + +``` r +# Plotting the 'metric' column from 'dat_clim' using the generic plotter +splnr_plot( + df = dat_clim, + colNames = "metric", + plotTitle = "Generic Plot of Climate Metric", + legendTitle = "Metric Value", + paletteName = "viridis" # Use a viridis color palette +) +#> Coordinate system already present. Adding new coordinate system, which will +#> replace the existing one. +``` + + + +# Example Workflow + +The following example illustrates a typical workflow using +*spatialplanr* for climate-smart conservation planning. It assumes you +have already prepared your planning units, biodiversity features, and +cost layers. + +A typical workflow using *spatialplanr* for climate-smart conservation +planning might look like this: + +## Data Preparation: + +- Load your planning units (PUs). +- Load your biodiversity features (features). +- Load your cost layer (costs). +- Load your climate metric data (climate_data). +- (Optional) Use splnr_get_IUCNRedList() to refine species targets or + splnr_get_gfw() to generate a cost layer. + +Choose Climate-Smart Approach: + +- Decide which climate-smart approach best suits your planning goals: + - `splnr_climate_priorityAreaApproach()` for prioritizing refugia for + each feature. + - `splnr_climate_featureApproach()` for treating climate resilience as + a standalone feature. + - `splnr_climate_percentileApproach()` for targeting specific climate + metric ranges. +- Run the chosen function to obtain climate_features and + climate_targets. +- Define and Solve Prioritization Problem (using *prioritizr*): + +Create a prioritizr problem using your PUs, climate_features, and +climate_targets. \* Add objectives (e.g., `add_min_set_objective()`, +`add_max_targets_objective()`). \* Add constraints (e.g., +`add_budget_constraint()`, `add_contiguity_constraint()`, +`add_locked_in_constraint()` for existing MPAs). \* Add solvers (e.g., +`add_gurobi_solver()`, `add_cbc_solver()`). \* Solve the problem to get +your solution. + +Visualize and Analyze Results (using *spatialplanr* plotting functions): +\* `splnr_plot_solution()` to visualize the selected planning units. \* +`splnr_plot_costOverlay()` to see costs in selected areas. \* +`splnr_plot_climKernelDensity()` to assess the climate characteristics +of the solution. \* `splnr_plot_comparison()` to compare different +scenarios or solutions. \* `splnr_plot_selectionFreq()` for robust +solutions. \* `splnr_plot_importanceScore()` to identify key planning +units. \* `splnr_plot_corrMat()` to understand feature relationships. + +# Conclusion + +*spatialplanr* provides a powerful and flexible toolkit for integrating +climate change considerations into spatial conservation prioritization. +By offering multiple methodological approaches, streamlining data +handling, and providing comprehensive visualization capabilities, it +aims to empower conservation planners to create more resilient and +effective protected area networks for the future. diff --git a/data-raw/CreateHex.R b/data-raw/CreateHex.R index 928a20a..09587ef 100644 --- a/data-raw/CreateHex.R +++ b/data-raw/CreateHex.R @@ -1,6 +1,6 @@ # Some code to create a hex sticker for the spatialplanr package # -# Last updated: Saturday 8th July 2023 +# Last updated: Friday 13th June 2025 # # Jason D. Everett (UQ/CSIRO/UNSW) # @@ -95,12 +95,12 @@ hexSticker::sticker(gg, s_height = 2.2, # h_fill = "#9FE2BF", h_color = "black", # "grey40", - url = "mathmarecol.github.io/spatialplanr", + url = "spatialplanning.github.io/spatialplanr", u_color = "grey90", # u_family = "sans", - u_size = 15.5, - u_x = 0.99, - u_y = 0.06, + u_size = 15, + u_x = 0.98, + u_y = 0.055, dpi = 1000, asp = 1, filename = file.path("data-raw", "spatialplanr.png")) diff --git a/data-raw/spatialplanr.png b/data-raw/spatialplanr.png index e6b11af..a25c009 100644 Binary files a/data-raw/spatialplanr.png and b/data-raw/spatialplanr.png differ diff --git a/man/figures/README-unnamed-chunk-13-1.png b/man/figures/README-unnamed-chunk-13-1.png new file mode 100644 index 0000000..cae4a41 Binary files /dev/null and b/man/figures/README-unnamed-chunk-13-1.png differ diff --git a/man/figures/README-unnamed-chunk-5-1.png b/man/figures/README-unnamed-chunk-5-1.png new file mode 100644 index 0000000..009cad8 Binary files /dev/null and b/man/figures/README-unnamed-chunk-5-1.png differ diff --git a/man/figures/README-unnamed-chunk-6-1.png b/man/figures/README-unnamed-chunk-6-1.png new file mode 100644 index 0000000..5f48857 Binary files /dev/null and b/man/figures/README-unnamed-chunk-6-1.png differ diff --git a/man/figures/README-unnamed-chunk-6-2.png b/man/figures/README-unnamed-chunk-6-2.png new file mode 100644 index 0000000..ee5a463 Binary files /dev/null and b/man/figures/README-unnamed-chunk-6-2.png differ diff --git a/man/figures/README-unnamed-chunk-7-1.png b/man/figures/README-unnamed-chunk-7-1.png new file mode 100644 index 0000000..972ac6b Binary files /dev/null and b/man/figures/README-unnamed-chunk-7-1.png differ diff --git a/man/figures/README-unnamed-chunk-8-1.png b/man/figures/README-unnamed-chunk-8-1.png new file mode 100644 index 0000000..c6f8ab9 Binary files /dev/null and b/man/figures/README-unnamed-chunk-8-1.png differ diff --git a/man/figures/README-unnamed-chunk-9-1.png b/man/figures/README-unnamed-chunk-9-1.png new file mode 100644 index 0000000..b008fb9 Binary files /dev/null and b/man/figures/README-unnamed-chunk-9-1.png differ diff --git a/man/figures/logo.png b/man/figures/logo.png index 65f2495..6b94dcb 100644 Binary files a/man/figures/logo.png and b/man/figures/logo.png differ diff --git a/man/spatialplanr-package.Rd b/man/spatialplanr-package.Rd index dd2e641..88adcb5 100644 --- a/man/spatialplanr-package.Rd +++ b/man/spatialplanr-package.Rd @@ -13,8 +13,8 @@ This package provides a range of tools for setting up, running and plotting a sp \seealso{ Useful links: \itemize{ - \item \url{https://github.com/MathMarEcol/spatialplanr} - \item Report bugs at \url{https://github.com/MathMarEcol/spatialplanr/issues} + \item \url{https://github.com/SpatialPlanning/spatialplanr} + \item Report bugs at \url{https://github.com/SpatialPlanning/spatialplanr/issues} } } diff --git a/man/splnr_apply_cutoffs.Rd b/man/splnr_apply_cutoffs.Rd index 78f23d5..36f7f64 100644 --- a/man/splnr_apply_cutoffs.Rd +++ b/man/splnr_apply_cutoffs.Rd @@ -2,23 +2,93 @@ % Please edit documentation in R/splnr_apply_cutoffs.R \name{splnr_apply_cutoffs} \alias{splnr_apply_cutoffs} -\title{Function to apply cutoffs to feature data} +\title{Apply Cutoffs to Feature Data} \usage{ splnr_apply_cutoffs(features, Cutoffs, inverse = FALSE) } \arguments{ -\item{features}{A sf dataframe with all the feature information} +\item{features}{An \code{sf} dataframe. It must contain a \code{geometry} column and +at least one numeric column to which cutoffs will be applied.} -\item{Cutoffs}{A single value or a named vector of cutoffs.} +\item{Cutoffs}{A numeric value or a named numeric vector of cutoffs. +\itemize{ +\item If a single unnamed numeric value, it's applied to all numeric columns. +\item If a named numeric vector, names must correspond to numeric column names in \code{features}. +} +All cutoff values must be between \code{0} and \code{1}.} -\item{inverse}{If TRUE, values below the \code{Cutoffs} are used. If FALSE (default), values above are kept.} +\item{inverse}{A logical value (\code{TRUE} or \code{FALSE}). If \code{TRUE}, values below +the \code{Cutoffs} are converted to \code{1} (and others to \code{0}). If \code{FALSE} (default), +values at or above the \code{Cutoffs} are converted to \code{1}.} } \value{ -A new sf dataframe that has cutoffs applied. +A modified \code{sf} dataframe with the same structure and geometry as +\code{features}, but with all targeted numeric columns transformed into binary +(0 or 1) values based on the specified cutoffs and \code{inverse} setting. } \description{ -Function to apply cutoffs to feature data +\code{splnr_apply_cutoffs()} transforms numeric feature data in an \code{sf} dataframe +into binary (0 or 1) presence/absence values based on specified cutoffs. +It provides flexibility to either keep values above a cutoff as 1 (default) +or invert this logic to keep values below a cutoff as 1. +} +\details{ +This function is crucial for standardizing feature data, such as species +probability distributions or habitat suitability scores, into a binary format +often required for conservation planning and spatial analysis (e.g., in +\code{prioritizr}). + +The function operates in two primary modes based on the \code{Cutoffs} parameter: +\itemize{ +\item \strong{Single Cutoff:} If \code{Cutoffs} is a single numeric value (e.g., \code{0.5}), +this value is applied uniformly to \strong{all numeric columns} in the +\code{features} dataframe, excluding the \code{geometry} column. +For each numeric cell: +- If \code{value >= Cutoffs}, it becomes \code{1}. +- If \code{value < Cutoffs}, it becomes \code{0}. +- \code{NA} values are always converted to \code{0}. +\item \strong{Named Vector of Cutoffs:} If \code{Cutoffs} is a named numeric vector +(e.g., \code{c("feature1" = 0.5, "feature2" = 0.3)}), each specified cutoff +is applied individually to its corresponding named column in \code{features}. +This allows for different thresholds for different features. The same +transformation rules as above apply to each specified column. +} + +The \code{inverse} parameter provides additional control over the binarization: +\itemize{ +\item \code{inverse = FALSE} (default): Values \strong{at or above} the cutoff become \code{1}. +\item \code{inverse = TRUE}: Values \strong{below} the cutoff become \code{1}. After initial +binarization (where values >= cutoff are 1), the binary results are +flipped (0s become 1s, and 1s become 0s) to achieve the inverse effect. +} +All \code{NA} values in the numeric columns are consistently converted to \code{0} during +the binarization process, regardless of the \code{inverse} setting. } \examples{ -df <- splnr_apply_cutoffs(dat_species_prob, Cutoffs = 0.5) + +# Example 1: Single cutoff (0.5) applied to all numeric feature columns +# (Spp1_Prob, Spp2_Prob, and Cost will be binarized based on 0.5) +df_single_cutoff <- splnr_apply_cutoffs(dat_species_prob, Cutoffs = 0.5) +print(df_single_cutoff) + +# Example 2: Named cutoffs for specific columns +# Spp1_Prob >= 0.6 becomes 1, Spp2_Prob >= 0.4 becomes 1 +df_named_cutoffs <- splnr_apply_cutoffs( + dat_species_prob, + Cutoffs = c("Spp1" = 0.6, "Spp2" = 0.4) +) +print(df_named_cutoffs) + +# Example 3: Single cutoff (0.5) with inverse logic +# Values BELOW 0.5 become 1. +df_inverse_cutoff <- splnr_apply_cutoffs(dat_species_prob, Cutoffs = 0.5, inverse = TRUE) +print(df_inverse_cutoff) + +# Example 4: Named cutoffs with inverse logic +df_named_inverse <- splnr_apply_cutoffs( + dat_species_prob, + Cutoffs = c("Spp1" = 0.7, "Spp2" = 0.3), + inverse = TRUE +) +print(df_named_inverse) } diff --git a/man/splnr_arrangeFeatures.Rd b/man/splnr_arrangeFeatures.Rd index eee875d..0feb870 100644 --- a/man/splnr_arrangeFeatures.Rd +++ b/man/splnr_arrangeFeatures.Rd @@ -2,20 +2,42 @@ % Please edit documentation in R/utils.R \name{splnr_arrangeFeatures} \alias{splnr_arrangeFeatures} -\title{Ensure all features are in the same order.} +\title{Arrange Features by Spatial Coordinates} \usage{ splnr_arrangeFeatures(df) } \arguments{ -\item{df}{An sf object to sort by Lon and Lat} +\item{df}{An \code{sf} object whose rows are to be sorted.} } \value{ -A sorted sf object +A sorted \code{sf} object, with rows ordered primarily by longitude (X) +and secondarily by latitude (Y) of their centroids. } \description{ -\code{splnr_arrangeFeatures()} sorts your data based on longitude and latitude values. +\code{splnr_arrangeFeatures()} sorts the rows of an \code{sf} object based on the +longitude (X) and then latitude (Y) of its centroids. This ensures a +consistent ordering of planning units, which can be important for +reproducibility in some spatial analyses or data processing steps. +} +\details{ +This function computes the centroid for each polygon (or point/multipoint) +in the input \code{sf} object. It then extracts the X and Y coordinates of these +centroids and uses them to sort the entire \code{sf} object. The primary sort key +is the longitude (X-coordinate), and the secondary sort key is the latitude +(Y-coordinate). + +Sorting can be beneficial for tasks like debugging, comparing data from +different runs, or ensuring deterministic behavior in algorithms that +process spatial units sequentially. } \examples{ -df <- dat_species_prob \%>\% - splnr_arrangeFeatures() +\dontrun{ +print("Original order:") +print(dat_species_prob) + +# Sort the features. +df_arranged <- splnr_arrangeFeatures(df = dat_species_prob) +print("Sorted order:") +print(df_arranged) +} } diff --git a/man/splnr_climate_featureApproach.Rd b/man/splnr_climate_featureApproach.Rd index de729f0..24d3be1 100644 --- a/man/splnr_climate_featureApproach.Rd +++ b/man/splnr_climate_featureApproach.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/utils-climate.R \name{splnr_climate_featureApproach} \alias{splnr_climate_featureApproach} -\title{Function to run the feature approach} +\title{Run the Feature Climate-Smart Approach} \usage{ splnr_climate_featureApproach( features, @@ -14,40 +14,92 @@ splnr_climate_featureApproach( ) } \arguments{ -\item{features}{feature \code{sf}object} +\item{features}{An \code{sf} object representing conservation features (e.g., species +distribution data).} -\item{metric}{climate metric \code{sf} object with 'metric' as the column name of the metric values per planning unit.} +\item{metric}{An \code{sf} object containing climate metric information. It must +have a column named 'metric' with the climate metric values per planning unit.} -\item{targets}{\code{data.frame}with list of features under "feature" column and their corresponding targets under "target" column} +\item{targets}{A \code{data.frame} with two columns: \code{feature} (character, listing +the original feature names) and \code{target} (numeric, the initial conservation +target for each feature as a proportion, e.g., 0.3).} -\item{direction}{If direction = 1, metric values are from low (least climate-smart) to high (most climate-smart). If direction = -1, metric values are from high (least climate-smart) to low (most climate-smart).} +\item{direction}{An integer specifying the direction of climate-smartness: +\itemize{ +\item \code{1}: Higher metric values mean more climate-smart areas. +\item \code{-1}: Lower metric values mean more climate-smart areas. +}} -\item{percentile}{cut-off threshold for determining whether an area is a climate priority area or not (e.g., lower 35th percentile of warming or upper 65th percentile of acidification). Note that the percentile here is the lower limit of the threshold.} +\item{percentile}{A numeric value (0-100) representing the cutoff threshold for +determining whether an area is a climate priority area or not. This is applied +globally to the \code{metric} data. Defaults to \code{35}.} -\item{refugiaTarget}{target assigned to climate-smart areas} +\item{refugiaTarget}{A numeric value (0-1) representing the target proportion +assigned to the overall climate-smart layer. Defaults to \code{0.3} (30\%).} } \value{ -A \code{list} with two components: 1. is the data frame passed to \code{prioritizr} when creating a conservation problem containing the binary information per planning unit per feature. 2. are the targets for the features in the conservation problem when the CPA approach is used. +A \code{list} with two components: +\itemize{ +\item \code{Features}: An \code{sf} object containing the binary information per +planning unit for each original feature, plus the new \code{climate_layer} +feature. This is ready to be passed to \code{prioritizr}. +\item \code{Targets}: A \code{data.frame} with the adjusted targets for all features, +including the \code{climate_layer}. This is also ready for \code{prioritizr}. +} } \description{ -Function to run the feature approach +\code{splnr_climate_featureApproach()} implements the Feature Approach to +climate-smart conservation planning. This involves defining a global +"climate-smart" layer and adjusting conservation targets to ensure that +a specified proportion of this layer is captured in the solution. +} +\details{ +This function orchestrates the steps for the Feature Approach: +\enumerate{ +\item \strong{Preprocessing:} It calls \code{splnr_climate_feature_preprocess()} to +identify a region-wide climate-smart layer based on a percentile cutoff +of the climate metric. This layer is then added as a new binary feature +to your conservation data. +\item \strong{Target Assignment:} It then calls \code{splnr_climate_feature_assignTargets()} +to calculate and assign new targets. Crucially, a specific \code{refugiaTarget} +is set for the newly created \code{climate_layer} feature, ensuring that a +certain proportion of the most climate-resilient areas are included in +the final conservation plan. +} + +The output is a list containing the modified features (now including the +\code{climate_layer}) and their corresponding adjusted targets, ready to be used +in a \code{prioritizr} conservation problem. } \examples{ -Features <- dat_species_bin +\dontrun{ +# Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects +# in your package. -targets <- Features \%>\% +# Define initial targets for species features. +initial_targets <- dat_species_bin \%>\% sf::st_drop_geometry() \%>\% colnames() \%>\% data.frame() \%>\% setNames(c("feature")) \%>\% dplyr::mutate(target = 0.3) -Feature_Approach <- splnr_climate_featureApproach( +# Run the Feature Approach where higher climate metric values mean +# more climate-smart areas. +Feature_Approach_result <- splnr_climate_featureApproach( features = dat_species_bin, metric = dat_clim, - targets = targets, - direction = 1 + targets = initial_targets, + direction = 1, # Example: higher metric values are more climate-smart + percentile = 35, + refugiaTarget = 0.3 ) -out_sf <- Feature_Approach$Features -targets <- Feature_Approach$Targets + +# Access the processed features and targets: +out_sf_feature <- Feature_Approach_result$Features +targets_feature <- Feature_Approach_result$Targets + +print(head(out_sf_feature)) +print(head(targets_feature)) +} } diff --git a/man/splnr_climate_percentileApproach.Rd b/man/splnr_climate_percentileApproach.Rd index 3a891c5..d3f93d5 100644 --- a/man/splnr_climate_percentileApproach.Rd +++ b/man/splnr_climate_percentileApproach.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/utils-climate.R \name{splnr_climate_percentileApproach} \alias{splnr_climate_percentileApproach} -\title{Function to run the percentile approach} +\title{Run the Percentile Climate-Smart Approach} \usage{ splnr_climate_percentileApproach( features, @@ -13,37 +13,88 @@ splnr_climate_percentileApproach( ) } \arguments{ -\item{features}{feature \code{sf}object} +\item{features}{An \code{sf} object representing conservation features (e.g., species +distribution data).} -\item{metric}{climate metric \code{sf} object with 'metric' as the column name of the metric values per planning unit.} +\item{metric}{An \code{sf} object containing climate metric information. It must +have a column named 'metric' with the climate metric values per planning unit.} -\item{targets}{\code{data.frame}with list of features under "feature" column and their corresponding targets under "target" column} +\item{targets}{A \code{data.frame} with two columns: \code{feature} (character, listing +the original feature names) and \code{target} (numeric, the initial conservation +target for each feature as a proportion, e.g., 0.3).} -\item{direction}{If direction = 1, metric values are from low (least climate-smart) to high (most climate-smart). If direction = -1, metric values are from high (least climate-smart) to low (most climate-smart).} +\item{direction}{An integer specifying the direction of climate-smartness: +\itemize{ +\item \code{1}: Higher metric values mean more climate-smart areas. +\item \code{-1}: Lower metric values mean more climate-smart areas. +}} -\item{percentile}{cut-off threshold for determining whether an area is a climate priority area or not (e.g., lower 35th percentile of warming or upper 65th percentile of acidification). Note that the percentile here is the lower limit of the threshold.} +\item{percentile}{A numeric value (0-100) representing the cutoff threshold for +determining whether an area is a climate priority area or not. This is applied +\emph{per feature} to its distribution. Defaults to \code{35}.} } \value{ -A \code{list} with two components: 1. is the data frame passed to \code{prioritizr} when creating a conservation problem containing the binary information per planning unit per feature. 2. are the targets for the features in the conservation problem when the CPA approach is used. +A \code{list} with two components: +\itemize{ +\item \code{Features}: An \code{sf} object containing the binary information per +planning unit for each feature, now filtered to include only its +climate-smart occurrences. This is ready to be passed to \code{prioritizr}. +\item \code{Targets}: A \code{data.frame} with the adjusted targets for the +filtered features. This is also ready for \code{prioritizr}. +} } \description{ -Function to run the percentile approach +\code{splnr_climate_percentileApproach()} implements the Percentile Approach to +climate-smart conservation planning. This involves filtering features to +their most climate-resilient areas and adjusting their conservation targets +to account for this reduced feature distribution. +} +\details{ +This function orchestrates the steps for the Percentile Approach: +\enumerate{ +\item \strong{Preprocessing:} It calls \code{splnr_climate_percentile_preprocess()} to +identify, for each feature, its occurrences within the most climate-resilient +\code{percentile} of its distribution based on a climate metric. This effectively +"filters" the feature data to only include its climate-smart components. +\item \strong{Target Assignment:} It then calls \code{splnr_climate_percentile_assignTargets()} +to calculate and assign new targets for these filtered features. The targets +are scaled up to ensure that the original conservation goals are still met, +but specifically by selecting areas from the climate-smart portions of the +features' distributions. +} + +The output is a list containing the modified features (filtered to their +climate-smart occurrences) and their corresponding adjusted targets, ready +to be used in a \code{prioritizr} conservation problem. } \examples{ +\dontrun{ +# Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects +# in your package. -targets <- dat_species_bin \%>\% +# Define initial targets for species features. +initial_targets <- dat_species_bin \%>\% sf::st_drop_geometry() \%>\% colnames() \%>\% data.frame() \%>\% setNames(c("feature")) \%>\% dplyr::mutate(target = 0.3) -Percentile_Approach <- splnr_climate_percentileApproach( +# Run the Percentile Approach where higher climate metric values mean +# more climate-smart areas. +Percentile_Approach_result <- splnr_climate_percentileApproach( features = dat_species_bin, metric = dat_clim, - targets = targets, - direction = 1 + targets = initial_targets, + direction = 1, # Example: higher metric values are more climate-smart + percentile = 35 ) -out_sf <- Percentile_Approach$Features -targets <- Percentile_Approach$Targets + +# Access the processed features and targets: +out_sf_percentile <- Percentile_Approach_result$Features +targets_percentile <- Percentile_Approach_result$Targets + +print(head(out_sf_percentile)) +print(head(targets_percentile)) +} } diff --git a/man/splnr_climate_priorityAreaApproach.Rd b/man/splnr_climate_priorityAreaApproach.Rd index d76d4fb..fc272ec 100644 --- a/man/splnr_climate_priorityAreaApproach.Rd +++ b/man/splnr_climate_priorityAreaApproach.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/utils-climate.R \name{splnr_climate_priorityAreaApproach} \alias{splnr_climate_priorityAreaApproach} -\title{Function to run the climate-priority-area approach} +\title{Run the Climate Priority Area (CPA) Approach} \usage{ splnr_climate_priorityAreaApproach( features, @@ -14,39 +14,94 @@ splnr_climate_priorityAreaApproach( ) } \arguments{ -\item{features}{feature \code{sf}object} +\item{features}{An \code{sf} object representing conservation features (e.g., species +distribution data). Each column (excluding geometry) should typically be a +binary representation of a feature's presence (1) or absence (0) in each +planning unit.} -\item{metric}{climate metric \code{sf} object with 'metric' as the column name of the metric values per planning unit.} +\item{metric}{An \code{sf} object containing climate metric information. It must +have a column named 'metric' with the climate metric values per planning unit.} -\item{targets}{\code{data.frame}with list of features under "feature" column and their corresponding targets under "target" column} +\item{targets}{A \code{data.frame} with two columns: \code{feature} (character, listing +the original feature names) and \code{target} (numeric, the initial conservation +target for each feature as a proportion, e.g., 0.3).} -\item{direction}{If direction = 1, metric values are from low (least climate-smart) to high (most climate-smart). If direction = -1, metric values are from high (least climate-smart) to low (most climate-smart).} +\item{direction}{An integer specifying the direction of climate-smartness: +\itemize{ +\item \code{1}: Higher metric values mean more climate-smart areas. +\item \code{-1}: Lower metric values mean more climate-smart areas. +}} -\item{percentile}{cut-off threshold for determining whether an area is a climate priority area or not (e.g., lower 35th percentile of warming or upper 65th percentile of acidification). Note that the percentile here is the lower limit of the threshold.} +\item{percentile}{A numeric value (0-100) representing the cutoff threshold for +determining climate-smart areas. For example, \code{percentile = 5} means the +most climate-smart 5\% of areas (based on \code{direction}) are considered. +This value represents the lower limit of the threshold. Defaults to \code{5}.} -\item{refugiaTarget}{target assigned to climate-smart areas} +\item{refugiaTarget}{A numeric value (0-1) representing the target proportion +assigned specifically to climate-smart areas (refugia). Defaults to \code{1} (100\%).} } \value{ -A \code{list} with two components: 1. is the data frame passed to \code{prioritizr} when creating a conservation problem containing the binary information per planning unit per feature. 2. are the targets for the features in the conservation problem when the CPA approach is used. +A \code{list} with two components: +\itemize{ +\item \code{Features}: An \code{sf} object containing the binary information per +planning unit for each feature, now split into \verb{_CS} (climate-smart) +and \verb{_NCS} (non-climate-smart) components. This is ready to be +passed to \code{prioritizr} when creating a conservation problem. +\item \code{Targets}: A \code{data.frame} with the adjusted targets for the +climate-split features. This is also ready for \code{prioritizr}. +} } \description{ -Function to run the climate-priority-area approach +\code{splnr_climate_priorityAreaApproach()} implements the Climate Priority Area +approach by splitting conservation features into climate-smart (CS) and +non-climate-smart (NCS) components and adjusting their targets accordingly. +This allows conservation planning to prioritize areas with higher climate resilience. +} +\details{ +This function orchestrates the steps required for the CPA approach: +\enumerate{ +\item \strong{Preprocessing:} It calls \code{splnr_climate_priorityArea_preprocess()} to +categorize each feature's occurrences into CS and NCS areas based on a +climate metric and a specified \code{percentile} cutoff. +\item \strong{Target Assignment:} It then calls \code{splnr_climate_priorityArea_assignTargets()} +to calculate and assign new targets for these CS and NCS feature components. +This ensures that conservation goals reflect the desired emphasis on climate-smart +areas (e.g., aiming for 100\% representation of features in highly resilient areas). +} + +The output of this function is a list containing the modified features (now +split into CS/NCS components) and their corresponding adjusted targets, ready +to be used in a \code{prioritizr} conservation problem. } \examples{ +\dontrun{ +# Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects +# in your package. -targets <- dat_species_bin \%>\% +# Define initial targets for species features. +initial_targets <- dat_species_bin \%>\% sf::st_drop_geometry() \%>\% colnames() \%>\% data.frame() \%>\% setNames(c("feature")) \%>\% dplyr::mutate(target = 0.3) -CPA_Approach <- splnr_climate_priorityAreaApproach( +# Run the Climate Priority Area Approach where lower climate metric +# values mean more climate-smart areas. +CPA_Approach_result <- splnr_climate_priorityAreaApproach( features = dat_species_bin, metric = dat_clim, - targets = targets, - direction = -1 + targets = initial_targets, + direction = -1, # Example: lower metric values are more climate-smart + percentile = 5, + refugiaTarget = 1 ) -out_sf <- CPA_Approach$Features -targets <- CPA_Approach$Targets + +# Access the processed features and targets: +out_sf_cpa <- CPA_Approach_result$Features +targets_cpa <- CPA_Approach_result$Targets + +print(head(out_sf_cpa)) +print(head(targets_cpa)) +} } diff --git a/man/splnr_create_polygon.Rd b/man/splnr_create_polygon.Rd index 172f4b2..dc43c68 100644 --- a/man/splnr_create_polygon.Rd +++ b/man/splnr_create_polygon.Rd @@ -2,24 +2,50 @@ % Please edit documentation in R/utils.R \name{splnr_create_polygon} \alias{splnr_create_polygon} -\title{Function for creating polygon} +\title{Create Spatial Polygon from Coordinates} \usage{ splnr_create_polygon(x, cCRS = "EPSG:4326") } \arguments{ -\item{x}{A named vector of lon/lat coordinates from which to make an \code{sf} polygon} +\item{x}{A \code{tibble} (or \code{tbl_df}) object with at least two columns, +typically named \code{x} (for longitude) and \code{y} (for latitude), representing +the vertices of the polygon in sequence. The first and last coordinate +pair should be the same to form a closed polygon.} -\item{cCRS}{The CRS to use for the polygon} +\item{cCRS}{A character string specifying the target CRS for the output polygon +in an EPSG code format (e.g., "EPSG:4326"). Defaults to "EPSG:4326" (WGS 84).} } \value{ -An \code{sf} object for the polygon +An \code{sf} object representing the created polygon, with the specified CRS. } \description{ -\code{splnr_create_polygon()} allows you to create a polygon based on longitude and latitude coordinates in your input data. +\code{splnr_create_polygon()} constructs an \code{sf} polygon object from a series +of longitude and latitude coordinates provided in a tibble. +} +\details{ +This utility function simplifies the creation of spatial polygons from a +tabular format of coordinates. It takes a tibble where columns 'x' and 'y' +represent longitude and latitude, respectively. These coordinates are +converted into a matrix, then to an \code{sf} polygon, and finally to an \code{sf} +object with the specified Coordinate Reference System (CRS). + +The function assumes that the input coordinates (\code{x}) are initially in +WGS 84 (EPSG:4326) and then transforms them to the \code{cCRS} if a different +CRS is specified. } \examples{ -splnr_create_polygon(x = dplyr::tibble(x = seq(-50, 50, by = 1), y = 120) \%>\% - dplyr::bind_rows(dplyr::tibble(x = 50, y = seq(120, 180, by = 1))) \%>\% - dplyr::bind_rows(dplyr::tibble(x = seq(50, -50, by = -1), y = 180)) \%>\% - dplyr::bind_rows(dplyr::tibble(x = -50, y = seq(150, 120, by = -1)))) +# Example: Create a simple square polygon +square_coords <- dplyr::tibble( + x = c(-50, 50, 50, -50, -50), + y = c(120, 120, 180, 180, 120) +) +simple_polygon <- splnr_create_polygon(x = square_coords) +print(simple_polygon) + +# Example: Create a polygon and transform to a different CRS (e.g., a UTM zone) +\dontrun{ +# Note: EPSG:32611 is UTM Zone 11N. Ensure it's appropriate for your coordinates. +transformed_polygon <- splnr_create_polygon(x = square_coords, cCRS = "EPSG:32611") +print(transformed_polygon) +} } diff --git a/man/splnr_featureNames.Rd b/man/splnr_featureNames.Rd index 93f5731..f5d4d8e 100644 --- a/man/splnr_featureNames.Rd +++ b/man/splnr_featureNames.Rd @@ -2,23 +2,55 @@ % Please edit documentation in R/utils.R \name{splnr_featureNames} \alias{splnr_featureNames} -\title{Returns the feature names} +\title{Extract Feature Names from Spatial Data} \usage{ splnr_featureNames(dat, exclude = NA) } \arguments{ -\item{dat}{sf dataframe of features} +\item{dat}{An \code{sf} dataframe representing conservation features. Each +non-geometry column is assumed to be a feature.} -\item{exclude}{Character vector of any columns to exclude} +\item{exclude}{A character vector of column names (or prefixes) to exclude +from the output. By default, it excludes columns starting with "Cost_". +If you provide a value, it will be \emph{appended} to the default exclusion. +Set to \code{NULL} or \code{character(0)} if you want no exclusions beyond the default.} } \value{ -A character vector of names +A character vector containing the names of the conservation features. } \description{ -\code{splnr_featureNames()} allows you to extract the names of features you want to pass to a \code{prioritizr} prioritization. -It requires an \code{sf} object input and returns the column names of the object excluding any columns you specify in the \code{exclude} argument. +\code{splnr_featureNames()} extracts the names of conservation features +from an \code{sf} dataframe, excluding geometry and any specified columns. +} +\details{ +This function is a utility for preparing data for \code{prioritizr} or other +conservation planning packages that require a vector of feature names. +It typically removes the geometry column and any columns related to cost +(prefixed with "Cost_") by default, allowing you to specify additional +columns to exclude. + +The output is a simple character vector of column names, which can be +directly used as feature identifiers in conservation problems. } \examples{ -df <- dat_species_prob \%>\% - splnr_featureNames() +\dontrun{ +# Assuming 'dat_species_prob' is an existing sf object in your package. +# It likely has columns like 'Spp1', 'Spp2', 'Cost_SomeMeasure', etc. + +# Example 1: Get all feature names, excluding default 'Cost_' columns. +feature_names_default <- splnr_featureNames(dat = dat_species_prob) +print(feature_names_default) + +# Example 2: Get feature names, excluding 'Cost_' columns and 'Spp5'. +feature_names_custom_exclude <- splnr_featureNames( + dat = dat_species_prob, + exclude = "Spp5" +) +print(feature_names_custom_exclude) + +# Example 3: If you only want to exclude a specific column and not 'Cost_' +# (you'd need to manually specify exclude = "geometry" and then your column) +# This case is more complex and usually handled by direct dplyr::select. +# This function's primary use is to remove cost columns and potentially others. +} } diff --git a/man/splnr_get_IUCNRedList.Rd b/man/splnr_get_IUCNRedList.Rd index a20c7ff..744f0bd 100644 --- a/man/splnr_get_IUCNRedList.Rd +++ b/man/splnr_get_IUCNRedList.Rd @@ -2,44 +2,71 @@ % Please edit documentation in R/splnr_get_IUCNRedList.R \name{splnr_get_IUCNRedList} \alias{splnr_get_IUCNRedList} -\title{Match Species to IUCN RedList} +\title{Match Species to IUCN RedList Categories} \usage{ splnr_get_IUCNRedList(df, species_col = "Species") } \arguments{ -\item{df}{The dataframe containing the species to be matched with the IUCN redlist} +\item{df}{The input dataframe containing the species names to be matched.} -\item{species_col}{A string name for the column containting the species name} +\item{species_col}{A character string specifying the name of the column in \code{df} +that contains the species scientific names (e.g., "Species" or "scientific_name"). +Defaults to "Species".} } \value{ -A dataframe with an additional column \code{IUCN_Category} +A dataframe identical to the input \code{df}, but with an additional column +named \code{IUCN_Category}. If a species is not found on the IUCN Red List, its +\code{IUCN_Category} will be \code{NA}. } \description{ -First of all you will need your own API key, an alphanumeric string provided by IUCN that you need to send in every request; -the following function takes you to their website, where you will need to fill up a form (it might take 1-2 days to receive your key) -rl_use_iucn() -Once you receive an email with your API key, set it up as an environmental variable (it MUST be named IUCN_REDLIST_KEY) -you will need to re-do this step everytime you restart R -Sys.setenv(IUCN_REDLIST_KEY = "") OR add IUCN_REDLIST_KEY = "" to your .Renviron file to permanently set it -Sys.getenv("IUCN_REDLIST_KEY") #' check -Not Evaluated -DD: Data Deficient -LC: Least Concern -NT: Near Threatened -VU: Vulnerable -EN: Endangered -CR: Critically Endangered -EW: Extinct in the Wild -EX: Extinct -LRlc: Low risk – least concern -LRnt: Low risk – near threatened -LRcd: Low risk - conservation dependent -Categories we care about -cate <- c("EX","EW","CR","EN","VU") +The \code{splnr_get_IUCNRedList} function retrieves IUCN Red List category information +for a given set of species and appends it to your input dataframe. +} +\details{ +To use this function, you must first obtain an API key from IUCN. This is an +alphanumeric string required for every request. You can visit the IUCN website +to request a key using \code{rl_use_iucn()}. Please note that receiving your key +might take 1-2 days after submitting the form. + +Once you receive your API key, it is crucial to set it as an environment variable +named \code{IUCN_REDLIST_KEY}. You can do this temporarily for the current R session +using \code{Sys.setenv(IUCN_REDLIST_KEY = "YOUR_API_KEY_HERE")}. To set it permanently, +you should add \code{IUCN_REDLIST_KEY = "YOUR_API_KEY_HERE"} to your \code{.Renviron} file. +You can check if the key is set correctly using \code{Sys.getenv("IUCN_REDLIST_KEY")}. + +The IUCN Red List uses various categories to assess extinction risk. This function +queries the Red List for the following categories: +\itemize{ +\item \strong{DD}: Data Deficient +\item \strong{LC}: Least Concern +\item \strong{NT}: Near Threatened +\item \strong{VU}: Vulnerable +\item \strong{EN}: Endangered +\item \strong{CR}: Critically Endangered +\item \strong{EW}: Extinct in the Wild +\item \strong{EX}: Extinct +\item \strong{LRlc}: Lower Risk / least concern (old category) +\item \strong{LRnt}: Lower Risk / near threatened (old category) +\item \strong{LRcd}: Lower Risk / conservation dependent (old category) +} +The function will attempt to match your species against any of these categories +present in the IUCN Red List database. } \examples{ \dontrun{ -df <- data.frame(Species = c("Diomedea exulans", "Hippocampus kuda", "Squatina squatina")) \%>\% +# Ensure your IUCN_REDLIST_KEY is set as an environment variable before running. +# For example: Sys.setenv(IUCN_REDLIST_KEY = "YOUR_API_KEY_HERE") + +# Example: Create a dataframe with species names and retrieve their IUCN Red List categories. +df_species_redlist <- data.frame(Species = c("Diomedea exulans", + "Hippocampus kuda", + "Squatina squatina")) \%>\% splnr_get_IUCNRedList() +print(df_species_redlist) + +# Example with a different column name for species +df_alt_col <- data.frame(ScientificName = c("Panthera leo", "Orcinus orca")) \%>\% + splnr_get_IUCNRedList(species_col = "ScientificName") +print(df_alt_col) } } diff --git a/man/splnr_get_MPAs.Rd b/man/splnr_get_MPAs.Rd index f221eed..e184717 100644 --- a/man/splnr_get_MPAs.Rd +++ b/man/splnr_get_MPAs.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/splnr_get_MPAs.R \name{splnr_get_MPAs} \alias{splnr_get_MPAs} -\title{Get marine parks from the WDPA.} +\title{Get Marine Protected Areas (MPAs) from WDPA} \usage{ splnr_get_MPAs( PlanUnits, @@ -14,31 +14,82 @@ splnr_get_MPAs( ) } \arguments{ -\item{PlanUnits}{Planning Units as an \code{sf} object} +\item{PlanUnits}{An \code{sf} object representing the planning units to be used for intersection. +This object should have a valid CRS defined.} -\item{Countries}{A character vector of the countries for which to extract MPAs. To get all MPAs, use \code{"global"} here.} +\item{Countries}{A character vector specifying the countries for which to extract MPAs. +To retrieve all global MPAs, use the value \code{"global"}. Country names should match +those recognized by the WDPA database.} -\item{Status}{The status field in the WDPA provides information on whether a protected area has been established, designated, or proposed at the time the data was submitted.} +\item{Status}{A character vector specifying the desired status of protected areas +to include. Defaults to \code{c("Designated", "Established", "Inscribed")}.} -\item{Desig}{The designation type is the category or type of protected area as legally/officially designated or proposed.} +\item{Desig}{A character vector specifying the desired designation types of +protected areas. Defaults to \code{c("National", "Regional", "International", "Not Applicable")}.} -\item{Category}{Stores the IUCN Protected Area Management Categories (recorded in field IUCN_CAT) for each of the protected areas where these categories are reported} +\item{Category}{A character vector specifying the desired IUCN Protected Area +Management Categories. Defaults to \code{c("Ia", "Ib", "II", "III", "IV")}.} -\item{...}{Other arguments passed to \code{wdpa_fetch()}} +\item{...}{Other arguments that are passed directly to the \code{wdpa_fetch()} function +from the \code{wdpar} package (e.g., \code{verbose = TRUE}).} } \value{ -A \code{sf} object with the MPAs intersected with the planning units +An \code{sf} object. This object contains the planning units, with an +additional \code{wdpa} column (set to 1) for areas that intersect with the +selected MPAs. } \description{ -This code is a wrapper for the wonderful \code{wdpar} package written by Jeffrey O. Hanson. This data is then interfaced with the planning units. -An \code{sf} object is returned with the PU area covered by the selected marine protected areas. +This function serves as a wrapper for the \code{wdpar} package, facilitating the +retrieval of Marine Protected Areas (MPAs) from the World Database on Protected +Areas (WDPA) and intersecting them with provided planning units. +The result is an \code{sf} object indicating the area of planning units covered by +the selected marine protected areas. +} +\details{ +This function leverages the robust capabilities of the \code{wdpar} package by +Jeffrey O. Hanson to access and process WDPA data. It allows filtering of MPAs +based on country, status, designation type, and IUCN category, and then +spatially intersects these MPAs with your defined planning units. + +For a comprehensive understanding of the WDPA data fields: +\itemize{ +\item \strong{Status}: Refers to the establishment, designation, or proposal +status of a protected area at the time of data submission. Valid options +include "Designated", "Established", "Inscribed", "Proposed", and "Adopted". +\item \strong{Desig} (Designation Type): Categorizes the legal or official +designation of the protected area. Valid options include "National", +"Regional", "International", and "Not Applicable". +\item \strong{Category} (IUCN Protected Area Management Categories): Represents +the IUCN management categories for protected areas. Valid options include +"Ia", "Ib", "II", "III", "IV", "V", "VI", "Not Reported", "Not Applicable", +and "Not Assigned". +} } \examples{ -dat <- splnr_get_MPAs(PlanUnits = dat_PUs, Countries = "Australia") +\dontrun{ +# Assuming 'dat_PUs' is an existing sf object of planning units in your package. + +# Example: Get MPAs for Australia and intersect with planning units. +dat_mpas <- splnr_get_MPAs(PlanUnits = dat_PUs, Countries = "Australia") +# Example: Get MPAs for multiple countries with specific status and categories. +dat_mpas_specific <- splnr_get_MPAs( + PlanUnits = dat_PUs, + Countries = c("Australia", "New Zealand"), + Status = c("Designated", "Proposed"), + Category = c("II", "IV") +) + +# Example: Visualize the result using ggplot2. +# Assuming 'aust' is an sf object representing Australia's coastline, +# perhaps loaded from rnaturalearth::ne_countries. aust <- rnaturalearth::ne_countries(country = "Australia", returnclass = "sf") gg <- ggplot2::ggplot() + - ggplot2::geom_sf(data = dat, ggplot2::aes(fill = wdpa)) + - ggplot2::geom_sf(data = aust, fill = "grey50") + ggplot2::geom_sf(data = dat_mpas, ggplot2::aes(fill = wdpa)) + + ggplot2::geom_sf(data = aust, fill = "grey50") + + ggplot2::labs(title = "Marine Protected Areas in Australia") + + ggplot2::theme_minimal() +print(gg) +} } diff --git a/man/splnr_get_boundary.Rd b/man/splnr_get_boundary.Rd index a7b1c7c..a6aa7e2 100644 --- a/man/splnr_get_boundary.Rd +++ b/man/splnr_get_boundary.Rd @@ -2,27 +2,64 @@ % Please edit documentation in R/splnr_get_boundary.R \name{splnr_get_boundary} \alias{splnr_get_boundary} -\title{Get the boundary of the planning region.} +\title{Create a Planning Region Boundary} \usage{ splnr_get_boundary(Limits, Type = NULL, res = 1, cCRS = "ESRI:54009") } \arguments{ -\item{Limits}{The limits of the boundary. This can either be a 4 element numeric named vector (c(xmin = 150, xmax = 160, ymin = -40, ymax = -30)), a vector of ocean/sea names, or a vector of EEZs.,} +\item{Limits}{A required input that defines the spatial extent. This can be: +\itemize{ +\item A named numeric vector of four elements: \code{c("xmin" = ..., "xmax" = ..., "ymin" = ..., "ymax" = ...)}. +\item The string \code{"Global"} to create a worldwide boundary. +\item A character vector of ocean/sea names (e.g., \code{"North Atlantic Ocean"}) to be used with \code{Type = "Ocean"}. +}} -\item{Type}{The type of Limits being provided. Options are "Ocean" or "EEZ". (not required if numeric or "Global" limits are provided)} +\item{Type}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} The type of Limits being provided. This is only required if \code{Limits} is a character vector of ocean names, in which case it should be \code{"Ocean"}. It is no longer required and will be removed in a future version.} -\item{res}{The resolution (in degrees) from which to create the boundary polygon if numeric limits are provided.} +\item{res}{\verb{[numeric(1)]}\cr The resolution (in decimal degrees) used to +construct the polygon vertices when \code{Limits} is numeric or \code{"Global"}. +Defaults to \code{1}. Must be a positive number.} -\item{cCRS}{The CRS the boundary is to be returned in} +\item{cCRS}{\verb{[character(1)]}\cr The coordinate reference system (CRS) for the +output \code{sf} object. Can be a PROJ4 string or an EPSG code. Defaults to +\code{"ESRI:54009"} (Mollweide).} } \value{ -The boundary of the planning region +An \code{sf} object containing a single polygon feature representing the +planning boundary. } \description{ -\code{splnr_get_boundary()} allows to create an \code{sf} object of your planning region either based on specific coordinate information, or \code{rnaturalearth} inputs such as ocean data. Creating a boundary is often the first step in conservation planning and a requirement for downstream function sin \code{spatialplanr}. +This function generates a spatial boundary for the planning region as an \code{sf} +polygon object. The boundary can be defined in several ways: +\enumerate{ +\item A simple rectangular bounding box using numeric coordinates. +\item A global boundary spanning the entire world. +\item A complex shape based on marine ecoregions from \code{rnaturalearth}. +} +} +\details{ +A planning region boundary is the foundational first step for most spatial +conservation planning exercises. All subsequent analyses and data preparation +steps within the \code{spatialplanr} package rely on a defined boundary. The +coordinate reference system (CRS) of the returned object is projected by +default (Mollweide), which is suitable for equal-area calculations. } \examples{ -Bndry <- splnr_get_boundary(Limits = "North Atlantic Ocean", Type = "Ocean") -Bndry <- splnr_get_boundary(Limits = "Global") -Bndry <- splnr_get_boundary(Limits = c("xmin" = 150, "xmax" = 170, "ymin" = -40, "ymax" = -20)) +\dontrun{ +# Example 1: Create a boundary from an ocean name. +# This fetches polygon data for the specified ocean. +bndry_ocean <- splnr_get_boundary(Limits = "North Atlantic Ocean", Type = "Ocean") +plot(bndry_ocean) + +# Example 2: Create a global boundary. +bndry_global <- splnr_get_boundary(Limits = "Global") +plot(bndry_global) + +# Example 3: Create a boundary from a numeric bounding box. +bndry_coords <- splnr_get_boundary( + Limits = c("xmin" = 150, "xmax" = 170, "ymin" = -40, "ymax" = -20) +) +plot(bndry_coords) +} } +\concept{planning_region} diff --git a/man/splnr_get_distCoast.Rd b/man/splnr_get_distCoast.Rd index 3bcc6d4..f4fce31 100644 --- a/man/splnr_get_distCoast.Rd +++ b/man/splnr_get_distCoast.Rd @@ -2,47 +2,68 @@ % Please edit documentation in R/splnr_get_distCoast.R \name{splnr_get_distCoast} \alias{splnr_get_distCoast} -\title{Function to compute distances to nearest coastline for each centroid of each planning unit in the 'sf' object provided.} +\title{Calculate Distance to Coastline} \usage{ -splnr_get_distCoast(dat_sf, custom_coast = NULL, res = NULL) +splnr_get_distCoast(dat_sf, custom_coast = NULL, res = "medium") } \arguments{ -\item{dat_sf}{An sf object.} +\item{dat_sf}{\verb{[sf]} \cr An \code{sf} object containing polygon or point features +representing the planning units. Must have a valid CRS.} -\item{custom_coast}{An sf coastline object (optional)} +\item{custom_coast}{\verb{[sf]} \cr An optional \code{sf} object representing a +custom coastline. If \code{NULL} (the default), the coastline is downloaded +from \code{rnaturalearth}.} -\item{res}{Allow user to choose resolution (\code{small}, \code{medium}, \code{large}) of \code{rnaturalearth} data used for coastline.} +\item{res}{\verb{[character(1)]} \cr The resolution of the \code{rnaturalearth} +coastline to use. Options are \code{"small"}, \code{"medium"} (default), or +\code{"large"}. This parameter is ignored if \code{custom_coast} is provided.} } \value{ -An \code{sf} object with distances to the nearest coast +An \code{sf} object identical to \code{dat_sf} but with an added column +\code{coastDistance_km} representing the distance to the nearest coastline in +kilometers. } \description{ -The code takes a sf object and return it updated with a new coastDistance column. -The output inherits the crs from this sf object so ensure it is in the correct projection for your needs +This function calculates the shortest distance from the centroid of each +planning unit in an \code{sf} object to the nearest coastline. It can use either +a default coastline from the \code{rnaturalearth} package or a custom-provided +coastline \code{sf} object. } \details{ -Written by Kristine Buenafe -Written: March/April 2023 -Modified by Kilian Barreiro -Updated: December 2023 +The function adds a new column named \code{coastDistance_km} to the input \code{sf} +object, containing the calculated distances in kilometers. The CRS of the +input data is preserved. It is crucial to ensure the input \code{sf} object has +a suitable projected CRS for accurate distance calculations. } \examples{ +\dontrun{ +# Example 1: Calculate distance to coast for a simple grid bbox <- sf::st_bbox(c(xmin = 0, ymin = 0, xmax = 3, ymax = 3)) -grid <- sf::st_make_grid(bbox, n = c(3, 3), what = "polygons") -grid <- sf::st_sf(geometry = grid) \%>\% - sf::st_set_crs("EPSG:4326") -splnr_get_distCoast(grid) +grid <- sf::st_as_sf(sf::st_make_grid(bbox, n = c(3, 3))) +grid_with_dist <- splnr_get_distCoast(grid) +plot(grid_with_dist["coastDistance_km"]) -cCRS <- "ESRI:54009" - -Bndry <- splnr_get_boundary(Limits = "Coral Sea", - Type = "Oceans", - cCRS = cCRS) +# Example 2: Using a specific resolution for the coastline +# Note: Requires the 'dat_sf' object to be created first, e.g., using +# splnr_get_planning_units() +if (exists("dat_sf")) { + dat_sf_dist <- splnr_get_distCoast(dat_sf, res = "large") + summary(dat_sf_dist$coastDistance_km) +} +# Example 3: Using a custom coastline +# First, create a custom coastline (e.g., from a country polygon) landmass <- rnaturalearth::ne_countries( scale = "medium", returnclass = "sf" -) \%>\% - sf::st_transform(cCRS) +) +if (exists("dat_sf") && exists("landmass")) { + # Transform landmass to the same CRS as the planning units + landmass_proj <- sf::st_transform(landmass, sf::st_crs(dat_sf)) + dat_sf_custom_coast <- splnr_get_distCoast(dat_sf, custom_coast = landmass_proj) + summary(dat_sf_custom_coast$coastDistance_km) +} +} } +\concept{cost_features} diff --git a/man/splnr_get_featureRep.Rd b/man/splnr_get_featureRep.Rd index 90dd70e..19b80d2 100644 --- a/man/splnr_get_featureRep.Rd +++ b/man/splnr_get_featureRep.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/splnr_featureRep.R \name{splnr_get_featureRep} \alias{splnr_get_featureRep} -\title{Prepare data to plot how well targets are met} +\title{Prepare Data to Plot How Well Targets Are Met} \usage{ splnr_get_featureRep( soln, @@ -14,27 +14,97 @@ splnr_get_featureRep( ) } \arguments{ -\item{soln}{The \code{prioritizr} solution} +\item{soln}{An \code{sf} object representing the \code{prioritizr} solution, containing +a column indicating selected planning units (default: \code{solution_1}).} -\item{pDat}{The \code{prioritizr} problem} +\item{pDat}{A \code{prioritizr} problem object, as defined by \code{prioritizr::problem()}. +This object provides the original feature data and targets.} -\item{targets}{\code{data.frame}with list of features under "feature" column and their corresponding targets under "target" column} +\item{targets}{A \code{data.frame} (optional). If provided, it should contain a +\code{feature} column (character) and a \code{target} column (numeric). This is used +to override or supplement targets from \code{pDat}, especially for climate-smart +approaches where targets might be pre-adjusted. Defaults to \code{NA}.} -\item{climsmart}{logical denoting whether spatial planning was done climate-smart (and targets have to be calculated differently)} +\item{climsmart}{A logical value (\code{TRUE} or \code{FALSE}). If \code{TRUE}, special +handling for climate-smart approaches is enabled. Defaults to \code{FALSE}.} -\item{climsmartApproach}{either 0,1,2 or 3 depending on the climate-smart approach used (0 = None; 1 = Climate Priority Area; 2 = Feature; 3 = Percentile).} +\item{climsmartApproach}{An integer (0, 1, 2, or 3) indicating the type of +climate-smart approach used: +\itemize{ +\item \code{0}: No climate-smart approach. +\item \code{1}: Climate Priority Area approach (features split into CS/NCS). +\item \code{2}: Feature approach (not explicitly handled in this function's +\code{climsmart} logic, targets taken from \code{pDat} by default). +\item \code{3}: Percentile approach (features are filtered). +} +Defaults to \code{0}.} -\item{solnCol}{Name of the column with the solution} +\item{solnCol}{A character string specifying the name of the column in \code{soln} +that contains the binary solution (1 for selected, 0 for not selected). +Defaults to \code{"solution_1"}.} } \value{ -\code{tbl_df} dataframe +A \code{tibble} dataframe containing the \code{feature} names, their +\code{total_amount} (total units available), \code{absolute_held} (total units +selected), \code{relative_held} (proportion held), \code{target} (conservation target), +and \code{incidental} (TRUE if target was 0 or NA, but feature still present). } \description{ -Prepare data to plot how well targets are met +\code{splnr_get_featureRep()} calculates the representation of conservation +features within a \code{prioritizr} solution. This function determines how much +of each feature's total abundance (or area) is captured in the selected +planning units, and compares it against specified conservation targets. +It can also account for different climate-smart planning approaches. +} +\details{ +This function processes the output of a \code{prioritizr} conservation problem +(\code{soln}) and its corresponding problem definition (\code{pDat}) to provide a +summary of feature representation. It is designed to work whether or not +explicit targets are provided, and can adjust calculations based on the +climate-smart approach used. + +The function calculates: +\itemize{ +\item \code{total_amount}: The total available amount/area of each feature across all planning units. +\item \code{absolute_held}: The total amount/area of each feature captured in the +\emph{selected} planning units (where \code{solution_1} is 1). +\item \code{relative_held}: The proportion of \code{absolute_held} relative to \code{total_amount}, +indicating the percentage representation of the feature in the solution. +\item \code{target}: The conservation target for each feature (either from the +\code{pDat} problem definition or the \code{targets} dataframe). +\item \code{incidental}: A logical flag indicating if a feature's representation +was 'incidental' (i.e., its target was 0 or NA, but it was still +partially or fully captured in the solution). +} + +\strong{Climate-Smart Considerations (\code{climsmart = TRUE}):} +If \code{climsmart} is \code{TRUE}, the function adjusts its calculations based on the +\code{climsmartApproach} parameter: +\itemize{ +\item \code{climsmartApproach = 1} (Climate Priority Area): The function sums the +\code{absolute_held} and \code{total_amount} for features that were split into +\verb{_CS} (Climate-Smart) and \verb{_NCS} (Non-Climate-Smart) components. This +provides a single, aggregated representation for the original feature, +allowing comparison with its original target. +\item \code{climsmartApproach = 3} (Percentile Approach): The function directly +uses the targets provided in the \code{targets} dataframe, which are +expected to be adjusted for the percentile approach. +\item For other \code{climsmartApproach} values or if \code{climsmart} is \code{FALSE}, +targets are taken directly from the \code{prioritizr} problem's target data. +} + +The output dataframe is designed to be directly plottable by functions +like \code{splnr_plot_featureRep()}. } \examples{ -pDat <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), - features = c("Spp1", "Spp2", "Spp3"), +\dontrun{ +# Assuming 'dat_species_bin' is an existing sf object with binary species data +# and 'Cost' column. + +# Create a dummy prioritizr problem for basic demonstration +pDat_basic <- prioritizr::problem( + dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), + features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" ) \%>\% prioritizr::add_min_set_objective() \%>\% @@ -42,11 +112,62 @@ pDat <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = prioritizr::add_binary_decisions() \%>\% prioritizr::add_default_solver(verbose = FALSE) -soln <- pDat \%>\% +# Solve the problem +soln_basic <- pDat_basic \%>\% prioritizr::solve.ConservationProblem() -df <- splnr_get_featureRep( - soln = soln, - pDat = pDat +# Get feature representation for a basic (non-climate-smart) solution +df_basic_rep <- splnr_get_featureRep( + soln = soln_basic, + pDat = pDat_basic +) +print(df_basic_rep) + +# Example with Climate Priority Area (CPA) approach +# Assuming 'dat_clim' is an sf object with a 'metric' column. +# These would typically come from splnr_climate_priorityAreaApproach() +# For example purposes, we'll create some dummy data and targets. + +# Simulate CPA processed features and targets +cpa_features_sim <- dat_species_bin \%>\% + dplyr::mutate( + Spp1_CS = ifelse(Spp1 == 1 & runif(n()) < 0.5, 1, 0), + Spp1_NCS = ifelse(Spp1 == 1 & Spp1_CS == 0, 1, 0), + Spp2_CS = ifelse(Spp2 == 1 & runif(n()) < 0.6, 1, 0), + Spp2_NCS = ifelse(Spp2 == 1 & Spp2_CS == 0, 1, 0), + Spp3_CS = ifelse(Spp3 == 1 & runif(n()) < 0.7, 1, 0), + Spp3_NCS = ifelse(Spp3 == 1 & Spp3_CS == 0, 1, 0) + ) \%>\% + dplyr::select(Spp1_CS, Spp1_NCS, Spp2_CS, Spp2_NCS, Spp3_CS, Spp3_NCS, geometry) + +cpa_targets_sim <- data.frame( + feature = c("Spp1_CS", "Spp1_NCS", "Spp2_CS", "Spp2_NCS", "Spp3_CS", "Spp3_NCS"), + target = c(0.8, 0.2, 0.9, 0.1, 0.7, 0.3) # Example targets for CS/NCS parts ) + +# Create a problem with the simulated CPA features +pDat_cpa_sim <- prioritizr::problem( + cpa_features_sim \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), + features = c("Spp1_CS", "Spp1_NCS", "Spp2_CS", "Spp2_NCS", "Spp3_CS", "Spp3_NCS"), + cost_column = "Cost" +) \%>\% + prioritizr::add_min_set_objective() \%>\% + prioritizr::add_relative_targets(cpa_targets_sim$target, cpa_targets_sim$feature) \%>\% + prioritizr::add_binary_decisions() \%>\% + prioritizr::add_default_solver(verbose = FALSE) + +# Solve the CPA problem +soln_cpa_sim <- pDat_cpa_sim \%>\% + prioritizr::solve.ConservationProblem() + +# Get feature representation for CPA approach +df_cpa_rep <- splnr_get_featureRep( + soln = soln_cpa_sim, + pDat = pDat_cpa_sim, + targets = cpa_targets_sim, # Pass the original CPA targets + climsmart = TRUE, + climsmartApproach = 1 # Indicate CPA approach +) +print(df_cpa_rep) +} } diff --git a/man/splnr_get_gfw.Rd b/man/splnr_get_gfw.Rd index 85dc2cd..c99b60d 100644 --- a/man/splnr_get_gfw.Rd +++ b/man/splnr_get_gfw.Rd @@ -2,8 +2,7 @@ % Please edit documentation in R/splnr_get_gfw.R \name{splnr_get_gfw} \alias{splnr_get_gfw} -\title{The \code{get_gfwData} function recover the data of Global Fishing Watch and -returns it as a sf object.} +\title{Retrieve Global Fishing Watch Data} \usage{ splnr_get_gfw( region, @@ -18,54 +17,88 @@ splnr_get_gfw( ) } \arguments{ -\item{region}{Region studied (character) or a geojson shape to filter raster} +\item{region}{A character string specifying the name of the region (e.g., an EEZ name) +or a numeric ID for the region, or an \code{sf} object if \code{region_source} is set +to "USER_SHAPEFILE".} -\item{start_date}{Start date (waited format : "\%Y-\%m-\%d").} +\item{start_date}{The start date for data retrieval, expected in "\%Y-\%m-\%d" format (e.g., "2021-01-01").} -\item{end_date}{End date (waited format : "\%Y-\%m-\%d").} +\item{end_date}{The end date for data retrieval, expected in "\%Y-\%m-\%d" format (e.g., "2022-12-31").} -\item{temp_res}{Temporal resolution ("daily","monthly","yearly").} +\item{temp_res}{The desired temporal resolution for the data. Must be one of: +"DAILY", "MONTHLY", or "YEARLY".} -\item{spat_res}{Spatial resolution ("low" for 0.1 degree, "high" for 0.01 degree).} +\item{spat_res}{The desired spatial resolution for the data. Must be one of: +"LOW" (0.1 degree) or "HIGH" (0.01 degree). Defaults to "LOW".} -\item{region_source}{source of the region ('eez','mpa', 'rfmo' or 'user_json')} +\item{region_source}{The source of the region definition. Must be one of: +'EEZ', 'MPA', 'RFMO', or 'USER_SHAPEFILE'. Defaults to "EEZ".} -\item{key}{Token for GFW API (see details GlobalFishingWatch vignette).} +\item{key}{Your API token for the GFW API. If not provided, it attempts to +authenticate using \code{gfwr::gfw_auth()}. See the GlobalFishingWatch vignette +for details on obtaining a key.} -\item{cCRS}{The crs to which the sf will be returned (default = "EPSG:4326").} +\item{cCRS}{The Coordinate Reference System (CRS) to which the output \code{sf} object +will be transformed. Defaults to "EPSG:4326".} -\item{compress}{Binary operator to compress (aggregate) the data per coordinates (default = FALSE).} +\item{compress}{A logical value. If \code{TRUE}, the data will be compressed (aggregated) +by coordinates, summing fishing hours for each unique location. If \code{FALSE}, +the raw data points are returned. Defaults to \code{FALSE}.} } \value{ -An \code{sf} object with gfw data. +An \code{sf} object containing the requested GFW data. The structure of +the \code{sf} object will vary depending on the \code{compress} and \code{temp_res} +parameters. } \description{ -The possibilities offered by this function are explained in \code{vignette("GlobalFishingWatch")} +The \code{splnr_get_gfw} function retrieves Global Fishing Watch (GFW) data and +returns it as an \code{sf} (simple features) object. This function allows for +flexible data queries based on geographical region, time range, and desired +spatial and temporal resolutions. } \details{ -We have the same parameters than the \code{get_raster} function, plus \code{cCRS} -which is the crs for the sf_modification \if{html}{\out{
}} -Different possible values can be combined and are : \if{html}{\out{
}} +The possibilities offered by this function are extensively explained in +\code{vignette("GlobalFishingWatch")}. + +This function shares many parameters with the \code{get_raster} function from the +\code{gfwr} package, with the addition of \code{cCRS} for specifying the Coordinate +Reference System of the output \code{sf} object. + +Fishing activity data can be aggregated (\code{group_by}) by "FLAGANDGEARTYPE" +by default, combining flags and gear types. + +\strong{Notes:} \itemize{ -\item \verb{Time Range}, \code{Flag}, \code{Geartype}. \if{html}{\out{
}} -\strong{(A combination can be : c('Time Range','Geartype'), if you want to get} -\strong{the sum of fishing hours per date and geartype, for example you want to} -\strong{display the drifting longline fishing in a specific year)} \if{html}{\out{
}} \if{html}{\out{
}} -\strong{Notes :} \if{html}{\out{
}} +\item Currently, the function is primarily designed for data within +Exclusive Economic Zones (EEZs), but it can potentially be +extended to specific Marine Protected Areas (MPAs) or RFMOs. +\item Days specified in the \code{start_date} and \code{end_date} variables are +inclusive in the data recovery. } -\enumerate{ -\item For the moment we are limited to the EEZs of each region, but we can -potentially restrict the working area to specific MPAs. \if{html}{\out{
}} -\item Days indicated in the__ \code{start_date} \strong{and} \code{end_date} __variables are -included in the data recovery. -} - -The code takes several parameters described below and return an sf object -with gfw data aggregated or not (param compress) } \examples{ \dontrun{ -gfw_data <- splnr_get_gfw('Australia', "2021-01-01", "2022-12-31", "YEARLY", - cCRS = "ESRI:54009", compress = TRUE) +# Example: Retrieve yearly GFW data for Australia, transformed to a +# Mollweide projection (ESRI:54009) and compressed (aggregated) by location. +gfw_data <- splnr_get_gfw( + region = 'Australia', + start_date = "2021-01-01", + end_date = "2022-12-31", + temp_res = "YEARLY", + cCRS = "ESRI:54009", + compress = TRUE +) + +# Example: Retrieve monthly GFW data for a specific EEZ ID, +# keeping individual time ranges and locations. +# Note: Replace 1000 with an actual EEZ ID if needed for testing. +gfw_data_monthly <- splnr_get_gfw( + region = 1000, # Example numeric EEZ ID + start_date = "2022-01-01", + end_date = "2022-03-31", + temp_res = "MONTHLY", + region_source = "EEZ", + compress = FALSE +) } } diff --git a/man/splnr_get_kappaCorrData.Rd b/man/splnr_get_kappaCorrData.Rd index a99c9fc..342a371 100644 --- a/man/splnr_get_kappaCorrData.Rd +++ b/man/splnr_get_kappaCorrData.Rd @@ -2,28 +2,53 @@ % Please edit documentation in R/utils.R \name{splnr_get_kappaCorrData} \alias{splnr_get_kappaCorrData} -\title{Prepare data to plot Cohen's Kappa correlation matrix} +\title{Prepare Data to Plot Cohen's Kappa Correlation Matrix} \usage{ splnr_get_kappaCorrData(sol, name_sol) } \arguments{ -\item{sol}{List of \code{prioritizr} solutions (\code{sf} objects) with solutions having a column name \code{solution_1}} +\item{sol}{A \code{list} of \code{prioritizr} solution objects. Each element in the list +must be an \code{sf} object containing a binary column named \code{solution_1}.} -\item{name_sol}{Name tags to the different solutions} +\item{name_sol}{A character vector providing descriptive names for each +solution in the \code{sol} list. The length of this vector must match the +length of \code{sol}. These names will be used as row and column names in the +output correlation matrix.} } \value{ -\code{matrixOut} matrix +A numeric \code{matrix} (\code{matrixOut}) representing the Cohen's Kappa +correlation matrix between all pairs of solutions. Rows and columns are +named according to \code{name_sol}. } \description{ -Conservation planning often requires the comparison of the outputs of the solutions of different conservation problems. -One way to compare solutions is by correlating the solutions using Cohen's Kappa. -\code{splnr_get_kappaCorrData()} takes a list of \code{prioritizr} solutions to perform the Cohen's Kappa correlation between the solution. -The resulting correlation matrix is symmetrical along the main diagonal and contains Cohen's Kappa of pairwise correlation between the solutions. -The main diagonal should always be 1. The correlation matrix obtained from this function can be passed onto \code{\link[=splnr_plot_corrMat]{splnr_plot_corrMat()}}. +\code{splnr_get_kappaCorrData()} calculates Cohen's Kappa correlation coefficients +between a list of \code{prioritizr} conservation solutions. The output is a +symmetrical matrix suitable for visualizing pairwise agreement using a heatmap. +} +\details{ +This function is essential for assessing the similarity or divergence among +different conservation plans. It takes a list of \code{prioritizr} solution objects, +each expected to contain a binary column named \code{solution_1} (indicating +selected or unselected planning units). + +For every unique pair of solutions in the input list, it computes Cohen's Kappa +using the \code{irr::kappa2()} function. Cohen's Kappa measures the agreement +between two raters (in this case, two conservation solutions) for categorical +items, correcting for chance agreement. A Kappa value of 1 indicates perfect +agreement, 0 indicates agreement equivalent to chance, and negative values +indicate agreement worse than chance. + +The resulting matrix is symmetrical, with diagonal elements always equal to 1 +(a solution perfectly agrees with itself). This matrix can then be passed to +visualization functions like \code{splnr_plot_corrMat()} to create a correlation heatmap. } \examples{ -# 30 \% target for problem/solution 1 -dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +\dontrun{ +# Assuming 'dat_species_bin' is an existing sf object in your package. + +# Create a dummy prioritizr problem and solve it for solution 1 (30\% target). +dat_problem1 <- prioritizr::problem( + dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" ) \%>\% @@ -32,10 +57,10 @@ dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = ru prioritizr::add_binary_decisions() \%>\% prioritizr::add_default_solver(verbose = FALSE) -dat_soln <- dat_problem \%>\% +dat_soln1 <- dat_problem1 \%>\% prioritizr::solve.ConservationProblem() -# 50 \% target for problem/solution 2 +# Create another dummy prioritizr problem and solve it for solution 2 (50\% target). dat_problem2 <- prioritizr::problem( dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), @@ -50,5 +75,14 @@ dat_problem2 <- prioritizr::problem( dat_soln2 <- dat_problem2 \%>\% prioritizr::solve.ConservationProblem() -corrMat <- splnr_get_kappaCorrData(list(dat_soln, dat_soln2), name_sol = c("soln1", "soln2")) +# Calculate the Cohen's Kappa correlation matrix between the two solutions. +corrMat <- splnr_get_kappaCorrData( + sol = list(dat_soln1, dat_soln2), + name_sol = c("Solution_A_30pct", "Solution_B_50pct") +) +print(corrMat) + +# This output can then be directly passed to splnr_plot_corrMat(). +# splnr_plot_corrMat(corrMat, AxisLabels = c("Sol A (30\%)", "Sol B (50\%)")) +} } diff --git a/man/splnr_get_selFreq.Rd b/man/splnr_get_selFreq.Rd index 04a520e..4a6e567 100644 --- a/man/splnr_get_selFreq.Rd +++ b/man/splnr_get_selFreq.Rd @@ -2,24 +2,60 @@ % Please edit documentation in R/utils.R \name{splnr_get_selFreq} \alias{splnr_get_selFreq} -\title{Prepare data to plot Selection Frequency of planning units} +\title{Prepare Data to Plot Selection Frequency of Planning Units} \usage{ splnr_get_selFreq(solnMany, type = "portfolio") } \arguments{ -\item{solnMany}{List or portfolio of \code{prioritizr} solutions} +\item{solnMany}{A \code{list} of \code{prioritizr} solutions (if \code{type = "list"}) +or a single \code{sf} object representing a \code{prioritizr} portfolio of solutions +(if \code{type = "portfolio"}). Each individual solution must contain a +column named \code{solution_1}.} -\item{type}{Either "portfolio" (\code{sf} object) with a portfolio produced using \code{prioritizr} or "list" with a list of solutions} +\item{type}{A character string indicating the input type: \code{"portfolio"} +(for a single \code{sf} object with multiple solution columns) or \code{"list"} +(for a list of single-solution \code{sf} objects). Defaults to \code{"portfolio"}.} } \value{ -\code{selFreq} \code{sf} object containing a column with the selection frequency (sum over all solutions). +An \code{sf} object (\code{selFreq}) containing a column named \code{selFreq}. +This column is a factor representing the selection frequency (sum of +selected occurrences across all solutions) for each planning unit. } \description{ -When multiple spatial plans are generated, we are often interested in how many times a planning unit is selected across an array of solutions. This array can either be a \code{list} of the solutions of different conservation problems or generated through a \href{https://prioritizr.net/reference/portfolios.html}{portfolio approach} with \code{prioritizr}. -\code{splnr_get_selFreq()} allows you to calculate the selection frequency of each planning unit of either a \code{list} or a \code{portfolio} of solutions. The resulting \code{sf} object can be passed for visualization to the \code{spatialplanr} function \code{\link[=splnr_plot_selectionFreq]{splnr_plot_selectionFreq()}}. +\code{splnr_get_selFreq()} calculates how many times each planning unit is +selected across an array of \code{prioritizr} solutions. This "selection +frequency" can be derived from either a list of individual solutions or +a \code{prioritizr} portfolio object. +} +\details{ +Understanding selection frequency is crucial for identifying robust +conservation areas—those that are consistently chosen across multiple +planning scenarios or alternative optimal solutions. + +The function supports two types of input: +\itemize{ +\item \code{"portfolio"}: If \code{solnMany} is a single \code{sf} object representing a +portfolio of solutions (e.g., generated by \code{prioritizr::add_cuts_portfolio()}). +In this case, the function assumes columns starting with "solution_" +represent individual solutions within the portfolio. +\item \code{"list"}: If \code{solnMany} is a \code{list} where each element is an \code{sf} +object representing a single \code{prioritizr} solution (each with a +"solution_1" column). +} +For both types, the function sums the binary \code{solution} values (0 or 1) +across all solutions for each planning unit. The result is converted to a +factor to represent discrete frequency levels. + +The output \code{sf} object can then be passed to \code{splnr_plot_selectionFreq()} +for visualization as a heatmap. } \examples{ -dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +\dontrun{ +# Assuming 'dat_species_bin' is an existing sf object in your package. + +# Create a base prioritizr problem. +dat_problem <- prioritizr::problem( + dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" ) \%>\% @@ -28,15 +64,32 @@ dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = ru prioritizr::add_binary_decisions() \%>\% prioritizr::add_default_solver(verbose = FALSE) -dat_soln <- dat_problem \%>\% - prioritizr::solve.ConservationProblem() - -# create conservation problem that contains a portfolio of solutions +# --- Example 1: Using a portfolio of solutions --- +# Create a conservation problem that contains a portfolio of solutions (e.g., 5 solutions). dat_soln_portfolio <- dat_problem \%>\% prioritizr::add_cuts_portfolio(number_solutions = 5) \%>\% prioritizr::solve.ConservationProblem() -selFreq <- splnr_get_selFreq(solnMany = dat_soln_portfolio, type = "portfolio") -(splnr_plot_selectionFreq(selFreq)) +# Calculate selection frequency from the portfolio. +selFreq_portfolio <- splnr_get_selFreq(solnMany = dat_soln_portfolio, type = "portfolio") +print(head(selFreq_portfolio)) +# You can then plot this: splnr_plot_selectionFreq(selFreq_portfolio) +# --- Example 2: Using a list of individual solutions --- +# Solve the problem multiple times to get different solutions (e.g., by randomizing costs) +dat_soln_list <- list( + dat_problem \%>\% prioritizr::solve.ConservationProblem(), + dat_problem \%>\% + dplyr::mutate(Cost = runif(n = dim(.)[[1]])) \%>\% # Vary cost for a different solution + prioritizr::solve.ConservationProblem(), + dat_problem \%>\% + dplyr::mutate(Cost = runif(n = dim(.)[[1]])) \%>\% # Another different solution + prioritizr::solve.ConservationProblem() +) + +# Calculate selection frequency from the list of solutions. +selFreq_list <- splnr_get_selFreq(solnMany = dat_soln_list, type = "list") +print(head(selFreq_list)) +# You can then plot this: splnr_plot_selectionFreq(selFreq_list) +} } diff --git a/man/splnr_gg_add.Rd b/man/splnr_gg_add.Rd index c0fb2ca..e60a2f8 100644 --- a/man/splnr_gg_add.Rd +++ b/man/splnr_gg_add.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/splnr_gg_add.R \name{splnr_gg_add} \alias{splnr_gg_add} -\title{Add-ons for plotting} +\title{Add-ons for Plotting \code{spatialplanr} Maps} \usage{ splnr_gg_add( PUs = NULL, @@ -29,70 +29,112 @@ splnr_gg_add( ) } \arguments{ -\item{PUs}{Planning Units as an \code{sf} object} +\item{PUs}{An \code{sf} object representing planning units. If provided, their +outlines will be drawn. Defaults to \code{NULL}.} -\item{colorPUs}{A color value for the outline of planning units.} +\item{colorPUs}{A character string specifying the color for the outlines of the +planning units. Defaults to \code{"grey80"}.} -\item{Bndry}{The planning region boundaries as an \code{sf} object} +\item{Bndry}{An \code{sf} object representing the main planning region boundaries. +If provided, its outline will be drawn. Defaults to \code{NULL}.} -\item{colorBndry}{A color value for the outline of the boundary.} +\item{colorBndry}{A character string specifying the color for the outline of the +\code{Bndry} object. Defaults to \code{"black"}.} -\item{overlay}{An \code{sf} object of overlay polygon.} +\item{overlay}{An \code{sf} object to be plotted as a general overlay. Defaults to \code{NULL}.} -\item{colorOverlay}{A color value for overlay.} +\item{colorOverlay}{A character string specifying the color for \code{overlay}. +Defaults to \code{"grey20"}.} -\item{overlay2}{An \code{sf} object of overlay polygon.} +\item{overlay2}{An \code{sf} object for a second general overlay. Defaults to \code{NULL}.} -\item{colorOverlay2}{A color value for overlay.} +\item{colorOverlay2}{A character string specifying the color for \code{overlay2}. +Defaults to \code{"grey30"}.} -\item{overlay3}{An \code{sf} object of overlay polygon.} +\item{overlay3}{An \code{sf} object for a third general overlay. Defaults to \code{NULL}.} -\item{colorOverlay3}{A color value for overlay.} +\item{colorOverlay3}{A character string specifying the color for \code{overlay3}. +Defaults to \code{"grey40"}.} -\item{contours}{An \code{sf} object of contours that are important to visualise -(e.g. outline of sea mounts, ridges; can be produced with -terra::as.contour()); up to 6 different contours possible.} +\item{contours}{An \code{sf} object containing contour lines (e.g., bathymetry or +seamount outlines). It is expected to have a \code{Category} column for differentiating +lines. Up to 6 categories are supported. Defaults to \code{NULL}.} -\item{colorConts}{A color value for contours.} +\item{colorConts}{A character string specifying the color for the contour lines. +Defaults to \code{"black"}.} -\item{cropOverlay}{An \code{sf} object with the boundary box used for cropping the -overlay object.} +\item{cropOverlay}{An \code{sf} object. Its bounding box will be used to set the +\code{xlim} and \code{ylim} of the \code{ggplot2::coord_sf} layer, effectively cropping the view. +Defaults to \code{NULL}.} -\item{lockIn}{An \code{sf} object with binary data of locked in areas in -the prioritisation (e.g. MPAs).} +\item{lockIn}{An \code{sf} object representing 'locked-in' areas (e.g., existing +Marine Protected Areas) that are fixed in a conservation prioritization. +Defaults to \code{NULL}.} -\item{typeLockIn}{Either "Full" or "Contours"; "Full" maps the locked in areas on -top of the planning units; "Contours" draws the outline of the locked in -areas.} +\item{typeLockIn}{A character string specifying how \code{lockIn} areas should be +plotted. Can be \code{"Full"} (fills the areas with \code{colorLockIn}) or \code{"Contours"} +(draws only the outlines of the areas). Defaults to \code{"Full"}.} -\item{nameLockIn}{column of data frame that contains binary information of -the locked in areas to plot} +\item{nameLockIn}{A character string specifying the column name in the \code{lockIn} +data frame that contains binary (0/1 or TRUE/FALSE) information indicating +locked-in status. Required if \code{lockIn} is not \code{NULL}.} -\item{alphaLockIn}{A value (0-1) for the opacity of the locked in areas when -plotted on top of other plots.} +\item{alphaLockIn}{A numeric value (0 to 1) for the opacity of the \code{lockIn} +areas when \code{typeLockIn} is \code{"Full"}. Defaults to \code{0.5}.} -\item{colorLockIn}{A color value for the locked in areas.} +\item{colorLockIn}{A character string specifying the color for the \code{lockIn} areas. +Defaults to \code{"black"}.} -\item{legendLockIn}{A character value for the title of the legend of the locked in -areas. Can be empty ("").} +\item{legendLockIn}{A character string for the title of the \code{lockIn} legend. +Can be an empty string \code{""} to suppress the title. Defaults to \code{""}.} -\item{labelLockIn}{The legend label of the locked in area (e.g. MPAs)} +\item{labelLockIn}{A character string for the legend label of the \code{lockIn} areas +(e.g., "MPAs"). Defaults to \code{"MPAs"}.} -\item{ggtheme}{The theme applied to the plot. Can either be NA (default -ggplot), "Default" (default spatialplanr: theme_bw() and some basic theme -settings) or a user-defined list of theme properties.} +\item{ggtheme}{The \code{ggplot2} theme to apply. Can be: +\itemize{ +\item \code{NA} or \code{FALSE}: No theme is applied, using \code{ggplot2} defaults. +\item \code{"Default"}: Applies a \code{spatialplanr} default theme (\code{theme_bw()} +with custom text/axis settings). +\item A \code{list} of \code{ggplot2::theme()} properties for custom styling. +} +Defaults to \code{"Default"}.} } \value{ -A ggplot object of the plot +A \code{list} of \code{ggplot2} layers and theme elements that can be added to +an existing \code{ggplot} object using \code{+}. } \description{ -This function allows to customise plots in a simple and reproducible way, by -giving the option for several inputs that can be included in maps produced -with the other functions of this package.It can be combined with the -\code{spatialplanr} spatial plotting functions. +This function allows users to customize existing \code{ggplot2} maps, particularly +those produced by other \code{spatialplanr} spatial plotting functions. It provides +options to add various spatial layers and apply consistent theming in a +simple and reproducible manner. +} +\details{ +The \code{splnr_gg_add} function enhances \code{ggplot2} objects by layering additional +spatial data such as planning unit outlines, study area boundaries, general +overlays, geographical contours, and 'locked-in' areas (e.g., existing protected +areas in a conservation prioritization). It offers fine-grained control over +colors, opacities, and legend appearance for each added layer. + +When using \code{contours}, the input \code{sf} object is expected to have a column +named \code{Category} that defines the different contour lines to be plotted. +The function currently supports up to 6 distinct contour categories for plotting. + +The \code{ggtheme} parameter offers flexibility in plot styling. \code{"Default"} applies +a standard \code{spatialplanr} theme (\code{theme_bw()} with custom text and axis settings). +A \code{list} of \code{ggplot2::theme()} elements can be provided for full customization, +or \code{NA} (logical \code{FALSE}) to apply no default theme, allowing the user to manage +all theme elements manually. } \examples{ -dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +\dontrun{ +# Assuming 'dat_species_bin' and 'dat_PUs' are existing sf objects +# in your package, suitable for prioritisation problems and plotting. + +# Create a dummy prioritizr problem and solve it for demonstration. +dat_problem <- prioritizr::problem( + dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" ) \%>\% @@ -104,6 +146,44 @@ dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = ru dat_soln <- dat_problem \%>\% prioritizr::solve.ConservationProblem() -splnr_plot_solution(dat_soln) + +# Basic plot of the solution with default planning unit outlines and theme. +plot_basic <- splnr_plot_solution(dat_soln) + splnr_gg_add(PUs = dat_PUs, ggtheme = "Default") +print(plot_basic) + +# Example with boundary, a custom overlay, and locked-in areas shown as contours. +# For this example, let's create dummy `bndry_sf` and `locked_in_sf` based on `dat_PUs` +# In a real scenario, these would be loaded from your package or data. +bndry_sf <- sf::st_union(dat_PUs) \%>\% sf::st_as_sf() +locked_in_sf <- dat_PUs[1:100, ] \%>\% dplyr::mutate(is_mpa = 1) + +plot_custom <- splnr_plot_solution(dat_soln) + + splnr_gg_add( + PUs = dat_PUs, + Bndry = bndry_sf, + colorBndry = "darkblue", + overlay = bndry_sf, # Using boundary as an example overlay + colorOverlay = "lightblue", + alphaOverlay = 0.3, + lockIn = locked_in_sf, + typeLockIn = "Contours", + nameLockIn = "is_mpa", + colorLockIn = "darkred", + labelLockIn = "Existing MPAs", + ggtheme = "Default" + ) +print(plot_custom) + +# Example with custom ggplot2 theme settings (as a list) +custom_theme_list <- list( + ggplot2::theme_classic(), + ggplot2::theme( + plot.background = ggplot2::element_rect(fill = "lightyellow"), + legend.position = "top" + ) +) +plot_with_custom_theme <- splnr_plot_solution(dat_soln) + + splnr_gg_add(PUs = dat_PUs, ggtheme = custom_theme_list) +print(plot_with_custom_theme) +} } diff --git a/man/splnr_match_names.Rd b/man/splnr_match_names.Rd index dea1a52..50b107a 100644 --- a/man/splnr_match_names.Rd +++ b/man/splnr_match_names.Rd @@ -2,25 +2,43 @@ % Please edit documentation in R/utils.R \name{splnr_match_names} \alias{splnr_match_names} -\title{Substitute numbers for all_names in regionalisations} +\title{Substitute Numbers for Names in Regionalizations} \usage{ splnr_match_names(dat, nam) } \arguments{ -\item{dat}{\code{sf} data frame with one column of numeric/integer corresponding to \code{nam}} +\item{dat}{An \code{sf} data frame with a single non-geometry column containing +numeric or integer values that correspond to the names in \code{nam}.} -\item{nam}{Named character vector of names corresponding to column of dat to recode} +\item{nam}{A named character vector. The \emph{names} of this vector should be +the numeric/integer values found in \code{dat}'s column, and the \emph{values} of +this vector should be the desired character names for substitution.} } \value{ -An \code{sf} dataframe with numeric regionalisations substituted for category names +An \code{sf} dataframe where the numeric/integer values in the relevant +column have been substituted with the corresponding character names from \code{nam}. } \description{ -Many regionalisations have numeric values in the shape files that correspond -to a vector of names. Here we provide a function to quickly replace the -numbers with names. +\code{splnr_match_names()} replaces numeric or integer values in a spatial +(sf) dataframe's column with corresponding character names, typically used +for regionalization data. +} +\details{ +This function is designed for scenarios where spatial data contains numeric +identifiers for regions, and you have a mapping (a named character vector) +to convert these IDs into more descriptive names. It assumes that the \code{sf} +dataframe (\code{dat}) has only one non-geometry column that needs recoding. + +The function directly applies the mapping from the \code{nam} vector to the +specified column. The names of the \code{nam} vector should correspond to the +numeric/integer values in the \code{dat} column, and the values of \code{nam} will +be the new character names. } \examples{ -dat <- dat_region -nam <- c("Region1" = "SE Aust", "Region2" = "Tas", "Region3" = "NE Aust") -df <- splnr_match_names(dat, nam) +# Define the named character vector for mapping. +region_names <- c("Region1" = "SE Aust", "Region2" = "Tas", "Region3" = "NE Aust") + +# Apply the function to substitute numeric codes with names. +df_named_regions <- splnr_match_names(dat = dat_region, nam = region_names) +print(df_named_regions) } diff --git a/man/splnr_plot.Rd b/man/splnr_plot.Rd index 0ede1af..ecb802a 100644 --- a/man/splnr_plot.Rd +++ b/man/splnr_plot.Rd @@ -1,73 +1,197 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/splnr_plot.R +% Please edit documentation in R/splnr_plot.R, R/splnr_plotting.R \name{splnr_plot} \alias{splnr_plot} -\title{Function to plot data.} +\title{Plot Spatial Data} \usage{ splnr_plot( df, - col_names = NULL, + colNames = NULL, paletteName = "YlGnBu", colourVals = c("#c6dbef", "#3182bd"), - plot_title = "", - legend_title = NULL, - legend_labels = NULL + plotTitle = "", + legendTitle = NULL, + legendLabels = NULL +) + +splnr_plot( + df, + colNames = NULL, + paletteName = "YlGnBu", + colourVals = c("#c6dbef", "#3182bd"), + plotTitle = "", + legendTitle = NULL, + legendLabels = NULL ) } \arguments{ -\item{df}{The dataframe containing the data to be plotted. It must include a geometry column to be used with geom_sf.} +\item{df}{The input dataframe containing the data to be plotted. This must be +an \code{sf} object and include a geometry column.} -\item{col_names}{A list of column names to include in the plot. If specified, only these columns will be used to colour the plot.} +\item{colNames}{A character vector of column names from \code{df} to be used for +coloring the plot. If \code{NULL} (default), only the planning unit outlines are plotted. +If a single column is specified, it checks for binary, logical, or continuous data. +If multiple columns are specified, it sums the values across these columns to create +a "FeatureSum" for plotting.} -\item{paletteName}{The name of the colour palette to use for filling. Default is "YlGnBu".} +\item{paletteName}{A character string specifying the name of the \code{RColorBrewer} +palette to use for filling continuous data. Defaults to \code{"YlGnBu"}.} -\item{colourVals}{The colour values to use if col_names is specified and the data is binary.} +\item{colourVals}{A character vector of two color values to use for binary +(0/1) or logical (FALSE/TRUE) data. The first color is for '0' or 'FALSE' +(absence), and the second is for '1' or 'TRUE' (presence). +Defaults to \code{c("#c6dbef", "#3182bd")}.} -\item{plot_title}{The title of the plot.} +\item{plotTitle}{A character string for the subtitle of the plot. +Defaults to \code{""} (no subtitle).} -\item{legend_title}{The title of the legend.} +\item{legendTitle}{A character string for the title of the legend. If \code{NULL}, +a default title will be used based on the data type.} -\item{legend_labels}{A vector of strings containing the labels to use for legend values.} +\item{legendLabels}{A character vector of strings to use for the legend labels, +particularly useful for binary or logical data (e.g., \code{c("Absent", "Present")}). +If \code{NULL}, default labels are used for binary/logical plots.} } \value{ -A ggplot object. +A \code{ggplot} object representing the spatial plot. + +A \code{ggplot} object representing the spatial plot. } \description{ -(For now can replace splnr_plot_cost(), splnr_plot_binFeature(), splnr_plot_MPAs(), splnr_plot_featureNo()) +This function provides a versatile way to plot spatial data (\code{sf} objects) +within the \code{spatialplanr} package. It can visualize various data types, +including binary presence/absence, logical values, continuous data, or simply +the planning unit outlines. + +This function provides a versatile way to plot spatial data (\code{sf} objects) +within the \code{spatialplanr} package. It can visualize various data types, +including binary presence/absence, logical values, continuous data, or simply +the planning unit outlines. } \details{ -Written by Kilian Barreiro and Jason Everett -Written: February 2024 +The \code{splnr_plot} function automatically detects the type of data specified by +\code{colNames} (binary, logical, or continuous) and adjusts the plotting +aesthetics accordingly. If multiple \code{colNames} are provided, it calculates +the sum of features for each planning unit and plots this sum. If \code{colNames} +is \code{NULL}, it will simply plot the outlines of the planning units. + +This function is designed to be a flexible replacement for several plotting +functions, such as \code{splnr_plot_cost()}, \code{splnr_plot_binFeature()}, +\code{splnr_plot_MPAs()}, and \code{splnr_plot_featureNo()}, streamlining the plotting +workflow within the package. + +Written by Kilian Barreiro and Jason Everett. +Last modified: February 2024. + +The \code{splnr_plot} function automatically detects the type of data specified by +\code{colNames} (binary, logical, or continuous) and adjusts the plotting +aesthetics accordingly. If multiple \code{colNames} are provided, it calculates +the sum of features for each planning unit and plots this sum. If \code{colNames} +is \code{NULL}, it will simply plot the outlines of the planning units. + +This function is designed to be a flexible replacement for several plotting +functions, such as \code{splnr_plot_cost()}, \code{splnr_plot_binFeature()}, +\code{splnr_plot_MPAs()}, and \code{splnr_plot_featureNo()}, streamlining the plotting +workflow within the package. + +Written by Kilian Barreiro and Jason Everett. +Last modified: February 2024. } \examples{ -# Binary plot of species distribution -splnr_plot(df = dat_species_bin, - col_names = "Spp1", - legend_title = "Legend", - legend_labels = c("Absent", "Present")) - -# Logical plot of species distribution -splnr_plot(df = dat_species_bin \%>\% - dplyr::mutate(dplyr::across( - tidyselect::starts_with("Spp"), as.logical)), - col_names = "Spp1", - legend_title = "Legend", - legend_labels = c("Absent", "Present")) - -# Continuous plot of bathymetry# -splnr_plot(df = dat_bathy, - col_names = "bathymetry", - plot_title = "Bathymetry", - legend_title = "Bathymetry (m)") - -# Plot Planning Units -splnr_plot(df = dat_PUs) - -# Multi binary features -splnr_plot(df = dat_species_bin, - col_names = colnames(dat_species_bin \%>\% - sf::st_drop_geometry() \%>\% - dplyr::select( - tidyselect::starts_with("Spp"))), - legend_title = "Number of features") +\dontrun{ +# Assuming 'dat_species_bin', 'dat_bathy', and 'dat_PUs' are existing sf objects +# in your package, suitable for plotting. + +# Binary plot of species distribution for "Spp1" +plot_spp1_binary <- splnr_plot( + df = dat_species_bin, + colNames = "Spp1", + legendTitle = "Species Presence", + legendLabels = c("Absent", "Present") +) +print(plot_spp1_binary) + +# Logical plot of species distribution for "Spp1" (converted from binary) +plot_spp1_logical <- splnr_plot( + df = dat_species_bin \%>\% + dplyr::mutate(dplyr::across( + tidyselect::starts_with("Spp"), as.logical + )), + colNames = "Spp1", + legendTitle = "Species Presence", + legendLabels = c("Absent", "Present") +) +print(plot_spp1_logical) + +# Continuous plot of bathymetry +plot_bathymetry <- splnr_plot( + df = dat_bathy, + colNames = "bathymetry", + plotTitle = "Bathymetry", + legendTitle = "Bathymetry (m)" +) +print(plot_bathymetry) + +# Plot Planning Units outlines only +plot_planning_units <- splnr_plot(df = dat_PUs) +print(plot_planning_units) + +# Multi-binary features: Plotting the sum of multiple "Spp" features +plot_multi_spp_sum <- splnr_plot( + df = dat_species_bin, + colNames = colnames(dat_species_bin \%>\% + sf::st_drop_geometry() \%>\% + dplyr::select(tidyselect::starts_with("Spp"))), + legendTitle = "Number of Features" +) +print(plot_multi_spp_sum) +} +\dontrun{ +# Assuming 'dat_species_bin', 'dat_bathy', and 'dat_PUs' are existing sf objects +# in your package, suitable for plotting. + +# Binary plot of species distribution for "Spp1" +plot_spp1_binary <- splnr_plot( + df = dat_species_bin, + colNames = "Spp1", + legendTitle = "Species Presence", + legendLabels = c("Absent", "Present") +) +print(plot_spp1_binary) + +# Logical plot of species distribution for "Spp1" (converted from binary) +plot_spp1_logical <- splnr_plot( + df = dat_species_bin \%>\% + dplyr::mutate(dplyr::across( + tidyselect::starts_with("Spp"), as.logical + )), + colNames = "Spp1", + legendTitle = "Species Presence", + legendLabels = c("Absent", "Present") +) +print(plot_spp1_logical) + +# Continuous plot of bathymetry +plot_bathymetry <- splnr_plot( + df = dat_bathy, + colNames = "bathymetry", + plotTitle = "Bathymetry", + legendTitle = "Bathymetry (m)" +) +print(plot_bathymetry) + +# Plot Planning Units outlines only +plot_planning_units <- splnr_plot(df = dat_PUs) +print(plot_planning_units) + +# Multi-binary features: Plotting the sum of multiple "Spp" features +plot_multi_spp_sum <- splnr_plot( + df = dat_species_bin, + colNames = colnames(dat_species_bin \%>\% + sf::st_drop_geometry() \%>\% + dplyr::select(tidyselect::starts_with("Spp"))), + legendTitle = "Number of Features" +) +print(plot_multi_spp_sum) +} } diff --git a/man/splnr_plot_circBplot.Rd b/man/splnr_plot_circBplot.Rd index c0a4369..5d2c7b5 100644 --- a/man/splnr_plot_circBplot.Rd +++ b/man/splnr_plot_circBplot.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/splnr_featureRep.R \name{splnr_plot_circBplot} \alias{splnr_plot_circBplot} -\title{Plot circular barplot} +\title{Plot Circular Barplot for Feature Representation} \usage{ splnr_plot_circBplot( df, @@ -15,29 +15,46 @@ splnr_plot_circBplot( ) } \arguments{ -\item{df}{data frame that should have the following column names: feature, value, group} +\item{df}{A \link[base:data.frame]{data.frame} or \link[tibble:tibble]{tibble} that +\strong{must} contain the following columns: +\itemize{ +\item \code{feature}: \link[base:character]{character} or \link[base:factor]{factor} unique identifier for each individual bar (e.g., species names). +\item \code{value}: \link[base:numeric]{numeric} the value to be plotted on the y-axis (bar height, typically percentage representation). +\item \code{group}: \link[base:character]{character} or \link[base:factor]{factor} for grouping factors (e.g., "important", "representative"). +}} -\item{legend_color}{vector list of colors; should have the group names and their corresponding colors} +\item{legend_color}{A \link[base:vector]{named vector} of colors. Names must correspond +to the unique values in the \code{group} column of \code{df}, and values are the +corresponding colors. For example: \code{c("group_name1" = "red", "group_name2" = "blue")}.} -\item{legend_list}{list of groups/legends of groups} +\item{legend_list}{A \link[base:character]{character vector} of labels for the legend. +This should match the names used in \code{legend_color} or the levels of \code{group}.} -\item{indicateTargets}{logical on whether to show where the targets were set} +\item{indicateTargets}{A \link[base:logical]{logical} value. If \code{TRUE}, horizontal +lines indicating \code{impTarget} and \code{repTarget} will be drawn on the plot.} -\item{impTarget}{target of the important features (in \%)} +\item{impTarget}{A \link[base:numeric]{numeric} value representing the target +percentage for 'important' features. Required if \code{indicateTargets} is \code{TRUE}.} -\item{repTarget}{target of the representative features (in \%)} +\item{repTarget}{A \link[base:numeric]{numeric} value representing the target +percentage for 'representative' features. Required if \code{indicateTargets} is \code{TRUE}.} -\item{colTarget}{string with a colour value for the indicator line} +\item{colTarget}{A \link[base:character]{character} string specifying the color +for the target indicator lines.} } \value{ -A ggplot object of the plot +A \link[ggplot2:ggplot]{ggplot2::ggplot} object of the circular bar plot. } \description{ -Plot circular barplot +\code{splnr_plot_circBplot()} creates a circular bar plot to visualize feature +representation, categorized by groups. It's particularly useful for +displaying how different categories of features meet certain targets in a radial layout. } \examples{ # DISCLAIMER: THIS SOLUTION IS NOT ACTUALLY RUN WITH THESE TARGETS YET +\dontrun{ + dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" @@ -55,6 +72,7 @@ s1 <- dat_soln \%>\% p1 <- dat_problem +# Assuming eval_feature_representation_summary is from prioritizr df_rep_imp <- prioritizr::eval_feature_representation_summary( p1, s1[, "solution_1"] @@ -74,7 +92,7 @@ target <- data.frame(feature = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5")) \%>\% df <- merge(df_rep_imp, target) \%>\% dplyr::select(-target) \%>\% - na.omit() \%>\% + stats::na.omit() \%>\% # Use stats::na.omit dplyr::rename(value = relative_held) \%>\% dplyr::rename(group = class) @@ -90,3 +108,4 @@ legends <- c("Important", "Representative") impTarget = 50, repTarget = 30 )) } +} diff --git a/man/splnr_plot_climData.Rd b/man/splnr_plot_climData.Rd index 689b561..d750e15 100644 --- a/man/splnr_plot_climData.Rd +++ b/man/splnr_plot_climData.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/splnr_plotting_climate.R \name{splnr_plot_climData} \alias{splnr_plot_climData} -\title{Plot climate data} +\title{Plot Climate Metric Data} \usage{ splnr_plot_climData( df, @@ -13,22 +13,62 @@ splnr_plot_climData( ) } \arguments{ -\item{df}{An \code{sf} object with climate metric information with} +\item{df}{An \code{sf} object containing the climate metric information. It must +have a geometry column.} -\item{colInterest}{column of data frame that contains the metric informatin} +\item{colInterest}{A character string specifying the name of the column in \code{df} +that contains the climate metric data to be plotted.} -\item{colorMap}{A character string indicating the color map to use (see https://ggplot2.tidyverse.org/reference/scale_viridis.html for all options)} +\item{colorMap}{A character string indicating the \code{viridis} color map to use +(e.g., "A", "B", "C", "D", "E"). See +\url{https://ggplot2.tidyverse.org/reference/scale_viridis.html} for all options. +Defaults to \code{"C"}.} -\item{plotTitle}{A character value for the title of the plot. Can be empty ("").} +\item{plotTitle}{A character string for the subtitle of the plot. +Defaults to \code{" "} (a single space, effectively no subtitle).} -\item{legendTitle}{A character value for the title of the legend. Can be empty ("").} +\item{legendTitle}{A character string for the title of the legend. +Defaults to \code{"Climate metric"}.} } \value{ -A ggplot object of the plot +A \code{ggplot} object representing the spatial plot of the climate metric. } \description{ -Plot climate data +The \code{splnr_plot_climData()} function creates a spatial plot of climate metric +information from an \code{sf} object. It provides a customizable visualization +using \code{ggplot2} and \code{viridis} color palettes. +} +\details{ +This function is designed to visualize spatial data that contains a specific +climate metric. It expects an \code{sf} object (\code{df}) with a geometry column and +the climate metric data in a column specified by \code{colInterest}. The plot uses +a continuous color scale (viridis) to represent the metric values across the +planning units. + +This function can be easily integrated into a larger plotting workflow or +used independently to inspect climate data distributions. } \examples{ -splnr_plot_climData(df = dat_clim, colInterest = "metric") +\dontrun{ +# Assuming 'dat_clim' is an existing sf object in your package +# with a column named "metric" or another relevant climate metric. + +# Example: Plot climate data using "metric" column +plot_climate_metric <- splnr_plot_climData( + df = dat_clim, + colInterest = "metric", + plotTitle = "Annual Climate Warming", + legendTitle = "Warming (°C/year)" +) +print(plot_climate_metric) + +# Example with a different color map +plot_climate_alt_cmap <- splnr_plot_climData( + df = dat_clim, + colInterest = "metric", + colorMap = "D", # Using 'D' for a different viridis palette + plotTitle = "Climate Metric (Alternative Colors)" +) +print(plot_climate_alt_cmap) +} } diff --git a/man/splnr_plot_climKernelDensity.Rd b/man/splnr_plot_climKernelDensity.Rd index 621e15c..b319492 100644 --- a/man/splnr_plot_climKernelDensity.Rd +++ b/man/splnr_plot_climKernelDensity.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/splnr_plotting_climate.R \name{splnr_plot_climKernelDensity} \alias{splnr_plot_climKernelDensity} -\title{Kernel Density Plots for climate-smart spatial plans} +\title{Kernel Density Plots for Climate-Smart Spatial Plans} \usage{ splnr_plot_climKernelDensity( soln, @@ -14,25 +14,60 @@ splnr_plot_climKernelDensity( ) } \arguments{ -\item{soln}{For type "Publication": A list of \code{prioirtizr} solutions (e.g. solution_list = list(s1, s2)) containing a "metric" column containing the used climate metric information; For type "App": needs to be a prioritizr solution} +\item{soln}{For \code{type = "Normal"}: A \code{list} of \code{prioritizr} solution objects +(e.g., \code{list(s1, s2)}). Each solution must contain a \code{metric} column and +a \code{solution_1} column. +For \code{type = "Basic"}: A single \code{prioritizr} solution \code{sf} object.} -\item{names}{A list of names of the solutions (names = c("Input 1", "Input 2"))} +\item{names}{A character vector of names corresponding to each solution in +\code{soln} when \code{type = "Normal"}. Not used for \code{type = "Basic"}. +Defaults to \code{NA}.} -\item{type}{The plotting style of the kernel density plots. Either "Publication" which gives axis information etc., or "App" which condenses the information in the plot to simplify it for stakeholders.} +\item{type}{A character string specifying the plotting style. Must be either +\code{"Normal"} or \code{"Basic"}. Defaults to \code{"Normal"}.} -\item{colorMap}{A character string indicating the color map to use (see https://ggplot2.tidyverse.org/reference/scale_viridis.html for all options)} +\item{colorMap}{A character string indicating the \code{viridis} color map to use +(e.g., "A", "B", "C", "D", "E"). See +\url{https://ggplot2.tidyverse.org/reference/scale_viridis.html} for all options. +Defaults to \code{"C"}.} -\item{legendTitle}{A character value for the title of the legend. Can be empty ("").} +\item{legendTitle}{A character string or \code{expression} for the title of the legend. +Defaults to \code{expression(" \\u00B0C y"^"-1" * "")}, representing "°C year⁻¹".} -\item{xAxisLab}{A characted value for the x Axis label depending on the climate metric input} +\item{xAxisLab}{A character string or \code{expression} for the x-axis label, +depending on the climate metric input. Defaults to +\code{expression("Climate warming ( \\u00B0C y"^"-1" * ")")}.} } \value{ -A ggplot object of the plot +A \code{ggplot} object representing the kernel density plot. } \description{ -Kernel Density Plots for climate-smart spatial plans +\code{splnr_plot_climKernelDensity()} generates kernel density plots for +climate-smart spatial plans, offering two distinct plotting styles: +"Normal" (for publication-quality comparison of multiple solutions) and +"Basic" (for simplified visualization for stakeholders). +} +\details{ +This wrapper function intelligently dispatches to either +\code{splnr_plot_climKernelDensity_Fancy()} (for \code{type = "Normal"}) or +\code{splnr_plot_climKernelDensity_Basic()} (for \code{type = "Basic"}) based on the +\code{type} parameter. + +The "Normal" (Fancy) style is suitable for detailed comparisons, +accommodating a list of solutions and custom axis labels, while the "Basic" +style is streamlined for clarity and quick interpretation, ideal for +stakeholder engagement. + +Both underlying functions require a \code{prioritizr} solution containing a +\code{metric} column with climate metric information and a \code{solution_1} column +indicating selected planning units. } \examples{ +\dontrun{ +# Assuming 'dat_species_bin' and 'dat_clim' are existing sf objects +# in your package. + +# Prepare data for a climate-priority area approach (CPA) target <- dat_species_bin \%>\% sf::st_drop_geometry() \%>\% colnames() \%>\% @@ -48,15 +83,18 @@ CPA <- splnr_climate_priorityAreaApproach( refugiaTarget = 1 ) +# Join climate metric to features for the problem out_sf <- CPA$Features \%>\% - dplyr::mutate(Cost_None = rep(1, 780)) \%>\% + dplyr::mutate(Cost_None = rep(1, dim(.)[[1]])) \%>\% # Ensure enough costs for PUs sf::st_join(dat_clim, join = sf::st_equals) +# Define features for the prioritizr problem usedFeatures <- out_sf \%>\% sf::st_drop_geometry() \%>\% dplyr::select(-tidyselect::starts_with("Cost_"), -"metric") \%>\% names() +# Create and solve a prioritizr problem p1 <- prioritizr::problem(out_sf, usedFeatures, "Cost_None") \%>\% prioritizr::add_min_set_objective() \%>\% prioritizr::add_relative_targets(CPA$Targets$target) \%>\% @@ -64,6 +102,32 @@ p1 <- prioritizr::problem(out_sf, usedFeatures, "Cost_None") \%>\% prioritizr::add_default_solver(verbose = FALSE) dat_solnClim <- prioritizr::solve.ConservationProblem(p1) -splnr_plot_climKernelDensity(dat_solnClim, type = "Basic") -splnr_plot_climKernelDensity(soln = list(dat_solnClim), names = c("Input 1"), type = "Normal") + +# Example 1: Basic kernel density plot +plot_basic_kde <- splnr_plot_climKernelDensity(soln = dat_solnClim, type = "Basic") +print(plot_basic_kde) + +# Example 2: Normal (Fancy) kernel density plot for a single solution +plot_normal_kde_single <- splnr_plot_climKernelDensity( + soln = list(dat_solnClim), + names = c("Solution 1"), + type = "Normal" +) +print(plot_normal_kde_single) + +# Example 3: Normal (Fancy) plot comparing two solutions (create a dummy second solution) +# For demonstration, let's create another dummy solution +dat_solnClim_2 <- dat_solnClim \%>\% + dplyr::mutate(solution_1 = sample(c(0, 1), n(), replace = TRUE)) # Randomize selection + +plot_normal_kde_multi <- splnr_plot_climKernelDensity( + soln = list(dat_solnClim, dat_solnClim_2), + names = c("Solution A", "Solution B"), + type = "Normal", + colorMap = "plasma", + legendTitle = "Climate Value", + xAxisLab = "Climate Metric (units)" +) +print(plot_normal_kde_multi) +} } diff --git a/man/splnr_plot_comparison.Rd b/man/splnr_plot_comparison.Rd index bd70801..a00a813 100644 --- a/man/splnr_plot_comparison.Rd +++ b/man/splnr_plot_comparison.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/splnr_plotting.R \name{splnr_plot_comparison} \alias{splnr_plot_comparison} -\title{Plot solution comparison} +\title{Plot Solution Comparison} \usage{ splnr_plot_comparison( soln1, @@ -11,22 +11,43 @@ splnr_plot_comparison( ) } \arguments{ -\item{soln1}{The first \code{prioritizr} solution} +\item{soln1}{The first \code{prioritizr} solution, expected as an \code{sf} object +with a \code{solution_1} column. This serves as the baseline for comparison.} -\item{soln2}{The second \code{prioritizr} solution} +\item{soln2}{The second \code{prioritizr} solution, expected as an \code{sf} object +with a \code{solution_1} column. This is the solution being compared against \code{soln1}.} -\item{legendTitle}{A character value for the title of the legend. Can be empty ("").} +\item{legendTitle}{A character string for the title of the legend. +Defaults to \code{"Scenario 2 compared to Scenario 1:"}.} } \value{ -A ggplot object of the plot +A \code{ggplot} object representing the spatial comparison of the two solutions. } \description{ -Conservation planning often requires the comparison of the outputs of the solutions of different conservation problems. One way to compare solutions is by spatially visualising the different planning units that were selected in two separate solutions to conservation problems. -\code{splnr_plot_comparison()} allows to map the differences of two solutions in customisable way using \code{ggplot2}. This function requires two separate \code{sf} objects each containing a \code{solution_1} column indicating the binary solution (selected vs not selected) of a \code{prioritizr} conservation problem. It outputs a \code{ggobject} and can be combined with the \code{spatialplanr} function \code{\link[=splnr_gg_add]{splnr_gg_add()}}. +The \code{splnr_plot_comparison()} function spatially visualizes the differences +between two \code{prioritizr} conservation solutions. This helps in understanding +which planning units are common, added, or removed between two scenarios. +} +\details{ +Conservation planning often involves comparing outputs from different +conservation problems or scenarios. This function facilitates this comparison +by requiring two \code{sf} objects, \code{soln1} and \code{soln2}, each representing a +\code{prioritizr} solution and containing a \code{solution_1} column (binary, +indicating selected vs. not selected). + +The function categorizes planning units into "Same" (selected in both), +"Added (+)" (selected in \code{soln2} but not \code{soln1}), and "Removed (-)" +(selected in \code{soln1} but not \code{soln2}). It then plots these categories with +distinct colors for clear visualization. The output is a \code{ggplot} object +that can be combined with \code{splnr_gg_add()} for further customization. } \examples{ -# 30 \% target for problem/solution 1 -dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +\dontrun{ +# Assuming 'dat_species_bin' is an existing sf object in your package. + +# Create Problem 1 with 30\% target and solve it. +dat_problem <- prioritizr::problem( + dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" ) \%>\% @@ -38,7 +59,7 @@ dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = ru dat_soln <- dat_problem \%>\% prioritizr::solve.ConservationProblem() -# 50 \% target for problem/solution 2 +# Create Problem 2 with 50\% target and solve it. dat_problem2 <- prioritizr::problem( dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), @@ -53,5 +74,8 @@ dat_problem2 <- prioritizr::problem( dat_soln2 <- dat_problem2 \%>\% prioritizr::solve.ConservationProblem() -(splnr_plot_comparison(dat_soln, dat_soln2)) +# Plot the comparison between the two solutions. +plot_comparison <- splnr_plot_comparison(dat_soln, dat_soln2) +print(plot_comparison) +} } diff --git a/man/splnr_plot_corrMat.Rd b/man/splnr_plot_corrMat.Rd index 9f408af..110d9b9 100644 --- a/man/splnr_plot_corrMat.Rd +++ b/man/splnr_plot_corrMat.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/splnr_plotting.R \name{splnr_plot_corrMat} \alias{splnr_plot_corrMat} -\title{Plot correlation matrices} +\title{Plot Correlation Matrices of Conservation Solutions} \usage{ splnr_plot_corrMat( x, @@ -13,26 +13,55 @@ splnr_plot_corrMat( ) } \arguments{ -\item{x}{A correlation matrix of \code{prioritizr} solutions} +\item{x}{A numeric correlation matrix of \code{prioritizr} solutions.} -\item{colourGradient}{A list of three colour values for high positive, no and high negative correlation} +\item{colourGradient}{A character vector of three color values: +\itemize{ +\item \code{colourGradient[1]}: Color for high positive correlation. +\item \code{colourGradient[2]}: Color for no correlation (midpoint). +\item \code{colourGradient[3]}: Color for high negative correlation. +} +Defaults to \code{c("#BB4444", "#FFFFFF", "#4477AA")}.} -\item{legendTitle}{A character value for the title of the legend. Can be empty ("").} +\item{legendTitle}{A character string for the title of the legend. +Defaults to \code{"Correlation \\ncoefficient"}.} -\item{AxisLabels}{A list of labels of the solutions to be correlated (Default: NULL). Length needs to match number of correlated solutions.} +\item{AxisLabels}{A character vector of labels for the x and y axes of the +correlation matrix, representing the names of the correlated solutions. +If \code{NULL} (default), the column names of \code{x} will be used. The length of +this vector must match the number of rows/columns in \code{x}.} -\item{plotTitle}{A character value for the title of the plot. Can be empty ("").} +\item{plotTitle}{A character string for the title of the plot. Defaults to \code{""}.} } \value{ -A ggplot object of the plot +A \code{ggplot} object representing the correlation matrix plot. } \description{ -Conservation planning often requires the comparison of the outputs of the solutions of different conservation problems. -One way to compare solutions is by correlating the solutions using Cohen's Kappa. \code{splnr_plot_corrMat()} allows to visualize the correlation matrix of the different solutions (for example produced with the \code{spatialplanr} function \code{\link[=splnr_get_kappaCorrData]{splnr_get_kappaCorrData()}}). +The \code{splnr_plot_corrMat()} function visualizes a correlation matrix of +\code{prioritizr} conservation solutions, typically computed using Cohen's Kappa. +This helps in understanding the agreement or disagreement between different +spatial plans. +} +\details{ +Conservation planning often involves comparing the outputs of various +conservation problems. One effective method for this is correlating solutions +using metrics like Cohen's Kappa. This function takes a correlation matrix +(e.g., produced by the \code{spatialplanr} function \code{splnr_get_kappaCorrData()}) +and generates a heatmap visualization using \code{ggcorrplot}. + +The plot highlights positive, negative, and no correlation using a color +gradient, and labels the correlation coefficients directly on the plot. +The output is a \code{ggplot} object that can be combined with the \code{spatialplanr} +function \code{splnr_gg_add()} for further customization, though its primary use +is for standalone correlation visualization. } \examples{ -# 30 \% target for problem/solution 1 -dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +\dontrun{ +# Assuming 'dat_species_bin' is an existing sf object in your package. + +# Create Problem 1 (30\% target) and solve it. +dat_problem <- prioritizr::problem( + dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" ) \%>\% @@ -44,7 +73,7 @@ dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = ru dat_soln <- dat_problem \%>\% prioritizr::solve.ConservationProblem() -# 50 \% target for problem/solution 2 +# Create Problem 2 (50\% target) and solve it. dat_problem2 <- prioritizr::problem( dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), @@ -59,7 +88,14 @@ dat_problem2 <- prioritizr::problem( dat_soln2 <- dat_problem2 \%>\% prioritizr::solve.ConservationProblem() +# Get the Kappa correlation data for the two solutions. CorrMat <- splnr_get_kappaCorrData(list(dat_soln, dat_soln2), name_sol = c("soln1", "soln2")) -(splnr_plot_corrMat(CorrMat, AxisLabels = c("Solution 1", "Solution 2"))) +# Plot the correlation matrix with custom axis labels. +plot_correlation_matrix <- splnr_plot_corrMat( + CorrMat, + AxisLabels = c("Solution 1", "Solution 2") +) +print(plot_correlation_matrix) +} } diff --git a/man/splnr_plot_cost.Rd b/man/splnr_plot_cost.Rd index a229fc6..d7b4c0c 100644 --- a/man/splnr_plot_cost.Rd +++ b/man/splnr_plot_cost.Rd @@ -5,17 +5,17 @@ \title{Plot cost} \usage{ splnr_plot_cost( - Cost, - Cost_name = "Cost", + cost, + costName = "Cost", legendTitle = "Cost", paletteName = "YlGnBu", plotTitle = "" ) } \arguments{ -\item{Cost}{An \code{sf} object of cost for \code{prioritizr}} +\item{cost}{An \code{sf} object of cost for \code{prioritizr}} -\item{Cost_name}{Name of the cost column} +\item{costName}{Name of the cost column} \item{legendTitle}{A character value for the title of the legend. Can be empty ("").} @@ -34,7 +34,7 @@ A ggplot object of the plot } \examples{ \dontrun{ -dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(cost = runif(n = dim(.)[[1]])), features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" ) \%>\% diff --git a/man/splnr_plot_costOverlay.Rd b/man/splnr_plot_costOverlay.Rd index b080663..79f1699 100644 --- a/man/splnr_plot_costOverlay.Rd +++ b/man/splnr_plot_costOverlay.Rd @@ -2,35 +2,60 @@ % Please edit documentation in R/splnr_plotting.R \name{splnr_plot_costOverlay} \alias{splnr_plot_costOverlay} -\title{Plot cost overlay} +\title{Plot Cost Overlay on Solution} \usage{ splnr_plot_costOverlay( soln, - Cost = NA, - Cost_name = "Cost", + cost = NA, + costName = "Cost", legendTitle = "Cost", plotTitle = "Solution overlaid with cost" ) } \arguments{ -\item{soln}{The \code{prioritizr} solution} +\item{soln}{The \code{prioritizr} solution object, expected as an \code{sf} object, +containing at least a \code{solution_1} column.} -\item{Cost}{An \code{sf} object of cost for \code{prioritizr}.In case \code{prioritizr}solution does not contain cost, alternative cost object has to be provided here that was used to generate solution (default: NA).} +\item{cost}{An \code{sf} object containing the cost data for planning units. +If the \code{prioritizr} solution \code{soln} already contains the cost column +specified by \code{costName}, this parameter can be \code{NA} (default). Otherwise, +provide an \code{sf} object with the cost data.} -\item{Cost_name}{Name of the cost column} +\item{costName}{A character string specifying the name of the cost column +within the \code{soln} object or the \code{Cost} object. Defaults to \code{"Cost"}.} -\item{legendTitle}{A character value for the title of the legend. Can be empty ("").} +\item{legendTitle}{A character string for the title of the cost legend. +Defaults to \code{"Cost"}.} -\item{plotTitle}{A character value for the title of the plot. Can be empty ("").} +\item{plotTitle}{A character string for the subtitle of the plot. +Defaults to \code{"Solution overlaid with cost"}.} } \value{ -A ggplot object of the plot +A \code{ggplot} object representing the solution with cost overlay. } \description{ -\code{splnr_plot_costOverlay()} allows to plot the cost of each planning units of a planning region on top of the solution of a conservation problem created with \code{prioritizr} in a customisable way using \code{ggplot2}. This function requires a solution as an \code{sf} object with a column called \code{solution_1} as well as a cost column and outputs a \code{ggobject}. It can be combined with the \code{spatialplanr} function \code{\link[=splnr_gg_add]{splnr_gg_add()}}. +The \code{splnr_plot_costOverlay()} function visualizes the cost of each planning +unit overlaid on the solution of a \code{prioritizr} conservation problem. This +allows for a customizable \code{ggplot2} visualization, highlighting the costs +within selected planning units. +} +\details{ +This function requires a \code{prioritizr} solution as an \code{sf} object, which +must contain a \code{solution_1} column indicating selected (1) or unselected (0) +planning units. It also requires a cost column, either present within the +\code{soln} object or provided separately via the \code{Cost} parameter. + +The function filters the solution to show only the selected planning units +and then overlays these with a gradient representing the cost. This output +is a \code{ggplot} object that can be further customized using \code{splnr_gg_add()}. } \examples{ -dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +\dontrun{ +# Assuming 'dat_species_bin' is an existing sf object in your package. + +# Create a dummy prioritizr problem and solve it for demonstration. +dat_problem <- prioritizr::problem( + dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" ) \%>\% @@ -42,5 +67,21 @@ dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = ru dat_soln <- dat_problem \%>\% prioritizr::solve.ConservationProblem() -splnr_plot_costOverlay(soln = dat_soln) +# Plot the solution overlaid with cost +plot_cost_overlay <- splnr_plot_costOverlay(soln = dat_soln) +print(plot_cost_overlay) + +# Example: If cost is in a separate sf object (e.g., dat_PUs with a cost column) +# Create a dummy cost column in dat_PUs for this example +# Replace this with your actual cost data if it's external +dat_PUs_with_cost <- dat_PUs \%>\% dplyr::mutate(MyCost = runif(n = dim(.)[[1]])) +plot_cost_overlay_external <- splnr_plot_costOverlay( + soln = dat_soln, + cost = dat_PUs_with_cost, + costName = "MyCost", + legendTitle = "Custom Cost", + plotTitle = "Solution with External Cost" +) +print(plot_cost_overlay_external) +} } diff --git a/man/splnr_plot_featureRep.Rd b/man/splnr_plot_featureRep.Rd index 6b24aa7..6481a21 100644 --- a/man/splnr_plot_featureRep.Rd +++ b/man/splnr_plot_featureRep.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/splnr_featureRep.R \name{splnr_plot_featureRep} \alias{splnr_plot_featureRep} -\title{Plot how well targets are met} +\title{Plot Feature Representation (Target Achievement)} \usage{ splnr_plot_featureRep( df, @@ -13,35 +13,63 @@ splnr_plot_featureRep( nr = 1, showTarget = NA, plotTitle = "", + sort_by = "category", ... ) } \arguments{ -\item{df}{A \code{df} containing the target information (resulting from the splnr_get_featureRep() function)} +\item{df}{A \link[base:data.frame]{data.frame} or \link[tibble:tibble]{tibble} +containing the feature representation information. This typically +results from the \code{splnr_get_featureRep()} function and should include at +least \code{feature} and \code{relative_held} columns, and optionally \code{target} and \code{incidental}.} -\item{category}{A named data frame of feature and category for grouping the plot output} +\item{category}{A named \link[base:data.frame]{data.frame} or \link[tibble:tibble]{tibble} +that provides grouping information for features. It should contain a column +that can be matched with the \code{feature} column in \code{df} (by default, a column +named \code{feature}, or specified by \code{categoryFeatureCol}), and a column named +\code{category} for grouping the plot output. If \code{NA} (default), no categorization is applied.} -\item{categoryFeatureCol}{A character with the column containing the feature infromation to be plotted if the category data frame does not contain a column named 'feature' that can be matched with the 'df' infromation.} +\item{categoryFeatureCol}{A \link[base:character]{character} string specifying the +name of the column in the \code{category} data frame that contains the feature +information to be matched with \code{df$feature}. This is used if the \code{category} +data frame does not have a column explicitly named \code{'feature'}.} -\item{renameFeatures}{A logical on whether variable names should be used or they should be replaced with common names} +\item{renameFeatures}{A \link[base:logical]{logical} value. If \code{TRUE}, feature names +in the plot will be replaced with common names provided in \code{namesToReplace}.} -\item{namesToReplace}{A data frame containing the variable name ('nameVariable') and a common name ('nameCommon').} +\item{namesToReplace}{A \link[base:data.frame]{data.frame} containing two columns: +\code{'nameVariable'} (the original feature name) and \code{'nameCommon'} (the common name +to replace it with). Required if \code{renameFeatures} is \code{TRUE}.} -\item{nr}{Number of rows of the legend} +\item{nr}{An \link[base:integer]{integer} specifying the number of rows for the legend.} -\item{showTarget}{\code{logical} Should the targets be shown on the bar plot} +\item{showTarget}{A \link[base:logical]{logical} value. If \code{TRUE}, a transparent bar +representing the target level for each feature will be shown on the plot.} -\item{plotTitle}{A character value for the title of the plot. Can be empty ("").} +\item{plotTitle}{A \link[base:character]{character} string for the title of the plot. +Can be an empty string \code{""} (default).} -\item{...}{Other arguments passed on to \code{ggplot2::theme()}} +\item{sort_by}{A \link[base:character]{character} string specifying the column +by which to sort the features on the x-axis. Accepted values include: +\code{"category"}, \code{"feature"}, \code{"target"}, \code{"representation"} (\code{relative_held}), +or \code{"difference"} (between representation and target).} + +\item{...}{Other arguments passed on to \code{\link[ggplot2:theme]{ggplot2::theme()}} to customize the plot's theme.} } \value{ -A ggplot object of the plot +A \link[ggplot2:ggplot]{ggplot2::ggplot} object representing the feature representation bar plot. } \description{ -Plot how well targets are met +\code{splnr_plot_featureRep()} creates a bar plot to visualize the representation +of features in a conservation solution, indicating how well targets are met. +It can categorize features, rename them for clarity, and optionally display +the target levels on the plot. } \examples{ +# For a full example, ensure 'dat_species_bin', 'dat_category' are available +# (e.g., from the 'prioritizrdata' package or defined in your package's data) + + pDat <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" @@ -54,13 +82,27 @@ pDat <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = soln <- pDat \%>\% prioritizr::solve.ConservationProblem() - # including incidental species coverage -df <- splnr_get_featureRep( +df <- splnr_get_featureRep( # Assuming splnr_get_featureRep is available soln = soln, pDat = pDat ) -(splnr_plot_featureRep(df, category = dat_category)) +# Basic plot with categories and targets shown +(splnr_plot_featureRep(df, category = dat_category, showTarget = TRUE)) + +# Plot without categories, sorted by feature name +(splnr_plot_featureRep(df, showTarget = TRUE, sort_by = "feature")) +# Example with feature renaming +names_to_replace_df <- tibble::tibble( + nameVariable = c("Spp1", "Spp2"), + nameCommon = c("Species One", "Species Two") +) +(splnr_plot_featureRep(df, + category = dat_category, + renameFeatures = TRUE, + namesToReplace = names_to_replace_df, + showTarget = TRUE +)) } diff --git a/man/splnr_plot_importanceScore.Rd b/man/splnr_plot_importanceScore.Rd index f089941..4f965be 100644 --- a/man/splnr_plot_importanceScore.Rd +++ b/man/splnr_plot_importanceScore.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/splnr_plotting.R \name{splnr_plot_importanceScore} \alias{splnr_plot_importanceScore} -\title{Plot importance score} +\title{Plot Importance Score of Planning Units} \usage{ splnr_plot_importanceScore( soln, @@ -15,31 +15,61 @@ splnr_plot_importanceScore( ) } \arguments{ -\item{soln}{The \code{prioritizr} solution} +\item{soln}{The \code{prioritizr} solution object, expected as an \code{sf} object. +It should contain a \code{solution_1} column.} -\item{pDat}{The \code{prioritizr} problem} +\item{pDat}{The \code{prioritizr} problem object that was solved to generate \code{soln}.} -\item{method}{The method for calcualting importance scores. Can be either "Ferrier" for the Ferrier Score, which can only be used with the minimum set objective function, "RWR" for Rarity Weighted Richness Score, or "RC" for Replacement Cost which takes longer than the other approaches due to its iterative process.} +\item{method}{A character string specifying the method for calculating importance +scores. Must be one of \code{"Ferrier"}, \code{"RWR"}, or \code{"RC"}. Defaults to \code{"Ferrier"}.} -\item{plotTitle}{A character value for the title of the plot. Can be empty ("").} +\item{plotTitle}{A character string for the title of the plot. Defaults to \code{""}.} -\item{colorMap}{A character string indicating the color map to use (see https://ggplot2.tidyverse.org/reference/scale_viridis.html for all options)} +\item{colorMap}{A character string indicating the \code{viridis} color map to use +(e.g., "A", "B", "C", "D", "E"). See +\url{https://ggplot2.tidyverse.org/reference/scale_viridis.html} for all options. +Defaults to \code{"A"}.} -\item{decimals}{The number of decimals shown in the plot. Ferrier Score often requires a higher number of decimals (>4) than the other two approaches (2) for this analysis to work.} +\item{decimals}{The number of decimal places to display for the importance scores +in the legend. Ferrier Score often benefits from a higher number of decimals (>4). +Defaults to \code{4}.} -\item{legendTitle}{A character value for the title of the legend. Can be empty ("").} +\item{legendTitle}{A character string for the title of the legend. +Defaults to \code{"Importance Score"}.} } \value{ -A ggplot object of the plot +A \code{ggplot} object representing the plot of importance scores. } \description{ -\href{https://prioritizr.net/reference/importance.html}{Importance scores} are a mean to reflect the irreplaceability of a planning unit in the solution of a \code{prioirtizr} conservation problem. Based on the \code{prioritizr} package, \code{splnr_plot_importanceScore()} allows to visualize three different types of importance scores with \code{ggplot2} that should be used based on the conservation problem at hand. The \code{prioritizr} development team generally recommend using the \href{https://prioritizr.net/reference/eval_replacement_importance.html}{replacement cost score}, however this might be not be feasible for conservation problems with many planning units or features. +The \code{splnr_plot_importanceScore()} function visualizes the importance scores +(irreplaceability) of planning units from a \code{prioritizr} conservation problem +using \code{ggplot2}. It supports different methods for calculating importance scores. } \details{ -The function outputs a \code{ggobject} and can be combined with the \code{spatialplanr} function \code{\link[=splnr_gg_add]{splnr_gg_add()}}. +Importance scores quantify the irreplaceability of a planning unit in a +conservation solution. This function leverages the \code{prioritizr} package to +calculate and plot three different types of importance scores: +\itemize{ +\item \strong{"Ferrier"}: The Ferrier Score, which is applicable only with +the minimum set objective function. It often requires a higher number +of decimals (e.g., >4) for accurate representation. +\item \strong{"RWR"}: Rarity Weighted Richness Score. +\item \strong{"RC"}: Replacement Cost. This method is generally recommended +by the \code{prioritizr} development team for its robustness, but it can be +computationally intensive and take longer, especially for problems with +many planning units or features. +} + +The function outputs a \code{ggplot} object that can be combined with the +\code{spatialplanr} function \code{splnr_gg_add()} for further customization. } \examples{ -dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +\dontrun{ +# Assuming 'dat_species_bin' and 'dat_PUs' are existing sf objects in your package. + +# Create a dummy prioritizr problem and solve it for demonstration. +dat_problem <- prioritizr::problem( + dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" ) \%>\% @@ -51,5 +81,24 @@ dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = ru dat_soln <- dat_problem \%>\% prioritizr::solve.ConservationProblem() -(splnr_plot_importanceScore(soln = dat_soln, pDat = dat_problem, method = "Ferrier", decimals = 4)) +# Plot importance score using the "Ferrier" method. +plot_ferrier_importance <- splnr_plot_importanceScore( + soln = dat_soln, + pDat = dat_problem, + method = "Ferrier", + decimals = 4, + plotTitle = "Ferrier Importance Score" +) +print(plot_ferrier_importance) + +# Plot importance score using the "RWR" (Rarity Weighted Richness) method. +plot_rwr_importance <- splnr_plot_importanceScore( + soln = dat_soln, + pDat = dat_problem, + method = "RWR", + decimals = 2, + plotTitle = "Rarity Weighted Richness" +) +print(plot_rwr_importance) +} } diff --git a/man/splnr_plot_selectionFreq.Rd b/man/splnr_plot_selectionFreq.Rd index e78dc8f..3030a67 100644 --- a/man/splnr_plot_selectionFreq.Rd +++ b/man/splnr_plot_selectionFreq.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/splnr_plotting.R \name{splnr_plot_selectionFreq} \alias{splnr_plot_selectionFreq} -\title{Plot selection frequency of a planning unit in an array of prioritisations} +\title{Plot Planning Unit Selection Frequency} \usage{ splnr_plot_selectionFreq( selFreq, @@ -12,23 +12,48 @@ splnr_plot_selectionFreq( ) } \arguments{ -\item{selFreq}{An \code{sf} object containing the selection frequency of a planning unit from an array of solutions} +\item{selFreq}{An \code{sf} object containing the selection frequency data for planning units. +This object must include a \code{selFreq} column (e.g., generated by \code{splnr_get_selFreq()}).} -\item{plotTitle}{A character value for the title of the plot. Can be empty ("").} +\item{plotTitle}{A character string for the title of the plot. Defaults to \code{""}.} -\item{paletteName}{A string (or number) for the color palette to use. Available palettes can be found at https://ggplot2.tidyverse.org/reference/scale_brewer.html.} +\item{paletteName}{A character string or numeric value specifying the name of the +\code{RColorBrewer} palette to use for the fill. Available palettes can be found at +\url{https://ggplot2.tidyverse.org/reference/scale_brewer.html}. +Defaults to \code{"Greens"}.} -\item{legendTitle}{A character value for the title of the legend. Can be empty ("").} +\item{legendTitle}{A character string for the title of the legend. +Defaults to \code{"Selection \\nFrequency"}.} } \value{ -A ggplot object of the plot +A \code{ggplot} object representing the plot of planning unit selection frequency. } \description{ -When multiple spatial plans are generated, we are often interested in how many times a planning unit is selected across an array of solutions. This array can either be made up of the solutions to different conservation problems or generated through a \href{https://prioritizr.net/reference/portfolios.html}{portfolio approach} with \code{prioritizr}. -Either way, this function requires an \code{sf} object input that contains a column (\code{selFreq}) with the selection frequency of each planning unit that can be generated with the \code{spatialplanr}function \code{splnr_get_selFreq()}. \code{splnr_plot_selectionFreq()} allows to visualize this selection frequency using \code{ggplot2}. It outputs a \code{ggobject} and can be combined with the \code{spatialplanr} function \code{splnr_gg_add()}. +The \code{splnr_plot_selectionFreq()} function visualizes the selection frequency +of planning units across an array of \code{prioritizr} solutions. This is useful +for understanding which areas are consistently selected as important for +conservation. +} +\details{ +When multiple spatial plans are generated (either from solutions to different +conservation problems or via a \code{prioritizr} portfolio approach), it's +valuable to assess the robustness of planning unit selection. This function +takes an \code{sf} object as input, which must contain a \code{selFreq} column +representing the selection frequency of each planning unit. This \code{selFreq} +column can be generated using the \code{spatialplanr} function \code{splnr_get_selFreq()}. + +The function uses \code{ggplot2} to create a spatial plot of these frequencies, +allowing for customization of the color palette, plot title, and legend title. +The output is a \code{ggplot} object that can be further enhanced by combining it +with the \code{spatialplanr} function \code{splnr_gg_add()}. } \examples{ -dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +\dontrun{ +# Assuming 'dat_species_bin' is an existing sf object in your package. + +# Create a dummy prioritizr problem. +dat_problem <- prioritizr::problem( + dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" ) \%>\% @@ -37,11 +62,16 @@ dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = ru prioritizr::add_binary_decisions() \%>\% prioritizr::add_default_solver(verbose = FALSE) -# create conservation problem that contains a portfolio of solutions +# Create a conservation problem that contains a portfolio of solutions (e.g., 5 solutions). dat_soln_portfolio <- dat_problem \%>\% prioritizr::add_cuts_portfolio(number_solutions = 5) \%>\% prioritizr::solve.ConservationProblem() -selFreq <- splnr_get_selFreq(solnMany = dat_soln_portfolio, type = "portfolio") -(splnr_plot_selectionFreq(selFreq)) +# Calculate selection frequency using splnr_get_selFreq(). +selFreq_data <- splnr_get_selFreq(solnMany = dat_soln_portfolio, type = "portfolio") + +# Plot the selection frequency. +plot_selection_frequency <- splnr_plot_selectionFreq(selFreq_data) +print(plot_selection_frequency) +} } diff --git a/man/splnr_plot_solution.Rd b/man/splnr_plot_solution.Rd index 44aed33..7ff5167 100644 --- a/man/splnr_plot_solution.Rd +++ b/man/splnr_plot_solution.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/splnr_plotting.R \name{splnr_plot_solution} \alias{splnr_plot_solution} -\title{Plot prioritizr solution} +\title{Plot \code{prioritizr} Solution} \usage{ splnr_plot_solution( soln, @@ -15,28 +15,57 @@ splnr_plot_solution( ) } \arguments{ -\item{soln}{The \code{prioritizr} solution} +\item{soln}{The \code{prioritizr} solution object, expected as an \code{sf} object.} -\item{colorVals}{A \code{list} object of named vectors that will match the color value with the according name. "TRUE" stands for selected planning units.} +\item{colorVals}{A character vector of color values. For single-zone +problems, this should typically be two colors (for "Not selected" and +"Selected"). For multi-zone problems, the length should match the number of +zones plus one (for "Not selected").} -\item{showLegend}{A logical command on whether to show the legend of the solution (Default: TRUE).} +\item{showLegend}{A logical value indicating whether to display the legend +of the solution. Defaults to \code{TRUE}.} -\item{legendLabels}{Character values (number of zones + 1) of what the legend should be labelled.} +\item{legendLabels}{A character vector of strings to label the legend values. +Its length must match the number of levels in the solution (e.g., "Not selected", +"Selected" for single zone; "Not selected", "Zone 1", "Zone 2" for two zones).} -\item{plotTitle}{A character value for the title of the plot. Can be empty ("").} +\item{plotTitle}{A character string for the title of the plot. Can be empty (\code{""}). +Defaults to \code{"Solution"}.} -\item{legendTitle}{A character value for the title of the legend. Can be empty ("").} +\item{legendTitle}{A character string for the title of the legend. Can be empty (\code{""}). +Defaults to \code{"Planning Units"}.} -\item{zones}{A logical value, indicating whether the spatial plan contains zones or not (default = FALSE).} +\item{zones}{A logical value. Set to \code{TRUE} if the \code{prioritizr} solution +contains multiple zones (i.e., it's a multi-zone problem). Defaults to \code{FALSE}.} } \value{ -A ggplot object of the plot +A \code{ggplot} object representing the plot of the conservation solution. } \description{ -\code{splnr_plot_solution()} allows to plot the solution of a \code{prioritizr} conservation problem with our without in a customisable way using \code{ggplot2}. This function requires a solution as an \code{sf} object with a column called \code{solution_1} and outputs a \code{ggobject}. It can be combined with the \code{spatialplanr} function \code{\link[=splnr_gg_add]{splnr_gg_add()}}. +The \code{splnr_plot_solution()} function visualizes the solution of a +\code{prioritizr} conservation problem using \code{ggplot2}. It can handle +single-zone and multi-zone solutions, offering customization for colors +and legend. +} +\details{ +This function requires a \code{prioritizr} solution object, which should be an +\code{sf} object containing at least a \code{solution_1} column (for single-zone +problems) or \code{solution_1_zone1}, \code{solution_1_zone2}, etc. (for multi-zone +problems). It outputs a \code{ggplot} object, which can be further customized +by combining it with the \code{spatialplanr} function \code{splnr_gg_add()}. + +For multi-zone problems (\code{zones = TRUE}), the function sums the selected +zones for each planning unit and plots the resulting combined selection. +The \code{colorVals} and \code{legendLabels} should be provided to match the number of +selection levels (e.g., "Not selected", "Zone 1", "Zone 2", etc.). } \examples{ -dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), +\dontrun{ +# Assuming 'dat_species_bin' is an existing sf object in your package. + +# Example 1: Plotting a single-zone prioritizr solution +dat_problem <- prioritizr::problem( + dat_species_bin \%>\% dplyr::mutate(Cost = runif(n = dim(.)[[1]])), features = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), cost_column = "Cost" ) \%>\% @@ -48,17 +77,22 @@ dat_problem <- prioritizr::problem(dat_species_bin \%>\% dplyr::mutate(Cost = ru dat_soln <- dat_problem \%>\% prioritizr::solve.ConservationProblem() -splnr_plot_solution(dat_soln) -# example 2 -t2 <- matrix(NA, ncol = 2, nrow = 5) # create targets +plot_soln_single_zone <- splnr_plot_solution(dat_soln) +print(plot_soln_single_zone) + +# Example 2: Plotting a multi-zone prioritizr solution +# Create targets for two zones +t2 <- matrix(NA, ncol = 2, nrow = 5) t2[, 1] <- 0.1 t2[, 2] <- 0.05 +# Define zones for species z2 <- prioritizr::zones( "zone 1" = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5"), "zone 2" = c("Spp1", "Spp2", "Spp3", "Spp4", "Spp5") ) -# when giving sf input, we need as many cost columns as we have zones + +# Create a multi-zone problem (requires as many cost columns as zones) p2 <- prioritizr::problem( dat_species_bin \%>\% dplyr::mutate( Cost1 = runif(n = dim(.)[[1]]), @@ -74,8 +108,12 @@ p2 <- prioritizr::problem( s2 <- p2 \%>\% prioritizr::solve.ConservationProblem() -(splnr_plot_solution(s2, - zones = TRUE, colorVals = c("#c6dbef", "#3182bd", "black"), + +plot_soln_multi_zone <- splnr_plot_solution(s2, + zones = TRUE, + colorVals = c("#c6dbef", "#3182bd", "black"), # Colors for Not selected, Zone 1, Zone 2 legendLabels = c("Not selected", "Zone 1", "Zone 2") -)) +) +print(plot_soln_multi_zone) +} } diff --git a/man/splnr_replace_NAs.Rd b/man/splnr_replace_NAs.Rd index 027e209..7cd9a62 100644 --- a/man/splnr_replace_NAs.Rd +++ b/man/splnr_replace_NAs.Rd @@ -2,23 +2,47 @@ % Please edit documentation in R/utils.R \name{splnr_replace_NAs} \alias{splnr_replace_NAs} -\title{Remove NAs from spatial data using nearest neighbour} +\title{Remove NAs from Spatial Data Using Nearest Neighbour} \usage{ splnr_replace_NAs(df, vari) } \arguments{ -\item{df}{An \code{sf} dataframe} +\item{df}{An \code{sf} dataframe. This dataframe must contain a geometry column +and the \code{vari} column with potential NA values.} -\item{vari}{Variable to remove NAs from} +\item{vari}{A character string specifying the name of the column in \code{df} +from which NA values are to be removed and replaced. This column must +exist in \code{df}.} } \value{ -An \code{sf} object with NAs replaced with the nearest neighbour +An \code{sf} object identical to the input \code{df}, but with NA values +in the \code{vari} column replaced by values from their nearest non-NA neighbors. +If no NAs are found, the original \code{df} is returned unchanged. } \description{ -\code{splnr_replace_NAs()} allows you to replace NA values in your data with the value of the nearest neighbour. -The nearest neighbour is determined using \code{st_nearest_feature()} from the \code{sf} package. +\code{splnr_replace_NAs()} replaces missing (NA) values in a specified column +of an \code{sf} dataframe with the value from the nearest spatial neighbor. +} +\details{ +This function is useful for imputing missing data in spatial contexts. +It identifies all planning units with \code{NA} values in the \code{vari} column. +For each of these, it finds the geographically closest planning unit that +\emph{does not} have an \code{NA} value in \code{vari}, and then copies that non-missing +value. This approach leverages the spatial autocorrelation often present +in environmental and species data. + +The \code{st_nearest_feature()} function from the \code{sf} package is used for +determining the closest neighbor. } \examples{ -df <- dat_species_prob \%>\% - splnr_replace_NAs("Spp2") +\dontrun{ +# Assuming 'dat_species_prob' is an existing sf object in your package. +# For demonstration, let's artificially introduce some NAs in 'Spp2'. +df_with_na <- dat_species_prob \%>\% + dplyr::mutate(Spp2 = ifelse(runif(n()) < 0.2, NA, Spp2)) + +# Replace NAs in 'Spp2' using nearest neighbor imputation. +df_no_na <- splnr_replace_NAs(df = df_with_na, vari = "Spp2") +print(sum(is.na(df_no_na$Spp2))) # Should be 0 if successful +} } diff --git a/man/splnr_scale_01.Rd b/man/splnr_scale_01.Rd index 04b50dc..3206582 100644 --- a/man/splnr_scale_01.Rd +++ b/man/splnr_scale_01.Rd @@ -2,23 +2,49 @@ % Please edit documentation in R/utils.R \name{splnr_scale_01} \alias{splnr_scale_01} -\title{Scale spatial layers to between 0 and 1} +\title{Scale Spatial Layers to Between 0 and 1} \usage{ splnr_scale_01(dat, col_name) } \arguments{ -\item{dat}{\code{sf} dataframe} +\item{dat}{An \code{sf} dataframe containing the column to be scaled.} -\item{col_name}{Name of the column to scale} +\item{col_name}{A character string specifying the name of the numeric column +in \code{dat} that needs to be scaled.} } \value{ -\code{sf} dataframe +An \code{sf} dataframe identical to the input \code{dat}, but with the values +in the \code{col_name} column re-scaled to be between 0 and 1. } \description{ -\code{splnr_scale_01()} allows you to re-scale your data from values that are greater than 1 to values that are between 0 and 1. +\code{splnr_scale_01()} re-scales the numeric values in a specified column of an +\code{sf} dataframe to a range between 0 and 1. This is particularly useful for +normalizing data like probabilities or costs. +} +\details{ +This function inspects the maximum value (\code{mx}) in the \code{col_name} column. +It then divides all values in that column by a \code{divi} factor to bring them +into the 0-1 range. The \code{divi} factor is determined heuristically: +\itemize{ +\item If \code{mx > 100}, \code{divi} is \code{1000}. +\item If \code{mx > 10}, \code{divi} is \code{100}. +\item If \code{mx > 1}, \code{divi} is \code{10}. +\item If \code{mx <= 1}, no division is performed (\code{divi} is \code{1}), as the data is +already within the desired range. +} + +This approach ensures that the data is scaled appropriately without +hardcoding a fixed division factor. } \examples{ -df <- dat_species_prob \%>\% - dplyr::mutate(Spp1 = Spp1 * 100) \%>\% - splnr_scale_01(col_name = "Spp1") +\dontrun{ +# Scale the 'Spp1' column. +df_scaled_spp1 <- splnr_scale_01(dat = dat_species_prob, col_name = "Spp1") +print(df_scaled_spp1) + +# Example where max is already <= 1 +df_already_scaled <- dat_species_prob \%>\% dplyr::mutate(Spp1 = Spp1 / 100) +df_no_change <- splnr_scale_01(dat = df_already_scaled, col_name = "Spp1") +print(df_no_change) # Spp1 values should remain unchanged +} } diff --git a/man/splnr_targets_byCategory.Rd b/man/splnr_targets_byCategory.Rd index 68222dc..514bd8e 100644 --- a/man/splnr_targets_byCategory.Rd +++ b/man/splnr_targets_byCategory.Rd @@ -2,27 +2,63 @@ % Please edit documentation in R/splnr_targets.R \name{splnr_targets_byCategory} \alias{splnr_targets_byCategory} -\title{Assign targets to all features by category} +\title{Assign Targets by Category} \usage{ splnr_targets_byCategory(dat, catTarg, catName = "Category") } \arguments{ -\item{dat}{A sf object with the features and categories} +\item{dat}{An \code{sf} object (or data frame) containing the features and their +associated categories. Each row should represent a feature (e.g., a species) +with its attributes, including the category.} -\item{catTarg}{A named character vector with categories and target} +\item{catTarg}{A named numeric vector where names are the categories +(e.g., \code{"Group1"}, \code{"Endangered"}) and values are the corresponding +conservation targets (e.g., \code{0.5}, \code{0.8}).} -\item{catName}{An optional argument for the name of the category column in dat} +\item{catName}{A character string specifying the name of the column in \code{dat} +that contains the category information. Defaults to \code{"Category"}.} } \value{ -An sf object with targets added +An \code{sf} object (or data frame) identical to the input \code{dat}, but with an +additional column named \code{target} containing the assigned conservation target +for each feature. Features whose categories are not found in \code{catTarg} will +have \code{NA} in the \code{target} column unless they already have a 'target' column. } \description{ -\code{splnr_targets_byCategory()} allows to assign targets for conservation planning based on species categories. +The \code{splnr_targets_byCategory()} function assigns conservation targets to +features (e.g., species) based on their assigned categories. This allows for +differentiated conservation goals for different groups of features. +} +\details{ +This function is useful in conservation planning when different types of +features (e.g., endangered species, common species, ecosystem types) require +distinct conservation targets. It performs a left join with a provided +named vector (\code{catTarg}) where names correspond to categories in your data +and values are the desired targets. + +The \code{dat} input should be an \code{sf} object (or data frame) that contains a +column (\code{catName}) identifying the category for each feature. } \examples{ -dat <- splnr_targets_byCategory( - dat = dat_category, +\dontrun{ +# Assuming 'dat_category' is an existing sf object in your package +# with a column named "category" and other feature data. + +# Example: Assign targets based on predefined categories +targets_by_group <- splnr_targets_byCategory( + dat = dat_category, # Assuming dat_category has a 'category' column catTarg = c("Group1" = 0.5, "Group2" = 0.2), catName = "category" ) +print(targets_by_group) + +# Example: Assign targets with a different category column name +dat_alt_cat <- data.frame(Feature = letters[1:5], Type = c("A", "B", "A", "C", "B")) +targets_by_type <- splnr_targets_byCategory( + dat = dat_alt_cat, + catTarg = c("A" = 0.7, "B" = 0.4), + catName = "Type" +) +print(targets_by_type) +} } diff --git a/man/splnr_targets_byIUCN.Rd b/man/splnr_targets_byIUCN.Rd index 3102e9d..13acb7b 100644 --- a/man/splnr_targets_byIUCN.Rd +++ b/man/splnr_targets_byIUCN.Rd @@ -2,26 +2,91 @@ % Please edit documentation in R/splnr_targets.R \name{splnr_targets_byIUCN} \alias{splnr_targets_byIUCN} -\title{Assign targets bu IUCN Red List categories} +\title{Assign Targets by IUCN Red List Categories} \usage{ splnr_targets_byIUCN(dat, IUCN_target, IUCN_col = "IUCN_Category") } \arguments{ -\item{dat}{A dataframe or sf object with IUCN categories} +\item{dat}{A dataframe or \code{sf} object containing species information, +including a column with IUCN categories.} -\item{IUCN_target}{Either a numeric or named numeric of targets to apply to IUCN categories} +\item{IUCN_target}{Either: +\itemize{ +\item A single numeric value (e.g., \code{0.3}) to apply this target to all +threatened IUCN categories ("EX", "EW", "CR", "EN", "VU"). +\item A named numeric vector (e.g., \code{c("EX" = 0.8, "CR" = 0.6)}) to +apply specific targets to particular IUCN categories. +}} -\item{IUCN_col}{Optional string to indicate the name of the column with the IUCN categories} +\item{IUCN_col}{A character string specifying the name of the column in \code{dat} +that contains the IUCN category information. Defaults to \code{"IUCN_Category"}.} } \value{ -dataframe or sf object +A dataframe or \code{sf} object identical to the input \code{dat}, but with an +updated or newly added \code{target} column reflecting the assigned conservation goals. } \description{ -\code{splnr_targets_byIUCN()} allows to assign targets for species used in conservation planning based on IUCN categories. Species can be extracted based on IUCN categories with the \code{spatoalplnr}function \code{splnr_get_IUCNRedList()}. -Accessing the IUCN database requires a login token from \code{rl_use_iucn()} that needs to be added to the environment using \code{Sys.setenv(IUCN_REDLIST_KEY = "[Your Token]")}. You can start by running \code{rredlist::rl_use_iucn()}. +The \code{splnr_targets_byIUCN()} function assigns conservation targets for species +based on their IUCN Red List categories. This allows for prioritizing species +at higher risk of extinction with more stringent conservation goals. +} +\details{ +This function is crucial for integrating species' extinction risk into +conservation planning. It allows you to specify targets either as a single +numeric value (applied to all 'threatened' IUCN categories) or as a named +numeric vector for specific categories. + +Species can be extracted based on IUCN categories using the \code{spatialplanr} +function \code{splnr_get_IUCNRedList()}. + +\strong{Important:} To access the IUCN database (e.g., via \code{splnr_get_IUCNRedList()}), +you need an API login token. This token, obtained from \code{rredlist::rl_use_iucn()}, +must be set as an environment variable named \code{IUCN_REDLIST_KEY} +(e.g., \code{Sys.setenv(IUCN_REDLIST_KEY = "[Your Token]")}). + +The function checks if a 'target' column already exists in \code{dat}. If not, +it creates one. If it exists, new targets are coalesced with existing ones, +allowing for sequential application or refinement of targets. + +The "threatened" IUCN categories considered for target assignment (when a +single \code{IUCN_target} is provided) are: "EX" (Extinct), "EW" (Extinct in the Wild), +"CR" (Critically Endangered), "EN" (Endangered), and "VU" (Vulnerable). } \examples{ -dat <- data.frame(IUCN_Category = c("EW", "EX", NA), target = c(0.3, 0.3, 0.3)) -IUCN_target <- c("EX" = 0.8, "EW" = 0.6) -dat <- splnr_targets_byIUCN(dat, IUCN_target) +\dontrun{ +# Example 1: Assigning specific targets to categories +# Create a dummy dataframe resembling output from splnr_get_IUCNRedList +df_species_iucn <- data.frame( + Species = c("Diomedea exulans", "Hippocampus kuda", + "Squatina squatina", "Common Dolphin"), + IUCN_Category = c("VU", "EN", "CR", "LC") +) + +iucn_specific_targets <- c("EX" = 0.9, "EW" = 0.8, "CR" = 0.75, "EN" = 0.6, "VU" = 0.5) + +df_with_iucn_targets <- splnr_targets_byIUCN( + dat = df_species_iucn, + IUCN_target = iucn_specific_targets, + IUCN_col = "IUCN_Category" +) +print(df_with_iucn_targets) + +# Example 2: Assigning a single target to all threatened categories +df_single_target <- splnr_targets_byIUCN( + dat = df_species_iucn, + IUCN_target = 0.4, # Apply 40\% target to all threatened species + IUCN_col = "IUCN_Category" +) +print(df_single_target) + +# Example 3: When 'dat' already has a 'target' column +df_pre_targets <- data.frame( + Species = c("A", "B", "C"), + IUCN_Category = c("CR", "LC", "EN"), + target = c(0.1, 0.2, 0.1) # Existing targets +) +iucn_update_targets <- c("CR" = 0.7) # Only update CR +df_updated_targets <- splnr_targets_byIUCN(df_pre_targets, iucn_update_targets) +print(df_updated_targets) +} } diff --git a/man/splnr_targets_byInverseArea.Rd b/man/splnr_targets_byInverseArea.Rd index b259985..49203ce 100644 --- a/man/splnr_targets_byInverseArea.Rd +++ b/man/splnr_targets_byInverseArea.Rd @@ -2,24 +2,67 @@ % Please edit documentation in R/splnr_targets.R \name{splnr_targets_byInverseArea} \alias{splnr_targets_byInverseArea} -\title{Assign targets by Inverse Area} +\title{Assign Targets by Inverse Area} \usage{ splnr_targets_byInverseArea(df, target_min, target_max) } \arguments{ -\item{df}{An \code{sf} dataframe with features to calculate} +\item{df}{An \code{sf} dataframe containing the features (e.g., species distribution +data) for which to calculate inverse area targets. Each column (excluding +geometry) should represent a feature, and each row a planning unit.} -\item{target_min}{The minimum target for inverse area} +\item{target_min}{A numeric value between 0 and 1 (inclusive) specifying the +minimum target percentage. This will be the target for the most widespread feature.} -\item{target_max}{The maximum target for inverse area} +\item{target_max}{A numeric value between 0 and 1 (inclusive) specifying the +maximum target percentage. This will be the target for the rarest feature.} } \value{ -An \code{sf} dataframe with Inverse Area Targets added in \code{Target} +A \code{tibble} (data frame) with two columns: \code{Species} (or feature name) +and \code{target} (the calculated inverse area target for each feature). } \description{ -This function takes a min (\code{target_min}) and max (\code{target_max}) target range and calculates an inverse area target for each feature based on areal coverage. +This function calculates inverse area targets for each conservation feature +within an \code{sf} dataframe, based on their areal coverage. The target is set +to be inversely proportional to the feature's area, ranging between a +specified minimum (\code{target_min}) and maximum (\code{target_max}). +} +\details{ +The inverse area target approach aims to assign higher conservation targets +to features that have a smaller overall distribution or areal coverage within +the study region. This can be particularly useful for prioritizing rare or +range-restricted features. + +The calculation proceeds as follows: +\enumerate{ +\item The area of a single planning unit is determined. +\item The total area of the study region is estimated by multiplying the number +of planning units by the individual planning unit area. +\item For each feature (species), its total area across all planning units is +calculated. +\item The target for each feature is then scaled between \code{target_min} and +\code{target_max} such that features with smaller areas receive targets closer +to \code{target_max}, and features with larger areas receive targets closer +to \code{target_min}. +} + +The input \code{df} is expected to be an \code{sf} object where columns (excluding +geometry) represent different features (e.g., species presence/absence) and +rows represent planning units. } \examples{ -targets <- dat_species_prob \%>\% +\dontrun{ +# Assuming 'dat_species_prob' is an existing sf object in your package, +# representing species distribution in planning units. + +# Calculate inverse area targets with a range from 30\% to 80\%. +targets_inverse_area <- dat_species_prob \%>\% splnr_targets_byInverseArea(target_min = 0.3, target_max = 0.8) +print(targets_inverse_area) + +# Example with a different target range (e.g., 20\% to 70\%) +targets_custom_range <- dat_species_prob \%>\% + splnr_targets_byInverseArea(target_min = 0.2, target_max = 0.7) +print(targets_custom_range) +} } diff --git a/pkgdown/favicon/apple-touch-icon.png b/pkgdown/favicon/apple-touch-icon.png index 1f3f654..dee267e 100644 Binary files a/pkgdown/favicon/apple-touch-icon.png and b/pkgdown/favicon/apple-touch-icon.png differ diff --git a/pkgdown/favicon/favicon-96x96.png b/pkgdown/favicon/favicon-96x96.png new file mode 100644 index 0000000..1dc72d2 Binary files /dev/null and b/pkgdown/favicon/favicon-96x96.png differ diff --git a/pkgdown/favicon/favicon.ico b/pkgdown/favicon/favicon.ico index e93f581..fd5b6de 100644 Binary files a/pkgdown/favicon/favicon.ico and b/pkgdown/favicon/favicon.ico differ diff --git a/pkgdown/favicon/favicon.svg b/pkgdown/favicon/favicon.svg new file mode 100644 index 0000000..54b448f --- /dev/null +++ b/pkgdown/favicon/favicon.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/pkgdown/favicon/site.webmanifest b/pkgdown/favicon/site.webmanifest new file mode 100644 index 0000000..4ebda26 --- /dev/null +++ b/pkgdown/favicon/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/pkgdown/favicon/web-app-manifest-192x192.png b/pkgdown/favicon/web-app-manifest-192x192.png new file mode 100644 index 0000000..b342a74 Binary files /dev/null and b/pkgdown/favicon/web-app-manifest-192x192.png differ diff --git a/pkgdown/favicon/web-app-manifest-512x512.png b/pkgdown/favicon/web-app-manifest-512x512.png new file mode 100644 index 0000000..282cb18 Binary files /dev/null and b/pkgdown/favicon/web-app-manifest-512x512.png differ diff --git a/tests/testthat/test-splnr_plot.R b/tests/testthat/test-splnr_plot.R index 2e3c89b..428bff0 100644 --- a/tests/testthat/test-splnr_plot.R +++ b/tests/testthat/test-splnr_plot.R @@ -38,9 +38,9 @@ distance <- splnr_get_distCoast(dat_PUs) testthat::test_that("Correct function output", { expect_s3_class( splnr_plot(df = dat_species_bin, - col_names = "Spp1", - legend_title = "Legend", - legend_labels = c("Absent", "Present")) + colNames = "Spp1", + legendTitle = "Legend", + legendLabels = c("Absent", "Present")) , "gg" ) }) @@ -49,9 +49,9 @@ testthat::test_that("Correct function output", { testthat::test_that("Correct function output", { expect_s3_class( splnr_plot(df = dat_species_bin %>% dplyr::mutate(dplyr::across(tidyselect::starts_with("Spp"), as.logical)), - col_names = "Spp1", - legend_title = "Legend", - legend_labels = c("Absent", "Present")) + colNames = "Spp1", + legendTitle = "Legend", + legendLabels = c("Absent", "Present")) , "gg" ) }) @@ -60,9 +60,9 @@ testthat::test_that("Correct function output", { testthat::test_that("Correct function output", { expect_s3_class( splnr_plot(df = distance, - col_names = "coastDistance_km", - plot_title = "Distance to Coast", - legend_title = "Distance (km)") + colNames = "coastDistance_km", + plotTitle = "Distance to Coast", + legendTitle = "Distance (km)") , "gg" ) }) @@ -79,11 +79,11 @@ testthat::test_that("Correct function output", { testthat::test_that("Correct function output", { expect_s3_class( splnr_plot(df = dat_species_bin, - col_names = colnames(dat_species_bin %>% + colNames = colnames(dat_species_bin %>% sf::st_drop_geometry() %>% dplyr::select( tidyselect::starts_with("Spp"))), - legend_title = "Number of features") + legendTitle = "Number of features") , "gg" ) diff --git a/vignettes/ClimateSmart.R b/vignettes/ClimateSmart.R deleted file mode 100644 index 50d922b..0000000 --- a/vignettes/ClimateSmart.R +++ /dev/null @@ -1,174 +0,0 @@ -## ----include = FALSE---------------------------------------------------------- -knitr::opts_chunk$set( -collapse = TRUE, -comment = "#>", -warning = FALSE, -cache = FALSE, -message = FALSE, -eval = TRUE, -fig.width = 9 -) - -## ----setup-------------------------------------------------------------------- -library(spatialplanr) - -## ----------------------------------------------------------------------------- -Region <- "Coral Sea" # "Australia" -Type <- "Oceans" # "EEZ" -cCRS <- "ESRI:54009" # Mollweide - -## ----------------------------------------------------------------------------- -PU_size <- 107460 # m - -## ----------------------------------------------------------------------------- -splnr_theme <- list( - ggplot2::theme_bw(), - ggplot2::theme( - legend.position = "right", - legend.direction = "vertical", - text = ggplot2::element_text(size = 9, colour = "black"), - axis.text = ggplot2::element_text(size = 9, colour = "black"), - plot.title = ggplot2::element_text(size = 9), - axis.title = ggplot2::element_blank() - ) -) - -## ----------------------------------------------------------------------------- -Bndry <- splnr_get_boundary(Limits = Region, Type = Type, cCRS = cCRS) - -landmass <- rnaturalearth::ne_countries(scale = "medium", returnclass = "sf") %>% - sf::st_transform(cCRS) - -## ----------------------------------------------------------------------------- -PUs <- spatialgridr::get_grid(boundary = Bndry, - crs = cCRS, - output = "sf_hex", - resolution = PU_size) - -## ----------------------------------------------------------------------------- -Dict <- tibble::tribble( - ~nameCommon, ~nameVariable, ~category, - "Green sea turtle", "Chelonia_mydas", "Reptiles", - "Loggerhead sea turtle", "Caretta_caretta", "Reptiles", - "Hawksbill sea turtle", "Eretmochelys_imbricata", "Reptiles", - "Olive ridley sea turtle", "Lepidochelys_olivacea", "Reptiles", - "Saltwater crocodile", "Crocodylus_porosus", "Reptiles", - "Humpback whale", "Megaptera_novaeangliae", "Mammals", - "Common Minke whale", "Balaenoptera_acutorostrata", "Mammals", - "Dugong", "Dugong_dugon", "Mammals", - "Grey nurse shark", "Carcharias_taurus", "Sharks and rays", - "Tiger shark", "Galeocerdo_cuvier", "Sharks and rays", - "Great hammerhead shark", "Sphyrna_mokarran", - "Sharks and rays", - "Giant oceanic manta ray", "Mobula_birostris", "Sharks and rays", - "Reef manta ray", "Mobula_alfredi", "Sharks and rays", - "Whitetip reef shark", "Triaenodon_obesus", "Sharks and rays", - "Red-footed booby", "Sula_sula", "Birds" -) - -## ----------------------------------------------------------------------------- -datEx_species_bin <- spDataFiltered %>% - dplyr::as_tibble() %>% - dplyr::mutate(dplyr::across( - -dplyr::any_of(c("geometry")), # Don't apply to geometry - ~ dplyr::case_when( - . >= 0.5 ~ 1, - . < 0.5 ~ 0, - is.na(.data) ~ 0 - ) - )) %>% - sf::st_as_sf() - -col_name <- spDataFiltered %>% - sf::st_drop_geometry() %>% - colnames() - -## ----------------------------------------------------------------------------- -metric_df <- CoralSeaVelocity %>% - dplyr::rename(metric = voccMag_transformed) - -## ----fig.width = 9------------------------------------------------------------ -(ggclim <- splnr_plot_climData(metric_df, "metric") + - splnr_gg_add( - Bndry = Bndry, overlay = landmass, - cropOverlay = PUs, ggtheme = splnr_theme - )) - -## ----fig.width = 9------------------------------------------------------------ -set.seed(5) - -metric_df <- CoralSeaVelocity %>% - dplyr::rename(metric = voccMag_transformed) %>% - dplyr::mutate( - metricOG = metric, - metric = ifelse(metric > 0.99, runif(., 0.85, 1.0), metric) - ) - -(ggclim <- splnr_plot_climData(metric_df, "metric") + - splnr_gg_add( - Bndry = Bndry, overlay = landmass, - cropOverlay = PUs, ggtheme = splnr_theme - )) - -## ----------------------------------------------------------------------------- -target <- datEx_species_bin %>% - sf::st_drop_geometry() %>% - colnames() %>% - data.frame() %>% - setNames(c("feature")) %>% - dplyr::mutate(target = 0.3) - - -CPA_Approach <- splnr_climate_priorityAreaApproach( - featuresDF = datEx_species_bin, - metricDF = metric_df, - targetsDF = target, - direction = -1, refugiaTarget = 1 -) - -out_sf <- CPA_Approach$Features %>% - sf::st_join( - datEx_species_bin %>% - dplyr::select( - tidyselect::starts_with("Cost_") - ), - join = sf::st_equals) %>% - sf::st_join(metric_df, join = sf::st_equals) - -targets <- CPA_Approach$Targets - -## ----------------------------------------------------------------------------- -out_sf$Cost_None <- rep(1, 397) - -usedFeatures <- out_sf %>% - sf::st_drop_geometry() %>% - dplyr::select( - -tidyselect::starts_with("Cost_"), - -tidyselect::starts_with("metric") - ) %>% - names() - -## ----------------------------------------------------------------------------- -p1 <- prioritizr::problem(out_sf, usedFeatures, "Cost_None") %>% - prioritizr::add_min_set_objective() %>% - prioritizr::add_relative_targets(targets$target) %>% - prioritizr::add_binary_decisions() %>% - prioritizr::add_default_solver(verbose = FALSE) - -dat_solnClim <- prioritizr::solve.ConservationProblem(p1) - -## ----fig.width = 9------------------------------------------------------------ -(ggSoln <- splnr_plot_solution(dat_solnClim) + - splnr_gg_add( - Bndry = Bndry, overlay = landmass, - cropOverlay = PUs, ggtheme = splnr_theme - )) - -## ----fig.width = 9------------------------------------------------------------ -(ggClimDens <- splnr_plot_climKernelDensity( - soln = list(dat_solnClim), - names = c("Input 1"), type = "Normal", - legendTitle = "Climate velocity (add unit)", - xAxisLab = "Climate velocity" -)) - diff --git a/vignettes/GlobalFishingWatch.R b/vignettes/GlobalFishingWatch.R deleted file mode 100644 index ca398e5..0000000 --- a/vignettes/GlobalFishingWatch.R +++ /dev/null @@ -1,204 +0,0 @@ -## ----setup_chunks, include=FALSE---------------------------------------------- -knitr::opts_chunk$set(echo = TRUE, warning = FALSE) - -## ----eval=FALSE--------------------------------------------------------------- -# remotes::install_github("GlobalFishingWatch/gfwr") - -## ----setup-------------------------------------------------------------------- -library(gfwr) -library(spatialplanr) - -## ----eval=FALSE--------------------------------------------------------------- -# usethis::edit_r_environ() - -## ----eval=FALSE--------------------------------------------------------------- -# key <- gfwr::gfw_auth() - -## ----results='hide'----------------------------------------------------------- -region_id <- gfwr::get_region_id(region_name = "Australia", - region_source = "EEZ", - key = gfwr::gfw_auth())$id - -## ----eval=FALSE, message=FALSE------------------------------------------------ -# gfwr::get_raster( -# spatial_resolution = "LOW", -# temporal_resolution = "MONTHLY", -# group_by = "FLAGANDGEARTYPE", -# start_date = "2022-01-01", -# end_date = "2023-01-01", -# region = region_id, -# region_source = "EEZ", -# key = gfwr::gfw_auth() -# ) - -## ----message=FALSE------------------------------------------------------------ -data_sf_combined <- splnr_get_gfw(region = "Australia", - start_date = "2019-01-01", - end_date = "2023-12-31", - temp_res = "YEARLY", - spat_res = "LOW", - compress = FALSE) - -## ----message = FALSE, results='hide'------------------------------------------ -# Check and modify if necessary the spatial reference of data_sf_combined -data_sf_combined <- sf::st_set_crs(data_sf_combined, - sf::st_crs(rnaturalearth::ne_coastline(scale = "large"))) - -coast_clipped <- rnaturalearth::ne_coastline(scale = "large") %>% - sf::st_as_sf() %>% - sf::st_intersection(sf::st_as_sfc(sf::st_bbox(data_sf_combined))) - -# Load EEZ polygons -eezs <- spatialgridr::get_boundary(name = "Australia", type = "eez", country_type = "country") %>% - sf::st_transform(crs = sf::st_crs(data_sf_combined)) %>% - sf::st_make_valid() %>% - sf::st_intersection(sf::st_as_sfc(sf::st_bbox(data_sf_combined))) - -## ----echo=FALSE--------------------------------------------------------------- -main_plot <- ggplot2::ggplot(data_sf_combined) + - ggplot2::geom_sf(ggplot2::aes(color = log10(ApparentFishingHrs))) + - ggplot2::geom_sf(data = coast_clipped, color = "black", fill = NA) + # Add coastline - ggplot2::geom_sf(data = eezs, fill = NA, color = "red") + # Add the EEZ with hatching - ggplot2::scale_color_viridis_c(guide = "legend") + - ggplot2::theme_minimal() + - ggplot2::labs(title = "2022 Vessel Activity Map", - subtitle = "Fishing Hours recorded by GFW in Australia", - color = "Fishing Hours (log10)") + - ggplot2::theme( - legend.position = "bottom", - legend.text = ggplot2::element_text(size = 8), - legend.title = ggplot2::element_text(size = 10) - ) + - ggplot2::guides(color = ggplot2::guide_colorbar( - title.position = "top", - title.vjust = 0.5, - title.hjust = -0.5, - label.theme = ggplot2::element_text(size = 8), - barwidth = 5, - barheight = 0.5 - )) - -# The display and writing in this section is for information purposes only, to understand how the information on the grid is translated. -overlay_plot <- ggplot2::ggplot(data_sf_combined) + - ggplot2::geom_rect(ggplot2::aes(xmin = -Inf,xmax = Inf,ymin = -Inf,ymax = Inf), fill = "white") + - ggplot2::geom_sf(ggplot2::aes(color = log10(ApparentFishingHrs))) + - ggplot2::geom_sf(data = coast_clipped, color = "black", fill = NA) + # Add coastline - ggplot2::geom_sf(data = eezs, fill = NA, color = "red") + - ggplot2::scale_color_viridis_c(guide = "legend") + - ggplot2::labs(title = "Vessel Activity Map in Australia between 2019 and 2023", - subtitle = "Fishing Hours data recorded by GFW", - color = "Fishing Hours \n (log10)") + - ggplot2::theme_minimal() + - ggplot2::theme( - legend.position = "none", - title = ggplot2::element_blank(), - axis.text.x = ggplot2::element_blank(), - axis.text.y = ggplot2::element_blank(), - axis.title.x = ggplot2::element_blank(), - axis.title.y = ggplot2::element_blank(), - panel.border = ggplot2::element_rect(color = "black", fill = NA, linewidth = 1) - ) + - ggplot2::coord_sf(xlim = c(152, 155), ylim = c(-27, -29)) - -main_plot + - ggplot2::annotation_custom( - ggplot2::ggplotGrob(overlay_plot), - xmin = 130, - xmax = 170, - ymin = -20, - ymax = -36 - ) - -## ----echo=FALSE--------------------------------------------------------------- -ggplot2::ggplot(data_sf_combined) + - ggplot2::geom_sf(ggplot2::aes(color = as.factor(Year))) + - ggplot2::geom_sf(data = coast_clipped, color = "black", fill = NA) + # Add coastline - ggplot2::geom_sf(data = eezs, color = "red", fill = NA) + # Add the EEZ - ggplot2::theme_minimal() + - ggplot2::scale_color_viridis_d(guide = "legend") + - ggplot2::labs(title = "Vessel Activity Map in Australia between 2019 and 2024", subtitle = "Fishing Hours data recorded by GFW", color = "Years") + - ggplot2::theme( - legend.position = "bottom", - legend.text = ggplot2::element_text(size = 8), - legend.title = ggplot2::element_text(size = 10) - ) - -## ----message=FALSE------------------------------------------------------------ -# We need to change the temporal range according to our need group by it to display the total fishing hours.
-data_sf_combined <- splnr_get_gfw(region = "Australia", - start_date = "2019-01-01", - end_date = "2023-12-31", - temp_res = "MONTHLY", - key = gfwr::gfw_auth()) %>% - dplyr::group_by(Year, Month) %>% - dplyr::summarize(Total_Fishing_Hours = sum(ApparentFishingHrs)) - -## ----echo=FALSE--------------------------------------------------------------- -ggplot2::ggplot(data_sf_combined, ggplot2::aes(x = Month, y = Total_Fishing_Hours, color = Year, group = Year)) + - ggplot2::geom_line() + - ggplot2::geom_point() + - ggplot2::labs( - title = "Total Fishing Hours per month (2014-2023)", - x = "Month", y = "Total Fishing Hours" - ) + - ggplot2::theme_minimal() - -## ----message=FALSE------------------------------------------------------------ -data_sf_combined <- splnr_get_gfw(region = "Micronesia", - start_date = "2019-12-31", - end_date = "2021-01-01", - temp_res = "MONTHLY") - -## ----echo=FALSE, message=FALSE, results='hide'-------------------------------- -# Check and modify if necessary the spatial reference of data_sf_combined -data_sf_combined <- sf::st_set_crs(data_sf_combined, sf::st_crs(rnaturalearth::ne_coastline(scale = "large"))) - -coast_clipped <- rnaturalearth::ne_coastline(scale = "large") %>% - sf::st_as_sf() %>% - sf::st_intersection(sf::st_as_sfc(sf::st_bbox(data_sf_combined))) - -# Load EEZ polygons -eezs <- spatialgridr::get_boundary(name = "Micronesia", type = "eez", country_type = "country") %>% - sf::st_transform(crs = sf::st_crs(data_sf_combined)) %>% - sf::st_make_valid() %>% - sf::st_intersection(sf::st_as_sfc(sf::st_bbox(data_sf_combined))) - -## ----echo=FALSE--------------------------------------------------------------- -# Create the map -ggplot2::ggplot(data_sf_combined) + - ggplot2::geom_sf(ggplot2::aes(color = `Geartype`)) + - ggplot2::geom_sf(data = coast_clipped, color = "black", fill = NA) + # Add coastline - ggplot2::geom_sf(data = eezs, color = "red", fill = NA) + # Ajouter la EEZ avec hachures - ggplot2::theme_minimal() + - ggplot2::labs(title = "2020 Vessel Activity Map", subtitle = "recorded by GFW in Micronesia", color = "Gear types") + - ggplot2::theme(legend.position = "right") - -## ----echo=FALSE, message=FALSE, results='hide'-------------------------------- -data_sf_combined <- splnr_get_gfw(region = "Papua New Guinea", - start_date = "2019-12-31", - end_date = "2021-01-01", - temp_res = "YEARLY", - spat_res = "LOW") %>% - sf::st_set_crs(sf::st_crs(rnaturalearth::ne_coastline(scale = "large"))) - -coast_clipped <- rnaturalearth::ne_coastline(scale = "large") %>% - sf::st_as_sf() %>% - sf::st_intersection(sf::st_as_sfc(sf::st_bbox(data_sf_combined))) - -# Load EEZ polygons -eezs <- spatialgridr::get_boundary(name = "Papua New Guinea", type = "eez", country_type = "country") %>% - sf::st_transform(crs = sf::st_crs(data_sf_combined)) %>% - sf::st_make_valid() %>% - sf::st_intersection(sf::st_as_sfc(sf::st_bbox(data_sf_combined))) - -## ----echo=FALSE--------------------------------------------------------------- -# Create the map -ggplot2::ggplot(data_sf_combined) + - ggplot2::geom_sf(ggplot2::aes(color = `Flag`)) + - ggplot2::geom_sf(data = coast_clipped, color = "black", fill = NA) + # Add coastline - ggplot2::geom_sf(data = eezs, color = "red", fill = NA) + # Add EEZ - ggplot2::scale_size_continuous(range = c(1, 10), guide = "legend", name = "Flag") + - ggplot2::theme_minimal() + - ggplot2::labs(title = "2021 Vessel Activity Map", subtitle = "recorded by GFW in Papua New Guinea", color = "Flag") + - ggplot2::theme(legend.position = "right") - diff --git a/vignettes/GlobalFishingWatch.html b/vignettes/GlobalFishingWatch.html deleted file mode 100644 index 4b8b52a..0000000 --- a/vignettes/GlobalFishingWatch.html +++ /dev/null @@ -1,535 +0,0 @@ - - - - - - - - - - - - - - -Global Fishing Watch - - - - - - - - - - - - - - - - - - - - - - - - - - -

Global Fishing Watch

- - - -
-

Global Fishing Watch R Package (gfwr)

-

The gfwr package provides convenient functions to pull -GFW data directly into R into usable formats. It contains three main -functions, including : get_vessel_info(), -get_event() and get_raster(). The two first -being devoted to retrieving information and features on one ore several -specific vessels. The last is of particular interest to us because it -allows us to gather information from global fishing watch raster on the -fishing effort (further details in the function appropriate section). -Here we mainly use the splnr_get_gfw function which has -been created to enable data to be retrieved and processed in a variety -of ways, some of which are described here.

-

The time spent fishing is computed using Automatic Identification -System (AIS) data, which is transmitted by most industrial fishing -vessels. The AIS data provides information on the location, speed, and -direction of the vessel, which can be used to identify when the vessel -is actively fishing.

-
-

AIS Caveats and limitations

-

The AIS coverage of vessels has several limitations such as:
-1. The number of vessels that are captured (AIS provides approximately -70’000 of the 2.8 million identified fishing vessels).
-2. The size of the vessels (52-85% for vessels larger than 24 meters -against 1% for vessels under 12 meters).
-Good to know: IMO mandates AIS for most vessels larger than 36 -meters.
-3. AIS interference with each other in areas of high vessel -density.
-4. Some terrestrial satellites only receive messages near shore.

-
-
-

Installation

-
remotes::install_github("GlobalFishingWatch/gfwr")
-
library(gfwr)
-library(spatialplanr)
-
-
-

API

-

To access GFW APIs, you need to :
1. register for a GFW account -here.
-2. Request API key here.

-

Once you have your token, add it to your .Renviron file (by executing -the chunk below), by writing (GFW_TOKEN = “YOUR_TOKEN”) in the file. -
(You could be asked to restart R for changes to take -effect.)

-
usethis::edit_r_environ()
-

We save the key in an object that will be used in gfwr functions.

-
key <- gfwr::gfw_auth()
-
-
-

Fishing effort visualization

-

A region_id is necessary to use the get_raster -function.

-
region_id <- gfwr::get_region_id(region_name = "Australia", 
-                                 region_source = "EEZ",
-                                 key = gfwr::gfw_auth())$id
-

The get_raster function gets a raster of fishing effort -from the API and converts the response to a data frame which contains -occurrences for each vessel and for each grid cell (data is binned into -grid cells of different resolution), the Vessel IDs, -Flag, Geartype and -Apparent fishing Hours which are basically the amount of -fishing hours of each vessel per grid cell (geometry).

-

Data can be provided through :
- DAILY, -MONTHLY and YEARLY temporal resolutions.
-- LOW (0.1 deg) and HIGH (0.01 deg) spatial -resolutions.
- VESSEL_ID, FLAG, -GEARTYPE, FLAGANDGEARTYPE.

-
gfwr::get_raster(
-  spatial_resolution = "LOW",
-  temporal_resolution = "MONTHLY",
-  group_by = "FLAGANDGEARTYPE",
-  start_date = "2022-01-01",
-  end_date = "2023-01-01",
-  region = region_id,
-  region_source = "EEZ",
-  key = gfwr::gfw_auth()
-)
-

(You can remove the option message = FALSE -to see the columns types.)

-
-

get_raster caveats and limitations.

-

Date range is limited to 1-year. Nevertheless, with some -modifications, we can get round these problems through -splnr_get_gfw.

-
data_sf_combined <- splnr_get_gfw(region = "Australia", 
-                                  start_date = "2019-01-01",
-                                  end_date =  "2023-12-31",
-                                  temp_res = "YEARLY",
-                                  spat_res = "LOW",
-                                  compress = FALSE)
-
-
-
-

Visualization

-

To display the data, we load :
- The coastline from -rnaturalearth package and modify it to get an sf object, -and we constrain it to the boundaries of the given data.
- EEZ -Polygons from oceandatr package

-
# Check and modify if necessary the spatial reference of data_sf_combined
-data_sf_combined <- sf::st_set_crs(data_sf_combined, 
-                                   sf::st_crs(rnaturalearth::ne_coastline(scale = "large")))
-
-coast_clipped <- rnaturalearth::ne_coastline(scale = "large") %>%
-  sf::st_as_sf() %>%
-  sf::st_intersection(sf::st_as_sfc(sf::st_bbox(data_sf_combined)))
-
-# Load EEZ polygons
-eezs <- spatialgridr::get_boundary(name = "Australia", type = "eez", country_type = "country") %>%
-  sf::st_transform(crs = sf::st_crs(data_sf_combined)) %>%
-  sf::st_make_valid() %>%
-  sf::st_intersection(sf::st_as_sfc(sf::st_bbox(data_sf_combined)))
-
-
-

Here we display the Fishing Effort in Australia from 2019 to -2023.

-
-

Raw Fishing Effort

-

-
-
-

By years

-

-
-
-

Year-on-year comparison

-

We may need to compare different timeframes, such as seasons, to see -if there are any patterns.
Note : As more vessels -have adopted AIS (mainly in economically developed countries) since the -deployment of these technologies, the rise in activities must be seen in -the context of this increase and not necessarily of more intense fishing -activity.

-
# We need to change the temporal range according to our need group by it to display the total fishing hours. <br>
-data_sf_combined <- splnr_get_gfw(region = "Australia", 
-                                  start_date = "2019-01-01", 
-                                  end_date = "2023-12-31", 
-                                  temp_res = "MONTHLY", 
-                                  key = gfwr::gfw_auth()) %>%
-  dplyr::group_by(Year, Month) %>%
-  dplyr::summarize(Total_Fishing_Hours = sum(ApparentFishingHrs))
-

-
-
-

Fishing gear type

-

Here we display the Vessel activity in ‘Micronesia’ in 2020 according -to the fishing gear type.

-
data_sf_combined <- splnr_get_gfw(region = "Micronesia", 
-                                  start_date = "2019-12-31", 
-                                  end_date = "2021-01-01", 
-                                  temp_res = "MONTHLY")
-

-
-
-

Flags

-

Here we display the Vessel activity in Papua New Guinea according to -Vessels flags.

-

-
-
-

Supplementary materials.

-

The fishing detection model was trained on AIS data from 503 vessels -and identified fishing activity with over 90% accuracy, which means that -it can identify a fishing and non-fishing activity with high accuracy. -More details on AIS operation and limitations here.

-
-
-

Hierarchy of vessels gear types :

-

Fishing Classification Hierarchy -

-

Source : https://globalfishingwatch.org/datasets-and-code-vessel-identity/ -

-
-
-
- - - - - - - - - - - diff --git a/vignettes/MultipleUse.R b/vignettes/MultipleUse.R deleted file mode 100644 index ce774d8..0000000 --- a/vignettes/MultipleUse.R +++ /dev/null @@ -1,301 +0,0 @@ -## ----include = FALSE---------------------------------------------------------- -knitr::opts_chunk$set( -collapse = TRUE, -comment = "#>", -warning = FALSE, -cache = FALSE, -message = FALSE, -eval = TRUE -) - -## ----setup-------------------------------------------------------------------- -library(spatialplanr) -set.seed(100) - -## ----------------------------------------------------------------------------- -Region <- "Coral Sea" # "Australia" -Type <- "Oceans" # "EEZ" - -## ----------------------------------------------------------------------------- -PU_size <- 107460 # m - -## ----------------------------------------------------------------------------- -cCRS <- "ESRI:54009" - -## ----------------------------------------------------------------------------- -Bndry <- splnr_get_boundary(Limits = Region, Type = Type, cCRS = cCRS) - -landmass <- rnaturalearth::ne_countries( - scale = "medium", - returnclass = "sf" -) %>% - sf::st_transform(cCRS) - -## ----------------------------------------------------------------------------- -PUs <- spatialgridr::get_grid(boundary = Bndry, - crs = cCRS, - output = "sf_hex", - resolution = PU_size) - - -## ----------------------------------------------------------------------------- -splnr_theme <- list( - ggplot2::theme_bw(), - ggplot2::theme( - legend.position = "right", - legend.direction = "vertical", - text = ggplot2::element_text(size = 9, colour = "black"), - axis.text = ggplot2::element_text(size = 9, colour = "black"), - plot.title = ggplot2::element_text(size = 9), - axis.title = ggplot2::element_blank() - ) -) - -## ----------------------------------------------------------------------------- -Dict <- tibble::tribble( - ~nameCommon, ~nameVariable, ~category, - "Green sea turtle", "Chelonia_mydas", "Reptiles", - "Loggerhead sea turtle", "Caretta_caretta", "Reptiles", - "Hawksbill sea turtle", "Eretmochelys_imbricata", "Reptiles", - "Olive ridley sea turtle", "Lepidochelys_olivacea", "Reptiles", - "Saltwater crocodile", "Crocodylus_porosus", "Reptiles", - "Humpback whale", "Megaptera_novaeangliae", "Mammals", - "Common Minke whale", "Balaenoptera_acutorostrata", - "Mammals", - "Dugong", "Dugong_dugon", "Mammals", - "Grey nurse shark", "Carcharias_taurus", "Sharks and rays", - "Tiger shark", "Galeocerdo_cuvier", "Sharks and rays", - "Great hammerhead shark", "Sphyrna_mokarran", - "Sharks and rays", - "Giant oceanic manta ray", "Mobula_birostris", "Sharks and rays", - "Reef manta ray", "Mobula_alfredi", "Sharks and rays", - "Whitetip reef shark", "Triaenodon_obesus", "Sharks and rays", - "Red-footed booby", "Sula_sula", "Birds" -) - -## ----------------------------------------------------------------------------- -datEx_species_bin <- spDataFiltered %>% - splnr_apply_cutoffs(Cutoffs = 0.5) - -col_name <- spDataFiltered %>% - sf::st_drop_geometry() %>% - colnames() - -## ----------------------------------------------------------------------------- -target <- rep(0.3, nrow(Dict)) - -p1 <- prioritizr::problem( - datEx_species_bin %>% dplyr::mutate(Cost1 = rep(1, 397)), - col_name, - "Cost1" -) %>% - prioritizr::add_min_set_objective() %>% - prioritizr::add_relative_targets(target) %>% - prioritizr::add_binary_decisions() %>% - prioritizr::add_default_solver(verbose = FALSE) - -## ----fig.width=9-------------------------------------------------------------- -s1 <- p1 %>% - prioritizr::solve.ConservationProblem() - -(ggSoln <- splnr_plot_solution(s1) + - splnr_gg_add(PUs = PUs, Bndry = Bndry, overlay = landmass, cropOverlay = PUs, ggtheme = splnr_theme)) - - -## ----------------------------------------------------------------------------- -s1T <- s1 %>% - dplyr::select(tidyselect::starts_with(c("solution"))) %>% - sf::st_drop_geometry() %>% - tibble::as_tibble() - -r1 <- prioritizr::eval_feature_representation_summary(p1, s1T) -print(r1) - -## ----------------------------------------------------------------------------- -target2 <- matrix(NA, ncol = 2, nrow = nrow(Dict)) -target2[, 1] <- 0.2 -target2[, 2] <- 0.05 - -## ----------------------------------------------------------------------------- -z2 <- prioritizr::zones("zone 1" = col_name, "zone 2" = col_name) - -## ----------------------------------------------------------------------------- -p2 <- prioritizr::problem( - datEx_species_bin %>% dplyr::mutate( - Cost1 = rep(1, 397), # when giving sf input, we need as many cost columns as we have zones - Cost2 = runif(n = dim(.)[[1]]) - ), - z2, - cost_column = c("Cost1", "Cost2") -) %>% - prioritizr::add_min_set_objective() %>% - prioritizr::add_relative_targets(target2) %>% - prioritizr::add_binary_decisions() %>% - prioritizr::add_default_solver(verbose = FALSE) - -s2 <- p2 %>% - prioritizr::solve.ConservationProblem() - -## ----fig.width = 9------------------------------------------------------------ -(gg_s2 <- splnr_plot_solution( - s2, - zones = TRUE, - colorVals = c("#c6dbef", "#3182bd", "black"), - legendLabels = c("Not selected", "Zone 1", "Zone 2") -) + - splnr_gg_add( - PUs = PUs, Bndry = Bndry, overlay = landmass, - cropOverlay = PUs, ggtheme = splnr_theme - )) - -## ----------------------------------------------------------------------------- -targets2b <- Dict %>% - dplyr::mutate( - targetZ1 = dplyr::if_else(category == "Reptiles", 30 / 100, 0), - targetZ2 = dplyr::if_else(category != "Reptiles", 10 / 100, 0) - ) %>% - dplyr::select("targetZ1", "targetZ2") %>% - as.matrix() - -## ----------------------------------------------------------------------------- -# NOTE: when using sf input, we need as many cost columns as we have zones -p2b <- prioritizr::problem( - datEx_species_bin %>% dplyr::mutate( - Cost1 = rep(1, 397), - Cost2 = runif(n = dim(.)[[1]]) - ), - z2, - cost_column = c("Cost1", "Cost2") -) %>% - prioritizr::add_min_set_objective() %>% - prioritizr::add_relative_targets(targets2b) %>% - prioritizr::add_binary_decisions() %>% - prioritizr::add_default_solver(verbose = FALSE) - -s2b <- p2b %>% - prioritizr::solve.ConservationProblem() - -## ----------------------------------------------------------------------------- -r2b <- s2b %>% - dplyr::select(tidyselect::starts_with(c("solution"))) %>% - sf::st_drop_geometry() %>% - tibble::as_tibble() %>% - prioritizr::eval_feature_representation_summary(p2b, .) -print(r2b, n = 45) - -## ----------------------------------------------------------------------------- -Dict[[1]][6] -Dict[[1]][7] - -## ----fig.width = 9------------------------------------------------------------ -(gg_s2b <- splnr_plot_solution( - s2b, - zones = TRUE, - colorVals = c("#c6dbef", "#3182bd", "black"), - legendLabels = c("Not selected", "Zone 1", "Zone 2") -) + - splnr_gg_add( - PUs = PUs, Bndry = Bndry, overlay = landmass, - cropOverlay = PUs, ggtheme = splnr_theme - )) - -## ----------------------------------------------------------------------------- -zm1 <- diag(2) -print(zm1) - -## ----------------------------------------------------------------------------- -p3 <- prioritizr::problem( - datEx_species_bin %>% dplyr::mutate( - Cost1 = rep(1, 397), # when giving sf input, we need as many cost columns as we have zones - Cost2 = runif(n = dim(.)[[1]]) - ), - z2, - cost_column = c("Cost1", "Cost2") -) %>% - prioritizr::add_min_set_objective() %>% - prioritizr::add_boundary_penalties(0.5, zone = zm1) %>% - prioritizr::add_relative_targets(target2) %>% - prioritizr::add_binary_decisions() %>% - prioritizr::add_default_solver(time_limit = 10, verbose = FALSE) - -s3 <- p3 %>% - prioritizr::solve.ConservationProblem() - -## ----fig.width=9-------------------------------------------------------------- -(gg_s3 <- splnr_plot_solution( - s3, - zones = TRUE, - colorVals = c("#c6dbef", "#3182bd", "black"), - legendLabels = c("Not selected", "Zone 1", "Zone 2") -) + - splnr_gg_add( - PUs = PUs, Bndry = Bndry, overlay = landmass, - cropOverlay = PUs, ggtheme = splnr_theme - )) - -## ----fig.width=9-------------------------------------------------------------- -zm2 <- zm1 -zm2[2, 2] <- 0 - -# NOTE: When using sf input, we need as many cost columns as we have zones -p4 <- prioritizr::problem( - datEx_species_bin %>% dplyr::mutate( - Cost1 = rep(1, 397), - Cost2 = runif(n = dim(.)[[1]]) - ), - z2, - cost_column = c("Cost1", "Cost2") -) %>% - prioritizr::add_min_set_objective() %>% - prioritizr::add_boundary_penalties(0.5, zone = zm2) %>% - prioritizr::add_relative_targets(target2) %>% - prioritizr::add_binary_decisions() %>% - prioritizr::add_default_solver(time_limit = 10, verbose = FALSE) - -s4 <- p4 %>% - prioritizr::solve.ConservationProblem() - -(gg_s4 <- splnr_plot_solution( - s4, - zones = TRUE, - colorVals = c("#c6dbef", "#3182bd", "black"), - legendLabels = c("Not selected", "Zone 1", "Zone 2") -) + - splnr_gg_add( - PUs = PUs, Bndry = Bndry, overlay = landmass, - cropOverlay = PUs, ggtheme = splnr_theme - )) - -## ----------------------------------------------------------------------------- -zm3 <- matrix(1, ncol = 2, nrow = 2) -print(zm3) - -## ----fig.width=9-------------------------------------------------------------- -p5 <- prioritizr::problem( - datEx_species_bin %>% dplyr::mutate( - Cost1 = rep(1, 397), # when giving sf input, we need as many cost columns as we have zones - Cost2 = runif(n = dim(.)[[1]]) - ), - z2, - cost_column = c("Cost1", "Cost2") -) %>% - prioritizr::add_min_set_objective() %>% - prioritizr::add_boundary_penalties(0.5, zone = zm3) %>% - prioritizr::add_relative_targets(target2) %>% - prioritizr::add_binary_decisions() %>% - prioritizr::add_default_solver(time_limit = 10, verbose = FALSE) - -s5 <- p5 %>% - prioritizr::solve.ConservationProblem() - -(gg_s5 <- splnr_plot_solution( - s5, - zones = TRUE, - colorVals = c("#c6dbef", "#3182bd", "black"), - legendLabels = c("Not selected", "Zone 1", "Zone 2") -) + - splnr_gg_add( - PUs = PUs, Bndry = Bndry, overlay = landmass, - cropOverlay = PUs, ggtheme = splnr_theme - )) - diff --git a/vignettes/spatialplanr.R b/vignettes/spatialplanr.R deleted file mode 100644 index 10467e0..0000000 --- a/vignettes/spatialplanr.R +++ /dev/null @@ -1,99 +0,0 @@ -## ----include = FALSE---------------------------------------------------------- -knitr::opts_chunk$set( -collapse = TRUE, -comment = "#>", -warning = FALSE, -cache = FALSE, -message = FALSE, -eval = TRUE, -fig.width = 9 -) - -## ----setup-------------------------------------------------------------------- -# library(spatialplanr) -devtools::load_all() - -## ----------------------------------------------------------------------------- -Region <- "Coral Sea" # "Australia" -Type <- "Oceans" # "EEZ" - -## ----eval=FALSE--------------------------------------------------------------- -# Region <- c(xmin = 150, xmax = 160, ymin = -40, ymax = -30) - -## ----------------------------------------------------------------------------- -cCRS <- "ESRI:54009" - -## ----------------------------------------------------------------------------- -PU_size <- 107460 # m - -## ----------------------------------------------------------------------------- -Bndry <- splnr_get_boundary(Limits = Region, Type = Type, cCRS = cCRS) - -## ----------------------------------------------------------------------------- -landmass <- rnaturalearth::ne_countries( - scale = "medium", - returnclass = "sf" -) %>% - sf::st_transform(cCRS) - -## ----------------------------------------------------------------------------- -PUs <- spatialgridr::get_grid(boundary = Bndry, - crs = cCRS, - output = "sf_hex", - resolution = PU_size) - - -## ----------------------------------------------------------------------------- -(ggPU <- splnr_plot(df = PUs) + - ggplot2::theme_bw()) # Plot Planning Units - -## ----------------------------------------------------------------------------- -(ggPU <- splnr_plot(df = PUs) + - splnr_gg_add( - Bndry = Bndry, overlay = landmass, - cropOverlay = PUs, ggtheme = "Default" - )) - -## ----------------------------------------------------------------------------- -splnr_theme <- list( - ggplot2::theme_bw(), - ggplot2::theme( - legend.position = "right", - legend.direction = "vertical", - text = ggplot2::element_text(size = 9, colour = "black"), - axis.text = ggplot2::element_text(size = 9, colour = "black"), - plot.title = ggplot2::element_text(size = 9), - axis.title = ggplot2::element_blank() - ) -) - -(ggPU <- splnr_plot(PUs) + - splnr_gg_add( - Bndry = Bndry, overlay = landmass, - cropOverlay = PUs, ggtheme = splnr_theme - )) - -## ----------------------------------------------------------------------------- -Dict <- tibble::tribble( - ~nameCommon, ~nameVariable, ~category, - "Green sea turtle", "Chelonia_mydas", "Reptiles", - "Loggerhead sea turtle", "Caretta_caretta", "Reptiles", - "Hawksbill sea turtle", "Eretmochelys_imbricata", "Reptiles", - "Olive ridley sea turtle", "Lepidochelys_olivacea", "Reptiles", - "Saltwater crocodile", "Crocodylus_porosus", "Reptiles", - "Humpback whale", "Megaptera_novaeangliae", "Mammals", - "Common Minke whale", "Balaenoptera_acutorostrata", "Mammals", - "Dugong", "Dugong_dugon", "Mammals", - "Grey nurse shark", "Carcharias_taurus", "Sharks and rays", - "Tiger shark", "Galeocerdo_cuvier", "Sharks and rays", - "Great hammerhead shark", "Sphyrna_mokarran", "Sharks and rays", - "Giant oceanic manta ray", "Mobula_birostris", "Sharks and rays", - "Reef manta ray", "Mobula_alfredi", "Sharks and rays", - "Whitetip reef shark", "Triaenodon_obesus", "Sharks and rays", - "Red-footed booby", "Sula_sula", "Birds" -) - -## ----------------------------------------------------------------------------- -datEx_species_bin <- dat_species_prob %>% - splnr_apply_cutoffs(Cutoffs = 0.5) - diff --git a/vignettes/spatialplanr.Rmd b/vignettes/spatialplanr.Rmd index dd190c8..029fc71 100644 --- a/vignettes/spatialplanr.Rmd +++ b/vignettes/spatialplanr.Rmd @@ -185,9 +185,9 @@ the binary habitat suitability map for Green sea turtles: ```{r} (ggFeature1 <- splnr_plot( df = datEx_species_bin, - col_names = "Chelonia_mydas", - plot_title = "Chelonia mydas", - legend_labels = c("Absence", "Presence") + colNames = "Chelonia_mydas", + plotTitle = "Chelonia mydas", + legendLabels = c("Absence", "Presence") ) + splnr_gg_add( PUs = PUs, Bndry = Bndry, overlay = landmass, @@ -205,7 +205,7 @@ are fairly ubiquitous across the whole Coral Sea. (ggFeature <- splnr_plot( datEx_species_bin, "Megaptera_novaeangliae", - plot_title = "Megaptera novaeangliae" + plotTitle = "Megaptera novaeangliae" ) + splnr_gg_add( PUs = PUs, Bndry = Bndry, overlay = landmass, @@ -224,10 +224,10 @@ planning unit of the study region. ```{r} (ggFeatNo <- splnr_plot(df = datEx_species_bin, - col_names = colnames(datEx_species_bin %>% + colNames = colnames(datEx_species_bin %>% sf::st_drop_geometry()), - plot_title = "", - legend_title = "Number of features") + + plotTitle = "", + legendTitle = "Number of features") + splnr_gg_add( PUs = PUs, Bndry = Bndry, overlay = landmass, cropOverlay = PUs, ggtheme = splnr_theme @@ -260,8 +260,8 @@ units are assigned an equal cost of 1. ```{r} out_sf$Cost_None <- 1 -(ggCost <- splnr_plot(out_sf, col_names = "Cost_None", - legend_title = "Cost", legend_labels = "1") + +(ggCost <- splnr_plot(out_sf, colNames = "Cost_None", + legendTitle = "Cost", legendLabels = "1") + splnr_gg_add( PUs = PUs, Bndry = Bndry, overlay = landmass, cropOverlay = PUs, ggtheme = splnr_theme @@ -289,7 +289,7 @@ out_sf$Apparent.Fishing.Hours[as.numeric(rownames(PUs))] <- gfw_data$Apparent.Fishing.Hours # Put corresponding data in PUs -(ggCost <- splnr_plot(out_sf, col_names = "Apparent.Fishing.Hours") + +(ggCost <- splnr_plot(out_sf, colNames = "Apparent.Fishing.Hours") + splnr_gg_add( PUs = PUs, Bndry = Bndry, overlay = landmass, cropOverlay = PUs, ggtheme = splnr_theme @@ -316,7 +316,7 @@ enviro_regions <- oceandatr::get_enviro_regions(planning_grid = PUs, ```{r, eval=FALSE, echo=FALSE} -splnr_plot(df = bathymetry, col_names = "bathymetry", plot_title = "") + +splnr_plot(df = bathymetry, colNames = "bathymetry", plotTitle = "") + splnr_gg_add( PUs = PUs, Bndry = Bndry, overlay = landmass, cropOverlay = PUs, ggtheme = splnr_theme @@ -370,9 +370,9 @@ importance scores ```{r} (ggSoln <- splnr_plot(datEx_soln, - col_names = "solution_1", - legend_title = "Solution", - legend_labels = c("0","1")) + + colNames = "solution_1", + legendTitle = "Solution", + legendLabels = c("0","1")) + splnr_gg_add( PUs = PUs, Bndry = Bndry, overlay = landmass, cropOverlay = PUs, ggtheme = splnr_theme @@ -389,8 +389,8 @@ overlay of the cost to show how the solution avoids highly costly areas: ```{r} (ggCostOverlay <- splnr_plot_costOverlay( soln = datEx_soln, - Cost = NA, - Cost_name = "Cost_None" + cost = NA, + costName = "Cost_None" ) + splnr_gg_add( PUs = PUs, Bndry = Bndry, overlay = landmass,