iss-tui/src/main.rs

293 lines
9.3 KiB
Rust

use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, cursor::position,
};
use sgp4::Prediction;
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use chrono::{prelude::*};
use libm::*;
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::Span,
widgets::{
canvas::{Canvas, Map, MapResolution, Line, Points},
Block, Borders,
},
Frame, Terminal,
};
const TWOPI: f64 = 2.0*std::f64::consts::PI;
const DEG2RAD: f64 = 1.0 / 360.0 * TWOPI;
const RAD2DEG: f64 = 1.0 / TWOPI * 360.0;
fn jd_to_gmst(jd_: f64) ->f64 {
let twopi = 2.0*std::f64::consts::PI;
let ut = (jd_ + 0.5).fract();
let jd = jd_ - ut;
let tu = (jd - 2451545.0)/36525.;
let mut gmst = 24110.54841 + tu * (8640184.812866 + tu * (0.093104 - tu * 6.2e-6));
gmst = (gmst + 86400.0*1.00273790934*ut) % 86400.0;
((twopi * gmst/86400.0) + twopi) % twopi
}
fn wgs84_to_eci(geodetic_lat: f64, geodetic_lon: f64, julian_date: f64) -> (f64, f64, f64) {
let a = 6378.137; // km
let f = 1. / 298.257223563;
let alt = 0.;
let theta = (geodetic_lon + jd_to_gmst(julian_date) + TWOPI) % TWOPI;
let c = 1. / (1. + f * (f-2.) * sin(geodetic_lat).powi(2)).sqrt();
let s = (1. - f).powi(2) * c;
let achcp = (a * c + alt)*cos(geodetic_lat);
let x = achcp * cos(theta);
let y = achcp * sin(theta);
let z = (a * s + alt) * sin(geodetic_lat);
(x, y, z)
}
fn eci_to_wgs84(x:f64, y:f64, z:f64, jd:f64) -> (f64, f64, f64) {
let actan: f64;
if x == 0. {
if y > 0. {actan = std::f64::consts::PI/2.;}
else {actan = 3. * std::f64::consts::PI / 2.;}
} else if x > 0. {actan = atan(y/x);}
else {actan = std::f64::consts::PI + atan(y / x);}
let mut lon = (actan - jd_to_gmst(jd) + TWOPI) % TWOPI;
let a = 6378.137; // km
let f: f64 = 1. / 298.257223563;
let e2 = 2. * f - f.powi(2);
let r = (x*x + y*y).sqrt();
let mut c = 0.0;
let mut approx_lat = atan(z / r);
let mut lat = approx_lat + TWOPI; //initialize to value not equal to approx_lat
while (lat - approx_lat).abs() > 1E-10 {
lat = approx_lat;
c = 1. / (1. - e2 * sin(lat).powi(2)).sqrt();
approx_lat =atan((z + a * c* e2 * sin(lat)) / r);
}
let height = (r / cos(lat)) - a * c;
if lon > std::f64::consts::PI {
lon -= TWOPI;
}
(lat, lon, height)
}
fn to_julian_day(t: &DateTime<Utc>) -> f64 {
let dur: chrono::Duration = *t - Utc.with_ymd_and_hms(2000, 1, 1, 12, 0, 0).single().unwrap();
dur.num_seconds() as f64 / (24. * 60. * 60.) + 2451545.0
}
struct App {
show_lines: bool,
show_points: bool,
gpoints: Vec<(f64, f64)>,
sat_x: f64,
sat_y: f64,
ts: f64,
elements: sgp4::Elements,
constants: sgp4::Constants,
epoch_jd: f64,
}
impl App {
fn new(elements: sgp4::Elements, constants: sgp4::Constants) -> App {
App {
show_lines: false,
show_points: true,
gpoints: vec![],
sat_x: 0.,
sat_y: 0.,
ts: 0.,
epoch_jd: to_julian_day(&DateTime::<Utc>::from_utc(elements.datetime, Utc)),
elements,
constants,
}
}
fn on_tick(&mut self) {
//
let now = Utc::now();
let dt = DateTime::<Utc>::from_utc(self.elements.datetime, Utc);
//let now = dt + chrono::Duration::days(2);
//let min_ms_acc = (now - dt).to_std()?.as_secs_f64() / 60.;
let min = (now - dt).num_seconds() as f64 / 60.;
self.gpoints.clear();
match self.constants.propagate(min) {
Ok(prediction) => {
let (lat, lon, _alt) = eci_to_wgs84(prediction.position[0], prediction.position[1], prediction.position[2], to_julian_day(&now));
self.sat_x = lon*RAD2DEG;
self.sat_y = lat*RAD2DEG;
//self.gpoints.push((self.sat_x, self.sat_y));
},
Err(_e) => {},
}
let step: i64 = 5;
for i in 0..24 {
match self.constants.propagate((i * step) as f64 + min) {
Ok(prediction) => {
let (y, x, _z) = eci_to_wgs84(prediction.position[0], prediction.position[1], prediction.position[2], to_julian_day(&(now + chrono::Duration::minutes(i * step as i64))));
self.gpoints.push((x*RAD2DEG, y*RAD2DEG));
},
Err(_e) => {
//self.gpoints.push(self.gpoints.last().unwrap_or(&(0.,0.)).clone());
}
}
}
}
}
fn main() -> Result<(), Box<dyn Error>> {
let text = "ISS (ZARYA)
1 25544U 98067A 23003.52502237 .00016767 00000+0 30219-3 0 9998
2 25544 51.6453 64.1711 0005004 218.5032 238.7671 15.49892242376259".to_owned();
let data: Vec<&str> = text.split("\n").map(|s| s.trim()).collect();
assert!(data.len() >= 3);
let elements = sgp4::Elements::from_tle(Some(data[0].to_owned()), data[1].as_bytes(), data[2].as_bytes()).expect("Could not parse TLE Data!");
let constants = sgp4::Constants::from_elements(&elements).expect("Could not create constants!");
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(1000);
let app = App::new(elements, constants);
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => {
return Ok(());
}
KeyCode::Down => {
//
}
KeyCode::Up => {
//
}
KeyCode::Right => {
//
}
KeyCode::Left => {
//
}KeyCode::Char('l') => {
app.show_lines = !app.show_lines;
}KeyCode::Char('p') => {
app.show_points = !app.show_points;
}
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui(f: &mut Frame, app: &App) {
// let chunks = Layout::default()
// .direction(Direction::Horizontal)
// .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
// .split(f.size());
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
resolution: MapResolution::High,
});
if app.show_lines {
for (i, (x1, y1)) in app.gpoints.iter().enumerate() {
if let Some((x2, y2)) = app.gpoints.get(i+1) {
//TODO fix polar crossing
if *x1 > 90. && *x2 < -90. {
continue;
}
ctx.draw(&Line {
x1: *x1,
y1: *y1,
x2: *x2,
y2: *y2,
color: Color::Yellow,
});
}
}
}if app.show_points {
for (_i, (x1, y1)) in app.gpoints.iter().enumerate() {
ctx.draw(&Points {
coords: &[(*x1, *y1)],
color: Color::Red,
});
}
}
ctx.print(
app.sat_x,
app.sat_y,
Span::styled("🛰️", Style::default().fg(Color::Yellow)),
);
})
.x_bounds([app.sat_x.min(-180.0), app.sat_x.max(180.0)])
.y_bounds([-90.0, 90.0]);
f.render_widget(canvas, f.size());
/*let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.paint(|ctx| {
ctx.draw(&app.ball);
})
.x_bounds([10.0, 110.0])
.y_bounds([10.0, 110.0]);
f.render_widget(canvas, chunks[1]); */
}