celestial_pointing/commands/
apply.rs1use 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}