# ============================================================================== # SmartCane Script 10: Create Per-Field TIFFs # ============================================================================== # # PURPOSE: # Split full-farm satellite TIFFs into per-field file structure across TWO phases: # # PHASE 1 - MIGRATION (Legacy Data): # Input: merged_final_tif/{DATE}.tif (5-band: R,G,B,NIR,CI - with CI calculated) # Output: field_tiles_CI/{FIELD}/{DATE}.tif # Status: One-time reorganization of existing data; will be removed after 2-3 weeks # # PHASE 2 - PROCESSING (New Downloads): # Input: merged_tif/{DATE}.tif (4-band: R,G,B,NIR - raw from Planet API) # Output: field_tiles/{FIELD}/{DATE}.tif # Status: Ongoing for all new downloads; always runs (not conditional) # # INTEGRATION WITH DOWNSTREAM SCRIPTS: # - Script 20 (CI Extraction): # Reads from field_tiles/{FIELD}/{DATE}.tif # Adds CI calculation → outputs to field_tiles_CI/{FIELD}/{DATE}.tif (5-band) # - Script 40 (Mosaic Creation): # Reads from field_tiles_CI/{FIELD}/{DATE}.tif (via per-field weekly aggregation) # Creates weekly_mosaic/{FIELD}/week_{WW}.tif # # ARCHITECTURE: # This script uses field/date folder organization: # field_tiles/ # ├── field_1/ # │ ├── 2024-01-15.tif # │ └── 2024-01-16.tif # └── field_2/ # ├── 2024-01-15.tif # └── 2024-01-16.tif # # Benefits: Upstream scripts iterate per-field → per-date, enabling clean # aggregation for mosaics (Script 40) and KPIs (Script 80/90). # # ============================================================================== library(terra) library(sf) # ============================================================================ # HELPER FUNCTIONS (DEFINE FIRST) # ============================================================================ smartcane_log <- function(msg) { cat(paste0("[", Sys.time(), "] ", msg, "\n")) } # Load field boundaries from GeoJSON load_field_boundaries <- function(geojson_path) { smartcane_log(paste("Loading field boundaries from:", geojson_path)) if (!file.exists(geojson_path)) { stop("GeoJSON file not found:", geojson_path) } fields <- st_read(geojson_path, quiet = TRUE) # Standardize field name property if (!"field_name" %in% names(fields)) { if ("field" %in% names(fields)) { fields$field_name <- fields$field } else if ("FIELD_ID" %in% names(fields)) { fields$field_name <- fields$FIELD_ID } else if ("Name" %in% names(fields)) { fields$field_name <- fields$Name } else { # Default: use first non-geometry column field_col <- names(fields)[!names(fields) %in% c("geometry", "geom")] if (length(field_col) > 0) { fields$field_name <- fields[[field_col[1]]] } else { stop("No suitable field name column found in GeoJSON") } } } smartcane_log(paste("Loaded", nrow(fields), "field(s)")) return(fields) } # ============================================================================ # PROJECT SETUP # ============================================================================ # Get project parameter args <- commandArgs(trailingOnly = TRUE) if (length(args) == 0) { PROJECT <- "angata" } else { PROJECT <- args[1] } # Construct paths directly (avoid complex parameter initialization) base_path <- file.path(getwd(), "laravel_app", "storage", "app", PROJECT) data_dir <- file.path(base_path, "Data") smartcane_log(paste("Project:", PROJECT)) smartcane_log(paste("Base path:", base_path)) smartcane_log(paste("Data dir:", data_dir)) # Unified function to crop TIFF to field boundaries # Called by both migration and processing phases crop_tiff_to_fields <- function(tif_path, tif_date, fields, output_base_dir) { created <- 0 skipped <- 0 errors <- 0 # Load raster if (!file.exists(tif_path)) { smartcane_log(paste("ERROR: TIFF not found:", tif_path)) return(list(created = 0, skipped = 0, errors = 1)) } rast <- tryCatch({ rast(tif_path) }, error = function(e) { smartcane_log(paste("ERROR loading raster:", e$message)) return(NULL) }) if (is.null(rast)) { return(list(created = 0, skipped = 0, errors = 1)) } # Create raster bounding box in raster CRS rast_bbox <- st_as_sfc(st_bbox(rast)) st_crs(rast_bbox) <- st_crs(rast) # Reproject fields to match raster CRS fields_reprojected <- st_transform(fields, st_crs(rast_bbox)) # Find which fields intersect this raster (CRITICAL: raster bbox first, then fields) overlapping_indices <- st_intersects(rast_bbox, fields_reprojected, sparse = TRUE) overlapping_indices <- unique(unlist(overlapping_indices)) if (length(overlapping_indices) == 0) { smartcane_log(paste("No fields intersect TIFF:", basename(tif_path))) return(list(created = 0, skipped = 0, errors = 0)) } # Process each overlapping field for (field_idx in overlapping_indices) { field_name <- fields$field_name[field_idx] field_geom <- fields_reprojected[field_idx, ] # Create field directory field_dir <- file.path(output_base_dir, field_name) if (!dir.exists(field_dir)) { dir.create(field_dir, recursive = TRUE, showWarnings = FALSE) } # Output file path output_path <- file.path(field_dir, paste0(tif_date, ".tif")) # Check if file already exists (idempotent) if (file.exists(output_path)) { skipped <- skipped + 1 next } # Crop raster to field boundary tryCatch({ field_rast <- crop(rast, field_geom) writeRaster(field_rast, output_path, overwrite = TRUE) created <- created + 1 }, error = function(e) { smartcane_log(paste("ERROR cropping field", field_name, ":", e$message)) errors <<- errors + 1 }) } return(list(created = created, skipped = skipped, errors = errors)) } # Migrate legacy 5-band TIFFs with CI from merged_final_tif migrate_old_merged_final_tif <- function(merged_final_dir, field_tiles_ci_dir, fields) { smartcane_log("\n========================================") smartcane_log("PHASE 1: MIGRATING LEGACY DATA") smartcane_log("========================================") # Check if legacy directory exists if (!dir.exists(merged_final_dir)) { smartcane_log("No legacy merged_final_tif/ directory found - skipping migration") return(list(total_created = 0, total_skipped = 0, total_errors = 0)) } # Create output directory if (!dir.exists(field_tiles_ci_dir)) { dir.create(field_tiles_ci_dir, recursive = TRUE, showWarnings = FALSE) } # Find all date-pattern TIFFs in root of merged_final_tif tiff_files <- list.files( merged_final_dir, pattern = "^[0-9]{4}-[0-9]{2}-[0-9]{2}\\.tif$", full.names = TRUE ) smartcane_log(paste("Found", length(tiff_files), "legacy TIFF(s) to migrate")) if (length(tiff_files) == 0) { smartcane_log("No legacy TIFFs found - skipping migration") return(list(total_created = 0, total_skipped = 0, total_errors = 0)) } # Process each legacy TIFF total_created <- 0 total_skipped <- 0 total_errors <- 0 for (tif_path in tiff_files) { tif_date <- gsub("\\.tif$", "", basename(tif_path)) smartcane_log(paste("Migrating:", tif_date)) result <- crop_tiff_to_fields(tif_path, tif_date, fields, field_tiles_ci_dir) total_created <- total_created + result$created total_skipped <- total_skipped + result$skipped total_errors <- total_errors + result$errors } smartcane_log(paste("Migration complete: created =", total_created, ", skipped =", total_skipped, ", errors =", total_errors)) return(list(total_created = total_created, total_skipped = total_skipped, total_errors = total_errors)) } # Process new 4-band raw TIFFs from merged_tif process_new_merged_tif <- function(merged_tif_dir, field_tiles_dir, fields) { smartcane_log("\n========================================") smartcane_log("PHASE 2: PROCESSING NEW DOWNLOADS") smartcane_log("========================================") # Check if download directory exists if (!dir.exists(merged_tif_dir)) { smartcane_log("No merged_tif/ directory found - no new data to process") return(list(total_created = 0, total_skipped = 0, total_errors = 0)) } # Create output directory if (!dir.exists(field_tiles_dir)) { dir.create(field_tiles_dir, recursive = TRUE, showWarnings = FALSE) } # Find all date-pattern TIFFs in root of merged_tif tiff_files <- list.files( merged_tif_dir, pattern = "^[0-9]{4}-[0-9]{2}-[0-9]{2}\\.tif$", full.names = TRUE ) smartcane_log(paste("Found", length(tiff_files), "TIFF(s) to process")) if (length(tiff_files) == 0) { smartcane_log("No new TIFFs found - nothing to process") return(list(total_created = 0, total_skipped = 0, total_errors = 0)) } # Process each new TIFF total_created <- 0 total_skipped <- 0 total_errors <- 0 for (tif_path in tiff_files) { tif_date <- gsub("\\.tif$", "", basename(tif_path)) smartcane_log(paste("Processing:", tif_date)) result <- crop_tiff_to_fields(tif_path, tif_date, fields, field_tiles_dir) total_created <- total_created + result$created total_skipped <- total_skipped + result$skipped total_errors <- total_errors + result$errors } smartcane_log(paste("Processing complete: created =", total_created, ", skipped =", total_skipped, ", errors =", total_errors)) return(list(total_created = total_created, total_skipped = total_skipped, total_errors = total_errors)) } # ============================================================================ # MAIN EXECUTION # ============================================================================ smartcane_log("========================================") smartcane_log(paste("Script 10: Per-Field TIFF Creation for", PROJECT)) smartcane_log("========================================") # Create necessary directories dir.create(data_dir, recursive = TRUE, showWarnings = FALSE) # Load field boundaries geojson_path <- file.path(data_dir, "pivot.geojson") fields <- load_field_boundaries(geojson_path) # Define input and output directories merged_final_dir <- file.path(base_path, "merged_final_tif") merged_tif_dir <- file.path(base_path, "merged_tif") field_tiles_dir <- file.path(base_path, "field_tiles") field_tiles_ci_dir <- file.path(base_path, "field_tiles_CI") # PHASE 1: Migrate legacy data (if exists) migrate_result <- migrate_old_merged_final_tif(merged_final_dir, field_tiles_ci_dir, fields) # PHASE 2: Process new downloads (always runs) process_result <- process_new_merged_tif(merged_tif_dir, field_tiles_dir, fields) smartcane_log("\n========================================") smartcane_log("FINAL SUMMARY") smartcane_log("========================================") smartcane_log(paste("Migration: created =", migrate_result$total_created, ", skipped =", migrate_result$total_skipped, ", errors =", migrate_result$total_errors)) smartcane_log(paste("Processing: created =", process_result$total_created, ", skipped =", process_result$total_skipped, ", errors =", process_result$total_errors)) smartcane_log("Script 10 complete") smartcane_log("========================================\n")