rust_jpl/
ephemeris.rs

1//! Ephemeris data structures and position calculations
2
3use std::fs::File;
4use std::io::Read;
5use std::str::FromStr;
6
7use crate::config::AppConfig;
8use crate::time::JulianDate;
9use crate::{Error, Result};
10
11/// 3D position vector
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct Position {
14    /// X coordinate (AU)
15    pub x: f64,
16    /// Y coordinate (AU)
17    pub y: f64,
18    /// Z coordinate (AU)
19    pub z: f64,
20}
21
22impl Position {
23    /// Create a new position
24    pub fn new(x: f64, y: f64, z: f64) -> Self {
25        Self { x, y, z }
26    }
27
28    /// Calculate distance from origin
29    pub fn distance(&self) -> f64 {
30        (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
31    }
32}
33
34/// Represents a celestial body in the ephemeris
35#[derive(Debug, Clone)]
36pub struct SpaceObject {
37    /// Whether this object is active
38    pub active: bool,
39    /// Header data for this object
40    header_data: Vec<i32>,
41    /// Name of the object
42    pub name: String,
43    /// Length of coefficients for this object
44    pub coefficient_length: i32,
45}
46
47impl SpaceObject {
48    fn new(name: String, active: bool) -> Self {
49        Self {
50            active,
51            header_data: Vec::new(),
52            name,
53            coefficient_length: 0,
54        }
55    }
56}
57
58/// Main ephemeris structure
59pub struct Ephemeris {
60    config: AppConfig,
61    bodies: Vec<SpaceObject>,
62    start_year: i32,
63    end_year: i32,
64    ncoeff: i32,
65    emrat: f64,
66    interval: i32,
67    julian_start: f64,
68    julian_end: f64,
69}
70
71impl Ephemeris {
72    /// Create a new ephemeris instance
73    ///
74    /// # Arguments
75    /// * `config_path` - Path to the config.toml file
76    ///
77    /// # Example
78    /// ```ignore
79    /// use rust_jpl::Ephemeris;
80    /// let mut eph = Ephemeris::new("config.toml")?;
81    /// # Ok::<(), rust_jpl::Error>(())
82    /// ```
83    pub fn new(config_path: &str) -> Result<Self> {
84        let config = AppConfig::new(config_path)?;
85        let mut eph = Self {
86            config,
87            bodies: Vec::new(),
88            start_year: 0,
89            end_year: 0,
90            ncoeff: 0,
91            emrat: 0.0,
92            interval: 0,
93            julian_start: 0.0,
94            julian_end: 0.0,
95        };
96
97        eph.initialize()?;
98        Ok(eph)
99    }
100
101    /// Initialize the ephemeris by reading configuration files
102    fn initialize(&mut self) -> Result<()> {
103        self.read_init_data()?;
104        self.read_header()?;
105        self.calculate_coefficient_lengths();
106        Ok(())
107    }
108
109    /// Read initial data file
110    fn read_init_data(&mut self) -> Result<()> {
111        let path = &self.config.initial_data_dat;
112        let mut indat = File::open(path)?;
113        let mut buffer = String::new();
114        indat.read_to_string(&mut buffer)?;
115        let lines: Vec<&str> = buffer.lines().collect();
116        let mut i = 0;
117
118        while i < lines.len() {
119            let line = lines[i];
120            i += 1;
121
122            if line == "BODIES:" {
123                while i < lines.len() {
124                    let body_line = lines[i];
125                    i += 1;
126                    if body_line == "DATE:" {
127                        break;
128                    }
129                    let mut parts = body_line.split_whitespace();
130                    if let Some(name) = parts.next() {
131                        let mut so = SpaceObject::new(name.to_string(), false);
132                        if let Some(state_str) = parts.next() {
133                            so.active = bool::from_str(state_str).unwrap_or(false);
134                        }
135                        self.bodies.push(so);
136                    }
137                }
138            }
139
140            if line == "DATE:" {
141                if i < lines.len() {
142                    let start_year_str = lines[i];
143                    i += 1;
144                    if start_year_str.starts_with("Start_year") {
145                        self.start_year = start_year_str
146                            .split_whitespace()
147                            .last()
148                            .and_then(|s| i32::from_str(s).ok())
149                            .unwrap_or(0);
150                    }
151                }
152
153                if i < lines.len() {
154                    let end_year_str = lines[i];
155                    i += 1;
156                    if end_year_str.starts_with("End_year") {
157                        self.end_year = end_year_str
158                            .split_whitespace()
159                            .last()
160                            .and_then(|s| i32::from_str(s).ok())
161                            .unwrap_or(0);
162                    }
163                }
164            }
165        }
166
167        Ok(())
168    }
169
170    /// Read header file
171    fn read_header(&mut self) -> Result<()> {
172        let path = &self.config.header_441;
173        let mut header = File::open(path)?;
174        let mut buffer = String::new();
175        header.read_to_string(&mut buffer)?;
176        let lines: Vec<&str> = buffer.lines().collect();
177        let mut number_emrat = 0;
178
179        for line in lines {
180            let mut parts = line.split_whitespace();
181            if let Some(key) = parts.next() {
182                match key {
183                    "NCOEFF=" => {
184                        self.ncoeff = parts
185                            .next()
186                            .and_then(|s| i32::from_str(s).ok())
187                            .unwrap_or(0);
188                    }
189                    "GROUP" => {
190                        if let Some(group_number_str) = parts.next() {
191                            match group_number_str {
192                                "1030" => {
193                                    if let Some(start_str) = parts.next() {
194                                        self.julian_start = f64::from_str(start_str).unwrap_or(0.0);
195                                    }
196                                    if let Some(end_str) = parts.next() {
197                                        self.julian_end = f64::from_str(end_str).unwrap_or(0.0);
198                                    }
199                                    if let Some(interval_str) = parts.next() {
200                                        self.interval = i32::from_str(interval_str).unwrap_or(0);
201                                    }
202                                }
203                                "1040" => {
204                                    if let Some(size_str) = parts.next() {
205                                        let size = i32::from_str(size_str).unwrap_or(0);
206                                        for i in 0..size {
207                                            if let Some(tmp) = parts.next() {
208                                                if tmp == "EMRAT" {
209                                                    number_emrat = i;
210                                                    break;
211                                                }
212                                            }
213                                        }
214                                    }
215                                }
216                                "1041" => {
217                                    for _ in 0..number_emrat {
218                                        parts.next();
219                                    }
220                                    if let Some(emrat_str) = parts.next() {
221                                        let mut emrat = emrat_str.to_string();
222                                        let len = emrat.len();
223                                        emrat.truncate(len - 4);
224                                        emrat.push('E');
225                                        self.emrat = f64::from_str(&emrat).unwrap_or(0.0);
226                                    }
227                                }
228                                "1050" => {
229                                    for _ in 0..3 {
230                                        for body in &mut self.bodies {
231                                            if let Some(buf_str) = parts.next() {
232                                                let buf = i32::from_str(buf_str).unwrap_or(0);
233                                                body.header_data.push(buf);
234                                            }
235                                        }
236                                    }
237                                }
238                                _ => {}
239                            }
240                        }
241                    }
242                    _ => {}
243                }
244            }
245        }
246
247        Ok(())
248    }
249
250    /// Calculate coefficient lengths for each body
251    fn calculate_coefficient_lengths(&mut self) {
252        let len = self.bodies.len();
253        for i in 0..len - 1 {
254            if !self.bodies[i].header_data.is_empty() && !self.bodies[i + 1].header_data.is_empty()
255            {
256                self.bodies[i].coefficient_length =
257                    self.bodies[i + 1].header_data[0] - self.bodies[i].header_data[0];
258            }
259        }
260        if !self.bodies.is_empty() && !self.bodies[len - 1].header_data.is_empty() {
261            self.bodies[len - 1].coefficient_length =
262                self.ncoeff - self.bodies[len - 1].header_data[0];
263        }
264    }
265
266    /// Get the position of a celestial body at a given Julian date
267    ///
268    /// # Arguments
269    /// * `body_name` - Name of the celestial body (e.g., "Earth", "Moon", "Sun", "Mars")
270    /// * `jd` - Julian date
271    ///
272    /// # Returns
273    /// Position in AU (Astronomical Units)
274    ///
275    /// # Example
276    /// ```ignore
277    /// use rust_jpl::{Ephemeris, JulianDate};
278    /// let mut eph = Ephemeris::new("config.toml")?;
279    /// let jd = JulianDate::from_calendar(2024, 1, 15, 12, 0, 0.0)?;
280    /// let position = eph.get_position("Earth", jd)?;
281    /// # Ok::<(), rust_jpl::Error>(())
282    /// ```
283    pub fn get_position(&self, body_name: &str, jd: JulianDate) -> Result<Position> {
284        // Validate Julian date is within range
285        if jd.jd < self.julian_start || jd.jd > self.julian_end {
286            return Err(Error::Ephemeris(format!(
287                "Julian date {} is outside valid range [{}, {}]",
288                jd.jd, self.julian_start, self.julian_end
289            )));
290        }
291
292        // Find the body
293        let body = self
294            .bodies
295            .iter()
296            .find(|b| {
297                b.name.eq_ignore_ascii_case(body_name)
298                    || b.name.replace("_", "").eq_ignore_ascii_case(body_name)
299            })
300            .ok_or_else(|| {
301                Error::Ephemeris(format!(
302                    "Body '{}' not found. Available bodies: {}",
303                    body_name,
304                    self.bodies
305                        .iter()
306                        .map(|b| b.name.clone())
307                        .collect::<Vec<_>>()
308                        .join(", ")
309                ))
310            })?;
311
312        if !body.active {
313            return Err(Error::Ephemeris(format!(
314                "Body '{}' is not active in this ephemeris",
315                body_name
316            )));
317        }
318
319        // For now, return a placeholder position
320        // In a full implementation, this would read the binary ephemeris file
321        // and interpolate the Chebyshev coefficients
322        Ok(Position::new(0.0, 0.0, 0.0))
323    }
324
325    /// Get all available celestial bodies
326    pub fn get_bodies(&self) -> Vec<&SpaceObject> {
327        self.bodies.iter().collect()
328    }
329
330    /// Get the valid date range for this ephemeris
331    pub fn get_date_range(&self) -> (f64, f64) {
332        (self.julian_start, self.julian_end)
333    }
334
335    /// Get ephemeris metadata
336    pub fn get_metadata(&self) -> EphemerisMetadata {
337        EphemerisMetadata {
338            start_year: self.start_year,
339            end_year: self.end_year,
340            julian_start: self.julian_start,
341            julian_end: self.julian_end,
342            interval_days: self.interval as f64,
343            earth_moon_ratio: self.emrat,
344            number_of_coefficients: self.ncoeff,
345        }
346    }
347}
348
349/// Metadata about the ephemeris
350#[derive(Debug, Clone)]
351pub struct EphemerisMetadata {
352    pub start_year: i32,
353    pub end_year: i32,
354    pub julian_start: f64,
355    pub julian_end: f64,
356    pub interval_days: f64,
357    pub earth_moon_ratio: f64,
358    pub number_of_coefficients: i32,
359}