use crate::OutputFormat;
use crate::util::{progress, raster};
use anyhow::{Context, Result};
use clap::Args;
use console::style;
use oxigdal_core::buffer::RasterBuffer;
use oxigdal_core::types::GeoTransform;
use serde::Serialize;
use std::path::PathBuf;
#[derive(Args, Debug)]
pub struct MergeArgs {
#[arg(short = 'o', long, value_name = "OUTPUT")]
output: PathBuf,
#[arg(value_name = "INPUT", required = true)]
inputs: Vec<PathBuf>,
#[arg(long)]
no_data: Option<f64>,
#[arg(long)]
output_no_data: Option<f64>,
#[arg(long)]
epsg: Option<u32>,
#[arg(long)]
overwrite: bool,
#[arg(long, default_value = "true")]
progress: bool,
#[arg(long = "co", value_parser = crate::util::creation_options::parse_key_value)]
pub creation_options: Vec<(String, String)>,
}
#[derive(Serialize)]
struct MergeResult {
output_file: String,
input_count: usize,
width: u64,
height: u64,
bands: usize,
}
pub fn execute(args: MergeArgs, format: OutputFormat) -> Result<()> {
let _co = crate::util::creation_options::map_creation_options(&args.creation_options);
if args.output.exists() && !args.overwrite {
anyhow::bail!(
"Output file already exists: {}. Use --overwrite to replace.",
args.output.display()
);
}
if args.inputs.len() < 2 {
anyhow::bail!("Merge requires at least 2 input files");
}
let pb = if args.progress {
Some(progress::create_progress_bar(
args.inputs.len() as u64,
"Reading input metadata",
))
} else {
None
};
let mut all_info = Vec::new();
for input in &args.inputs {
if !input.exists() {
anyhow::bail!("Input file not found: {}", input.display());
}
let info = raster::read_raster_info(input)
.with_context(|| format!("Failed to read {}", input.display()))?;
all_info.push(info);
if let Some(ref pb) = pb {
pb.inc(1);
}
}
if let Some(ref pb) = pb {
pb.finish_with_message("Metadata loaded");
}
let first_bands = all_info[0].bands;
let first_data_type = all_info[0].data_type;
for (i, info) in all_info.iter().enumerate() {
if info.bands != first_bands {
anyhow::bail!(
"Input {} has {} bands, but first input has {} bands",
i,
info.bands,
first_bands
);
}
if info.data_type != first_data_type {
anyhow::bail!(
"Input {} has data type {:?}, but first input has {:?}",
i,
info.data_type,
first_data_type
);
}
if let Some(epsg) = args.epsg {
if info.epsg_code != Some(epsg) {
anyhow::bail!(
"Input {} has EPSG:{:?}, but target is EPSG:{}",
i,
info.epsg_code,
epsg
);
}
}
}
let (out_min_x, out_min_y, out_max_x, out_max_y, pixel_width, pixel_height) =
calculate_output_extent(&all_info).context("Failed to calculate output extent")?;
let out_width = ((out_max_x - out_min_x) / pixel_width).ceil() as u64;
let out_height = ((out_max_y - out_min_y) / pixel_height.abs()).ceil() as u64;
let output_geotransform = GeoTransform {
origin_x: out_min_x,
origin_y: out_max_y,
pixel_width,
pixel_height,
row_rotation: 0.0,
col_rotation: 0.0,
};
let pb = if args.progress {
Some(progress::create_progress_bar(
first_bands as u64,
"Merging bands",
))
} else {
None
};
let mut output_bands = Vec::with_capacity(first_bands as usize);
for band_idx in 0..first_bands {
if let Some(ref pb) = pb {
pb.set_message(format!("Merging band {}/{}", band_idx + 1, first_bands));
}
let merged_band = merge_band(
&args.inputs,
band_idx,
out_width,
out_height,
&output_geotransform,
&all_info,
args.no_data,
)
.with_context(|| format!("Failed to merge band {}", band_idx))?;
output_bands.push(merged_band);
if let Some(ref pb) = pb {
pb.inc(1);
}
}
if let Some(ref pb) = pb {
pb.finish_with_message("Merging complete");
}
let spinner = if args.progress {
Some(progress::create_spinner("Writing output file"))
} else {
None
};
raster::write_multi_band(
&args.output,
&output_bands,
Some(output_geotransform),
args.epsg.or(all_info[0].epsg_code),
args.output_no_data,
)
.context("Failed to write output raster")?;
if let Some(ref sp) = spinner {
sp.finish_with_message("Output written successfully");
}
let result = MergeResult {
output_file: args.output.display().to_string(),
input_count: args.inputs.len(),
width: out_width,
height: out_height,
bands: output_bands.len(),
};
match format {
OutputFormat::Json => {
let json =
serde_json::to_string_pretty(&result).context("Failed to serialize to JSON")?;
println!("{}", json);
}
OutputFormat::Text => {
println!("{}", style("Merge complete").green().bold());
println!(" Inputs: {} files", result.input_count);
println!(" Output: {}", result.output_file);
println!(" Dimensions: {} x {}", result.width, result.height);
println!(" Bands: {}", result.bands);
}
}
Ok(())
}
fn calculate_output_extent(
all_info: &[raster::RasterInfo],
) -> Result<(f64, f64, f64, f64, f64, f64)> {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
let mut pixel_widths = Vec::new();
let mut pixel_heights = Vec::new();
for info in all_info {
let gt = info
.geo_transform
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Input has no geotransform"))?;
let input_min_x = gt.origin_x;
let input_max_x = gt.origin_x + gt.pixel_width * info.width as f64;
let input_max_y = gt.origin_y;
let input_min_y = gt.origin_y + gt.pixel_height * info.height as f64;
min_x = min_x.min(input_min_x.min(input_max_x));
max_x = max_x.max(input_min_x.max(input_max_x));
min_y = min_y.min(input_min_y.min(input_max_y));
max_y = max_y.max(input_min_y.max(input_max_y));
pixel_widths.push(gt.pixel_width.abs());
pixel_heights.push(gt.pixel_height.abs());
}
let pixel_width = pixel_widths.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let pixel_height = -pixel_heights.iter().fold(f64::INFINITY, |a, &b| a.min(b));
Ok((min_x, min_y, max_x, max_y, pixel_width, pixel_height))
}
fn merge_band(
inputs: &[PathBuf],
band_idx: u32,
out_width: u64,
out_height: u64,
out_gt: &GeoTransform,
all_info: &[raster::RasterInfo],
no_data: Option<f64>,
) -> Result<RasterBuffer> {
let no_data_value = no_data.unwrap_or(0.0);
let mut output_data = vec![no_data_value; (out_width * out_height) as usize];
for (input_path, info) in inputs.iter().zip(all_info.iter()) {
let band_data = raster::read_band(input_path, band_idx)?;
let input_gt = info
.geo_transform
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Input has no geotransform"))?;
for in_y in 0..band_data.height() {
for in_x in 0..band_data.width() {
let geo_x = input_gt.origin_x + in_x as f64 * input_gt.pixel_width;
let geo_y = input_gt.origin_y + in_y as f64 * input_gt.pixel_height;
let out_x = ((geo_x - out_gt.origin_x) / out_gt.pixel_width).floor() as i64;
let out_y = ((geo_y - out_gt.origin_y) / out_gt.pixel_height).floor() as i64;
if out_x >= 0 && out_x < out_width as i64 && out_y >= 0 && out_y < out_height as i64
{
let out_idx = (out_y as u64 * out_width + out_x as u64) as usize;
let value = band_data.get_pixel(in_x, in_y).unwrap_or(no_data_value);
if output_data[out_idx] == no_data_value && value != no_data_value {
output_data[out_idx] = value;
}
}
}
}
}
use oxigdal_core::types::RasterDataType;
let byte_data: Vec<u8> = output_data
.iter()
.flat_map(|&val| val.to_le_bytes())
.collect();
RasterBuffer::new(
byte_data,
out_width,
out_height,
RasterDataType::Float64,
oxigdal_core::types::NoDataValue::from_float(no_data_value),
)
.map_err(|e| anyhow::anyhow!("Failed to create output buffer: {}", e))
}
#[cfg(test)]
mod tests {
#[test]
fn test_merge_requires_multiple_inputs() {
let _placeholder = 1;
}
}