--- params: ref: "word-styles-reference-var1.docx" output_file: CI_report.docx report_date: "2024-08-28" data_dir: "Chemba" mail_day: "Wednesday" borders: TRUE use_breaks: FALSE output: # html_document: # toc: yes # df_print: paged word_document: reference_docx: !expr file.path("word-styles-reference-var1.docx") toc: yes editor_options: chunk_output_type: console --- ```{r setup_parameters, include=FALSE} # Set up basic report parameters from input values report_date <- params$report_date mail_day <- params$mail_day borders <- params$borders use_breaks <- params$use_breaks # Whether to use breaks or continuous spectrum in visualizations # Environment setup notes (commented out) # # Activeer de renv omgeving # renv::activate() # renv::deactivate() # # Optioneel: Herstel de omgeving als dat nodig is # # Je kunt dit commentaar geven als je het normaal niet wilt uitvoeren # renv::restore() ``` ```{r load_libraries, message=FALSE, warning=FALSE, include=FALSE} # Configure knitr options knitr::opts_chunk$set(warning = FALSE, message = FALSE) # Path management library(here) # Spatial data libraries library(sf) library(terra) library(exactextractr) # library(raster) - Removed as it's no longer maintained # Data manipulation and visualization library(tidyverse) # Includes dplyr, ggplot2, etc. library(tmap) library(lubridate) library(zoo) # Machine learning library(rsample) library(caret) library(randomForest) library(CAST) # Load custom utility functions tryCatch({ source("report_utils.R") }, error = function(e) { message(paste("Error loading report_utils.R:", e$message)) # Try alternative path if the first one fails tryCatch({ source(here::here("r_app", "report_utils.R")) }, error = function(e) { stop("Could not load report_utils.R from either location: ", e$message) }) }) ``` ```{r initialize_project_config, message=FALSE, warning=FALSE, include=FALSE} # Set the project directory from parameters project_dir <- params$data_dir # Source project parameters with error handling tryCatch({ source(here::here("r_app", "parameters_project.R")) }, error = function(e) { stop("Error loading parameters_project.R: ", e$message) }) # Log initial configuration safe_log("Starting the R Markdown script") safe_log(paste("mail_day params:", params$mail_day)) safe_log(paste("report_date params:", params$report_date)) safe_log(paste("mail_day variable:", mail_day)) ``` ```{r calculate_dates_and_weeks, message=FALSE, warning=FALSE, include=FALSE} # Set locale for consistent date formatting Sys.setlocale("LC_TIME", "C") # Initialize date variables from parameters today <- as.character(report_date) mail_day_as_character <- as.character(mail_day) # Calculate week days report_date_as_week_day <- weekdays(lubridate::ymd(today)) days_of_week <- c("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") # Calculate initial week number week <- lubridate::week(today) safe_log(paste("Initial week calculation:", week, "today:", today)) # Calculate previous dates for comparisons today_minus_1 <- as.character(lubridate::ymd(today) - 7) today_minus_2 <- as.character(lubridate::ymd(today) - 14) today_minus_3 <- as.character(lubridate::ymd(today) - 21) # Log the weekday calculations for debugging safe_log(paste("Report date weekday:", report_date_as_week_day)) safe_log(paste("Weekday index:", which(days_of_week == report_date_as_week_day))) safe_log(paste("Mail day:", mail_day_as_character)) safe_log(paste("Mail day index:", which(days_of_week == mail_day_as_character))) # Adjust week calculation based on mail day if (which(days_of_week == report_date_as_week_day) > which(days_of_week == mail_day_as_character)) { safe_log("Adjusting weeks because of mail day") week <- lubridate::week(today) + 1 today_minus_1 <- as.character(lubridate::ymd(today)) today_minus_2 <- as.character(lubridate::ymd(today) - 7) today_minus_3 <- as.character(lubridate::ymd(today) - 14) } # Generate subtitle for report subtitle_var <- paste("Report generated on", Sys.Date()) # Calculate week numbers for previous weeks week_minus_1 <- week - 1 week_minus_2 <- week - 2 week_minus_3 <- week - 3 # Format current week with leading zeros week <- sprintf("%02d", week) # Get years for each date year <- lubridate::year(today) year_1 <- lubridate::year(today_minus_1) year_2 <- lubridate::year(today_minus_2) year_3 <- lubridate::year(today_minus_3) ``` ```{r data, message=TRUE, warning=TRUE, include=FALSE} # Load CI index data with error handling tryCatch({ CI_quadrant <- readRDS(here::here(cumulative_CI_vals_dir, "All_pivots_Cumulative_CI_quadrant_year_v2.rds")) safe_log("Successfully loaded CI quadrant data") }, error = function(e) { stop("Error loading CI quadrant data: ", e$message) }) # Get file paths for different weeks using the utility function tryCatch({ path_to_week_current = get_week_path(weekly_CI_mosaic, today, 0) path_to_week_minus_1 = get_week_path(weekly_CI_mosaic, today, -1) path_to_week_minus_2 = get_week_path(weekly_CI_mosaic, today, -2) path_to_week_minus_3 = get_week_path(weekly_CI_mosaic, today, -3) # Log the calculated paths safe_log("Required mosaic paths:") safe_log(paste("Path to current week:", path_to_week_current)) safe_log(paste("Path to week minus 1:", path_to_week_minus_1)) safe_log(paste("Path to week minus 2:", path_to_week_minus_2)) safe_log(paste("Path to week minus 3:", path_to_week_minus_3)) # Validate that files exist if (!file.exists(path_to_week_current)) warning("Current week mosaic file does not exist: ", path_to_week_current) if (!file.exists(path_to_week_minus_1)) warning("Week minus 1 mosaic file does not exist: ", path_to_week_minus_1) if (!file.exists(path_to_week_minus_2)) warning("Week minus 2 mosaic file does not exist: ", path_to_week_minus_2) if (!file.exists(path_to_week_minus_3)) warning("Week minus 3 mosaic file does not exist: ", path_to_week_minus_3) # Load raster data with terra functions CI <- terra::rast(path_to_week_current)$CI CI_m1 <- terra::rast(path_to_week_minus_1)$CI CI_m2 <- terra::rast(path_to_week_minus_2)$CI CI_m3 <- terra::rast(path_to_week_minus_3)$CI }, error = function(e) { stop("Error loading raster data: ", e$message) }) ``` ```{r calculate_difference_rasters, message=TRUE, warning=TRUE, include=FALSE} # Calculate difference rasters for comparisons tryCatch({ # Calculate weekly difference last_week_dif_raster_abs <- (CI - CI_m1) safe_log("Calculated weekly difference raster") # Calculate three-week difference three_week_dif_raster_abs <- (CI - CI_m3) safe_log("Calculated three-week difference raster") }, error = function(e) { safe_log(paste("Error calculating difference rasters:", e$message), "ERROR") # Create placeholder rasters if calculations fail if (!exists("last_week_dif_raster_abs")) { last_week_dif_raster_abs <- CI * 0 } if (!exists("three_week_dif_raster_abs")) { three_week_dif_raster_abs <- CI * 0 } }) ``` ```{r load_field_boundaries, message=TRUE, warning=TRUE, include=FALSE} # Load field boundaries from parameters tryCatch({ AllPivots0 <- field_boundaries_sf safe_log("Successfully loaded field boundaries") }, error = function(e) { stop("Error loading field boundaries: ", e$message) }) ``` ```{r create_farm_health_data, message=FALSE, warning=FALSE, include=FALSE} # Create farm health summary data from scratch tryCatch({ # Ensure we have the required data if (!exists("AllPivots0") || !exists("CI") || !exists("CI_m1") || !exists("harvesting_data")) { stop("Required input data (field boundaries, CI data, or harvesting data) not available") } safe_log("Starting to calculate farm health data") # Get unique field names fields <- unique(AllPivots0$field) safe_log(paste("Found", length(fields), "unique fields")) # Initialize result dataframe farm_health_data <- data.frame( field = character(), mean_ci = numeric(), ci_change = numeric(), ci_uniformity = numeric(), status = character(), anomaly_type = character(), priority_level = numeric(), age_weeks = numeric(), harvest_readiness = character(), stringsAsFactors = FALSE ) # Process each field with robust error handling for (field_name in fields) { tryCatch({ safe_log(paste("Processing field:", field_name)) # Get field boundary field_shape <- AllPivots0 %>% dplyr::filter(field == field_name) # Skip if field shape is empty if (nrow(field_shape) == 0) { safe_log(paste("Empty field shape for", field_name), "WARNING") next } # Get field age from harvesting data - use direct filtering to avoid dplyr errors field_age_data <- NULL if (exists("harvesting_data") && !is.null(harvesting_data) && nrow(harvesting_data) > 0) { field_age_data <- harvesting_data[harvesting_data$field == field_name, ] if (nrow(field_age_data) > 0) { field_age_data <- field_age_data[order(field_age_data$season_start, decreasing = TRUE), ][1, ] } } # Default age if not available field_age_weeks <- if (!is.null(field_age_data) && nrow(field_age_data) > 0 && !is.na(field_age_data$age)) { field_age_data$age } else { 10 # Default age } # Extract CI values using terra's extract function which is more robust ci_values <- terra::extract(CI, field_shape) ci_prev_values <- terra::extract(CI_m1, field_shape) # Check if we got valid data if (nrow(ci_values) == 0 || nrow(ci_prev_values) == 0) { safe_log(paste("No CI data extracted for field", field_name), "WARNING") # Add a placeholder row with Unknown status farm_health_data <- rbind(farm_health_data, data.frame( field = field_name, mean_ci = NA, ci_change = NA, ci_uniformity = NA, status = "Unknown", anomaly_type = "Unknown", priority_level = 5, # Low priority age_weeks = field_age_weeks, harvest_readiness = "Unknown", stringsAsFactors = FALSE )) next } # Calculate metrics - Handle NA values properly ci_column <- if ("CI" %in% names(ci_values)) "CI" else colnames(ci_values)[1] ci_prev_column <- if ("CI" %in% names(ci_prev_values)) "CI" else colnames(ci_prev_values)[1] mean_ci <- mean(ci_values[[ci_column]], na.rm=TRUE) mean_ci_prev <- mean(ci_prev_values[[ci_prev_column]], na.rm=TRUE) ci_change <- mean_ci - mean_ci_prev ci_sd <- sd(ci_values[[ci_column]], na.rm=TRUE) ci_uniformity <- ci_sd / max(0.1, mean_ci) # Avoid division by zero # Handle NaN or Inf results if (is.na(mean_ci) || is.na(ci_change) || is.na(ci_uniformity) || is.nan(mean_ci) || is.nan(ci_change) || is.nan(ci_uniformity) || is.infinite(mean_ci) || is.infinite(ci_change) || is.infinite(ci_uniformity)) { safe_log(paste("Invalid calculation results for field", field_name), "WARNING") # Add a placeholder row with Unknown status farm_health_data <- rbind(farm_health_data, data.frame( field = field_name, mean_ci = NA, ci_change = NA, ci_uniformity = NA, status = "Unknown", anomaly_type = "Unknown", priority_level = 5, # Low priority age_weeks = field_age_weeks, harvest_readiness = "Unknown", stringsAsFactors = FALSE )) next } # Determine field status status <- dplyr::case_when( mean_ci >= 5 ~ "Excellent", mean_ci >= 3.5 ~ "Good", mean_ci >= 2 ~ "Fair", mean_ci >= 1 ~ "Poor", TRUE ~ "Critical" ) # Determine anomaly type anomaly_type <- dplyr::case_when( ci_change > 2 ~ "Potential Weed Growth", ci_change < -2 ~ "Potential Weeding/Harvesting", ci_uniformity > 0.5 ~ "High Variability", mean_ci < 1 ~ "Low Vigor", TRUE ~ "None" ) # Calculate priority level (1-5, with 1 being highest priority) priority_score <- dplyr::case_when( mean_ci < 1 ~ 1, # Critical - highest priority anomaly_type == "Potential Weed Growth" ~ 2, anomaly_type == "High Variability" ~ 3, ci_change < -1 ~ 4, TRUE ~ 5 # No urgent issues ) # Determine harvest readiness harvest_readiness <- dplyr::case_when( field_age_weeks >= 52 & mean_ci >= 4 ~ "Ready for harvest", field_age_weeks >= 48 & mean_ci >= 3.5 ~ "Approaching harvest", field_age_weeks >= 40 & mean_ci >= 3 ~ "Mid-maturity", field_age_weeks >= 12 ~ "Growing", TRUE ~ "Early stage" ) # Add to summary data farm_health_data <- rbind(farm_health_data, data.frame( field = field_name, mean_ci = round(mean_ci, 2), ci_change = round(ci_change, 2), ci_uniformity = round(ci_uniformity, 2), status = status, anomaly_type = anomaly_type, priority_level = priority_score, age_weeks = field_age_weeks, harvest_readiness = harvest_readiness, stringsAsFactors = FALSE )) }, error = function(e) { safe_log(paste("Error processing field", field_name, ":", e$message), "ERROR") # Add a placeholder row with Error status farm_health_data <<- rbind(farm_health_data, data.frame( field = field_name, mean_ci = NA, ci_change = NA, ci_uniformity = NA, status = "Unknown", anomaly_type = "Unknown", priority_level = 5, # Low priority since we don't know the status age_weeks = NA, harvest_readiness = "Unknown", stringsAsFactors = FALSE )) }) } # Make sure we have data for all fields if (nrow(farm_health_data) == 0) { safe_log("No farm health data was created", "ERROR") stop("Failed to create farm health data") } # Sort by priority level farm_health_data <- farm_health_data %>% dplyr::arrange(priority_level, field) safe_log(paste("Successfully created farm health data for", nrow(farm_health_data), "fields")) }, error = function(e) { safe_log(paste("Error creating farm health data:", e$message), "ERROR") # Create an empty dataframe that can be filled by the verification chunk }) ``` ```{r verify_farm_health_data, message=FALSE, warning=FALSE, include=FALSE} # Verify farm_health_data exists and has content if (!exists("farm_health_data") || nrow(farm_health_data) == 0) { safe_log("farm_health_data not found or empty, generating default data", "WARNING") # Create minimal fallback data tryCatch({ # Get fields from boundaries fields <- unique(AllPivots0$field) # Create basic data frame with just field names farm_health_data <- data.frame( field = fields, mean_ci = rep(NA, length(fields)), ci_change = rep(NA, length(fields)), ci_uniformity = rep(NA, length(fields)), status = rep("Unknown", length(fields)), anomaly_type = rep("Unknown", length(fields)), priority_level = rep(5, length(fields)), # Low priority age_weeks = rep(NA, length(fields)), harvest_readiness = rep("Unknown", length(fields)), stringsAsFactors = FALSE ) safe_log("Created fallback farm_health_data with basic field information") }, error = function(e) { safe_log(paste("Error creating fallback farm_health_data:", e$message), "ERROR") farm_health_data <<- data.frame( field = character(), mean_ci = numeric(), ci_change = numeric(), ci_uniformity = numeric(), status = character(), anomaly_type = character(), priority_level = numeric(), age_weeks = numeric(), harvest_readiness = character(), stringsAsFactors = FALSE ) }) } ``` ```{r calculate_farm_health, message=FALSE, warning=FALSE, include=FALSE} # Calculate farm health summary metrics tryCatch({ # Generate farm health summary data farm_health_data <- generate_farm_health_summary( field_boundaries = AllPivots0, ci_current = CI, ci_previous = CI_m1, harvesting_data = harvesting_data ) # Log the summary data safe_log(paste("Generated farm health summary with", nrow(farm_health_data), "fields")) }, error = function(e) { safe_log(paste("Error in farm health calculation:", e$message), "ERROR") # Create empty dataframe if calculation failed farm_health_data <- data.frame( field = character(), mean_ci = numeric(), ci_change = numeric(), ci_uniformity = numeric(), status = character(), anomaly_type = character(), priority_level = numeric(), age_weeks = numeric(), harvest_readiness = character(), stringsAsFactors = FALSE ) }) ``` ```{r executive_summary_functions, message=FALSE, warning=FALSE, include=FALSE} # EXECUTIVE SUMMARY HELPER FUNCTIONS #' Generate a summary of farm health status #' #' @param field_boundaries Field boundaries spatial data (sf object) #' @param ci_current Current CI raster #' @param ci_previous Previous week's CI raster #' @param harvesting_data Data frame with harvesting information #' @return A data frame with farm status summary metrics #' generate_farm_health_summary <- function(field_boundaries, ci_current, ci_previous, harvesting_data) { # Generate a summary data frame of farm health by field tryCatch({ # Get unique field names fields <- unique(field_boundaries$field) # Initialize result dataframe summary_data <- data.frame( field = character(), mean_ci = numeric(), ci_change = numeric(), ci_uniformity = numeric(), status = character(), anomaly_type = character(), priority_level = numeric(), age_weeks = numeric(), harvest_readiness = character(), stringsAsFactors = FALSE ) # Process each field with better error handling for (field_name in fields) { tryCatch({ # Get field boundary field_shape <- field_boundaries %>% dplyr::filter(field == field_name) # Skip if field shape is empty if (nrow(field_shape) == 0) { safe_log(paste("Empty field shape for", field_name), "WARNING") next } # Get field age from harvesting data field_age_data <- harvesting_data %>% dplyr::filter(field == field_name) %>% dplyr::arrange(desc(season_start)) %>% dplyr::slice(1) # Default age if not available field_age_weeks <- if (nrow(field_age_data) > 0 && !is.na(field_age_data$age)) { field_age_data$age } else { 10 # Default age } # Extract CI values for this field using extract instead of crop/mask to avoid pointer issues # This is more robust than the crop+mask approach field_bbox <- sf::st_bbox(field_shape) extent_vec <- c(field_bbox$xmin, field_bbox$xmax, field_bbox$ymin, field_bbox$ymax) # Use terra extract function instead of crop+mask ci_values <- terra::extract(ci_current, field_shape) ci_prev_values <- terra::extract(ci_previous, field_shape) # Calculate metrics mean_ci <- mean(ci_values$CI, na.rm=TRUE) mean_ci_prev <- mean(ci_prev_values$CI, na.rm=TRUE) ci_change <- mean_ci - mean_ci_prev ci_sd <- sd(ci_values$CI, na.rm=TRUE) ci_uniformity <- ci_sd / max(0.1, mean_ci) # Avoid division by zero # Determine field status status <- dplyr::case_when( mean_ci >= 5 ~ "Excellent", mean_ci >= 3.5 ~ "Good", mean_ci >= 2 ~ "Fair", mean_ci >= 1 ~ "Poor", TRUE ~ "Critical" ) # Determine anomaly type anomaly_type <- dplyr::case_when( ci_change > 2 ~ "Potential Weed Growth", ci_change < -2 ~ "Potential Weeding/Harvesting", ci_uniformity > 0.5 ~ "High Variability", mean_ci < 1 ~ "Low Vigor", TRUE ~ "None" ) # Calculate priority level (1-5, with 1 being highest priority) priority_score <- dplyr::case_when( mean_ci < 1 ~ 1, # Critical - highest priority anomaly_type == "Potential Weed Growth" ~ 2, anomaly_type == "High Variability" ~ 3, ci_change < -1 ~ 4, TRUE ~ 5 # No urgent issues ) # Determine harvest readiness harvest_readiness <- dplyr::case_when( field_age_weeks >= 52 & mean_ci >= 4 ~ "Ready for harvest", field_age_weeks >= 48 & mean_ci >= 3.5 ~ "Approaching harvest", field_age_weeks >= 40 & mean_ci >= 3 ~ "Mid-maturity", field_age_weeks >= 12 ~ "Growing", TRUE ~ "Early stage" ) # Add to summary data summary_data <- rbind(summary_data, data.frame( field = field_name, mean_ci = round(mean_ci, 2), ci_change = round(ci_change, 2), ci_uniformity = round(ci_uniformity, 2), status = status, anomaly_type = anomaly_type, priority_level = priority_score, age_weeks = field_age_weeks, harvest_readiness = harvest_readiness, stringsAsFactors = FALSE )) }, error = function(e) { safe_log(paste("Error calculating health score for field", field_name, ":", e$message), "ERROR") # Add a row with NA values for this field to ensure it still appears in outputs summary_data <- rbind(summary_data, data.frame( field = field_name, mean_ci = NA, ci_change = NA, ci_uniformity = NA, status = "Error", anomaly_type = "Error", priority_level = 1, # High priority because it needs investigation age_weeks = NA, harvest_readiness = "Unknown", stringsAsFactors = FALSE )) }) } # Sort by priority level summary_data <- summary_data %>% dplyr::arrange(priority_level, field) return(summary_data) }, error = function(e) { safe_log(paste("Error in generate_farm_health_summary:", e$message), "ERROR") return(data.frame( field = character(), mean_ci = numeric(), ci_change = numeric(), ci_uniformity = numeric(), status = character(), anomaly_type = character(), priority_level = numeric(), age_weeks = numeric(), harvest_readiness = character(), stringsAsFactors = FALSE )) }) } #' Create a farm-wide anomaly detection map #' #' @param ci_current Current CI raster #' @param ci_previous Previous week's CI raster #' @param field_boundaries Field boundaries spatial data (sf object) #' @return A tmap object with anomaly visualization #' create_anomaly_map <- function(ci_current, ci_previous, field_boundaries) { tryCatch({ # Calculate difference raster ci_diff <- ci_current - ci_previous # Create a categorical raster for anomalies anomaly_raster <- ci_current * 0 # Initialize with same extent/resolution # Extract values to manipulate diff_values <- terra::values(ci_diff) curr_values <- terra::values(ci_current) # Define anomaly categories: # 4: Significant growth (potential weeds) - CI increase > 2 # 3: Moderate growth - CI increase 1-2 # 2: Stable - CI change between -1 and 1 # 1: Moderate decline - CI decrease 1-2 # 0: Significant decline (potential weeding/harvesting) - CI decrease > 2 # Apply classification anomaly_values <- rep(NA, length(diff_values)) # Significant growth (potential weeds) sig_growth <- which(diff_values > 2 & !is.na(diff_values)) anomaly_values[sig_growth] <- 4 # Moderate growth mod_growth <- which(diff_values > 1 & diff_values <= 2 & !is.na(diff_values)) anomaly_values[mod_growth] <- 3 # Stable stable <- which(diff_values >= -1 & diff_values <= 1 & !is.na(diff_values)) anomaly_values[stable] <- 2 # Moderate decline mod_decline <- which(diff_values < -1 & diff_values >= -2 & !is.na(diff_values)) anomaly_values[mod_decline] <- 1 # Significant decline (potential weeding) sig_decline <- which(diff_values < -2 & !is.na(diff_values)) anomaly_values[sig_decline] <- 0 # Set values in raster terra::values(anomaly_raster) <- anomaly_values # Create anomaly map map <- tm_shape(anomaly_raster) + tm_raster( style = "cat", palette = c("#d73027", "#fc8d59", "#ffffbf", "#91cf60", "#1a9850"), labels = c("Significant Decline", "Moderate Decline", "Stable", "Moderate Growth", "Significant Growth"), title = "Weekly CI Change" ) + tm_shape(field_boundaries) + tm_borders(col = "black", lwd = 1.5) + tm_text("field", size = 0.6) + tm_layout( main.title = "Farm-Wide Anomaly Detection", legend.outside = TRUE, legend.outside.position = "bottom" ) + tm_scale_bar(position = tm_pos_out("right", "bottom")) return(map) }, error = function(e) { safe_log(paste("Error in create_anomaly_map:", e$message), "ERROR") return(NULL) }) } #' Create a choropleth map of field health status #' #' @param field_boundaries Field boundaries with health data #' @param attribute Field to visualize (e.g., "priority_level", "mean_ci") #' @param title Map title #' @param palette Color palette to use #' @param legend_title Legend title #' @return A tmap object #' create_field_status_map <- function(field_boundaries, health_data, attribute, title = "Field Status Overview", palette = "RdYlGn", legend_title = "Status") { tryCatch({ # Join health data to field boundaries field_data <- field_boundaries %>% dplyr::left_join(health_data, by = "field") # Create style based on attribute type if (attribute == "status") { # Categorical styling for status map <- tm_shape(field_data) + tm_fill( col = attribute, palette = c("Critical" = "#d73027", "Poor" = "#fc8d59", "Fair" = "#ffffbf", "Good" = "#91cf60", "Excellent" = "#1a9850", "Error" = "#999999"), # Added Error category title = legend_title ) } else if (attribute == "priority_level") { # Numeric with custom breaks for priority (5 to 1, with 1 being highest priority) map <- tm_shape(field_data) + tm_fill( col = attribute, palette = "-RdYlGn", # Reversed so red is high priority breaks = c(0.5, 1.5, 2.5, 3.5, 4.5, 5.5), labels = c("Critical", "High", "Medium", "Low", "Minimal"), title = legend_title ) } else if (attribute == "anomaly_type") { # Categorical styling for anomalies map <- tm_shape(field_data) + tm_fill( col = attribute, palette = c("Potential Weed Growth" = "#d73027", "Potential Weeding/Harvesting" = "#4575b4", "High Variability" = "#f46d43", "Low Vigor" = "#fee090", "None" = "#91cf60", "Error" = "#999999"), # Added Error category title = legend_title ) } else if (attribute == "harvest_readiness") { # Categorical styling for harvest readiness map <- tm_shape(field_data) + tm_fill( col = attribute, palette = c("Ready for harvest" = "#1a9850", "Approaching harvest" = "#91cf60", "Mid-maturity" = "#ffffbf", "Growing" = "#fc8d59", "Early stage" = "#d73027", "Unknown" = "#999999"), # Added Unknown category title = legend_title ) } else { # Default numerical styling map <- tm_shape(field_data) + tm_fill( col = attribute, palette = palette, title = legend_title, style = "cont", na.color = "#999999" # Color for NA values ) } # Complete the map with borders and labels map <- map + tm_borders(col = "black", lwd = 1) + tm_text("field", size = 0.7) + tm_layout( main.title = title, legend.outside = TRUE, legend.outside.position = "bottom" ) + tm_scale_bar(position = tm_pos_out("right", "bottom")) return(map) }, error = function(e) { safe_log(paste("Error in create_field_status_map:", e$message), "ERROR") return(NULL) }) } #' Create a summary statistics visualization #' #' @param health_data Farm health summary data #' @return A ggplot2 object #' create_summary_stats <- function(health_data) { tryCatch({ # Handle empty dataframe case if (nrow(health_data) == 0) { return(ggplot2::ggplot() + ggplot2::annotate("text", x = 0, y = 0, label = "No field data available") + ggplot2::theme_void()) } # Count fields by status status_counts <- health_data %>% dplyr::group_by(status) %>% dplyr::summarise(count = n()) %>% dplyr::mutate(status = factor(status, levels = c("Excellent", "Good", "Fair", "Poor", "Critical", "Error"))) # Create colors for status categories status_colors <- c( "Excellent" = "#1a9850", "Good" = "#91cf60", "Fair" = "#ffffbf", "Poor" = "#fc8d59", "Critical" = "#d73027", "Error" = "#999999" ) # Create bar chart p <- ggplot2::ggplot(status_counts, ggplot2::aes(x = status, y = count, fill = status)) + ggplot2::geom_bar(stat = "identity") + ggplot2::scale_fill_manual(values = status_colors) + ggplot2::labs( title = "Field Status Summary", x = "Status", y = "Number of Fields", fill = "Field Status" ) + ggplot2::theme_minimal() + ggplot2::theme( axis.text.x = ggplot2::element_text(angle = 45, hjust = 1), legend.position = "bottom" ) return(p) }, error = function(e) { safe_log(paste("Error in create_summary_stats:", e$message), "ERROR") return(ggplot2::ggplot() + ggplot2::annotate("text", x = 0, y = 0, label = paste("Error:", e$message)) + ggplot2::theme_void()) }) } #' Create a bar chart of fields requiring attention #' #' @param health_data Farm health summary data #' @param max_fields Maximum number of fields to display #' @return A ggplot2 object #' create_priority_fields_chart <- function(health_data, max_fields = 10) { tryCatch({ # Handle empty dataframe case if (nrow(health_data) == 0) { return(ggplot2::ggplot() + ggplot2::annotate("text", x = 0, y = 0, label = "No field data available") + ggplot2::theme_void()) } # Filter for fields that need attention (priority 1-3) priority_fields <- health_data %>% dplyr::filter(priority_level <= 3) %>% dplyr::arrange(priority_level) %>% dplyr::slice_head(n = max_fields) # If no priority fields, return message if (nrow(priority_fields) == 0) { return(ggplot2::ggplot() + ggplot2::annotate("text", x = 0, y = 0, label = "No priority fields requiring attention") + ggplot2::theme_void()) } # Create priority labels priority_fields$priority_label <- factor( dplyr::case_when( priority_fields$priority_level == 1 ~ "Critical", priority_fields$priority_level == 2 ~ "High", priority_fields$priority_level == 3 ~ "Medium", TRUE ~ "Low" ), levels = c("Critical", "High", "Medium", "Low") ) # Priority colors priority_colors <- c( "Critical" = "#d73027", "High" = "#fc8d59", "Medium" = "#fee090", "Low" = "#91cf60" ) # Create chart p <- ggplot2::ggplot(priority_fields, ggplot2::aes(x = reorder(field, -priority_level), y = mean_ci, fill = priority_label)) + ggplot2::geom_bar(stat = "identity") + ggplot2::geom_text(ggplot2::aes(label = anomaly_type), position = ggplot2::position_stack(vjust = 0.5), size = 3, angle = 90, hjust = 0) + ggplot2::scale_fill_manual(values = priority_colors) + ggplot2::labs( title = "Priority Fields Requiring Attention", subtitle = "With anomaly types and CI values", x = "Field", y = "Chlorophyll Index (CI)", fill = "Priority" ) + ggplot2::theme_minimal() + ggplot2::theme( axis.text.x = ggplot2::element_text(angle = 45, hjust = 1), legend.position = "bottom" ) return(p) }, error = function(e) { safe_log(paste("Error in create_priority_fields_chart:", e$message), "ERROR") return(ggplot2::ggplot() + ggplot2::annotate("text", x = 0, y = 0, label = paste("Error:", e$message)) + ggplot2::theme_void()) }) } #' Creates a harvest readiness visualization #' #' @param health_data Farm health summary data #' @return A ggplot2 object create_harvest_readiness_chart <- function(health_data) { tryCatch({ # Handle empty dataframe case if (nrow(health_data) == 0) { return(ggplot2::ggplot() + ggplot2::annotate("text", x = 0, y = 0, label = "No field data available") + ggplot2::theme_void()) } # Count fields by harvest readiness harvest_counts <- health_data %>% dplyr::group_by(harvest_readiness) %>% dplyr::summarise(count = n()) # Order factor levels harvest_order <- c("Ready for harvest", "Approaching harvest", "Mid-maturity", "Growing", "Early stage", "Unknown") harvest_counts$harvest_readiness <- factor(harvest_counts$harvest_readiness, levels = harvest_order) # Create colors for harvest readiness categories harvest_colors <- c( "Ready for harvest" = "#1a9850", "Approaching harvest" = "#91cf60", "Mid-maturity" = "#ffffbf", "Growing" = "#fc8d59", "Early stage" = "#d73027", "Unknown" = "#999999" ) # Create pie chart p <- ggplot2::ggplot(harvest_counts, ggplot2::aes(x="", y=count, fill=harvest_readiness)) + ggplot2::geom_bar(stat="identity", width=1) + ggplot2::coord_polar("y", start=0) + ggplot2::scale_fill_manual(values = harvest_colors) + ggplot2::labs( title = "Harvest Readiness Overview", fill = "Harvest Stage" ) + ggplot2::theme_minimal() + ggplot2::theme( axis.title.x = ggplot2::element_blank(), axis.title.y = ggplot2::element_blank(), panel.border = ggplot2::element_blank(), panel.grid = ggplot2::element_blank(), axis.ticks = ggplot2::element_blank(), axis.text = ggplot2::element_blank(), plot.title = ggplot2::element_text(size=14, face="bold") ) return(p) }, error = function(e) { safe_log(paste("Error in create_harvest_readiness_chart:", e$message), "ERROR") return(ggplot2::ggplot() + ggplot2::annotate("text", x = 0, y = 0, label = paste("Error:", e$message)) + ggplot2::theme_void()) }) } #' Generate recommendations based on farm health #' #' @param health_data Farm health summary data #' @return HTML formatted recommendations generate_executive_recommendations <- function(health_data) { tryCatch({ # Handle empty dataframe case if (nrow(health_data) == 0) { return("

