1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
use std::io::{BufWriter, Cursor};

use image::{ImageBuffer, Rgba, RgbaImage};

use crate::{hex_to_rgba, GraphConfig, GraphType};

/// Renders a graph based on the given config
/// # Returns
/// A vector of bytes containing the RGB8 png image
/// # Arguments
/// * `graph_config` - The config for the graph
pub fn render(graph_config: &GraphConfig) -> Vec<u8> {
    let width = graph_config.width;
    let height = graph_config.height;

    // Prepare the data for the graph
    let graph_data = prepare_graph_data(width, &graph_config.sensor_values);

    // Render the graph
    let mut image = match graph_config.graph_type {
        GraphType::Line => render_line_chart(&graph_data, graph_config),
        GraphType::LineFill => render_line_chart_filled(&graph_data, graph_config),
    };

    // Draw border if border is visible
    if !graph_config.border_color.ends_with("00") {
        draw_border(&mut image, &graph_config.border_color, width, height);
    }

    // Encode to png and return encoded bytes
    let mut writer = BufWriter::new(Cursor::new(Vec::new()));
    image
        .write_to(&mut writer, image::ImageOutputFormat::Png)
        .unwrap();

    writer.into_inner().unwrap().into_inner()
}

/// Draws a border around the specified image
fn draw_border(
    image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
    border_color: &str,
    width: u32,
    height: u32,
) {
    let border_color = hex_to_rgba(border_color);
    let border_x: i32 = 0;
    let border_y: i32 = 0;
    let border_width: u32 = width;
    let border_height: u32 = height;
    imageproc::drawing::draw_hollow_rect_mut(
        image,
        imageproc::rect::Rect::at(border_x, border_y).of_size(border_width, border_height),
        border_color,
    );
}

/// Prepares the plot data for the graph.
/// Aligns the sensor values to the width of the desired graph width.
fn prepare_graph_data(width: u32, sensor_values: &Vec<f64>) -> Vec<f64> {
    // Ensure that sensor values does not exceed the width, if so cut them and keep the last values
    let sensor_values = if sensor_values.len() > width as usize {
        sensor_values[(sensor_values.len() - width as usize)..].to_vec()
    } else {
        sensor_values.to_vec()
    };

    // Create a new vector for the width of the image, initialize with 0
    let mut plot_data: Vec<f64> = vec![0.0; (width) as usize];

    // Set the gen values to the end of plat data
    plot_data.splice(
        (width - sensor_values.len() as u32) as usize..,
        sensor_values,
    );
    plot_data
}

/// Renders a graph based on the given config
fn render_line_chart(numbers: &[f64], config: &GraphConfig) -> RgbaImage {
    let width = config.width;
    let height = config.height;
    let min_value = config.min_sensor_value.unwrap_or(get_min(numbers));
    let max_value = config.max_sensor_value.unwrap_or(get_max(numbers));
    let line_width = config.graph_stroke_width;
    let line_color = hex_to_rgba(&config.graph_color);
    let background_color = hex_to_rgba(&config.background_color);

    let mut image = RgbaImage::from_pixel(width, height, background_color);
    let half_line_width = (line_width / 2) as f32;

    for i in 0..numbers.len() - 1 {
        let current_value = numbers[i];
        let next_value = numbers[i + 1];

        // First move value between 0 and 1, where min_value is the lower bound and max_value the upper bound
        let current_value_normalized = (current_value - min_value) / (max_value - min_value);
        let next_value_normalized = (next_value - min_value) / (max_value - min_value);

        // Then move the value between 0 and height
        let img_line_start = current_value_normalized * height as f64;
        let img_line_end = next_value_normalized * height as f64;

        // Render line on image
        let x0 = i;
        let y0 = height as f64 - img_line_start;

        let x1 = i + 1;
        let y1 = height as f64 - img_line_end;

        // Draw graph line
        for offset in -half_line_width as i32..=half_line_width as i32 {
            imageproc::drawing::draw_line_segment_mut(
                &mut image,
                ((x0 as f32) + offset as f32, y0 as f32),
                ((x1 as f32) + offset as f32, y1 as f32),
                line_color,
            );
        }
    }

    image
}

/// Renders a graph based on the given config
fn render_line_chart_filled(numbers: &[f64], config: &GraphConfig) -> RgbaImage {
    let width = config.width;
    let height = config.height;
    let min_value = config.min_sensor_value.unwrap_or(get_min(numbers));
    let max_value = config.max_sensor_value.unwrap_or(get_max(numbers));
    let line_width = config.graph_stroke_width;
    let line_color = hex_to_rgba(&config.graph_color);
    let fill_color = line_color;
    let background_color = hex_to_rgba(&config.background_color);

    let mut image = RgbaImage::from_pixel(width, height, background_color);
    let half_line_width = (line_width / 2) as f32;

    for i in 0..numbers.len() - 1 {
        let current_value = numbers[i];
        let next_value = numbers[i + 1];

        // First move value between 0 and 1, where min_value is the lower bound and max_value the upper bound
        let current_value_normalized = (current_value - min_value) / (max_value - min_value);
        let next_value_normalized = (next_value - min_value) / (max_value - min_value);

        // Then move the value between 0 and height
        let img_line_start = current_value_normalized * height as f64;
        let img_line_end = next_value_normalized * height as f64;

        // Render line on image
        // Avoid attempt to subtract with overflow by using saturating_sub
        let x0 = i;
        let y0 = height.saturating_sub(img_line_start as u32);

        let x1 = i + 1;
        let y1 = height.saturating_sub(img_line_end as u32);

        // Fill the area under the line until image bottom, respect the line width
        let mut y = y0;
        while y < height {
            image.put_pixel(x0 as u32, y, fill_color);
            y += 1;
        }

        // Draw graph line
        for offset in -half_line_width as i32..=half_line_width as i32 {
            imageproc::drawing::draw_line_segment_mut(
                &mut image,
                ((x0 as f32) + offset as f32, y0 as f32),
                ((x1 as f32) + offset as f32, y1 as f32),
                line_color,
            );
        }
    }

    image
}

/// Returns the minimum value of the given vector
fn get_min(values: &[f64]) -> f64 {
    let mut min = values[0];
    for value in values {
        if *value < min {
            min = *value;
        }
    }
    min
}

/// Returns the maximum value of the given vector
fn get_max(values: &[f64]) -> f64 {
    let mut max = values[0];
    for value in values {
        if *value > max {
            max = *value;
        }
    }
    max
}