1use serde::{Serialize, Serializer};
5use serde_json::Value;
6
7use crate::aspects::{natal_aspects, Aspect, OrbSettings, Point};
8use crate::dignities::{
9 antiscia, condition, is_diurnal, receptions, AntisciaContact, Condition, DignityScheme,
10 Reception,
11};
12use crate::ephemeris::KernelSet;
13use crate::error::EngineError;
14use crate::fixed_stars::{fixed_stars, FixedStarPosition};
15use crate::horary::{planetary_hours, PlanetaryHours};
16use crate::houses::{calculate_houses, HouseSystem, Houses};
17use crate::instant::UtcInstant;
18use crate::lots::{arabic_parts, Lot};
19use crate::lunar::{lunar_status, LunarStatus};
20use crate::planets::calculate_planets;
21use crate::symbolic::symbolic_degree_number;
22use crate::temperament::{temperament, Temperament};
23use crate::zodiac::{
24 ayanamsa, ayanamsa_info, determine_house, longitude_to_zodiac, AyanamsaInfo, DEFAULT_AYANAMSA,
25};
26
27const EXPECTED_BODIES: [&str; 16] = [
30 "Sun",
31 "Moon",
32 "Mercury",
33 "Venus",
34 "Mars",
35 "Jupiter",
36 "Saturn",
37 "Uranus",
38 "Neptune",
39 "Pluto",
40 "North Node",
41 "South Node",
42 "Chiron",
43 "Lilith",
44 "Ascendant",
45 "Midheaven",
46];
47
48#[derive(Debug, Clone)]
50pub struct ChartRequest<'a> {
51 pub instant: UtcInstant,
53 pub latitude: f64,
55 pub longitude: f64,
57 pub house_system: &'a str,
59 pub zodiac_type: &'a str,
61 pub ayanamsa: &'a str,
63 pub orb_settings: Option<&'a Value>,
65 pub dignity_scheme: &'a str,
67}
68
69impl<'a> ChartRequest<'a> {
70 pub fn new(instant: UtcInstant, latitude: f64, longitude: f64) -> Self {
72 ChartRequest {
73 instant,
74 latitude,
75 longitude,
76 house_system: "P",
77 zodiac_type: "tropical",
78 ayanamsa: DEFAULT_AYANAMSA,
79 orb_settings: None,
80 dignity_scheme: "lilly",
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize)]
87pub struct Placement {
88 pub name: &'static str,
90 pub symbol: &'static str,
92 pub sign: &'static str,
94 pub sign_symbol: &'static str,
96 pub degree: i64,
98 pub minute: i64,
100 pub ecliptic_longitude: f64,
102 pub house: u8,
104 pub speed: f64,
106 pub is_retrograde: bool,
108 pub symbolic_degree: i64,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub condition: Option<Condition>,
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize)]
117pub struct HouseCusp {
118 pub house_number: u8,
120 pub sign: &'static str,
122 pub sign_symbol: &'static str,
124 pub degree: i64,
126 pub minute: i64,
128 pub ecliptic_longitude: f64,
130 pub symbolic_degree: i64,
132}
133
134#[derive(Debug, Clone, PartialEq, Serialize)]
136pub struct Chart {
137 pub house_system: String,
139 pub zodiac_type: &'static str,
141 pub ayanamsa: Option<&'static str>,
143 pub ayanamsa_name: Option<&'static str>,
145 pub ayanamsa_value: Option<f64>,
147 pub ayanamsa_formatted: Option<String>,
149 pub precession_rate_arcsec_yr: f64,
151 pub orb_settings: Value,
153 pub planets: Vec<Placement>,
155 pub unavailable_bodies: Vec<&'static str>,
157 pub houses: Vec<HouseCusp>,
159 pub aspects: Vec<Aspect>,
161 pub fixed_stars: Vec<FixedStarPosition>,
163 pub arabic_parts: Vec<Lot>,
165 pub temperament: Temperament,
167 #[serde(serialize_with = "empty_object_if_none")]
169 pub lunar_status: Option<LunarStatus>,
170 pub sect: Option<&'static str>,
172 pub dignity_scheme: &'static str,
174 pub receptions: Vec<Reception>,
176 pub antiscia: Vec<AntisciaContact>,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub planetary_hours: Option<PlanetaryHours>,
181}
182
183fn empty_object_if_none<S: Serializer>(v: &Option<LunarStatus>, s: S) -> Result<S::Ok, S::Error> {
184 match v {
185 Some(status) => status.serialize(s),
186 None => serde_json::Map::new().serialize(s),
187 }
188}
189
190impl Chart {
191 pub fn points(&self) -> Vec<Point<'_>> {
193 self.planets
194 .iter()
195 .map(|p| Point {
196 name: p.name,
197 longitude: p.ecliptic_longitude,
198 })
199 .collect()
200 }
201}
202
203fn symbol_of(name: &str) -> &'static str {
204 match name {
205 "Sun" => "☉",
206 "Moon" => "☽",
207 "Mercury" => "☿",
208 "Venus" => "♀",
209 "Mars" => "♂",
210 "Jupiter" => "♃",
211 "Saturn" => "♄",
212 "Uranus" => "♅",
213 "Neptune" => "♆",
214 "Pluto" => "♇",
215 "North Node" => "☊",
216 "South Node" => "☋",
217 "Chiron" => "⚷",
218 "Lilith" => "⚸",
219 "Ascendant" => "ASC",
220 "Midheaven" => "MC",
221 _ => "",
222 }
223}
224
225pub(crate) struct Sky {
228 pub zodiac_type: &'static str,
229 pub is_sidereal: bool,
230 pub ayanamsa_code: &'static str,
231 pub info: AyanamsaInfo,
232 pub jd: f64,
233 pub cusps: Vec<f64>,
235 pub house_cusps: Vec<HouseCusp>,
236 pub planets: Vec<Placement>,
237}
238
239pub(crate) fn sky(kernels: &KernelSet, req: &ChartRequest) -> Result<Sky, EngineError> {
241 let zodiac_type = match req.zodiac_type.trim().to_lowercase().as_str() {
242 "sidereal" => "sidereal",
243 _ => "tropical",
244 };
245 let is_sidereal = zodiac_type == "sidereal";
246 let ayanamsa_code = ayanamsa(req.ayanamsa).code;
247
248 let jd = req.instant.julian_day();
249 let info = ayanamsa_info(jd, ayanamsa_code);
250 let shift = if is_sidereal { info.value } else { 0.0 };
251
252 let engine = calculate_planets(kernels, jd, shift)?;
253 let houses = calculate_houses(
254 jd,
255 req.latitude,
256 req.longitude,
257 HouseSystem::from_code(req.house_system),
258 shift,
259 );
260 let cusps = houses.cusps.to_vec();
261
262 let house_cusps: Vec<HouseCusp> = houses
263 .cusps
264 .iter()
265 .enumerate()
266 .map(|(i, &lon)| {
267 let z = longitude_to_zodiac(lon);
268 HouseCusp {
269 house_number: i as u8 + 1,
270 sign: z.sign.name,
271 sign_symbol: z.sign.symbol,
272 degree: z.degree,
273 minute: z.minute,
274 ecliptic_longitude: lon,
275 symbolic_degree: symbolic_degree_number(z.degree, z.minute),
276 }
277 })
278 .collect();
279
280 let raw: Vec<(&'static str, f64, f64, bool)> = engine
281 .bodies
282 .iter()
283 .map(|b| (b.name, b.longitude, b.speed, b.is_retrograde))
284 .collect();
285 let planets = placements(raw, &houses);
286
287 Ok(Sky {
288 zodiac_type,
289 is_sidereal,
290 ayanamsa_code,
291 info,
292 jd,
293 cusps,
294 house_cusps,
295 planets,
296 })
297}
298
299pub(crate) fn placements(
302 mut raw: Vec<(&'static str, f64, f64, bool)>,
303 houses: &Houses,
304) -> Vec<Placement> {
305 raw.push(("Ascendant", houses.ascendant, 0.0, false));
306 raw.push(("Midheaven", houses.midheaven, 0.0, false));
307 raw.iter()
308 .map(|&(name, lon, speed, is_retrograde)| {
309 let z = longitude_to_zodiac(lon);
310 Placement {
311 name,
312 symbol: symbol_of(name),
313 sign: z.sign.name,
314 sign_symbol: z.sign.symbol,
315 degree: z.degree,
316 minute: z.minute,
317 ecliptic_longitude: lon,
318 house: determine_house(lon, &houses.cusps),
319 speed,
320 is_retrograde,
321 symbolic_degree: symbolic_degree_number(z.degree, z.minute),
322 condition: None,
323 }
324 })
325 .collect()
326}
327
328fn add_conditions(planets: &mut [Placement], scheme: DignityScheme) -> Option<&'static str> {
330 let at = |name: &str| {
331 planets
332 .iter()
333 .find(|p| p.name == name)
334 .map(|p| p.ecliptic_longitude)
335 };
336 let (sun, asc) = (at("Sun")?, at("Ascendant")?);
337 for planet in planets.iter_mut() {
338 planet.condition = condition(planet, sun, asc, scheme);
339 }
340 Some(if is_diurnal(sun, asc) {
341 "diurnal"
342 } else {
343 "nocturnal"
344 })
345}
346
347pub fn calculate_chart(kernels: &KernelSet, req: &ChartRequest) -> Result<Chart, EngineError> {
349 let mut chart = chart_without_hours(kernels, req)?;
350 chart.planetary_hours = Some(planetary_hours(
351 kernels,
352 req.instant,
353 req.latitude,
354 req.longitude,
355 ));
356 Ok(chart)
357}
358
359pub(crate) fn chart_without_hours(
362 kernels: &KernelSet,
363 req: &ChartRequest,
364) -> Result<Chart, EngineError> {
365 let orbs = OrbSettings::merge(req.orb_settings)?;
366 let scheme = DignityScheme::from_code(req.dignity_scheme);
367 let Sky {
368 zodiac_type,
369 is_sidereal,
370 ayanamsa_code,
371 info,
372 jd,
373 cusps,
374 house_cusps,
375 mut planets,
376 } = sky(kernels, req)?;
377 let sect = add_conditions(&mut planets, scheme);
378 let points: Vec<Point> = planets
379 .iter()
380 .map(|p| Point {
381 name: p.name,
382 longitude: p.ecliptic_longitude,
383 })
384 .collect();
385
386 let mut aspects = natal_aspects(&points, &orbs)?;
387 let (stars, star_aspects) = fixed_stars(
388 jd,
389 &cusps,
390 &points,
391 orbs.fixed_star_orb()?,
392 is_sidereal.then_some(info.value),
393 );
394 aspects.extend(star_aspects);
395
396 let lots = arabic_parts(&points, &cusps);
397 let temperament = temperament(&planets, &aspects);
398 let lunar = lunar_status(&planets);
399 let chart_receptions = receptions(&planets, &aspects, scheme);
400 let chart_antiscia = antiscia(&planets);
401
402 let mut unavailable: Vec<&'static str> = EXPECTED_BODIES
403 .iter()
404 .copied()
405 .filter(|name| !planets.iter().any(|p| p.name == *name))
406 .collect();
407 unavailable.sort_unstable();
408
409 Ok(Chart {
410 house_system: req.house_system.to_string(),
411 zodiac_type,
412 ayanamsa: is_sidereal.then_some(ayanamsa_code),
413 ayanamsa_name: is_sidereal.then_some(info.name),
414 ayanamsa_value: is_sidereal.then_some(info.value),
415 ayanamsa_formatted: is_sidereal.then(|| info.formatted.clone()),
416 precession_rate_arcsec_yr: info.precession_rate_arcsec_yr,
417 orb_settings: orbs.to_value(),
418 planets,
419 unavailable_bodies: unavailable,
420 houses: house_cusps,
421 aspects,
422 fixed_stars: stars,
423 arabic_parts: lots,
424 temperament,
425 lunar_status: lunar,
426 sect,
427 dignity_scheme: scheme.code(),
428 receptions: chart_receptions,
429 antiscia: chart_antiscia,
430 planetary_hours: None,
431 })
432}