use clap::Parser;
use geodesy::authoring::Jacobian;
use geodesy::prelude::*;
use log::{info, trace}; use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use std::time;
#[derive(Parser, Debug)]
#[command(name = "kp")]
#[command(author, version, about = "KP: The Rust Geodesy 'Coordinate Processing' program", long_about = None)]
struct Cli {
operation: String,
#[clap(long = "inv")]
inverse: bool,
#[clap(long)]
factors: Option<String>,
#[clap(short = 'z', long)]
height: Option<f64>,
#[clap(short = 't', long)]
time: Option<f64>,
#[clap(short = 'd', long)]
decimals: Option<usize>,
#[clap(short = 'D', long)]
dimension: Option<usize>,
#[clap(long)]
debug: bool,
#[clap(short, long)]
roundtrip: bool,
#[clap(short, long)]
echo: bool,
#[clap(flatten)]
verbose: clap_verbosity_flag::Verbosity,
#[clap(short, long)]
_output: Option<PathBuf>,
args: Vec<String>,
}
fn main() -> Result<(), anyhow::Error> {
let mut options = Cli::parse();
env_logger::Builder::new()
.filter_level(options.verbose.log_level_filter())
.init();
log::trace!("This is KP");
if options.debug {
eprintln!("args: {:?}", options.args);
if let Some(dir) = dirs::data_local_dir() {
eprintln!("data_local_dir: {}", dir.to_str().unwrap_or_default());
}
eprintln!("options: {options:#?}");
}
if options.args.is_empty() {
options.args.push("-".to_string());
}
let start = time::Instant::now();
let mut ctx = Plain::new();
let (lap, duration) = (start.elapsed(), start.elapsed());
trace!("Created context in: {lap:?}");
let op = ctx.op(&options.operation)?;
let (lap, duration) = (start.elapsed() - duration, start.elapsed());
trace!("Created operation in: {lap:?}");
trace!("{op:#?}");
let mut number_of_operands_read = 0_usize;
let mut number_of_operands_succesfully_transformed = 0_usize;
let mut number_of_dimensions_in_input = 0;
let mut operands = Vec::new();
for arg in &options.args {
let reader: Box<dyn BufRead> = if arg == "-" {
Box::new(BufReader::new(std::io::stdin().lock()))
} else {
Box::new(BufReader::new(File::open(arg)?))
};
for line in reader.lines() {
let line = line?;
let line = line.trim();
let mut args: Vec<&str> = line.split_whitespace().collect();
for (n, arg) in args.iter().enumerate() {
if arg.starts_with('#') {
args.truncate(n);
break;
}
}
let n = args.len();
if n < 1 {
continue;
}
number_of_dimensions_in_input = number_of_dimensions_in_input.max(n);
args.extend(&(["0", "0", "0", "NaN", "0"][args.len()..]));
let mut b: Vec<f64> = vec![];
for e in args {
b.push(angular::parse_sexagesimal(e));
}
b[2] = options.height.unwrap_or(b[2]);
b[3] = options.time.unwrap_or(b[3]);
let coord = Coor4D([b[0], b[1], b[2], b[3]]);
number_of_operands_read += 1;
operands.push(coord);
if operands.len() == 100_000 {
number_of_operands_succesfully_transformed += transform(
&options,
op,
number_of_dimensions_in_input,
&mut operands,
&ctx,
)?;
operands.clear();
}
}
}
let (lap, duration) = (start.elapsed() - duration, start.elapsed());
let each = if number_of_operands_read == 0 {
std::time::Duration::new(0, 0)
} else {
lap / number_of_operands_read.try_into().unwrap_or(u32::MAX)
};
trace!("Read {number_of_operands_read} coordinates in {lap:?} ({each:?} each)");
if !operands.is_empty() {
number_of_operands_succesfully_transformed += transform(
&options,
op,
number_of_dimensions_in_input,
&mut operands,
&ctx,
)?;
}
let lap = start.elapsed() - duration;
let each = if number_of_operands_read == 0 || number_of_operands_succesfully_transformed == 0 {
std::time::Duration::new(0, 0)
} else {
lap / number_of_operands_succesfully_transformed
.try_into()
.unwrap_or(u32::MAX)
};
let total = start.elapsed();
let total_each = if number_of_operands_succesfully_transformed == 0 {
std::time::Duration::new(0, 0)
} else {
total
/ number_of_operands_succesfully_transformed
.try_into()
.unwrap_or(u32::MAX)
};
trace!(
"Transformed {number_of_operands_succesfully_transformed} coordinates in {lap:?} ({each:?} each)"
);
info!(
"Read {number_of_operands_read} coordinates and succesfully transformed {number_of_operands_succesfully_transformed} in {total:?} ({total_each:?} each)"
);
Ok(())
}
fn transform(
options: &Cli,
op: OpHandle,
number_of_dimensions_in_input: usize,
operands: &mut Vec<Coor4D>,
ctx: &Plain,
) -> Result<usize, geodesy::Error> {
let start = time::Instant::now();
let duration = time::Duration::new(0, 1);
let lap = duration;
let output_dimension = options.dimension.unwrap_or(number_of_dimensions_in_input);
let mut buffer = Vec::new();
if options.roundtrip {
buffer.clone_from(operands);
}
let factors = options.factors.is_some();
let ellps = if factors {
Ellipsoid::named(options.factors.as_ref().unwrap())?
} else {
Ellipsoid::named("GRS80")?
};
let (lap, duration) = (start.elapsed() - duration, duration + lap);
trace!(" Prepared for transformation in : {lap:?}");
let mut n = if options.inverse {
ctx.apply(op, Inv, operands)?
} else {
ctx.apply(op, Fwd, operands)?
};
let m = if options.roundtrip {
let m = if options.inverse {
ctx.apply(op, Fwd, operands)?
} else {
ctx.apply(op, Inv, operands)?
};
if m != n {
return Err(Error::General(
"Roundtrip - mismatch between number of Fwd and Inv results",
));
}
for index in 0..n {
operands[index] = operands[index] - buffer[index];
}
let lap = start.elapsed() - duration;
let each = lap / m.try_into().unwrap_or(u32::MAX).max(1);
trace!(" Roundtripped {m} coordinates in: {lap:?} ({each:?} each)");
m
} else {
let lap = start.elapsed() - duration;
let each = lap / n.try_into().unwrap_or(u32::MAX).max(1);
trace!(" Transformed {n} coordinates in: {lap:?} ({each:?} each)");
n
};
let duration = start.elapsed();
n = n.min(m);
let decimals = options
.decimals
.unwrap_or(if operands[0][0] > 1000. { 5 } else { 10 });
let stdout = std::io::stdout();
let mut w = std::io::BufWriter::new(stdout.lock());
for (index, coord) in operands.iter().enumerate() {
match output_dimension {
0 | 4 => write!(
w,
"{1:.0$} {2:.0$} {3:.0$} {4:.0$} ",
decimals, coord[0], coord[1], coord[2], coord[3]
)?,
1 => write!(w, "{1:.0$} ", decimals, coord[0])?,
2 => write!(w, "{1:.0$} {2:.0$} ", decimals, coord[0], coord[1])?,
3 => write!(
w,
"{1:.0$} {2:.0$} {3:.0$} ",
decimals, coord[0], coord[1], coord[2]
)?,
_ => write!(
w,
"{1:.0$} {2:.0$} {3:.0$} {4:.0$} ",
decimals, coord[0], coord[1], coord[2], coord[3]
)?,
}
if factors {
let scale = [1., 1.];
let swap = [true, true];
let at = Coor2D([buffer[index][0], buffer[index][1]]);
let f = Jacobian::new(ctx, op, scale, swap, ellps, at)?.factors();
if options.verbose.is_present() {
writeln!(w)?;
writeln!(w, "{f:#?}")?;
} else {
writeln!(
w,
" < {0:.10} {1:.10} {2:.10} | {3:.5} {4:.5} {5:.5} >",
f.meridional_scale,
f.parallel_scale,
f.areal_scale,
f.angular_distortion,
f.meridian_parallel_angle,
f.meridian_convergence
)?;
}
} else {
writeln!(w)?;
}
if index < 10 {
w.flush()?;
}
}
let lap = start.elapsed() - duration;
let each = lap / n.try_into().unwrap_or(u32::MAX).max(1);
let total = start.elapsed();
let total_each = total / n.try_into().unwrap_or(u32::MAX).max(1);
trace!(" Wrote {n} coordinates to stdout in: {lap:?} ({each:?} each)");
trace!(" Handled {n} coordinates in {total:?} ({total_each:?} each)");
Ok(n)
}
#[cfg(test)]
mod tests {
use super::*;
use float_eq::assert_float_eq;
fn some_basic_coordinates() -> [Coor4D; 2] {
let copenhagen = Coor4D::raw(55., 12., 0., 0.);
let stockholm = Coor4D::raw(59., 18., 0., 0.);
[copenhagen, stockholm]
}
#[test]
fn introspection() -> Result<(), Error> {
let mut ctx = Minimal::new();
let op = ctx.op("geo:in | utm zone=32 | neu:out")?;
let mut data = some_basic_coordinates();
let expected = [6098907.825005002, 691875.6321396609, 0., 0.];
ctx.apply(op, Fwd, &mut data)?;
assert_float_eq!(data[0].0, expected, abs_all <= 1e-9);
let steps = ctx.steps(op)?;
assert_eq!(steps.len(), 3);
assert_eq!(steps[0], "geo:in");
assert_eq!(steps[1], "utm zone=32");
assert_eq!(steps[2], "neu:out");
assert_eq!("adapt", ctx.params(op, 0)?.name);
assert_eq!("adapt", ctx.params(op, 2)?.name);
assert_eq!("utm", ctx.params(op, 1)?.name);
let params = ctx.params(op, 1)?;
let ellps = params.ellps(0);
assert_eq!(ellps.semimajor_axis(), 6378137.);
assert_eq!(0., ctx.params(op, 1)?.lat(0));
let zone = ctx.params(op, 1)?.natural("zone")?;
assert_eq!(zone, 32);
Ok(())
}
}