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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
use std::ffi::OsString;
use std::{cmp, fs};

use log::error;

use crate::{ConditionalImageConfig, ElementType, SensorType};

/// Get the image data based on the current sensor value and type
pub fn render(
    element_id: &str,
    sensor_type: &SensorType,
    conditional_image_config: &ConditionalImageConfig,
) -> Option<Vec<u8>> {
    let cache_image_folder = crate::get_cache_dir(element_id, &ElementType::ConditionalImage);
    let cache_image_folder = cache_image_folder.to_str().unwrap();

    match sensor_type {
        SensorType::Text => render_text_sensor(conditional_image_config, cache_image_folder),
        SensorType::Number => render_number_sensor(conditional_image_config, cache_image_folder),
    }
}

/// Renders a given text sensor to an conditional image
fn render_text_sensor(
    conditional_image_config: &ConditionalImageConfig,
    cache_images_folder: &str,
) -> Option<Vec<u8>> {
    // Select image based on sensor value
    let image_path = get_image_based_on_text_sensor_value(
        &conditional_image_config.sensor_value,
        cache_images_folder,
    );

    // Read image to memory
    // We heavily assume that this is already png encoded to skip the expensive png decoding
    // So just read the image here
    image_path.and_then(|image_path| fs::read(image_path).ok())
}

/// Renders a given number sensor to an conditional image
fn render_number_sensor(
    conditional_image_config: &ConditionalImageConfig,
    cache_images_folder: &str,
) -> Option<Vec<u8>> {
    // Select image based on sensor value
    let sensor_value: f64 = conditional_image_config.sensor_value.parse().unwrap();
    let image_path = get_image_based_on_numeric_sensor_value(
        conditional_image_config.min_sensor_value,
        conditional_image_config.max_sensor_value,
        sensor_value,
        cache_images_folder,
    );

    // Read image to memory
    // We heavily assume that this is already png encoded to skip the expensive png decoding
    // So just read the image here
    image_path.and_then(|image_path| fs::read(image_path).ok())
}

/// Selects the image that fits the sensor value best
fn get_image_based_on_text_sensor_value(
    sensor_value: &str,
    images_folder_path: &str,
) -> Option<String> {
    let images: Vec<(String, String)> = fs::read_dir(images_folder_path)
        .unwrap()
        .flatten()
        .filter(|dir_entry| dir_entry.file_type().unwrap().is_file())
        .filter(crate::is_image)
        .map(|dir_entry| {
            (
                remove_file_extension(dir_entry.file_name()),
                dir_entry.path().to_str().unwrap().to_string(),
            )
        })
        .collect();

    // If there is no image
    if images.is_empty() {
        error!("No images found in folder {}", images_folder_path);
        return None;
    }

    // Select the image based on the lowest levehnstein distance to the sensor value
    let mut best_image_path = None;
    let mut min_distance = usize::MAX;
    for (image_name, image_path) in images {
        let distance = levenshtein_distance(sensor_value, &image_name);
        if distance < min_distance {
            min_distance = distance;
            best_image_path = Some(image_path);
        }
    }

    best_image_path
}

/// Returns the image path that fits the sensor value best
/// The sensor value is transformed to the image number coordination system
/// The image number coordination system is the range of all image numbers
/// # Arguments
/// * `sensor_min` - The minimum value of the sensor
/// * `sensor_max` - The maximum value of the sensor
/// * `sensor_value` - The current value of the sensor
/// * `images_folder` - The folder where the images are stored
fn get_image_based_on_numeric_sensor_value(
    sensor_min: f64,
    sensor_max: f64,
    sensor_value: f64,
    images_folder: &str,
) -> Option<String> {
    let numbered_images = get_image_numbers_sorted(images_folder);

    // If there is none
    if numbered_images.is_empty() {
        error!("No images found in folder {}", images_folder);
        return None;
    }

    // get min and max of images
    let image_number_min = numbered_images.first().unwrap().0 as f64;
    let image_number_max = numbered_images.last().unwrap().0 as f64;

    // Move the sensor value number into the image number coordination system / range
    let transformed_sensor_value = (sensor_value - sensor_min) / (sensor_max - sensor_min)
        * (image_number_max - image_number_min)
        + image_number_min;

    // Get the image that has the lowest distance to the calculated value
    get_best_fitting_image_path(numbered_images, transformed_sensor_value)
}