Executive Recommendations

No field data available to generate recommendations.

") } # Count fields by priority level priority_counts <- health_data %>% dplyr::group_by(priority_level) %>% dplyr::summarise(count = n()) # Get critical and high priority fields critical_fields <- health_data %>% dplyr::filter(priority_level == 1) %>% dplyr::pull(field) high_priority_fields <- health_data %>% dplyr::filter(priority_level == 2) %>% dplyr::pull(field) # Count harvest-ready fields harvest_ready <- health_data %>% dplyr::filter(harvest_readiness == "Ready for harvest") %>% dplyr::pull(field) approaching_harvest <- health_data %>% dplyr::filter(harvest_readiness == "Approaching harvest") %>% dplyr::pull(field) # Count anomalies by type anomaly_counts <- health_data %>% dplyr::filter(anomaly_type != "None" & anomaly_type != "Error") %>% dplyr::group_by(anomaly_type) %>% dplyr::summarise(count = n()) # Generate HTML recommendations html_output <- "
" html_output <- paste0(html_output, "

Executive Recommendations

") # Priority recommendations html_output <- paste0(html_output, "

Priority Actions:

") # Anomaly notifications if (nrow(anomaly_counts) > 0) { html_output <- paste0(html_output, "

Anomaly Notifications:

") } # Farm status summary html_output <- paste0(html_output, "

