use crate::{formats, gpr, tools, user_metadata};
use clap::{ArgGroup, Parser, Subcommand};
use std::collections::BTreeMap;
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
pub struct Args {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Debug, Subcommand)]
pub enum Commands {
Process(ProcessArgs),
BatchProcess(BatchProcessArgs),
Info(InfoArgs),
Steps(StepsArgs),
Formats(FormatsArgs),
}
#[derive(Debug, clap::Args)]
#[command(group(
ArgGroup::new("step_choice")
.required(false)
.args(["steps", "default", "default_with_topo"]),
))]
pub struct ProcessArgs {
#[arg(required = true)]
pub inputs: Vec<PathBuf>,
#[arg(short, long, default_value = "0.168")]
pub velocity: f32,
#[arg(short, long)]
pub cor: Option<PathBuf>,
#[arg(short, long)]
pub dem: Option<PathBuf>,
#[arg(long)]
pub crs: Option<String>,
#[arg(short, long)]
pub track: Option<Option<PathBuf>>,
#[arg(long)]
pub default: bool,
#[arg(long = "default-with-topo")]
pub default_with_topo: bool,
#[arg(long)]
pub steps: Option<String>,
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(short, long)]
pub quiet: bool,
#[arg(short, long)]
pub render: Option<Option<PathBuf>>,
#[arg(long)]
pub no_export: bool,
#[arg(long)]
pub override_antenna_mhz: Option<f32>,
#[arg(long = "metadata", value_name = "KEY=VALUE", action = clap::ArgAction::Append)]
pub metadata: Vec<String>,
}
#[derive(Debug, clap::Args)]
#[command(group(
ArgGroup::new("step_choice")
.required(false)
.args(["steps", "default", "default_with_topo"]),
))]
pub struct BatchProcessArgs {
#[arg(required = true)]
pub inputs: Vec<PathBuf>,
#[arg(short, long, required = true)]
pub output: PathBuf,
#[arg(short, long, default_value = "0.168")]
pub velocity: f32,
#[arg(short, long)]
pub cor: Option<PathBuf>,
#[arg(short, long)]
pub dem: Option<PathBuf>,
#[arg(long)]
pub crs: Option<String>,
#[arg(short, long)]
pub track: Option<Option<PathBuf>>,
#[arg(long)]
pub default: bool,
#[arg(long = "default-with-topo")]
pub default_with_topo: bool,
#[arg(long)]
pub steps: Option<String>,
#[arg(short, long)]
pub quiet: bool,
#[arg(short, long)]
pub render: Option<Option<PathBuf>>,
#[arg(long)]
pub no_export: bool,
#[arg(long)]
pub merge: Option<String>,
#[arg(long)]
pub override_antenna_mhz: Option<f32>,
#[arg(long = "metadata", value_name = "KEY=VALUE", action = clap::ArgAction::Append)]
pub metadata: Vec<String>,
}
#[derive(Debug, clap::Args)]
pub struct InfoArgs {
#[arg(required = true)]
pub inputs: Vec<PathBuf>,
#[arg(long)]
pub json: bool,
#[arg(short, long, default_value = "0.168")]
pub velocity: f32,
#[arg(short, long)]
pub cor: Option<PathBuf>,
#[arg(short, long)]
pub dem: Option<PathBuf>,
#[arg(long)]
pub crs: Option<String>,
#[arg(long)]
pub override_antenna_mhz: Option<f32>,
}
#[derive(Debug, clap::Args)]
#[command(group(
ArgGroup::new("steps_mode")
.required(false)
.args(["describe_all", "describe", "default"]),
))]
pub struct StepsArgs {
#[arg(long = "describe-all")]
pub describe_all: bool,
#[arg(long)]
pub describe: Option<String>,
#[arg(long)]
pub default: bool,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, clap::Args)]
pub struct FormatsArgs {
#[arg(long)]
pub json: bool,
}
fn resolve_steps(
default: bool,
default_with_topo: bool,
steps: Option<&str>,
) -> Result<Vec<String>, String> {
let resolved_steps = if default_with_topo {
let mut profile = gpr::default_processing_profile();
profile.push("correct_topography".to_string());
profile
} else if default {
gpr::default_processing_profile()
} else if let Some(step_text) = steps {
tools::parse_step_list(step_text)?
} else {
vec![]
};
gpr::validate_steps(&resolved_steps)?;
Ok(resolved_steps)
}
fn optional_existing_dir(
value: &Option<Option<PathBuf>>,
label: &str,
) -> Result<Option<PathBuf>, String> {
match value {
None => Ok(None),
Some(None) => Ok(None),
Some(Some(path)) => {
if !path.is_dir() {
Err(format!(
"{label} must be an existing directory in batch mode: {}",
path.display()
))
} else {
Ok(Some(path.clone()))
}
}
}
}
#[cfg(feature = "cli")]
#[allow(dead_code)]
pub fn main(arguments: Args) -> i32 {
match run(arguments) {
Ok(()) => 0,
Err(message) => error(&message, 1),
}
}
pub fn run(arguments: Args) -> Result<(), String> {
match arguments.command {
Commands::Process(args) => process_command(&args),
Commands::BatchProcess(args) => batch_process_command(&args),
Commands::Info(args) => info_command(args),
Commands::Steps(args) => steps_command(args),
Commands::Formats(args) => formats_command(args),
}
}
fn process_command(args: &ProcessArgs) -> Result<(), String> {
let resolved_steps =
resolve_steps(args.default, args.default_with_topo, args.steps.as_deref())?;
let user_metadata = user_metadata::parse_cli_metadata(&args.metadata)?;
let params = gpr::RunParams {
filepaths: args.inputs.clone(),
output_path: args.output.clone(),
dem_path: args.dem.clone(),
cor_path: args.cor.clone(),
medium_velocity: args.velocity,
crs: args.crs.clone(),
quiet: args.quiet,
track_path: args.track.clone(),
steps: resolved_steps,
no_export: args.no_export,
render_path: args.render.clone(),
override_antenna_mhz: args.override_antenna_mhz,
user_metadata,
};
let result = gpr::run(params).map_err(|e| e.to_string())?;
if !args.quiet {
println!("{}", result.output_path.display());
}
Ok(())
}
fn batch_process_command(args: &BatchProcessArgs) -> Result<(), String> {
if !args.output.is_dir() {
return Err(format!(
"output must be an existing directory in batch mode: {}",
args.output.display()
));
}
let render_dir = optional_existing_dir(&args.render, "render")?;
let track_dir = optional_existing_dir(&args.track, "track")?;
let resolved_steps =
resolve_steps(args.default, args.default_with_topo, args.steps.as_deref())?;
let user_metadata = user_metadata::parse_cli_metadata(&args.metadata)?;
let params = gpr::BatchRunParams {
filepaths: args.inputs.clone(),
output_dir: args.output.clone(),
dem_path: args.dem.clone(),
cor_path: args.cor.clone(),
medium_velocity: args.velocity,
crs: args.crs.clone(),
quiet: args.quiet,
track_dir,
steps: resolved_steps,
no_export: args.no_export,
render_dir,
merge: args.merge.clone(),
override_antenna_mhz: args.override_antenna_mhz,
user_metadata,
};
let result = gpr::run_batch(params)?;
if !args.quiet {
for path in result.output_paths {
println!("{}", path.display());
}
}
Ok(())
}
fn info_command(args: InfoArgs) -> Result<(), String> {
let params = gpr::InfoParams {
filepaths: args.inputs,
dem_path: args.dem,
cor_path: args.cor,
medium_velocity: args.velocity,
crs: args.crs,
override_antenna_mhz: args.override_antenna_mhz,
};
let records = gpr::inspect(params).map_err(|e| format!("{e:?}"))?;
if args.json {
if records.len() == 1 {
println!(
"{}",
serde_json::to_string_pretty(&records[0]).map_err(|e| e.to_string())?
);
} else {
println!(
"{}",
serde_json::to_string_pretty(&records).map_err(|e| e.to_string())?
);
}
} else {
for (i, record) in records.iter().enumerate() {
if i > 0 {
println!();
}
print_info_record(record);
}
}
Ok(())
}
fn steps_command(args: StepsArgs) -> Result<(), String> {
let all_steps = gpr::all_available_steps();
if args.json {
if args.default {
println!(
"{}",
serde_json::to_string_pretty(&gpr::default_processing_profile())
.map_err(|e| e.to_string())?
);
return Ok(());
}
if let Some(step_name) = args.describe {
let mapping = step_mapping(Some(step_name), &all_steps)?;
println!(
"{}",
serde_json::to_string_pretty(&mapping).map_err(|e| e.to_string())?
);
return Ok(());
}
if args.describe_all {
let mapping = step_mapping(None, &all_steps)?;
println!(
"{}",
serde_json::to_string_pretty(&mapping).map_err(|e| e.to_string())?
);
return Ok(());
}
let names = all_steps
.into_iter()
.map(|(name, _)| name)
.collect::<Vec<String>>();
println!(
"{}",
serde_json::to_string_pretty(&names).map_err(|e| e.to_string())?
);
return Ok(());
}
if args.default {
for step in gpr::default_processing_profile() {
println!("{step}");
}
return Ok(());
}
if let Some(step_name) = args.describe {
let mapping = step_mapping(Some(step_name), &all_steps)?;
for (name, description) in mapping {
println!("{name}\n{}\n{description}\n", "-".repeat(name.len()));
}
return Ok(());
}
if args.describe_all {
for (name, description) in all_steps {
println!("{name}\n{}\n{description}\n", "-".repeat(name.len()));
}
return Ok(());
}
for (name, _) in all_steps {
println!("{name}");
}
Ok(())
}
fn formats_command(args: FormatsArgs) -> Result<(), String> {
let all_formats = formats::all_formats();
if args.json {
let payload = serde_json::json!({ "formats": all_formats });
println!(
"{}",
serde_json::to_string_pretty(&payload).map_err(|e| e.to_string())?
);
return Ok(());
}
for fmt in all_formats {
println!("{}", fmt.name);
println!("{}", "-".repeat(fmt.name.len()));
println!("{}", fmt.description);
println!("Read: {}", fmt.capabilities.read);
println!("Write: {}", fmt.capabilities.write);
println!(
"Files: header={} data={} coordinates={}",
fmt.files.header, fmt.files.data, fmt.files.coordinates
);
println!();
}
Ok(())
}
#[allow(dead_code)]
fn choose_steps(
default: bool,
default_with_topo: bool,
steps: Option<&str>,
) -> Result<Vec<String>, String> {
if default_with_topo {
let mut profile = gpr::default_processing_profile();
profile.push("correct_topography".to_string());
return Ok(profile);
}
if default {
return Ok(gpr::default_processing_profile());
}
match steps {
Some(step_text) => tools::parse_step_list(step_text),
None => Ok(vec![]),
}
}
fn step_mapping(
only_name: Option<String>,
all_steps: &[(String, String)],
) -> Result<BTreeMap<String, String>, String> {
let mut out = BTreeMap::<String, String>::new();
for (name, description) in all_steps {
if let Some(target) = &only_name {
if name != target {
continue;
}
}
out.insert(name.clone(), description.clone());
}
if let Some(target) = only_name {
if out.is_empty() {
return Err(format!("Unknown step: {target}"));
}
}
Ok(out)
}
fn print_info_record(record: &gpr::InfoRecord) {
println!("Input:\t\t{}", record.input);
println!(
"Format:\t\t{} ({})",
record.format.name, record.format.description
);
println!("Header:\t\t{}", record.related_files.header);
println!("Data:\t\t{}", record.related_files.data);
println!("Coordinates:\t{}", record.related_files.coordinates);
println!();
println!("Metadata");
println!("--------");
println!("Samples:\t{}", record.metadata.samples);
println!("Traces:\t\t{}", record.metadata.last_trace);
println!("Time window:\t{} ns", record.metadata.time_window_ns);
println!(
"Velocity:\t{} m/ns",
record.metadata.medium_velocity_m_per_ns
);
println!(
"Sampling freq:\t{} MHz",
record.metadata.sampling_frequency_mhz
);
println!("Antenna:\t{}", record.metadata.antenna_name);
println!("Antenna MHz:\t{}", record.metadata.antenna_mhz);
println!("Antenna sep:\t{} m", record.metadata.antenna_separation_m);
println!();
println!("Location");
println!("--------");
println!("Points:\t\t{}", record.location.n_points);
println!("CRS:\t\t{}", record.location.crs);
println!("Start:\t\t{}", record.location.start_time);
println!("Stop:\t\t{}", record.location.stop_time);
println!("Duration:\t{:.3} s", record.location.duration_s);
println!("Track length:\t{:.3} m", record.location.track_length_m);
println!(
"Altitude:\t{:.3} - {:.3} m",
record.location.altitude_min_m, record.location.altitude_max_m
);
println!(
"Centroid:\tE {:.3}, N {:.3}, Z {:.3}",
record.location.centroid.easting,
record.location.centroid.northing,
record.location.centroid.altitude
);
println!(
"Correction:\t{}{}",
record.location.correction.kind,
record
.location
.correction
.source
.as_ref()
.map(|s| format!(" ({s})"))
.unwrap_or_default()
);
}
#[cfg(feature = "cli")]
#[allow(dead_code)]
fn error(message: &str, code: i32) -> i32 {
eprintln!("{message}");
code
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn test_parse_process_command() {
let args = Args::parse_from([
"ridal",
"process",
"a.rad",
"b.rad",
"--default",
"-o",
"out.nc",
]);
match args.command {
Commands::Process(process) => {
assert_eq!(
process.inputs,
vec![PathBuf::from("a.rad"), PathBuf::from("b.rad")]
);
assert!(process.default);
assert_eq!(process.output, Some(PathBuf::from("out.nc")));
}
_ => panic!("Expected process command"),
}
}
#[test]
fn test_parse_info_command() {
let args = Args::parse_from(["ridal", "info", "line01.rad", "--json"]);
match args.command {
Commands::Info(info) => {
assert_eq!(info.inputs, vec![PathBuf::from("line01.rad")]);
assert!(info.json);
}
_ => panic!("Expected info command"),
}
}
#[test]
fn test_choose_steps_default_with_topo() {
let steps = choose_steps(false, true, None).unwrap();
assert!(steps.iter().any(|step| step == "correct_topography"));
}
}