/// Returns the image name that has the lowest distance to the transformed sensor value
fn get_best_fitting_image_path(
    numbered_images: Vec<(f32, String)>,
    transformed_sensor_value: f64,
) -> Option<String> {
    let mut best_image_path = None;
    let mut min_distance = f64::MAX;
    for (number, image_path) in numbered_images {
        let distance = (transformed_sensor_value - number as f64).abs();
        if distance < min_distance {
            min_distance = distance;
            best_image_path = Some(image_path);
        }
    }
    best_image_path
}

fn remove_file_extension(file_name: OsString) -> String {
    let file_name = file_name.to_str().unwrap();
    let mut file_name = file_name.to_string();
    let extension = file_name.split('.').last();
    if let Some(extension) = extension {
        file_name = file_name
            .chars()
            .take(file_name.len() - extension.len() - 1)
            .collect();
    }
    file_name
}

/// Returns a vector of tuples with the image number and the image path
/// The vector is sorted by the image number
fn get_image_numbers_sorted(images_folder: &str) -> Vec<(f32, String)> {
    // Get all image names and parse them to numbers
    // "1.png" -> 1.0
    // "-1,123.png" -> -1.123
    let mut image_names: Vec<(f32, String)> = fs::read_dir(images_folder)
        .unwrap()
        .flatten()
        .filter(|dir_entry| dir_entry.file_type().unwrap().is_file())
        .filter(crate::is_image)
        .flat_map(|dir_entry| {
            let number = to_number(dir_entry.file_name());
            number.map(|num| (num, dir_entry.path().to_str().unwrap().to_string()))
        })
        .collect();

    // Sort by number
    image_names.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());

    image_names
}

/// Converts a OsString to a float number
fn to_number(string: OsString) -> Option<f32> {
    // Replace "," with "." to make it a parseable number
    let number_string = string.to_str().unwrap().replace(',', ".");

    let mut number_string: String = number_string
        .chars()
        .filter(|c| c.is_ascii_digit() || *c == '.')
        .collect();

    // Remove all "." at the beginning
    while number_string.starts_with('.') {
        number_string = number_string.chars().skip(1).collect()
    }

    // Remove all "." at the end
    while number_string.ends_with('.') {
        number_string = number_string
            .chars()
            .take(number_string.len() - 1)
            .collect()
    }

    number_string.parse().ok()
}

/// Returns the Levenshtein distance between two strings
/// Source: https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Rust
fn levenshtein_distance(s1: &str, s2: &str) -> usize {
    let v1: Vec<char> = s1.chars().collect();
    let v2: Vec<char> = s2.chars().collect();
    let v1len = v1.len();
    let v2len = v2.len();

    // Early exit if one of the strings is empty
    if v1len == 0 {
        return v2len;
    }
    if v2len == 0 {
        return v1len;
    }

    fn min3<T: Ord>(v1: T, v2: T, v3: T) -> T {
        cmp::min(v1, cmp::min(v2, v3))
    }
    fn delta(x: char, y: char) -> usize {
        if x == y {
            0
        } else {
            1
        }
    }

    let mut column: Vec<usize> = (0..v1len + 1).collect();
    for x in 1..v2len + 1 {
        column[0] = x;
        let mut lastdiag = x - 1;
        for y in 1..v1len + 1 {
            let olddiag = column[y];
            column[y] = min3(
                column[y] + 1,
                column[y - 1] + 1,
                lastdiag + delta(v1[y - 1], v2[x - 1]),
            );
            lastdiag = olddiag;
        }
    }
    column[v1len]
}