ix_match/
lib.rs

1use std::collections::HashMap;
2use std::fs;
3use std::hash::Hash;
4use std::path::{Path, PathBuf};
5use std::time::Duration;
6
7use anyhow::{anyhow, Context, Result};
8use chrono::prelude::*;
9use chrono::TimeDelta;
10
11mod filesystem;
12pub use filesystem::find_dir_by_pattern;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
15struct IIQFile {
16    path: PathBuf,
17    name: String,
18    stem: String,
19    datetime: NaiveDateTime,
20    bytes: u64,
21}
22
23impl IIQFile {
24    pub fn new(path: &PathBuf) -> Result<Self> {
25        let name = path
26            .file_name()
27            .context("Failed to get file name")?
28            .to_str()
29            .context("Failed to convert file name to string")?;
30        let stem = path
31            .file_stem()
32            .context("Failed to get file stem")?
33            .to_str()
34            .context("Failed to convert file stem to string")?;
35        let datetime = NaiveDateTime::parse_from_str(&stem[..16], "%y%m%d_%H%M%S%3f")
36            .context("Failed to parse datetime from stem")?;
37        let bytes = path
38            .metadata()
39            .context("Failed to get file metadata")?
40            .len();
41        Ok(IIQFile {
42            path: path.to_owned(),
43            name: name.to_owned(),
44            stem: stem.to_owned(),
45            datetime,
46            bytes,
47        })
48    }
49
50    fn diff(&self, other: &NaiveDateTime) -> TimeDelta {
51        self.datetime.signed_duration_since(*other)
52    }
53
54    fn abs_diff(&self, other: &NaiveDateTime) -> Duration {
55        Duration::from_millis(self.diff(other).num_milliseconds().unsigned_abs())
56    }
57
58    fn original_parent_dir_name(&self) -> String {
59        self.stem[..11].to_string()
60    }
61}
62
63#[derive(Debug, Clone)]
64struct IIQCollection {
65    files: Vec<IIQFile>,
66}
67
68impl IIQCollection {
69    pub fn new(paths: &[PathBuf]) -> Result<Self> {
70        let mut files = paths
71            .iter()
72            .map(IIQFile::new)
73            .collect::<Result<Vec<IIQFile>>>()
74            .context("Could not parse all files")?;
75        // Sort files by datetime
76        files.sort_by_key(|f| f.datetime);
77        Ok(IIQCollection { files })
78    }
79
80    fn paths(&self) -> Vec<PathBuf> {
81        self.files.iter().map(|f| f.path.clone()).collect()
82    }
83
84    fn len(&self) -> usize {
85        self.files.len()
86    }
87
88    fn empty_files_len(&self) -> usize {
89        self.files.iter().filter(|f| f.bytes == 0).count()
90    }
91
92    fn pop_empty_files(&mut self) -> IIQCollection {
93        let (empty_files, non_empty_files): (Vec<IIQFile>, Vec<IIQFile>) =
94            self.files.drain(..).partition(|f| f.bytes == 0);
95
96        self.files = non_empty_files;
97
98        IIQCollection { files: empty_files }
99    }
100
101    fn iter(&self) -> std::slice::Iter<'_, IIQFile> {
102        self.files.iter()
103    }
104
105    fn get_closest_file_by_datetime(&self, target_datetime: &NaiveDateTime) -> Result<&IIQFile> {
106        if self.files.is_empty() {
107            return Err(anyhow!("No files in collection"));
108        }
109
110        // Do binary search for the closest file
111        let mut low = 0;
112        let mut high = self.files.len() - 1;
113
114        // Initialize closest diff
115        let mut closest_diff = i64::MAX;
116        let mut closest_file = None;
117
118        while low <= high {
119            let mid = (low + high) / 2;
120            // Find diff in millis
121            let diff = self.files[mid]
122                .diff(target_datetime)
123                .num_milliseconds()
124                .abs();
125            if diff == 0 {
126                return Ok(&self.files[mid]);
127            }
128
129            if diff < closest_diff
130                || (diff == closest_diff && self.files[mid].datetime < *target_datetime)
131            {
132                closest_diff = diff;
133                closest_file = Some(&self.files[mid]);
134            }
135
136            if self.files[mid].datetime < *target_datetime {
137                low = mid + 1;
138            } else if mid > 0 {
139                high = mid - 1;
140            } else {
141                break;
142            }
143        }
144
145        if let Some(closest_file) = closest_file {
146            Ok(closest_file)
147        } else {
148            Err(anyhow!("Failed to get closest file by datetime"))
149        }
150    }
151}
152
153impl From<Vec<IIQFile>> for IIQCollection {
154    fn from(files: Vec<IIQFile>) -> Self {
155        IIQCollection { files }
156    }
157}
158
159#[derive(Debug)]
160struct JoinedIIQCollection<'a> {
161    joined: Vec<(Option<&'a IIQFile>, Option<&'a IIQFile>, Duration)>,
162}
163
164impl<'a> JoinedIIQCollection<'a> {
165    pub fn new(rgb: &'a IIQCollection, nir: &'a IIQCollection) -> Result<Self> {
166        let rgb_shorter = rgb.len() < nir.len();
167        let key_collection = if rgb_shorter { rgb } else { nir };
168        let other_collection = if rgb_shorter { nir } else { rgb };
169
170        let mut join_hash = other_collection
171            .files
172            .iter()
173            .map(|f| (f, (None, Duration::MAX)))
174            .collect::<HashMap<_, _>>();
175
176        // Match 1:1 the files.
177        for iiq in key_collection.files.iter() {
178            let closest_other_file =
179                other_collection.get_closest_file_by_datetime(&iiq.datetime)?;
180            let dt = iiq.abs_diff(&closest_other_file.datetime);
181
182            let v = join_hash.get_mut(&closest_other_file);
183            let (existing_match, existing_dt) = v.unwrap();
184            if dt < *existing_dt {
185                *existing_match = Some(iiq);
186                *existing_dt = dt;
187            }
188        }
189
190        // Turn the hashmap into a vector
191        let mut joined: Vec<(Option<&IIQFile>, Option<&IIQFile>, Duration)> = join_hash
192            .into_iter()
193            .map(|(k, (v, dt))| (Some(k), v, dt))
194            .collect();
195
196        if rgb_shorter {
197            // Reverse tuples, so that order is (rgb, nir)
198            joined = joined
199                .into_iter()
200                .map(|(nir, rgb, dt)| (rgb, nir, dt))
201                .collect();
202        }
203
204        Ok(JoinedIIQCollection { joined })
205    }
206
207    fn get_matched(&self, max_dt: &Duration) -> Vec<(&IIQFile, &IIQFile)> {
208        self.joined
209            .iter()
210            .filter(|(rgb, nir, dt)| rgb.is_some() && nir.is_some() && dt <= max_dt)
211            .map(|(rgb, nir, _)| (rgb.unwrap(), nir.unwrap()))
212            .collect()
213    }
214
215    fn get_matched_rgb(&self, max_dt: &Duration) -> IIQCollection {
216        self.get_matched(max_dt)
217            .iter()
218            .map(|(rgb, _)| (*rgb).clone())
219            .collect::<Vec<IIQFile>>()
220            .into()
221    }
222
223    fn get_matched_nir(&self, max_dt: &Duration) -> IIQCollection {
224        self.get_matched(max_dt)
225            .iter()
226            .map(|(_, nir)| (*nir).clone())
227            .collect::<Vec<IIQFile>>()
228            .into()
229    }
230
231    fn get_unmatched(&self, max_dt: &Duration) -> Vec<(Option<&IIQFile>, Option<&IIQFile>)> {
232        self.joined
233            .iter()
234            .filter(|(rgb, nir, dt)| (rgb.is_none() || nir.is_none()) || dt > max_dt)
235            .map(|(rgb, nir, _)| (*rgb, *nir))
236            .collect()
237    }
238
239    fn get_unmatched_rgb(&self, max_dt: &Duration) -> IIQCollection {
240        self.get_unmatched(max_dt)
241            .iter()
242            .filter(|(rgb, _)| rgb.is_some())
243            .map(|(rgb, _)| (*rgb).unwrap().clone())
244            .collect::<Vec<IIQFile>>()
245            .into()
246    }
247
248    fn get_unmatched_nir(&self, max_dt: &Duration) -> IIQCollection {
249        self.get_unmatched(max_dt)
250            .iter()
251            .filter(|(_, nir)| nir.is_some())
252            .map(|(_, nir)| (*nir).unwrap().clone())
253            .collect::<Vec<IIQFile>>()
254            .into()
255    }
256}
257
258fn check_rgb_nir_dirs_exist(rgb_dir: &Path, nir_dir: &Path) -> Result<()> {
259    let rgb_exists = rgb_dir.exists();
260    let nir_exists = nir_dir.exists();
261    if !rgb_exists && !nir_exists {
262        Err(anyhow!("RGB and NIR directories do not exist"))
263    } else if !rgb_exists {
264        Err(anyhow!("RGB directory does not exist"))
265    } else if !nir_exists {
266        Err(anyhow!("NIR directory does not exist"))
267    } else {
268        Ok(())
269    }
270}
271
272pub fn process_images(
273    rgb_dir: &Path,
274    nir_dir: &Path,
275    match_threshold: Duration,
276    keep_empty_files: bool,
277    dry_run: bool,
278    verbose: bool,
279) -> Result<(usize, usize, usize, usize, usize)> {
280    check_rgb_nir_dirs_exist(rgb_dir, nir_dir)?;
281
282    // Find IIQ files
283    let rgb_iiq_files = filesystem::find_files(rgb_dir, "iiq")?;
284    let nir_iiq_files = filesystem::find_files(nir_dir, "iiq")?;
285
286    // Create collections
287    let mut rgb_collection = IIQCollection::new(&rgb_iiq_files)?;
288    let mut nir_collection = IIQCollection::new(&nir_iiq_files)?;
289
290    // Get 0 byte file counts
291    let empty_rgb_files_len = rgb_collection.empty_files_len();
292    let empty_nir_files_len = nir_collection.empty_files_len();
293
294    if !keep_empty_files && !dry_run {
295        // Move empty files
296        let empty_rgb_files = rgb_collection.pop_empty_files();
297        let empty_nir_files = nir_collection.pop_empty_files();
298
299        if empty_rgb_files.len() > 0 {
300            let empty_rgb_dir = rgb_dir.join("empty");
301            if verbose {
302                println!("Moving empty RGB files to {:?}", empty_rgb_dir);
303            }
304            fs::create_dir_all(&empty_rgb_dir)?;
305            filesystem::move_files(empty_rgb_files.paths(), &empty_rgb_dir, verbose)?;
306        }
307
308        if empty_nir_files.len() > 0 {
309            let empty_nir_dir = nir_dir.join("empty");
310            if verbose {
311                println!("Moving empty NIR files to {:?}", empty_nir_dir);
312            }
313            fs::create_dir_all(&empty_nir_dir)?;
314            filesystem::move_files(empty_nir_files.paths(), &empty_nir_dir, verbose)?;
315        }
316    }
317
318    // Do the join
319    let joined = JoinedIIQCollection::new(&rgb_collection, &nir_collection)?;
320
321    let matched_rgb = joined.get_matched_rgb(&match_threshold);
322    let matched_nir = joined.get_matched_nir(&match_threshold);
323    let unmatched_rgb = joined.get_unmatched_rgb(&match_threshold);
324    let unmatched_nir = joined.get_unmatched_nir(&match_threshold);
325
326    if !dry_run {
327        // Move all matched iiq files to camera dirs root
328        filesystem::move_files(matched_rgb.paths(), rgb_dir, verbose)?;
329        filesystem::move_files(matched_nir.paths(), nir_dir, verbose)?;
330
331        // Move unmatched files
332        if unmatched_rgb.len() > 0 {
333            let unmatched_rgb_dir = rgb_dir.join("unmatched");
334            if verbose {
335                println!("Moving unmatched RGB files to {:?}", unmatched_rgb_dir);
336            }
337            fs::create_dir_all(&unmatched_rgb_dir)?;
338            filesystem::move_files(unmatched_rgb.paths(), &unmatched_rgb_dir, verbose)?;
339        }
340        if unmatched_nir.len() > 0 {
341            let unmatched_nir_dir = nir_dir.join("unmatched");
342            if verbose {
343                println!("Moving unmatched NIR files to {:?}", unmatched_nir_dir);
344            }
345            fs::create_dir_all(&unmatched_nir_dir)?;
346            filesystem::move_files(unmatched_nir.paths(), &unmatched_nir_dir, verbose)?;
347        }
348    }
349
350    Ok((
351        rgb_iiq_files.len(),
352        nir_iiq_files.len(),
353        matched_rgb.len(),
354        empty_rgb_files_len,
355        empty_nir_files_len,
356    ))
357}
358
359fn remove_dir_if_empty(dir: &Path) -> Result<()> {
360    if dir.exists() {
361        let is_empty = dir.read_dir()?.next().is_none();
362        if is_empty {
363            fs::remove_dir(dir)?;
364        }
365    }
366    Ok(())
367}
368
369pub fn revert_changes(
370    rgb_dir: &Path,
371    nir_dir: &Path,
372    dry_run: bool,
373    verbose: bool,
374) -> Result<(usize, usize)> {
375    check_rgb_nir_dirs_exist(rgb_dir, nir_dir)?;
376
377    // Find IIQ files
378    let rgb_iiq_files = filesystem::find_files(rgb_dir, "iiq")?;
379    let nir_iiq_files = filesystem::find_files(nir_dir, "iiq")?;
380
381    // Create collections
382    let rgb_collection = IIQCollection::new(&rgb_iiq_files)?;
383    let nir_collection = IIQCollection::new(&nir_iiq_files)?;
384
385    if !dry_run {
386        for file in rgb_collection.iter() {
387            let dest = &rgb_dir.join(file.original_parent_dir_name());
388            if dest.exists() {
389                filesystem::move_files(vec![file.path.clone()], dest, verbose)?;
390            } else {
391                eprintln!("Parent directory does not exist for file {}", file.name);
392            }
393        }
394        remove_dir_if_empty(&rgb_dir.join("empty"))?;
395        remove_dir_if_empty(&rgb_dir.join("unmatched"))?;
396
397        for file in nir_collection.iter() {
398            let dest = &nir_dir.join(file.original_parent_dir_name());
399            if dest.exists() {
400                filesystem::move_files(vec![file.path.clone()], dest, verbose)?;
401            } else {
402                eprintln!("Parent directory does not exist for file {}", file.name);
403            }
404        }
405        remove_dir_if_empty(&nir_dir.join("empty"))?;
406        remove_dir_if_empty(&nir_dir.join("unmatched"))?;
407    }
408
409    Ok((rgb_iiq_files.len(), nir_iiq_files.len()))
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415    use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
416    use tempfile::TempDir;
417
418    use std::fs;
419
420    #[test]
421    fn test_iiq_file_new() {
422        let temp_dir = TempDir::new().unwrap();
423        let path = temp_dir.path().join("210101_120000000.iiq");
424        fs::write(&path, "content").unwrap();
425
426        let file = IIQFile::new(&path).unwrap();
427        assert_eq!(file.stem, "210101_120000000");
428        let date = NaiveDate::from_ymd_opt(2021, 1, 1).unwrap();
429        let time = NaiveTime::from_hms_opt(12, 0, 0).unwrap();
430        let dt = NaiveDateTime::new(date, time);
431        assert_eq!(file.datetime, dt);
432        assert_eq!(file.bytes, 7);
433        assert_eq!(file.path, path);
434        assert_eq!(file.name, "210101_120000000.iiq");
435    }
436
437    #[test]
438    fn test_make_iiq_collection() {
439        let temp_dir = TempDir::new().unwrap();
440        let base_path = temp_dir.path();
441
442        let files = vec![
443            base_path.join("210101_120000000.iiq"),
444            base_path.join("210101_120001000.iiq"),
445        ];
446
447        files.iter().for_each(|file| {
448            fs::write(file, "content").unwrap();
449        });
450
451        let collection = IIQCollection::new(&files).unwrap();
452        assert_eq!(collection.len(), 2);
453        assert_eq!(collection.paths(), files);
454    }
455
456    #[test]
457    fn test_join_collections() {
458        let temp_dir_rgb = TempDir::new().unwrap();
459        let rgb_files = vec![
460            temp_dir_rgb.path().join("210101_120000000.iiq"),
461            temp_dir_rgb.path().join("210101_120001000.iiq"),
462        ];
463        for file in &rgb_files {
464            fs::write(file, "content").unwrap();
465        }
466        let rgb_collection = IIQCollection::new(&rgb_files).unwrap();
467
468        let temp_dir_nir = TempDir::new().unwrap();
469        let nir_files = vec![
470            temp_dir_nir.path().join("210101_120000100.iiq"),
471            temp_dir_nir.path().join("210101_120001100.iiq"),
472        ];
473        for file in &nir_files {
474            fs::write(file, "content").unwrap();
475        }
476        let nir_collection = IIQCollection::new(&nir_files).unwrap();
477
478        let result = JoinedIIQCollection::new(&rgb_collection, &nir_collection).unwrap();
479
480        assert_eq!(result.joined.len(), 2);
481        let mut joined = result.joined;
482        joined.sort();
483        assert_eq!(
484            joined,
485            vec![
486                (
487                    Some(&rgb_collection.files[0]),
488                    Some(&nir_collection.files[0]),
489                    Duration::from_millis(100)
490                ),
491                (
492                    Some(&rgb_collection.files[1]),
493                    Some(&nir_collection.files[1]),
494                    Duration::from_millis(100)
495                ),
496            ]
497        );
498    }
499
500    #[test]
501    fn test_collection() {
502        let temp_dir = TempDir::new().unwrap();
503        let rgb_dir = temp_dir.path().join("rgb");
504        let nir_dir = temp_dir.path().join("nir");
505        fs::create_dir_all(&rgb_dir).unwrap();
506        fs::create_dir_all(&nir_dir).unwrap();
507
508        // Create test files
509        fs::write(rgb_dir.join("210101_120000000.iiq"), "content").unwrap();
510        fs::write(nir_dir.join("210101_120000100.iiq"), "content").unwrap();
511        fs::write(rgb_dir.join("210101_120001000.iiq"), "content").unwrap();
512        fs::write(nir_dir.join("210101_120001100.iiq"), "content").unwrap();
513
514        let rgb_files = filesystem::find_files(&rgb_dir, "iiq").unwrap();
515        let nir_files = filesystem::find_files(&nir_dir, "iiq").unwrap();
516
517        let rgb_collection = IIQCollection::new(&rgb_files).unwrap();
518        let nir_collection = IIQCollection::new(&nir_files).unwrap();
519
520        assert_eq!(rgb_collection.len(), 2);
521        assert_eq!(nir_collection.len(), 2);
522    }
523
524    #[test]
525    fn test_process_images() {
526        let temp_dir = TempDir::new().unwrap();
527        let rgb_dir = temp_dir.path().join("rgb");
528        let nir_dir = temp_dir.path().join("nir");
529        fs::create_dir_all(&rgb_dir).unwrap();
530        fs::create_dir_all(&nir_dir).unwrap();
531
532        // Create test files
533        fs::write(rgb_dir.join("210101_120000000.iiq"), "content").unwrap();
534        fs::write(nir_dir.join("210101_120000100.iiq"), "content").unwrap();
535        fs::write(rgb_dir.join("210101_120001000.iiq"), "content").unwrap();
536        fs::write(nir_dir.join("210101_120001100.iiq"), "content").unwrap();
537
538        let threshold = Duration::from_millis(200);
539        let (rgb_count, nir_count, matched_count, empty_rgb_count, empty_nir_count) =
540            process_images(&rgb_dir, &nir_dir, threshold, false, false, false).unwrap();
541
542        assert_eq!(rgb_count, 2);
543        assert_eq!(nir_count, 2);
544        assert_eq!(matched_count, 2);
545        assert_eq!(empty_rgb_count, 0);
546        assert_eq!(empty_nir_count, 0);
547
548        // Check if files are in their original locations
549        // (process_images doesn't move matched files in this case)
550        assert!(rgb_dir.join("210101_120000000.iiq").exists());
551        assert!(rgb_dir.join("210101_120001000.iiq").exists());
552        assert!(nir_dir.join("210101_120000100.iiq").exists());
553        assert!(nir_dir.join("210101_120001100.iiq").exists());
554
555        // Unmatched directories should not be created in this case
556        assert!(!rgb_dir.join("unmatched").exists());
557        assert!(!nir_dir.join("unmatched").exists());
558    }
559
560    #[test]
561    fn test_process_images_dry_run() {
562        let temp_dir = TempDir::new().unwrap();
563        let rgb_dir = temp_dir.path().join("rgb");
564        let nir_dir = temp_dir.path().join("nir");
565        fs::create_dir_all(&rgb_dir).unwrap();
566        fs::create_dir_all(&nir_dir).unwrap();
567
568        // Create test files
569        fs::write(rgb_dir.join("210101_120000000.iiq"), "content").unwrap();
570        fs::write(rgb_dir.join("210101_120001000.iiq"), "content").unwrap();
571        fs::write(nir_dir.join("210101_120000100.iiq"), "content").unwrap();
572        fs::write(nir_dir.join("210101_120005000.iiq"), "content").unwrap(); // This one won't match
573
574        let threshold = Duration::from_millis(200);
575        let (rgb_count, nir_count, matched_count, empty_rgb_count, empty_nir_count) =
576            process_images(&rgb_dir, &nir_dir, threshold, true, true, false).unwrap();
577
578        assert_eq!(rgb_count, 2);
579        assert_eq!(nir_count, 2);
580        assert_eq!(matched_count, 1);
581        assert_eq!(empty_rgb_count, 0);
582        assert_eq!(empty_nir_count, 0);
583
584        // Check if all files are in their original locations (dry run)
585        assert!(rgb_dir.join("210101_120000000.iiq").exists());
586        assert!(rgb_dir.join("210101_120001000.iiq").exists());
587        assert!(nir_dir.join("210101_120000100.iiq").exists());
588        assert!(nir_dir.join("210101_120005000.iiq").exists());
589        assert!(!rgb_dir.join("unmatched").exists());
590        assert!(!nir_dir.join("unmatched").exists());
591    }
592
593    #[test]
594    fn test_process_images_with_unmatched() {
595        let temp_dir = TempDir::new().unwrap();
596        let rgb_dir = temp_dir.path().join("rgb");
597        let nir_dir = temp_dir.path().join("nir");
598        fs::create_dir_all(&rgb_dir).unwrap();
599        fs::create_dir_all(&nir_dir).unwrap();
600
601        // Create test files
602        fs::write(rgb_dir.join("210101_120000000.iiq"), "content").unwrap();
603        fs::write(nir_dir.join("210101_120000100.iiq"), "content").unwrap();
604        // These won't match
605        fs::write(rgb_dir.join("210101_120001000.iiq"), "content").unwrap();
606        fs::write(nir_dir.join("210101_120005000.iiq"), "content").unwrap();
607
608        let threshold = Duration::from_millis(200);
609        let (rgb_count, nir_count, matched_count, empty_rgb_count, empty_nir_count) =
610            process_images(&rgb_dir, &nir_dir, threshold, true, false, false).unwrap();
611
612        assert_eq!(rgb_count, 2);
613        assert_eq!(nir_count, 2);
614        assert_eq!(matched_count, 1);
615        assert_eq!(empty_rgb_count, 0);
616        assert_eq!(empty_nir_count, 0);
617
618        // Check if matched files are in their original locations
619        assert!(rgb_dir.join("210101_120000000.iiq").exists());
620        assert!(nir_dir.join("210101_120000100.iiq").exists());
621
622        // Check if unmatched files are moved to the unmatched directory
623        assert!(rgb_dir
624            .join("unmatched")
625            .join("210101_120001000.iiq")
626            .exists());
627        assert!(!rgb_dir.join("210101_120001000.iiq").exists());
628        assert!(nir_dir
629            .join("unmatched")
630            .join("210101_120005000.iiq")
631            .exists());
632        assert!(!nir_dir.join("210101_120005000.iiq").exists());
633    }
634
635    #[test]
636    fn test_process_images_with_uneven_numbers() {
637        let temp_dir = TempDir::new().unwrap();
638        let rgb_dir = temp_dir.path().join("rgb");
639        let nir_dir = temp_dir.path().join("nir");
640        fs::create_dir_all(&rgb_dir).unwrap();
641        fs::create_dir_all(&nir_dir).unwrap();
642
643        // Create test files
644        fs::write(rgb_dir.join("210101_120000000.iiq"), "content").unwrap();
645        fs::write(nir_dir.join("210101_120000100.iiq"), "content").unwrap();
646        // These won't match
647        fs::write(nir_dir.join("210101_120005000.iiq"), "content").unwrap();
648
649        let threshold = Duration::from_millis(200);
650        let (rgb_count, nir_count, matched_count, empty_rgb_count, empty_nir_count) =
651            process_images(&rgb_dir, &nir_dir, threshold, true, false, false).unwrap();
652
653        assert_eq!(rgb_count, 1);
654        assert_eq!(nir_count, 2);
655        assert_eq!(matched_count, 1);
656        assert_eq!(empty_rgb_count, 0);
657        assert_eq!(empty_nir_count, 0);
658
659        // Check if matched files are in their original locations
660        assert!(rgb_dir.join("210101_120000000.iiq").exists());
661        assert!(nir_dir.join("210101_120000100.iiq").exists());
662
663        // Check if unmatched files are moved to the unmatched directory
664        assert!(!rgb_dir.join("unmatched").exists());
665        assert!(nir_dir
666            .join("unmatched")
667            .join("210101_120005000.iiq")
668            .exists());
669        assert!(!nir_dir.join("210101_120005000.iiq").exists());
670    }
671
672    #[test]
673    fn test_process_images_with_no_dirs() {
674        let temp_dir = TempDir::new().unwrap();
675        let rgb_dir = temp_dir.path().join("rgb");
676        let nir_dir = temp_dir.path().join("nir");
677
678        let threshold = Duration::from_millis(200);
679        let result = process_images(&rgb_dir, &nir_dir, threshold, true, false, false);
680        assert!(result.is_err());
681    }
682
683    #[test]
684    fn test_process_images_with_keep_empty() {
685        let temp_dir = TempDir::new().unwrap();
686        let rgb_dir = temp_dir.path().join("rgb");
687        let nir_dir = temp_dir.path().join("nir");
688        fs::create_dir_all(&rgb_dir).unwrap();
689        fs::create_dir_all(&nir_dir).unwrap();
690
691        // Create test files
692        fs::write(rgb_dir.join("210101_120000000.iiq"), "content").unwrap();
693        fs::write(rgb_dir.join("210101_130000000.iiq"), "").unwrap();
694        fs::write(nir_dir.join("210101_120000100.iiq"), "content").unwrap();
695        fs::write(nir_dir.join("210101_130000100.iiq"), "").unwrap();
696
697        let threshold = Duration::from_millis(200);
698        let (rgb_count, nir_count, matched_count, empty_rgb_count, empty_nir_count) =
699            process_images(&rgb_dir, &nir_dir, threshold, true, false, false).unwrap();
700
701        assert_eq!(rgb_count, 2);
702        assert_eq!(nir_count, 2);
703        assert_eq!(matched_count, 2);
704        assert_eq!(empty_rgb_count, 1);
705        assert_eq!(empty_nir_count, 1);
706
707        // Check if matched files are in their original locations
708        assert!(rgb_dir.join("210101_120000000.iiq").exists());
709        assert!(nir_dir.join("210101_120000100.iiq").exists());
710        assert!(rgb_dir.join("210101_130000000.iiq").exists());
711        assert!(nir_dir.join("210101_130000100.iiq").exists());
712
713        // Check that no empty directories were created
714        assert!(!rgb_dir.join("empty").exists());
715        assert!(!nir_dir.join("empty").exists());
716    }
717
718    #[test]
719    fn test_process_images_with_no_keep_empty() {
720        let temp_dir = TempDir::new().unwrap();
721        let rgb_dir = temp_dir.path().join("rgb");
722        let nir_dir = temp_dir.path().join("nir");
723        fs::create_dir_all(&rgb_dir).unwrap();
724        fs::create_dir_all(&nir_dir).unwrap();
725
726        // Create test files
727        fs::write(rgb_dir.join("210101_120000000.iiq"), "content").unwrap();
728        fs::write(rgb_dir.join("210101_130000000.iiq"), "").unwrap();
729        fs::write(nir_dir.join("210101_120000100.iiq"), "content").unwrap();
730        fs::write(nir_dir.join("210101_130000100.iiq"), "").unwrap();
731
732        let threshold = Duration::from_millis(200);
733        let (rgb_count, nir_count, matched_count, empty_rgb_count, empty_nir_count) =
734            process_images(&rgb_dir, &nir_dir, threshold, false, false, false).unwrap();
735
736        assert_eq!(rgb_count, 2);
737        assert_eq!(nir_count, 2);
738        assert_eq!(matched_count, 1);
739        assert_eq!(empty_rgb_count, 1);
740        assert_eq!(empty_nir_count, 1);
741
742        // Check if matched files are in their original locations
743        assert!(rgb_dir.join("210101_120000000.iiq").exists());
744        assert!(nir_dir.join("210101_120000100.iiq").exists());
745
746        // Check if empty files are moved to the empty directory
747        assert!(rgb_dir.join("empty").join("210101_130000000.iiq").exists());
748        assert!(!rgb_dir.join("210101_130000000.iiq").exists());
749        assert!(nir_dir.join("empty").join("210101_130000100.iiq").exists());
750        assert!(!nir_dir.join("210101_130000100.iiq").exists());
751    }
752
753    #[test]
754    fn test_get_closest_file_by_datetime() {
755        let temp_dir = TempDir::new().unwrap();
756        let base_path = temp_dir.path();
757
758        let files = vec![
759            base_path.join("210101_120000000.iiq"),
760            base_path.join("210101_120001000.iiq"),
761            base_path.join("210101_120002000.iiq"),
762        ];
763
764        files.iter().for_each(|file| {
765            fs::write(file, "content").unwrap();
766        });
767
768        let collection = IIQCollection::new(&files).unwrap();
769
770        let target_datetime =
771            NaiveDateTime::parse_from_str("210101_120000500", "%y%m%d_%H%M%S%3f").unwrap();
772        let closest_file = collection
773            .get_closest_file_by_datetime(&target_datetime)
774            .unwrap();
775        assert_eq!(closest_file.path, files[0]);
776
777        let target_datetime =
778            NaiveDateTime::parse_from_str("210101_120001500", "%y%m%d_%H%M%S%3f").unwrap();
779        let closest_file = collection
780            .get_closest_file_by_datetime(&target_datetime)
781            .unwrap();
782        assert_eq!(closest_file.path, files[1]);
783
784        let target_datetime =
785            NaiveDateTime::parse_from_str("210101_120002500", "%y%m%d_%H%M%S%3f").unwrap();
786        let closest_file = collection
787            .get_closest_file_by_datetime(&target_datetime)
788            .unwrap();
789        assert_eq!(closest_file.path, files[2]);
790    }
791
792    #[test]
793    fn test_get_closest_file_by_datetime_empty_collection() {
794        let collection = IIQCollection { files: vec![] };
795        let target_datetime =
796            NaiveDateTime::parse_from_str("210101_120000500", "%y%m%d_%H%M%S%3f").unwrap();
797        let result = collection.get_closest_file_by_datetime(&target_datetime);
798        assert!(result.is_err());
799    }
800
801    #[test]
802    fn test_revert_changes() {
803        let temp_dir = TempDir::new().unwrap();
804        let rgb_dir = temp_dir.path().join("rgb");
805        let nir_dir = temp_dir.path().join("nir");
806        let rgb_empty_dir = rgb_dir.join("empty");
807        let nir_unmatched_dir = nir_dir.join("unmatched");
808        fs::create_dir_all(&rgb_dir).unwrap();
809        fs::create_dir_all(&nir_dir).unwrap();
810        fs::create_dir_all(&rgb_empty_dir).unwrap();
811        fs::create_dir_all(&nir_unmatched_dir).unwrap();
812
813        // Create test files
814        fs::write(rgb_dir.join("210101_120100000.iiq"), "content").unwrap();
815        fs::write(nir_dir.join("210101_120100100.iiq"), "content").unwrap();
816
817        fs::write(rgb_dir.join("210101_130000040.iiq"), "content").unwrap();
818        fs::write(nir_dir.join("210101_130000040.iiq"), "content").unwrap();
819
820        fs::write(rgb_empty_dir.join("210101_140000000.iiq"), "").unwrap();
821        fs::write(nir_unmatched_dir.join("210101_140000100.iiq"), "").unwrap();
822
823        // Create original directories
824        fs::create_dir_all(&rgb_dir.join("210101_1201")).unwrap();
825        fs::create_dir_all(&nir_dir.join("210101_1201")).unwrap();
826        fs::create_dir_all(&rgb_dir.join("210101_1300")).unwrap();
827        fs::create_dir_all(&nir_dir.join("210101_1300")).unwrap();
828        fs::create_dir_all(&rgb_dir.join("210101_1400")).unwrap();
829        fs::create_dir_all(&nir_dir.join("210101_1400")).unwrap();
830
831        let (rgb_count, nir_count) = revert_changes(&rgb_dir, &nir_dir, false, false).unwrap();
832
833        assert_eq!(rgb_count, 3);
834        assert_eq!(nir_count, 3);
835
836        assert!(rgb_dir.join("210101_1201/210101_120100000.iiq").exists());
837        assert!(nir_dir.join("210101_1201/210101_120100100.iiq").exists());
838        assert!(rgb_dir.join("210101_1300/210101_130000040.iiq").exists());
839        assert!(nir_dir.join("210101_1300/210101_130000040.iiq").exists());
840        assert!(rgb_dir.join("210101_1400/210101_140000000.iiq").exists());
841        assert!(nir_dir.join("210101_1400/210101_140000100.iiq").exists());
842    }
843
844    #[test]
845    fn test_revert_changes_with_empty_dir() {
846        let temp_dir = TempDir::new().unwrap();
847        let rgb_dir = temp_dir.path().join("rgb");
848        let nir_dir = temp_dir.path().join("nir");
849        let rgb_empty_dir = rgb_dir.join("empty");
850        let nir_unmatched_dir = nir_dir.join("unmatched");
851        fs::create_dir_all(&rgb_dir).unwrap();
852        fs::create_dir_all(&nir_dir).unwrap();
853        fs::create_dir_all(&rgb_empty_dir).unwrap();
854        fs::create_dir_all(&nir_unmatched_dir).unwrap();
855
856        // Create test files
857        fs::write(rgb_dir.join("210101_120100000.iiq"), "content").unwrap();
858        fs::write(nir_dir.join("210101_120100100.iiq"), "content").unwrap();
859
860        fs::write(rgb_dir.join("210101_130000040.iiq"), "content").unwrap();
861        fs::write(nir_dir.join("210101_130000040.iiq"), "content").unwrap();
862
863        fs::write(rgb_empty_dir.join("210101_140000000.iiq"), "").unwrap();
864        fs::write(nir_unmatched_dir.join("210101_140000100.iiq"), "").unwrap();
865
866        // Create original directories
867        // --Removed fs::create_dir_all(&rgb_dir.join("210101_1201")).unwrap();
868        fs::create_dir_all(&nir_dir.join("210101_1201")).unwrap();
869        fs::create_dir_all(&rgb_dir.join("210101_1300")).unwrap();
870        fs::create_dir_all(&nir_dir.join("210101_1300")).unwrap();
871        fs::create_dir_all(&rgb_dir.join("210101_1400")).unwrap();
872        fs::create_dir_all(&nir_dir.join("210101_1400")).unwrap();
873
874        let (rgb_count, nir_count) = revert_changes(&rgb_dir, &nir_dir, false, false).unwrap();
875
876        assert_eq!(rgb_count, 3);
877        assert_eq!(nir_count, 3);
878
879        assert!(rgb_dir.join("210101_120100000.iiq").exists());
880        assert!(nir_dir.join("210101_1201/210101_120100100.iiq").exists());
881        assert!(rgb_dir.join("210101_1300/210101_130000040.iiq").exists());
882        assert!(nir_dir.join("210101_1300/210101_130000040.iiq").exists());
883        assert!(rgb_dir.join("210101_1400/210101_140000000.iiq").exists());
884        assert!(nir_dir.join("210101_1400/210101_140000100.iiq").exists());
885    }
886}