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, Houses};
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, AyanamsaInfo, 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(crate) struct Sky {
206 pub zodiac_type: &'static str,
207 pub is_sidereal: bool,
208 pub ayanamsa_code: &'static str,
209 pub info: AyanamsaInfo,
210 pub jd: f64,
211 pub cusps: Vec<f64>,
213 pub house_cusps: Vec<HouseCusp>,
214 pub planets: Vec<Placement>,
215}
216
217pub(crate) fn sky(kernels: &KernelSet, req: &ChartRequest) -> Result<Sky, EngineError> {
219 let zodiac_type = match req.zodiac_type.trim().to_lowercase().as_str() {
220 "sidereal" => "sidereal",
221 _ => "tropical",
222 };
223 let is_sidereal = zodiac_type == "sidereal";
224 let ayanamsa_code = ayanamsa(req.ayanamsa).code;
225
226 let jd = req.instant.julian_day();
227 let info = ayanamsa_info(jd, ayanamsa_code);
228 let shift = if is_sidereal { info.value } else { 0.0 };
229
230 let engine = calculate_planets(kernels, jd, shift)?;
231 let houses = calculate_houses(
232 jd,
233 req.latitude,
234 req.longitude,
235 HouseSystem::from_code(req.house_system),
236 shift,
237 );
238 let cusps = houses.cusps.to_vec();
239
240 let house_cusps: Vec<HouseCusp> = houses
241 .cusps
242 .iter()
243 .enumerate()
244 .map(|(i, &lon)| {
245 let z = longitude_to_zodiac(lon);
246 HouseCusp {
247 house_number: i as u8 + 1,
248 sign: z.sign.name,
249 sign_symbol: z.sign.symbol,
250 degree: z.degree,
251 minute: z.minute,
252 ecliptic_longitude: lon,
253 symbolic_degree: symbolic_degree_number(z.degree, z.minute),
254 }
255 })
256 .collect();
257
258 let raw: Vec<(&'static str, f64, f64, bool)> = engine
259 .bodies
260 .iter()
261 .map(|b| (b.name, b.longitude, b.speed, b.is_retrograde))
262 .collect();
263 let planets = placements(raw, &houses);
264
265 Ok(Sky {
266 zodiac_type,
267 is_sidereal,
268 ayanamsa_code,
269 info,
270 jd,
271 cusps,
272 house_cusps,
273 planets,
274 })
275}
276
277pub(crate) fn placements(
280 mut raw: Vec<(&'static str, f64, f64, bool)>,
281 houses: &Houses,
282) -> Vec<Placement> {
283 raw.push(("Ascendant", houses.ascendant, 0.0, false));
284 raw.push(("Midheaven", houses.midheaven, 0.0, false));
285 raw.iter()
286 .map(|&(name, lon, speed, is_retrograde)| {
287 let z = longitude_to_zodiac(lon);
288 Placement {
289 name,
290 symbol: symbol_of(name),
291 sign: z.sign.name,
292 sign_symbol: z.sign.symbol,
293 degree: z.degree,
294 minute: z.minute,
295 ecliptic_longitude: lon,
296 house: determine_house(lon, &houses.cusps),
297 speed,
298 is_retrograde,
299 symbolic_degree: symbolic_degree_number(z.degree, z.minute),
300 }
301 })
302 .collect()
303}
304
305pub fn calculate_chart(kernels: &KernelSet, req: &ChartRequest) -> Result<Chart, EngineError> {
307 let orbs = OrbSettings::merge(req.orb_settings)?;
308 let Sky {
309 zodiac_type,
310 is_sidereal,
311 ayanamsa_code,
312 info,
313 jd,
314 cusps,
315 house_cusps,
316 planets,
317 } = sky(kernels, req)?;
318 let points: Vec<Point> = planets
319 .iter()
320 .map(|p| Point {
321 name: p.name,
322 longitude: p.ecliptic_longitude,
323 })
324 .collect();
325
326 let mut aspects = natal_aspects(&points, &orbs)?;
327 let (stars, star_aspects) = fixed_stars(
328 jd,
329 &cusps,
330 &points,
331 orbs.fixed_star_orb()?,
332 is_sidereal.then_some(info.value),
333 );
334 aspects.extend(star_aspects);
335
336 let lots = arabic_parts(&points, &cusps);
337 let temperament = temperament(&planets, &aspects);
338 let lunar = lunar_status(&planets);
339
340 let mut unavailable: Vec<&'static str> = EXPECTED_BODIES
341 .iter()
342 .copied()
343 .filter(|name| !planets.iter().any(|p| p.name == *name))
344 .collect();
345 unavailable.sort_unstable();
346
347 Ok(Chart {
348 house_system: req.house_system.to_string(),
349 zodiac_type,
350 ayanamsa: is_sidereal.then_some(ayanamsa_code),
351 ayanamsa_name: is_sidereal.then_some(info.name),
352 ayanamsa_value: is_sidereal.then_some(info.value),
353 ayanamsa_formatted: is_sidereal.then(|| info.formatted.clone()),
354 precession_rate_arcsec_yr: info.precession_rate_arcsec_yr,
355 orb_settings: orbs.to_value(),
356 planets,
357 unavailable_bodies: unavailable,
358 houses: house_cusps,
359 aspects,
360 fixed_stars: stars,
361 arabic_parts: lots,
362 temperament,
363 lunar_status: lunar,
364 })
365}