sensor_core/
conditional_image_renderer.rs1use std::ffi::OsString;
2use std::{cmp, fs};
3
4use log::error;
5
6use crate::{ConditionalImageConfig, ElementType, SensorType};
7
8pub 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
23fn render_text_sensor(
25 conditional_image_config: &ConditionalImageConfig,
26 cache_images_folder: &str,
27) -> Option<Vec<u8>> {
28 let image_path = get_image_based_on_text_sensor_value(
30 &conditional_image_config.sensor_value,
31 cache_images_folder,
32 );
33
34 image_path.and_then(|image_path| fs::read(image_path).ok())
38}
39
40fn render_number_sensor(
42 conditional_image_config: &ConditionalImageConfig,
43 cache_images_folder: &str,
44) -> Option<Vec<u8>> {
45 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 image_path.and_then(|image_path| fs::read(image_path).ok())
58}
59
60fn 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 images.is_empty() {
80 error!("No images found in folder {}", images_folder_path);
81 return None;
82 }
83
84 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
98fn 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 numbered_images.is_empty() {
116 error!("No images found in folder {}", images_folder);
117 return None;
118 }
119
120 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 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_best_fitting_image_path(numbered_images, transformed_sensor_value)
131}
132
133fn 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
163fn get_image_numbers_sorted(images_folder: &str) -> Vec<(f32, String)> {
166 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 image_names.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
182
183 image_names
184}
185
186fn to_number(string: OsString) -> Option<f32> {
188 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 while number_string.starts_with('.') {
198 number_string = number_string.chars().skip(1).collect()
199 }
200
201 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
212fn 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 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}