use serde::{Serialize, Serializer};
use serde_json::Value;
use crate::aspects::{natal_aspects, Aspect, OrbSettings, Point};
use crate::chinese::calendar::{chinese_calendar, ChineseCalendar};
use crate::dignities::{
antiscia, condition, is_diurnal, receptions, AntisciaContact, Body, Condition, DignityScheme,
Reception,
};
use crate::ephemeris::KernelSet;
use crate::error::EngineError;
use crate::fixed_stars::{fixed_stars, FixedStarPosition};
use crate::horary::{planetary_hours, PlanetaryHours};
use crate::houses::{houses_at_sidereal_time, HouseSystem, Houses};
use crate::instant::UtcInstant;
use crate::lots::{arabic_parts, Lot};
use crate::lunar::{lunar_status, LunarStatus};
use crate::minor_bodies::{is_minor_body, minor_body_symbol};
use crate::planets::{planets_and_sidereal_time, NodeType};
use crate::symbolic::symbolic_degree_number;
use crate::temperament::{temperament, Temperament};
use crate::vedic::calendar::{vedic_calendar, VedicCalendar};
use crate::zodiac::{
ayanamsa, ayanamsa_info, determine_house, longitude_to_zodiac, AyanamsaInfo, DEFAULT_AYANAMSA,
};
const EXPECTED_BODIES: [&str; 26] = [
"Sun",
"Moon",
"Mercury",
"Venus",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
"Pluto",
"North Node",
"South Node",
"Chiron",
"Lilith",
"Ceres",
"Pallas",
"Juno",
"Vesta",
"Astraea",
"Eris",
"Sedna",
"Makemake",
"Orcus",
"Ixion",
"Ascendant",
"Midheaven",
];
#[derive(Debug, Clone)]
pub struct ChartRequest<'a> {
pub instant: UtcInstant,
pub latitude: f64,
pub longitude: f64,
pub house_system: &'a str,
pub zodiac_type: &'a str,
pub ayanamsa: &'a str,
pub orb_settings: Option<&'a Value>,
pub dignity_scheme: &'a str,
pub node_type: &'a str,
pub chinese_calendar: bool,
pub vedic_calendar: bool,
}
impl<'a> ChartRequest<'a> {
pub fn new(instant: UtcInstant, latitude: f64, longitude: f64) -> Self {
ChartRequest {
instant,
latitude,
longitude,
house_system: "P",
zodiac_type: "tropical",
ayanamsa: DEFAULT_AYANAMSA,
orb_settings: None,
dignity_scheme: "lilly",
node_type: "mean",
chinese_calendar: false,
vedic_calendar: false,
}
}
pub(crate) fn check_place(&self) -> Result<(), EngineError> {
let (lat, lon) = (self.latitude, self.longitude);
if !(lat.is_finite() && lat.abs() <= 90.0) {
return Err(EngineError::InvalidInput(format!(
"latitude must be within ±90°, got {lat}"
)));
}
if !(lon.is_finite() && lon.abs() <= 360.0) {
return Err(EngineError::InvalidInput(format!(
"longitude must be within ±360°, got {lon}"
)));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Placement {
pub name: &'static str,
pub symbol: &'static str,
pub sign: &'static str,
pub sign_symbol: &'static str,
pub degree: i64,
pub minute: i64,
pub ecliptic_longitude: f64,
pub house: u8,
pub speed: f64,
pub is_retrograde: bool,
pub symbolic_degree: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub condition: Option<Condition>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct HouseCusp {
pub house_number: u8,
pub sign: &'static str,
pub sign_symbol: &'static str,
pub degree: i64,
pub minute: i64,
pub ecliptic_longitude: f64,
pub symbolic_degree: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Chart {
pub house_system: String,
pub zodiac_type: &'static str,
pub ayanamsa: Option<&'static str>,
pub ayanamsa_name: Option<&'static str>,
pub ayanamsa_value: Option<f64>,
pub ayanamsa_formatted: Option<String>,
pub precession_rate_arcsec_yr: f64,
pub orb_settings: Value,
pub planets: Vec<Placement>,
pub unavailable_bodies: Vec<&'static str>,
pub houses: Vec<HouseCusp>,
pub aspects: Vec<Aspect>,
pub fixed_stars: Vec<FixedStarPosition>,
pub arabic_parts: Vec<Lot>,
pub temperament: Temperament,
#[serde(serialize_with = "empty_object_if_none")]
pub lunar_status: Option<LunarStatus>,
pub sect: Option<&'static str>,
pub dignity_scheme: &'static str,
pub node_type: &'static str,
pub receptions: Vec<Reception>,
pub antiscia: Vec<AntisciaContact>,
#[serde(skip_serializing_if = "Option::is_none")]
pub planetary_hours: Option<PlanetaryHours>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chinese_calendar: Option<ChineseCalendar>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vedic_calendar: Option<VedicCalendar>,
}
fn empty_object_if_none<S: Serializer>(v: &Option<LunarStatus>, s: S) -> Result<S::Ok, S::Error> {
match v {
Some(status) => status.serialize(s),
None => serde_json::Map::new().serialize(s),
}
}
impl Chart {
pub(crate) fn points(&self) -> Vec<Point<'_>> {
self.planets
.iter()
.map(|p| Point {
name: p.name,
longitude: p.ecliptic_longitude,
})
.collect()
}
}
fn symbol_of(name: &str) -> &'static str {
match name {
"Sun" => "☉",
"Moon" => "☽",
"Mercury" => "☿",
"Venus" => "♀",
"Mars" => "♂",
"Jupiter" => "♃",
"Saturn" => "♄",
"Uranus" => "â™…",
"Neptune" => "♆",
"Pluto" => "♇",
"North Node" => "☊",
"South Node" => "☋",
"Chiron" => "âš·",
"Lilith" => "⚸",
"Ascendant" => "ASC",
"Midheaven" => "MC",
_ => minor_body_symbol(name),
}
}
pub(crate) struct Sky {
pub zodiac_type: &'static str,
pub is_sidereal: bool,
pub ayanamsa_code: &'static str,
pub info: AyanamsaInfo,
pub jd: f64,
pub cusps: Vec<f64>,
pub house_cusps: Vec<HouseCusp>,
pub planets: Vec<Placement>,
}
pub(crate) fn sky(kernels: &KernelSet, req: &ChartRequest) -> Result<Sky, EngineError> {
req.check_place()?;
let zodiac_type = match req.zodiac_type.trim().to_lowercase().as_str() {
"sidereal" => "sidereal",
_ => "tropical",
};
let is_sidereal = zodiac_type == "sidereal";
let ayanamsa_code = ayanamsa(req.ayanamsa).code;
let jd = req.instant.julian_day();
let info = ayanamsa_info(jd, ayanamsa_code);
let shift = if is_sidereal { info.value } else { 0.0 };
let node = NodeType::from_code(req.node_type);
let (engine, gast_hours) = planets_and_sidereal_time(kernels, jd, shift, node)?;
let houses = houses_at_sidereal_time(
jd,
gast_hours,
req.latitude,
req.longitude,
HouseSystem::from_code(req.house_system),
shift,
);
let cusps = houses.cusps.to_vec();
let house_cusps: Vec<HouseCusp> = houses
.cusps
.iter()
.enumerate()
.map(|(i, &lon)| {
let z = longitude_to_zodiac(lon);
HouseCusp {
house_number: i as u8 + 1,
sign: z.sign.name,
sign_symbol: z.sign.symbol,
degree: z.degree,
minute: z.minute,
ecliptic_longitude: lon,
symbolic_degree: symbolic_degree_number(z.degree, z.minute),
}
})
.collect();
let raw: Vec<(&'static str, f64, f64, bool)> = engine
.bodies
.iter()
.map(|b| (b.name, b.longitude, b.speed, b.is_retrograde))
.collect();
let planets = placements(raw, &houses);
Ok(Sky {
zodiac_type,
is_sidereal,
ayanamsa_code,
info,
jd,
cusps,
house_cusps,
planets,
})
}
pub(crate) fn placements(
mut raw: Vec<(&'static str, f64, f64, bool)>,
houses: &Houses,
) -> Vec<Placement> {
raw.push(("Ascendant", houses.ascendant, 0.0, false));
raw.push(("Midheaven", houses.midheaven, 0.0, false));
raw.iter()
.map(|&(name, lon, speed, is_retrograde)| {
let z = longitude_to_zodiac(lon);
Placement {
name,
symbol: symbol_of(name),
sign: z.sign.name,
sign_symbol: z.sign.symbol,
degree: z.degree,
minute: z.minute,
ecliptic_longitude: lon,
house: determine_house(lon, &houses.cusps),
speed,
is_retrograde,
symbolic_degree: symbolic_degree_number(z.degree, z.minute),
condition: None,
}
})
.collect()
}
fn add_conditions(planets: &mut [Placement], scheme: DignityScheme) -> Option<&'static str> {
let at = |name: &str| {
planets
.iter()
.find(|p| p.name == name)
.map(|p| p.ecliptic_longitude)
};
let (sun, asc) = (at("Sun")?, at("Ascendant")?);
for planet in planets.iter_mut() {
planet.condition = condition(Body::from(&*planet), sun, asc, scheme);
}
Some(if is_diurnal(sun, asc) {
"diurnal"
} else {
"nocturnal"
})
}
pub fn calculate_chart(kernels: &KernelSet, req: &ChartRequest) -> Result<Chart, EngineError> {
let mut chart = chart_without_hours(kernels, req)?;
chart.planetary_hours = Some(planetary_hours(
kernels,
req.instant,
req.latitude,
req.longitude,
));
Ok(chart)
}
pub(crate) fn chart_without_hours(
kernels: &KernelSet,
req: &ChartRequest,
) -> Result<Chart, EngineError> {
let orbs = OrbSettings::merge(req.orb_settings)?;
let scheme = DignityScheme::from_code(req.dignity_scheme);
let Sky {
zodiac_type,
is_sidereal,
ayanamsa_code,
info,
jd,
cusps,
house_cusps,
mut planets,
} = sky(kernels, req)?;
let sect = add_conditions(&mut planets, scheme);
let points: Vec<Point> = planets
.iter()
.map(|p| Point {
name: p.name,
longitude: p.ecliptic_longitude,
})
.collect();
let mut aspects = natal_aspects(&points, &orbs)?;
let star_points: Vec<Point> = points
.iter()
.copied()
.filter(|p| !is_minor_body(p.name))
.collect();
let (stars, star_aspects) = fixed_stars(
jd,
&cusps,
&star_points,
orbs.fixed_star_orb()?,
is_sidereal.then_some(info.value),
);
aspects.extend(star_aspects);
let lots = arabic_parts(&points, &cusps);
let temperament = temperament(&planets, &aspects);
let lunar = lunar_status(&planets);
let bodies: Vec<Body> = planets.iter().map(Body::from).collect();
let aspect_between = |a: &str, b: &str| {
aspects
.iter()
.find(|x| (x.body1 == a && x.body2 == b) || (x.body1 == b && x.body2 == a))
.map(|x| x.aspect_type)
};
let chart_receptions = receptions(&bodies, aspect_between, scheme);
let chart_antiscia = antiscia(&planets);
let mut unavailable: Vec<&'static str> = EXPECTED_BODIES
.iter()
.copied()
.filter(|name| !planets.iter().any(|p| p.name == *name))
.collect();
unavailable.sort_unstable();
Ok(Chart {
house_system: req.house_system.to_string(),
zodiac_type,
ayanamsa: is_sidereal.then_some(ayanamsa_code),
ayanamsa_name: is_sidereal.then_some(info.name),
ayanamsa_value: is_sidereal.then_some(info.value),
ayanamsa_formatted: is_sidereal.then(|| info.formatted.clone()),
precession_rate_arcsec_yr: info.precession_rate_arcsec_yr,
orb_settings: orbs.to_value(),
planets,
unavailable_bodies: unavailable,
houses: house_cusps,
aspects,
fixed_stars: stars,
arabic_parts: lots,
temperament,
lunar_status: lunar,
sect,
dignity_scheme: scheme.code(),
node_type: NodeType::from_code(req.node_type).code(),
receptions: chart_receptions,
antiscia: chart_antiscia,
planetary_hours: None,
chinese_calendar: if req.chinese_calendar {
chinese_calendar(kernels, req.instant)?
} else {
None
},
vedic_calendar: if req.vedic_calendar {
vedic_calendar(kernels, req.instant, req.latitude, req.longitude)?
} else {
None
},
})
}