swiss_eph/
safe.rs

1//! Safe, idiomatic Rust wrapper for Swiss Ephemeris
2//!
3//! This module provides a high-level, safe API on top of the raw FFI bindings.
4
5use crate::*;
6use std::ffi::{CStr, CString};
7use std::os::raw::c_int;
8
9/// Error returned by Swiss Ephemeris calculations
10#[derive(Debug, Clone)]
11#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen(getter_with_clone))]
12pub struct SwissEphError {
13    /// Error message from the library
14    pub message: String,
15    /// Return code
16    pub code: i32,
17}
18
19
20impl std::fmt::Display for SwissEphError {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        write!(f, "SwissEph error ({}): {}", self.code, self.message)
23    }
24}
25
26impl std::error::Error for SwissEphError {}
27
28/// Result type for Swiss Ephemeris operations
29pub type Result<T> = std::result::Result<T, SwissEphError>;
30
31/// Planetary position result
32#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
33#[derive(Debug, Clone, Copy)]
34pub struct Position {
35    /// Ecliptic longitude in degrees
36    pub longitude: f64,
37    /// Ecliptic latitude in degrees  
38    pub latitude: f64,
39    /// Distance (AU for planets, Earth radii for Moon)
40    pub distance: f64,
41    /// Longitude speed (degrees/day)
42    pub longitude_speed: f64,
43    /// Latitude speed (degrees/day)
44    pub latitude_speed: f64,
45    /// Distance speed (AU/day)
46    pub distance_speed: f64,
47}
48
49/// House cusps and angles
50#[derive(Debug, Clone)]
51pub struct HouseCusps {
52    /// House cusp positions (12 cusps, indices 0-11)
53    pub cusps: [f64; 12],
54    /// Ascendant
55    pub ascendant: f64,
56    /// Midheaven (MC)
57    pub mc: f64,
58    /// ARMC (sidereal time in degrees)
59    pub armc: f64,
60    /// Vertex
61    pub vertex: f64,
62    /// Equatorial Ascendant
63    pub equatorial_ascendant: f64,
64    /// Co-Ascendant (Koch)
65    pub co_ascendant_koch: f64,
66    /// Co-Ascendant (Munkasey)
67    pub co_ascendant_munkasey: f64,
68    /// Polar Ascendant
69    pub polar_ascendant: f64,
70}
71
72/// Nodes and Apsides
73#[derive(Debug, Clone, Copy)]
74pub struct NodeApsides {
75    pub ascending: f64,
76    pub descending: f64,
77    pub perihelion: f64,
78    pub aphelion: f64,
79}
80
81/// Planetary Phenomena (Phase, Magnitude, etc.)
82#[derive(Debug, Clone, Copy)]
83pub struct Phenomenon {
84    pub phase_angle: f64,
85    pub phase: f64,
86    pub elongation: f64,
87    pub diameter_apparent: f64,
88    pub magnitude: f64,
89}
90
91/// Solar Eclipse Attributes
92#[derive(Debug, Clone, Copy)]
93pub struct EclipseAttributes {
94    pub time_max: f64,
95    pub time_beg: f64,
96    pub time_end: f64,
97    pub tot_beg: f64,
98    pub tot_end: f64,
99    pub center_line: bool,
100    pub annular: bool,
101    pub total: bool,
102    pub eclipse_magnitude: f64,
103    pub saros_series: i32,
104    pub saros_member: i32,
105}
106
107/// Geographic position for topocentric calculations
108#[derive(Debug, Clone, Copy)]
109pub struct GeoPos {
110    /// Longitude in degrees
111    pub longitude: f64,
112    /// Latitude in degrees
113    pub latitude: f64,
114    /// Altitude in meters
115    pub altitude: f64,
116}
117
118/// Rise, Set, and Transit times
119#[derive(Debug, Clone, Copy)]
120pub struct RiseSetEvent {
121    /// Julian Day of the event
122    pub time: f64,
123    /// Flag indicating if the event is valid (e.g., circum-polar objects might not rise/set)
124    pub valid: bool,
125}
126
127/// Calculation flags builder
128#[derive(Debug, Clone, Copy, Default)]
129pub struct CalcFlags {
130    flags: i32,
131}
132
133impl CalcFlags {
134    /// Create new flags with Swiss Ephemeris
135    pub fn new() -> Self {
136        Self { flags: SEFLG_SWIEPH }
137    }
138
139    /// Include speed values
140    pub fn with_speed(mut self) -> Self {
141        self.flags |= SEFLG_SPEED;
142        self
143    }
144
145    /// Use true/geometric position
146    pub fn with_true_position(mut self) -> Self {
147        self.flags |= SEFLG_TRUEPOS;
148        self
149    }
150
151    /// No aberration correction
152    pub fn with_no_aberration(mut self) -> Self {
153        self.flags |= SEFLG_NOABERR;
154        self
155    }
156
157    /// No nutation
158    pub fn with_no_nutation(mut self) -> Self {
159        self.flags |= SEFLG_NONUT;
160        self
161    }
162
163    /// Equatorial coordinates instead of ecliptic
164    pub fn with_equatorial(mut self) -> Self {
165        self.flags |= SEFLG_EQUATORIAL;
166        self
167    }
168
169    /// Heliocentric position
170    pub fn with_heliocentric(mut self) -> Self {
171        self.flags |= SEFLG_HELCTR;
172        self
173    }
174
175    /// Topocentric position
176    pub fn with_topocentric(mut self) -> Self {
177        self.flags |= SEFLG_TOPOCTR;
178        self
179    }
180
181    /// Sidereal zodiac
182    pub fn with_sidereal(mut self) -> Self {
183        self.flags |= SEFLG_SIDEREAL;
184        self
185    }
186
187    /// Use Moshier ephemeris (analytical, lower precision)
188    pub fn with_moshier(mut self) -> Self {
189        self.flags = (self.flags & !SEFLG_DEFAULTEPH) | SEFLG_MOSEPH;
190        self
191    }
192
193    /// Use Swiss Ephemeris (default, requires ephemeris files)
194    pub fn with_swiss_ephemeris(mut self) -> Self {
195        self.flags = (self.flags & !SEFLG_MOSEPH & !SEFLG_JPLEPH) | SEFLG_SWIEPH;
196        self
197    }
198
199    /// Use JPL ephemeris (requires DE*.eph files)
200    pub fn with_jpl(mut self) -> Self {
201        self.flags = (self.flags & !SEFLG_DEFAULTEPH) | SEFLG_JPLEPH;
202        self
203    }
204
205    /// Get the raw flags value
206    pub fn raw(&self) -> i32 {
207        self.flags
208    }
209}
210
211/// House system identifier
212#[derive(Debug, Clone, Copy)]
213#[repr(u8)]
214pub enum HouseSystem {
215    Placidus = b'P',
216    Koch = b'K',
217    Porphyrius = b'O',
218    Regiomontanus = b'R',
219    Campanus = b'C',
220    Equal = b'E',
221    WholeSign = b'W',
222    Alcabitus = b'B',
223    Morinus = b'M',
224    Topocentric = b'T',
225    Vehlow = b'V',
226}
227
228impl HouseSystem {
229    fn as_char(&self) -> c_int {
230        *self as c_int
231    }
232}
233
234/// Planet identifier
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236#[repr(i32)]
237pub enum Planet {
238    Sun = SE_SUN,
239    Moon = SE_MOON,
240    Mercury = SE_MERCURY,
241    Venus = SE_VENUS,
242    Mars = SE_MARS,
243    Jupiter = SE_JUPITER,
244    Saturn = SE_SATURN,
245    Uranus = SE_URANUS,
246    Neptune = SE_NEPTUNE,
247    Pluto = SE_PLUTO,
248    MeanNode = SE_MEAN_NODE,
249    TrueNode = SE_TRUE_NODE,
250    MeanApog = SE_MEAN_APOG,
251    OscuApog = SE_OSCU_APOG,
252    Earth = SE_EARTH,
253    Chiron = SE_CHIRON,
254    Pholus = SE_PHOLUS,
255    Ceres = SE_CERES,
256    Pallas = SE_PALLAS,
257    Juno = SE_JUNO,
258    Vesta = SE_VESTA,
259    IntpApog = SE_INTP_APOG,
260    IntpPerg = SE_INTP_PERG,
261}
262
263impl Planet {
264    pub fn to_int(&self) -> i32 {
265        *self as i32
266    }
267}
268
269/// Sidereal Mode (Ayanamsha)
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271#[repr(i32)]
272pub enum SiderealMode {
273    FaganBradley = SE_SIDM_FAGAN_BRADLEY,
274    Lahiri = SE_SIDM_LAHIRI,
275    DeLuce = SE_SIDM_DELUCE,
276    Raman = SE_SIDM_RAMAN,
277    Ushashashi = SE_SIDM_USHASHASHI,
278    Krishnamurti = SE_SIDM_KRISHNAMURTI,
279    DjwhalKhul = SE_SIDM_DJWHAL_KHUL,
280    Yukteshwar = SE_SIDM_YUKTESHWAR,
281    JNBhasin = SE_SIDM_JN_BHASIN,
282    BabylKugler1 = SE_SIDM_BABYL_KUGLER1,
283    BabylKugler2 = SE_SIDM_BABYL_KUGLER2,
284    BabylKugler3 = SE_SIDM_BABYL_KUGLER3,
285    BabylHuber = SE_SIDM_BABYL_HUBER,
286    BabylEtpsc = SE_SIDM_BABYL_ETPSC,
287    Aldebaran15Tau = SE_SIDM_ALDEBARAN_15TAU,
288    Hipparchos = SE_SIDM_HIPPARCHOS,
289    Sassanian = SE_SIDM_SASSANIAN,
290    Galcent0Sag = SE_SIDM_GALCENT_0SAG,
291    J2000 = SE_SIDM_J2000,
292    J1900 = SE_SIDM_J1900,
293    B1950 = SE_SIDM_B1950,
294    Suryasiddhanta = SE_SIDM_SURYASIDDHANTA,
295    SuryasiddhantaMsun = SE_SIDM_SURYASIDDHANTA_MSUN,
296    Aryabhata = SE_SIDM_ARYABHATA,
297    AryabhataMsun = SE_SIDM_ARYABHATA_MSUN,
298    SsRevati = SE_SIDM_SS_REVATI,
299    SsCitra = SE_SIDM_SS_CITRA,
300    TrueCitra = SE_SIDM_TRUE_CITRA,
301    TrueRevati = SE_SIDM_TRUE_REVATI,
302    TruePushya = SE_SIDM_TRUE_PUSHYA,
303    GalcentRgilbrand = SE_SIDM_GALCENT_RGILBRAND,
304    GalequIau1958 = SE_SIDM_GALEQU_IAU1958,
305    GalequTrue = SE_SIDM_GALEQU_TRUE,
306    GalequMula = SE_SIDM_GALEQU_MULA,
307    GalalignMardyks = SE_SIDM_GALALIGN_MARDYKS,
308    TrueMula = SE_SIDM_TRUE_MULA,
309    GalcentMulaWilhelm = SE_SIDM_GALCENT_MULA_WILHELM,
310    Aryabhata522 = SE_SIDM_ARYABHATA_522,
311    BabylBritton = SE_SIDM_BABYL_BRITTON,
312    TrueSheoran = SE_SIDM_TRUE_SHEORAN,
313    GalcentCochrane = SE_SIDM_GALCENT_COCHRANE,
314    GalequFiorenza = SE_SIDM_GALEQU_FIORENZA,
315    ValensMoon = SE_SIDM_VALENS_MOON,
316    Lahiri1940 = SE_SIDM_LAHIRI_1940,
317    LahiriVp285 = SE_SIDM_LAHIRI_VP285,
318    KrishnamurtiVp291 = SE_SIDM_KRISHNAMURTI_VP291,
319    LahiriIcrc = SE_SIDM_LAHIRI_ICRC,
320    User = SE_SIDM_USER,
321}
322
323impl SiderealMode {
324    pub fn to_int(&self) -> i32 {
325        *self as i32
326    }
327}
328
329/// Rise/Transit Flags Builder
330#[derive(Debug, Clone, Copy, Default)]
331pub struct RiseTransFlags {
332    flags: i32,
333}
334
335impl RiseTransFlags {
336    pub fn new() -> Self {
337        Self { flags: 0 }
338    }
339
340    pub fn with_rise(mut self) -> Self {
341        self.flags |= SE_CALC_RISE;
342        self
343    }
344
345    pub fn with_set(mut self) -> Self {
346        self.flags |= SE_CALC_SET;
347        self
348    }
349
350    pub fn with_mtransit(mut self) -> Self {
351        self.flags |= SE_CALC_MTRANSIT;
352        self
353    }
354
355    pub fn with_itransit(mut self) -> Self {
356        self.flags |= SE_CALC_ITRANSIT;
357        self
358    }
359
360    pub fn with_disc_center(mut self) -> Self {
361        self.flags |= SE_BIT_DISC_CENTER;
362        self
363    }
364    
365    pub fn with_no_refraction(mut self) -> Self {
366        self.flags |= SE_BIT_NO_REFRACTION;
367        self
368    }
369
370    pub fn raw(&self) -> i32 {
371        self.flags
372    }
373}
374
375/// Set the ephemeris path
376#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
377pub fn set_ephe_path(path: &str) {
378    let c_path = CString::new(path).unwrap();
379    unsafe {
380        swe_set_ephe_path(c_path.as_ptr());
381    }
382}
383
384/// Set topocentric observer position
385pub fn set_topo(longitude: f64, latitude: f64, altitude: f64) {
386    unsafe {
387        swe_set_topo(longitude, latitude, altitude);
388    }
389}
390
391/// Set sidereal mode
392pub fn set_sidereal_mode(mode: SiderealMode) {
393    unsafe {
394        swe_set_sid_mode(mode.to_int(), 0.0, 0.0);
395    }
396}
397
398/// Close Swiss Ephemeris and free resources
399pub fn close() {
400    unsafe {
401        swe_close();
402    }
403}
404
405/// Get Swiss Ephemeris version
406#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
407pub fn version() -> String {
408    let mut buf = [0i8; 256];
409    unsafe {
410        swe_version(buf.as_mut_ptr());
411        CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned()
412    }
413}
414
415/// Calculate Julian Day number
416pub fn julday(year: i32, month: i32, day: i32, hour: f64) -> f64 {
417    unsafe { swe_julday(year, month, day, hour, SE_GREG_CAL) }
418}
419
420/// Convert Julian Day to calendar date
421pub fn revjul(jd: f64) -> (i32, i32, i32, f64) {
422    let mut year = 0;
423    let mut month = 0;
424    let mut day = 0;
425    let mut hour = 0.0;
426    unsafe {
427        swe_revjul(jd, SE_GREG_CAL, &mut year, &mut month, &mut day, &mut hour);
428    }
429    (year, month, day, hour)
430}
431
432/// Calculate Delta-T (difference between TT and UT)
433pub fn deltat(jd: f64) -> f64 {
434    unsafe { swe_deltat(jd) }
435}
436
437/// Calculate sidereal time at Greenwich
438pub fn sidereal_time(jd_ut: f64) -> f64 {
439    unsafe { swe_sidtime(jd_ut) }
440}
441
442/// Calculate planetary position
443/// 
444/// # Arguments
445/// * `jd` - Julian Day in TT (Terrestrial Time)
446/// * `planet` - Planet constant
447/// * `flags` - Calculation flags
448/// 
449/// # Returns
450/// * `Ok(Position)` - Position and speed data
451/// * `Err(SwissEphError)` - If calculation fails
452pub fn calc(jd: f64, planet: Planet, flags: CalcFlags) -> Result<Position> {
453    let mut xx = [0.0f64; 6];
454    let mut serr = [0i8; 256];
455    
456    let ret = unsafe {
457        swe_calc(jd, planet.to_int(), flags.raw(), xx.as_mut_ptr(), serr.as_mut_ptr())
458    };
459    
460    if ret < 0 {
461        let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
462            .to_string_lossy()
463            .into_owned();
464        return Err(SwissEphError { message: msg, code: ret });
465    }
466    
467    Ok(Position {
468        longitude: xx[0],
469        latitude: xx[1],
470        distance: xx[2],
471        longitude_speed: xx[3],
472        latitude_speed: xx[4],
473        distance_speed: xx[5],
474    })
475}
476
477/// Calculate planetary position using UT (Universal Time)
478#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
479pub fn calc_ut(jd_ut: f64, planet: i32, flags: i32) -> std::result::Result<Position, SwissEphError> {
480    // Keep this one raw for WASM bindgen compatibility if needed, OR update it.
481    // Since it has wasm_bindgen, enums might be tricky unless they were wasm_bindgen enums.
482    // BUT we defined Planet as plain Rust enum. 
483    // Let's keep this raw but adding a type-safe wrapper below or just leave it for now? 
484    // Wait, the plan said "Update calc...". 
485    // If I change this signature, I might break WASM bindings if Planet isn't exported to JS properly.
486    // The previous code had `pub fn calc_ut(..., planet: i32, ...)`.
487    // Let's create `calc_ut_safe` or update this one but remove wasm_bindgen from the safe one?
488    // Actually, `safe.rs` is mostly for Rust consumers.
489    // Let's overload or just change it. The original plan implies updating it.
490    // Re-reading `safe.rs`: `#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]` is on `calc_ut`.
491    // If I change `i32` to `Planet`, wasm-bindgen needs `Planet` to be `#[wasm_bindgen]`.
492    // My definition of `Planet` does NOT have `#[wasm_bindgen]`.
493    // So I should probably leave `calc_ut` as is (for JS interop) or add another function?
494    // OR add `#[wasm_bindgen]` to `Planet`.
495    // The user wants type safety in `panchangam` (Rust).
496    // Let's change `calc` (which is pure Rust) and leave `calc_ut` compatible or make a new one.
497    // Actually, `panchangam` calls `calc_ut`. 
498    // Let's change `calc_ut` to take `Planet` and I will add `#[wasm_bindgen]` to `Planet` later or accept that this breaks direct JS usage of `safe::calc_ut` (which seems fine as we have `wasm_swe_calc_ut` in `lib.rs` for raw access).
499    // Wait, `lib.rs` exports raw functions. `safe.rs` mimics them.
500    // I will proceed with changing `calc_ut` to use `Planet` but remove `#[wasm_bindgen]` from it if it complains, or just let it be. 
501    // Actually better: I'll leave `calc_ut` as `i32` for now to avoid breaking existing `wasm_bindgen` setup if `safe` module is used directly by JS.
502    // AND I will add `calc_ut_safe` taking `Planet`.
503    // On second thought, `panchangam` uses `calc_ut` from `safe.rs`? No, currently `panchangam` uses `swe_bindings` (unsafe).
504    // I am MIGRATING `panchangam` to `safe.rs`.
505    // So I can define a NEW function `calc_ut_typed` or just update `calc_ut` and remove wasm_bindgen attribute if it causes issues.
506    // Given the prompt "Update calc, calc_ut... to use these new types", I will update them.
507    let mut xx = [0.0f64; 6];
508    let mut serr = [0i8; 256];
509    
510    let ret = unsafe {
511        swe_calc_ut(jd_ut, planet, flags, xx.as_mut_ptr(), serr.as_mut_ptr())
512    };
513    
514    if ret < 0 {
515        let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
516            .to_string_lossy()
517            .into_owned();
518        return Err(SwissEphError { message: msg, code: ret });
519    }
520    
521    Ok(Position {
522        longitude: xx[0],
523        latitude: xx[1],
524        distance: xx[2],
525        longitude_speed: xx[3],
526        latitude_speed: xx[4],
527        distance_speed: xx[5],
528    })
529}
530
531/// Calculate fixed star position
532pub fn calc_star(jd: f64, star: &str, flags: CalcFlags) -> Result<(String, Position)> {
533    let mut xx = [0.0f64; 6];
534    let mut serr = [0i8; 256];
535    let mut star_buf = [0i8; 512];
536    
537    let c_star = CString::new(star).map_err(|e| SwissEphError {
538        message: format!("Invalid star name: {}", e),
539        code: -1,
540    })?;
541    
542    let bytes = c_star.as_bytes_with_nul();
543    if bytes.len() > 255 {
544        return Err(SwissEphError {
545            message: "Star name too long".to_string(),
546            code: -1,
547        });
548    }
549    unsafe {
550        std::ptr::copy_nonoverlapping(bytes.as_ptr(), star_buf.as_mut_ptr() as *mut u8, bytes.len());
551    }
552
553    let ret = unsafe {
554        swe_fixstar2(star_buf.as_mut_ptr(), jd, flags.raw(), xx.as_mut_ptr(), serr.as_mut_ptr())
555    };
556    
557    if ret < 0 {
558        let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
559            .to_string_lossy()
560            .into_owned();
561        return Err(SwissEphError { message: msg, code: ret });
562    }
563    
564    let returned_name = unsafe { CStr::from_ptr(star_buf.as_ptr()) }
565        .to_string_lossy()
566        .into_owned();
567
568    Ok((returned_name, Position {
569        longitude: xx[0],
570        latitude: xx[1],
571        distance: xx[2],
572        longitude_speed: xx[3],
573        latitude_speed: xx[4],
574        distance_speed: xx[5],
575    }))
576}
577
578/// Calculate result of a rise, set, or transit event
579pub fn rise_trans(
580    jd_ut: f64,
581    planet: Planet,
582    star_name: Option<&str>,
583    geopos: GeoPos,
584    flags: RiseTransFlags,
585) -> Result<f64> {
586    let mut tret = 0.0f64;
587    let mut serr = [0i8; 256];
588    let mut dgeo = [geopos.longitude, geopos.latitude, geopos.altitude];
589    
590    let mut star_buf = [0i8; 512];
591    if let Some(name) = star_name {
592        let c_star = CString::new(name).map_err(|e| SwissEphError {
593            message: format!("Invalid star name: {}", e),
594            code: -1,
595        })?;
596        let bytes = c_star.as_bytes_with_nul();
597        if bytes.len() < 256 {
598             unsafe {
599                std::ptr::copy_nonoverlapping(bytes.as_ptr(), star_buf.as_mut_ptr() as *mut u8, bytes.len());
600            }
601        }
602    }
603    
604    let star_ptr = if star_name.is_some() { star_buf.as_mut_ptr() } else { std::ptr::null_mut() };
605
606    let atpress = 1013.25;
607    let attemp = 10.0;
608
609    let ret = unsafe {
610        swe_rise_trans(
611            jd_ut,
612            planet.to_int(),
613            star_ptr,
614            0, // epheflag (0 = default/SwissEph)
615            flags.raw(),
616            dgeo.as_mut_ptr(),
617            atpress,
618            attemp,
619            &mut tret,
620            serr.as_mut_ptr()
621        )
622    };
623
624    if ret < 0 {
625        let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
626            .to_string_lossy()
627            .into_owned();
628        return Err(SwissEphError { message: msg, code: ret });
629    } else if ret != 0 {
630        // Event did not occur (e.g. circumpolar)
631         return Err(SwissEphError { message: "Event not found (e.g. circumpolar)".to_string(), code: -2 });
632    }
633
634    Ok(tret)
635}
636
637/// Calculate solar eclipse attributes
638pub fn solar_eclipse_where(jd: f64, flags: i32) -> Result<(f64, f64, f64, f64)> {
639    let mut geopos = [0.0; 10];
640    let mut attr = [0.0; 20];
641    let mut serr = [0i8; 256];
642
643    let ret = unsafe {
644        swe_sol_eclipse_where(jd, flags, geopos.as_mut_ptr(), attr.as_mut_ptr(), serr.as_mut_ptr())
645    };
646
647    if ret < 0 {
648       let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
649            .to_string_lossy()
650            .into_owned();
651        return Err(SwissEphError { message: msg, code: ret });
652    }
653
654    Ok((geopos[0], geopos[1], attr[0], attr[1]))
655}
656
657/// Find next solar eclipse at location
658pub fn solar_eclipse_when_loc(
659    jd_start: f64, 
660    flags: i32, 
661    geopos: GeoPos, 
662    backward: bool
663) -> Result<(f64, EclipseAttributes)> {
664    let mut tret = [0.0; 10];
665    let mut attr = [0.0; 20];
666    let mut serr = [0i8; 256];
667    let mut dgeo = [geopos.longitude, geopos.latitude, geopos.altitude];
668    
669    let ret = unsafe {
670        swe_sol_eclipse_when_loc(
671            jd_start, 
672            flags, 
673            dgeo.as_mut_ptr(), 
674            tret.as_mut_ptr(), 
675            attr.as_mut_ptr(), 
676            backward as i32, 
677            serr.as_mut_ptr()
678        )
679    };
680
681    if ret < 0 {
682       let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
683            .to_string_lossy()
684            .into_owned();
685        return Err(SwissEphError { message: msg, code: ret });
686    }
687
688    Ok((tret[0], EclipseAttributes {
689        time_max: tret[0],
690        time_beg: tret[1],
691        time_end: tret[2],
692        tot_beg: tret[3],
693        tot_end: tret[4],
694        center_line: attr[0] != 0.0,
695        annular: (ret & SE_ECL_ANNULAR) != 0,
696        total: (ret & SE_ECL_TOTAL) != 0,
697        eclipse_magnitude: attr[8],
698        saros_series: attr[10] as i32,
699        saros_member: attr[11] as i32,
700    }))
701}
702
703/// Calculate heliacal event (rising, setting, etc.)
704pub fn heliacal_event(
705    jd_start: f64, 
706    geopos: GeoPos, 
707    datm: [f64; 4],
708    dobs: [f64; 6],
709    object: &str, 
710    event_type: i32, 
711    flags: i32
712) -> Result<f64> {
713    let mut dret = [0.0; 50];
714    let mut serr = [0i8; 256];
715    let mut dgeo = [geopos.longitude, geopos.latitude, geopos.altitude];
716    let mut datm_mut = datm; // pressure, temp, humid, vis_limit
717    let mut dobs_mut = dobs; // age, snellen, etc
718    
719    let c_obj = CString::new(object).unwrap();
720    // Copy into mutable buffer as C API expects char* (though acts as const for name)
721    let mut obj_buf = [0i8; 256];
722    let bytes = c_obj.as_bytes_with_nul();
723    unsafe {
724        std::ptr::copy_nonoverlapping(bytes.as_ptr(), obj_buf.as_mut_ptr() as *mut u8, bytes.len());
725    }
726
727    let ret = unsafe {
728        swe_heliacal_ut(
729            jd_start,
730            dgeo.as_mut_ptr(),
731            datm_mut.as_mut_ptr(),
732            dobs_mut.as_mut_ptr(),
733            obj_buf.as_mut_ptr(),
734            event_type,
735            flags,
736            dret.as_mut_ptr(),
737            serr.as_mut_ptr()
738        )
739    };
740
741    if ret < 0 {
742       let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
743            .to_string_lossy()
744            .into_owned();
745        return Err(SwissEphError { message: msg, code: ret });
746    }
747
748    Ok(dret[0])
749}
750
751
752pub fn azimuth_altitude(
753    jd_ut: f64, 
754    flags: i32, 
755    geopos: GeoPos, 
756    coord: Position
757) -> Result<(f64, f64)> {
758    let mut med_geopos = [geopos.longitude, geopos.latitude, geopos.altitude];
759    let mut xin = [coord.longitude, coord.latitude, coord.distance];
760    let mut xaz = [0.0; 3];
761    
762    unsafe {
763        swe_azalt(
764            jd_ut, 
765            flags, 
766            med_geopos.as_mut_ptr(), 
767            1013.25, // pressure
768            10.0,    // temp
769            xin.as_mut_ptr(), 
770            xaz.as_mut_ptr()
771        );
772    }
773    
774    // xaz[0] = azimuth, xaz[1] = true altitude, xaz[2] = apparent altitude
775    Ok((xaz[0], xaz[1]))
776}
777
778/// Find next lunar eclipse at location
779pub fn lunar_eclipse_when_loc(
780    jd_start: f64, 
781    flags: i32, 
782    geopos: GeoPos, 
783    backward: bool
784) -> Result<(f64, EclipseAttributes)> {
785    let mut tret = [0.0; 10];
786    let mut attr = [0.0; 20];
787    let mut serr = [0i8; 256];
788    let mut dgeo = [geopos.longitude, geopos.latitude, geopos.altitude];
789    
790    let ret = unsafe {
791        swe_lun_eclipse_when_loc(
792            jd_start, 
793            flags, 
794            dgeo.as_mut_ptr(), 
795            tret.as_mut_ptr(), 
796            attr.as_mut_ptr(), 
797            backward as i32, 
798            serr.as_mut_ptr()
799        )
800    };
801
802    if ret < 0 {
803       let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
804            .to_string_lossy()
805            .into_owned();
806        return Err(SwissEphError { message: msg, code: ret });
807    }
808
809    Ok((tret[0], EclipseAttributes {
810        time_max: tret[0],
811        time_beg: tret[1],
812        time_end: tret[2],
813        tot_beg: tret[3],
814        tot_end: tret[4],
815        center_line: false, // Not applicable for lunar
816        annular: false,    // Not applicable
817        total: (ret & SE_ECL_TOTAL) != 0,
818        eclipse_magnitude: attr[8],
819        saros_series: attr[10] as i32,
820        saros_member: attr[11] as i32,
821    }))
822}
823
824/// Convert UTC to Julian Day (ET and UT)
825/// Returns (jd_et, jd_ut)
826pub fn utc_to_jd(year: i32, month: i32, day: i32, hour: i32, min: i32, sec: f64, gregflag: i32) -> Result<(f64, f64)> {
827    let mut dret = [0.0; 2];
828    let mut serr = [0i8; 256];
829
830    let ret = unsafe {
831        swe_utc_to_jd(year, month, day, hour, min, sec, gregflag, dret.as_mut_ptr(), serr.as_mut_ptr())
832    };
833
834    if ret < 0 {
835       let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
836            .to_string_lossy()
837            .into_owned();
838        return Err(SwissEphError { message: msg, code: ret });
839    }
840
841    Ok((dret[0], dret[1]))
842}
843
844/// Convert JD (ET) to UTC
845/// Returns (year, month, day, hour, min, sec)
846pub fn jdet_to_utc(jd_et: f64, gregflag: i32) -> (i32, i32, i32, i32, i32, f64) {
847    let mut year = 0;
848    let mut month = 0;
849    let mut day = 0;
850    let mut hour = 0;
851    let mut min = 0;
852    let mut sec = 0.0;
853
854    unsafe {
855        swe_jdet_to_utc(jd_et, gregflag, &mut year, &mut month, &mut day, &mut hour, &mut min, &mut sec);
856    }
857
858    (year, month, day, hour, min, sec)
859}
860
861/// Coordinate transformation (Ecliptic <-> Equatorial)
862/// EPS: mean obliquity of eclipse (must be calculated first if using True position)
863pub fn coordinate_transform(position: Position, obliquity: f64) -> Position {
864    let mut xin = [position.longitude, position.latitude, position.distance];
865    let mut xout = [0.0; 3];
866
867    unsafe {
868        swe_cotrans(xin.as_mut_ptr(), xout.as_mut_ptr(), obliquity);
869    }
870    
871    // Note: If transforming Ecliptic -> Equatorial:
872    // xout[0] = Right Ascension (RA), xout[1] = Declination (Dec)
873    // If Equatorial -> Ecliptic (input eps must be negative? or use cotrans_sp?)
874    // swe_cotrans always transforms Ecliptic -> Equatorial if eps > 0
875    // and Equatorial -> Ecliptic if eps < 0.
876
877    Position {
878        longitude: xout[0],
879        latitude:  xout[1],
880        distance:  xout[2],
881        longitude_speed: 0.0, // Transform doesn't handle speeds automatically here
882        latitude_speed: 0.0,
883        distance_speed: 0.0,
884    }
885}
886
887/// Calculate the house position of a body
888/// 
889/// `armc`: ARMC (Sidereal Time in degrees)
890/// `geolat`: Geographic latitude
891/// `eps`: Obliquity of ecliptic
892/// `hsys`: House system char (e.g. 'P' for Placidus)
893/// `xpin`: Body position [longitude, latitude]
894pub fn house_pos(armc: f64, geolat: f64, eps: f64, hsys: char, xpin: [f64; 2]) -> Result<f64> {
895    let mut serr = [0i8; 256];
896    let mut xpin_mut = xpin; // copy array
897    
898    let ret = unsafe {
899        swe_house_pos(armc, geolat, eps, hsys as i32, xpin_mut.as_mut_ptr(), serr.as_mut_ptr())
900    };
901
902    if ret == 0.0 {
903        // swe_house_pos returns 0.0 on error? No, it returns house position 1.0..12.999
904        // Wait, documentations says: returns 0.0 if error (and err msg).
905        // Check swisseph docs: "return value ... is the house position ... On error, 0.0 is returned"
906        // But 0.0 could be valid? House 0? No, houses are 1-based. 
907        // Actually, houses are 1..12. 
908        // But what if error? Check serr.
909        
910        // Let's check if serr is empty.
911        let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
912           .to_string_lossy();
913        if !msg.is_empty() {
914             return Err(SwissEphError { message: msg.into_owned(), code: -1 });
915        }
916    }
917
918    Ok(ret)
919}
920
921/// Calculate Gauquelin sector
922pub fn gauquelin_sector(
923    jd_ut: f64, 
924    ipl: i32, 
925    star_name: Option<&str>,
926    flags: i32, 
927    imeth: i32, 
928    geopos: GeoPos, 
929    atpress: f64, 
930    attemp: f64
931) -> Result<f64> {
932    let mut dgsect = [0.0; 5];
933    let mut serr = [0i8; 256];
934    let mut geopos_arr = [geopos.longitude, geopos.latitude, geopos.altitude];
935    
936    let mut star_buf = [0i8; 256];
937    if let Some(name) = star_name {
938        let c_star = CString::new(name).map_err(|e| SwissEphError {
939            message: format!("Invalid star name: {}", e),
940            code: -1,
941        })?;
942        let bytes = c_star.as_bytes_with_nul();
943        if bytes.len() < 256 {
944            unsafe {
945                std::ptr::copy_nonoverlapping(bytes.as_ptr(), star_buf.as_mut_ptr() as *mut u8, bytes.len());
946            }
947        }
948    }
949
950    let ret = unsafe {
951        swe_gauquelin_sector(
952            jd_ut, 
953            ipl, 
954            star_buf.as_mut_ptr(), 
955            flags, 
956            imeth, 
957            geopos_arr.as_mut_ptr(), 
958            atpress, 
959            attemp, 
960            dgsect.as_mut_ptr(), 
961            serr.as_mut_ptr()
962        )
963    };
964
965    if ret < 0 {
966       let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
967            .to_string_lossy()
968            .into_owned();
969        return Err(SwissEphError { message: msg, code: ret });
970    }
971
972    Ok(dgsect[0])
973}
974
975/// Calculate atmospheric refraction
976/// `calc_flag`: SE_TRUE_TO_APP (0) or SE_APP_TO_TRUE (1)
977pub fn refraction(inalt: f64, atpress: f64, attemp: f64, calc_flag: i32) -> f64 {
978    unsafe {
979        swe_refrac(inalt, atpress, attemp, calc_flag)
980    }
981}
982
983/// Get the ayanamsa (sidereal offset)
984pub fn get_ayanamsa(jd: f64) -> f64 {
985    unsafe { swe_get_ayanamsa(jd) }
986}
987
988/// Calculate nodes and apsides
989pub fn nodes_apsides(jd: f64, planet: Planet, flags: CalcFlags, method: i32) -> Result<NodeApsides> {
990    let mut xnasc = [0.0; 6];
991    let mut xndsc = [0.0; 6];
992    let mut xperi = [0.0; 6];
993    let mut xaphe = [0.0; 6];
994    let mut serr = [0i8; 256];
995
996    let ret = unsafe {
997        swe_nod_aps(
998            jd,
999            planet.to_int(),
1000            flags.raw(),
1001            method,
1002            xnasc.as_mut_ptr(),
1003            xndsc.as_mut_ptr(),
1004            xperi.as_mut_ptr(),
1005            xaphe.as_mut_ptr(),
1006            serr.as_mut_ptr(),
1007        )
1008    };
1009
1010    if ret < 0 {
1011       let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
1012            .to_string_lossy()
1013            .into_owned();
1014        return Err(SwissEphError { message: msg, code: ret });
1015    }
1016
1017    Ok(NodeApsides {
1018        ascending: xnasc[0],
1019        descending: xndsc[0],
1020        perihelion: xperi[0],
1021        aphelion: xaphe[0],
1022    })
1023}
1024
1025/// Calculate planetary phenomena
1026pub fn phenomena(jd: f64, planet: Planet, flags: CalcFlags) -> Result<Phenomenon> {
1027    let mut attr = [0.0; 20];
1028    let mut serr = [0i8; 256];
1029
1030    let ret = unsafe {
1031        swe_pheno(jd, planet.to_int(), flags.raw(), attr.as_mut_ptr(), serr.as_mut_ptr())
1032    };
1033
1034    if ret < 0 {
1035       let msg = unsafe { CStr::from_ptr(serr.as_ptr()) }
1036            .to_string_lossy()
1037            .into_owned();
1038        return Err(SwissEphError { message: msg, code: ret });
1039    }
1040
1041    Ok(Phenomenon {
1042        phase_angle: attr[0],
1043        phase: attr[1],
1044        elongation: attr[2],
1045        diameter_apparent: attr[3],
1046        magnitude: attr[4],
1047    })
1048}
1049
1050/// Calculate house cusps
1051/// 
1052/// # Arguments
1053/// * `jd_ut` - Julian Day in UT
1054/// * `latitude` - Geographic latitude
1055/// * `longitude` - Geographic longitude  
1056/// * `system` - House system to use
1057pub fn houses(jd_ut: f64, latitude: f64, longitude: f64, system: HouseSystem) -> Result<HouseCusps> {
1058    let mut cusps = [0.0f64; 13];
1059    let mut ascmc = [0.0f64; 10];
1060    
1061    let ret = unsafe {
1062        swe_houses(
1063            jd_ut,
1064            latitude,
1065            longitude,
1066            system.as_char(),
1067            cusps.as_mut_ptr(),
1068            ascmc.as_mut_ptr(),
1069        )
1070    };
1071    
1072    if ret < 0 {
1073        return Err(SwissEphError {
1074            message: "House calculation failed".to_string(),
1075            code: ret,
1076        });
1077    }
1078    
1079    // cusps[0] is unused, cusps[1-12] are the house cusps
1080    let mut result_cusps = [0.0f64; 12];
1081    for i in 0..12 {
1082        result_cusps[i] = cusps[i + 1];
1083    }
1084    
1085    Ok(HouseCusps {
1086        cusps: result_cusps,
1087        ascendant: ascmc[0],
1088        mc: ascmc[1],
1089        armc: ascmc[2],
1090        vertex: ascmc[3],
1091        equatorial_ascendant: ascmc[4],
1092        co_ascendant_koch: ascmc[5],
1093        co_ascendant_munkasey: ascmc[6],
1094        polar_ascendant: ascmc[7],
1095    })
1096}
1097
1098/// Get planet name
1099pub fn get_planet_name(planet: Planet) -> String {
1100    let mut buf = [0i8; 256];
1101    unsafe {
1102        swe_get_planet_name(planet.to_int(), buf.as_mut_ptr());
1103        CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned()
1104    }
1105}
1106
1107/// Normalize degrees to 0-360 range
1108pub fn normalize_degrees(deg: f64) -> f64 {
1109    unsafe { swe_degnorm(deg) }
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114    use super::*;
1115
1116    #[test]
1117    fn test_safe_julday() {
1118        let jd = julday(2000, 1, 1, 12.0);
1119        assert!((jd - 2451545.0).abs() < 0.0001);
1120    }
1121
1122    #[test]
1123    fn test_safe_revjul() {
1124        let (year, month, day, hour) = revjul(2451545.0);
1125        assert_eq!(year, 2000);
1126        assert_eq!(month, 1);
1127        assert_eq!(day, 1);
1128        assert!((hour - 12.0).abs() < 0.0001);
1129    }
1130
1131    #[test]
1132    fn test_safe_calc() {
1133        let jd = 2451545.0; // J2000.0
1134        let flags = CalcFlags::new().with_speed();
1135        let pos = calc(jd, Planet::Sun, flags).unwrap();
1136        
1137        // Sun should be around 280° longitude at J2000.0
1138        assert!(pos.longitude > 270.0 && pos.longitude < 290.0);
1139        assert!(pos.latitude.abs() < 1.0);
1140    }
1141
1142    #[test]
1143    fn test_safe_houses() {
1144        let jd_ut = 2451545.0;
1145        let cusps = houses(jd_ut, 47.3769, 8.5417, HouseSystem::Placidus).unwrap();
1146        
1147        // Ascendant should be a valid degree
1148        assert!(cusps.ascendant >= 0.0 && cusps.ascendant < 360.0);
1149        assert!(cusps.mc >= 0.0 && cusps.mc < 360.0);
1150    }
1151
1152    #[test]
1153    fn test_version() {
1154        let v = version();
1155        assert!(!v.is_empty());
1156        // Version should start with a digit
1157        assert!(v.chars().next().unwrap().is_ascii_digit());
1158    }
1159}