Skip to content
Snippets Groups Projects
node_list.rs 6.95 KiB
use chrono::DateTime;
use chrono_humanize::HumanTime;
use egui_extras::{Column, TableBuilder};
use figment::providers::Data;
use log::{error, info};
use longitude::{DistanceUnit, Location};
use meshtastic::protobufs::{channel::Role, HardwareModel, Position};

use crate::{db::Database, CONFIG};

// The list of nodes
// This lists all the info about each node.
// Sortable by any column, allow hiding columns?
// Nice signal strength bar (also allow clicking to view signal strength history?)
// Filters:
// - Direct Connection (direct neighbours / nodes with 0 hops away)
// - Active (seen in the last `n` minutes)
// - Inactive (not seen in the last `n` minutes)
// - Location (has/doesn't have gps location)

/// Calculates the distance betwen 2 coordinate points.
pub(crate) fn calc_distance_to_node(away: Position) -> Option<f64> {
    let home_pos = Database::read_home_node().expect("Failed to find Home Node").position.unwrap();

    let distance = Location {
        // Home Base
        latitude: home_pos.latitude_i as f64 * 1e-7,
        longitude: home_pos.longitude_i as f64 * 1e-7,
    }.distance(&Location {
        // Away
        latitude: away.latitude_i as f64 * 1e-7,
        longitude: away.longitude_i as f64 * 1e-7,
    });
    // convert to the right distance depending on settings
    match CONFIG.gui.units.as_ref() {
        "Centimeters" => Some(distance.in_unit(DistanceUnit::Centimeters)),
        "Meters" => Some(distance.in_unit(DistanceUnit::Meters)),
        "Kilometers" => Some(distance.in_unit(DistanceUnit::Kilometers)),
        "Inches" => Some(distance.in_unit(DistanceUnit::Inches)),
        "Feet" => Some(distance.in_unit(DistanceUnit::Feet)),
        "Yards" => Some(distance.in_unit(DistanceUnit::Yards)),
        "Miles" => Some(distance.in_unit(DistanceUnit::Miles)),
        _ => Some(distance.in_unit(DistanceUnit::Kilometers)), // if you can't type, force km.
    }
}

/// UI Elements for the Node List
pub fn nodelist_ui(ui: &mut egui::Ui) {
    // Get nodes from DB
    let mut nodes = Database::read_node_list().unwrap_or_else(|e| {
        error!("Failed to read nodes: {}", e);
        // Return a default value or handle the error as needed
        Vec::new() // Example default value, replace with appropriate handling
    });

    info!("Loaded {} nodes from DB", nodes.len());

    // let mut nodes = Database::read_node_list().expect("Failed to read nodes");
    
    // Sort nodes by `last_heard` in descending order
    nodes.sort_by(|a, b| b.last_heard.cmp(&a.last_heard));
    
    TableBuilder::new(ui)
    // TODO for the table:
    // - Clickable Table Sorting
    // - Add more info
    // - Auto hide/show scrollbar on smaller devices
    // - Add to a panel that we can move around / shitty windowing.
        .column(Column::remainder())            // node id
        .column(Column::remainder().clip(true)) // node name
        .column(Column::remainder())            // snr
        .column(Column::remainder())            // dist
        .column(Column::remainder())            // hops
        .column(Column::remainder().clip(true))            // hw model
        .column(Column::remainder())            // role
        .column(Column::remainder())            // battery
        .column(Column::remainder().clip(true))            // last heard
        .striped(true)
        .header(15.0, |mut header| {
            header.col(|ui| {
                ui.heading("Node ID");
            });
            header.col(|ui| {
                ui.heading("Name");
            });
            header.col(|ui| {
                ui.heading("SNR");
            });
            header.col(|ui| {
            
                // Get distance units
                let unit = match CONFIG.gui.units.as_ref() {
                    "Centimeters" => "cm",
                    "Meters" => "m",
                    "Kilometers" => "km",
                    "Inches" => "in",
                    "Feet" => "ft",
                    "Yards" => "yd",
                    "Miles" => "mi",
                    _ => "km", // Default to km if no match found.
                };
            
                ui.heading(format!("Distance ({})", unit));
            });
            header.col(|ui| {
                ui.heading("Hops Away");
            });
            header.col(|ui| {
                ui.heading("HW Model");
            });
            header.col(|ui| {
                ui.heading("Role");
            });
            header.col(|ui| {
                ui.heading("Battery");
            });
            header.col(|ui| {
                ui.heading("Last Heard");
            });
        })
        .body(|mut body| {
            for node in nodes {
                let user = node.user.unwrap();
                let device = node.device_metrics.unwrap();
                // Convert the last heard to a time, then make it nice to read.
                let now = chrono::Local::now().to_utc();
                let humanised = if node.last_heard == 0 {
                    String::from("Unknown")
                } else {
                    let last_heard = DateTime::from_timestamp(node.last_heard.into(), 0).unwrap();
                    let duration = last_heard - now;
                    HumanTime::from(duration).to_string()
                };
            
                body.row(15.0, |mut row| {
                    row.col(|ui| {
                        ui.label(user.id);
                    });
                    row.col(|ui| {
                        ui.label(format!("{} ({})", user.long_name, user.short_name));
                    });
                    row.col(|ui| {
                        ui.label(node.snr.to_string());
                    });
                    row.col(|ui| {
                        if let Some(distance) = calc_distance_to_node(node.position.unwrap()) {
                            if distance.is_nan() {
                                ui.label("-");
                            } else {
                                ui.label(format!("{:.2}", distance));
                            }
                        } else {
                            ui.label("-");
                        }
                    });
                    row.col(|ui| {
                        ui.label(node.hops_away.to_string());
                    });
                    row.col(|ui| {
                        // Convert the i32 to a HardwareModel object, then convert to string
                        ui.label(HardwareModel::from_i32(user.hw_model).unwrap_or_default().as_str_name());
                    });
                    row.col(|ui| {
                        // Convert the i32 to a Role object, convert to string.
                        ui.label(Role::from_i32(user.role).unwrap_or_default().as_str_name());
                    });
                    row.col(|ui| {
                        ui.label(device.battery_level.to_string());
                    });
                    row.col(|ui| {
                        ui.label(humanised);
                    });
                });
            }
        });
}