From b487cc983f723b8c92241c2e0f5234b025cee3e0 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 24 Feb 2026 12:16:44 +0100 Subject: [PATCH] Refactor translation function and update reports to use new area unit preference - Renamed translation function from `t` to `tr_key` for clarity and consistency. - Updated all instances of translation calls in `90_CI_report_with_kpis_agronomic_support.Rmd` and `91_CI_report_with_kpis_cane_supply.Rmd` to use `tr_key`. - Introduced a new helper function `get_area_unit_label` to manage area unit preferences across the project. - Modified area calculations in `91_CI_report_with_kpis_cane_supply.Rmd` to utilize area from analysis data instead of recalculating. - Added area unit preference setting in `parameters_project.R` to allow for flexible reporting in either hectares or acres. - Updated `MANUAL_PIPELINE_RUNNER.R` to include language parameter for report generation. - Adjusted translations in the `translations.xlsx` file to reflect changes in the report structure. --- IMPLEMENTATION_GUIDE.md | 106 ++++ r_app/80_utils_agronomic_support.R | 48 +- r_app/80_utils_cane_supply.R | 77 +-- r_app/80_utils_common.R | 151 +++++- ..._CI_report_with_kpis_agronomic_support.Rmd | 464 ++++++++++-------- r_app/91_CI_report_with_kpis_cane_supply.Rmd | 46 +- r_app/MANUAL_PIPELINE_RUNNER.R | 11 +- r_app/parameters_project.R | 28 ++ r_app/translations/translations.xlsx | Bin 39456 -> 44729 bytes 9 files changed, 649 insertions(+), 282 deletions(-) diff --git a/IMPLEMENTATION_GUIDE.md b/IMPLEMENTATION_GUIDE.md index 3111208..f07a9a8 100644 --- a/IMPLEMENTATION_GUIDE.md +++ b/IMPLEMENTATION_GUIDE.md @@ -871,6 +871,112 @@ python python_app/23_convert_harvest_format.py angata --- +### OPTIONAL 5: Area Unit Selection UI (Hectares vs Acres) + +**Why**: Allow projects to choose their preferred area unit (hectares or acres) in reports and dashboards + +**Status**: R/Python business logic complete. Database schema + UI implementation pending. + +**What's already done** (in this codebase): +- ✅ Unified area calculation function in `80_utils_common.R`: `calculate_area_from_geometry()` +- ✅ Area unit preference in `parameters_project.R`: `AREA_UNIT_PREFERENCE` (default: "hectare") +- ✅ Helper function: `get_area_unit_label()` for dynamic "ha" or "ac" display +- ✅ Refactored scripts 80/90/91 to use unified function and support user's area preference +- ✅ Area now included in KPI outputs (CSV/RDS/Excel) from script 80 +- ✅ Scripts 90/91 read area from KPI files instead of recalculating + +**What needs implementation (for your Laravel colleague)**: + +**Step 1: Create database migration** + +```php +enum('preferred_area_unit', ['hectare', 'acre']) + ->default('hectare') + ->after('client_type'); + }); + } + + public function down(): void + { + Schema::table('projects', function (Blueprint $table) { + $table->dropColumn('preferred_area_unit'); + }); + } +}; +``` + +**Step 2: Update Project model** (`laravel_app/app/Models/Project.php`) + +```php +protected $fillable = [ + // ... existing fields ... + 'preferred_area_unit', // ADD THIS +]; +``` + +**Step 3: Add form UI** (`laravel_app/app/Livewire/Projects/ProjectManager.php` or Blade template) + +```blade +
+ +
+ + + + + +
+ @error('formData.preferred_area_unit') + {{ $message }} + @enderror +
+``` + +**Step 4: Pass preference to R scripts** (in job/shell wrapper) + +When launching R scripts, read `project->preferred_area_unit` and either: +- **Option A**: Write to `parameters_project.R` dynamically before script execution +- **Option B**: Pass as environment variable to scripts (R scripts read `Sys.getenv("AREA_UNIT")`) + +Example (PowerShell wrapper): +```powershell +$areaUnit = $project->preferred_area_unit # From database +$env:AREA_UNIT = $areaUnit # Set environment variable +& "C:\Program Files\R\R-4.4.3\bin\x64\Rscript.exe" r_app/80_calculate_kpis.R $project +``` + +**Testing checklist**: +- [ ] Database migration runs successfully +- [ ] Project form shows area unit radio buttons/dropdown +- [ ] Can select and save area unit preference +- [ ] Area unit persists in database +- [ ] Run script 80 with one project set to "hectare", observe KPI output +- [ ] Run script 80 with another project set to "acre", compare outputs +- [ ] Reports (scripts 90/91) display area in user's chosen unit + +**Notes**: +- Default preference: "hectare" (metric standard) +- Conversion factor used: 0.404686 (1 hectare = 0.404686 acres) +- All area calculations use EPSG:6933 (equal-area projection) for accuracy +- Area column in KPI outputs named dynamically: "Area_ha" or "Area_ac" + +--- + ## 📊 Summary: What Gets Changed | Category | Files Modified | Changes Required | diff --git a/r_app/80_utils_agronomic_support.R b/r_app/80_utils_agronomic_support.R index f93435d..04f0e9a 100644 --- a/r_app/80_utils_agronomic_support.R +++ b/r_app/80_utils_agronomic_support.R @@ -352,6 +352,14 @@ calculate_tch_forecasted_kpi <- function(field_statistics, harvesting_data = NUL #' @param ci_values_list List of CI values for each field (multiple weeks) #' #' @return Data frame with field-level decline indicators +#' @details +#' Uses FOUR_WEEK_TREND_* thresholds defined in 80_utils_common.R: +#' - FOUR_WEEK_TREND_STRONG_GROWTH_MIN (0.3) +#' - FOUR_WEEK_TREND_GROWTH_MIN (0.1) +#' - FOUR_WEEK_TREND_STABLE_THRESHOLD (0.1 and -0.1) +#' - FOUR_WEEK_TREND_WEAK_DECLINE_THRESHOLD (-0.3) +#' - FOUR_WEEK_TREND_STRONG_DECLINE_MAX (-0.3) +#' calculate_growth_decline_kpi <- function(ci_values_list) { result <- data.frame( field_idx = seq_len(length(ci_values_list)), @@ -382,23 +390,18 @@ calculate_growth_decline_kpi <- function(ci_values_list) { result$four_week_trend[field_idx] <- round(as.numeric(slope), 3) - # Categorize trend using consistent thresholds (note: must use global constants if available) - # Category ranges: - # slope >= 0.5: Strong growth (↑↑) - # 0.1 <= slope < 0.5: Weak growth (↑) - # -0.1 <= slope < 0.1: Stable (→) - # -0.3 < slope < -0.1: Weak decline (↓) - # slope <= -0.3: Strong decline (↓↓) - if (slope >= 0.5) { + # Categorize trend using shared constants from 80_utils_common.R + + if (slope >= FOUR_WEEK_TREND_STRONG_GROWTH_MIN) { result$trend_interpretation[field_idx] <- "Strong growth" result$decline_severity[field_idx] <- "None" - } else if (slope >= 0.1) { + } else if (slope >= FOUR_WEEK_TREND_GROWTH_MIN) { result$trend_interpretation[field_idx] <- "Weak growth" result$decline_severity[field_idx] <- "None" - } else if (slope >= -0.1) { + } else if (slope >= -FOUR_WEEK_TREND_STABLE_THRESHOLD) { result$trend_interpretation[field_idx] <- "Stable" result$decline_severity[field_idx] <- "None" - } else if (slope > -0.3) { + } else if (slope > FOUR_WEEK_TREND_WEAK_DECLINE_THRESHOLD) { result$trend_interpretation[field_idx] <- "Weak decline" result$decline_severity[field_idx] <- "Low" } else { @@ -626,6 +629,29 @@ create_field_detail_table <- function(field_boundaries_sf, all_kpis, current_wee result$Mean_CI <- NA_real_ } + # ============================================ + # GROUP 0b: FIELD AREA (from geometry) + # ============================================ + # Calculate field area using unified function and preferred unit + tryCatch({ + field_areas <- calculate_area_from_geometry(field_boundaries_sf, unit = AREA_UNIT_PREFERENCE) + unit_label <- get_area_unit_label(AREA_UNIT_PREFERENCE) + area_col_name <- paste0("Area_", unit_label) + + area_df <- data.frame( + field_idx = seq_along(field_areas), + area_value = field_areas, + stringsAsFactors = FALSE + ) + colnames(area_df) <- c("field_idx", area_col_name) + + result <- result %>% + left_join(area_df, by = "field_idx") + }, error = function(e) { + message(paste("Warning: Could not calculate field areas:", e$message)) + result[[paste0("Area_", get_area_unit_label(AREA_UNIT_PREFERENCE))]] <<- NA_real_ + }) + # ============================================ # GROUP 1: FIELD UNIFORMITY (KPI 1) # ============================================ diff --git a/r_app/80_utils_cane_supply.R b/r_app/80_utils_cane_supply.R index 0a324c2..eaa904b 100644 --- a/r_app/80_utils_cane_supply.R +++ b/r_app/80_utils_cane_supply.R @@ -48,47 +48,48 @@ CI_CHANGE_INCREASE_THRESHOLD <- CI_CHANGE_RAPID_GROWTH_THRESHOLD # Weekly CI #' Calculate acreage for each field from geometry #' @param field_boundaries_sf sf object with field geometries -#' @return data.frame with field and acreage columns -calculate_field_acreages <- function(field_boundaries_sf) { +#' @param unit Character. Unit preference: "hectare" or "acre" (default from AREA_UNIT_PREFERENCE) +#' @return data.frame with field and area columns (column name reflects unit: Area_ha or Area_ac) +calculate_field_acreages <- function(field_boundaries_sf, unit = AREA_UNIT_PREFERENCE) { tryCatch({ - # Project to equal-area CRS (EPSG:6933) for accurate area calculations - field_boundaries_proj <- sf::st_transform(field_boundaries_sf, "EPSG:6933") - - lookup_df <- field_boundaries_proj %>% + # Get field identifier (handles pivot.geojson structure) + field_names <- field_boundaries_sf %>% sf::st_drop_geometry() %>% - as.data.frame() %>% - mutate( - geometry_valid = sapply(seq_len(nrow(field_boundaries_proj)), function(idx) { - tryCatch({ - sf::st_is_valid(field_boundaries_proj[idx, ]) - }, error = function(e) FALSE) - }), - area_ha = 0 - ) + pull(any_of(c("field", "field_id", "Field_id", "name", "Name"))) - # Calculate area for valid geometries - valid_indices <- which(lookup_df$geometry_valid) - areas_ha <- vapply(valid_indices, function(idx) { - tryCatch({ - area_m2 <- as.numeric(sf::st_area(field_boundaries_proj[idx, ])) - area_m2 / 10000 - }, error = function(e) NA_real_) - }, numeric(1)) - lookup_df$area_ha[valid_indices] <- areas_ha + if (length(field_names) == 0 || all(is.na(field_names))) { + field_names <- seq_len(nrow(field_boundaries_sf)) + } - # Convert hectares to acres - lookup_df %>% - mutate(acreage = area_ha / 0.404686) %>% - # Aggregate by field to handle multi-row fields (e.g., sub_fields) + # Use unified area calculation function + areas <- calculate_area_from_geometry(field_boundaries_sf, unit = unit) + + # Create output data frame with unit-aware column name + unit_label <- get_area_unit_label(unit) + col_name <- paste0("Area_", unit_label) + + result_df <- data.frame( + field = field_names, + area = areas, + stringsAsFactors = FALSE + ) + colnames(result_df) <- c("field", col_name) + + # Aggregate by field to handle multi-row fields (e.g., sub_fields) + result_df %>% group_by(field) %>% - summarise(acreage = sum(acreage, na.rm = TRUE), .groups = "drop") %>% - select(field, acreage) + summarise(across(all_of(col_name), list(~ sum(., na.rm = TRUE))), .groups = "drop") }, error = function(e) { - message(paste("Warning: Could not calculate acreages from geometries -", e$message)) - data.frame(field = character(0), acreage = numeric(0)) + message(paste("Warning: Could not calculate areas from geometries -", e$message)) + unit_label <- get_area_unit_label(unit) + col_name <- paste0("Area_", unit_label) + result_df <- data.frame(field = character(0)) + result_df[[col_name]] <- numeric(0) + return(result_df) }) } + #' Calculate age in weeks from planting date #' #' @param planting_date Date of planting @@ -219,8 +220,12 @@ calculate_all_field_kpis <- function(current_stats, message("\nBuilding final field analysis output...") - # Pre-calculate acreages - acreage_lookup <- calculate_field_acreages(field_boundaries_sf) + # Pre-calculate areas using unified function + acreage_lookup <- calculate_field_acreages(field_boundaries_sf, unit = AREA_UNIT_PREFERENCE) + + # Determine area column name from result + unit_label <- get_area_unit_label(AREA_UNIT_PREFERENCE) + area_col_name <- paste0("Area_", unit_label) field_analysis_df <- current_stats %>% mutate( @@ -230,9 +235,9 @@ calculate_all_field_kpis <- function(current_stats, # Column 3: Field_name (from GeoJSON) Field_name = Field_id, - # Column 4: Acreage (from geometry) + # Column 4: Acreage (from geometry, unit-aware) Acreage = { - acreages_vec <- acreage_lookup$acreage[match(Field_id, acreage_lookup$field)] + acreages_vec <- acreage_lookup[[area_col_name]][match(Field_id, acreage_lookup$field)] if_else(is.na(acreages_vec), 0, acreages_vec) }, diff --git a/r_app/80_utils_common.R b/r_app/80_utils_common.R index 05c1b20..e47bab0 100644 --- a/r_app/80_utils_common.R +++ b/r_app/80_utils_common.R @@ -40,14 +40,23 @@ if (!exists("PROJECT", envir = .GlobalEnv)) { # CONSTANTS (from 80_calculate_kpis.R) # ============================================================================ -# Four-week trend thresholds -FOUR_WEEK_TREND_STRONG_GROWTH_MIN <- 0.5 +# Four-week trend thresholds (CI units/week) - SYMMETRIC by design +# Report mapping: +# Strong growth (↑↑): Slope ≥ 0.3 +# Weak growth (↑): Slope 0.1–0.3 +# Stable (→): Slope −0.1 to +0.1 +# Weak Decline (↓): Slope −0.3 to −0.1 +# Strong Decline (↓↓): Slope < −0.3 +FOUR_WEEK_TREND_STRONG_GROWTH_MIN <- 0.3 FOUR_WEEK_TREND_GROWTH_MIN <- 0.1 -FOUR_WEEK_TREND_GROWTH_MAX <- 0.5 -FOUR_WEEK_TREND_NO_GROWTH_RANGE <- 0.1 -FOUR_WEEK_TREND_DECLINE_MAX <- -0.1 -FOUR_WEEK_TREND_DECLINE_MIN <- -0.5 -FOUR_WEEK_TREND_STRONG_DECLINE_MAX <- -0.5 +FOUR_WEEK_TREND_STABLE_THRESHOLD <- 0.1 +FOUR_WEEK_TREND_WEAK_DECLINE_THRESHOLD <- -0.1 # upper bound of Weak Decline / lower bound of Stable +FOUR_WEEK_TREND_STRONG_DECLINE_MAX <- -0.3 +# ============================================================================ +# AREA CALCULATION UNITS & CONVERSION +# ============================================================================ +# Conversion constant: 1 hectare = 0.404686 acres (exact: 0.40468564224...) +HECTARE_TO_ACRE_CONVERSION <- 0.404686 # CV Trend thresholds (8-week slope interpretation) CV_SLOPE_STRONG_IMPROVEMENT_MIN <- -0.03 @@ -71,6 +80,85 @@ PHASE_DEFINITIONS <- data.frame( stringsAsFactors = FALSE ) +# ============================================================================ +# AREA CALCULATION FUNCTIONS (Unified across all scripts) +# ============================================================================ + +#' Calculate field area from geometry in specified unit +#' +#' Unified function for calculating polygon area from sf or SpatVect geometries. +#' Uses equal-area projection (EPSG:6933) for accurate calculations across all zones. +#' +#' @param geometry sf or SpatVect object containing field polygons +#' If sf: must have geometry column (auto-detected) +#' If SpatVect: terra object with geometry +#' @param unit Character. Output unit: "hectare" (default) or "acre" +#' +#' @return Numeric vector of areas in specified unit +#' +#' @details +#' **Projection Logic**: +#' - Input geometries are reprojected to EPSG:6933 (Equal Earth projection) +#' - This ensures accurate area calculations regardless of original CRS +#' - Equal-area projections are essential for agricultural analysis +#' +#' **Unit Conversion**: +#' - m² → hectares: divide by 10,000 +#' - hectares → acres: multiply by 2.4711 (or divide by 0.404686) +#' - Direct m² → acres: divide by 4046.8564 +#' +#' **Handling Multiple Geometries**: +#' - If `geometry` has multiple rows/features, returns vector of areas (one per feature) +#' - NA values are preserved and propagated to output +#' +#' @examples +#' # With sf object +#' library(sf) +#' fields_sf <- st_read("pivot.geojson") +#' areas_ha <- calculate_area_from_geometry(fields_sf, unit = "hectare") +#' areas_ac <- calculate_area_from_geometry(fields_sf, unit = "acre") +#' +#' # With SpatVect +#' library(terra) +#' fields_vect <- vect("pivot.geojson") +#' areas_ha <- calculate_area_from_geometry(fields_vect, unit = "hectare") +#' +#' @export +calculate_area_from_geometry <- function(geometry, unit = "hectare") { + # Validate unit parameter + unit_lower <- tolower(unit) + if (!unit_lower %in% c("hectare", "acre")) { + stop("Unit must be 'hectare' or 'acre'. Got: ", unit) + } + + tryCatch({ + # Branch by geometry type + if (inherits(geometry, "sf")) { + # Handle sf object + geometry_proj <- sf::st_transform(geometry, 6933) + areas_m2 <- as.numeric(sf::st_area(geometry_proj)) + } else if (inherits(geometry, "SpatVect")) { + # Handle terra SpatVect object + geometry_proj <- terra::project(geometry, "EPSG:6933") + areas_m2 <- as.numeric(terra::expanse(geometry_proj)) + } else { + stop("geometry must be an sf or terra SpatVect object. Got: ", + paste(class(geometry), collapse = ", ")) + } + + # Convert units + areas_ha <- areas_m2 / 10000 # m² → hectares + + if (unit_lower == "hectare") { + return(areas_ha) + } else { # unit_lower == "acre" + return(areas_ha / HECTARE_TO_ACRE_CONVERSION) + } + }, error = function(e) { + stop("Error calculating area from geometry: ", e$message) + }) +} + # ============================================================================ # WEEK/YEAR CALCULATION HELPERS (Consistent across all scripts) # ============================================================================ @@ -97,10 +185,12 @@ calculate_target_week_and_year <- function(current_week, current_year, offset_we target_week <- current_week - offset_weeks target_year <- current_year - # Handle wrapping: when going back from week 1, wrap to week 52 of previous year + # Handle wrapping: when going back from week 1, wrap to last ISO week of previous year + # Compute last_week_of_year dynamically (some years have 53 weeks) while (target_week < 1) { - target_week <- target_week + 52 target_year <- target_year - 1 + last_week_of_year <- as.numeric(format(as.Date(paste0(target_year, "-12-28")), "%V")) + target_week <- target_week + last_week_of_year } return(list(week = target_week, year = target_year)) @@ -262,11 +352,11 @@ categorize_four_week_trend <- function(ci_values_list) { if (avg_weekly_change >= FOUR_WEEK_TREND_STRONG_GROWTH_MIN) { return("strong growth") } else if (avg_weekly_change >= FOUR_WEEK_TREND_GROWTH_MIN && - avg_weekly_change < FOUR_WEEK_TREND_GROWTH_MAX) { + avg_weekly_change < FOUR_WEEK_TREND_STRONG_GROWTH_MIN) { return("growth") - } else if (abs(avg_weekly_change) <= FOUR_WEEK_TREND_NO_GROWTH_RANGE) { + } else if (abs(avg_weekly_change) <= FOUR_WEEK_TREND_STABLE_THRESHOLD) { return("no growth") - } else if (avg_weekly_change <= FOUR_WEEK_TREND_DECLINE_MIN && + } else if (avg_weekly_change <= FOUR_WEEK_TREND_WEAK_DECLINE_THRESHOLD && avg_weekly_change > FOUR_WEEK_TREND_STRONG_DECLINE_MAX) { return("decline") } else if (avg_weekly_change < FOUR_WEEK_TREND_STRONG_DECLINE_MAX) { @@ -977,6 +1067,28 @@ calculate_field_statistics <- function(field_boundaries_sf, week_num, year, next } + # Guard: detect cloud-masked dates (CI == 0 indicates no-data) + # When any extracted value is 0, treat the entire date as cloud-masked + has_zeros <- any(extracted$CI == 0, na.rm = TRUE) + + if (has_zeros) { + # Cloud-masked date: skip temporal analysis, set stats to NA + message(paste(" [CLOUD] Field", field_name, "- entire date is cloud-masked (CI==0)")) + + results_list[[length(results_list) + 1]] <- data.frame( + Field_id = field_name, + Mean_CI = NA_real_, + CV = NA_real_, + CI_range = NA_character_, + CI_Percentiles = NA_character_, + Pct_pixels_CI_gte_2 = NA_real_, + Cloud_pct_clear = 0, + Cloud_category = "No image available", + stringsAsFactors = FALSE + ) + next + } + ci_vals <- extracted$CI[!is.na(extracted$CI)] if (length(ci_vals) == 0) { @@ -1139,6 +1251,12 @@ calculate_kpi_trends <- function(current_stats, prev_stats = NULL, prev_field_analysis <- readr::read_csv(recent_file, show_col_types = FALSE, col_types = readr::cols(.default = readr::col_character()), col_select = c(Field_id, nmr_of_weeks_analysed, Phase)) + + # Convert nmr_of_weeks_analysed from character to integer (read as character via .default) + # Handle NAs appropriately during conversion + if (!is.null(prev_field_analysis) && "nmr_of_weeks_analysed" %in% names(prev_field_analysis)) { + prev_field_analysis$nmr_of_weeks_analysed <- suppressWarnings(as.integer(prev_field_analysis$nmr_of_weeks_analysed)) + } } } }, error = function(e) { @@ -1286,7 +1404,7 @@ calculate_kpi_trends <- function(current_stats, prev_stats = NULL, prev_analysis_row <- prev_field_analysis %>% dplyr::filter(Field_id == field_id) if (nrow(prev_analysis_row) > 0) { - prev_nmr_weeks_analysis <- prev_analysis_row$nmr_of_weeks_analysed[1] + prev_nmr_weeks_analysis <- as.integer(prev_analysis_row$nmr_of_weeks_analysed[1]) if (!is.na(prev_nmr_weeks_analysis)) { current_stats$nmr_of_weeks_analysed[i] <- prev_nmr_weeks_analysis + 1L } else { @@ -1402,7 +1520,9 @@ extract_ci_values <- function(ci_raster, field_vect) { } else if (ncol(extracted) > 1) { return(extracted[, ncol(extracted)]) } else { - return(extracted[, 1]) + # Degenerate case: extracted has only ID column, no CI values + # Return NA vector of appropriate length instead of the ID column + return(rep(NA_real_, nrow(extracted))) } } @@ -1415,8 +1535,9 @@ calculate_week_numbers <- function(report_date = Sys.Date()) { previous_year <- current_year if (previous_week < 1) { - previous_week <- 52 + # Compute last ISO week of previous year dynamically (some years have 53 weeks) previous_year <- current_year - 1 + previous_week <- as.numeric(format(as.Date(paste0(previous_year, "-12-28")), "%V")) } return(list( diff --git a/r_app/90_CI_report_with_kpis_agronomic_support.Rmd b/r_app/90_CI_report_with_kpis_agronomic_support.Rmd index 9263b6b..b9ff3ab 100644 --- a/r_app/90_CI_report_with_kpis_agronomic_support.Rmd +++ b/r_app/90_CI_report_with_kpis_agronomic_support.Rmd @@ -499,37 +499,88 @@ tryCatch({ }) # Helper function to handle missing translation keys -t <- function(key) { +tr_key <- function(key) { if (key %in% names(tr)) { txt <- glue(tr[key], .envir = parent.frame()) txt <- gsub("\n", " \n", txt) return(enc2utf8(as.character(txt))) } else if (is.na(key)) { - return(t("NA")) + return(tr_key("NA")) } else if (key == "") { return("") } else { return(paste0(key)) } } + +# ============================================================================ +# SHARED TREND MAPPING HELPER +# ============================================================================ +# Canonical function for converting trend text to arrows/formatted text +# Normalizes all legacy and current trend category names to standardized output +# Used by: combined_kpi_table, field_details_table, and compact_field_display chunks +map_trend_to_arrow <- function(text_vec, include_text = FALSE) { + # Normalize: convert to character and lowercase + text_lower <- tolower(as.character(text_vec)) + + # Apply mapping to each element + sapply(text_lower, function(text) { + # Handle NA and empty values + if (is.na(text) || text == "" || nchar(trimws(text)) == 0) { + return(NA_character_) + } + + # Determine category and build output with translated labels + if (grepl("strong growth", text)) { + arrow <- "↑↑" + trans_key <- "Strong growth" + } else if (grepl("slight growth|weak growth|growth|increasing", text)) { + arrow <- "↑" + trans_key <- "Slight growth" + } else if (grepl("stable|no growth", text)) { + arrow <- "→" + trans_key <- "Stable" + } else if (grepl("weak decline|slight decline|moderate decline", text)) { + arrow <- "↓" + trans_key <- "Slight decline" + } else if (grepl("strong decline|severe", text)) { + arrow <- "↓↓" + trans_key <- "Strong decline" + } else { + # Fallback: return "—" (em-dash) for arrow-only mode, raw text for text mode + # This signals an unmatched trend value that should be logged + return(if (include_text) as.character(text) else "—") + } + + # Get translated label using tr_key() + label <- tr_key(trans_key) + + # Return formatted output based on include_text flag + if (include_text) { + paste0(label, " (", arrow, ")") + } else { + arrow + } + }, USE.NAMES = FALSE) +} ``` ::: {custom-style="Cover_title" style="text-align:center; margin-top:120px;"} -`r t("cover_title")` +`r tr_key("cover_title")` ::: ::: {custom-style="Cover_subtitle" style="text-align:center; margin-top:18px;"} -`r t("cover_subtitle")` +`r tr_key("cover_subtitle")` ::: \newpage -`r t("report_summary")` +`r tr_key("report_summary")` -`r t("report_structure")` +`r tr_key("report_structure")` -`r t("key_insights")` +`r tr_key("key_insights")` ```{r key_insights, echo=FALSE, results='asis'} # Calculate key insights from KPI data @@ -539,7 +590,7 @@ if (exists("summary_tables") && !is.null(summary_tables) && length(summary_table # 1. Uniformity insights - group by interpretation if (!is.null(summary_tables$uniformity) && nrow(summary_tables$uniformity) > 0) { - cat("\n", t("field_unif")) + cat("\n", tr_key("field_unif")) uniformity_counts <- summary_tables$uniformity %>% dplyr::select(interpretation, count = field_count) @@ -547,14 +598,14 @@ if (exists("summary_tables") && !is.null(summary_tables) && length(summary_table status <- uniformity_counts$interpretation[i] count <- uniformity_counts$count[i] if (!is.na(status) && !is.na(count) && count > 0) { - cat(" -", t("unif_status")) + cat(" -", tr_key("unif_status")) } } } # 2. Area change insights - group by interpretation if (!is.null(summary_tables$area_change) && nrow(summary_tables$area_change) > 0) { - cat("\n\n", t("field_area")) + cat("\n\n", tr_key("field_area")) area_counts <- summary_tables$area_change %>% dplyr::select(interpretation, count = field_count) @@ -562,14 +613,14 @@ if (exists("summary_tables") && !is.null(summary_tables) && length(summary_table status <- area_counts$interpretation[i] count <- area_counts$count[i] if (!is.na(status) && !is.na(count) && count > 0) { - cat(" -", t("area_status")) + cat(" -", tr_key("area_status")) } } } # 3. Growth trend insights - group by trend_interpretation if (!is.null(summary_tables$growth_decline) && nrow(summary_tables$growth_decline) > 0) { - cat("\n\n", t("growth_trend")) + cat("\n\n", tr_key("growth_trend")) growth_counts <- summary_tables$growth_decline %>% dplyr::select(trend = trend_interpretation, count = field_count) @@ -577,14 +628,14 @@ if (exists("summary_tables") && !is.null(summary_tables) && length(summary_table trend <- growth_counts$trend[i] count <- growth_counts$count[i] if (!is.na(trend) && !is.na(count) && count > 0) { - cat(" -", t("trend_status")) + cat(" -", tr_key("trend_status")) } } } # 4. Patchiness insights - group by patchiness_risk if (!is.null(summary_tables$patchiness) && nrow(summary_tables$patchiness) > 0) { - cat("\n\n", t("patch_risk")) + cat("\n\n", tr_key("patch_risk")) patchiness_counts <- summary_tables$patchiness %>% dplyr::select(patchiness_risk, count = field_count) @@ -592,25 +643,32 @@ if (exists("summary_tables") && !is.null(summary_tables) && length(summary_table risk <- patchiness_counts$patchiness_risk[i] count <- patchiness_counts$count[i] if (!is.na(risk) && !is.na(count) && count > 0) { - cat(" -", t("patch_status")) + cat(" -", tr_key("patch_status")) } } } # 5. Total fields analyzed - total_fields <- sum(summary_tables$uniformity$field_count, na.rm = TRUE) - cat("\n\n", t("tot_fields_analyzed")) + if (!is.null(summary_tables$uniformity) && "field_count" %in% names(summary_tables$uniformity)) { + total_fields <- sum(summary_tables$uniformity$field_count, na.rm = TRUE) + } else { + total_fields <- NA_integer_ + } + + if (!is.na(total_fields)) { + cat("\n\n", tr_key("tot_fields_analyzed")) + } } else { - cat(t("kpi_na")) + cat(tr_key("kpi_na")) } ``` \newpage -`r t("section_i")` +`r tr_key("section_i")` -`r t("exec_summary")`\n\n +`r tr_key("exec_summary")`\n\n ```{r combined_kpi_table, echo=FALSE, results='asis'} # Display KPI tables - standardized format with Level, Count, Percent columns @@ -641,40 +699,9 @@ if (exists("summary_tables") && !is.null(summary_tables) && length(summary_table display_level <- df[[level_col]] } - # Helper function to convert trend interpretation to text + arrow format - # Works on vectors of text - handles both old and new category names - add_trend_arrows <- function(text_vec) { - # Handle NA and empty values - text_lower <- tolower(as.character(text_vec)) - - # Use sapply to apply mapping logic to each element - sapply(text_lower, function(text) { - if (is.na(text) || text == "") return(NA_character_) - - # Map trend categories to text with arrows for KPI table - # Handles both OLD names (moderate/slight decline) and NEW names (weak/strong) - if (grepl("strong growth", text)) { - "Strong Growth (↑↑)" - } else if (grepl("weak growth", text)) { - "Weak Growth (↑)" - } else if (grepl("stable|no growth", text)) { - "Stable (→)" - } else if (grepl("weak decline", text)) { - "Weak Decline (↓)" - } else if (grepl("slight decline|moderate decline", text)) { - # Map old category names to new arrow format - "Weak Decline (↓)" - } else if (grepl("strong decline", text)) { - "Strong Decline (↓↓)" - } else { - as.character(text) - } - }, USE.NAMES = FALSE) - } - df %>% dplyr::transmute( - Level = if (level_col == "trend_interpretation") add_trend_arrows(display_level) else as.character(display_level), + Level = if (level_col == "trend_interpretation") map_trend_to_arrow(display_level, include_text = TRUE) else as.character(display_level), Count = as.integer(round(as.numeric(.data[[count_col]]))), Percent = if (is.na(total)) { NA_real_ @@ -720,11 +747,11 @@ if (exists("summary_tables") && !is.null(summary_tables) && length(summary_table dplyr::select(KPI = KPI_display, Level, Count, Percent) # Translate the table for visualization - names(display_df) <- c(t("KPI"), t("Level"), t("Count"), t("Percent")) + names(display_df) <- c(tr_key("KPI"), tr_key("Level"), tr_key("Count"), tr_key("Percent")) display_df[, 1:2] <- lapply(display_df[, 1:2], function(col) sapply(col, t)) ft <- flextable(display_df) %>% - merge_v(j = t("KPI")) %>% + merge_v(j = tr_key("KPI")) %>% autofit() cum_rows <- cumsum(kpi_group_sizes) @@ -737,12 +764,12 @@ if (exists("summary_tables") && !is.null(summary_tables) && length(summary_table ft } else { - cat(t("no_kpi_table")) + cat(tr_key("no_kpi_table")) } }, error = function(e) { safe_log(paste("Error displaying KPI tables:", e$message), "WARNING") - cat(t("kpi_table_error")) + cat(tr_key("kpi_table_error")) }) } else { @@ -751,7 +778,7 @@ if (exists("summary_tables") && !is.null(summary_tables) && length(summary_table ``` \newpage -`r t("field_alerts")` +`r tr_key("field_alerts")` ```{r field_alerts_table, echo=FALSE, results='asis'} # Generate alerts for all fields @@ -815,21 +842,21 @@ generate_field_alerts <- function(field_details_table) { # Generate alerts based on priority level if (priority_level == 1) { - field_alerts <- c(field_alerts, t("priority")) + field_alerts <- c(field_alerts, tr_key("priority")) } else if (priority_level == 2) { - field_alerts <- c(field_alerts, t("monitor")) + field_alerts <- c(field_alerts, tr_key("monitor")) } # Priority 3: No alert (no stress) # Keep other alerts for decline risk, patchiness risk, gap score if (field_summary$highest_decline_risk %in% c("High", "Very-high")) { - field_alerts <- c(field_alerts, t("growth_decline")) + field_alerts <- c(field_alerts, tr_key("growth_decline")) } if (field_summary$highest_patchiness_risk == "High") { - field_alerts <- c(field_alerts, t("high_patchiness")) + field_alerts <- c(field_alerts, tr_key("high_patchiness")) } if (field_summary$max_gap_score > 20) { - field_alerts <- c(field_alerts, t("gaps_present")) + field_alerts <- c(field_alerts, tr_key("gaps_present")) } # Only add alerts if there are any (skip fields with no alerts) @@ -898,7 +925,7 @@ if (exists("field_details_table") && !is.null(field_details_table) && nrow(field autofit() ft } else { - cat(t("alerts_na")) + cat(tr_key("alerts_na")) } } else { cat("Note: Field details data not available for alerts generation. Run 80_calculate_kpis.R to generate KPI data.\n") @@ -956,18 +983,26 @@ if (!exists("field_details_table") || is.null(field_details_table)) { # Get field names from geometries field_names <- AllPivots0$field - # Try to calculate field sizes (area) from geometry if available + # Calculate field sizes (area) from geometry using unified function field_sizes <- if (!is.null(sf::st_geometry(AllPivots0)) && !all(sf::st_is_empty(sf::st_geometry(AllPivots0)))) { - sf::st_area(sf::st_geometry(AllPivots0)) / 4046.86 # Convert m² to acres (1 acre = 4046.86 m²) + tryCatch({ + calculate_area_from_geometry(AllPivots0, unit = AREA_UNIT_PREFERENCE) + }, error = function(e) { + safe_log(paste("Warning: Could not calculate area from geometry:", e$message), "WARNING") + rep(NA_real_, length(field_names)) + }) } else { rep(NA_real_, length(field_names)) } # Create minimal field details table with actual data we have + NAs for missing KPI columns # IMPORTANT: Use column names that match downstream code expectations (no spaces, match exact names) + unit_label <- get_area_unit_label(AREA_UNIT_PREFERENCE) + area_col_name <- paste0("Area_", unit_label) + field_details_table <- tibble::tibble( Field_id = field_names, - Acreage = as.numeric(field_sizes), + !! area_col_name := as.numeric(field_sizes), Growth_Uniformity = NA_character_, TCH_Forecasted = NA_real_, Gap_Score = NA_real_, @@ -1145,7 +1180,7 @@ tryCatch({ # Choose color palette based on colorblind_friendly parameter if (colorblind_friendly) { fill_scale <- ggplot2::scale_fill_viridis_c( - name = t("legend_ci"), + name = tr_key("legend_ci"), limits = c(1, 8), direction = 1, na.value = "transparent", @@ -1155,7 +1190,7 @@ tryCatch({ # Use Red-Yellow-Green diverging palette (reversed for intuitive interpretation) fill_scale <- ggplot2::scale_fill_distiller( palette = "RdYlGn", - name = t("legend_ci"), + name = tr_key("legend_ci"), limits = c(1, 8), direction = 1, # Standard direction for RdYlGn na.value = "transparent" @@ -1219,7 +1254,7 @@ tryCatch({ panel.background = ggplot2::element_rect(fill = "white", color = NA) ) + ggplot2::labs( - title = t("ci_overview_title") + title = tr_key("ci_overview_title") ) # Print the map @@ -1252,7 +1287,7 @@ tryCatch({ if (colorblind_friendly) { # Use plasma for colorblind-friendly diverging visualization fill_scale <- ggplot2::scale_fill_viridis_c( - name = t("legend_ci_change"), + name = tr_key("legend_ci_change"), option = "plasma", limits = c(-3, 3), na.value = "transparent", @@ -1262,7 +1297,7 @@ tryCatch({ # Use Red-Blue diverging palette (red=decline, blue=increase) fill_scale <- ggplot2::scale_fill_distiller( palette = "RdBu", - name = t("legend_ci_change"), + name = tr_key("legend_ci_change"), limits = c(-3, 3), direction = 1, na.value = "transparent" @@ -1326,7 +1361,7 @@ tryCatch({ panel.background = ggplot2::element_rect(fill = "white", color = NA) ) + ggplot2::labs( - title = t("ci_change_title") + title = tr_key("ci_change_title") ) # Print the map @@ -1347,7 +1382,7 @@ tryCatch({ \newpage -`r t("section_ii")` +`r tr_key("section_ii")` \newpage @@ -1486,35 +1521,36 @@ tryCatch({ if (nrow(field_kpi) > 0) { # Format KPIs as compact single line (no interpretations, just values) kpi_parts <- c( - sprintf("**%s:** %.2f", t("cv_value"), field_kpi$CV), - sprintf("**%s:** %.2f", t("mean_ci"), field_kpi$Mean_CI) + sprintf("**%s:** %.2f", tr_key("cv_value"), field_kpi$CV), + sprintf("**%s:** %.2f", tr_key("mean_ci"), field_kpi$Mean_CI) ) # Add Weekly_CI_Change if available (note: capital C and I) if (!is.null(field_kpi$Weekly_CI_Change) && !is.na(field_kpi$Weekly_CI_Change)) { change_sign <- ifelse(field_kpi$Weekly_CI_Change >= 0, "+", "") - kpi_parts <- c(kpi_parts, sprintf("**Δ%s:** %s%.2f", t("CI"), change_sign, field_kpi$Weekly_CI_Change)) + kpi_parts <- c(kpi_parts, sprintf("**Δ%s:** %s%.2f", tr_key("CI"), change_sign, field_kpi$Weekly_CI_Change)) } # Compact trend display with symbols (arrows only) - trend_compact <- case_when( - grepl("Strong growth", field_kpi$Trend_Interpretation, ignore.case = TRUE) ~ "↑↑", - grepl("Weak growth|Growth|Increasing", field_kpi$Trend_Interpretation, ignore.case = TRUE) ~ "↑", - grepl("Stable|No growth", field_kpi$Trend_Interpretation, ignore.case = TRUE) ~ "→", - grepl("Weak decline", field_kpi$Trend_Interpretation, ignore.case = TRUE) ~ "↓", - grepl("Strong decline|Severe", field_kpi$Trend_Interpretation, ignore.case = TRUE) ~ "↓↓", - TRUE ~ "?" # Fallback if no match found (shows as ? in report) - ) - kpi_parts <- c(kpi_parts, sprintf("**%s:** %s", t("Trend"), trend_compact)) + trend_compact <- map_trend_to_arrow(field_kpi$Trend_Interpretation, include_text = FALSE) + + # Log warning if trend value was unmatched (fallback to em-dash) + if (!is.na(trend_compact) && trend_compact == "—") { + warning(paste0("Trend_Interpretation mismatch for field '", field_name, "': '", + field_kpi$Trend_Interpretation, "' did not match any expected pattern. ", + "Check 80_utils_agronomic_support.R trend categories or map_trend_to_arrow() patterns.")) + } + + kpi_parts <- c(kpi_parts, sprintf("**%s:** %s", tr_key("Trend"), trend_compact)) if (!is.na(field_kpi$TCH_Forecasted) && field_kpi$TCH_Forecasted > 0) { - kpi_parts <- c(kpi_parts, sprintf("**%s:** %.1f t/ha", t("Yield"), field_kpi$TCH_Forecasted)) + kpi_parts <- c(kpi_parts, sprintf("**%s:** %.1f t/ha", tr_key("Yield"), field_kpi$TCH_Forecasted)) } kpi_parts <- c( kpi_parts, - sprintf("**%s:** %.0f%%", t("Gaps"), field_kpi$Gap_Score), - sprintf("**%s:** %s", t("Patchiness"), t(field_kpi$Patchiness_Risk)) + sprintf("**%s:** %.0f%%", tr_key("Gaps"), field_kpi$Gap_Score), + sprintf("**%s:** %s", tr_key("Patchiness"), tr_key(field_kpi$Patchiness_Risk)) ) cat(paste(kpi_parts, collapse = " | "), "\n\n") # Double newline for markdown paragraph break @@ -1568,24 +1604,44 @@ tryCatch({ }) ``` -`r t("detailed_field")` +`r tr_key("detailed_field")` ```{r detailed_field_table, echo=FALSE, results='asis'} # Detailed field performance table if (!exists("field_details_table") || is.null(field_details_table) || nrow(field_details_table) == 0) { safe_log("No field details available for table", "WARNING") - cat(t("no_field_data")) + cat(tr_key("no_field_data")) } else { - # Calculate field sizes from boundaries (convert to acres) - field_sizes_source <- if (exists("AllPivots_merged") && inherits(AllPivots_merged, "sf")) AllPivots_merged else AllPivots0 - field_sizes_df <- field_sizes_source %>% - mutate( - field_size_acres = as.numeric(sf::st_area(geometry) / 4046.86) # m² to acres - ) %>% - sf::st_drop_geometry() %>% - select(field, field_size_acres) + # Get area column from KPI data (e.g., Area_ha or Area_ac based on preference) + # If not available, calculate from boundaries + unit_label <- get_area_unit_label(AREA_UNIT_PREFERENCE) + area_col_name <- paste0("Area_", unit_label) + + # Check if area column exists in field_details_table + if (area_col_name %in% names(field_details_table)) { + # Use area from KPI data + field_sizes_df <- field_details_table %>% + select(Field_id, !! area_col_name) %>% + rename(field_size_area = !! area_col_name) %>% + mutate(field = Field_id) %>% + select(field, field_size_area) + } else { + # Fallback: calculate field sizes from boundaries using unified function + field_sizes_source <- if (exists("AllPivots_merged") && inherits(AllPivots_merged, "sf")) AllPivots_merged else AllPivots0 + field_sizes_df <- tryCatch({ + field_areas <- calculate_area_from_geometry(field_sizes_source, unit = AREA_UNIT_PREFERENCE) + data.frame( + field = field_sizes_source$field, + field_size_area = as.numeric(field_areas), + stringsAsFactors = FALSE + ) + }, error = function(e) { + safe_log(paste("Warning: Could not calculate field areas:", e$message), "WARNING") + data.frame(field = character(), field_size_area = numeric()) + }) + } # Get field ages from CI quadrant if available field_ages_df <- if (!is.null(CI_quadrant) && nrow(CI_quadrant) > 0) { @@ -1616,8 +1672,8 @@ if (!exists("field_details_table") || is.null(field_details_table) || nrow(field mutate( # Only show yield forecast for fields >= 240 days old TCH_Forecasted = if_else(is.na(Age_days) | Age_days < 240, NA_real_, TCH_Forecasted), - # Round numeric columns - field_size_acres = round(field_size_acres, 1), + # Round numeric columns (make sure field_size_area is rounded before rename) + field_size_area = round(field_size_area, 1), Mean_CI = round(Mean_CI, 2), CV = round(CV, 2), Gap_Score = round(Gap_Score, 2), @@ -1631,7 +1687,7 @@ if (!exists("field_details_table") || is.null(field_details_table) || nrow(field mutate(Weekly_CI_Change = round(Weekly_CI_Change, 2)) %>% select( field = Field_id, - field_size = field_size_acres, + field_size = field_size_area, mean_ci = Mean_CI, weekly_ci_change = Weekly_CI_Change, yield_forecast = TCH_Forecasted, @@ -1654,39 +1710,45 @@ if (!exists("field_details_table") || is.null(field_details_table) || nrow(field ) } - # Convert trend to arrows only (no text, just symbols) + # Convert trend to arrows only (canonical shared helper) # Translate patchiness_risk levels field_details_clean <- field_details_clean %>% mutate( - # Map trend categories to arrows only - trend = case_when( - grepl("Strong growth", trend, ignore.case = TRUE) ~ "↑↑", - grepl("Weak growth", trend, ignore.case = TRUE) ~ "↑", - grepl("Stable", trend, ignore.case = TRUE) ~ "→", - grepl("Weak decline", trend, ignore.case = TRUE) ~ "↓", - grepl("Strong decline", trend, ignore.case = TRUE) ~ "↓↓", - TRUE ~ trend - ), - patchiness_risk = sapply(patchiness_risk, t) + # Map trend categories to arrows only using shared helper + trend = map_trend_to_arrow(trend, include_text = FALSE), + patchiness_risk = sapply(patchiness_risk, tr_key) ) + # Log warnings for any unmatched trend values (em-dash fallback) + unmatched_trends <- field_details_clean %>% + filter(trend == "—") %>% + pull(field) + + if (length(unmatched_trends) > 0) { + for (field_with_mismatch in unmatched_trends) { + warning(paste0("Trend_Interpretation mismatch in field_details_table for field '", + field_with_mismatch, "': value did not match expected trend patterns. ", + "Check 80_utils_agronomic_support.R trend categories or map_trend_to_arrow() patterns.")) + } + } + # Translation labels for flextable header_labels <- list( - field = t("field"), - field_size = t("field_size"), - mean_ci = t("mean_ci"), - weekly_ci_change = t("weekly_ci_change"), - yield_forecast = t("yield_forecast"), - gap_score = t("gap_score"), - trend = t("Trend"), - patchiness_risk = t("patchiness_risk"), - cv_value = t("cv_value") + field = tr_key("field"), + field_size = tr_key("field_size"), + mean_ci = tr_key("mean_ci"), + weekly_ci_change = tr_key("weekly_ci_change"), + yield_forecast = tr_key("yield_forecast"), + gap_score = tr_key("gap_score"), + trend = tr_key("Trend"), + patchiness_risk = tr_key("patchiness_risk"), + cv_value = tr_key("cv_value") ) # Display the cleaned field table with flextable (fit to page width) ft <- flextable(field_details_clean) %>% set_header_labels(values = header_labels) %>% - set_caption(t("detailed_field_caption")) %>% + set_caption(tr_key("detailed_field_caption")) %>% theme_booktabs() %>% set_table_properties(width = 1, layout = "autofit") # Fit to 100% page width with auto-adjust @@ -1696,7 +1758,7 @@ if (!exists("field_details_table") || is.null(field_details_table) || nrow(field \newpage -`r t("section_iii")` +`r tr_key("section_iii")` ```{r include=FALSE} # Tries to get the CI graph in different language, otherwise falls back on English @@ -1705,116 +1767,116 @@ img_path <- ifelse(file.exists(target_img), target_img, "CI_graph_example.png") ```
-![`r t("ci_caption")`](`r img_path`){width=4in} +![`r tr_key("ci_caption")`](`r img_path`){width=4in}
-`r t("sec_iii_1")` +`r tr_key("sec_iii_1")` - - `r t("kpi_i")` - - `r t("kpi_i_metric")` - - `r t("kpi_i_calc")` - - `r t("kpi_categories")` - - `r t("kpi_i_excellent")` - - `r t("kpi_i_good")` - - `r t("kpi_i_accept")` - - `r t("kpi_i_poor")` - - `r t("kpi_i_verypoor")` - - `r t("kpi_i_why")` + - `r tr_key("kpi_i")` + - `r tr_key("kpi_i_metric")` + - `r tr_key("kpi_i_calc")` + - `r tr_key("kpi_categories")` + - `r tr_key("kpi_i_excellent")` + - `r tr_key("kpi_i_good")` + - `r tr_key("kpi_i_accept")` + - `r tr_key("kpi_i_poor")` + - `r tr_key("kpi_i_verypoor")` + - `r tr_key("kpi_i_why")` - - `r t("kpi_ii")` - - `r t("kpi_ii_calc")` - - `r t("kpi_categories")` - - `r t("kpi_ii_rapid")` - - `r t("kpi_ii_positive")` - - `r t("kpi_ii_stable")` - - `r t("kpi_ii_declining")` - - `r t("kpi_ii_rapid_decline")` - - `r t("kpi_ii_why")` + - `r tr_key("kpi_ii")` + - `r tr_key("kpi_ii_calc")` + - `r tr_key("kpi_categories")` + - `r tr_key("kpi_ii_rapid")` + - `r tr_key("kpi_ii_positive")` + - `r tr_key("kpi_ii_stable")` + - `r tr_key("kpi_ii_declining")` + - `r tr_key("kpi_ii_rapid_decline")` + - `r tr_key("kpi_ii_why")` - - `r t("kpi_iii")` - - `r t("kpi_iii_applies")` - - `r t("kpi_iii_method")` - - `r t("kpi_iii_input")` - - `r t("kpi_iii_output")` - - `r t("kpi_iii_why")` + - `r tr_key("kpi_iii")` + - `r tr_key("kpi_iii_applies")` + - `r tr_key("kpi_iii_method")` + - `r tr_key("kpi_iii_input")` + - `r tr_key("kpi_iii_output")` + - `r tr_key("kpi_iii_why")` - - `r t("kpi_iv")` - - `r t("kpi_iv_calc")` - - `r t("kpi_categories")` - - `r t("kpi_iv_str_improve")` - - `r t("kpi_iv_weak_improve")` - - `r t("kpi_iv_stable")` - - `r t("kpi_iv_weak_decline")` - - `r t("kpi_iv_str_decline")` - - `r t("kpi_iv_why")` + - `r tr_key("kpi_iv")` + - `r tr_key("kpi_iv_calc")` + - `r tr_key("kpi_categories")` + - `r tr_key("kpi_iv_str_improve")` + - `r tr_key("kpi_iv_weak_improve")` + - `r tr_key("kpi_iv_stable")` + - `r tr_key("kpi_iv_weak_decline")` + - `r tr_key("kpi_iv_str_decline")` + - `r tr_key("kpi_iv_why")` - - `r t("kpi_v")` - - `r t("kpi_v_met1")` - - `r t("kpi_v_form")` - - `r t("kpi_v_range")` - - `r t("kpi_v_interpretation")` - - `r t("kpi_v_met2")` - - `r t("kpi_v_met2_range")` - - `r t("kpi_v_thresh")` - - `r t("kpi_v_risk")` - - `r t("kpi_v_minimal")` - - `r t("kpi_v_low")` - - `r t("kpi_v_medium")` - - `r t("kpi_v_high")` - - `r t("kpi_v_why")` + - `r tr_key("kpi_v")` + - `r tr_key("kpi_v_met1")` + - `r tr_key("kpi_v_form")` + - `r tr_key("kpi_v_range")` + - `r tr_key("kpi_v_interpretation")` + - `r tr_key("kpi_v_met2")` + - `r tr_key("kpi_v_met2_range")` + - `r tr_key("kpi_v_thresh")` + - `r tr_key("kpi_v_risk")` + - `r tr_key("kpi_v_minimal")` + - `r tr_key("kpi_v_low")` + - `r tr_key("kpi_v_medium")` + - `r tr_key("kpi_v_high")` + - `r tr_key("kpi_v_why")` - - `r t("kpi_vi")` - - `r t("kpi_vi_calc")` - - `r t("kpi_vi_identify")` - - `r t("kpi_vi_calculates")` - - `r t("kpi_vi_example")` - - `r t("kpi_vi_scores")` - - `r t("kpi_vi_0")` - - `r t("kpi_vi_10")` - - `r t("kpi_vi_25")` - - `r t("kpi_vi_why")` + - `r tr_key("kpi_vi")` + - `r tr_key("kpi_vi_calc")` + - `r tr_key("kpi_vi_identify")` + - `r tr_key("kpi_vi_calculates")` + - `r tr_key("kpi_vi_example")` + - `r tr_key("kpi_vi_scores")` + - `r tr_key("kpi_vi_0")` + - `r tr_key("kpi_vi_10")` + - `r tr_key("kpi_vi_25")` + - `r tr_key("kpi_vi_why")` -`r t("sec_iii_2_to_6")` +`r tr_key("sec_iii_2_to_6")` --- -`r t("hist_benchmark")` +`r tr_key("hist_benchmark")` -`r t("unif_v_patch")` +`r tr_key("unif_v_patch")` -- `r t("unif")` -- `r t("patch")` +- `r tr_key("unif")` +- `r tr_key("patch")` -`r t("practical_example")` +`r tr_key("practical_example")` -- `r t("field_a")` -- `r t("field_b")` +- `r tr_key("field_a")` +- `r tr_key("field_b")` -`r t("scouting")` +`r tr_key("scouting")` \newpage -`r t("metadata")` +`r tr_key("metadata")` ```{r report_metadata, echo=FALSE, results='asis'} metadata_info <- data.frame( - Metric = c(t("report_gen"), t("data_source"), t("analysis_period"), t("tot_fields"), t("next_update")), + Metric = c(tr_key("report_gen"), tr_key("data_source"), tr_key("analysis_period"), tr_key("tot_fields"), tr_key("next_update")), Value = c( format(Sys.time(), "%Y-%m-%d %H:%M:%S"), - paste(t("project"), toupper(project_dir)), - paste(t("week"), current_week, t("of"), year), - ifelse(exists("AllPivots0"), nrow(AllPivots0 %>% filter(!is.na(field)) %>% group_by(field) %>% summarise()), t("unknown")), - t("next_wed") + paste(tr_key("project"), toupper(project_dir)), + paste(tr_key("week"), current_week, tr_key("of"), year), + ifelse(exists("AllPivots0"), nrow(AllPivots0 %>% filter(!is.na(field)) %>% group_by(field) %>% summarise()), tr_key("unknown")), + tr_key("next_wed") ) ) # Set names of columns according to localisation -names(metadata_info) <- c(t("metric"), t("value")) +names(metadata_info) <- c(tr_key("metric"), tr_key("value")) ft <- flextable(metadata_info) %>% - set_caption(t("metadata_caption")) %>% + set_caption(tr_key("metadata_caption")) %>% autofit() ft ``` -`r t("disclaimer")` +`r tr_key("disclaimer")` diff --git a/r_app/91_CI_report_with_kpis_cane_supply.Rmd b/r_app/91_CI_report_with_kpis_cane_supply.Rmd index 2a1bcb2..76ef0be 100644 --- a/r_app/91_CI_report_with_kpis_cane_supply.Rmd +++ b/r_app/91_CI_report_with_kpis_cane_supply.Rmd @@ -522,32 +522,44 @@ tryCatch({ } } - # Define constants - ACRE_CONV <- 4046.856 - TARGET_CRS <- 4326 # WGS84 for web basemap compatibility (was 32736 UTM) + # Define constants and unit preference + TARGET_CRS <- 4326 # WGS84 for web basemap compatibility + unit_label <- get_area_unit_label(AREA_UNIT_PREFERENCE) # Process polygons into points - # IMPORTANT: Calculate centroids in projected CRS (UTM 36S for southern Africa) to avoid - # st_centroid warnings about longitude/latitude data, then transform back to WGS84 + # IMPORTANT: Use area from field_analysis_df (calculated in script 80) rather than recalculating + # This ensures consistent unit preference across all outputs points_processed <- field_boundaries_sf %>% st_make_valid() %>% + # Note: Area will come from the left_join with analysis_data below + filter( + # Only include fields that exist in the analysis data + field %in% analysis_data$Field_id + ) %>% + left_join( + # Get area and status from analysis data (area calculated in script 80) + analysis_data %>% + left_join( + summary_data$field_analysis %>% select(Field_id, Acreage), + by = "Field_id" + ) %>% + select(Field_id, Status_trigger, Acreage), + by = c("field" = "Field_id") + ) %>% mutate( - # Calculate area, convert to numeric to strip units, divide by conversion factor - area_ac = round(as.numeric(st_area(geometry)) / ACRE_CONV, 2) + # Rename Acreage to area_value for consistency with rest of code + # (If we ever change unit preference, this will have already been calculated in the correct unit by script 80) + area_value = round(Acreage, 2) ) %>% filter( - # Filter polygons with no surface area - !is.na(area_ac), area_ac > 0 - ) %>% - left_join ( - # Add the status_trigger information - analysis_data %>% select(Field_id, Status_trigger), - by = c("field" = "Field_id") + # Filter polygons with no area + !is.na(area_value), area_value > 0 ) %>% st_transform(crs = 32736) %>% # UTM zone 36S (southern Africa) st_centroid() %>% st_transform(crs = TARGET_CRS) %>% bind_cols(st_coordinates(.)) + # Validate coordinates - check for NaN, Inf, or missing values if (any(!is.finite(points_processed$X)) || any(!is.finite(points_processed$Y))) { @@ -591,7 +603,7 @@ tryCatch({ # Hexbin for NOT ready fields (light background) geom_hex( data = points_not_ready, - aes(x = X, y = Y, weight = area_ac, alpha = "Not harvest ready"), + aes(x = X, y = Y, weight = area_value, alpha = "Not harvest ready"), binwidth = c(0.012, 0.012), fill = "#ffffff", colour = "#0000009a", @@ -600,13 +612,13 @@ tryCatch({ # Hexbin for READY fields (colored gradient) geom_hex( data = points_ready, - aes(x = X, y = Y, weight = area_ac), + aes(x = X, y = Y, weight = area_value), binwidth = c(0.012, 0.012), alpha = 0.9, colour = "#0000009a", linewidth = 0.1 ) + - # Color gradient scale for acreage + # Color gradient scale for area scale_fill_viridis_b( option = "viridis", direction = -1, diff --git a/r_app/MANUAL_PIPELINE_RUNNER.R b/r_app/MANUAL_PIPELINE_RUNNER.R index f7bf81b..0133024 100644 --- a/r_app/MANUAL_PIPELINE_RUNNER.R +++ b/r_app/MANUAL_PIPELINE_RUNNER.R @@ -438,8 +438,15 @@ # rmarkdown::render( rmarkdown::render( "r_app/90_CI_report_with_kpis_agronomic_support.Rmd", - params = list(data_dir = "aura", report_date = as.Date("2026-02-18")), - output_file = "SmartCane_Report_agronomic_support_aura_2026-02-18.docx", + params = list(data_dir = "aura", report_date = as.Date("2026-02-18"), language = "en" ), + output_file = "SmartCane_Report_agronomic_support_aura_2026-02-18_en.docx", + output_dir = "laravel_app/storage/app/aura/reports" +) + +rmarkdown::render( + "r_app/90_CI_report_with_kpis_agronomic_support.Rmd", + params = list(data_dir = "aura", report_date = as.Date("2026-02-18"), language = "es" ), + output_file = "SmartCane_Report_agronomic_support_aura_2026-02-18_es.docx", output_dir = "laravel_app/storage/app/aura/reports" ) # diff --git a/r_app/parameters_project.R b/r_app/parameters_project.R index 44931b9..d3af7bc 100644 --- a/r_app/parameters_project.R +++ b/r_app/parameters_project.R @@ -41,6 +41,34 @@ suppressPackageStartupMessages({ # ~240 days ≈ 8 months, typical sugarcane maturity window DAH_MATURITY_THRESHOLD <- 240 +# ============================================================================ +# AREA UNIT PREFERENCE +# ============================================================================ +# Unit preference for area reporting: "hectare" or "acre" +# This cascades through all KPI calculations, exports, and reports +# Future: can be overridden per-project from Laravel database (preferred_area_unit column) +AREA_UNIT_PREFERENCE <- "acre" # Options: "hectare", "acre" + +#' Get area unit label for display +#' +#' @param unit Character. Unit preference ("hectare" or "acre") +#' @return Character. Short label ("ha" or "ac") +#' +#' @examples +#' get_area_unit_label("hectare") # Returns "ha" +#' get_area_unit_label("acre") # Returns "ac" +get_area_unit_label <- function(unit = AREA_UNIT_PREFERENCE) { + unit_lower <- tolower(unit) + if (unit_lower == "hectare") { + return("ha") + } else if (unit_lower == "acre") { + return("ac") + } else { + warning("Unknown unit: ", unit, ". Defaulting to 'ha'") + return("ha") + } +} + # Maps project names to client types for pipeline control # This determines which scripts run and what outputs they produce diff --git a/r_app/translations/translations.xlsx b/r_app/translations/translations.xlsx index 33cd092722ec4f91e4bf5e4aca5ec13ce975d052..5e661811f0199eefe960289bbd3a8a2c6e784a28 100644 GIT binary patch delta 29584 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 JPW+eA{{Yr$8V&#e