Skip to main content

celestial_pointing/commands/
apply.rs

1use super::{Command, CommandOutput};
2use crate::error::Result;
3use crate::observation::PierSide;
4use crate::parser::parse_coordinates;
5use crate::session::Session;
6use celestial_core::Angle;
7
8pub struct Apply;
9
10impl Command for Apply {
11    fn name(&self) -> &str {
12        "APPLY"
13    }
14    fn description(&self) -> &str {
15        "Compute commanded position for target"
16    }
17
18    fn execute(&self, session: &mut Session, args: &[&str]) -> Result<CommandOutput> {
19        let (ra, dec) = parse_coordinates(args)?;
20        let lst = session.current_lst()?;
21        let lat = Angle::from_radians(session.latitude());
22        let ha = lst - ra;
23        let pier = pier_from_ha(ha);
24        let (cmd_ra, cmd_dec) = session.model.target_to_command(ra, dec, lst, lat, pier);
25        let delta_ra = (cmd_ra - ra).wrapped();
26        let delta_dec = (cmd_dec - dec).wrapped();
27        Ok(CommandOutput::Text(format_result(
28            ra, dec, cmd_ra, cmd_dec, delta_ra, delta_dec,
29        )))
30    }
31}
32
33fn pier_from_ha(ha: Angle) -> PierSide {
34    if ha.radians() >= 0.0 {
35        PierSide::East
36    } else {
37        PierSide::West
38    }
39}
40
41fn format_result(
42    ra: Angle,
43    dec: Angle,
44    cmd_ra: Angle,
45    cmd_dec: Angle,
46    dra: Angle,
47    ddec: Angle,
48) -> String {
49    format!(
50        "Target:   {}  {}\nCommand:  {}  {}\n  \u{0394}RA:  {:+.2}s\n  \u{0394}Dec: {:+.1}\"",
51        format_ra(ra),
52        format_dec(dec),
53        format_ra(cmd_ra),
54        format_dec(cmd_dec),
55        dra.arcseconds() / 15.0,
56        ddec.arcseconds(),
57    )
58}
59
60fn format_ra(a: Angle) -> String {
61    let total_h = a.normalized().hours();
62    let h = libm::floor(total_h) as u32;
63    let rem = (total_h - h as f64) * 60.0;
64    let m = libm::floor(rem) as u32;
65    let s = (rem - m as f64) * 60.0;
66    format!("{:02}h {:02}m {:05.2}s", h, m, s)
67}
68
69fn format_dec(a: Angle) -> String {
70    let deg = a.degrees();
71    let sign = if deg < 0.0 { '-' } else { '+' };
72    let abs = deg.abs();
73    let d = libm::floor(abs) as u32;
74    let rem = (abs - d as f64) * 60.0;
75    let m = libm::floor(rem) as u32;
76    let s = (rem - m as f64) * 60.0;
77    format!("{}{:02}\u{00b0} {:02}' {:04.1}\"", sign, d, m, s)
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::session::Session;
84
85    #[test]
86    fn empty_model_returns_target_equals_command() {
87        let mut session = Session::new();
88        session.lst_override = Some(Angle::from_hours(14.0));
89        let args = vec!["12", "30", "00", "+45", "00", "00"];
90        let result = Apply.execute(&mut session, &args).unwrap();
91        match result {
92            CommandOutput::Text(s) => {
93                assert!(s.contains("Target:"));
94                assert!(s.contains("Command:"));
95                assert!(s.contains("\u{0394}RA:  +0.00s"));
96                assert!(s.contains("\u{0394}Dec: +0.0\""));
97            }
98            _ => panic!("expected Text output"),
99        }
100    }
101
102    #[test]
103    fn pier_east_when_ha_positive() {
104        let ha = Angle::from_hours(2.0);
105        assert_eq!(pier_from_ha(ha), PierSide::East);
106    }
107
108    #[test]
109    fn pier_west_when_ha_negative() {
110        let ha = Angle::from_hours(-2.0);
111        assert_eq!(pier_from_ha(ha), PierSide::West);
112    }
113
114    #[test]
115    fn pier_east_when_ha_zero() {
116        let ha = Angle::from_hours(0.0);
117        assert_eq!(pier_from_ha(ha), PierSide::East);
118    }
119
120    #[test]
121    fn apply_requires_lst() {
122        let mut session = Session::new();
123        let args = vec!["12.5", "45.0"];
124        let result = Apply.execute(&mut session, &args);
125        assert!(result.is_err());
126    }
127
128    #[test]
129    fn apply_with_model_produces_nonzero_deltas() {
130        let mut session = Session::new();
131        session.lst_override = Some(Angle::from_hours(14.0));
132        session.model.add_term("IH").unwrap();
133        session.model.set_coefficients(&[30.0]).unwrap();
134        let args = vec!["12.5", "45.0"];
135        let result = Apply.execute(&mut session, &args).unwrap();
136        match result {
137            CommandOutput::Text(s) => {
138                assert!(!s.contains("\u{0394}RA:  +0.00s"));
139            }
140            _ => panic!("expected Text output"),
141        }
142    }
143
144    #[test]
145    fn apply_decimal_args() {
146        let mut session = Session::new();
147        session.lst_override = Some(Angle::from_hours(14.0));
148        let args = vec!["12.5", "45.0"];
149        let result = Apply.execute(&mut session, &args);
150        assert!(result.is_ok());
151    }
152
153    #[test]
154    fn format_ra_zero() {
155        let s = format_ra(Angle::from_hours(0.0));
156        assert_eq!(s, "00h 00m 00.00s");
157    }
158
159    #[test]
160    fn format_ra_12h() {
161        let s = format_ra(Angle::from_hours(12.5));
162        assert_eq!(s, "12h 30m 00.00s");
163    }
164
165    #[test]
166    fn format_dec_positive() {
167        let s = format_dec(Angle::from_degrees(45.5));
168        assert_eq!(s, "+45\u{00b0} 30' 00.0\"");
169    }
170
171    #[test]
172    fn format_dec_negative() {
173        let s = format_dec(Angle::from_degrees(-30.25));
174        assert_eq!(s, "-30\u{00b0} 15' 00.0\"");
175    }
176}