sensor_core/
conditional_image_renderer.rs

1use std::ffi::OsString;
2use std::{cmp, fs};
3
4use log::error;
5
6use crate::{ConditionalImageConfig, ElementType, SensorType};
7
8/// Get the image data based on the current sensor value and type
9pub fn render(
10    element_id: &str,
11    sensor_type: &SensorType,
12    conditional_image_config: &ConditionalImageConfig,
13) -> Option<Vec<u8>> {
14    let cache_image_folder = crate::get_cache_dir(element_id, &ElementType::ConditionalImage);
15    let cache_image_folder = cache_image_folder.to_str().unwrap();
16
17    match sensor_type {
18        SensorType::Text => render_text_sensor(conditional_image_config, cache_image_folder),
19        SensorType::Number => render_number_sensor(conditional_image_config, cache_image_folder),
20    }
21}
22
23/// Renders a given text sensor to an conditional image
24fn render_text_sensor(
25    conditional_image_config: &ConditionalImageConfig,
26    cache_images_folder: &str,
27) -> Option<Vec<u8>> {
28    // Select image based on sensor value
29    let image_path = get_image_based_on_text_sensor_value(
30        &conditional_image_config.sensor_value,
31        cache_images_folder,
32    );
33
34    // Read image to memory
35    // We heavily assume that this is already png encoded to skip the expensive png decoding
36    // So just read the image here
37    image_path.and_then(|image_path| fs::read(image_path).ok())
38}
39
40/// Renders a given number sensor to an conditional image
41fn render_number_sensor(
42    conditional_image_config: &ConditionalImageConfig,
43    cache_images_folder: &str,
44) -> Option<Vec<u8>> {
45    // Select image based on sensor value
46    let sensor_value: f64 = conditional_image_config.sensor_value.parse().unwrap();
47    let image_path = get_image_based_on_numeric_sensor_value(
48        conditional_image_config.min_sensor_value,
49        conditional_image_config.max_sensor_value,
50        sensor_value,
51        cache_images_folder,
52    );
53
54    // Read image to memory
55    // We heavily assume that this is already png encoded to skip the expensive png decoding
56    // So just read the image here
57    image_path.and_then(|image_path| fs::read(image_path).ok())
58}
59
60/// Selects the image that fits the sensor value best
61fn get_image_based_on_text_sensor_value(
62    sensor_value: &str,
63    images_folder_path: &str,
64) -> Option<String> {
65    let images: Vec<(String, String)> = fs::read_dir(images_folder_path)
66        .unwrap()
67        .flatten()
68        .filter(|dir_entry| dir_entry.file_type().unwrap().is_file())
69        .filter(crate::is_image)
70        .map(|dir_entry| {
71            (
72                remove_file_extension(dir_entry.file_name()),
73                dir_entry.path().to_str().unwrap().to_string(),
74            )
75        })
76        .collect();
77
78    // If there is no image
79    if images.is_empty() {
80        error!("No images found in folder {}", images_folder_path);
81        return None;
82    }
83
84    // Select the image based on the lowest levehnstein distance to the sensor value
85    let mut best_image_path = None;
86    let mut min_distance = usize::MAX;
87    for (image_name, image_path) in images {
88        let distance = levenshtein_distance(sensor_value, &image_name);
89        if distance < min_distance {
90            min_distance = distance;
91            best_image_path = Some(image_path);
92        }
93    }
94
95    best_image_path
96}
97
98/// Returns the image path that fits the sensor value best
99/// The sensor value is transformed to the image number coordination system
100/// The image number coordination system is the range of all image numbers
101/// # Arguments
102/// * `sensor_min` - The minimum value of the sensor
103/// * `sensor_max` - The maximum value of the sensor
104/// * `sensor_value` - The current value of the sensor
105/// * `images_folder` - The folder where the images are stored
106fn get_image_based_on_numeric_sensor_value(
107    sensor_min: f64,
108    sensor_max: f64,
109    sensor_value: f64,
110    images_folder: &str,
111) -> Option<String> {
112    let numbered_images = get_image_numbers_sorted(images_folder);
113
114    // If there is none
115    if numbered_images.is_empty() {
116        error!("No images found in folder {}", images_folder);
117        return None;
118    }
119
120    // get min and max of images
121    let image_number_min = numbered_images.first().unwrap().0 as f64;
122    let image_number_max = numbered_images.last().unwrap().0 as f64;
123
124    // Move the sensor value number into the image number coordination system / range
125    let transformed_sensor_value = (sensor_value - sensor_min) / (sensor_max - sensor_min)
126        * (image_number_max - image_number_min)
127        + image_number_min;
128
129    // Get the image that has the lowest distance to the calculated value
130    get_best_fitting_image_path(numbered_images, transformed_sensor_value)
131}
132
133/// Returns the image name that has the lowest distance to the transformed sensor value
134fn get_best_fitting_image_path(
135    numbered_images: Vec<(f32, String)>,
136    transformed_sensor_value: f64,
137) -> Option<String> {
138    let mut best_image_path = None;
139    let mut min_distance = f64::MAX;
140    for (number, image_path) in numbered_images {
141        let distance = (transformed_sensor_value - number as f64).abs();
142        if distance < min_distance {
143            min_distance = distance;
144            best_image_path = Some(image_path);
145        }
146    }
147    best_image_path
148}
149
150fn remove_file_extension(file_name: OsString) -> String {
151    let file_name = file_name.to_str().unwrap();
152    let mut file_name = file_name.to_string();
153    let extension = file_name.split('.').last();
154    if let Some(extension) = extension {
155        file_name = file_name
156            .chars()
157            .take(file_name.len() - extension.len() - 1)
158            .collect();
159    }
160    file_name
161}
162
163/// Returns a vector of tuples with the image number and the image path
164/// The vector is sorted by the image number
165fn get_image_numbers_sorted(images_folder: &str) -> Vec<(f32, String)> {
166    // Get all image names and parse them to numbers
167    // "1.png" -> 1.0
168    // "-1,123.png" -> -1.123
169    let mut image_names: Vec<(f32, String)> = fs::read_dir(images_folder)
170        .unwrap()
171        .flatten()
172        .filter(|dir_entry| dir_entry.file_type().unwrap().is_file())
173        .filter(crate::is_image)
174        .flat_map(|dir_entry| {
175            let number = to_number(dir_entry.file_name());
176            number.map(|num| (num, dir_entry.path().to_str().unwrap().to_string()))
177        })
178        .collect();
179
180    // Sort by number
181    image_names.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
182
183    image_names
184}
185
186/// Converts a OsString to a float number
187fn to_number(string: OsString) -> Option<f32> {
188    // Replace "," with "." to make it a parseable number
189    let number_string = string.to_str().unwrap().replace(',', ".");
190
191    let mut number_string: String = number_string
192        .chars()
193        .filter(|c| c.is_ascii_digit() || *c == '.')
194        .collect();
195
196    // Remove all "." at the beginning
197    while number_string.starts_with('.') {
198        number_string = number_string.chars().skip(1).collect()
199    }
200
201    // Remove all "." at the end
202    while number_string.ends_with('.') {
203        number_string = number_string
204            .chars()
205            .take(number_string.len() - 1)
206            .collect()
207    }
208
209    number_string.parse().ok()
210}
211
212/// Returns the Levenshtein distance between two strings
213/// Source: https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Rust
214fn levenshtein_distance(s1: &str, s2: &str) -> usize {
215    let v1: Vec<char> = s1.chars().collect();
216    let v2: Vec<char> = s2.chars().collect();
217    let v1len = v1.len();
218    let v2len = v2.len();
219
220    // Early exit if one of the strings is empty
221    if v1len == 0 {
222        return v2len;
223    }
224    if v2len == 0 {
225        return v1len;
226    }
227
228    fn min3<T: Ord>(v1: T, v2: T, v3: T) -> T {
229        cmp::min(v1, cmp::min(v2, v3))
230    }
231    fn delta(x: char, y: char) -> usize {
232        if x == y {
233            0
234        } else {
235            1
236        }
237    }
238
239    let mut column: Vec<usize> = (0..v1len + 1).collect();
240    for x in 1..v2len + 1 {
241        column[0] = x;
242        let mut lastdiag = x - 1;
243        for y in 1..v1len + 1 {
244            let olddiag = column[y];
245            column[y] = min3(
246                column[y] + 1,
247                column[y - 1] + 1,
248                lastdiag + delta(v1[y - 1], v2[x - 1]),
249            );
250            lastdiag = olddiag;
251        }
252    }
253    column[v1len]
254}