Files
Astronome/backend/src/api/calendar.rs
T
2026-04-10 00:02:40 +02:00

243 lines
9.8 KiB
Rust

use axum::{
extract::{Path, Query, State},
Json,
};
use chrono::NaiveDate;
use serde::Deserialize;
use crate::astronomy::{julian_date, moon_illumination};
use super::{AppError, AppState};
/// Returns new moon windows (dates where moon < 5%) with top 3 emission nebulae each.
pub async fn get_new_moon_windows(
State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, AppError> {
use sqlx::Row;
// Get all new moon dates in the next 365 days
let today = chrono::Utc::now().naive_utc().date();
let end = today + chrono::Duration::days(365);
let mut windows: Vec<serde_json::Value> = Vec::new();
let mut cur = today;
let mut prev_illum = moon_illum_for_date(cur);
while cur <= end {
let illum = moon_illum_for_date(cur);
let next_illum = moon_illum_for_date(cur + chrono::Duration::days(1));
// New moon = local minimum < 5%
if illum < 0.05 && illum <= prev_illum && illum <= next_illum {
let date_str = cur.to_string();
// Top 3 emission nebulae for this night from nightly_cache
let targets = sqlx::query(
r#"SELECT c.id, c.name, c.common_name, nc.max_alt_deg, nc.recommended_filter
FROM nightly_cache nc
JOIN catalog c ON c.id = nc.catalog_id
WHERE nc.night_date = ?
AND c.obj_type IN ('emission_nebula', 'snr', 'planetary_nebula')
AND nc.max_alt_deg >= 20
ORDER BY nc.max_alt_deg DESC
LIMIT 3"#,
)
.bind(&date_str)
.fetch_all(&state.pool)
.await?;
let top_targets: Vec<serde_json::Value> = targets.iter().map(|r| serde_json::json!({
"id": r.try_get::<String, _>("id").unwrap_or_default(),
"name": r.try_get::<String, _>("name").unwrap_or_default(),
"common_name": r.try_get::<Option<String>, _>("common_name").unwrap_or_default(),
"max_alt_deg": r.try_get::<Option<f64>, _>("max_alt_deg").unwrap_or_default(),
"recommended_filter": r.try_get::<Option<String>, _>("recommended_filter").unwrap_or_default(),
})).collect();
windows.push(serde_json::json!({
"date": date_str,
"illumination": illum,
"top_targets": top_targets,
}));
}
prev_illum = illum;
cur += chrono::Duration::days(1);
}
Ok(Json(serde_json::json!({ "windows": windows })))
}
#[derive(Debug, Deserialize)]
pub struct CalendarQuery {
pub months: Option<u32>,
}
/// Compute moon illumination for a given calendar date (at 21:00 UTC = start of night).
fn moon_illum_for_date(date: NaiveDate) -> f64 {
let dt = date.and_hms_opt(21, 0, 0)
.map(|dt| chrono::DateTime::from_naive_utc_and_offset(dt, chrono::Utc))
.unwrap_or_else(chrono::Utc::now);
let jd = julian_date(dt);
moon_illumination(jd)
}
pub async fn get_calendar(
State(state): State<AppState>,
Query(params): Query<CalendarQuery>,
) -> Result<Json<serde_json::Value>, AppError> {
let months = params.months.unwrap_or(3).min(12) as i64;
let today = chrono::Utc::now().naive_utc().date();
let end = today + chrono::Duration::days(months * 30);
// Pull nightly cache data for the date range
let rows = sqlx::query(
r#"SELECT
nc.night_date,
COUNT(CASE WHEN nc.max_alt_deg >= 15 THEN 1 END) as visible_count,
MAX(nc.usable_min) as max_usable_min,
AVG(nc.max_alt_deg) as avg_max_alt
FROM nightly_cache nc
WHERE nc.night_date >= ? AND nc.night_date <= ?
GROUP BY nc.night_date
ORDER BY nc.night_date"#,
)
.bind(today.to_string())
.bind(end.to_string())
.fetch_all(&state.pool)
.await?;
// Build a map from date string → nightly cache data
let cache_map: std::collections::HashMap<String, (i64, i64, f64)> = rows.iter().map(|r| {
use sqlx::Row;
let date = r.try_get::<Option<String>, _>("night_date").unwrap_or_default().unwrap_or_default();
let visible = r.try_get::<Option<i64>, _>("visible_count").unwrap_or_default().unwrap_or(0);
let usable = r.try_get::<Option<i64>, _>("max_usable_min").unwrap_or_default().unwrap_or(0);
let avg_alt = r.try_get::<Option<f64>, _>("avg_max_alt").unwrap_or_default().unwrap_or(0.0);
(date, (visible, usable, avg_alt))
}).collect();
// Generate a day entry for every calendar day in range (so moon is always shown)
let mut days = Vec::new();
let mut cur = today;
while cur <= end {
let date_str = cur.to_string();
let moon_illum = moon_illum_for_date(cur);
let (visible_count, max_usable_min, avg_max_alt) = cache_map.get(&date_str)
.copied()
.unwrap_or((0, 0, 0.0));
days.push(serde_json::json!({
"date": date_str,
"visible_count": visible_count,
"max_usable_min": max_usable_min,
"avg_max_alt": avg_max_alt,
"moon_illumination": moon_illum,
}));
cur += chrono::Duration::days(1);
}
Ok(Json(serde_json::json!({ "days": days })))
}
pub async fn get_calendar_date(
State(state): State<AppState>,
Path(date): Path<String>,
) -> Result<Json<serde_json::Value>, AppError> {
use sqlx::Row;
// Top 10 targets for this night
let targets = sqlx::query(
r#"SELECT c.id, c.name, c.common_name, c.obj_type,
nc.max_alt_deg, nc.usable_min, nc.transit_utc, nc.recommended_filter
FROM nightly_cache nc
JOIN catalog c ON c.id = nc.catalog_id
WHERE nc.night_date = ? AND nc.max_alt_deg >= 15
ORDER BY nc.max_alt_deg DESC
LIMIT 10"#,
)
.bind(&date)
.fetch_all(&state.pool)
.await?;
let target_list: Vec<serde_json::Value> = targets.iter().map(|r| {
serde_json::json!({
"id": r.try_get::<String, _>("id").unwrap_or_default(),
"name": r.try_get::<String, _>("name").unwrap_or_default(),
"common_name": r.try_get::<Option<String>, _>("common_name").unwrap_or_default(),
"obj_type": r.try_get::<String, _>("obj_type").unwrap_or_default(),
"max_alt_deg": r.try_get::<Option<f64>, _>("max_alt_deg").unwrap_or_default(),
"usable_min": r.try_get::<Option<i32>, _>("usable_min").unwrap_or_default(),
"transit_utc": r.try_get::<Option<String>, _>("transit_utc").unwrap_or_default(),
"recommended_filter": r.try_get::<Option<String>, _>("recommended_filter").unwrap_or_default(),
})
}).collect();
// Tonight summary from the `tonight` table (only available for tonight's date)
let tonight_row = sqlx::query("SELECT * FROM tonight WHERE id = 1 AND date = ?")
.bind(&date)
.fetch_optional(&state.pool)
.await?;
let tonight_summary = tonight_row.map(|r| serde_json::json!({
"astro_dusk_utc": r.try_get::<Option<String>, _>("astro_dusk_utc").unwrap_or_default(),
"astro_dawn_utc": r.try_get::<Option<String>, _>("astro_dawn_utc").unwrap_or_default(),
"moon_rise_utc": r.try_get::<Option<String>, _>("moon_rise_utc").unwrap_or_default(),
"moon_set_utc": r.try_get::<Option<String>, _>("moon_set_utc").unwrap_or_default(),
"moon_illumination": r.try_get::<Option<f64>, _>("moon_illumination").unwrap_or_default(),
"moon_phase_name": r.try_get::<Option<String>, _>("moon_phase_name").unwrap_or_default(),
"true_dark_start_utc": r.try_get::<Option<String>, _>("true_dark_start_utc").unwrap_or_default(),
"true_dark_end_utc": r.try_get::<Option<String>, _>("true_dark_end_utc").unwrap_or_default(),
"true_dark_minutes": r.try_get::<Option<i32>, _>("true_dark_minutes").unwrap_or_default(),
}));
// Weather summary from cache (only meaningful for today/near future)
let weather_row = sqlx::query(
"SELECT go_nogo, temp_c, dew_point_c, seventimer_json FROM weather_cache WHERE id = 1"
)
.fetch_optional(&state.pool)
.await?;
let weather_summary = weather_row.map(|r| {
let go_nogo = r.try_get::<Option<String>, _>("go_nogo").unwrap_or_default();
let temp_c = r.try_get::<Option<f64>, _>("temp_c").unwrap_or_default();
let dew_c = r.try_get::<Option<f64>, _>("dew_point_c").unwrap_or_default();
let seventimer: Option<serde_json::Value> = r.try_get::<Option<String>, _>("seventimer_json")
.unwrap_or_default()
.and_then(|s| serde_json::from_str(&s).ok());
// Extract tonight's cloudcover/seeing from 7timer if available
let (cloudcover, seeing, transparency) = seventimer
.as_ref()
.and_then(|v| v.get("dataseries")?.as_array()?.first().cloned())
.map(|slot| (
slot.get("cloudcover").and_then(|v| v.as_i64()),
slot.get("seeing").and_then(|v| v.as_i64()),
slot.get("transparency").and_then(|v| v.as_i64()),
))
.unwrap_or((None, None, None));
serde_json::json!({
"go_nogo": go_nogo,
"temp_c": temp_c,
"dew_point_c": dew_c,
"cloudcover": cloudcover,
"seeing": seeing,
"transparency": transparency,
})
});
// Moon illumination for the requested date (always computable)
let requested_date = chrono::NaiveDate::parse_from_str(&date, "%Y-%m-%d")
.unwrap_or_else(|_| chrono::Utc::now().naive_utc().date());
let moon_illum = moon_illum_for_date(requested_date);
Ok(Json(serde_json::json!({
"date": date,
"moon_illumination": moon_illum,
"top_targets": target_list,
"tonight": tonight_summary,
"weather": weather_summary,
})))
}