Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ pub struct App {

pub selected_bodies: Vec<bool>,
pub almanac_picker_sel: usize,
pub almanac_show_times: bool,

pub forecasts: Option<Vec<HourlyForecast>>,
pub weather_loading: bool,
Expand Down Expand Up @@ -152,6 +153,7 @@ impl App {
almanac: AlmanacInfo { tracks: Vec::new(), current_step: 0 },
selected_bodies: Vec::new(),
almanac_picker_sel: 0,
almanac_show_times: false,
sun_moon: SunMoonInfo {
sun_stereo: None,
moon_stereo: None,
Expand Down
3 changes: 3 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ fn run(
app.input_mode = InputMode::LocationPicker;
app.picker_sel = app.location_index;
}
KeyCode::Char('t') if matches!(app.tab, Tab::Almanac) => {
app.almanac_show_times = !app.almanac_show_times;
}
KeyCode::Char('t') | KeyCode::Char('T') => {
app.input_mode = InputMode::EditingDatetime;
app.input_buf = if let Some(tz) = app.timezone {
Expand Down
104 changes: 97 additions & 7 deletions src/sky.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use astronomy_engine_bindings::{
Astronomy_Ecliptic, Astronomy_Equator, Astronomy_HelioVector, Astronomy_Horizon,
Astronomy_Illumination, astro_aberration_t_ABERRATION, astro_body_t_BODY_EARTH,
astro_body_t_BODY_JUPITER, astro_body_t_BODY_MARS, astro_body_t_BODY_MERCURY,
astro_body_t_BODY_MOON, astro_body_t_BODY_NEPTUNE, astro_body_t_BODY_SATURN,
astro_body_t_BODY_SUN, astro_body_t_BODY_URANUS, astro_body_t_BODY_VENUS,
Astronomy_Illumination, Astronomy_SearchRiseSetEx, astro_aberration_t_ABERRATION,
astro_body_t_BODY_EARTH, astro_body_t_BODY_JUPITER, astro_body_t_BODY_MARS,
astro_body_t_BODY_MERCURY, astro_body_t_BODY_MOON, astro_body_t_BODY_NEPTUNE,
astro_body_t_BODY_SATURN, astro_body_t_BODY_SUN, astro_body_t_BODY_URANUS,
astro_body_t_BODY_VENUS, astro_direction_t_DIRECTION_RISE, astro_direction_t_DIRECTION_SET,
astro_equator_date_t_EQUATOR_OF_DATE, astro_observer_t, astro_refraction_t_REFRACTION_NORMAL,
astro_status_t_ASTRO_SUCCESS,
astro_status_t_ASTRO_SUCCESS, astro_time_t,
};
use chrono::{DateTime, Duration, TimeZone, Utc};
use stellui::astro::{
Expand Down Expand Up @@ -236,8 +237,95 @@ pub struct AlmanacTrack {
pub name: &'static str,
pub symbol: &'static str,
pub color_rgb: (u8, u8, u8),
/// altitude in degrees (-90..90) for each step; index 0 = UTC midnight
/// altitude in degrees (-90..90) for each step; index 0 = local midnight
pub altitudes: [f64; ALMANAC_STEPS],
pub rise: Option<DateTime<Utc>>,
pub transit: Option<DateTime<Utc>>,
pub transit_alt: Option<f64>,
pub set: Option<DateTime<Utc>>,
}

fn astro_time_to_utc(t: astro_time_t) -> DateTime<Utc> {
// t.ut = days since J2000.0 (2000-01-01 12:00:00 UTC)
use chrono::TimeZone;
let j2000 = Utc.with_ymd_and_hms(2000, 1, 1, 12, 0, 0).unwrap();
let micros = (t.ut * 86_400.0 * 1_000_000.0) as i64;
j2000 + Duration::microseconds(micros)
}

#[allow(clippy::type_complexity)]
fn compute_rise_set_transit(
body: i32,
observer: astro_observer_t,
day_start: DateTime<Utc>,
altitudes: &[f64; ALMANAC_STEPS],
height: f64,
) -> (Option<DateTime<Utc>>, Option<DateTime<Utc>>, Option<DateTime<Utc>>, Option<f64>) {
let all_up = altitudes.iter().all(|&a| a > 0.0);
let all_down = altitudes.iter().all(|&a| a <= 0.0);

// Transit: peak of altitude array with parabolic interpolation
let peak_idx = altitudes
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(i, _)| i)
.unwrap_or(0);

let transit_alt = altitudes[peak_idx];

let offset = if peak_idx > 0 && peak_idx < ALMANAC_STEPS - 1 {
let y0 = altitudes[peak_idx - 1];
let y1 = altitudes[peak_idx];
let y2 = altitudes[peak_idx + 1];
let denom = 2.0 * (2.0 * y1 - y0 - y2);
if denom.abs() > 1e-9 { (y0 - y2) / denom } else { 0.0 }
} else {
0.0
};

let transit_mins = (peak_idx as f64 + offset) * 15.0;
let transit = Some(day_start + Duration::seconds((transit_mins * 60.0) as i64));

if all_up || all_down {
return (None, transit, None, Some(transit_alt));
}

let start_time = astro_time_from_datetime(day_start);

let rise = unsafe {
let result = Astronomy_SearchRiseSetEx(
body,
observer,
astro_direction_t_DIRECTION_RISE,
start_time,
1.0,
height,
);
if result.status == astro_status_t_ASTRO_SUCCESS {
Some(astro_time_to_utc(result.time))
} else {
None
}
};

let set = unsafe {
let result = Astronomy_SearchRiseSetEx(
body,
observer,
astro_direction_t_DIRECTION_SET,
start_time,
1.0,
height,
);
if result.status == astro_status_t_ASTRO_SUCCESS {
Some(astro_time_to_utc(result.time))
} else {
None
}
};

(rise, transit, set, Some(transit_alt))
}

pub struct AlmanacInfo {
Expand Down Expand Up @@ -305,7 +393,9 @@ pub fn compute_almanac(lat: f64, lon: f64, height: f64, datetime: DateTime<Utc>,
}
};
}
AlmanacTrack { name, symbol, color_rgb, altitudes }
let (rise, transit, set, transit_alt) =
compute_rise_set_transit(body, observer, day_start, &altitudes, height);
AlmanacTrack { name, symbol, color_rgb, altitudes, rise, transit, transit_alt, set }
}).collect();

AlmanacInfo { tracks, current_step }
Expand Down
92 changes: 71 additions & 21 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ fn render_status(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
Tab::SolarSystem =>
" [L]locations [T]time [Z]tz [N]now [Space]pause [,/.]speed [S/W/P/A]tab [Q]quit",
Tab::Almanac =>
" [L]locations [T]time [Z]tz [N]now [Space]pause [,/.]speed [b]bodies [S/W/P/A]tab [Q]quit",
" [L]locations [T]time [Z]tz [N]now [Space]pause [,/.]speed [b]bodies [t]times [S/W/P/A]tab [Q]quit",
};

let text = vec![Line::from(line1), Line::from(line2)];
Expand Down Expand Up @@ -1070,30 +1070,80 @@ fn render_almanac_legend(f: &mut Frame, app: &App, area: ratatui::layout::Rect)
Style::default().fg(Color::DarkGray),
)),
Line::from(""),
Line::from(Span::styled(
" Body Alt",
Style::default().add_modifier(Modifier::BOLD),
)),
];

