use crate::Kinetics::experimental_kinetics::exp_engine_api::Ranges;
use crate::Kinetics::experimental_kinetics::experiment_series_main::TGASeries;
use crate::Kinetics::experimental_kinetics::one_experiment_dataset::TGADomainError;
use crate::gui::experimental_kinetics_gui::interaction::InteractionState;
use crate::gui::experimental_kinetics_gui::settings::Settings;
use log::info;
use simplelog::{Config, LevelFilter, SimpleLogger};
use std::sync::Once;
use tabled::{Table, Tabled};
const DEFAULT_VIEW_RANGE: (f64, f64) = (0.0, 10.0);
#[derive(Debug)]
pub enum TGAGUIError {
TGADomainError(TGADomainError),
SettingsErrors(String),
BindingError(String),
}
impl From<TGADomainError> for TGAGUIError {
fn from(err: TGADomainError) -> Self {
TGAGUIError::TGADomainError(err)
}
}
fn parse_log_level(level: Option<&str>) -> LevelFilter {
match level.unwrap_or("info").trim().to_ascii_lowercase().as_str() {
"off" => LevelFilter::Off,
"error" => LevelFilter::Error,
"warn" | "warning" => LevelFilter::Warn,
"info" => LevelFilter::Info,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
_ => LevelFilter::Info,
}
}
fn init_logging(settings: &Settings) {
static LOGGER_INIT: Once = Once::new();
let level = parse_log_level(settings.log_level());
let requested_level = settings.log_level().unwrap_or("info").to_string();
LOGGER_INIT.call_once(|| {
let _ = SimpleLogger::init(level, Config::default());
});
log::set_max_level(level);
info!(
"Experimental kinetics logger initialized with level '{}' ({:?})",
requested_level, level
);
}
#[derive(Debug, Clone)]
pub struct PlotCurve {
pub experiment_index: usize,
pub experiment_id: String,
pub plot_short_name: String,
pub color: [u8; 3],
pub selected: bool,
pub points: Vec<[f64; 2]>,
pub x_name: String,
pub y_name: String,
pub ranges: Ranges,
pub highlighted: bool,
pub shown: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Colours {
Blue,
Red,
Green,
Yellow,
Orange,
Purple,
Cyan,
Magenta,
Black,
White,
Gray,
}
impl Colours {
pub fn as_rgb(self) -> [u8; 3] {
match self {
Colours::Blue => [0, 0, 255],
Colours::Red => [255, 0, 0],
Colours::Green => [0, 255, 0],
Colours::Yellow => [255, 255, 0],
Colours::Orange => [255, 165, 0],
Colours::Purple => [128, 0, 128],
Colours::Cyan => [0, 255, 255],
Colours::Magenta => [255, 0, 255],
Colours::Black => [0, 0, 0],
Colours::White => [255, 255, 255],
Colours::Gray => [128, 128, 128],
}
}
}
impl PlotCurve {
pub fn find_nearest_distance(&self, point: [f64; 2]) -> Option<f64> {
if !self.shown || self.points.is_empty() {
return None;
}
let x_span = (self.ranges.x_max - self.ranges.x_min)
.abs()
.max(f64::EPSILON);
let y_span = (self.ranges.y_max - self.ranges.y_min)
.abs()
.max(f64::EPSILON);
let point_to_segment_normalized = |a: [f64; 2], b: [f64; 2], p: [f64; 2]| -> f64 {
let abx = b[0] - a[0];
let aby = b[1] - a[1];
let apx = p[0] - a[0];
let apy = p[1] - a[1];
let ab_len2 = abx * abx + aby * aby;
let t = if ab_len2 > 0.0 {
(apx * abx + apy * aby) / ab_len2
} else {
0.0
}
.clamp(0.0, 1.0);
let cx = a[0] + t * abx;
let cy = a[1] + t * aby;
let dx = (p[0] - cx) / x_span;
let dy = (p[1] - cy) / y_span;
(dx * dx + dy * dy).sqrt()
};
if self.points.len() == 1 {
let dx = (point[0] - self.points[0][0]) / x_span;
let dy = (point[1] - self.points[0][1]) / y_span;
return Some((dx * dx + dy * dy).sqrt());
}
self.points
.windows(2)
.map(|w| point_to_segment_normalized(w[0], w[1], point))
.min_by(|a, b| a.total_cmp(b))
}
pub fn set_shown(&mut self, shown: bool) {
self.shown = shown;
}
pub fn is_shown(&self) -> bool {
self.shown
}
pub fn get_name(&self) -> String {
self.experiment_id.clone()
}
pub fn get_label(&self) -> String {
format!("{}: {} vs {}", self.experiment_id, self.y_name, self.x_name)
}
pub fn get_short_name(&self) -> String {
self.plot_short_name.clone()
}
pub fn set_colour(&mut self, colour: Colours) {
self.color = colour.as_rgb();
}
pub fn set_colour_rgb(&mut self, rgb: [u8; 3]) {
self.color = rgb;
}
}
impl Default for PlotCurve {
fn default() -> Self {
Self {
experiment_index: 0,
experiment_id: String::new(),
plot_short_name: String::new(),
color: [0, 0, 255],
selected: false,
points: Vec::new(),
x_name: String::new(),
y_name: String::new(),
ranges: Ranges::default(),
highlighted: false,
shown: true,
}
}
}
#[derive(Debug, Clone)]
pub struct PlotModel {
pub series: TGASeries,
pub plots: Vec<PlotCurve>,
pub interaction: InteractionState,
pub settings: Settings,
pub reset_view_requested: bool,
pub plot_recreation_required: bool,
pub message: String,
}
impl Default for PlotModel {
fn default() -> Self {
let settings = Settings::new().unwrap_or_else(|_| Settings::default());
init_logging(&settings);
print_settings_table(&settings);
Self {
series: TGASeries::new(),
plots: Vec::new(),
interaction: InteractionState::default(),
settings,
reset_view_requested: false,
plot_recreation_required: false,
message: String::new(),
}
}
}
#[derive(Tabled)]
struct SettingsRow {
#[tabled(rename = "Parameter")]
parameter: String,
#[tabled(rename = "Value")]
value: String,
}
fn print_settings_table(settings: &Settings) {
let mut rows = Vec::new();
if let Some(cal) = settings.calibration_line() {
rows.push(SettingsRow {
parameter: "Calibration k".to_string(),
value: format!("{:.4}", cal.k()),
});
rows.push(SettingsRow {
parameter: "Calibration b".to_string(),
value: format!("{:.4}", cal.b()),
});
}
if let Some(n) = settings.n_points() {
rows.push(SettingsRow {
parameter: "Number of points".to_string(),
value: n.to_string(),
});
}
if let Some(expr) = settings.symbolic_expression() {
rows.push(SettingsRow {
parameter: "Symbolic expression".to_string(),
value: expr.to_string(),
});
}
if let Some(log) = settings.log_level() {
rows.push(SettingsRow {
parameter: "Log level".to_string(),
value: log.to_string(),
});
}
let table = Table::new(rows).to_string();
println!("\n=== TGA Application Settings ===");
println!("{}", table);
println!("================================\n");
}
impl PlotModel {
pub fn new() -> Self {
Self::default()
}
pub fn select_curve(&mut self, index: usize) {
for (i, curve) in self.plots.iter_mut().enumerate() {
curve.selected = i == index;
}
}
pub fn clear_selection(&mut self) {
for curve in &mut self.plots {
curve.selected = false;
}
}
pub fn get_selected_curve_index(&self) -> Option<usize> {
self.plots.iter().position(|c| c.selected)
}
pub fn get_experiment_by_selected_curve(&self) -> Result<String, TGAGUIError> {
if let Some(number) = self.get_selected_curve_index() {
return Ok(self.plots[number].experiment_id.clone());
} else if self.is_only_one_shown() {
let id = self
.plots
.iter()
.find(|curve| curve.is_shown())
.map(|curve| curve.experiment_id.clone())
.unwrap_or_default();
if !id.is_empty() {
return Ok(id);
}
}
Err(TGAGUIError::BindingError(
"No selected curve. Select a curve first.".to_string(),
))
}
pub fn is_only_one_shown(&self) -> bool {
let mut shown_curves = self.plots.iter().filter(|curve| curve.is_shown());
let Some(first) = shown_curves.next() else {
return false;
};
shown_curves.all(|curve| curve.experiment_id == first.experiment_id)
}
pub fn start_selection(&mut self, start_point: [f64; 2]) {
self.interaction.start_selection(start_point);
self.clear_highlights();
}
pub fn update_pan(&mut self, current_point: [f64; 2]) {
self.interaction.update_pan(current_point);
}
pub fn update_selection(&mut self, end_point: [f64; 2]) {
if self.interaction.update_selection(end_point) {
self.update_highlighted_curves();
}
}
pub fn end_selection(&mut self) {
self.interaction.end_selection();
self.update_highlighted_curves();
}
pub fn clear_selection_rect(&mut self) {
self.interaction.clear_selection_rect();
self.clear_highlights();
}
pub fn zoom(&mut self, factor: f64, center: [f64; 2]) {
self.interaction.zoom(factor, center);
}
pub fn fit_to_selection(&mut self) {
self.interaction.fit_to_selection();
}
pub fn update_highlighted_curves(&mut self) {
if let Some(rect) = &self.interaction.selection_rect {
let (x_min, x_max, y_min, y_max) = rect.bounds();
for curve in &mut self.plots {
let has_point_in_selection = curve
.points
.iter()
.any(|&[x, y]| x >= x_min && x <= x_max && y >= y_min && y <= y_max);
curve.highlighted = has_point_in_selection;
}
} else {
self.clear_highlights();
}
}
pub fn clear_highlights(&mut self) {
for curve in &mut self.plots {
curve.highlighted = false;
}
}
pub fn reset_view(&mut self) {
let visible_plots: Vec<_> = self.plots.iter().filter(|p| p.is_shown()).collect();
if visible_plots.is_empty() {
self.interaction.view_range = DEFAULT_VIEW_RANGE;
self.interaction.view_y_range = (-1.0, 1.0);
} else {
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for plot in visible_plots {
x_min = x_min.min(plot.ranges.x_min);
x_max = x_max.max(plot.ranges.x_max);
y_min = y_min.min(plot.ranges.y_min);
y_max = y_max.max(plot.ranges.y_max);
}
let x_span = (x_max - x_min).abs();
let y_span = (y_max - y_min).abs();
let x_floor = x_max.abs().max(x_min.abs()) * 1e-6 + 1e-9;
let y_floor = y_max.abs().max(y_min.abs()) * 1e-6 + 1e-9;
let x_margin = (x_span * 0.05).max(x_floor);
let y_margin = (y_span * 0.05).max(y_floor);
self.interaction.view_range = (x_min - x_margin, x_max + x_margin);
self.interaction.view_y_range = (y_min - y_margin, y_max + y_margin);
}
self.reset_view_requested = true;
}
pub fn take_reset_view_request(&mut self) -> bool {
let was = self.reset_view_requested;
self.reset_view_requested = false;
was
}
pub fn find_nearest_curve(&self, point: [f64; 2]) -> Option<usize> {
let tolerance = 0.06;
self.plots
.iter()
.enumerate()
.filter(|(_, curve)| curve.is_shown())
.filter_map(|(i, curve)| {
curve
.find_nearest_distance(point)
.filter(|&d| d < tolerance)
.map(|d| (i, d))
})
.min_by(|a, b| a.1.total_cmp(&b.1))
.map(|(i, _)| i)
}
}