gauges 0.1.0

A tool to visualize real time gauges
Documentation
use std::f64::consts::PI;

use angle::Angle;
use angle::Rad;
use dioxus::prelude::*;

use crate::core::{
    Age, ArcGaugeStyle, CircleGaugeStyle, GaugeStyle, GaugeTextFormat, ProtractorGaugeStyle, Range,
    SignalInfo, Value,
};

#[derive(PartialEq, Props)]
pub struct GaugeProps {
    value: Value,
    style: GaugeStyle,
    range: Range,
    signal: SignalInfo,
    format: GaugeTextFormat,
    age: Age,
}

fn circle_stroke(width: Rad<f64>, offset: Rad<f64>) -> (String, String) {
    let offset = -offset + Rad::half_pi();
    let a = width;
    let b = Rad::two_pi() - width;

    let offset = offset.value();
    let a = a.value();
    let b = b.value();

    (format!("{a},{b}"), format!("{offset}"))
}

fn format(value: &Value, options: &GaugeTextFormat) -> String {
    match value {
        Value::Float(x) => format!("{0:.1$}", x, options.precision),
        Value::Percent(x) => format!("{0:.1$}%", x, options.precision),
        Value::None => "N/A".to_string(),
    }
}

fn class_from_age(age: &Age) -> &'static str {
    match age {
        Age::New => "age-new",
        Age::Valid => "age-valid",
        Age::Expired => "age-expired",
        Age::Unknown => "age-unknown",
    }
}

#[allow(non_snake_case)]
pub fn Gauge(cx: Scope<GaugeProps>) -> Element {
    let inner_style: ExtArcGaugeStyle = match cx.props.style {
        GaugeStyle::Arc(style) => style.into(),
        GaugeStyle::Circle(style) => style.into(),
        GaugeStyle::Protractor(style) => style.into(),
    };

    let inner = ExtArcGauge(cx, inner_style);

    let info = cx.props.signal.name.as_deref().unwrap_or("-");
    let text = format(&cx.props.value, &cx.props.format);

    let inner_width = inner_style.width;
    let inner_height = inner_style.height;

    let info_width = inner_width;
    let info_height = 30.0;

    let full_width = inner_width;
    let full_height = inner_height + info_height;

    let indicator_age_class = class_from_age(&cx.props.age);

    cx.render(rsx! {
        div {
            class: "gauge",
            margin: "10px",
            width: "{full_width}px",
            height: "{full_height}px",
            div {
                class: "gauge-info-wrapper",
                padding: "5px",
                width: "{info_width}px",
                height: "{info_height}px",
                div {
                    class: "gauge-info",
                    "{info}"
                }
            }
            div {
                class: "gauge-value-text-wrapper",
                width: "{inner_width}px",
                height: "{inner_height}px",
                div {
                    class: "gauge-value-text",
                    "{text}"
                }
            }
            div {
                class: "gauge-inner-wrapper",
                inner
            }
            div {
                class: "gauge-update-indicator {indicator_age_class}",
            }
        }
    })
}

