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 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 let mut low = 0;
112 let mut high = self.files.len() - 1;
113
114 let mut closest_diff = i64::MAX;
116 let mut closest_file = None;
117
118 while low <= high {
119 let mid = (low + high) / 2;
120 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 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 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 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 let rgb_iiq_files = filesystem::find_files(rgb_dir, "iiq")?;
284 let nir_iiq_files = filesystem::find_files(nir_dir, "iiq")?;
285
286 let mut rgb_collection = IIQCollection::new(&rgb_iiq_files)?;
288 let mut nir_collection = IIQCollection::new(&nir_iiq_files)?;
289
290 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 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 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 filesystem::move_files(matched_rgb.paths(), rgb_dir, verbose)?;
329 filesystem::move_files(matched_nir.paths(), nir_dir, verbose)?;
330
331 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 let rgb_iiq_files = filesystem::find_files(rgb_dir, "iiq")?;
379 let nir_iiq_files = filesystem::find_files(nir_dir, "iiq")?;
380
381 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 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 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 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 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 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(); 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 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 fs::write(rgb_dir.join("210101_120000000.iiq"), "content").unwrap();
603 fs::write(nir_dir.join("210101_120000100.iiq"), "content").unwrap();
604 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 assert!(rgb_dir.join("210101_120000000.iiq").exists());
620 assert!(nir_dir.join("210101_120000100.iiq").exists());
621
622 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 fs::write(rgb_dir.join("210101_120000000.iiq"), "content").unwrap();
645 fs::write(nir_dir.join("210101_120000100.iiq"), "content").unwrap();
646 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 assert!(rgb_dir.join("210101_120000000.iiq").exists());
661 assert!(nir_dir.join("210101_120000100.iiq").exists());
662
663 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 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 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 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 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 assert!(rgb_dir.join("210101_120000000.iiq").exists());
744 assert!(nir_dir.join("210101_120000100.iiq").exists());
745
746 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 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 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 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 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}