1use serde::{Serialize, Serializer};
5use serde_json::Value;
6
7use crate::aspects::{natal_aspects, Aspect, OrbSettings, Point};
8use crate::ephemeris::KernelSet;
9use crate::error::EngineError;
10use crate::fixed_stars::{fixed_stars, FixedStarPosition};
11use crate::houses::{calculate_houses, HouseSystem};
12use crate::instant::UtcInstant;
13use crate::lots::{arabic_parts, Lot};
14use crate::lunar::{lunar_status, LunarStatus};
15use crate::planets::calculate_planets;
16use crate::symbolic::symbolic_degree_number;
17use crate::temperament::{temperament, Temperament};
18use crate::zodiac::{
19 ayanamsa, ayanamsa_info, determine_house, longitude_to_zodiac, DEFAULT_AYANAMSA,
20};
21
22const EXPECTED_BODIES: [&str; 16] = [
25 "Sun",
26 "Moon",
27 "Mercury",
28 "Venus",
29 "Mars",
30 "Jupiter",
31 "Saturn",
32 "Uranus",
33 "Neptune",
34 "Pluto",
35 "North Node",
36 "South Node",
37 "Chiron",
38 "Lilith",
39 "Ascendant",
40 "Midheaven",
41];
42
43#[derive(Debug, Clone)]
45pub struct ChartRequest<'a> {
46 pub instant: UtcInstant,
48 pub latitude: f64,
50 pub longitude: f64,
52 pub house_system: &'a str,
54 pub zodiac_type: &'a str,
56 pub ayanamsa: &'a str,
58 pub orb_settings: Option<&'a Value>,
60}
61
62impl<'a> ChartRequest<'a> {
63 pub fn new(instant: UtcInstant, latitude: f64, longitude: f64) -> Self {
65 ChartRequest {
66 instant,
67 latitude,
68 longitude,
69 house_system: "P",
70 zodiac_type: "tropical",
71 ayanamsa: DEFAULT_AYANAMSA,
72 orb_settings: None,
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize)]
79pub struct Placement {
80 pub name: &'static str,
82 pub symbol: &'static str,
84 pub sign: &'static str,
86 pub sign_symbol: &'static str,
88 pub degree: i64,
90 pub minute: i64,
92 pub ecliptic_longitude: f64,
94 pub house: u8,
96 pub speed: f64,
98 pub is_retrograde: bool,
100 pub symbolic_degree: i64,
102}
103
104#[derive(Debug, Clone, PartialEq, Serialize)]
106pub struct HouseCusp {
107 pub house_number: u8,
109 pub sign: &'static str,
111 pub sign_symbol: &'static str,
113 pub degree: i64,
115 pub minute: i64,
117 pub ecliptic_longitude: f64,
119 pub symbolic_degree: i64,
121}
122
123#[derive(Debug, Clone, PartialEq, Serialize)]
125pub struct Chart {
126 pub house_system: String,
128 pub zodiac_type: &'static str,
130 pub ayanamsa: Option<&'static str>,
132 pub ayanamsa_name: Option<&'static str>,
134 pub ayanamsa_value: Option<f64>,
136 pub ayanamsa_formatted: Option<String>,
138 pub precession_rate_arcsec_yr: f64,
140 pub orb_settings: Value,
142 pub planets: Vec<Placement>,
144 pub unavailable_bodies: Vec<&'static str>,
146 pub houses: Vec<HouseCusp>,
148 pub aspects: Vec<Aspect>,
150 pub fixed_stars: Vec<FixedStarPosition>,
152 pub arabic_parts: Vec<Lot>,
154 pub temperament: Temperament,
156 #[serde(serialize_with = "empty_object_if_none")]
158 pub lunar_status: Option<LunarStatus>,
159}
160
161fn empty_object_if_none<S: Serializer>(v: &Option<LunarStatus>, s: S) -> Result<S::Ok, S::Error> {
162 match v {
163 Some(status) => status.serialize(s),
164 None => serde_json::Map::new().serialize(s),
165 }
166}
167
168impl Chart {
169 pub fn points(&self) -> Vec<Point<'_>> {
171 self.planets
172 .iter()
173 .map(|p| Point {
174 name: p.name,
175 longitude: p.ecliptic_longitude,
176 })
177 .collect()
178 }
179}
180
181fn symbol_of(name: &str) -> &'static str {
182 match name {
183 "Sun" => "☉",
184 "Moon" => "☽",
185 "Mercury" => "☿",
186 "Venus" => "♀",
187 "Mars" => "♂",
188 "Jupiter" => "♃",
189 "Saturn" => "♄",
190 "Uranus" => "♅",
191 "Neptune" => "♆",
192 "Pluto" => "♇",
193 "North Node" => "☊",
194 "South Node" => "☋",
195 "Chiron" => "⚷",
196 "Lilith" => "⚸",
197 "Ascendant" => "ASC",
198 "Midheaven" => "MC",
199 _ => "",
200 }
201}
202
203pub fn calculate_chart(kernels: &KernelSet, req: &ChartRequest) -> Result<Chart, EngineError> {
205 let zodiac_type = match req.zodiac_type.trim().to_lowercase().as_str() {
206 "sidereal" => "sidereal",
207 _ => "tropical",
208 };
209 let is_sidereal = zodiac_type == "sidereal";
210 let ayanamsa_code = ayanamsa(req.ayanamsa).code;
211 let orbs = OrbSettings::merge(req.orb_settings)?;
212
213 let jd = req.instant.julian_day();
214 let info = ayanamsa_info(jd, ayanamsa_code);
215 let shift = if is_sidereal { info.value } else { 0.0 };
216
217 let engine = calculate_planets(kernels, jd, shift)?;
218 let houses = calculate_houses(
219 jd,
220 req.latitude,
221 req.longitude,
222 HouseSystem::from_code(req.house_system),
223 shift,
224 );
225 let cusps = houses.cusps.to_vec();
226
227 let house_cusps: Vec<HouseCusp> = houses
228 .cusps
229 .iter()
230 .enumerate()
231 .map(|(i, &lon)| {
232 let z = longitude_to_zodiac(lon);
233 HouseCusp {
234 house_number: i as u8 + 1,
235 sign: z.sign.name,
236 sign_symbol: z.sign.symbol,
237 degree: z.degree,
238 minute: z.minute,
239 ecliptic_longitude: lon,
240 symbolic_degree: symbolic_degree_number(z.degree, z.minute),
241 }
242 })
243 .collect();
244
245 let mut raw: Vec<(&'static str, f64, f64, bool)> = engine
246 .bodies
247 .iter()
248 .map(|b| (b.name, b.longitude, b.speed, b.is_retrograde))
249 .collect();
250 raw.push(("Ascendant", houses.ascendant, 0.0, false));
251 raw.push(("Midheaven", houses.midheaven, 0.0, false));
252
253 let planets: Vec<Placement> = raw
254 .iter()
255 .map(|&(name, lon, speed, is_retrograde)| {
256 let z = longitude_to_zodiac(lon);
257 Placement {
258 name,
259 symbol: symbol_of(name),
260 sign: z.sign.name,
261 sign_symbol: z.sign.symbol,
262 degree: z.degree,
263 minute: z.minute,
264 ecliptic_longitude: lon,
265 house: determine_house(lon, &cusps),
266 speed,
267 is_retrograde,
268 symbolic_degree: symbolic_degree_number(z.degree, z.minute),
269 }
270 })
271 .collect();
272 let points: Vec<Point> = planets
273 .iter()
274 .map(|p| Point {
275 name: p.name,
276 longitude: p.ecliptic_longitude,
277 })
278 .collect();
279
280 let mut aspects = natal_aspects(&points, &orbs)?;
281 let (stars, star_aspects) = fixed_stars(
282 jd,
283 &cusps,
284 &points,
285 orbs.fixed_star_orb()?,
286 is_sidereal.then_some(info.value),
287 );
288 aspects.extend(star_aspects);
289
290 let lots = arabic_parts(&points, &cusps);
291 let temperament = temperament(&planets, &aspects);
292 let lunar = lunar_status(&planets);
293
294 let mut unavailable: Vec<&'static str> = EXPECTED_BODIES
295 .iter()
296 .copied()
297 .filter(|name| !planets.iter().any(|p| p.name == *name))
298 .collect();
299 unavailable.sort_unstable();
300
301 Ok(Chart {
302 house_system: req.house_system.to_string(),
303 zodiac_type,
304 ayanamsa: is_sidereal.then_some(ayanamsa_code),
305 ayanamsa_name: is_sidereal.then_some(info.name),
306 ayanamsa_value: is_sidereal.then_some(info.value),
307 ayanamsa_formatted: is_sidereal.then(|| info.formatted.clone()),
308 precession_rate_arcsec_yr: info.precession_rate_arcsec_yr,
309 orb_settings: orbs.to_value(),
310 planets,
311 unavailable_bodies: unavailable,
312 houses: house_cusps,
313 aspects,
314 fixed_stars: stars,
315 arabic_parts: lots,
316 temperament,
317 lunar_status: lunar,
318 })
319}