#[allow(non_snake_case)]
fn ExtArcGauge(cx: Scope<GaugeProps>, style: ExtArcGaugeStyle) -> Element {
    let value = match cx.props.value {
        Value::None => None,
        Value::Float(x) => Some(x),
        Value::Percent(x) => Some(x),
    };

    if value.is_none() {
        return NoneGauge(cx);
    }

    let ExtArcGaugeStyle {
        radius,
        begin_angle,
        full_width,
        arrow,
        normalize_policy,
        width,
        height,
    } = style;

    let value: f64 = value?;

    let min_value = cx.props.range.min;
    let max_value = cx.props.range.max;

    let range_width = max_value - min_value;

    let norm_value = match normalize_policy {
        NormalizePolicy::Clamp => {
            let clamped = value.clamp(min_value, max_value);
            (clamped - min_value) / range_width
        }
        NormalizePolicy::Mod => {
            let z = value.rem_euclid(range_width);
            (z - min_value) / range_width
        }
    };

    let center_x = width / 2.;
    let center_y = width / 2.;

    let real_width = full_width * norm_value;

    let arrow_width = Rad(0.05);
    let arrow_angle = begin_angle + real_width - (arrow_width / 2.0);

    let show_arrow = arrow == ArrowType::OnlyArrow;
    let show_real = arrow == ArrowType::NoArrow;

    cx.render(rsx! {
        svg {
            class: "gauge-inner",
            width: width,
            height: height,
            Arc {
                class: "arc-background",
                center_x: center_x,
                center_y: center_y,
                radius: radius,
                begin_angle: begin_angle,
                width: full_width,
                stroke_width: 20.0,
            }
            if show_real {
                rsx!(Arc {
                    class: "arc-foreground",
                    center_x: center_x,
                    center_y: center_y,
                    radius: radius,
                    begin_angle: begin_angle,
                    width: real_width,
                    stroke_width: 20.0,
                })
            }
            if show_arrow {
                rsx!(Arc {
                    class: "arc-arrow",
                    center_x: center_x,
                    center_y: center_y,
                    radius: radius,
                    begin_angle: arrow_angle,
                    width: arrow_width,
                    stroke_width: 30.0,
                })
            }
        }
    })
}

#[allow(non_snake_case)]
fn NoneGauge(cx: Scope<GaugeProps>) -> Element {
    cx.render(rsx! {
        div {
        }
    })
}

#[derive(PartialEq, Props)]
struct ArcProps {
    center_x: f64,
    center_y: f64,
    begin_angle: Rad<f64>,
    width: Rad<f64>,
    radius: f64,
    class: &'static str,
    stroke_width: f64,
}

#[allow(non_snake_case)]
fn Arc(cx: Scope<ArcProps>) -> Element {
    let ArcProps {
        center_x,
        center_y,
        begin_angle,
        width,
        radius,
        class,
        stroke_width,
    } = *cx.props;

    let (dash_array, dash_offset) = circle_stroke(width, begin_angle);

    cx.render(rsx! {
        circle {
            fill: "none",
            class: class,
            stroke_width: stroke_width,
            cx: center_x,
            cy: center_y,
            r: radius,
            path_length: 2.0 * PI,
            stroke_dasharray: "{dash_array}",
            stroke_dashoffset: "{dash_offset}",
        }
    })
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum NormalizePolicy {
    Clamp,
    Mod,
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum ArrowType {
    NoArrow,
    OnlyArrow,
}

#[derive(Debug, PartialEq, Clone, Copy)]
struct ExtArcGaugeStyle {
    radius: f64,
    width: f64,
    height: f64,
    begin_angle: Rad<f64>,
    full_width: Rad<f64>,
    arrow: ArrowType,
    normalize_policy: NormalizePolicy,
}

impl From<ArcGaugeStyle> for ExtArcGaugeStyle {
    fn from(val: ArcGaugeStyle) -> Self {
        ExtArcGaugeStyle {
            radius: val.radius,
            begin_angle: val.begin_angle,
            full_width: val.full_width,
            arrow: ArrowType::NoArrow,
            normalize_policy: NormalizePolicy::Clamp,
            width: val.radius * 3.0,
            height: val.radius * 3.0,
        }
    }
}

impl From<CircleGaugeStyle> for ExtArcGaugeStyle {
    fn from(val: CircleGaugeStyle) -> Self {
        ExtArcGaugeStyle {
            radius: val.radius,
            begin_angle: Rad(0.0),
            full_width: Rad::two_pi(),
            arrow: ArrowType::NoArrow,
            normalize_policy: NormalizePolicy::Clamp,
            width: val.radius * 3.0,
            height: val.radius * 3.0,
        }
    }
}

impl From<ProtractorGaugeStyle> for ExtArcGaugeStyle {
    fn from(val: ProtractorGaugeStyle) -> Self {
        ExtArcGaugeStyle {
            radius: val.radius,
            begin_angle: Rad(0.0),
            full_width: Rad::two_pi(),
            arrow: ArrowType::OnlyArrow,
            normalize_policy: NormalizePolicy::Mod,
            width: val.radius * 3.0,
            height: val.radius * 3.0,
        }
    }
}