gmt_dos-clients_scope 3.1.1

GMT DOS Scope Client
use std::{
    env,
    sync::{Arc, OnceLock},
};

use super::{ClientError, Scope};
use eframe::egui;
use egui_plot::{Legend, Plot, PlotUi};
use interface::UniqueIdentifier;
use tokio::sync::broadcast;

const PLOT_SIZE: (f32, f32) = (600f32, 500f32);
const MAX_WINDOW_SIZE: (f32, f32) = (1200f32, 1000f32);

#[derive(Debug, thiserror::Error)]
pub enum GridScopeError {
    #[error("failed to create the scope within the grid")]
    Pin(#[from] ClientError),
}
pub type Result<T> = std::result::Result<T, GridScopeError>;

struct NodeScope {
    indices: (usize, usize),
    scope: Scope,
}

/// Display [Scope]s in a grid like pattern
pub struct GridScope {
    size: (usize, usize),
    scopes: Vec<NodeScope>,
    server_ip: String,
    client_address: String,
    n_sample: Option<usize>,
    egui_ctx: Arc<OnceLock<egui::Context>>,
}
impl GridScope {
    /// Creates a new grid layout for [Scope]s
    ///
    /// `size` sets the number of rows and columns
    pub fn new(size: (usize, usize)) -> Self {
        Self {
            size,
            scopes: vec![],
            server_ip: env::var("SCOPE_SERVER_IP").unwrap_or(crate::SERVER_IP.into()),
            client_address: crate::CLIENT_ADDRESS.into(),
            n_sample: None,
            egui_ctx: Arc::new(OnceLock::new()),
        }
    }
    /// Sets the number of samples to be displayed
    pub fn n_sample(mut self, n_sample: usize) -> Self {
        self.n_sample = Some(n_sample);
        self
    }
    /// Sets the server IP address
    pub fn server_ip<S: Into<String>>(mut self, server_ip: S) -> Self {
        self.server_ip = server_ip.into();
        self
    }
    /// Sets the client internet socket address
    pub fn client_address<S: Into<String>>(mut self, client_address: S) -> Self {
        self.client_address = client_address.into();
        self
    }
    fn window_size(&self) -> (f32, f32) {
        let (rows, cols) = self.size;
        let width = MAX_WINDOW_SIZE.0.min(PLOT_SIZE.0 * cols as f32) / cols as f32;
        let height = MAX_WINDOW_SIZE.1.min(PLOT_SIZE.1 * rows as f32) / rows as f32;
        (width * cols as f32, height * rows as f32)
    }
    /// Sets a [Scope] at position `(row,column)` in the grid layout
    pub fn pin<U>(mut self, indices: (usize, usize)) -> Result<Self>
    where
        U: UniqueIdentifier + 'static,
    {
        let (rows, cols) = self.size;
        let (row, col) = indices;
        assert!(
            row < rows,
            "The row index in the scopes grid must be less than {}",
            rows
        );
        assert!(
            col < cols,
            "The columm index in the scopes grid must be less than {}",
            cols
        );
        if let Some(node) = self.scopes.iter_mut().find(|node| node.indices == indices) {
            node.scope.as_mut_signal::<U>()?;
        } else {
            self.scopes.push(NodeScope {
                indices,
                scope: Scope::new()
                    .server_ip(&self.server_ip)
                    .client_address(&self.client_address)
                    .signal::<U>()?,
            });
        }

        /*         self.scopes.push(NodeScope {
            indices,
            scope: Scope::new()
                .server_ip(&self.server_ip)
                .client_address(&self.client_address)
                .signal::<U>()?,
        }); */
        Ok(self)
    }
    pub fn pin_with_legends<U>(
        mut self,
        indices: (usize, usize),
        items: &[String],
        rx: broadcast::Receiver<Vec<String>>,
    ) -> Result<Self>
    where
        U: UniqueIdentifier + 'static,
    {
        let (rows, cols) = self.size;
        let (row, col) = indices;
        assert!(
            row < rows,
            "The row index in the scopes grid must be less than {}",
            rows
        );
        assert!(
            col < cols,
            "The columm index in the scopes grid must be less than {}",
            cols
        );
        if let Some(node) = self.scopes.iter_mut().find(|node| node.indices == indices) {
            node.scope.as_mut_signal::<U>()?;
        } else {
            self.scopes.push(NodeScope {
                indices,
                scope: Scope::new()
                    .server_ip(&self.server_ip)
                    .client_address(&self.client_address)
                    .rx(rx)
                    .signal_with_legends::<U>(items.to_vec())?,
            });
        }

        /*         self.scopes.push(NodeScope {
            indices,
            scope: Scope::new()
                .server_ip(&self.server_ip)
                .client_address(&self.client_address)
                .signal::<U>()?,
        }); */
        Ok(self)
    }
    /// Returns a handle to the egui context, set once the window is created.
    /// Callers can use it to call `ctx.request_repaint()` from other threads.
    pub fn egui_ctx(&self) -> Arc<OnceLock<egui::Context>> {
        self.egui_ctx.clone()
    }
    /// Display the scope
    pub fn show(mut self) {
        for node in self.scopes.iter_mut() {
            node.scope.n_sample = self.n_sample.clone();
            let monitor = node.scope.monitor.take().unwrap();
            tokio::spawn(async move {
                match monitor.join().await {
                    Ok(_) => println!("*** data streaming complete ***"),
                    Err(e) => println!("!!! data streaming error with {:?} !!!", e),
                }
            });
        }
        let native_options = eframe::NativeOptions {
            viewport: egui::ViewportBuilder::default()
                .with_inner_size(egui::Vec2::from(self.window_size())),
            ..Default::default()
        };
        let _ = eframe::run_native(
            "GMT DOS Actors Scope",
            native_options,
            Box::new(|cc| {
                for node in self.scopes.iter_mut() {
                    let scope = &mut node.scope;
                    scope.run(cc.egui_ctx.clone());
                }
                Ok(Box::new(self))
            }),
        );
    }
}

impl eframe::App for GridScope {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        let _ = self.egui_ctx.set(ctx.clone());
        egui::CentralPanel::default().show(ctx, |ui| {
            let (rows, cols) = self.size;
            let style = ui.style_mut();
            style.spacing.item_spacing = egui::vec2(0.0, 0.0);

            // Calculate plot sizes dynamically based on available space
            let available_size = ui.available_size();
            let plot_width = available_size.x / cols as f32;
            let plot_height = available_size.y / rows as f32;

            for row in 0..rows {
                ui.horizontal(|ui| {
                    for col in 0..cols {
                        self.scopes
                            .iter_mut()
                            .find(|node| node.indices == (row, col))
                            .map(|node| {
                                let plot = Plot::new(format!("Scope_{}_{}", row, col))
                                    .legend(Legend::default().position(egui_plot::Corner::LeftTop))
                                    .width(plot_width)
                                    .height(plot_height)
                                    .set_margin_fraction(egui::Vec2::from((0.05, 0.05)))
                                    .link_axis("x_axis_link", [true, false]);
                                plot.show(ui, |plot_ui: &mut PlotUi| {
                                    for signal in &mut node.scope.signals {
                                        signal.plot_ui(plot_ui, node.scope.n_sample)
                                    }
                                });
                            });
                    }
                });
            }
        });
        for NodeScope { scope, .. } in &mut self.scopes {
            if let Some(rx) = scope.rx.as_mut()
                && let Ok(items) = rx.try_recv()
            {
                for signal in &mut scope.signals {
                    signal.set_hidden(items.clone());
                }
            }
        }
    }
}