diff --git a/src/gui.rs b/src/gui.rs index 2b5baf3..2e80e40 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -9,12 +9,13 @@ use egui::{Align, Color32, FontFamily, FontId, Key, Layout, Modifiers, Vec2}; use mithril::telemetry::*; -mod panels; mod fc_settings; mod map; mod maxi_grid; mod misc; +mod panels; mod plot; +mod plots; mod simulation_settings; mod tabs; mod theme; @@ -34,6 +35,7 @@ pub struct Sam { data_source: Box, tab: GuiTab, plot_tab: PlotTab, + launch_tab: LaunchTab, configure_tab: ConfigureTab, archive_window: ArchiveWindow, } @@ -56,6 +58,7 @@ impl Sam { let plot_tab = PlotTab::init(&settings); let configure_tab = ConfigureTab::init(); + let launch_tab = LaunchTab::init(&settings); egui_extras::install_image_loaders(ctx); @@ -66,6 +69,7 @@ impl Sam { tab: GuiTab::Plot, plot_tab, configure_tab, + launch_tab, archive_window: ArchiveWindow::default(), } @@ -151,7 +155,9 @@ impl Sam { } // Header containing text indicators and flight mode buttons - HeaderPanel::show(ctx, self.data_source.as_mut(), !self.archive_window.open); + if self.tab != GuiTab::Launch { + HeaderPanel::show(ctx, self.data_source.as_mut(), !self.archive_window.open); + } // Bottom status bar egui::TopBottomPanel::bottom("bottombar").min_height(30.0).show(ctx, |ui| { @@ -167,12 +173,12 @@ impl Sam { }); // ... and the current tab some space on the right. - ui.allocate_ui_with_layout(ui.available_size(), Layout::right_to_left(Align::Center), |ui| { - match self.tab { - GuiTab::Launch => {} - GuiTab::Plot => self.plot_tab.bottom_bar_ui(ui, self.data_source.as_mut()), - GuiTab::Configure => {} - } + ui.allocate_ui_with_layout(ui.available_size(), Layout::right_to_left(Align::Center), |ui| match self + .tab + { + GuiTab::Launch => {} + GuiTab::Plot => self.plot_tab.bottom_bar_ui(ui, self.data_source.as_mut()), + GuiTab::Configure => {} }); }); }); @@ -181,7 +187,7 @@ impl Sam { egui::CentralPanel::default().show(ctx, |ui| { ui.set_enabled(!self.archive_window.open); match self.tab { - GuiTab::Launch => {} + GuiTab::Launch => self.launch_tab.main_ui(ui, self.data_source.as_mut()), GuiTab::Plot => self.plot_tab.main_ui(ui, self.data_source.as_mut()), GuiTab::Configure => { let changed = self.configure_tab.main_ui(ui, self.data_source.as_mut(), &mut self.settings); diff --git a/src/gui/plots/accelerometer_plot.rs b/src/gui/plots/accelerometer_plot.rs new file mode 100644 index 0000000..27c4330 --- /dev/null +++ b/src/gui/plots/accelerometer_plot.rs @@ -0,0 +1,16 @@ +use std::{cell::RefCell, rc::Rc}; + +use crate::gui::plot::{PlotState, SharedPlotState}; + +use super::color_constants::*; + +pub fn accelerometer_plot(shared_plot: &Rc>) -> PlotState { + let accelerometer_plot = PlotState::new("Accelerometers", (Some(-10.0), Some(10.0)), shared_plot.clone()) + .line("Accel 2 (X) [m/s²]", R1, |vs| vs.accelerometer2.map(|a| a.x)) + .line("Accel 2 (Y) [m/s²]", G1, |vs| vs.accelerometer2.map(|a| a.y)) + .line("Accel 2 (Z) [m/s²]", B1, |vs| vs.accelerometer2.map(|a| a.z)) + .line("Accel 1 (X) [m/s²]", R, |vs| vs.accelerometer1.map(|a| a.x)) + .line("Accel 1 (Y) [m/s²]", G, |vs| vs.accelerometer1.map(|a| a.y)) + .line("Accel 1 (Z) [m/s²]", B, |vs| vs.accelerometer1.map(|a| a.z)); + accelerometer_plot +} diff --git a/src/gui/plots/altitude_plot.rs b/src/gui/plots/altitude_plot.rs new file mode 100644 index 0000000..d5919c6 --- /dev/null +++ b/src/gui/plots/altitude_plot.rs @@ -0,0 +1,14 @@ +use std::{cell::RefCell, rc::Rc}; + +use crate::gui::plot::{PlotState, SharedPlotState}; + +use super::color_constants::*; + +pub fn altitude_plot(shared_plot: &Rc>) -> PlotState { + let altitude_plot = PlotState::new("Altitude (ASL)", (None, None), shared_plot.clone()) + .line("Altitude (Ground) [m]", BR, |vs| vs.altitude_ground_asl) + .line("Altitude (Baro) [m]", B1, |vs| vs.altitude_baro) + .line("Altitude [m]", B, |vs| vs.altitude_asl) + .line("Altitude (GPS) [m]", G, |vs| vs.altitude_gps_asl); + altitude_plot +} diff --git a/src/gui/plots/barometer_plot.rs b/src/gui/plots/barometer_plot.rs new file mode 100644 index 0000000..d17dfba --- /dev/null +++ b/src/gui/plots/barometer_plot.rs @@ -0,0 +1,12 @@ +use std::{cell::RefCell, rc::Rc}; + +use crate::gui::plot::{PlotState, SharedPlotState}; + +use super::color_constants::*; + +pub fn barometer_plot(shared_plot: &Rc>) -> PlotState { + let barometer_plot = + PlotState::new("Pressures", (None, None), shared_plot.clone()) + .line("Barometer [bar]", C, |vs| vs.pressure_baro.map(|p| p / 1000.0)); + barometer_plot +} diff --git a/src/gui/plots/color_constants.rs b/src/gui/plots/color_constants.rs new file mode 100644 index 0000000..ceb6417 --- /dev/null +++ b/src/gui/plots/color_constants.rs @@ -0,0 +1,13 @@ +use egui::Color32; + +pub const R: Color32 = Color32::from_rgb(0xfb, 0x49, 0x34); +pub const G: Color32 = Color32::from_rgb(0xb8, 0xbb, 0x26); +pub const B: Color32 = Color32::from_rgb(0x83, 0xa5, 0x98); +pub const R1: Color32 = Color32::from_rgb(0xcc, 0x24, 0x1d); +pub const G1: Color32 = Color32::from_rgb(0x98, 0x97, 0x1a); +pub const B1: Color32 = Color32::from_rgb(0x45, 0x85, 0x88); +pub const O: Color32 = Color32::from_rgb(0xfa, 0xbd, 0x2f); +pub const O1: Color32 = Color32::from_rgb(0xd6, 0x5d, 0x0e); +pub const BR: Color32 = Color32::from_rgb(0x61, 0x48, 0x1c); +// pub const P: Color32 = Color32::from_rgb(0xb1, 0x62, 0x86); +pub const C: Color32 = Color32::from_rgb(0x68, 0x9d, 0x6a); diff --git a/src/gui/plots/mod.rs b/src/gui/plots/mod.rs new file mode 100644 index 0000000..209f712 --- /dev/null +++ b/src/gui/plots/mod.rs @@ -0,0 +1,12 @@ +pub mod accelerometer_plot; +pub mod altitude_plot; +pub mod barometer_plot; +pub mod color_constants; +pub mod orientation_plot; +pub mod vertical_speed_plot; + +pub use accelerometer_plot::*; +pub use altitude_plot::*; +pub use barometer_plot::*; +pub use orientation_plot::*; +pub use vertical_speed_plot::*; diff --git a/src/gui/plots/orientation_plot.rs b/src/gui/plots/orientation_plot.rs new file mode 100644 index 0000000..de88e98 --- /dev/null +++ b/src/gui/plots/orientation_plot.rs @@ -0,0 +1,18 @@ +use std::{cell::RefCell, rc::Rc}; + +use crate::gui::plot::{PlotState, SharedPlotState}; + +use super::color_constants::*; + +pub fn orientation_plot(shared_plot: &Rc>) -> PlotState { + let orientation_plot = PlotState::new("Orientation", (Some(-180.0), Some(540.0)), shared_plot.clone()) + .line("Roll (Z) [°]", B, |vs| vs.euler_angles.map(|a| a.z)) + .line("Pitch (X) [°]", R, |vs| vs.euler_angles.map(|a| a.x)) + .line("Yaw (Y) [°]", G, |vs| vs.euler_angles.map(|a| a.y)) + .line("Angle of Attack [°]", O, |vs| vs.angle_of_attack) + .line("Roll (True) (Z) [°]", B1, |vs| vs.true_euler_angles.map(|a| a.z)) + .line("Pitch (True) (X) [°]", R1, |vs| vs.true_euler_angles.map(|a| a.x)) + .line("Yaw (True) (Y) [°]", G1, |vs| vs.true_euler_angles.map(|a| a.y)) + .line("Angle of Attack (True) [°]", O1, |vs| vs.true_angle_of_attack); + orientation_plot +} diff --git a/src/gui/plots/vertical_speed_plot.rs b/src/gui/plots/vertical_speed_plot.rs new file mode 100644 index 0000000..cfd944f --- /dev/null +++ b/src/gui/plots/vertical_speed_plot.rs @@ -0,0 +1,15 @@ +use std::{cell::RefCell, rc::Rc}; + +use crate::gui::plot::{PlotState, SharedPlotState}; + +use super::color_constants::*; + +pub fn vertical_speed_plot(shared_plot: &Rc>) -> PlotState { + let vertical_speed_plot = PlotState::new("Vert. Speed & Accel.", (None, None), shared_plot.clone()) + .line("Vertical Accel [m/s²]", O1, |vs| vs.vertical_accel) + .line("Vertical Accel (Filt.) [m/s²]", O, |vs| vs.vertical_accel_filtered) + .line("Vario [m/s]", B, |vs| vs.vertical_speed) + .line("True Vertical Accel [m/s²]", G, |vs| vs.true_vertical_accel) + .line("True Vario [m/s]", B1, |vs| vs.true_vertical_speed); + vertical_speed_plot +} diff --git a/src/gui/tabs.rs b/src/gui/tabs.rs index dc8f523..ee36e0f 100644 --- a/src/gui/tabs.rs +++ b/src/gui/tabs.rs @@ -1,7 +1,9 @@ mod configure; +mod launch; mod plot; pub use configure::*; +pub use launch::*; pub use plot::*; #[derive(Debug, Copy, Clone, PartialEq)] diff --git a/src/gui/tabs/launch.rs b/src/gui/tabs/launch.rs new file mode 100644 index 0000000..f798adf --- /dev/null +++ b/src/gui/tabs/launch.rs @@ -0,0 +1,205 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use egui::Color32; +use egui::Rect; +use egui::Vec2; +use egui_gizmo::Gizmo; +use egui_gizmo::GizmoMode; +use egui_gizmo::GizmoVisuals; +use nalgebra::UnitQuaternion; +use nalgebra::Vector3; + +use crate::data_source::DataSource; +use crate::gui::plots::accelerometer_plot; +use crate::gui::plots::altitude_plot; +use crate::gui::plots::barometer_plot; +use crate::gui::plots::orientation_plot; +use crate::gui::plots::vertical_speed_plot; +use crate::settings::AppSettings; + +use crate::gui::map::*; +use crate::gui::maxi_grid::*; +use crate::gui::plot::*; +use crate::gui::plots::color_constants::*; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum SelectedPlot { + Orientation, + VerticalSpeed, + Altitude, + Accelerometers, + Barometer, + Map, +} + +impl std::fmt::Display for SelectedPlot { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + SelectedPlot::Orientation => write!(f, "Orientation"), + SelectedPlot::VerticalSpeed => write!(f, "Vertical Speed & Accel."), + SelectedPlot::Altitude => write!(f, "Altitude"), + SelectedPlot::Barometer => write!(f, "Barometer"), + SelectedPlot::Accelerometers => write!(f, "Accelerometers"), + SelectedPlot::Map => write!(f, "Map"), + } + } +} + +pub struct LaunchTab { + maxi_grid_state: MaxiGridState, + dropdown_selected_plot: SelectedPlot, + + shared_plot: Rc>, + orientation_plot: PlotState, + vertical_speed_plot: PlotState, + altitude_plot: PlotState, + accelerometer_plot: PlotState, + barometer_plot: PlotState, + + map: MapState, +} + +impl LaunchTab { + pub fn init(settings: &AppSettings) -> Self { + let shared_plot = Rc::new(RefCell::new(SharedPlotState::new())); + + let orientation_plot = orientation_plot(&shared_plot); + let vertical_speed_plot = vertical_speed_plot(&shared_plot); + let altitude_plot = altitude_plot(&shared_plot); + let accelerometer_plot = accelerometer_plot(&shared_plot); + let barometer_plot = barometer_plot(&shared_plot); + + let map = MapState::new(settings.mapbox_access_token.clone()); + + Self { + maxi_grid_state: MaxiGridState::default(), + dropdown_selected_plot: SelectedPlot::Orientation, + shared_plot, + orientation_plot, + vertical_speed_plot, + altitude_plot, + accelerometer_plot, + barometer_plot, + map, + } + } + + fn plot_gizmo( + &mut self, + ui: &mut egui::Ui, + viewport: Rect, + orientation: UnitQuaternion, + colors: (Color32, Color32, Color32), + ) { + // We can't disable interaction with the gizmo, so we disable the entire UI + // when the user gets too close. TODO: upstream way to disable interaction? + let enabled = !ui.rect_contains_pointer(viewport); + + // use top right of plot for indicator, space below for plot + let viewport = Rect::from_two_pos(viewport.lerp_inside(Vec2::new(0.4, 0.55)), viewport.right_top()); + + let fade_to_color = Color32::BLACK; + ui.visuals_mut().widgets.noninteractive.weak_bg_fill = fade_to_color; + + // square viewport + let viewport_square_side = f32::min(viewport.width(), viewport.height()); + let viewport = viewport.shrink2((viewport.size() - Vec2::splat(viewport_square_side)) * 0.5); + + let view = UnitQuaternion::from_euler_angles(-90.0f32.to_radians(), 180f32.to_radians(), 0.0f32.to_radians()); + + let visuals = GizmoVisuals { + x_color: colors.0, + y_color: colors.1, + z_color: colors.2, + inactive_alpha: 1.0, + highlight_alpha: 1.0, + stroke_width: 3.0, + gizmo_size: viewport_square_side * 0.4, + ..Default::default() + }; + + let gizmo = Gizmo::new("My gizmo") + .mode(GizmoMode::Translate) + .viewport(viewport) + .orientation(egui_gizmo::GizmoOrientation::Local) + .model_matrix(orientation.to_homogeneous()) + .view_matrix(view.to_homogeneous()) + .visuals(visuals); + + ui.add_enabled_ui(enabled, |ui| { + gizmo.interact(ui); + }); + } + + fn plot_orientation(&mut self, ui: &mut egui::Ui, data_source: &mut dyn DataSource) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + + let mut viewport = ui.cursor(); + viewport.set_width(ui.available_width()); + viewport.set_height(ui.available_height()); + + let orientation = data_source + .vehicle_states() + .rev() + .find_map(|(_, vs)| vs.orientation) + .unwrap_or(UnitQuaternion::new(Vector3::new(0.0, 0.0, 0.0))); + let true_orientation = data_source.vehicle_states().rev().find_map(|(_, vs)| vs.true_orientation); + + ui.plot_telemetry(&self.orientation_plot, data_source); + + if let Some(orientation) = true_orientation { + self.plot_gizmo(ui, viewport, orientation, (R1, G1, B1)); + } + + self.plot_gizmo(ui, viewport, orientation, (R, G, B)); + } + + pub fn main_ui(&mut self, ui: &mut egui::Ui, data_source: &mut dyn DataSource) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + + self.shared_plot.borrow_mut().set_end(data_source.end()); + + if ui.available_width() > 1000.0 { + MaxiGrid::new((3, 3), ui, self.maxi_grid_state.clone()) + .cell("Orientation", |ui| self.plot_orientation(ui, data_source)) + .cell("Vert. Speed & Accel", |ui| ui.plot_telemetry(&self.vertical_speed_plot, data_source)) + .cell("Altitude (ASL)", |ui| ui.plot_telemetry(&self.altitude_plot, data_source)) + .cell("Position", |ui| ui.map(&self.map, data_source)) + .cell("Accelerometers", |ui| ui.plot_telemetry(&self.accelerometer_plot, data_source)) + .cell("Barometer", |ui| ui.plot_telemetry(&self.barometer_plot, data_source)); + } else { + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.spacing_mut().combo_width = ui.available_width(); + egui::ComboBox::from_id_source("plot_selector") + .selected_text(format!("{}", self.dropdown_selected_plot)) + .show_ui(ui, |ui| { + ui.set_width(ui.available_width()); + for p in [ + SelectedPlot::Orientation, + SelectedPlot::VerticalSpeed, + SelectedPlot::Altitude, + SelectedPlot::Accelerometers, + SelectedPlot::Barometer, + SelectedPlot::Map, + ] { + ui.selectable_value(&mut self.dropdown_selected_plot, p, format!("{}", p)); + } + }); + }); + + match self.dropdown_selected_plot { + SelectedPlot::Orientation => self.plot_orientation(ui, data_source), + SelectedPlot::VerticalSpeed => ui.plot_telemetry(&self.vertical_speed_plot, data_source), + SelectedPlot::Altitude => ui.plot_telemetry(&self.altitude_plot, data_source), + SelectedPlot::Barometer => ui.plot_telemetry(&self.barometer_plot, data_source), + SelectedPlot::Accelerometers => ui.plot_telemetry(&self.accelerometer_plot, data_source), + SelectedPlot::Map => ui.map(&self.map, data_source), + } + }); + } + } +}