diff --git a/NAMESPACE b/NAMESPACE index d57c0b17f..5ff4366ea 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -24,9 +24,9 @@ export(TADA_CreateAUMLCrosswalk) export(TADA_CreateCSV) export(TADA_CreateComparableID) export(TADA_CreatePairRef) +export(TADA_CreatePointAUGeometry) export(TADA_CreatePointAUs) export(TADA_CreateUnitRef) -export(TADA_CrosswalkATTAINSWaterTypes) export(TADA_DataRetrieval) export(TADA_DayOfYearPlot) export(TADA_DefineCriteriaMethodology) @@ -108,6 +108,7 @@ export(TADA_TribalOptions) export(TADA_TwoCharacteristicScatterplot) export(TADA_UniqueCharUnitSpeciation) export(TADA_UpdateATTAINSAUMLCrosswalk) +export(TADA_UpdateATTAINSWaterTypes) export(TADA_UsesForAnalysis) export(TADA_ViewATTAINS) export(TADA_ViewColorPalette) diff --git a/R/ATTAINSCrosswalks.R b/R/ATTAINSCrosswalks.R index f47de5ff5..9642d16e2 100644 --- a/R/ATTAINSCrosswalks.R +++ b/R/ATTAINSCrosswalks.R @@ -4516,87 +4516,368 @@ TADA_MLSummary <- function( return(MLSummaryRef) } -#' Create an ATTAINS AU–ML Crosswalk from WQP Monitoring Location IDs +#' Crosswalk WQP Monitoring Location Type to ATTAINS Water Type +#' +#' Adds or updates ATTAINS.WaterType using TADA.MonitoringLocationTypeName. +#' By default, only missing ATTAINS.WaterType values are populated. +#' +#' @param .data A TADA data frame. +#' @param org_id Character string. Optional organization ID used to prioritize +#' organization-specific ATTAINS values. +#' @param org_only Logical. If TRUE, only org-specific ATTAINS values are used. +#' If FALSE, unmatched types fall back to the TADA default crosswalk. +#' @param replace_all Logical. If TRUE, replace all ATTAINS.WaterType values. +#' If FALSE, only fill missing values. Default is FALSE. +#' +#' @return A TADA data frame with ATTAINS.WaterType populated. +#' @export +#' +#' @examples +#' +#' \dontrun{ +#' +#' # example for MT data +#' testdat <- Data_MT_MissoulaCounty +#' +#' crosswalk <- TADA_UpdateATTAINSWaterTypes(testat, org_Id = "MTDEQ") +#' } +TADA_UpdateATTAINSWaterTypes <- function( + .data, + org_id = NULL, + org_only = FALSE, + replace_all = FALSE +) { + required_cols <- c( + "TADA.MonitoringLocationIdentifier", + "TADA.MonitoringLocationTypeName" + ) + + missing_cols <- setdiff(required_cols, names(.data)) + if (length(missing_cols) > 0) { + stop( + "TADA_UpdateATTAINSWaterTypes: .data must contain TADA.MonitoringLocationIdentifier and TADA.MonitoringLocationTypeName." + ) + } + + org.ref <- TADA_GetATTAINSOrgIDsRef() |> dplyr::select(code) |> dplyr::pull() + + if (length(org_id) > 1) { + print.org <- paste0(org_id, " is not a valid input.") + + stop(paste0( + "TADA_UpdateATTAINSWaterTypes: org_id must be either NULL or a single non-NA character string matching an ATTAINSOrganizationIdentifier." + )) + } + + if (!org_id %in% org.ref & !is.null(org_id)) { + print.org <- paste0(org_id, " is not a valid input.") + + stop(paste0( + "TADA_UpdateATTAINSWaterTypes: org_type must be a single non-NA logical. ", + print.org + )) + } + + # need better check for org_only + + # normalize NA to blanks + if ("ATTAINS.WaterType" %in% names(.data)) { + .data <- .data |> + dplyr::mutate(ATTAINS.WaterType = dplyr::na_if(ATTAINS.WaterType, "")) + } + + # Create one-row-per-location lookup + lookup <- .data |> + dplyr::select(dplyr::any_of(c( + "TADA.MonitoringLocationIdentifier", + "TADA.MonitoringLocationTypeName", + "ATTAINS.WaterType" + ))) |> + dplyr::distinct() + + # Build crosswalk + cw <- build_attains_water_type_crosswalk(org_id = org_id, org_only = org_only) + + # Add crosswalk recommendation by monitoring location type + lookup <- lookup |> + dplyr::left_join( + cw, + by = dplyr::join_by(TADA.MonitoringLocationTypeName), + relationship = "many-to-many" + ) + + # Fill or replace lookup water type + if (!"ATTAINS.WaterType" %in% names(lookup)) { + lookup <- lookup |> + dplyr::transmute( + TADA.MonitoringLocationIdentifier, + TADA.ATTAINS.WaterType = TADA.ATTAINS.WaterType + ) + } else if (isTRUE(replace_all)) { + lookup <- lookup |> + dplyr::transmute( + TADA.MonitoringLocationIdentifier, + TADA.ATTAINS.WaterType = TADA.ATTAINS.WaterType + ) + } else { + lookup <- lookup |> + dplyr::mutate( + TADA.ATTAINS.WaterType = dplyr::coalesce( + ATTAINS.WaterType, + TADA.ATTAINS.WaterType + ) + ) |> + dplyr::select(TADA.MonitoringLocationIdentifier, TADA.ATTAINS.WaterType) + } + + # Join back to original data + if (!"ATTAINS.WaterType" %in% names(.data)) { + .data <- .data |> + dplyr::left_join( + lookup, + by = dplyr::join_by(TADA.MonitoringLocationIdentifier), + relationship = "many-to-many" + ) |> + dplyr::rename(ATTAINS.WaterType = TADA.ATTAINS.WaterType) + } else if (isTRUE(replace_all)) { + .data <- .data |> + dplyr::left_join( + lookup |> dplyr::rename(New.ATTAINS.WaterType = TADA.ATTAINS.WaterType), + by = dplyr::join_by(TADA.MonitoringLocationIdentifier), + relationship = "many-to-many" + ) |> + dplyr::mutate(ATTAINS.WaterType = New.ATTAINS.WaterType) |> + dplyr::select(-New.ATTAINS.WaterType) + } else { + .data <- .data |> + dplyr::left_join( + lookup |> dplyr::rename(New.ATTAINS.WaterType = TADA.ATTAINS.WaterType), + by = dplyr::join_by(TADA.MonitoringLocationIdentifier), + relationship = "many-to-many" + ) |> + dplyr::mutate( + ATTAINS.WaterType = dplyr::coalesce( + ATTAINS.WaterType, + New.ATTAINS.WaterType + ) + ) |> + dplyr::select(-New.ATTAINS.WaterType) + } + + .data |> TADA_OrderCols() +} + + +#' Review ATTAINS Water Types +#' +#' Validates ATTAINS.WaterType against allowable ATTAINS domain values. +#' Can either flag invalid values or update them using the crosswalk. +#' +#' @param .data A TADA data frame. +#' @param review_action Character string. One of "flag" or "update". +#' +#' @return A TADA data frame with `TADA.ATTAINSWaterType.Flag` added. +#' If `review_action = "update"`, invalid values may also be replaced if an +#' ATTAINS water type match is available. +#' @export +#' +#' @examples +#' +#' \dontrun{ +#' +#' # example of updating invalid ATTAINS water types +#' example.df <- tibble::tibble( +#' TADA.MonitoringLocationIdentifier = c("id1", "id2"), +#' TADA.MonitoringLocationTypeName = c("RIVER/STREAM", "LAKE"), +#' ATTAINS.WaterType = c("INVALID WATER TYPE 1", "INVALID WATER TYPE 2") +#' ) +#' +#' review.df <- TADA_ReviewATTAINSWaterTypes(df, review_action = "update") +#' } +TADA_ReviewATTAINSWaterTypes <- function( + .data, + review_action = c("flag", "update") +) { + # set flags + allowable_value_flag <- "ATTAINS.WaterType matches an allowable ATTAINS.WaterType value." + invalid_flag <- "ATTAINS.WaterType value does not match any allowable ATTAINS.WaterType." + + # set required cols + required_cols <- c( + "TADA.MonitoringLocationIdentifier", + "TADA.MonitoringLocationTypeName", + "ATTAINS.WaterType" + ) + + # check columns and review_action param + TADA_CheckColumns(.data, required_cols) + review_action <- match.arg(review_action) + + # get ATTAINS water types + attains.types <- quiet( + rExpertQuery::EQ_DomainValues("water_type", api_key = .setEQKey()) |> + dplyr::pull(name) |> + unique() + ) |> + append(c("", NA)) + + # get rows with invalid ATTAINS water types + invalid_lookup <- .data |> + dplyr::filter(!ATTAINS.WaterType %in% attains.types) |> + dplyr::distinct( + TADA.MonitoringLocationIdentifier, + TADA.MonitoringLocationTypeName + ) + + # set water type flags + .data <- .data |> + dplyr::left_join( + invalid_lookup |> + dplyr::mutate(TADA.ATTAINSWaterType.Flag = invalid_flag), + by = dplyr::join_by( + TADA.MonitoringLocationIdentifier, + TADA.MonitoringLocationTypeName + ), + relationship = "many-to-many" + ) |> + dplyr::mutate( + TADA.ATTAINSWaterType.Flag = dplyr::if_else( + is.na(TADA.ATTAINSWaterType.Flag), + allowable_value_flag, + TADA.ATTAINSWaterType.Flag + ) + ) + + # return data if there are no invalid rows or review_action is flag + if (nrow(invalid_lookup) == 0 || review_action == "flag") { + return(.data |> TADA_OrderCols()) + } + + # set additional flags for "update" review_action + update_to_allowable <- "ATTAINS.WaterType was updated to match an allowable ATTAINS.WaterType value by crosswalking TADA.MonitoringLocationTypeName." + update_set_NA <- "ATTAINS.WaterType was updated to NA as no ATTAINS.WaterType value was found for the TADA.MonitoringLocationTypeName." + + # get water type crosswalk + cw <- build_attains_water_type_crosswalk() + + # identify replacement ATTAINS.WaterType values for invalid rows via crosswalk + update_lookup <- invalid_lookup |> + dplyr::left_join( + cw, + by = dplyr::join_by(TADA.MonitoringLocationTypeName) + ) |> + dplyr::transmute( + TADA.MonitoringLocationIdentifier, + TADA.MonitoringLocationTypeName, + New.ATTAINS.WaterType = TADA.ATTAINS.WaterType, + TADA.ATTAINSWaterType.Flag = dplyr::if_else( + !is.na(New.ATTAINS.WaterType), + update_to_allowable, + update_set_NA + ) + ) + + # merge update lookup back to the full data, update values where possible, and flag unchanged allowable rows + .data |> + dplyr::left_join( + update_lookup, + by = dplyr::join_by( + TADA.MonitoringLocationIdentifier, + TADA.MonitoringLocationTypeName + ), + relationship = "many-to-many" + ) |> + dplyr::mutate( + ATTAINS.WaterType = dplyr::coalesce( + New.ATTAINS.WaterType, + ATTAINS.WaterType + ), + TADA.ATTAINSWaterType.Flag = dplyr::if_else( + is.na(TADA.ATTAINSWaterType.Flag), + allowable_value_flag, + TADA.ATTAINSWaterType.Flag + ) + ) |> + dplyr::select(-New.ATTAINS.WaterType) |> + TADA_OrderCols() + + return(.data) +} +#' Create an ATTAINS AU–ML Crosswalk from WQP Monitoring Location IDs for New Point AUs #' #' Build a distinct crosswalk between WQP Monitoring Locations and ATTAINS -#' Assessment Units. For rows where `ATTAINS.AssessmentUnitIdentifier` is -#' missing or blank, the value is filled with -#' `TADA.MonitoringLocationIdentifier`, optionally with an `auid_prefix` -#' appended. Existing non-missing, non-blank AUIDs are left unchanged. +#' Assessment Units. Missing or blank `ATTAINS.AssessmentUnitIdentifier` values +#' are filled from `TADA.MonitoringLocationIdentifier`, optionally prefixed +#' with `auid_prefix`. Existing non-missing, non-blank AUIDs are left unchanged. +#' Optionally, this function can also return either a df or a shp file containing +#' the point or multipoint geometry and its corresponding assessment unit identifier. #' #' If `ATTAINS.WaterType` is missing or contains any blank values, the function -#' will attempt to populate it by calling -#' `TADA_CrosswalkATTAINSWaterTypes()` internally with -#' `overwrite_existing = FALSE` and `validation = "none"`. +#' attempts to populate it by calling `TADA_UpdateATTAINSWaterTypes()` +#' internally with `overwrite_existing = FALSE` and `validation = "none"`. #' #' @param .data A data frame containing, at minimum: #' - `TADA.MonitoringLocationIdentifier` #' +#' If `create_geo = TRUE`, the input must also contain: +#' - `TADA.LatitudeMeasure` +#' - `TADA.LongitudeMeasure` +#' - `HorizontalCoordinateReferenceSystemDatumName` +#' #' If missing water-type values need to be crosswalked, the input must also #' contain: #' - `TADA.MonitoringLocationTypeName` #' -#' Optionally, the input may already include: +#' Optional columns: #' - `ATTAINS.AssessmentUnitIdentifier` #' - `ATTAINS.WaterType` #' -#' If `ATTAINS.AssessmentUnitIdentifier` is absent, it will be added. -#' #' @param auid_prefix Character or `NULL`. If provided and non-empty, this -#' prefix is included only for newly created -#' `ATTAINS.AssessmentUnitIdentifier` values that were filled from -#' `TADA.MonitoringLocationIdentifier`. Existing non-missing AUIDs are not -#' modified. Use `NULL` to skip prefixing. -#' -#' @return A distinct AU–ML crosswalk data frame containing: +#' prefix is added only to newly created +#' `ATTAINS.AssessmentUnitIdentifier` values. +#' @param create_geo Logical. If `TRUE`, also create point geometry using +#' `TADA_CreatePointAUGeometry()`. +#' @param download_geo Logical. If `TRUE`, write the geometry shapefile to the +#' user's downloads folder. +#' +#' @return If `create_geo = FALSE`, a distinct crosswalk data frame containing: #' - `ATTAINS.MonitoringLocationIdentifier` #' - `ATTAINS.AssessmentUnitIdentifier` #' - `ATTAINS.WaterType` #' +#' If `create_geo = TRUE`, a named list with: +#' - `crosswalk` +#' - `geometry` +#' #' @details -#' - Missing `ATTAINS.AssessmentUnitIdentifier` -#' values are replaced with `TADA.MonitoringLocationIdentifier`. -#' - If `auid_prefix` is supplied and non-empty, it is included for only -#' newly created AUIDs. +#' - Missing or blank `ATTAINS.AssessmentUnitIdentifier` values are replaced +#' with `TADA.MonitoringLocationIdentifier`. +#' - If `auid_prefix` is supplied and non-empty, it is used only for newly +#' created AUIDs. #' - `ATTAINS.MonitoringLocationIdentifier` is created from #' `TADA.MonitoringLocationIdentifier`. #' - `ATTAINS.WaterType` is not overwritten unless it is missing or blank. #' -#' @seealso [TADA_CrosswalkATTAINSWaterTypes()] -#' -#' @examples -#' \dontrun{ -#' # Example 1: Create missing AUIDs -#' ex_df <- data.frame( -#' TADA.MonitoringLocationIdentifier = c("LOC1", "LOC2", "LOC3"), -#' TADA.MonitoringLocationTypeName = c("Stream", "Lake", "Estuary"), -#' ATTAINS.AssessmentUnitIdentifier = c(NA_character_, "EXISTING_AU_001", ""), -#' ATTAINS.WaterType = c(NA_character_, "", "ESTUARY"), -#' stringsAsFactors = FALSE -#' ) -#' -#' result <- TADA_CreatePointAUs(ex_df) -#' -#' # Example 2: Prefix only newly created AUIDs -#' result_prefixed <- TADA_CreatePointAUs( -#' ex_df, -#' auid_prefix = "WQX_" -#' ) -#' -#' # Example 3: AUID column is absent entirely -#' ex_df2 <- data.frame( -#' TADA.MonitoringLocationIdentifier = c("SITE_A", "SITE_B"), -#' TADA.MonitoringLocationTypeName = c("River/Stream", "Lake, Reservoir, Impoundment"), -#' ATTAINS.WaterType = c(NA_character_, NA_character_), -#' stringsAsFactors = FALSE -#' ) -#' -#' result_missing_auid <- TADA_CreatePointAUs(ex_df2) -#' } +#' @seealso [TADA_UpdateATTAINSWaterTypes()], [TADA_CreatePointAUGeometry()] #' #' @export -TADA_CreatePointAUs <- function(.data, auid_prefix = NULL) { +TADA_CreatePointAUs <- function( + .data, + auid_prefix = NULL, + create_geo = FALSE, + download_geo = FALSE +) { req <- c("TADA.MonitoringLocationIdentifier") + geo_req <- c( + "TADA.LatitudeMeasure", + "TADA.LongitudeMeasure", + "HorizontalCoordinateReferenceSystemDatumName" + ) + + if (isTRUE(create_geo)) { + req <- c(req, geo_req) + } + missing <- setdiff(req, names(.data)) if (length(missing) > 0) { stop( @@ -4605,17 +4886,13 @@ TADA_CreatePointAUs <- function(.data, auid_prefix = NULL) { ) } - if (!"ATTAINS.AssessmentUnitIdentifier" %in% names(.data)) { - .data$ATTAINS.AssessmentUnitIdentifier <- NA_character_ - } + # ensure AUID column exists and is filled from monitoring location ID as needed + .data <- fill_missing_assessment_unit_id(.data, auid_prefix = auid_prefix) - .data$TADA.MonitoringLocationIdentifier <- as.character( - .data$TADA.MonitoringLocationIdentifier - ) - .data$ATTAINS.AssessmentUnitIdentifier <- as.character( - .data$ATTAINS.AssessmentUnitIdentifier - ) + # create ATTAINS.MonitoringLocationIdentifier + .data$ATTAINS.MonitoringLocationIdentifier <- .data$TADA.MonitoringLocationIdentifier + # determine whether ATTAINS.WaterType needs crosswalking need_crosswalk <- !("ATTAINS.WaterType" %in% names(.data)) || any( is.na(.data$ATTAINS.WaterType) | @@ -4623,39 +4900,44 @@ TADA_CreatePointAUs <- function(.data, auid_prefix = NULL) { na.rm = TRUE ) - if (need_crosswalk) { + if (isTRUE(need_crosswalk)) { if (!"TADA.MonitoringLocationTypeName" %in% names(.data)) { stop( - "TADA_CreatePointAUs: Missing required column: TADA.MonitoringLocationTypeName" + "TADA_CreatePointAUs: Missing required column for water-type crosswalk: TADA.MonitoringLocationTypeName" ) } - .data <- TADA_CrosswalkATTAINSWaterTypes(.data, replace_all = FALSE) + .data <- TADA_UpdateATTAINSWaterTypes(.data, replace_all = FALSE) } - created_AUID <- is.na(.data$ATTAINS.AssessmentUnitIdentifier) | - trimws(.data$ATTAINS.AssessmentUnitIdentifier) == "" + if (nrow(.data) == 0) { + stop("No rows in data after crosswalk") + } + + retain <- c( + "ATTAINS.MonitoringLocationIdentifier", + "ATTAINS.AssessmentUnitIdentifier", + "ATTAINS.WaterType" + ) - .data$ATTAINS.AssessmentUnitIdentifier[ - created_AUID - ] <- .data$TADA.MonitoringLocationIdentifier[created_AUID] + PointAU.Crosswalk <- .data |> + dplyr::select(dplyr::any_of(retain)) |> + dplyr::distinct() - if (!is.null(auid_prefix) && nzchar(auid_prefix)) { - .data$ATTAINS.AssessmentUnitIdentifier[created_AUID] <- paste0( - auid_prefix, - .data$ATTAINS.AssessmentUnitIdentifier[created_AUID] - ) + if (!isTRUE(create_geo)) { + return(PointAU.Crosswalk) } - .data$ATTAINS.MonitoringLocationIdentifier <- .data$TADA.MonitoringLocationIdentifier + PointAU.Geometry <- TADA_CreatePointAUGeometry( + .data = .data, + target_crs = 4269, + download_geo = download_geo, + auid_prefix = auid_prefix + ) - .data |> - dplyr::select( - ATTAINS.MonitoringLocationIdentifier, - ATTAINS.AssessmentUnitIdentifier, - ATTAINS.WaterType - ) |> - dplyr::distinct() + PointAUs <- list(crosswalk = PointAU.Crosswalk, geometry = PointAU.Geometry) + + return(PointAUs) } #' Build ATTAINS water type crosswalk @@ -4663,7 +4945,7 @@ TADA_CreatePointAUs <- function(.data, auid_prefix = NULL) { #' Internal helper to construct the crosswalk used to assign ATTAINS.WaterType #' from TADA.MonitoringLocationTypeName. #' -#' @inheritParams TADA_CrosswalkATTAINSWaterTypes +#' @inheritParams TADA_UpdateATTAINSWaterTypes #' @param org_only Logical. If TRUE, only organization-specific ATTAINS water #' types are used. If FALSE, unmatched types fall back to the TADA default #' crosswalk. @@ -4799,9 +5081,9 @@ build_attains_water_type_crosswalk <- function( #' # example for MT data #' testdat <- Data_MT_MissoulaCounty #' -#' crosswalk <- TADA_CrosswalkATTAINSWaterTypes(testat, org_Id = "MTDEQ") +#' crosswalk <- TADA_UpdateATTAINSWaterTypes(testat, org_Id = "MTDEQ") #' } -TADA_CrosswalkATTAINSWaterTypes <- function( +TADA_UpdateATTAINSWaterTypes <- function( .data, org_id = NULL, org_only = FALSE, @@ -4815,7 +5097,7 @@ TADA_CrosswalkATTAINSWaterTypes <- function( missing_cols <- setdiff(required_cols, names(.data)) if (length(missing_cols) > 0) { stop( - "TADA_CrosswalkATTAINSWaterTypes: .data must contain TADA.MonitoringLocationIdentifier and TADA.MonitoringLocationTypeName." + "TADA_UpdateATTAINSWaterTypes: .data must contain TADA.MonitoringLocationIdentifier and TADA.MonitoringLocationTypeName." ) } @@ -4824,13 +5106,13 @@ TADA_CrosswalkATTAINSWaterTypes <- function( (!is.character(org_id) || length(org_id) != 1 || is.na(org_id)) ) { stop( - "TADA_CrosswalkATTAINSWaterTypes: org_id must be NULL or a single non-NA character string." + "TADA_UpdateATTAINSWaterTypes: org_id must be NULL or a single non-NA character string." ) } if (!is.logical(org_only) || length(org_only) != 1 || is.na(org_only)) { stop( - "TADA_CrosswalkATTAINSWaterTypes: org_only must be a single non-NA logical." + "TADA_UpdateATTAINSWaterTypes: org_only must be a single non-NA logical." ) } @@ -4838,29 +5120,28 @@ TADA_CrosswalkATTAINSWaterTypes <- function( !is.logical(replace_all) || length(replace_all) != 1 || is.na(replace_all) ) { stop( - "TADA_CrosswalkATTAINSWaterTypes: replace_all must be a single non-NA logical." + "TADA_UpdateATTAINSWaterTypes: replace_all must be a single non-NA logical." ) } - # normalize NA to blanks + # normalize blanks to NA if ("ATTAINS.WaterType" %in% names(.data)) { .data <- .data |> dplyr::mutate(ATTAINS.WaterType = dplyr::na_if(ATTAINS.WaterType, "")) } - # Create one-row-per-location lookup + # Keep one row per monitoring location for the lookup lookup <- .data |> - dplyr::select(dplyr::any_of(c( - "TADA.MonitoringLocationIdentifier", - "TADA.MonitoringLocationTypeName", - "ATTAINS.WaterType" - ))) |> - dplyr::distinct() + dplyr::distinct( + TADA.MonitoringLocationIdentifier, + TADA.MonitoringLocationTypeName, + .keep_all = TRUE + ) # Build crosswalk cw <- build_attains_water_type_crosswalk(org_id = org_id, org_only = org_only) - # Add crosswalk recommendation by monitoring location type + # Join crosswalk to lookup lookup <- lookup |> dplyr::left_join( cw, @@ -4868,63 +5149,37 @@ TADA_CrosswalkATTAINSWaterTypes <- function( relationship = "many-to-many" ) - # Fill or replace lookup water type - if (!"ATTAINS.WaterType" %in% names(lookup)) { - lookup <- lookup |> - dplyr::transmute( - TADA.MonitoringLocationIdentifier, - TADA.ATTAINS.WaterType = TADA.ATTAINS.WaterType - ) - } else if (isTRUE(replace_all)) { - lookup <- lookup |> - dplyr::transmute( - TADA.MonitoringLocationIdentifier, - TADA.ATTAINS.WaterType = TADA.ATTAINS.WaterType - ) + # Resolve water type + if ("ATTAINS.WaterType" %in% names(lookup)) { + if (isTRUE(replace_all)) { + lookup <- lookup |> + dplyr::mutate(ATTAINS.WaterType = TADA.ATTAINS.WaterType) + } else { + lookup <- lookup |> + dplyr::mutate( + ATTAINS.WaterType = dplyr::coalesce( + ATTAINS.WaterType, + TADA.ATTAINS.WaterType + ) + ) + } } else { lookup <- lookup |> - dplyr::mutate( - TADA.ATTAINS.WaterType = dplyr::coalesce( - ATTAINS.WaterType, - TADA.ATTAINS.WaterType - ) - ) |> - dplyr::select(TADA.MonitoringLocationIdentifier, TADA.ATTAINS.WaterType) + dplyr::mutate(ATTAINS.WaterType = TADA.ATTAINS.WaterType) } + # Return only what we need + lookup <- lookup |> + dplyr::select(TADA.MonitoringLocationIdentifier, ATTAINS.WaterType) + # Join back to original data - if (!"ATTAINS.WaterType" %in% names(.data)) { - .data <- .data |> - dplyr::left_join( - lookup, - by = dplyr::join_by(TADA.MonitoringLocationIdentifier), - relationship = "many-to-many" - ) |> - dplyr::rename(ATTAINS.WaterType = TADA.ATTAINS.WaterType) - } else if (isTRUE(replace_all)) { - .data <- .data |> - dplyr::left_join( - lookup |> dplyr::rename(New.ATTAINS.WaterType = TADA.ATTAINS.WaterType), - by = dplyr::join_by(TADA.MonitoringLocationIdentifier), - relationship = "many-to-many" - ) |> - dplyr::mutate(ATTAINS.WaterType = New.ATTAINS.WaterType) |> - dplyr::select(-New.ATTAINS.WaterType) - } else { - .data <- .data |> - dplyr::left_join( - lookup |> dplyr::rename(New.ATTAINS.WaterType = TADA.ATTAINS.WaterType), - by = dplyr::join_by(TADA.MonitoringLocationIdentifier), - relationship = "many-to-many" - ) |> - dplyr::mutate( - ATTAINS.WaterType = dplyr::coalesce( - ATTAINS.WaterType, - New.ATTAINS.WaterType - ) - ) |> - dplyr::select(-New.ATTAINS.WaterType) - } + .data <- .data |> + dplyr::select(-dplyr::any_of("ATTAINS.WaterType")) |> + dplyr::left_join( + lookup, + by = dplyr::join_by(TADA.MonitoringLocationIdentifier), + relationship = "many-to-many" + ) .data |> TADA_OrderCols() } @@ -4981,7 +5236,7 @@ TADA_ReviewATTAINSWaterTypes <- function( # Allowed ATTAINS water types attains.types <- quiet( - rExpertQuery::EQ_DomainValues("water_type", api_key = .setEQKey()) |> + rExpertQuery::EQ_DomainValues("water_type") |> dplyr::select(name) |> dplyr::distinct() |> dplyr::pull() diff --git a/R/GeospatialFunctions.R b/R/GeospatialFunctions.R index 8112a1786..59674976a 100644 --- a/R/GeospatialFunctions.R +++ b/R/GeospatialFunctions.R @@ -3588,3 +3588,161 @@ TADA_CreateAUMLCrosswalk <- function( # return final list of dfs based on user inputs return(final_list) } + +#' Create point geometry for ATTAINS Assessment Units +#' +#' Creates point or multipoint geometry for ATTAINS Assessment Units using +#' `ATTAINS.AssessmentUnitIdentifier` when available, otherwise +#' `TADA.MonitoringLocationIdentifier`. +#' +#' @param .data A data frame containing: +#' - `TADA.LongitudeMeasure` +#' - `TADA.LatitudeMeasure` +#' - `HorizontalCoordinateReferenceSystemDatumName` +#' +#' And at least one of: +#' - `ATTAINS.AssessmentUnitIdentifier` +#' - `TADA.MonitoringLocationIdentifier` +#' +#' @param target_crs Numeric. Target CRS EPSG code. Default is 4269. +#' @param download_geo Logical. If `TRUE`, write a shapefile to the user's +#' downloads folder. +#' @param auid_prefix Character or `NULL`. If provided and non-empty, this +#' prefix is applied only to newly created +#' `ATTAINS.AssessmentUnitIdentifier` values. +#' +#' @return An `sf` object with: +#' - `AU_ID` +#' - `geometry` +#' +#' If `download_geo = TRUE`, a shapefile is written to the downloads folder. +#' +#' @details +#' - Missing or blank `ATTAINS.AssessmentUnitIdentifier` values are replaced +#' with `TADA.MonitoringLocationIdentifier`. +#' - Single-location groups retain `POINT` geometry. +#' - Multi-location groups are written as `MULTIPOINT`. +#' +#' @seealso [TADA_CreatePointAUs()] +#' +#' @export +#' +#' @examples +#' \dontrun{ +#' # Example with all POINT geometry +#' df <- Data_TribalNations_Harmonized |> +#' dplyr::filter(OrganizationFormalName == "Blackfeet Nation (Montana)") +#' +#' result <- TADA_CreatePointAUGeometry(df) +#' +#' # Example with POINT and MULTIPOINT geometry +#' df <- data.frame( +#' ATTAINS.AssessmentUnitIdentifier = c("AU1", "AU1", "AU2"), +#' TADA.MonitoringLocationIdentifier = c("ML1", "ML1", "ML2"), +#' TADA.LongitudeMeasure = c(-90, -90, -91), +#' TADA.LatitudeMeasure = c(40, 41, 41), +#' HorizontalCoordinateReferenceSystemDatumName = c("NAD83", "NAD83", "NAD83"), +#' stringsAsFactors = FALSE) +#' +#' result <- TADA_CreatePointAUGeometry(df) +#' } +#' +TADA_CreatePointAUGeometry <- function( + .data, + target_crs = 4269, + download_geo = FALSE, + auid_prefix = NULL +) { + # Required geospatial cols + coord_req <- c( + "TADA.LongitudeMeasure", + "TADA.LatitudeMeasure", + "HorizontalCoordinateReferenceSystemDatumName" + ) + + # Required id cols + id_cols <- c( + "ATTAINS.AssessmentUnitIdentifier", + "TADA.MonitoringLocationIdentifier" + ) + + # Find missing coordinate cols + missing_coords <- setdiff(coord_req, names(.data)) + if (length(missing_coords) > 0) { + stop( + "TADA_CreatePointAUGeometry: Missing required coordinate column(s): ", + paste(missing_coords, collapse = ", ") + ) + } + + # Find missing id cols + if (!any(id_cols %in% names(.data))) { + stop( + "TADA_CreatePointAUGeometry: Input data must contain at least one of: ", + "ATTAINS.AssessmentUnitIdentifier or TADA.MonitoringLocationIdentifier" + ) + } + + # Fill in missing assessment unit ids if needed + .data <- fill_missing_assessment_unit_id(.data, auid_prefix = auid_prefix) + + # Set id col + id.col <- if ("ATTAINS.AssessmentUnitIdentifier" %in% names(.data)) { + "ATTAINS.AssessmentUnitIdentifier" + } else { + "TADA.MonitoringLocationIdentifier" + } + + # Set CRS target + crs_target <- sf::st_crs(target_crs) + + # Create geospatial df + sf_out <- .data |> + dplyr::select(rlang::sym(id.col), dplyr::all_of(coord_req)) |> + dplyr::distinct() |> + dplyr::filter( + !is.na(TADA.LongitudeMeasure), + !is.na(TADA.LatitudeMeasure) + ) |> + sf::st_as_sf( + coords = c("TADA.LongitudeMeasure", "TADA.LatitudeMeasure"), + crs = 4269, + remove = TRUE + ) |> + sf::st_transform(crs = target_crs) |> + dplyr::group_by(!!rlang::sym(id.col)) |> + dplyr::summarise( + geometry = { + coords <- sf::st_coordinates(geometry)[, 1:2, drop = FALSE] + if (dplyr::n() == 1) { + sf::st_sfc(geometry[[1]], crs = crs_target) + } else { + sf::st_sfc(sf::st_multipoint(coords), crs = crs_target) + } + }, + .groups = "drop" + ) |> + dplyr::rename(AU_ID = !!rlang::sym(id.col)) + + # Save as shp file if required + if (isTRUE(download_geo)) { + today <- format(Sys.Date(), "%m_%d_%Y") + file.name <- paste0("TADAPointAUGeometry_", today) + + if (!is.null(auid_prefix)) { + auid_prefix <- trimws(auid_prefix) + auid_prefix <- sub("[-_;:]+$", "", auid_prefix) + + if (nzchar(auid_prefix)) { + file.name <- paste0(auid_prefix, "_", file.name) + } + } + + point.path <- .get_downloads_path(file.name) + save_sf_as_zip(sf_out, point.path) + } + + return(sf_out) + + invisible(NULL) +} diff --git a/R/GeospatialUtilities.R b/R/GeospatialUtilities.R index 239fa6c8e..1839ae9aa 100644 --- a/R/GeospatialUtilities.R +++ b/R/GeospatialUtilities.R @@ -1923,3 +1923,101 @@ fetchWaterType <- function(au_list, api_key = NULL) { return(results) } + +#' Save an sf object as a shapefile +#' +#' @keywords internal +#' @noRd +save_sf_as_zip <- function(sf_out, zip_path) { + if (!inherits(sf_out, "sf")) { + stop("'sf_out' must be an sf object.") + } + + if (is.null(zip_path) || !nzchar(zip_path)) { + stop("'zip_path' must be a valid file path.") + } + + if (!grepl("\\.zip$", zip_path, ignore.case = TRUE)) { + zip_path <- paste0(zip_path, ".zip") + } + + out_dir <- dirname(zip_path) + if (!dir.exists(out_dir)) { + dir.create(out_dir, recursive = TRUE) + } + + temp_dir <- tempfile("shp_export_") + dir.create(temp_dir) + + layer_name <- tools::file_path_sans_ext(basename(zip_path)) + + # Write shapefile into the temp directory + sf::st_write( + sf_out, + dsn = temp_dir, + layer = layer_name, + driver = "ESRI Shapefile", + delete_dsn = TRUE, + quiet = TRUE + ) + + # List all files created for that layer + shp_files <- list.files( + temp_dir, + pattern = paste0("^", layer_name, "\\."), + full.names = FALSE + ) + + old_wd <- getwd() + on.exit(setwd(old_wd), add = TRUE) + setwd(temp_dir) + + utils::zip(zipfile = zip_path, files = shp_files) + + invisible(zip_path) +} + +#' Fill missing ATTAINS Assessment Unit Identifiers +#' +#' @keywords internal +#' @noRd +fill_missing_assessment_unit_id <- function(.data, auid_prefix = NULL) { + has_mloc <- "TADA.MonitoringLocationIdentifier" %in% names(.data) + has_auid <- "ATTAINS.AssessmentUnitIdentifier" %in% names(.data) + + if (!has_mloc && !has_auid) { + stop( + "At least one of 'TADA.MonitoringLocationIdentifier' or ", + "'ATTAINS.AssessmentUnitIdentifier' must be present." + ) + } + + # If target column doesn't exist, create it + if (!has_auid) { + .data$ATTAINS.AssessmentUnitIdentifier <- NA_character_ + } + + # If source column doesn't exist, we can still return .data unchanged + # unless you want to error when filling is impossible. + if (!has_mloc) { + return(.data) + } + + created_AUID <- is.na(.data$ATTAINS.AssessmentUnitIdentifier) | + trimws(as.character(.data$ATTAINS.AssessmentUnitIdentifier)) == "" + + if (any(created_AUID)) { + .data$ATTAINS.AssessmentUnitIdentifier[ + created_AUID + ] <- .data$TADA.MonitoringLocationIdentifier[created_AUID] + } + + if (!is.null(auid_prefix) && nzchar(auid_prefix)) { + .data$ATTAINS.AssessmentUnitIdentifier[created_AUID] <- paste0( + auid_prefix, + .data$ATTAINS.AssessmentUnitIdentifier[created_AUID] + ) + } + + .data +} diff --git a/R/Utilities.R b/R/Utilities.R index 00fc2a2f7..44ad55910 100644 --- a/R/Utilities.R +++ b/R/Utilities.R @@ -512,7 +512,8 @@ utils::globalVariables(c( "ATTAINSWaterTypeByOrgName", "TADA.ATTAINS.WaterType", "TADA.Rank", - "TADA.ResultValueAggregation.Flag" + "TADA.ResultValueAggregation.Flag", + "n_pts" )) # global variables for tribal feature layers used in TADA_OverviewMap in Utilities.R diff --git a/inst/WORDLIST b/inst/WORDLIST index 802744810..8e9347945 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -100,6 +100,7 @@ DurationValue EASP EASP's EMEF +EPSG EQ ExampleMod ExampleValues @@ -525,6 +526,7 @@ maxrecs mimeType mlid mlt +multipoint nd nhd nhdplus @@ -571,6 +573,7 @@ secchi setEQKey setnames showMissingATTAINSAUs +shp siteType siteid sitetype diff --git a/man/TADA_CreatePointAUGeometry.Rd b/man/TADA_CreatePointAUGeometry.Rd new file mode 100644 index 000000000..9cb2aac57 --- /dev/null +++ b/man/TADA_CreatePointAUGeometry.Rd @@ -0,0 +1,82 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/GeospatialFunctions.R +\name{TADA_CreatePointAUGeometry} +\alias{TADA_CreatePointAUGeometry} +\title{Create point geometry for ATTAINS Assessment Units} +\usage{ +TADA_CreatePointAUGeometry( + .data, + target_crs = 4269, + download_geo = FALSE, + auid_prefix = NULL +) +} +\arguments{ +\item{.data}{A data frame containing: +\itemize{ +\item \code{TADA.LongitudeMeasure} +\item \code{TADA.LatitudeMeasure} +\item \code{HorizontalCoordinateReferenceSystemDatumName} +} + +And at least one of: +\itemize{ +\item \code{ATTAINS.AssessmentUnitIdentifier} +\item \code{TADA.MonitoringLocationIdentifier} +}} + +\item{target_crs}{Numeric. Target CRS EPSG code. Default is 4269.} + +\item{download_geo}{Logical. If \code{TRUE}, write a shapefile to the user's +downloads folder.} + +\item{auid_prefix}{Character or \code{NULL}. If provided and non-empty, this +prefix is applied only to newly created +\code{ATTAINS.AssessmentUnitIdentifier} values.} +} +\value{ +An \code{sf} object with: +\itemize{ +\item \code{AU_ID} +\item \code{geometry} +} + +If \code{download_geo = TRUE}, a shapefile is written to the downloads folder. +} +\description{ +Creates point or multipoint geometry for ATTAINS Assessment Units using +\code{ATTAINS.AssessmentUnitIdentifier} when available, otherwise +\code{TADA.MonitoringLocationIdentifier}. +} +\details{ +\itemize{ +\item Missing or blank \code{ATTAINS.AssessmentUnitIdentifier} values are replaced +with \code{TADA.MonitoringLocationIdentifier}. +\item Single-location groups retain \code{POINT} geometry. +\item Multi-location groups are written as \code{MULTIPOINT}. +} +} +\examples{ +\dontrun{ +# Example with all POINT geometry +df <- Data_TribalNations_Harmonized |> +dplyr::filter(OrganizationFormalName == "Blackfeet Nation (Montana)") + +result <- TADA_CreatePointAUGeometry(df) + +# Example with POINT and MULTIPOINT geometry +df <- data.frame( +ATTAINS.AssessmentUnitIdentifier = c("AU1", "AU1", "AU2"), +TADA.MonitoringLocationIdentifier = c("ML1", "ML1", "ML2"), +TADA.LongitudeMeasure = c(-90, -90, -91), +TADA.LatitudeMeasure = c(40, 41, 41), + HorizontalCoordinateReferenceSystemDatumName = c("NAD83", "NAD83", "NAD83"), + stringsAsFactors = FALSE) + + result <- TADA_CreatePointAUGeometry(df) +} + +} +\seealso{ +\code{\link[=TADA_CreatePointAUs]{TADA_CreatePointAUs()}} +} diff --git a/man/TADA_CreatePointAUs.Rd b/man/TADA_CreatePointAUs.Rd index 835fbc616..a9bfef0a7 100644 --- a/man/TADA_CreatePointAUs.Rd +++ b/man/TADA_CreatePointAUs.Rd @@ -2,9 +2,14 @@ % Please edit documentation in R/ATTAINSCrosswalks.R \name{TADA_CreatePointAUs} \alias{TADA_CreatePointAUs} -\title{Create an ATTAINS AU–ML Crosswalk from WQP Monitoring Location IDs} +\title{Create an ATTAINS AU–ML Crosswalk from WQP Monitoring Location IDs for New Point AUs} \usage{ -TADA_CreatePointAUs(.data, auid_prefix = NULL) +TADA_CreatePointAUs( + .data, + auid_prefix = NULL, + create_geo = FALSE, + download_geo = FALSE +) } \arguments{ \item{.data}{A data frame containing, at minimum: @@ -12,87 +17,71 @@ TADA_CreatePointAUs(.data, auid_prefix = NULL) \item \code{TADA.MonitoringLocationIdentifier} } +If \code{create_geo = TRUE}, the input must also contain: +\itemize{ +\item \code{TADA.LatitudeMeasure} +\item \code{TADA.LongitudeMeasure} +\item \code{HorizontalCoordinateReferenceSystemDatumName} +} + If missing water-type values need to be crosswalked, the input must also contain: \itemize{ \item \code{TADA.MonitoringLocationTypeName} } -Optionally, the input may already include: +Optional columns: \itemize{ \item \code{ATTAINS.AssessmentUnitIdentifier} \item \code{ATTAINS.WaterType} -} - -If \code{ATTAINS.AssessmentUnitIdentifier} is absent, it will be added.} +}} \item{auid_prefix}{Character or \code{NULL}. If provided and non-empty, this -prefix is included only for newly created -\code{ATTAINS.AssessmentUnitIdentifier} values that were filled from -\code{TADA.MonitoringLocationIdentifier}. Existing non-missing AUIDs are not -modified. Use \code{NULL} to skip prefixing.} +prefix is added only to newly created +\code{ATTAINS.AssessmentUnitIdentifier} values.} + +\item{create_geo}{Logical. If \code{TRUE}, also create point geometry using +\code{TADA_CreatePointAUGeometry()}.} + +\item{download_geo}{Logical. If \code{TRUE}, write the geometry shapefile to the +user's downloads folder.} } \value{ -A distinct AU–ML crosswalk data frame containing: +If \code{create_geo = FALSE}, a distinct crosswalk data frame containing: \itemize{ \item \code{ATTAINS.MonitoringLocationIdentifier} \item \code{ATTAINS.AssessmentUnitIdentifier} \item \code{ATTAINS.WaterType} } + +If \code{create_geo = TRUE}, a named list with: +\itemize{ +\item \code{crosswalk} +\item \code{geometry} +} } \description{ Build a distinct crosswalk between WQP Monitoring Locations and ATTAINS -Assessment Units. For rows where \code{ATTAINS.AssessmentUnitIdentifier} is -missing or blank, the value is filled with -\code{TADA.MonitoringLocationIdentifier}, optionally with an \code{auid_prefix} -appended. Existing non-missing, non-blank AUIDs are left unchanged. +Assessment Units. Missing or blank \code{ATTAINS.AssessmentUnitIdentifier} values +are filled from \code{TADA.MonitoringLocationIdentifier}, optionally prefixed +with \code{auid_prefix}. Existing non-missing, non-blank AUIDs are left unchanged. +Optionally, this function can also return either a df or a shp file containing +the point or multipoint geometry and its corresponding assessment unit identifier. } \details{ If \code{ATTAINS.WaterType} is missing or contains any blank values, the function -will attempt to populate it by calling -\code{TADA_CrosswalkATTAINSWaterTypes()} internally with -\code{overwrite_existing = FALSE} and \code{validation = "none"}. +attempts to populate it by calling \code{TADA_UpdateATTAINSWaterTypes()} +internally with \code{overwrite_existing = FALSE} and \code{validation = "none"}. \itemize{ -\item Missing \code{ATTAINS.AssessmentUnitIdentifier} -values are replaced with \code{TADA.MonitoringLocationIdentifier}. -\item If \code{auid_prefix} is supplied and non-empty, it is included for only -newly created AUIDs. +\item Missing or blank \code{ATTAINS.AssessmentUnitIdentifier} values are replaced +with \code{TADA.MonitoringLocationIdentifier}. +\item If \code{auid_prefix} is supplied and non-empty, it is used only for newly +created AUIDs. \item \code{ATTAINS.MonitoringLocationIdentifier} is created from \code{TADA.MonitoringLocationIdentifier}. \item \code{ATTAINS.WaterType} is not overwritten unless it is missing or blank. } -} -\examples{ -\dontrun{ -# Example 1: Create missing AUIDs -ex_df <- data.frame( - TADA.MonitoringLocationIdentifier = c("LOC1", "LOC2", "LOC3"), - TADA.MonitoringLocationTypeName = c("Stream", "Lake", "Estuary"), - ATTAINS.AssessmentUnitIdentifier = c(NA_character_, "EXISTING_AU_001", ""), - ATTAINS.WaterType = c(NA_character_, "", "ESTUARY"), - stringsAsFactors = FALSE -) - -result <- TADA_CreatePointAUs(ex_df) - -# Example 2: Prefix only newly created AUIDs -result_prefixed <- TADA_CreatePointAUs( - ex_df, - auid_prefix = "WQX_" -) - -# Example 3: AUID column is absent entirely -ex_df2 <- data.frame( - TADA.MonitoringLocationIdentifier = c("SITE_A", "SITE_B"), - TADA.MonitoringLocationTypeName = c("River/Stream", "Lake, Reservoir, Impoundment"), - ATTAINS.WaterType = c(NA_character_, NA_character_), - stringsAsFactors = FALSE -) - -result_missing_auid <- TADA_CreatePointAUs(ex_df2) -} - } \seealso{ -\code{\link[=TADA_CrosswalkATTAINSWaterTypes]{TADA_CrosswalkATTAINSWaterTypes()}} +\code{\link[=TADA_UpdateATTAINSWaterTypes]{TADA_UpdateATTAINSWaterTypes()}}, \code{\link[=TADA_CreatePointAUGeometry]{TADA_CreatePointAUGeometry()}} } diff --git a/man/TADA_ReviewATTAINSWaterTypes.Rd b/man/TADA_ReviewATTAINSWaterTypes.Rd index 4385faa24..95b20a6f5 100644 --- a/man/TADA_ReviewATTAINSWaterTypes.Rd +++ b/man/TADA_ReviewATTAINSWaterTypes.Rd @@ -4,6 +4,8 @@ \alias{TADA_ReviewATTAINSWaterTypes} \title{Review ATTAINS Water Types} \usage{ +TADA_ReviewATTAINSWaterTypes(.data, review_action = c("flag", "update")) + TADA_ReviewATTAINSWaterTypes(.data, review_action = c("flag", "update")) } \arguments{ @@ -12,11 +14,18 @@ TADA_ReviewATTAINSWaterTypes(.data, review_action = c("flag", "update")) \item{review_action}{Character string. One of "flag" or "update".} } \value{ +A TADA data frame with \code{TADA.ATTAINSWaterType.Flag} added. +If \code{review_action = "update"}, invalid values may also be replaced if an +ATTAINS water type match is available. + A TADA data frame with \code{TADA.ATTAINSWaterType.Flag} added. If \code{review_action = "update"}, invalid values may also be replaced if an ATTAINS water type match is available. } \description{ +Validates ATTAINS.WaterType against allowable ATTAINS domain values. +Can either flag invalid values or update them using the crosswalk. + Validates ATTAINS.WaterType against allowable ATTAINS domain values. Can either flag invalid values or update them using the crosswalk. } @@ -31,6 +40,18 @@ TADA.MonitoringLocationTypeName = c("RIVER/STREAM", "LAKE"), ATTAINS.WaterType = c("INVALID WATER TYPE 1", "INVALID WATER TYPE 2") ) +review.df <- TADA_ReviewATTAINSWaterTypes(df, review_action = "update") +} + +\dontrun{ + +# example of updating invalid ATTAINS water types +example.df <- tibble::tibble( +TADA.MonitoringLocationIdentifier = c("id1", "id2"), +TADA.MonitoringLocationTypeName = c("RIVER/STREAM", "LAKE"), +ATTAINS.WaterType = c("INVALID WATER TYPE 1", "INVALID WATER TYPE 2") +) + review.df <- TADA_ReviewATTAINSWaterTypes(df, review_action = "update") } } diff --git a/man/TADA_CrosswalkATTAINSWaterTypes.Rd b/man/TADA_UpdateATTAINSWaterTypes.Rd similarity index 62% rename from man/TADA_CrosswalkATTAINSWaterTypes.Rd rename to man/TADA_UpdateATTAINSWaterTypes.Rd index 4db2ceccf..63c656802 100644 --- a/man/TADA_CrosswalkATTAINSWaterTypes.Rd +++ b/man/TADA_UpdateATTAINSWaterTypes.Rd @@ -1,10 +1,17 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/ATTAINSCrosswalks.R -\name{TADA_CrosswalkATTAINSWaterTypes} -\alias{TADA_CrosswalkATTAINSWaterTypes} +\name{TADA_UpdateATTAINSWaterTypes} +\alias{TADA_UpdateATTAINSWaterTypes} \title{Crosswalk WQP Monitoring Location Type to ATTAINS Water Type} \usage{ -TADA_CrosswalkATTAINSWaterTypes( +TADA_UpdateATTAINSWaterTypes( + .data, + org_id = NULL, + org_only = FALSE, + replace_all = FALSE +) + +TADA_UpdateATTAINSWaterTypes( .data, org_id = NULL, org_only = FALSE, @@ -24,9 +31,14 @@ If FALSE, unmatched types fall back to the TADA default crosswalk.} If FALSE, only fill missing values. Default is FALSE.} } \value{ +A TADA data frame with ATTAINS.WaterType populated. + A TADA data frame with ATTAINS.WaterType populated. } \description{ +Adds or updates ATTAINS.WaterType using TADA.MonitoringLocationTypeName. +By default, only missing ATTAINS.WaterType values are populated. + Adds or updates ATTAINS.WaterType using TADA.MonitoringLocationTypeName. By default, only missing ATTAINS.WaterType values are populated. } @@ -37,6 +49,14 @@ By default, only missing ATTAINS.WaterType values are populated. # example for MT data testdat <- Data_MT_MissoulaCounty -crosswalk <- TADA_CrosswalkATTAINSWaterTypes(testat, org_Id = "MTDEQ") +crosswalk <- TADA_UpdateATTAINSWaterTypes(testat, org_Id = "MTDEQ") +} + +\dontrun{ + +# example for MT data +testdat <- Data_MT_MissoulaCounty + +crosswalk <- TADA_UpdateATTAINSWaterTypes(testat, org_Id = "MTDEQ") } } diff --git a/tests/testthat/test-ATTAINSCrosswalks.R b/tests/testthat/test-ATTAINSCrosswalks.R index 3f8d42d9d..f54437e00 100644 --- a/tests/testthat/test-ATTAINSCrosswalks.R +++ b/tests/testthat/test-ATTAINSCrosswalks.R @@ -466,103 +466,135 @@ testthat::test_that("Excel file generation works with blank inputs in TADA_MLSum on.exit(if (file.exists(downloads_path)) file.remove(downloads_path)) }) -# Test TADA_CrosswalkATTAINSWaterTypes -test_that("TADA_CrosswalkATTAINSWaterTypes fills missing ATTAINS.WaterType", { +# test TADA_UpdateATTAINSWaterType +testthat::test_that("TADA_UpdateATTAINSWaterTypes errors when required columns are missing", { + df_missing_id <- data.frame( + TADA.MonitoringLocationTypeName = c("Stream", "Lake"), + stringsAsFactors = FALSE + ) + + testthat::expect_error( + TADA_UpdateATTAINSWaterTypes(df_missing_id), + "TADA_UpdateATTAINSWaterTypes: .data must contain TADA.MonitoringLocationIdentifier and TADA.MonitoringLocationTypeName." + ) + + df_missing_type <- data.frame( + TADA.MonitoringLocationIdentifier = c("A", "B"), + stringsAsFactors = FALSE + ) + + testthat::expect_error( + TADA_UpdateATTAINSWaterTypes(df_missing_type), + "TADA_UpdateATTAINSWaterTypes: .data must contain TADA.MonitoringLocationIdentifier and TADA.MonitoringLocationTypeName." + ) +}) + +testthat::test_that("TADA_UpdateATTAINSWaterTypes errors when overwrite_existing is not a single logical", { + df <- data.frame( + TADA.MonitoringLocationIdentifier = "A", + TADA.MonitoringLocationTypeName = "Stream", + stringsAsFactors = FALSE + ) + + testthat::expect_error( + TADA_UpdateATTAINSWaterTypes(df, replace_all = "yes"), + "TADA_UpdateATTAINSWaterTypes: replace_all must be a single non-NA logical." + ) + + testthat::expect_error( + TADA_UpdateATTAINSWaterTypes(df, replace_all = c(TRUE, FALSE)), + "TADA_UpdateATTAINSWaterTypes: replace_all must be a single non-NA logical." + ) +}) + +# Test TADA_UpdateATTAINSWaterType +testthat::test_that("TADA_UpdateATTAINSWaterTypes fills missing ATTAINS.WaterType", { df <- tibble::tibble( TADA.MonitoringLocationIdentifier = c("id1", "id2"), TADA.MonitoringLocationTypeName = c("RIVER/STREAM", "LAKE"), ATTAINS.WaterType = c(NA, "") ) - out <- TADA_CrosswalkATTAINSWaterTypes(df) + out <- TADA_UpdateATTAINSWaterTypes(df) - expect_s3_class(out, "data.frame") - expect_true("ATTAINS.WaterType" %in% names(out)) - expect_false(any(is.na(out$ATTAINS.WaterType))) + testthat::expect_s3_class(out, "data.frame") + testthat::expect_true("ATTAINS.WaterType" %in% names(out)) + testthat::expect_false(any(is.na(out$ATTAINS.WaterType))) }) -test_that("TADA_CrosswalkATTAINSWaterTypes does not overwrite existing values when replace_all = FALSE", { +testthat::test_that("TADA_UpdateATTAINSWaterTypes does not overwrite existing values when replace_all = FALSE", { df <- tibble::tibble( TADA.MonitoringLocationIdentifier = c("id1", "id2"), TADA.MonitoringLocationTypeName = c("RIVER/STREAM", "LAKE"), ATTAINS.WaterType = c("CUSTOM TYPE", NA) ) - out <- TADA_CrosswalkATTAINSWaterTypes(df, replace_all = FALSE) + out <- TADA_UpdateATTAINSWaterTypes(df, replace_all = FALSE) - expect_equal(out$ATTAINS.WaterType[1], "CUSTOM TYPE") + testthat::expect_equal(out$ATTAINS.WaterType[1], "CUSTOM TYPE") }) -test_that("TADA_CrosswalkATTAINSWaterTypes overwrites all values when replace_all = TRUE", { +testthat::test_that("TADA_UpdateATTAINSWaterTypes overwrites all values when replace_all = TRUE", { df <- tibble::tibble( TADA.MonitoringLocationIdentifier = c("id1", "id2"), TADA.MonitoringLocationTypeName = c("RIVER/STREAM", "LAKE"), ATTAINS.WaterType = c("CUSTOM TYPE", "ANOTHER TYPE") ) - out <- TADA_CrosswalkATTAINSWaterTypes(df, replace_all = TRUE) + out <- TADA_UpdateATTAINSWaterTypes(df, replace_all = TRUE) - expect_false(any(out$ATTAINS.WaterType %in% c("CUSTOM TYPE", "ANOTHER TYPE"))) + testthat::expect_false(any( + out$ATTAINS.WaterType %in% c("CUSTOM TYPE", "ANOTHER TYPE") + )) }) -test_that("TADA_CrosswalkATTAINSWaterTypes creates ATTAINS.WaterType when missing", { +testthat::test_that("TADA_UpdateATTAINSWaterTypes creates ATTAINS.WaterType when missing", { df <- tibble::tibble( TADA.MonitoringLocationIdentifier = c("id1", "id2"), TADA.MonitoringLocationTypeName = c("RIVER", "LAKE") ) - out <- TADA_CrosswalkATTAINSWaterTypes(df) + out <- TADA_UpdateATTAINSWaterTypes(df) - expect_true("ATTAINS.WaterType" %in% names(out)) + testthat::expect_true("ATTAINS.WaterType" %in% names(out)) }) -test_that("TADA_CrosswalkATTAINSWaterTypes errors when required columns are missing", { +testthat::test_that("TADA_UpdateATTAINSWaterTypes errors when required columns are missing", { df <- tibble::tibble(TADA.MonitoringLocationIdentifier = c("id1", "id2")) - expect_error( - TADA_CrosswalkATTAINSWaterTypes(df), + testthat::expect_error( + TADA_UpdateATTAINSWaterTypes(df), "must contain TADA.MonitoringLocationIdentifier and TADA.MonitoringLocationTypeName" ) }) -test_that("TADA_CrosswalkATTAINSWaterTypes errors on invalid org_id", { +testthat::test_that("TADA_UpdateATTAINSWaterTypes errors on invalid org_id", { df <- tibble::tibble( TADA.MonitoringLocationIdentifier = "id1", TADA.MonitoringLocationTypeName = "RIVER" ) - expect_error( - TADA_CrosswalkATTAINSWaterTypes(df, org_id = 1), + testthat::expect_error( + TADA_UpdateATTAINSWaterTypes(df, org_id = 1), "org_id must be NULL or a single non-NA character string" ) }) -test_that("TADA_CrosswalkATTAINSWaterTypes errors on invalid replace_all", { - df <- tibble::tibble( - TADA.MonitoringLocationIdentifier = "id1", - TADA.MonitoringLocationTypeName = "RIVER" - ) - - expect_error( - TADA_CrosswalkATTAINSWaterTypes(df, replace_all = "yes"), - "replace_all must be a single non-NA logical" - ) -}) # Test TADA_ReviewATTAINSWaterTypes -test_that("TADA_ReviewATTAINSWaterTypes errors when required columns are missing", { +testthat::test_that("TADA_ReviewATTAINSWaterTypes errors when required columns are missing", { df <- tibble::tibble( TADA.MonitoringLocationIdentifier = "id1", TADA.MonitoringLocationTypeName = "RIVER" ) - expect_error( + testthat::expect_error( TADA_ReviewATTAINSWaterTypes(df), "must contain TADA.MonitoringLocationIdentifier, TADA.MonitoringLocationTypeName, and ATTAINS.WaterType" ) }) -test_that("TADA_ReviewATTAINSWaterTypes creates a flag column for invalid values", { +testthat::test_that("TADA_ReviewATTAINSWaterTypes creates a flag column for invalid values", { skip_if_not_installed("rExpertQuery") df <- tibble::tibble( @@ -573,14 +605,14 @@ test_that("TADA_ReviewATTAINSWaterTypes creates a flag column for invalid values out <- TADA_ReviewATTAINSWaterTypes(df, review_action = "flag") - expect_true("TADA.ATTAINSWaterType.Flag" %in% names(out)) - expect_true(any(grepl( + testthat::expect_true("TADA.ATTAINSWaterType.Flag" %in% names(out)) + testthat::expect_true(any(grepl( "does not match any allowable", out$TADA.ATTAINSWaterType.Flag ))) }) -test_that("TADA_ReviewATTAINSWaterTypes leaves valid values flagged as valid", { +testthat::test_that("TADA_ReviewATTAINSWaterTypes leaves valid values flagged as valid", { skip_if_not_installed("rExpertQuery") # Use values that are very likely valid in ATTAINS, but if your environment @@ -593,14 +625,14 @@ test_that("TADA_ReviewATTAINSWaterTypes leaves valid values flagged as valid", { out <- TADA_ReviewATTAINSWaterTypes(df, review_action = "flag") - expect_true("TADA.ATTAINSWaterType.Flag" %in% names(out)) - expect_true( + testthat::expect_true("TADA.ATTAINSWaterType.Flag" %in% names(out)) + testthat::expect_true( any(grepl("matches an allowable", out$TADA.ATTAINSWaterType.Flag)) || any(is.na(out$TADA.ATTAINSWaterType.Flag)) ) }) -test_that("TADA_ReviewATTAINSWaterTypes updates invalid values when review_action = 'update'", { +testthat::test_that("TADA_ReviewATTAINSWaterTypes updates invalid values when review_action = 'update'", { skip_if_not_installed("rExpertQuery") df <- tibble::tibble( @@ -611,27 +643,91 @@ test_that("TADA_ReviewATTAINSWaterTypes updates invalid values when review_actio out <- TADA_ReviewATTAINSWaterTypes(df, review_action = "update") - expect_true("TADA.ATTAINSWaterType.Flag" %in% names(out)) - expect_false(any( - out$ATTAINS.WaterType %in% c("INVALID WATER TYPE 1", "INVALID WATER TYPE 2") + testthat::expect_true(all( + out$ATTAINS.WaterType %in% c("STREAM/CREEK/RIVER", "LAKE") + )) + testthat::expect_true(all( + out$TADA.ATTAINSWaterType.Flag == + "ATTAINS.WaterType was updated to match an allowable ATTAINS.WaterType value by crosswalking TADA.MonitoringLocationTypeName." )) }) -# Test TADA_CreatePointAUs +testthat::test_that("TADA_UpdateWaterTypes: duplicate monitoring location rows do not break output", { + df <- data.frame( + TADA.MonitoringLocationIdentifier = c("id1", "id1"), + TADA.MonitoringLocationTypeName = c("STREAM", "STREAM"), + stringsAsFactors = FALSE + ) + + out <- TADA_UpdateATTAINSWaterTypes(df, replace_all = TRUE) + + testthat::expect_equal(nrow(out), 2) + testthat::expect_true(all(out$ATTAINS.WaterType == "STREAM")) +}) + -test_that("TADA_CreatePointAUs errors when TADA.MonitoringLocationIdentifier is missing", { +testthat::test_that("TADA_UpdateATTAINSWaterTypes: existing ATTAINS.OrganizationIdentifier is preserved", { + df <- data.frame( + TADA.MonitoringLocationIdentifier = "id1", + TADA.MonitoringLocationTypeName = "STREAM", + ATTAINS.OrganizationIdentifier = "ORG123", + stringsAsFactors = FALSE + ) + + crosswalk_df <- data.frame( + Name = "STREAM", + ATTAINS.WaterType = "RIVER/CREEK/STREAM", + stringsAsFactors = FALSE + ) + + local_mocked_bindings( + read.csv = function(...) crosswalk_df, + .package = "utils" + ) + + out <- TADA_UpdateATTAINSWaterTypes(df) + + testthat::expect_equal(out$ATTAINS.OrganizationIdentifier, "ORG123") +}) + +testthat::test_that("TADA_UpdateATTAINSWaterTypes: replace_all = FALSE preserves nonblank existing ATTAINS.WaterType values", { + df <- data.frame( + TADA.MonitoringLocationIdentifier = "id1", + TADA.MonitoringLocationTypeName = "STREAM", + ATTAINS.WaterType = "KeepMe", + stringsAsFactors = FALSE + ) + + crosswalk_df <- data.frame( + Name = "STREAM", + ATTAINS.WaterType = "STREAM/CREEK/RIVER", + stringsAsFactors = FALSE + ) + + local_mocked_bindings( + read.csv = function(...) crosswalk_df, + .package = "utils" + ) + + out <- TADA_UpdateATTAINSWaterTypes(df, replace_all = FALSE) + + expect_equal(out$ATTAINS.WaterType, "KeepMe") +}) + +# Test TADA_CreatePointAUs +testthat::test_that("TADA_CreatePointAUs errors when TADA.MonitoringLocationIdentifier is missing", { df <- data.frame( TADA.MonitoringLocationTypeName = c("Stream", "Lake"), stringsAsFactors = FALSE ) - expect_error( + testthat::expect_error( TADA_CreatePointAUs(df), "Missing required column\\(s\\): TADA\\.MonitoringLocationIdentifier" ) }) -test_that("TADA_CreatePointAUs adds missing ATTAINS.AssessmentUnitIdentifier and fills blanks/NA without prefix", { +testthat::test_that("TADA_CreatePointAUs adds missing ATTAINS.AssessmentUnitIdentifier and fills blanks/NA without prefix", { df <- data.frame( TADA.MonitoringLocationIdentifier = c("LOC1", "LOC2", "LOC3"), TADA.MonitoringLocationTypeName = c("Stream", "Lake", "Estuary"), @@ -642,7 +738,7 @@ test_that("TADA_CreatePointAUs adds missing ATTAINS.AssessmentUnitIdentifier and result <- TADA_CreatePointAUs(df) - expect_s3_class(result, "data.frame") + testthat::expect_s3_class(result, "data.frame") expect_equal( names(result), c( @@ -652,18 +748,21 @@ test_that("TADA_CreatePointAUs adds missing ATTAINS.AssessmentUnitIdentifier and ) ) - expect_equal( + testthat::expect_equal( result$ATTAINS.MonitoringLocationIdentifier, c("LOC1", "LOC2", "LOC3") ) - expect_equal( + testthat::expect_equal( result$ATTAINS.AssessmentUnitIdentifier, c("LOC1", "EXISTING_AU_001", "LOC3") ) - expect_equal(result$ATTAINS.WaterType, c("STREAM", "LAKE", "ESTUARY")) + testthat::expect_equal( + result$ATTAINS.WaterType, + c("STREAM", "LAKE", "ESTUARY") + ) }) -test_that("TADA_CreatePointAUs applies auid_prefix only to newly created AUIDs", { +testthat::test_that("TADA_CreatePointAUs applies auid_prefix only to newly created AUIDs", { df <- data.frame( TADA.MonitoringLocationIdentifier = c("LOC1", "LOC2", "LOC3"), TADA.MonitoringLocationTypeName = c("Stream", "Lake", "Estuary"), @@ -674,13 +773,13 @@ test_that("TADA_CreatePointAUs applies auid_prefix only to newly created AUIDs", result <- TADA_CreatePointAUs(df, auid_prefix = "WQX_") - expect_equal( + testthat::expect_equal( result$ATTAINS.AssessmentUnitIdentifier, c("WQX_LOC1", "EXISTING_AU_001", "WQX_LOC3") ) }) -test_that("TADA_CreatePointAUs treats blank AUIDs as missing", { +testthat::test_that("TADA_CreatePointAUs treats blank AUIDs as missing", { df <- data.frame( TADA.MonitoringLocationIdentifier = c("LOC1", "LOC2"), TADA.MonitoringLocationTypeName = c("Stream", "Lake"), @@ -691,59 +790,44 @@ test_that("TADA_CreatePointAUs treats blank AUIDs as missing", { result <- TADA_CreatePointAUs(df) - expect_equal(result$ATTAINS.AssessmentUnitIdentifier, c("LOC1", "LOC2")) + testthat::expect_equal( + result$ATTAINS.AssessmentUnitIdentifier, + c("LOC1", "LOC2") + ) }) -test_that("TADA_CreatePointAUs calls TADA_CrosswalkATTAINSWaterTypes when ATTAINS.WaterType is missing", { +testthat::test_that("TADA_CreatePointAUs calls TADA_CrosswalkATTAINSWaterTypes when ATTAINS.WaterType is missing", { df <- data.frame( TADA.MonitoringLocationIdentifier = c("LOC1", "LOC2"), - TADA.MonitoringLocationTypeName = c("Stream", "Lake"), + TADA.MonitoringLocationTypeName = c("STREAM", "LAKE"), ATTAINS.AssessmentUnitIdentifier = c(NA_character_, NA_character_), stringsAsFactors = FALSE ) - mock_crosswalk <- function(.data, replace_all = FALSE) { - expect_false(replace_all) - .data$ATTAINS.WaterType <- c("STREAM", "LAKE") - .data - } - - testthat::local_mocked_bindings( - TADA_CrosswalkATTAINSWaterTypes = mock_crosswalk, - .env = environment(TADA_CreatePointAUs) - ) - result <- TADA_CreatePointAUs(df) - expect_equal(result$ATTAINS.WaterType, c("STREAM", "LAKE")) - expect_equal(result$ATTAINS.AssessmentUnitIdentifier, c("LOC1", "LOC2")) + testthat::expect_equal(result$ATTAINS.WaterType, c("STREAM", "LAKE")) + testthat::expect_equal( + result$ATTAINS.AssessmentUnitIdentifier, + c("LOC1", "LOC2") + ) }) -test_that("TADA_CreatePointAUs calls TADA_CrosswalkATTAINSWaterTypes when ATTAINS.WaterType has blanks", { +testthat::test_that("TADA_CreatePointAUs calls TADA_CrosswalkATTAINSWaterTypes when ATTAINS.WaterType has blanks", { df <- data.frame( TADA.MonitoringLocationIdentifier = c("LOC1", "LOC2"), - TADA.MonitoringLocationTypeName = c("Stream", "Lake"), + TADA.MonitoringLocationTypeName = c("STREAM", "LAKE"), ATTAINS.AssessmentUnitIdentifier = c(NA_character_, NA_character_), ATTAINS.WaterType = c("STREAM", ""), stringsAsFactors = FALSE ) - mock_crosswalk <- function(.data, replace_all = FALSE) { - .data$ATTAINS.WaterType <- c("STREAM", "LAKE") - .data - } - - testthat::local_mocked_bindings( - TADA_CrosswalkATTAINSWaterTypes = mock_crosswalk, - .env = environment(TADA_CreatePointAUs) - ) - result <- TADA_CreatePointAUs(df) - expect_equal(result$ATTAINS.WaterType, c("STREAM", "LAKE")) + testthat::expect_equal(result$ATTAINS.WaterType, c("STREAM", "LAKE")) }) -test_that("TADA_CreatePointAUs errors when water crosswalk is needed but TADA.MonitoringLocationTypeName is missing", { +testthat::test_that("TADA_CreatePointAUs errors when water crosswalk is needed but TADA.MonitoringLocationTypeName is missing", { df <- data.frame( TADA.MonitoringLocationIdentifier = c("LOC1", "LOC2"), ATTAINS.AssessmentUnitIdentifier = c(NA_character_, NA_character_), @@ -751,13 +835,13 @@ test_that("TADA_CreatePointAUs errors when water crosswalk is needed but TADA.Mo stringsAsFactors = FALSE ) - expect_error( + testthat::expect_error( TADA_CreatePointAUs(df), - "Missing required column: TADA\\.MonitoringLocationTypeName" + "TADA_CreatePointAUs: Missing required column for water-type crosswalk: TADA.MonitoringLocationTypeName" ) }) -test_that("TADA_CreatePointAUs returns distinct rows", { +testthat::test_that("TADA_CreatePointAUs returns distinct rows", { df <- data.frame( TADA.MonitoringLocationIdentifier = c("LOC1", "LOC1"), TADA.MonitoringLocationTypeName = c("Stream", "Stream"), @@ -768,13 +852,13 @@ test_that("TADA_CreatePointAUs returns distinct rows", { result <- TADA_CreatePointAUs(df) - expect_equal(nrow(result), 1) - expect_equal(result$ATTAINS.MonitoringLocationIdentifier, "LOC1") - expect_equal(result$ATTAINS.AssessmentUnitIdentifier, "LOC1") - expect_equal(result$ATTAINS.WaterType, "STREAM") + testthat::expect_equal(nrow(result), 1) + testthat::expect_equal(result$ATTAINS.MonitoringLocationIdentifier, "LOC1") + testthat::expect_equal(result$ATTAINS.AssessmentUnitIdentifier, "LOC1") + testthat::expect_equal(result$ATTAINS.WaterType, "STREAM") }) -test_that("TADA_CreatePointAUs does not modify existing non-missing, non-blank AUIDs when prefix is supplied", { +testthat::test_that("TADA_CreatePointAUs does not modify existing non-missing, non-blank AUIDs when prefix is supplied", { df <- data.frame( TADA.MonitoringLocationIdentifier = c("LOC1", "LOC2"), TADA.MonitoringLocationTypeName = c("Stream", "Lake"), @@ -785,8 +869,19 @@ test_that("TADA_CreatePointAUs does not modify existing non-missing, non-blank A result <- TADA_CreatePointAUs(df, auid_prefix = "WQX_") - expect_equal( + testthat::expect_equal( result$ATTAINS.AssessmentUnitIdentifier, c("EXISTING_AU_001", "WQX_LOC2") ) }) + +testthat::test_that("TADA_CreatePointAUs returns list containing crosswalk and geometry when create_geo equals TRUE", { + df <- Data_TribalNations_Harmonized |> + dplyr::filter(OrganizationFormalName == "Blackfeet Nation (Montana)") + + result <- TADA_CreatePointAUs(df, create_geo = TRUE) + + testthat::expect_type(result, "list") + testthat::expect_true(all(c("crosswalk", "geometry") %in% names(result))) + testthat::expect_s3_class(result$geometry, "sf") +}) diff --git a/tests/testthat/test-GeospatialFunctions.R b/tests/testthat/test-GeospatialFunctions.R index e38f262c9..463c64bb5 100644 --- a/tests/testthat/test-GeospatialFunctions.R +++ b/tests/testthat/test-GeospatialFunctions.R @@ -583,3 +583,123 @@ testthat::test_that("TADA_FindNearbySites does not combine known sites from diff fixed = TRUE ))) }) + +# tests for TADA_CreatePointAUGeometry +testthat::test_that("TADA_CreatePointAUGeometry errors when required coordinate columns are missing", { + base_df <- data.frame( + ATTAINS.AssessmentUnitIdentifier = "AU1", + TADA.MonitoringLocationIdentifier = "ML1", + TADA.LongitudeMeasure = -90, + TADA.LatitudeMeasure = 40, + HorizontalCoordinateReferenceSystemDatumName = "NAD83", + stringsAsFactors = FALSE + ) + + testthat::expect_error( + TADA_CreatePointAUGeometry(dplyr::select(base_df, -TADA.LongitudeMeasure)), + "TADA_CreatePointAUGeometry: Missing required coordinate column\\(s\\): TADA.LongitudeMeasure" + ) + + testthat::expect_error( + TADA_CreatePointAUGeometry(dplyr::select(base_df, -TADA.LatitudeMeasure)), + "TADA_CreatePointAUGeometry: Missing required coordinate column\\(s\\): TADA.LatitudeMeasure" + ) + + testthat::expect_error( + TADA_CreatePointAUGeometry(dplyr::select( + base_df, + -HorizontalCoordinateReferenceSystemDatumName + )), + "TADA_CreatePointAUGeometry: Missing required coordinate column\\(s\\): HorizontalCoordinateReferenceSystemDatumName" + ) +}) + +testthat::test_that("TADA_CreatePointAUGeometry: errors when neither ID column is present", { + df <- data.frame( + TADA.LongitudeMeasure = -90, + TADA.LatitudeMeasure = 40, + HorizontalCoordinateReferenceSystemDatumName = "NAD83", + stringsAsFactors = FALSE + ) + + testthat::expect_error( + TADA_CreatePointAUGeometry(df), + "TADA_CreatePointAUGeometry: Input data must contain at least one of: ATTAINS.AssessmentUnitIdentifier or TADA.MonitoringLocationIdentifier" + ) +}) + +testthat::test_that("TADA_CreateAUPointGeometry returns sf geometry for valid input", { + df <- data.frame( + ATTAINS.AssessmentUnitIdentifier = c("AU1", "AU1"), + TADA.MonitoringLocationIdentifier = c("ML1", "ML1"), + TADA.LongitudeMeasure = c(-90, -90.1), + TADA.LatitudeMeasure = c(40, 40.1), + HorizontalCoordinateReferenceSystemDatumName = c("NAD83", "NAD83"), + stringsAsFactors = FALSE + ) + + result <- TADA_CreatePointAUGeometry(df) + + testthat::expect_s3_class(result, "sf") + testthat::expect_true("geometry" %in% names(result)) + testthat::expect_true("AU_ID" %in% names(result)) +}) + +testthat::test_that("TADA_CreatePointAUGeometry creates POINT for one location and MULTIPOINT for multiple locations", { + df <- data.frame( + ATTAINS.AssessmentUnitIdentifier = c("AU1", "AU1", "AU2"), + TADA.MonitoringLocationIdentifier = c("ML1", "ML1", "ML2"), + TADA.LongitudeMeasure = c(-90, -90, -91), + TADA.LatitudeMeasure = c(40, 41, 41), + HorizontalCoordinateReferenceSystemDatumName = c("NAD83", "NAD83", "NAD83"), + stringsAsFactors = FALSE + ) + + result <- TADA_CreatePointAUGeometry(df) + + geom_types <- sf::st_geometry_type(result) + testthat::expect_true(any(geom_types %in% c("POINT", "MULTIPOINT"))) +}) + +testthat::test_that("TADA_CreatePointAUGeometry drops rows with missing coordinates", { + df <- data.frame( + ATTAINS.AssessmentUnitIdentifier = c("AU1", "AU2"), + TADA.MonitoringLocationIdentifier = c("ML1", "ML2"), + TADA.LongitudeMeasure = c(-90, NA), + TADA.LatitudeMeasure = c(40, 41), + HorizontalCoordinateReferenceSystemDatumName = c("NAD83", "NAD83"), + stringsAsFactors = FALSE + ) + + result <- TADA_CreatePointAUGeometry(df) + + testthat::expect_equal(nrow(result), 1) +}) + +testthat::test_that("TADA_CreatePointAUGeometry accepts auid_prefix", { + df <- data.frame( + TADA.MonitoringLocationIdentifier = "ML1", + TADA.LongitudeMeasure = -90, + TADA.LatitudeMeasure = 40, + HorizontalCoordinateReferenceSystemDatumName = "NAD83", + stringsAsFactors = FALSE + ) + + result <- TADA_CreatePointAUGeometry(df, auid_prefix = "TEST") + + testthat::expect_s3_class(result, "sf") +}) + +testthat::test_that("TADA_CreatePointAUGeometry works with only TADA.MonitoringLocationIdentifier present", { + df <- data.frame( + TADA.MonitoringLocationIdentifier = "ML1", + TADA.LongitudeMeasure = -90, + TADA.LatitudeMeasure = 40, + HorizontalCoordinateReferenceSystemDatumName = "NAD83", + stringsAsFactors = FALSE + ) + + result <- TADA_CreatePointAUGeometry(df) + + testthat::expect_s3_class(result, "sf") +})