# 80_REPORT_BUILDING_UTILS.R # ============================================================================ # UTILITY FUNCTIONS FOR REPORT GENERATION AND EXCEL/CSV EXPORT # # This file contains reusable functions for: # - Field analysis summary generation # - Excel/CSV/RDS export functionality # - Farm-level KPI aggregation and summary # - Tile-based KPI extraction (alternative calculation method) # # Used by: 80_calculate_kpis.R, run_full_pipeline.R, other reporting scripts # ============================================================================ # ============================================================================ # SUMMARY GENERATION # ============================================================================ generate_field_analysis_summary <- function(field_df) { message("Generating summary statistics...") total_acreage <- sum(field_df$Acreage, na.rm = TRUE) germination_acreage <- sum(field_df$Acreage[field_df$Phase == "Germination"], na.rm = TRUE) tillering_acreage <- sum(field_df$Acreage[field_df$Phase == "Tillering"], na.rm = TRUE) grand_growth_acreage <- sum(field_df$Acreage[field_df$Phase == "Grand Growth"], na.rm = TRUE) maturation_acreage <- sum(field_df$Acreage[field_df$Phase == "Maturation"], na.rm = TRUE) unknown_phase_acreage <- sum(field_df$Acreage[field_df$Phase == "Unknown"], na.rm = TRUE) harvest_ready_acreage <- sum(field_df$Acreage[field_df$Status_trigger == "harvest_ready"], na.rm = TRUE) stress_acreage <- sum(field_df$Acreage[field_df$Status_trigger == "stress_detected_whole_field"], na.rm = TRUE) recovery_acreage <- sum(field_df$Acreage[field_df$Status_trigger == "strong_recovery"], na.rm = TRUE) growth_on_track_acreage <- sum(field_df$Acreage[field_df$Status_trigger == "growth_on_track"], na.rm = TRUE) germination_complete_acreage <- sum(field_df$Acreage[field_df$Status_trigger == "germination_complete"], na.rm = TRUE) germination_started_acreage <- sum(field_df$Acreage[field_df$Status_trigger == "germination_started"], na.rm = TRUE) no_trigger_acreage <- sum(field_df$Acreage[is.na(field_df$Status_trigger)], na.rm = TRUE) clear_fields <- sum(field_df$Cloud_category == "Clear view", na.rm = TRUE) partial_fields <- sum(field_df$Cloud_category == "Partial coverage", na.rm = TRUE) no_image_fields <- sum(field_df$Cloud_category == "No image available", na.rm = TRUE) total_fields <- nrow(field_df) clear_acreage <- sum(field_df$Acreage[field_df$Cloud_category == "Clear view"], na.rm = TRUE) partial_acreage <- sum(field_df$Acreage[field_df$Cloud_category == "Partial coverage"], na.rm = TRUE) no_image_acreage <- sum(field_df$Acreage[field_df$Cloud_category == "No image available"], na.rm = TRUE) summary_df <- data.frame( Category = c( "--- PHASE DISTRIBUTION ---", "Germination", "Tillering", "Grand Growth", "Maturation", "Unknown phase", "--- STATUS TRIGGERS ---", "Harvest ready", "Stress detected", "Strong recovery", "Growth on track", "Germination complete", "Germination started", "No trigger", "--- CLOUD COVERAGE (FIELDS) ---", "Clear view", "Partial coverage", "No image available", "--- CLOUD COVERAGE (ACREAGE) ---", "Clear view", "Partial coverage", "No image available", "--- TOTAL ---", "Total Acreage" ), Acreage = c( NA, round(germination_acreage, 2), round(tillering_acreage, 2), round(grand_growth_acreage, 2), round(maturation_acreage, 2), round(unknown_phase_acreage, 2), NA, round(harvest_ready_acreage, 2), round(stress_acreage, 2), round(recovery_acreage, 2), round(growth_on_track_acreage, 2), round(germination_complete_acreage, 2), round(germination_started_acreage, 2), round(no_trigger_acreage, 2), NA, paste0(clear_fields, " fields"), paste0(partial_fields, " fields"), paste0(no_image_fields, " fields"), NA, round(clear_acreage, 2), round(partial_acreage, 2), round(no_image_acreage, 2), NA, round(total_acreage, 2) ), stringsAsFactors = FALSE ) return(summary_df) } # ============================================================================ # EXPORT FUNCTIONS # ============================================================================ export_field_analysis_excel <- function(field_df, summary_df, project_dir, current_week, year, reports_dir) { message("Exporting per-field analysis to Excel, CSV, and RDS...") field_df_rounded <- field_df %>% mutate(across(where(is.numeric), ~ round(., 2))) # Handle NULL summary_df summary_df_rounded <- if (!is.null(summary_df)) { summary_df %>% mutate(across(where(is.numeric), ~ round(., 2))) } else { NULL } output_subdir <- file.path(reports_dir, "kpis", "field_analysis") if (!dir.exists(output_subdir)) { dir.create(output_subdir, recursive = TRUE) } excel_filename <- paste0(project_dir, "_field_analysis_week", sprintf("%02d_%d", current_week, year), ".xlsx") excel_path <- file.path(output_subdir, excel_filename) excel_path <- normalizePath(excel_path, winslash = "\\", mustWork = FALSE) # Build sheets list dynamically sheets <- list( "Field Data" = field_df_rounded ) if (!is.null(summary_df_rounded)) { sheets[["Summary"]] <- summary_df_rounded } write_xlsx(sheets, excel_path) message(paste("✓ Field analysis Excel exported to:", excel_path)) kpi_data <- list( field_analysis = field_df_rounded, field_analysis_summary = summary_df_rounded, metadata = list( current_week = current_week, year = year, project = project_dir, created_at = Sys.time() ) ) rds_filename <- paste0(project_dir, "_kpi_summary_tables_week", sprintf("%02d_%d", current_week, year), ".rds") rds_path <- file.path(reports_dir, "kpis", rds_filename) saveRDS(kpi_data, rds_path) message(paste("✓ Field analysis RDS exported to:", rds_path)) csv_filename <- paste0(project_dir, "_field_analysis_week", sprintf("%02d_%d", current_week, year), ".csv") csv_path <- file.path(output_subdir, csv_filename) write_csv(field_df_rounded, csv_path) message(paste("✓ Field analysis CSV exported to:", csv_path)) return(list(excel = excel_path, rds = rds_path, csv = csv_path)) } # ============================================================================ # TILE-BASED KPI EXTRACTION (Alternative calculation method) # ============================================================================ # [COMMENTED OUT / UNUSED - kept for reference] # These functions provide tile-based extraction as an alternative to field_statistics approach # Currently replaced by calculate_field_statistics() in 80_weekly_stats_utils.R # Uncomment if parallel processing of tiles is needed in future # calculate_field_kpis_from_tiles <- function(tile_dir, week_num, year, field_boundaries_sf, tile_grid) { # message("Calculating field-level KPI statistics from tiles...") # # tile_pattern <- sprintf("week_%02d_%d_([0-9]{2})\\.tif", week_num, year) # tile_files <- list.files(tile_dir, pattern = tile_pattern, full.names = TRUE) # # if (length(tile_files) == 0) { # message("No tiles found for week", week_num, year) # return(NULL) # } # # message(paste("Processing", length(tile_files), "tiles in parallel...")) # # field_kpi_list <- furrr::future_map( # tile_files, # ~ process_single_kpi_tile( # tile_file = ., # field_boundaries_sf = field_boundaries_sf, # tile_grid = tile_grid # ), # .progress = TRUE, # .options = furrr::furrr_options(seed = TRUE) # ) # # field_kpi_stats <- dplyr::bind_rows(field_kpi_list) # # if (nrow(field_kpi_stats) == 0) { # message(" No KPI data extracted from tiles") # return(NULL) # } # # message(paste(" Extracted KPI stats for", length(unique(field_kpi_stats$field)), "unique fields")) # return(field_kpi_stats) # } # process_single_kpi_tile <- function(tile_file, field_boundaries_sf, tile_grid) { # # Helper function for calculate_field_kpis_from_tiles # tryCatch({ # tile_basename <- basename(tile_file) # tile_raster <- terra::rast(tile_file) # ci_band <- tile_raster[[1]] # # field_bbox <- sf::st_bbox(field_boundaries_sf) # ci_cropped <- terra::crop(ci_band, terra::ext(field_bbox), snap = "out") # # extracted_vals <- terra::extract(ci_cropped, field_boundaries_sf, fun = "mean", na.rm = TRUE) # # tile_results <- data.frame() # tile_id_match <- as.numeric(sub(".*_(\\d{2})\\.tif$", "\\1", tile_basename)) # # for (field_idx in seq_len(nrow(field_boundaries_sf))) { # field_id <- field_boundaries_sf$field[field_idx] # mean_ci <- extracted_vals[field_idx, 2] # # if (is.na(mean_ci)) { # next # } # # tile_results <- rbind(tile_results, data.frame( # field = field_id, # tile_id = tile_id_match, # tile_file = tile_basename, # mean_ci = round(mean_ci, 4), # stringsAsFactors = FALSE # )) # } # # return(tile_results) # # }, error = function(e) { # message(paste(" Warning: Error processing tile", basename(tile_file), ":", e$message)) # return(data.frame()) # }) # } # calculate_and_export_farm_kpis <- function(report_date, project_dir, field_boundaries_sf, # harvesting_data, cumulative_CI_vals_dir, # weekly_CI_mosaic, reports_dir, current_week, year, # tile_grid, use_tile_mosaic = FALSE, tile_grid_size = "5x5") { # # Farm-level KPI calculation using tile-based extraction (alternative approach) # # [Implementation kept as reference for alternative calculation method] # }