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) -> 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::::from_utc(elements.datetime, Utc)), elements, constants, } } fn on_tick(&mut self) { // let now = Utc::now(); let dt = DateTime::::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> { 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( terminal: &mut Terminal, 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]); */ }