1use serde::{Serialize, Serializer};
5use serde_json::Value;
6
7use crate::aspects::{natal_aspects, Aspect, OrbSettings, Point};
8use crate::chinese::calendar::{chinese_calendar, ChineseCalendar};
9use crate::dignities::{
10 antiscia, condition, is_diurnal, receptions, AntisciaContact, Body, Condition, DignityScheme,
11 Reception,
12};
13use crate::ephemeris::KernelSet;
14use crate::error::EngineError;
15use crate::fixed_stars::{fixed_stars, FixedStarPosition};
16use crate::horary::{planetary_hours, PlanetaryHours};
17use crate::houses::{calculate_houses, HouseSystem, Houses};
18use crate::instant::UtcInstant;
19use crate::lots::{arabic_parts, Lot};
20use crate::lunar::{lunar_status, LunarStatus};
21use crate::planets::calculate_planets;
22use crate::symbolic::symbolic_degree_number;
23use crate::temperament::{temperament, Temperament};
24use crate::zodiac::{
25 ayanamsa, ayanamsa_info, determine_house, longitude_to_zodiac, AyanamsaInfo, DEFAULT_AYANAMSA,
26};
27
28const EXPECTED_BODIES: [&str; 16] = [
31 "Sun",
32 "Moon",
33 "Mercury",
34 "Venus",
35 "Mars",
36 "Jupiter",
37 "Saturn",
38 "Uranus",
39 "Neptune",
40 "Pluto",
41 "North Node",
42 "South Node",
43 "Chiron",
44 "Lilith",
45 "Ascendant",
46 "Midheaven",
47];
48
49#[derive(Debug, Clone)]
51pub struct ChartRequest<'a> {
52 pub instant: UtcInstant,
54 pub latitude: f64,
56 pub longitude: f64,
58 pub house_system: &'a str,
60 pub zodiac_type: &'a str,
62 pub ayanamsa: &'a str,
64 pub orb_settings: Option<&'a Value>,
66 pub dignity_scheme: &'a str,
68 pub chinese_calendar: bool,
71}
72
73impl<'a> ChartRequest<'a> {
74 pub fn new(instant: UtcInstant, latitude: f64, longitude: f64) -> Self {
76 ChartRequest {
77 instant,
78 latitude,
79 longitude,
80 house_system: "P",
81 zodiac_type: "tropical",
82 ayanamsa: DEFAULT_AYANAMSA,
83 orb_settings: None,
84 dignity_scheme: "lilly",
85 chinese_calendar: false,
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize)]
92pub struct Placement {
93 pub name: &'static str,
95 pub symbol: &'static str,
97 pub sign: &'static str,
99 pub sign_symbol: &'static str,
101 pub degree: i64,
103 pub minute: i64,
105 pub ecliptic_longitude: f64,
107 pub house: u8,
109 pub speed: f64,
111 pub is_retrograde: bool,
113 pub symbolic_degree: i64,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub condition: Option<Condition>,
118}
119
120#[derive(Debug, Clone, PartialEq, Serialize)]
122pub struct HouseCusp {
123 pub house_number: u8,
125 pub sign: &'static str,
127 pub sign_symbol: &'static str,
129 pub degree: i64,
131 pub minute: i64,
133 pub ecliptic_longitude: f64,
135 pub symbolic_degree: i64,
137}
138
139#[derive(Debug, Clone, PartialEq, Serialize)]
141pub struct Chart {
142 pub house_system: String,
144 pub zodiac_type: &'static str,
146 pub ayanamsa: Option<&'static str>,
148 pub ayanamsa_name: Option<&'static str>,
150 pub ayanamsa_value: Option<f64>,
152 pub ayanamsa_formatted: Option<String>,
154 pub precession_rate_arcsec_yr: f64,
156 pub orb_settings: Value,
158 pub planets: Vec<Placement>,
160 pub unavailable_bodies: Vec<&'static str>,
162 pub houses: Vec<HouseCusp>,
164 pub aspects: Vec<Aspect>,
166 pub fixed_stars: Vec<FixedStarPosition>,
168 pub arabic_parts: Vec<Lot>,
170 pub temperament: Temperament,
172 #[serde(serialize_with = "empty_object_if_none")]
174 pub lunar_status: Option<LunarStatus>,
175 pub sect: Option<&'static str>,
177 pub dignity_scheme: &'static str,
179 pub receptions: Vec<Reception>,
181 pub antiscia: Vec<AntisciaContact>,
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub planetary_hours: Option<PlanetaryHours>,
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub chinese_calendar: Option<ChineseCalendar>,
189}
190
191fn empty_object_if_none<S: Serializer>(v: &Option<LunarStatus>, s: S) -> Result<S::Ok, S::Error> {
192 match v {
193 Some(status) => status.serialize(s),
194 None => serde_json::Map::new().serialize(s),
195 }
196}
197
198impl Chart {
199 pub fn points(&self) -> Vec<Point<'_>> {
201 self.planets
202 .iter()
203 .map(|p| Point {
204 name: p.name,
205 longitude: p.ecliptic_longitude,
206 })
207 .collect()
208 }
209}
210
211fn symbol_of(name: &str) -> &'static str {
212 match name {
213 "Sun" => "☉",
214 "Moon" => "☽",
215 "Mercury" => "☿",
216 "Venus" => "♀",
217 "Mars" => "♂",
218 "Jupiter" => "♃",
219 "Saturn" => "♄",
220 "Uranus" => "♅",
221 "Neptune" => "♆",
222 "Pluto" => "♇",
223 "North Node" => "☊",
224 "South Node" => "☋",
225 "Chiron" => "⚷",
226 "Lilith" => "⚸",
227 "Ascendant" => "ASC",
228 "Midheaven" => "MC",
229 _ => "",
230 }
231}
232
233pub(crate) struct Sky {
236 pub zodiac_type: &'static str,
237 pub is_sidereal: bool,
238 pub ayanamsa_code: &'static str,
239 pub info: AyanamsaInfo,
240 pub jd: f64,
241 pub cusps: Vec<f64>,
243 pub house_cusps: Vec<HouseCusp>,
244 pub planets: Vec<Placement>,
245}
246
247pub(crate) fn sky(kernels: &KernelSet, req: &ChartRequest) -> Result<Sky, EngineError> {
249 let zodiac_type = match req.zodiac_type.trim().to_lowercase().as_str() {
250 "sidereal" => "sidereal",
251 _ => "tropical",
252 };
253 let is_sidereal = zodiac_type == "sidereal";
254 let ayanamsa_code = ayanamsa(req.ayanamsa).code;
255
256 let jd = req.instant.julian_day();
257 let info = ayanamsa_info(jd, ayanamsa_code);
258 let shift = if is_sidereal { info.value } else { 0.0 };
259
260 let engine = calculate_planets(kernels, jd, shift)?;
261 let houses = calculate_houses(
262 jd,
263 req.latitude,
264 req.longitude,
265 HouseSystem::from_code(req.house_system),
266 shift,
267 );
268 let cusps = houses.cusps.to_vec();
269
270 let house_cusps: Vec<HouseCusp> = houses
271 .cusps
272 .iter()
273 .enumerate()
274 .map(|(i, &lon)| {
275 let z = longitude_to_zodiac(lon);
276 HouseCusp {
277 house_number: i as u8 + 1,
278 sign: z.sign.name,
279 sign_symbol: z.sign.symbol,
280 degree: z.degree,
281 minute: z.minute,
282 ecliptic_longitude: lon,
283 symbolic_degree: symbolic_degree_number(z.degree, z.minute),
284 }
285 })
286 .collect();
287
288 let raw: Vec<(&'static str, f64, f64, bool)> = engine
289 .bodies
290 .iter()
291 .map(|b| (b.name, b.longitude, b.speed, b.is_retrograde))
292 .collect();
293 let planets = placements(raw, &houses);
294
295 Ok(Sky {
296 zodiac_type,
297 is_sidereal,
298 ayanamsa_code,
299 info,
300 jd,
301 cusps,
302 house_cusps,
303 planets,
304 })
305}
306
307pub(crate) fn placements(
310 mut raw: Vec<(&'static str, f64, f64, bool)>,
311 houses: &Houses,
312) -> Vec<Placement> {
313 raw.push(("Ascendant", houses.ascendant, 0.0, false));
314 raw.push(("Midheaven", houses.midheaven, 0.0, false));
315 raw.iter()
316 .map(|&(name, lon, speed, is_retrograde)| {
317 let z = longitude_to_zodiac(lon);
318 Placement {
319 name,
320 symbol: symbol_of(name),
321 sign: z.sign.name,
322 sign_symbol: z.sign.symbol,
323 degree: z.degree,
324 minute: z.minute,
325 ecliptic_longitude: lon,
326 house: determine_house(lon, &houses.cusps),
327 speed,
328 is_retrograde,
329 symbolic_degree: symbolic_degree_number(z.degree, z.minute),
330 condition: None,
331 }
332 })
333 .collect()
334}
335
336fn add_conditions(planets: &mut [Placement], scheme: DignityScheme) -> Option<&'static str> {
338 let at = |name: &str| {
339 planets
340 .iter()
341 .find(|p| p.name == name)
342 .map(|p| p.ecliptic_longitude)
343 };
344 let (sun, asc) = (at("Sun")?, at("Ascendant")?);
345 for planet in planets.iter_mut() {
346 planet.condition = condition(Body::from(&*planet), sun, asc, scheme);
347 }
348 Some(if is_diurnal(sun, asc) {
349 "diurnal"
350 } else {
351 "nocturnal"
352 })
353}
354
355pub fn calculate_chart(kernels: &KernelSet, req: &ChartRequest) -> Result<Chart, EngineError> {
357 let mut chart = chart_without_hours(kernels, req)?;
358 chart.planetary_hours = Some(planetary_hours(
359 kernels,
360 req.instant,
361 req.latitude,
362 req.longitude,
363 ));
364 Ok(chart)
365}
366
367pub(crate) fn chart_without_hours(
370 kernels: &KernelSet,
371 req: &ChartRequest,
372) -> Result<Chart, EngineError> {
373 let orbs = OrbSettings::merge(req.orb_settings)?;
374 let scheme = DignityScheme::from_code(req.dignity_scheme);
375 let Sky {
376 zodiac_type,
377 is_sidereal,
378 ayanamsa_code,
379 info,
380 jd,
381 cusps,
382 house_cusps,
383 mut planets,
384 } = sky(kernels, req)?;
385 let sect = add_conditions(&mut planets, scheme);
386 let points: Vec<Point> = planets
387 .iter()
388 .map(|p| Point {
389 name: p.name,
390 longitude: p.ecliptic_longitude,
391 })
392 .collect();
393
394 let mut aspects = natal_aspects(&points, &orbs)?;
395 let (stars, star_aspects) = fixed_stars(
396 jd,
397 &cusps,
398 &points,
399 orbs.fixed_star_orb()?,
400 is_sidereal.then_some(info.value),
401 );
402 aspects.extend(star_aspects);
403
404 let lots = arabic_parts(&points, &cusps);
405 let temperament = temperament(&planets, &aspects);
406 let lunar = lunar_status(&planets);
407 let bodies: Vec<Body> = planets.iter().map(Body::from).collect();
408 let aspect_between = |a: &str, b: &str| {
409 aspects
410 .iter()
411 .find(|x| (x.body1 == a && x.body2 == b) || (x.body1 == b && x.body2 == a))
412 .map(|x| x.aspect_type)
413 };
414 let chart_receptions = receptions(&bodies, aspect_between, scheme);
415 let chart_antiscia = antiscia(&planets);
416
417 let mut unavailable: Vec<&'static str> = EXPECTED_BODIES
418 .iter()
419 .copied()
420 .filter(|name| !planets.iter().any(|p| p.name == *name))
421 .collect();
422 unavailable.sort_unstable();
423
424 Ok(Chart {
425 house_system: req.house_system.to_string(),
426 zodiac_type,
427 ayanamsa: is_sidereal.then_some(ayanamsa_code),
428 ayanamsa_name: is_sidereal.then_some(info.name),
429 ayanamsa_value: is_sidereal.then_some(info.value),
430 ayanamsa_formatted: is_sidereal.then(|| info.formatted.clone()),
431 precession_rate_arcsec_yr: info.precession_rate_arcsec_yr,
432 orb_settings: orbs.to_value(),
433 planets,
434 unavailable_bodies: unavailable,
435 houses: house_cusps,
436 aspects,
437 fixed_stars: stars,
438 arabic_parts: lots,
439 temperament,
440 lunar_status: lunar,
441 sect,
442 dignity_scheme: scheme.code(),
443 receptions: chart_receptions,
444 antiscia: chart_antiscia,
445 planetary_hours: None,
446 chinese_calendar: if req.chinese_calendar {
447 chinese_calendar(kernels, req.instant)?
448 } else {
449 None
450 },
451 })
452}