use crate::{settings::Settings, utils::is_visible};
use anyhow::{anyhow, Context, Result};
use chrono::{Datelike, TimeZone, Timelike, Utc};
use percent_encoding::percent_decode_str;
use reqwest;
use serde::{Deserialize, Serialize};
pub mod table_indices {
pub const DESIGNATION: usize = 0;
pub const MAGNITUDE: usize = 1;
pub const SOLAR_ELONG: usize = 2;
pub const LUNAR_ELONG: usize = 3;
pub const BEGIN_TIME: usize = 4;
pub const BEG_RA: usize = 5;
pub const BEG_DEC: usize = 6;
pub const BEG_ALT: usize = 7;
pub const MAX_TIME: usize = 8;
pub const MAX_RA: usize = 9;
pub const MAX_DEC: usize = 10;
pub const MAX_ALT: usize = 11;
pub const END_TIME: usize = 12;
pub const END_RA: usize = 13;
pub const END_DEC: usize = 14;
pub const END_ALT: usize = 15;
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PossibleTarget {
pub designation: String,
pub ra: String,
pub dec: String,
pub magnitude: f32,
pub altitude: f32,
}
#[derive(Debug)]
pub struct WhatsUpParams {
pub year: String,
pub month: String,
pub day: String,
pub hour: String,
pub minute: String,
pub duration: String,
pub max_objects: String,
pub min_alt: String,
pub solar_elong: String,
pub lunar_elong: String,
pub object_type: String,
}
impl Default for WhatsUpParams {
fn default() -> Self {
let current_datetime = Utc::now();
let params: WhatsUpParams = WhatsUpParams {
year: current_datetime.year().to_string(),
month: current_datetime.month().to_string(),
day: current_datetime.day().to_string(),
minute: current_datetime.minute().to_string(),
hour: current_datetime.hour().to_string(),
duration: "1".to_string(),
max_objects: "10".to_string(),
min_alt: "10".to_string(),
solar_elong: "0".to_string(),
lunar_elong: "0".to_string(),
object_type: "mp".to_string(),
};
params
}
}
impl Default for PossibleTarget {
fn default() -> Self {
PossibleTarget {
designation: "None".to_string(),
ra: "None".to_string(),
dec: "None".to_string(),
magnitude: 0.0,
altitude: 0.0,
}
}
}
fn get_observing_target_list(params: &WhatsUpParams) -> Result<String> {
let settings = Settings::new()
.context("Failed to load settings")?;
let mut full_params: Vec<(&str, &str)> = Vec::new();
let encoded_param = "%E2%9C%93";
let decoded = percent_decode_str(encoded_param)
.decode_utf8_lossy()
.into_owned();
full_params.push(("utf8", decoded.as_str()));
let auth_token = settings.get_mpc_auth_token();
let decoded_auth_token = percent_decode_str(&auth_token)
.decode_utf8_lossy()
.into_owned();
full_params.push(("authenticity_token", decoded_auth_token.as_str()));
let latitude = settings.get_latitude().to_string();
full_params.push(("latitude", latitude.as_str()));
let longitude = settings.get_longitude().to_string();
full_params.push(("longitude", longitude.as_str()));
full_params.push(("year", params.year.as_str()));
full_params.push(("month", params.month.as_str()));
full_params.push(("day", params.day.as_str()));
full_params.push(("hour", params.hour.as_str()));
full_params.push(("minute", params.minute.as_str()));
full_params.push(("duration", params.duration.as_str()));
full_params.push(("max_objects", params.max_objects.as_str()));
full_params.push(("min_alt", params.min_alt.as_str()));
full_params.push(("solar_elong", params.solar_elong.as_str()));
full_params.push(("lunar_elong", params.lunar_elong.as_str()));
full_params.push(("object_type", params.object_type.as_str()));
full_params.push(("submit", "Submit"));
let url = reqwest::Url::parse_with_params(
"https://www.minorplanetcenter.net/whatsup/index",
full_params,
)
.context("Failed to create MPC URL")?;
let client = reqwest::blocking::Client::new();
let response = client
.post(url)
.send()
.context("Failed to send request to MPC")?
.text()
.context("Failed to read MPC response")?;
Ok(response)
}
pub fn parse_whats_up_response(params: &WhatsUpParams) -> Result<Vec<PossibleTarget>> {
let mut objects: Vec<PossibleTarget> = Vec::new();
let data = get_observing_target_list(params)?;
let document = scraper::Html::parse_document(data.as_str());
let table_item_selector = scraper::Selector::parse("td")
.map_err(|e| anyhow!("Failed to parse table item selector: {:?}", e))?;
let rows_selector = scraper::Selector::parse("#main table:nth-child(1) tr:not(:first-child)")
.map_err(|e| anyhow!("Failed to parse rows selector: {:?}", e))?;
let rows: Vec<scraper::ElementRef<'_>> = document.select(&rows_selector).collect();
let date = Utc
.with_ymd_and_hms(
params.year.parse()
.context("Failed to parse year")?,
params.month.parse()
.context("Failed to parse month")?,
params.day.parse()
.context("Failed to parse day")?,
params.hour.parse()
.context("Failed to parse hour")?,
params.minute.parse()
.context("Failed to parse minute")?,
0,
)
.single()
.context("Invalid date/time")?;
for row in rows {
let cells: Vec<scraper::ElementRef<'_>> = row.select(&table_item_selector).collect();
match create_possible_target(cells) {
Ok(object) => {
if is_visible(
&object.ra.replace(" ", ":"),
&object.dec.replace(" ", ":"),
date,
) {
objects.push(object);
}
}
Err(e) => {
eprintln!("Warning: Failed to create object: {}", e);
}
}
}
Ok(objects)
}
fn create_possible_target(item: Vec<scraper::ElementRef<'_>>) -> Result<PossibleTarget> {
let mut possible_target = PossibleTarget::default();
if item.len() < 8 {
return Err(anyhow!("Not enough elements in input vector"));
}
let designation_selector =
scraper::Selector::parse("a").map_err(|e| anyhow!("Failed to parse selector: {}", e))?;
let designation = item[table_indices::DESIGNATION]
.select(&designation_selector)
.next()
.ok_or_else(|| anyhow!("Designation element not found"))?;
possible_target.designation = designation.inner_html();
possible_target.magnitude = item[table_indices::MAGNITUDE]
.inner_html()
.parse::<f32>()
.map_err(|e| anyhow!("Failed to parse magnitude: {}", e))?;
possible_target.altitude = item[table_indices::BEG_ALT]
.inner_html()
.replace(' ', "")
.parse::<f32>()
.map_err(|e| anyhow!("Failed to parse altitude: {}", e))?;
possible_target.ra = item[table_indices::BEG_RA].inner_html();
possible_target.dec = item[table_indices::BEG_DEC].inner_html();
Ok(possible_target)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_get_observing_target_list() {
let result = get_observing_target_list(&WhatsUpParams::default());
assert!(result.is_ok());
assert!(result.unwrap().contains("Designation"));
}
#[test]
fn test_parse_whats_up_response() {
let result = parse_whats_up_response(&WhatsUpParams::default());
assert!(result.is_ok());
let _objects = result.unwrap();
}
}