Farm Status Overview:

") return(html_output) }, error = function(e) { safe_log(paste("Error in generate_executive_recommendations:", e$message), "ERROR") return("

Error generating recommendations.

") }) } ``` `r subtitle_var` \pagebreak # Explanation of the Report This report provides a detailed analysis of your sugarcane fields based on satellite imagery, helping you monitor crop health and development throughout the growing season. The data is processed weekly to give you timely insights for optimal farm management decisions. ## What is the Chlorophyll Index (CI)? The **Chlorophyll Index (CI)** is a vegetation index that measures the relative amount of chlorophyll in plant leaves. Chlorophyll is the green pigment responsible for photosynthesis in plants. Higher CI values indicate: * Greater photosynthetic activity * Healthier plant tissue * Better nitrogen uptake * More vigorous crop growth CI values typically range from 0 (bare soil or severely stressed vegetation) to 7+ (very healthy, dense vegetation). For sugarcane, values between 3-7 generally indicate good crop health, depending on the growth stage. # Executive Dashboard ## Farm Health Status The map below shows the overall health status of all fields based on current Chlorophyll Index values. This provides a quick overview of which areas of your farm are performing well and which might need intervention. **How it works:** Field health status is determined by the average Chlorophyll Index (CI) value across each field: - **Excellent** (dark green): CI ≥ 5.0 - **Good** (light green): CI 3.5-4.99 - **Fair** (yellow): CI 2.0-3.49 - **Poor** (orange): CI 1.0-1.99 - **Critical** (red): CI < 1.0 Fields with higher CI values indicate better crop vigor and photosynthetic activity, which typically correlate with healthier plants. ```{r render_field_status_map, echo=FALSE, fig.height=6, fig.width=9, message=FALSE, warning=FALSE} # Create field status map tryCatch({ # Create and display the field status map field_status_map <- create_field_status_map( field_boundaries = AllPivots0, health_data = farm_health_data, attribute = "status", title = "Field Health Status Overview", palette = "RdYlGn", legend_title = "Health Status" ) # Print the map print(field_status_map) }, error = function(e) { safe_log(paste("Error creating field status map:", e$message), "ERROR") plot(1, type="n", axes=FALSE, xlab="", ylab="") text(1, 1, "Error creating field status map", cex=1.5) }) ``` ## Management Priorities This map highlights which fields require priority management attention based on current health indicators and trends. Fields in red require immediate attention, while green fields are performing well with minimal intervention needed. **How it works:** Priority levels are calculated based on a combination of factors: - **Critical Priority** (dark red): Fields with CI < 1.0 or critical health issues - **High Priority** (red): Fields with potential weed growth (CI increase > 2) - **Medium Priority** (orange): Fields with high internal variability - **Low Priority** (light green): Fields with moderate decline in CI - **Minimal Priority** (dark green): Stable, healthy fields The priority algorithm considers both absolute CI values and week-to-week changes to identify fields that need immediate management attention. ```{r render_priority_map, echo=FALSE, fig.height=6, fig.width=9, message=FALSE, warning=FALSE} # Create priority management map tryCatch({ # Fix the priority mapping so red = high priority, green = low priority # Reverse the priority levels before mapping (1=critical becomes 5, 5=minimal becomes 1) farm_health_data$display_priority <- 6 - farm_health_data$priority_level # Create and display the priority map with corrected priority levels priority_map <- tm_shape(AllPivots0 %>% dplyr::left_join(farm_health_data, by = "field")) + tm_fill( col = "display_priority", palette = "RdYlGn", # Now properly oriented: red = high priority, green = low priority breaks = c(0.5, 1.5, 2.5, 3.5, 4.5, 5.5), labels = c("Minimal", "Low", "Medium", "High", "Critical"), title = "Priority Level" ) + tm_borders(col = "black", lwd = 1) + tm_text("field", size = 0.7) + tm_layout( main.title = "Field Management Priority", legend.outside = TRUE, legend.outside.position = "bottom" ) + tm_scale_bar(position = tm_pos_out("right", "bottom")) # Print the map print(priority_map) }, error = function(e) { safe_log(paste("Error creating priority map:", e$message), "ERROR") plot(1, type="n", axes=FALSE, xlab="", ylab="") text(1, 1, "Error creating priority map", cex=1.5) }) ``` \pagebreak ## Crop Anomaly Detection The map below highlights potential anomalies in your fields that may require investigation. Areas with sudden changes in CI values could indicate weeding activities, rapid weed growth, or other management interventions. **How it works:** This map compares current week's CI values with those from the previous week: - **Significant Growth** (dark green): CI increase > 2 units (potential weed growth) - **Moderate Growth** (light green): CI increase of 1-2 units - **Stable** (yellow): CI change between -1 and +1 units - **Moderate Decline** (orange): CI decrease of 1-2 units - **Significant Decline** (red): CI decrease > 2 units (potential weeding/harvesting activities) Areas with significant growth (dark green) may indicate rapid weed growth that requires monitoring, while significant declines (red) often indicate recent management activities like weeding or harvesting. ```{r render_anomaly_map, echo=FALSE, fig.height=6, fig.width=9, message=FALSE, warning=FALSE} # Create anomaly detection map tryCatch({ # Create and display the anomaly map anomaly_map <- create_anomaly_map( ci_current = CI, ci_previous = CI_m1, field_boundaries = AllPivots0 ) # Print the map print(anomaly_map) }, error = function(e) { safe_log(paste("Error creating anomaly map:", e$message), "ERROR") plot(1, type="n", axes=FALSE, xlab="", ylab="") text(1, 1, "Error creating anomaly map", cex=1.5) }) ``` \pagebreak ## Harvest Planning This map shows the harvest readiness status of all fields, helping you plan harvest operations and logistics. Fields in dark green are ready for harvest, while those in yellow through red are at earlier growth stages. **How it works:** Harvest readiness is determined by combining field age and CI values: - **Ready for harvest** (dark green): Fields ≥52 weeks old with CI ≥4.0 - **Approaching harvest** (light green): Fields ≥48 weeks old with CI ≥3.5 - **Mid-maturity** (yellow): Fields ≥40 weeks old with CI ≥3.0 - **Growing** (orange): Fields ≥12 weeks old - **Early stage** (red): Fields <12 weeks old This classification helps prioritize harvesting operations and logistical planning by identifying fields that are at optimal maturity for maximum sugar content. ```{r render_harvest_map, echo=FALSE, fig.height=6, fig.width=9, message=FALSE, warning=FALSE} # Create harvest planning map tryCatch({ # Create and display the harvest readiness map harvest_map <- create_field_status_map( field_boundaries = AllPivots0, health_data = farm_health_data, attribute = "harvest_readiness", title = "Harvest Readiness Status", palette = "RdYlGn", legend_title = "Harvest Status" ) # Print the map print(harvest_map) }, error = function(e) { safe_log(paste("Error creating harvest map:", e$message), "ERROR") plot(1, type="n", axes=FALSE, xlab="", ylab="") text(1, 1, "Error creating harvest map", cex=1.5) }) ``` \pagebreak ## Field Status Summary The charts below provide an overview of your farm's health and harvest readiness status, showing the distribution of fields across different health categories and maturity stages. **How the Field Status Chart works:** This bar chart displays the count of fields in each health status category, based on the same CI thresholds described in the Farm Health Status section: - **Excellent** (dark green): CI ≥ 5.0 - **Good** (light green): CI 3.5-4.99 - **Fair** (yellow): CI 2.0-3.49 - **Poor** (orange): CI 1.0-1.99 - **Critical** (red): CI < 1.0 **How the Harvest Readiness Chart works:** This pie chart shows the distribution of fields by harvest readiness, allowing you to see at a glance how many fields are in each stage of development. Fields are categorized based on both age and CI values as described in the Harvest Planning section above. ```{r render_status_charts, echo=FALSE, fig.height=5, fig.width=10, message=FALSE, warning=FALSE} # Create field status summary visualization tryCatch({ # Create field status charts status_chart <- create_summary_stats(farm_health_data) # Print the chart print(status_chart) # Create a second row with harvest readiness chart harvest_chart <- create_harvest_readiness_chart(farm_health_data) # Print the chart print(harvest_chart) }, error = function(e) { safe_log(paste("Error creating status summary charts:", e$message), "ERROR") plot(1, type="n", axes=FALSE, xlab="", ylab="") text(1, 1, "Error creating status summary charts", cex=1.5) }) ``` ## Priority Fields Requiring Attention The chart below highlights fields that require immediate management attention based on their health scores and anomaly detection. These should be prioritized for field inspections. **How it works:** This chart shows fields with priority levels 1-3 (critical, high, and medium): - Fields are ordered by priority level, with the most critical fields on the left - Bar height represents the Chlorophyll Index (CI) value - Bar colors indicate priority level: red (critical), orange (high), yellow (medium) - Text labels show the detected anomaly type for each field The table below the chart provides detailed metrics for these priority fields, including CI values, weekly changes, anomaly types, and harvest status. Only fields requiring management attention (priority levels 1-3) are included. ```{r render_priority_fields_chart, echo=FALSE, fig.height=5, fig.width=10, message=FALSE, warning=FALSE} # Create priority fields chart tryCatch({ # Create and display priority fields chart priority_chart <- create_priority_fields_chart(farm_health_data) # Print the chart print(priority_chart) # Create a table of priority fields priority_table <- farm_health_data %>% dplyr::filter(priority_level <= 3) %>% dplyr::arrange(priority_level, field) %>% dplyr::select( Field = field, Status = status, `CI Value` = mean_ci, `Weekly Change` = ci_change, `Anomaly Type` = anomaly_type, `Age (Weeks)` = age_weeks, `Harvest Status` = harvest_readiness ) # Display the table if there are priority fields if (nrow(priority_table) > 0) { knitr::kable(priority_table, caption = "Priority Fields Requiring Management Attention") } else { cat("No priority fields requiring immediate attention this week.") } }, error = function(e) { safe_log(paste("Error creating priority fields chart:", e$message), "ERROR") cat("Error generating priority fields visualization. See log for details.") }) ``` \pagebreak ## Management Recommendations ```{r render_recommendations, echo=FALSE, results='asis', message=FALSE, warning=FALSE} # Generate executive recommendations tryCatch({ # Create and display recommendations recommendations_html <- generate_executive_recommendations(farm_health_data) # Print the HTML recommendations cat(recommendations_html) }, error = function(e) { safe_log(paste("Error creating recommendations:", e$message), "ERROR") cat("