for (i, track) in app.almanac.tracks.iter().enumerate() {
let visible = app.selected_bodies.get(i).copied().unwrap_or(true);
let alt = track.altitudes[app.almanac.current_step];
let (r, g, b) = track.color_rgb;
let label = if alt > 0.0 {
format!(" {} {} {:.1}°", track.symbol, track.name, alt)
} else {
format!(" {} {} below", track.symbol, track.name)
};
let style = if visible {
Style::default().fg(Color::Rgb(r, g, b))
} else {
Style::default().fg(Color::DarkGray).add_modifier(Modifier::DIM)
if app.almanac_show_times {
text.push(Line::from(Span::styled(
" Rise Trans Set ",
Style::default().add_modifier(Modifier::BOLD),
)));

let fmt = |dt: Option<chrono::DateTime<chrono::Utc>>| -> String {
match dt {
None => "--:--".to_string(),
Some(utc) => {
if let Some(tz) = app.timezone {
utc.with_timezone(&tz).format("%H:%M").to_string()
} else {
utc.format("%H:%M").to_string()
}
}
}
};
text.push(Line::from(Span::styled(label, style)));

for (i, track) in app.almanac.tracks.iter().enumerate() {
let visible = app.selected_bodies.get(i).copied().unwrap_or(true);
let (r, g, b) = track.color_rgb;

let all_down = track.altitudes.iter().all(|&a| a <= 0.0);
let all_up = track.altitudes.iter().all(|&a| a > 0.0);

let label = if all_down {
format!(" {} {} below horizon", track.symbol, track.name)
} else if all_up {
let max_alt = track.transit_alt.map(|a| format!(" ({:.0}°)", a)).unwrap_or_default();
format!(" {} {} always up{}", track.symbol, track.name, max_alt)
} else {
format!(
" {} {} {} {}",
track.symbol,
fmt(track.rise),
fmt(track.transit),
fmt(track.set),
)
};
let style = if visible {
Style::default().fg(Color::Rgb(r, g, b))
} else {
Style::default().fg(Color::DarkGray).add_modifier(Modifier::DIM)
};
text.push(Line::from(Span::styled(label, style)));
}
} else {
text.push(Line::from(Span::styled(
" Body Alt",
Style::default().add_modifier(Modifier::BOLD),
)));

for (i, track) in app.almanac.tracks.iter().enumerate() {
let visible = app.selected_bodies.get(i).copied().unwrap_or(true);
let alt = track.altitudes[app.almanac.current_step];
let (r, g, b) = track.color_rgb;
let label = if alt > 0.0 {
format!(" {} {} {:.1}°", track.symbol, track.name, alt)
} else {
format!(" {} {} below", track.symbol, track.name)
};
let style = if visible {
Style::default().fg(Color::Rgb(r, g, b))
} else {
Style::default().fg(Color::DarkGray).add_modifier(Modifier::DIM)
};
text.push(Line::from(Span::styled(label, style)));
}
}

let para =
Paragraph::new(text).block(Block::default().borders(Borders::ALL).title(" Legend "));
let title = if app.almanac_show_times { " Times [t] " } else { " Legend [t] " };
let para = Paragraph::new(text).block(Block::default().borders(Borders::ALL).title(title));
f.render_widget(para, area);
}
Loading