use core::fmt;
use crate::attitude::{Attitude, Frd};
use crate::local::{Aer, Enu, Ned};
use crate::position::{Ecef, Eci, Geodetic};
use crate::projection::UtmUps;
use crate::quaternion::Quaternion;
use crate::vector::Vec3;
fn num(f: &mut fmt::Formatter<'_>, x: f64, decimals: Option<usize>) -> fmt::Result {
let x = x + 0.0; match decimals {
Some(d) => write!(f, "{x:.d$}"),
None => write!(f, "{x}"),
}
}
fn metres_decimals(f: &fmt::Formatter<'_>) -> Option<usize> {
f.precision().map(|p| p.saturating_sub(4))
}
fn fields(f: &mut fmt::Formatter<'_>, parts: [(&str, f64); 3], unit: &str) -> fmt::Result {
let decimals = f.precision();
for (i, (label, value)) in parts.into_iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{label} ")?;
num(f, value, decimals)?;
f.write_str(unit)?;
}
Ok(())
}
impl fmt::Display for Geodetic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let decimals = f.precision();
if f.alternate() {
for (value, plus, minus) in [(self.lat_deg, " N", " S"), (self.lon_deg, " E", " W")] {
num(f, value.abs(), decimals)?;
f.write_str("°")?;
if !value.is_nan() {
f.write_str(if value.is_sign_negative() && value != 0.0 {
minus
} else {
plus
})?;
}
f.write_str(", ")?;
}
} else {
num(f, self.lat_deg, decimals)?;
f.write_str("°, ")?;
num(f, self.lon_deg, decimals)?;
f.write_str("°, ")?;
}
num(f, self.height_m, metres_decimals(f))?;
f.write_str(" m")
}
}
impl fmt::Display for Aer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let decimals = f.precision();
f.write_str("az ")?;
num(f, self.azimuth_deg, decimals)?;
f.write_str("°, el ")?;
num(f, self.elevation_deg, decimals)?;
f.write_str("°, ")?;
num(f, self.range_m, metres_decimals(f))?;
f.write_str(" m")
}
}
impl fmt::Display for Ecef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fields(f, [("x", self.x), ("y", self.y), ("z", self.z)], " m")
}
}
impl fmt::Display for Eci {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fields(f, [("x", self.x), ("y", self.y), ("z", self.z)], " m")
}
}
impl fmt::Display for Enu {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fields(f, [("east", self.east), ("north", self.north), ("up", self.up)], " m")
}
}
impl fmt::Display for Ned {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fields(
f,
[("north", self.north), ("east", self.east), ("down", self.down)],
" m",
)
}
}
impl fmt::Display for Frd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fields(
f,
[("forward", self.forward), ("right", self.right), ("down", self.down)],
" m",
)
}
}
impl fmt::Display for Attitude {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fields(
f,
[
("yaw", self.yaw_deg),
("pitch", self.pitch_deg),
("roll", self.roll_deg),
],
"°",
)
}
}
impl fmt::Display for Vec3 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let decimals = f.precision();
f.write_str("(")?;
num(f, self.x, decimals)?;
f.write_str(", ")?;
num(f, self.y, decimals)?;
f.write_str(", ")?;
num(f, self.z, decimals)?;
f.write_str(")")
}
}
impl fmt::Display for Quaternion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let decimals = f.precision();
num(f, self.w, decimals)?;
for (value, unit) in [(self.x, "i"), (self.y, "j"), (self.z, "k")] {
let value = value + 0.0;
f.write_str(if value.is_sign_negative() { " - " } else { " + " })?;
num(f, value.abs(), decimals)?;
f.write_str(unit)?;
}
Ok(())
}
}
impl fmt::Display for UtmUps {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let decimals = f.precision();
if self.zone != 0 {
write!(f, "{}", self.zone)?;
}
f.write_str(if self.north { "n " } else { "s " })?;
num(f, self.easting_m, decimals)?;
f.write_str(" ")?;
num(f, self.northing_m, decimals)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_is_lossless_and_precision_rounds() {
let g = Geodetic::new(-33.87, 151.21, 20.0);
assert_eq!(format!("{g}"), "-33.87°, 151.21°, 20 m");
assert_eq!(format!("{g:.5}"), "-33.87000°, 151.21000°, 20.0 m");
assert_eq!(format!("{g:.7}"), "-33.8700000°, 151.2100000°, 20.000 m");
assert_eq!(format!("{g:.2}"), "-33.87°, 151.21°, 20 m");
assert_eq!(format!("{g:#}"), "33.87° S, 151.21° E, 20 m");
assert_eq!(
format!("{:#.1}", Geodetic::new(51.5, -0.12, 30.0)),
"51.5° N, 0.1° W, 30 m"
);
let a = Aer::new(35.6, 7.3, 19_254.4);
assert_eq!(format!("{a:.1}"), "az 35.6°, el 7.3°, 19254 m");
assert_eq!(format!("{a}"), "az 35.6°, el 7.3°, 19254.4 m");
}
#[test]
fn every_type() {
assert_eq!(format!("{}", Ecef::new(1.5, -2.0, 3.0)), "x 1.5 m, y -2 m, z 3 m");
assert_eq!(format!("{:.1}", Eci::new(1.0, 2.0, 3.0)), "x 1.0 m, y 2.0 m, z 3.0 m");
assert_eq!(
format!("{}", Enu::new(0.0, 11_131.0, 2_960.0)),
"east 0 m, north 11131 m, up 2960 m"
);
assert_eq!(
format!("{}", Ned::new(1.0, 2.0, -3.0)),
"north 1 m, east 2 m, down -3 m"
);
assert_eq!(
format!("{}", Frd::new(500.0, 0.0, 30.0)),
"forward 500 m, right 0 m, down 30 m"
);
assert_eq!(
format!("{}", Attitude::new(90.0, 10.0, 0.0)),
"yaw 90°, pitch 10°, roll 0°"
);
assert_eq!(format!("{:.2}", Vec3::new(1.0, 2.0, 3.0)), "(1.00, 2.00, 3.00)");
assert_eq!(
format!("{:.1}", Quaternion::new(0.5, -0.5, 0.5, -0.5)),
"0.5 - 0.5i + 0.5j - 0.5k"
);
assert_eq!(
format!("{:.3}", UtmUps::new(56, false, 334_435.706_14, 6_250_816.397_8)),
"56s 334435.706 6250816.398"
);
assert_eq!(format!("{}", UtmUps::new(0, true, 2e6, 2e6)), "n 2000000 2000000");
}
#[test]
fn zero_nan_and_infinity() {
assert_eq!(format!("{}", Geodetic::new(-0.0, -0.0, -0.0)), "0°, 0°, 0 m");
assert_eq!(format!("{:#}", Geodetic::new(-0.0, 0.0, 0.0)), "0° N, 0° E, 0 m");
assert_eq!(format!("{:#}", Geodetic::new(f64::NAN, -1.0, 0.0)), "NaN°, 1° W, 0 m");
assert_eq!(
format!("{}", Ecef::new(f64::INFINITY, f64::NEG_INFINITY, 0.0)),
"x inf m, y -inf m, z 0 m"
);
assert_eq!(format!("{}", Quaternion::new(1.0, -0.0, 0.0, 0.0)), "1 + 0i + 0j + 0k");
}
#[test]
fn plain_form_round_trips_exactly() {
let mut s: u64 = 0x9e37_79b9_7f4a_7c15;
let mut u = || {
s ^= s >> 12;
s ^= s << 25;
s ^= s >> 27;
let bits = s.wrapping_mul(0x2545_f491_4f6c_dd1d);
let m = (bits >> 11) as f64 / (1u64 << 53) as f64;
let e = (bits % 41) as i32 - 20;
if bits & 1 == 0 {
m * 10f64.powi(e)
} else {
-m * 10f64.powi(e)
}
};
for _ in 0..10_000 {
let g = Geodetic::new(u(), u(), u());
let text = format!("{g}");
let numbers: Vec<f64> = text
.split(['°', ',', 'm'])
.map(str::trim)
.filter(|t| !t.is_empty())
.map(|t| t.parse().unwrap())
.collect();
assert_eq!(numbers, [g.lat_deg + 0.0, g.lon_deg + 0.0, g.height_m + 0.0], "{text}");
}
}
}