use crate::aspects::{Aspect, AspectType, BodyPosition, find_aspects};
use crate::dignity::{DignityPlanet, RulershipScheme, dignity_of, sign_of};
use crate::houses::{HouseCusps, HouseSystem, compute_houses};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct ChartConfig {
pub house_system: HouseSystem,
pub ayanamsha: Option<crate::sidereal::Ayanamsha>,
pub rulership_scheme: RulershipScheme,
pub aspect_types: Vec<AspectType>,
pub orb_factor: f64,
}
impl Default for ChartConfig {
fn default() -> Self {
Self {
house_system: HouseSystem::Placidus,
ayanamsha: None, rulership_scheme: RulershipScheme::Traditional,
aspect_types: AspectType::MAJOR.to_vec(),
orb_factor: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChartPlanet {
pub name: String,
pub longitude: f64,
pub latitude: f64,
pub distance: f64,
pub speed: f64,
pub retrograde: bool,
pub sign: String,
pub sign_index: u8,
pub house: u8,
pub dignity: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComputedChart {
pub planets: Vec<ChartPlanet>,
pub houses: HouseCusps,
pub aspects: Vec<Aspect>,
pub config_summary: String,
}
#[must_use]
pub fn compute_chart(
planet_data: &[(String, f64, f64, f64, f64)],
ramc: f64,
geo_latitude: f64,
obliquity: f64,
jd: f64,
config: &ChartConfig,
) -> ComputedChart {
let ayanamsha_offset = config
.ayanamsha
.map_or(0.0, |a| crate::sidereal::ayanamsha_value(a, jd));
let houses = compute_houses(ramc, geo_latitude, obliquity, config.house_system);
let mut planets = Vec::new();
let mut body_positions = Vec::new();
for (name, lon, lat, dist, speed) in planet_data {
let sidereal_lon = vedaksha_math::angle::normalize_degrees(*lon - ayanamsha_offset);
let sign = sign_of(sidereal_lon);
let house = determine_house(sidereal_lon, &houses);
let dignity = match name_to_dignity_planet(name) {
Some(dp) => format!("{:?}", dignity_of(dp, sign, config.rulership_scheme)),
None => "Peregrine".to_string(),
};
planets.push(ChartPlanet {
name: name.clone(),
longitude: sidereal_lon,
latitude: *lat,
distance: *dist,
speed: *speed,
retrograde: *speed < 0.0,
sign: sign.name().to_string(),
sign_index: sign as u8,
house,
dignity,
});
body_positions.push(BodyPosition {
longitude: sidereal_lon,
speed: *speed,
});
}
let aspects = find_aspects(&body_positions, &config.aspect_types, config.orb_factor);
let config_summary = format!(
"Houses: {:?}, Zodiac: {}, Rulership: {:?}",
config.house_system,
config
.ayanamsha
.map_or_else(|| "Tropical".to_string(), |a| format!("{a:?}")),
config.rulership_scheme,
);
ComputedChart {
planets,
houses,
aspects,
config_summary,
}
}
#[allow(clippy::cast_possible_truncation)]
fn determine_house(longitude: f64, houses: &HouseCusps) -> u8 {
for i in 0..12 {
let cusp = houses.cusps[i];
let next_cusp = houses.cusps[(i + 1) % 12];
if next_cusp > cusp {
if longitude >= cusp && longitude < next_cusp {
return (i as u8) + 1;
}
} else {
if longitude >= cusp || longitude < next_cusp {
return (i as u8) + 1;
}
}
}
1 }
fn name_to_dignity_planet(name: &str) -> Option<DignityPlanet> {
match name.to_lowercase().as_str() {
"sun" => Some(DignityPlanet::Sun),
"moon" => Some(DignityPlanet::Moon),
"mars" => Some(DignityPlanet::Mars),
"mercury" => Some(DignityPlanet::Mercury),
"jupiter" => Some(DignityPlanet::Jupiter),
"venus" => Some(DignityPlanet::Venus),
"saturn" => Some(DignityPlanet::Saturn),
"uranus" => Some(DignityPlanet::Uranus),
"neptune" => Some(DignityPlanet::Neptune),
"pluto" => Some(DignityPlanet::Pluto),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn planet(name: &str, lon: f64, speed: f64) -> (String, f64, f64, f64, f64) {
(name.to_string(), lon, 0.0, 1.0, speed)
}
const RAMC: f64 = 30.0;
const LAT: f64 = 45.0;
const OBL: f64 = 23.44;
const JD: f64 = 2_451_545.0;
#[test]
fn compute_chart_returns_planets_and_houses() {
let data = vec![planet("Sun", 130.0, 1.0), planet("Moon", 45.0, 13.0)];
let chart = compute_chart(&data, RAMC, LAT, OBL, JD, &ChartConfig::default());
assert_eq!(chart.planets.len(), 2);
assert_eq!(chart.houses.cusps.len(), 12);
assert!(!chart.config_summary.is_empty());
}
#[test]
fn planets_have_correct_sign_assignments() {
let data = vec![planet("Sun", 130.0, 1.0)];
let chart = compute_chart(&data, RAMC, LAT, OBL, JD, &ChartConfig::default());
assert_eq!(chart.planets[0].sign, "Leo");
assert_eq!(chart.planets[0].sign_index, 4);
}
#[test]
fn retrograde_flag_from_negative_speed() {
let data = vec![planet("Mars", 200.0, -0.3), planet("Venus", 100.0, 1.2)];
let chart = compute_chart(&data, RAMC, LAT, OBL, JD, &ChartConfig::default());
assert!(chart.planets[0].retrograde, "Mars should be retrograde");
assert!(!chart.planets[1].retrograde, "Venus should be direct");
}
#[test]
fn aspects_detected_between_planets() {
let data = vec![planet("Sun", 0.0, 1.0), planet("Moon", 120.0, 13.0)];
let chart = compute_chart(&data, RAMC, LAT, OBL, JD, &ChartConfig::default());
assert!(
chart
.aspects
.iter()
.any(|a| a.aspect_type == AspectType::Trine),
"Expected a trine between Sun and Moon"
);
}
#[test]
fn sidereal_mode_shifts_longitudes() {
let data = vec![planet("Sun", 130.0, 1.0)];
let mut config = ChartConfig::default();
config.ayanamsha = Some(crate::sidereal::Ayanamsha::Lahiri);
let chart = compute_chart(&data, RAMC, LAT, OBL, JD, &config);
let expected = vedaksha_math::angle::normalize_degrees(
130.0 - crate::sidereal::ayanamsha_value(crate::sidereal::Ayanamsha::Lahiri, JD),
);
assert!(
(chart.planets[0].longitude - expected).abs() < 0.01,
"Sidereal longitude mismatch: got {}, expected {}",
chart.planets[0].longitude,
expected
);
}
#[test]
fn dignity_computed_for_known_planet() {
let data = vec![planet("Sun", 130.0, 1.0)];
let chart = compute_chart(&data, RAMC, LAT, OBL, JD, &ChartConfig::default());
assert_eq!(chart.planets[0].dignity, "Domicile");
}
#[test]
fn unknown_body_gets_peregrine() {
let data = vec![planet("Chiron", 100.0, 0.05)];
let chart = compute_chart(&data, RAMC, LAT, OBL, JD, &ChartConfig::default());
assert_eq!(chart.planets[0].dignity, "Peregrine");
}
#[test]
fn determine_house_basic() {
let cusps: [f64; 12] = core::array::from_fn(|i| (i as f64) * 30.0);
let houses = HouseCusps {
cusps,
asc: 0.0,
mc: 270.0,
system: HouseSystem::Equal,
polar_fallback: false,
};
assert_eq!(determine_house(15.0, &houses), 1);
assert_eq!(determine_house(45.0, &houses), 2);
assert_eq!(determine_house(350.0, &houses), 12);
}
}