Error generating recommendations. Please see system administrator.

") }) ``` ## Yield Prediction Overview This section provides yield predictions for mature fields (over 300 days old) based on their Chlorophyll Index values and growth patterns. These predictions can help with harvest planning and yield forecasting. ```{r render_yield_summary, echo=FALSE, fig.height=5, fig.width=10, message=FALSE, warning=FALSE} # Create yield summary tryCatch({ if (exists("pred_rf_current_season") && nrow(pred_rf_current_season) > 0) { # Calculate total estimated production total_yield <- sum(pred_rf_current_season$predicted_Tcha, na.rm = TRUE) # Create summary box cat("
") cat("

Yield Summary

") cat("") cat("
") # Display yield prediction table harvest_ready_fields <- pred_rf_current_season %>% dplyr::arrange(desc(predicted_Tcha)) %>% dplyr::select( Field = field, `Sub Field` = sub_field, `Age (Days)` = Age_days, `Cumulative CI` = total_CI, `Predicted Yield (Tonnes/ha)` = predicted_Tcha ) knitr::kable(harvest_ready_fields, caption = "Predicted Yields for Harvest-Ready Fields", digits = 1) } else { cat("
") cat("

Yield Summary

") cat("

No fields currently meet harvest readiness criteria (>300 days) for yield prediction.

") cat("
") } }, error = function(e) { safe_log(paste("Error creating yield summary:", e$message), "ERROR") cat("

Error generating yield summary. Please see system administrator.

") }) ```