296 lines
12 KiB
R
296 lines
12 KiB
R
# ============================================================================
|
|
# SCRIPT 40: Weekly Mosaic Creation (CI Band Aggregation)
|
|
# ============================================================================
|
|
# PURPOSE:
|
|
# Create weekly 5-band (R, G, B, NIR, CI) mosaics from daily satellite
|
|
# imagery. Aggregates multi-day CI data into single weekly composite raster
|
|
# for field-level analysis. Supports per-field or single-file architectures.
|
|
#
|
|
# INPUT DATA:
|
|
# - Daily per-field TIFFs: laravel_app/storage/app/{project}/daily_tiles/{YYYY-MM-DD}/*.tif
|
|
# (or single-file mosaics: merged_tif/{YYYY-MM-DD}.tif + pivot.geojson masking)
|
|
# - CI data (RDS): laravel_app/storage/app/{project}/combined_CI/combined_CI_data.rds
|
|
# - Field boundaries: laravel_app/storage/app/{project}/pivot.geojson
|
|
#
|
|
# OUTPUT DATA:
|
|
# - Destination: laravel_app/storage/app/{project}/weekly_mosaic/
|
|
# - Format: 5-band GeoTIFF (uint16)
|
|
# - Naming: week_{WW}.tif (week number + year, e.g., week_35_2025.tif)
|
|
# - Spatial: Raster aligned to field boundaries; CRS preserved
|
|
#
|
|
# USAGE:
|
|
# Rscript 40_mosaic_creation.R [end_date] [offset] [project] [file_name] [data_source]
|
|
#
|
|
# Example (Windows PowerShell):
|
|
# & "C:\Program Files\R\R-4.4.3\bin\x64\Rscript.exe" r_app/40_mosaic_creation.R 2026-01-12 7 aura
|
|
#
|
|
# PARAMETERS:
|
|
# - end_date: End date (YYYY-MM-DD format); required for weekly aggregation
|
|
# - offset: Days to look back (typically 7 for one week)
|
|
# - project: Project name (aura, angata, chemba, xinavane, esa, simba)
|
|
# - file_name: Custom output filename (optional; default: week_{WW}_{YYYY}.tif)
|
|
# - data_source: Data folder (optional; auto-detects merged_tif or merged_tif_8b)
|
|
#
|
|
# CLIENT TYPES:
|
|
# - cane_supply (ANGATA): Yes - harvest readiness timeline depends on weekly mosaic
|
|
# - agronomic_support (AURA): Yes - KPI calculation requires weekly CI bands
|
|
#
|
|
# DEPENDENCIES:
|
|
# - Packages: sf, terra, tidyverse, lubridate, here
|
|
# - Utils files: parameters_project.R, 00_common_utils.R, 40_mosaic_creation_utils.R
|
|
# - Input data: Daily per-field TIFFs (Script 10) + CI extraction (Script 20)
|
|
# - Data: field boundaries (pivot.geojson), harvest dates (if available)
|
|
#
|
|
# NOTES:
|
|
# - Weekly aggregation: Combines 7 days of daily data into single composite
|
|
# - 5-band output: R, G, B, NIR, and Canopy Index (CI) derived from NDVI
|
|
# - Tiling support: Handles per-field TIFF architecture; auto-mosaics if needed
|
|
# - Data source auto-detection: Searches merged_tif/ or merged_tif_8b/ folders
|
|
# - Command-line driven: Designed for batch scheduling (cron/Task Scheduler)
|
|
# - Downstream: Script 80 (KPI calculation) depends on weekly_mosaic/ output
|
|
# - Performance: Multi-file mosaicing (~25 fields) takes 5-10 minutes per week
|
|
#
|
|
# RELATED ISSUES:
|
|
# SC-113: Script header standardization
|
|
# SC-112: Utilities restructuring
|
|
# SC-111: Script 10 geometry validation
|
|
#
|
|
# ============================================================================
|
|
|
|
# 1. Load required packages
|
|
# -----------------------
|
|
suppressPackageStartupMessages({
|
|
# File path handling
|
|
library(here) # For relative path resolution (platform-independent file paths)
|
|
|
|
# Spatial data handling
|
|
library(sf) # For spatial operations (field boundary masking)
|
|
library(terra) # For raster operations (reading/writing/stacking GeoTIFFs)
|
|
|
|
# Data manipulation
|
|
library(tidyverse) # For dplyr, readr (data wrangling)
|
|
library(lubridate) # For date/time operations (week extraction, date formatting)
|
|
})
|
|
|
|
# 2. Process command line arguments and run mosaic creation
|
|
# ------------------------------------------------------
|
|
main <- function() {
|
|
# Capture command line arguments
|
|
args <- commandArgs(trailingOnly = TRUE)
|
|
|
|
# Process project_dir argument with default
|
|
if (length(args) >= 3 && !is.na(args[3])) {
|
|
project_dir <- as.character(args[3])
|
|
} else if (exists("project_dir", envir = .GlobalEnv)) {
|
|
project_dir <- get("project_dir", envir = .GlobalEnv)
|
|
} else {
|
|
# Default project directory
|
|
project_dir <- "angata"
|
|
message("No project_dir provided. Using default:", project_dir)
|
|
}
|
|
|
|
# Make project_dir available globally so parameters_project.R can use it
|
|
assign("project_dir", project_dir, envir = .GlobalEnv)
|
|
|
|
# Process end_date argument with default
|
|
if (length(args) >= 1 && !is.na(args[1])) {
|
|
# Parse date explicitly in YYYY-MM-DD format from command line
|
|
end_date <- as.Date(args[1], format = "%Y-%m-%d")
|
|
if (is.na(end_date)) {
|
|
message("Invalid end_date provided. Using current date.")
|
|
end_date <- Sys.Date()
|
|
}
|
|
} else if (exists("end_date_str", envir = .GlobalEnv)) {
|
|
end_date <- as.Date(get("end_date_str", envir = .GlobalEnv))
|
|
} else {
|
|
# Default to current date if no argument is provided
|
|
end_date <- Sys.Date()
|
|
message("No end_date provided. Using current date: ", format(end_date))
|
|
}
|
|
|
|
# Process offset argument with default
|
|
if (length(args) >= 2 && !is.na(args[2])) {
|
|
offset <- as.numeric(args[2])
|
|
if (is.na(offset) || offset <= 0) {
|
|
message("Invalid offset provided. Using default (7 days).")
|
|
offset <- 7
|
|
}
|
|
} else {
|
|
# Default to 7 days if no argument is provided
|
|
offset <- 7
|
|
message("No offset provided. Using default:", offset, "days")
|
|
}
|
|
|
|
# Process data_source argument (optional, passed from pipeline)
|
|
# If provided, use it; otherwise auto-detect
|
|
data_source_from_args <- NULL
|
|
if (length(args) >= 5 && !is.na(args[5]) && nchar(args[5]) > 0) {
|
|
data_source_from_args <- as.character(args[5])
|
|
message("Data source explicitly provided via arguments: ", data_source_from_args)
|
|
}
|
|
|
|
# 3. Initialize project configuration
|
|
# --------------------------------
|
|
|
|
# Detect which data source directory exists (merged_tif or merged_tif_8b)
|
|
# IMPORTANT: Only consider a folder as valid if it contains actual files
|
|
laravel_storage <- here::here("laravel_app/storage/app", project_dir)
|
|
|
|
# Load centralized path structure
|
|
tryCatch({
|
|
source("r_app/parameters_project.R")
|
|
paths <- setup_project_directories(project_dir)
|
|
}, error = function(e) {
|
|
message("Note: Could not open files from r_app directory")
|
|
message("Attempting to source from default directory instead...")
|
|
tryCatch({
|
|
source("parameters_project.R")
|
|
paths <- setup_project_directories(project_dir)
|
|
message("✓ Successfully sourced files from default directory")
|
|
}, error = function(e) {
|
|
stop("Failed to source required files from both 'r_app' and default directories.")
|
|
})
|
|
})
|
|
data_source <- if (has_8b_data) {
|
|
message("Auto-detected data source: merged_tif_8b (8-band optimized) - contains files")
|
|
"merged_tif_8b"
|
|
} else if (has_legacy_data) {
|
|
message("Auto-detected data source: merged_tif (legacy 4-band) - contains files")
|
|
"merged_tif"
|
|
} else {
|
|
message("Warning: No valid data source found (both folders empty or missing). Using default: merged_tif")
|
|
"merged_tif"
|
|
}
|
|
}
|
|
|
|
# Set global data_source for parameters_project.R
|
|
assign("data_source", data_source, envir = .GlobalEnv)
|
|
|
|
tryCatch({
|
|
source("r_app/parameters_project.R")
|
|
source("r_app/00_common_utils.R")
|
|
source("r_app/40_mosaic_creation_utils.R")
|
|
safe_log(paste("Successfully sourced files from 'r_app' directory."))
|
|
}, error = function(e) {
|
|
message("Note: Could not open files from r_app directory")
|
|
message("Attempting to source from default directory instead...")
|
|
tryCatch({
|
|
source("parameters_project.R")
|
|
paths <- setup_project_directories(project_dir)
|
|
message("✓ Successfully sourced files from default directory")
|
|
}, error = function(e) {
|
|
stop("Failed to source required files from both 'r_app' and default directories.")
|
|
})
|
|
})
|
|
|
|
# Use centralized paths (no need to manually construct or create dirs)
|
|
merged_final <- paths$growth_model_interpolated_dir # or merged_final_tif if needed
|
|
daily_vrt <- paths$vrt_dir
|
|
|
|
safe_log(paste("Using growth model/mosaic directory:", merged_final))
|
|
safe_log(paste("Using daily VRT directory:", daily_vrt))
|
|
|
|
# 4. Generate date range for processing
|
|
# ---------------------------------
|
|
dates <- date_list(end_date, offset)
|
|
safe_log(paste("Processing data for week", dates$week, "of", dates$year))
|
|
|
|
# Create output filename
|
|
# Only use custom filename if explicitly provided (not empty string)
|
|
file_name_tif <- if (length(args) >= 4 && !is.na(args[4]) && nchar(args[4]) > 0) {
|
|
as.character(args[4])
|
|
} else {
|
|
paste0("week_", sprintf("%02d", dates$week), "_", dates$year, ".tif")
|
|
}
|
|
|
|
safe_log(paste("Output will be saved as:", file_name_tif))
|
|
|
|
# 5. Create weekly mosaics - route based on project tile detection
|
|
# ---------------------------------------------------------------
|
|
# The use_tile_mosaic flag is auto-detected by parameters_project.R
|
|
# based on whether tiles exist in merged_final_tif/
|
|
|
|
if (!exists("use_tile_mosaic")) {
|
|
# Fallback detection if flag not set (shouldn't happen)
|
|
merged_final_dir <- file.path(laravel_storage, "merged_final_tif")
|
|
tile_detection <- detect_tile_structure_from_merged_final(merged_final_dir)
|
|
use_tile_mosaic <- tile_detection$has_tiles
|
|
}
|
|
|
|
if (use_tile_mosaic) {
|
|
# TILE-BASED APPROACH: Create per-tile weekly MAX mosaics
|
|
# This is used for projects like Angata with large ROIs requiring spatial partitioning
|
|
# Input data comes from merged_final_tif/{grid_size}/{DATE}/{DATE}_XX.tif (5-band tiles from script 20)
|
|
tryCatch({
|
|
safe_log("Starting per-tile mosaic creation (tile-based approach)...")
|
|
|
|
# Detect grid size from merged_final_tif folder structure
|
|
# Expected: merged_final_tif/5x5/ or merged_final_tif/10x10/ etc.
|
|
merged_final_base <- file.path(laravel_storage, "merged_final_tif")
|
|
grid_subfolders <- list.dirs(merged_final_base, full.names = FALSE, recursive = FALSE)
|
|
# Look for grid size patterns like "5x5", "10x10", "20x20"
|
|
grid_patterns <- grep("^\\d+x\\d+$", grid_subfolders, value = TRUE)
|
|
|
|
if (length(grid_patterns) == 0) {
|
|
stop("No grid size subfolder found in merged_final_tif/ (expected: 5x5, 10x10, etc.)")
|
|
}
|
|
|
|
grid_size <- grid_patterns[1] # Use first grid size found
|
|
safe_log(paste("Detected grid size:", grid_size))
|
|
|
|
# Point to the grid-specific merged_final_tif directory
|
|
merged_final_with_grid <- file.path(merged_final_base, grid_size)
|
|
|
|
# Set output directory for per-tile mosaics, organized by grid size (from centralized paths)
|
|
# Output: weekly_tile_max/{grid_size}/week_WW_YYYY_TT.tif
|
|
tile_output_base <- file.path(paths$weekly_tile_max_dir, grid_size)
|
|
# Note: no dir.create needed - paths$weekly_tile_max_dir already created by setup_project_directories()
|
|
dir.create(tile_output_base, recursive = TRUE, showWarnings = FALSE) # Create grid-size subfolder
|
|
|
|
created_tile_files <- create_weekly_mosaic_from_tiles(
|
|
dates = dates,
|
|
merged_final_dir = merged_final_with_grid,
|
|
tile_output_dir = tile_output_base,
|
|
field_boundaries = field_boundaries
|
|
)
|
|
|
|
safe_log(paste("✓ Per-tile mosaic creation completed - created",
|
|
length(created_tile_files), "tile files"))
|
|
}, error = function(e) {
|
|
safe_log(paste("ERROR in tile-based mosaic creation:", e$message), "ERROR")
|
|
traceback()
|
|
stop("Mosaic creation failed")
|
|
})
|
|
|
|
} else {
|
|
# SINGLE-FILE APPROACH: Create single weekly mosaic file
|
|
# This is used for legacy projects (ESA, Chemba, Aura) expecting single-file output
|
|
tryCatch({
|
|
safe_log("Starting single-file mosaic creation (backward-compatible approach)...")
|
|
|
|
# Set output directory for single-file mosaics (from centralized paths)
|
|
single_file_output_dir <- paths$weekly_mosaic_dir
|
|
|
|
created_file <- create_weekly_mosaic(
|
|
dates = dates,
|
|
field_boundaries = field_boundaries,
|
|
daily_vrt_dir = daily_vrt,
|
|
merged_final_dir = merged_final,
|
|
output_dir = single_file_output_dir,
|
|
file_name_tif = file_name_tif,
|
|
create_plots = FALSE
|
|
)
|
|
|
|
safe_log(paste("✓ Single-file mosaic creation completed:", created_file))
|
|
}, error = function(e) {
|
|
safe_log(paste("ERROR in single-file mosaic creation:", e$message), "ERROR")
|
|
traceback()
|
|
stop("Mosaic creation failed")
|
|
})
|
|
}
|
|
}
|
|
|
|
if (sys.nframe() == 0) {
|
|
main()
|
|
}
|
|
|