Skip to main content

rvlib/control/
mod.rs

1use crate::cfg::{Connection, ExportPath, ExportPathConnection, PyHttpReaderCfg, get_log_folder};
2use crate::file_util::{
3    self, DEFAULT_HOMEDIR, DEFAULT_PRJ_NAME, DEFAULT_PRJ_PATH, PathPair, SavedCfg, osstr_to_str,
4};
5use crate::history::{History, Record};
6use crate::meta_data::{ConnectionData, MetaData, MetaDataFlags};
7use crate::result::{trace_ok_err, trace_ok_warn};
8use crate::sort_params::SortParams;
9use crate::tools::{ATTRIBUTES_NAME, BBOX_NAME, BRUSH_NAME, rotate90};
10use crate::tools_data::{ToolSpecifics, ToolsDataMap, coco_io::read_coco};
11use crate::types::{ImageMeta, ImageMetaPair, ThumbIms};
12use crate::util::version_label;
13use crate::wand_many::{
14    RestWandMany, WandMany, WandManyAnnotationsInput, WandManyData, WandManyOutput,
15};
16use crate::world::World;
17use crate::{
18    cfg::Cfg,
19    image_reader::ReaderFromCfg,
20    threadpool::ThreadPool,
21    types::AsyncResultImage,
22    wand_util::{CmdServer, WandServer},
23};
24use crate::{defer_file_removal, measure_time};
25use chrono::{DateTime, Utc};
26use detail::{create_lock_file, lock_file_path, read_user_from_lockfile};
27use image::imageops::FilterType;
28use image::{DynamicImage, ImageBuffer};
29use rvimage_domain::{RvError, RvResult, rverr, to_rv};
30use serde::{Deserialize, Serialize};
31use std::collections::HashMap;
32use std::collections::HashSet;
33use std::fmt::{Debug, Display};
34use std::io::Write;
35use std::path::{Path, PathBuf};
36use std::sync::mpsc;
37use std::thread::{self, JoinHandle};
38use std::time::Duration;
39use std::{fs, mem};
40use zip::write::ExtendedFileOptions;
41mod filter;
42pub mod paths_navigator;
43use crate::image_reader::LoadImageForGui;
44use paths_navigator::PathsNavigator;
45use walkdir::WalkDir;
46
47mod detail {
48    use std::{
49        mem,
50        path::{Path, PathBuf},
51    };
52
53    use image::{DynamicImage, GenericImage};
54    use imageproc::drawing::Canvas;
55    use serde::{Deserialize, Serialize, Serializer};
56
57    use crate::{
58        cfg::{Cfg, CfgPrj},
59        control::SavePrjData,
60        defer_file_removal,
61        file_util::{self, DEFAULT_HOMEDIR, SavedCfg, tf_to_annomap_key},
62        result::trace_ok_err,
63        tools::{ATTRIBUTES_NAME, BBOX_NAME, BRUSH_NAME, ROT90_NAME, add_tools_initial_data},
64        tools_data::{ToolsDataMap, merge},
65        toolsdata_by_name,
66        util::version_label,
67        wand_many::WandManyData,
68        world::World,
69    };
70    use rvimage_domain::ShapeI;
71    use rvimage_domain::{result::RvResult, to_rv};
72
73    use super::UserPrjOpened;
74
75    pub fn serialize_opened_folder<S>(
76        folder: &Option<String>,
77        serializer: S,
78    ) -> Result<S::Ok, S::Error>
79    where
80        S: Serializer,
81    {
82        let cfg = trace_ok_err(Cfg::read(&DEFAULT_HOMEDIR));
83        let prj_path = cfg.as_ref().map(|cfg| cfg.current_prj_path());
84        let folder = folder
85            .clone()
86            .map(|folder| tf_to_annomap_key(folder, prj_path));
87        folder.serialize(serializer)
88    }
89    pub fn deserialize_opened_folder<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
90    where
91        D: serde::Deserializer<'de>,
92    {
93        let cfg = trace_ok_err(Cfg::read(&DEFAULT_HOMEDIR));
94        let prj_path = cfg.as_ref().map(|cfg| cfg.current_prj_path());
95        let folder: Option<String> = Option::deserialize(deserializer)?;
96
97        Ok(folder.map(|p| tf_to_annomap_key(p, prj_path)))
98    }
99
100    pub(super) fn lock_file_path(prj_file_path: &Path) -> RvResult<PathBuf> {
101        let stem = file_util::osstr_to_str(prj_file_path.file_stem()).map_err(to_rv)?;
102        Ok(prj_file_path.with_file_name(format!(".{stem}_lock.json")))
103    }
104    pub(super) fn create_lock_file(prj_file_path: &Path) -> RvResult<()> {
105        let lock_file = lock_file_path(prj_file_path)?;
106        tracing::info!("creating lock file {lock_file:?}");
107        let upo = UserPrjOpened::new();
108        file_util::save(&lock_file, upo)
109    }
110    pub(super) fn remove_lock_file(prj_file_path: &Path) -> RvResult<()> {
111        let lock_file = lock_file_path(prj_file_path)?;
112        if lock_file.exists() {
113            tracing::info!("removing lock file {lock_file:?}");
114            defer_file_removal!(&lock_file);
115        }
116        Ok(())
117    }
118    pub(super) fn read_user_from_lockfile(prj_file_path: &Path) -> RvResult<Option<UserPrjOpened>> {
119        let lock_file = lock_file_path(prj_file_path)?;
120        let lock_file_content = file_util::read_to_string(lock_file).ok();
121        lock_file_content
122            .map(|lfc| serde_json::from_str(&lfc).map_err(to_rv))
123            .transpose()
124    }
125
126    pub(super) fn idx_change_check(
127        file_selected_idx: Option<usize>,
128        world_idx_pair: Option<(World, Option<usize>)>,
129    ) -> Option<(World, Option<usize>)> {
130        world_idx_pair.map(|(w, idx)| {
131            if idx != file_selected_idx {
132                (w, idx)
133            } else {
134                (w, None)
135            }
136        })
137    }
138
139    fn write<T>(
140        tools_data_map: &ToolsDataMap,
141        make_data: impl Fn(&ToolsDataMap) -> T,
142        export_path: &Path,
143    ) -> RvResult<()>
144    where
145        T: Serialize,
146    {
147        let tools_data_map = tools_data_map
148            .iter()
149            .map(|(k, v)| {
150                let mut v = v.clone();
151                v.menu_active = false;
152                (k.clone(), v)
153            })
154            .collect::<ToolsDataMap>();
155        let data = make_data(&tools_data_map);
156        file_util::save(export_path, data)
157    }
158
159    pub fn save(
160        opened_folder: Option<&str>,
161        tools_data_map: &ToolsDataMap,
162        prj_file_path: &Path,
163        cfg: &Cfg,
164        wand_many_data: &WandManyData,
165        filter_string: &str,
166    ) -> RvResult<()> {
167        // we need to write the cfg for correct prj-path mapping during serialization
168        // of annotations
169        trace_ok_err(cfg.write());
170        let make_data = |tdm: &ToolsDataMap| SavePrjData {
171            version: Some(version_label()),
172            opened_folder: opened_folder.map(|of| of.to_string()),
173            tools_data_map: tdm.clone(),
174            cfg: SavedCfg::CfgPrj(cfg.prj.clone()),
175            wand_many_data: Some(wand_many_data.clone()),
176            filter_string: Some(filter_string.to_string()),
177        };
178        tracing::info!("saved to {prj_file_path:?}");
179        create_lock_file(prj_file_path)?;
180        write(tools_data_map, make_data, prj_file_path)?;
181        Ok(())
182    }
183
184    pub(super) fn draw_loading_dots(im: &mut DynamicImage, counter: u128) {
185        let shape = ShapeI::from_im(im);
186        let radius = 7u32;
187        let centers = [
188            (shape.w - 70, shape.h - 20),
189            (shape.w - 50, shape.h - 20),
190            (shape.w - 30, shape.h - 20),
191        ];
192        let off_center_dim = |c_idx: usize, counter_mod: usize, rgb: &[u8; 3]| {
193            let mut res = *rgb;
194            for val in res.iter_mut() {
195                if counter_mod != c_idx {
196                    *val = (*val as f32 * 0.7) as u8;
197                }
198            }
199            res
200        };
201        for (c_idx, ctr) in centers.iter().enumerate() {
202            for y in ctr.1.saturating_sub(radius)..ctr.1.saturating_add(radius) {
203                for x in ctr.0.saturating_sub(radius)..ctr.0.saturating_add(radius) {
204                    let ctr0_x = x.abs_diff(ctr.0);
205                    let ctr1_y = y.abs_diff(ctr.1);
206                    let ctr0_x_sq = ctr0_x.saturating_mul(ctr0_x);
207                    let ctr1_y_sq = ctr1_y.saturating_mul(ctr1_y);
208                    if ctr0_x_sq + ctr1_y_sq < radius.pow(2) {
209                        let counter_mod = ((counter / 5) % 3) as usize;
210                        let rgb = off_center_dim(c_idx, counter_mod, &[195u8, 255u8, 205u8]);
211                        let mut pixel = im.get_pixel(x, y);
212                        pixel.0 = [rgb[0], rgb[1], rgb[2], 255];
213                        im.put_pixel(x, y, pixel);
214                    }
215                }
216            }
217        }
218    }
219    pub struct LoadedPrj {
220        pub tdm: ToolsDataMap,
221        pub opened_folder: Option<String>,
222        pub cfg_prj: CfgPrj,
223        pub wand_many_data: Option<WandManyData>,
224        pub filter_string: Option<String>,
225    }
226    pub(super) fn load(file_path: &Path) -> RvResult<LoadedPrj> {
227        let s = file_util::read_to_string(file_path)?;
228
229        let save_data = serde_json::from_str::<SavePrjData>(s.as_str()).map_err(to_rv)?;
230        let cfg_prj = match save_data.cfg {
231            SavedCfg::CfgLegacy(cfg) => cfg.to_cfg().prj,
232            SavedCfg::CfgPrj(cfg_prj) => cfg_prj,
233        };
234        Ok(LoadedPrj {
235            tdm: add_tools_initial_data(save_data.tools_data_map),
236            opened_folder: save_data.opened_folder,
237            cfg_prj,
238            wand_many_data: save_data.wand_many_data,
239            filter_string: save_data.filter_string,
240        })
241    }
242
243    #[derive(PartialEq)]
244    enum FillResult {
245        FilledCurWithLoaded,
246        LoadedEmpty,
247        BothNotEmpty,
248        BothEmpty,
249    }
250    fn fill_empty_curtdm(
251        tool: &str,
252        cur_tdm: &mut ToolsDataMap,
253        loaded_tdm: &mut ToolsDataMap,
254    ) -> FillResult {
255        if !cur_tdm.contains_key(tool) && loaded_tdm.contains_key(tool) {
256            cur_tdm.insert(tool.to_string(), loaded_tdm[tool].clone());
257            FillResult::FilledCurWithLoaded
258        } else if !loaded_tdm.contains_key(tool) {
259            FillResult::LoadedEmpty
260        } else if cur_tdm.contains_key(tool) {
261            FillResult::BothNotEmpty
262        } else {
263            FillResult::BothEmpty
264        }
265    }
266    pub fn import_annos(cur_tdm: &mut ToolsDataMap, file_path: &Path) -> RvResult<()> {
267        let mut loaded = load(file_path)?;
268
269        if fill_empty_curtdm(BBOX_NAME, cur_tdm, &mut loaded.tdm) == FillResult::BothNotEmpty {
270            let cur_bbox = toolsdata_by_name!(BBOX_NAME, bbox_mut, cur_tdm);
271            let loaded_bbox = toolsdata_by_name!(BBOX_NAME, bbox_mut, loaded.tdm);
272            let cur_annos = mem::take(&mut cur_bbox.annotations_map);
273            let cur_li = mem::take(&mut cur_bbox.label_info);
274            let loaded_annos = mem::take(&mut loaded_bbox.annotations_map);
275            let loaded_li = mem::take(&mut loaded_bbox.label_info);
276            let (merged_annos, merged_li) = merge(cur_annos, cur_li, loaded_annos, loaded_li);
277            cur_bbox.annotations_map = merged_annos;
278            cur_bbox.label_info = merged_li;
279        }
280
281        if fill_empty_curtdm(BRUSH_NAME, cur_tdm, &mut loaded.tdm) == FillResult::BothNotEmpty {
282            let cur_brush = toolsdata_by_name!(BRUSH_NAME, brush_mut, cur_tdm);
283            let loaded_brush = toolsdata_by_name!(BRUSH_NAME, brush_mut, loaded.tdm);
284            let cur_annos = mem::take(&mut cur_brush.annotations_map);
285            let cur_li = mem::take(&mut cur_brush.label_info);
286            let loaded_annos = mem::take(&mut loaded_brush.annotations_map);
287            let loaded_li = mem::take(&mut loaded_brush.label_info);
288            let (merged_annos, merged_li) = merge(cur_annos, cur_li, loaded_annos, loaded_li);
289            cur_brush.annotations_map = merged_annos;
290            cur_brush.label_info = merged_li;
291        }
292
293        if fill_empty_curtdm(ROT90_NAME, cur_tdm, &mut loaded.tdm) == FillResult::BothNotEmpty {
294            let cur_rot90 = toolsdata_by_name!(ROT90_NAME, rot90_mut, cur_tdm);
295            let loaded_rot90 = toolsdata_by_name!(ROT90_NAME, rot90_mut, loaded.tdm);
296            *cur_rot90 = mem::take(cur_rot90).merge(mem::take(loaded_rot90));
297        }
298
299        if fill_empty_curtdm(ATTRIBUTES_NAME, cur_tdm, &mut loaded.tdm) == FillResult::BothNotEmpty
300        {
301            let cur_attr = toolsdata_by_name!(ATTRIBUTES_NAME, attributes_mut, cur_tdm);
302            let loaded_attr = toolsdata_by_name!(ATTRIBUTES_NAME, attributes_mut, loaded.tdm);
303            *cur_attr = mem::take(cur_attr).merge(mem::take(loaded_attr));
304        }
305        Ok(())
306    }
307}
308const LOAD_ACTOR_NAME: &str = "Load";
309
310#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
311pub struct UserPrjOpened {
312    time: DateTime<Utc>,
313    username: String,
314    realname: String,
315}
316impl UserPrjOpened {
317    pub fn new() -> Self {
318        const UNKNOWN: &str = "Unknown";
319        UserPrjOpened {
320            time: Utc::now(),
321            username: trace_ok_err(whoami::username()).unwrap_or(UNKNOWN.into()),
322            realname: trace_ok_err(whoami::realname()).unwrap_or(UNKNOWN.into()),
323        }
324    }
325}
326impl Display for UserPrjOpened {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        let s = format!(
329            "{}-{}_{}",
330            self.username,
331            self.realname,
332            self.time.format("%y%m%d-%H%M%S")
333        );
334        f.write_str(&s)
335    }
336}
337impl Default for UserPrjOpened {
338    fn default() -> Self {
339        Self::new()
340    }
341}
342#[derive(Deserialize, Serialize, Debug, Clone)]
343pub struct SavePrjData {
344    pub version: Option<String>,
345    #[serde(serialize_with = "detail::serialize_opened_folder")]
346    #[serde(deserialize_with = "detail::deserialize_opened_folder")]
347    pub opened_folder: Option<String>,
348    pub tools_data_map: ToolsDataMap,
349    pub cfg: SavedCfg,
350    #[serde(default)]
351    pub wand_many_data: Option<WandManyData>,
352    #[serde(default)]
353    pub filter_string: Option<String>,
354}
355
356#[derive(Clone, Debug, Default)]
357pub enum Info {
358    Error(String),
359    Warning(String),
360    #[default]
361    None,
362}
363
364#[derive(Clone, Copy, Debug, Default, PartialEq)]
365pub enum PrjSettingImportSection {
366    #[default]
367    All,
368    Connection,
369    WandServer,
370}
371#[derive(Default)]
372pub struct ControlFlags {
373    pub undo_redo_load: bool,
374    pub is_loading_screen_active: bool,
375}
376
377// Project data to be serialized except cfg and tools data.
378#[derive(Default)]
379pub struct PrjData {
380    pub opened_folder: Option<PathPair>,
381    pub wand_many: WandManyData,
382    pub filter_buffer: String,
383}
384
385#[derive(Default)]
386pub struct Control {
387    pub data: PrjData,
388    pub reader: Option<ReaderFromCfg>,
389    pub info: Info,
390    pub paths_navigator: PathsNavigator,
391    tp: ThreadPool<RvResult<ReaderFromCfg>>,
392    last_open_folder_job_id: Option<u128>,
393    pub cfg: Cfg,
394    pub file_loaded: Option<usize>,
395    pub file_selected_idx: Option<usize>,
396    pub file_info_selected: Option<String>,
397    flags: ControlFlags,
398    pub loading_screen_animation_counter: u128,
399    pub log_export_path: Option<PathBuf>,
400    save_handle: Option<JoinHandle<()>>,
401    thumbnail_cache: HashMap<String, DynamicImage>,
402    wand_server: Option<CmdServer>,
403    wand_many_rx: Option<mpsc::Receiver<WandManyOutput>>,
404}
405
406impl Control {
407    pub fn is_wandmany_running(&self) -> bool {
408        self.wand_many_rx.is_some()
409    }
410    pub fn http_address(&self) -> String {
411        self.cfg.http_address().to_string()
412    }
413    pub fn flags(&self) -> &ControlFlags {
414        &self.flags
415    }
416    /// True if a different file is selected in the menu than the one currently
417    /// loaded, i.e. a file change will be triggered in this iteration.
418    pub fn is_file_change_pending(&self) -> bool {
419        self.file_selected_idx != self.paths_navigator.file_label_selected_idx()
420            || self.flags.is_loading_screen_active
421    }
422    pub fn reload(&mut self, sort_params: Option<SortParams>) -> RvResult<()> {
423        tracing::info!("reload");
424        if let Some(reader) = &mut self.reader {
425            reader.clear_cache()?;
426        }
427        if let Some(sort_params) = sort_params {
428            self.cfg.prj.sort_params = sort_params;
429        }
430        let label_selected = self.file_selected_idx.and_then(|idx| {
431            self.paths_navigator.len_filtered().and_then(|len_f| {
432                if idx < len_f {
433                    Some(self.file_label(idx).to_string())
434                } else {
435                    None
436                }
437            })
438        });
439        self.load_opened_folder_content(self.cfg.prj.sort_params)?;
440        if let Some(label_selected) = label_selected {
441            self.paths_navigator
442                .select_file_label(label_selected.as_str());
443        } else {
444            self.file_selected_idx = None;
445        }
446        Ok(())
447    }
448
449    pub fn replace_with_save(&mut self, input_prj_path: &Path) -> RvResult<ToolsDataMap> {
450        tracing::info!("replacing annotations with save from {input_prj_path:?}");
451        let cur_prj_path = self.cfg.current_prj_path().to_path_buf();
452        if let (Some(ifp_parent), Some(cpp_parent)) =
453            (input_prj_path.parent(), cur_prj_path.parent())
454        {
455            let loaded = if ifp_parent != cpp_parent {
456                // we need projects to be in the same folder for the correct resolution of relative paths
457                let copied_file_path = cpp_parent.join(
458                    input_prj_path
459                        .file_name()
460                        .ok_or_else(|| rverr!("could not get filename to copy to"))?,
461                );
462                defer_file_removal!(&copied_file_path);
463                trace_ok_err(fs::copy(input_prj_path, &copied_file_path));
464                let loaded = detail::load(input_prj_path)?;
465                loaded.tdm
466            } else {
467                // are in the same parent folder, i.e., we replace with the last manual save
468                let loaded = detail::load(input_prj_path)?;
469                loaded.tdm
470            };
471            self.set_current_prj_path(cur_prj_path)?;
472            self.cfg.write()?;
473            Ok(loaded)
474        } else {
475            Err(rverr!("{cur_prj_path:?} does not have a parent folder"))
476        }
477    }
478    pub fn load(&mut self, prj_path: PathBuf) -> RvResult<ToolsDataMap> {
479        tracing::info!("loading project from {prj_path:?}");
480        self.thumbnail_cache.clear();
481
482        // check if project is already opened by someone
483        let lockusr = read_user_from_lockfile(&prj_path)?;
484        if let Some(lockusr) = lockusr {
485            let usr = UserPrjOpened::new();
486            if usr.username != lockusr.username || usr.realname != lockusr.realname {
487                let lock_file_path = lock_file_path(&prj_path)?;
488                let err = rverr!(
489                    "The project is opened by {} ({}). Delete {:?} to unlock.",
490                    lockusr.username,
491                    lockusr.realname,
492                    lock_file_path
493                );
494                Err(err)
495            } else {
496                Ok(())
497            }
498        } else {
499            Ok(())
500        }?;
501
502        // we need the project path before reading the annotations to map
503        // their path correctly
504        self.set_current_prj_path(prj_path.clone())?;
505        self.cfg.write()?;
506        let loaded = detail::load(&prj_path).inspect_err(|_| {
507            self.cfg.unset_current_prj_path();
508            trace_ok_err(self.cfg.write());
509        })?;
510        if let Some(of) = loaded.opened_folder {
511            self.open_relative_folder(of)?;
512        }
513        self.cfg.prj = loaded.cfg_prj;
514        if let Some(wand_many_data) = loaded.wand_many_data {
515            self.data.wand_many = wand_many_data;
516        }
517        if let Some(filter_string) = loaded.filter_string {
518            self.data.filter_buffer = filter_string;
519        }
520        // save cfg of loaded project
521        trace_ok_err(self.cfg.write());
522        Ok(loaded.tdm)
523    }
524
525    fn wait_for_save(&mut self) {
526        if self.save_handle.is_some() {
527            mem::take(&mut self.save_handle).map(|h| trace_ok_err(h.join().map_err(to_rv)));
528        }
529    }
530    pub fn import_annos(&self, prj_path: &Path, tools_data_map: &mut ToolsDataMap) -> RvResult<()> {
531        tracing::info!("importing annotations from {prj_path:?}");
532        detail::import_annos(tools_data_map, prj_path)
533    }
534    pub fn show_settings(
535        &self,
536        prj_path: &Path,
537        import_section: PrjSettingImportSection,
538    ) -> String {
539        let loaded = detail::load(prj_path).unwrap();
540        let prj_cfg = loaded.cfg_prj;
541
542        match import_section {
543            PrjSettingImportSection::All => format!("{prj_cfg:#?}"),
544            PrjSettingImportSection::Connection => match prj_cfg.connection {
545                Connection::AzureBlob => format!("AzureBlob\n{:#?}", prj_cfg.azure_blob),
546                Connection::PyHttp => format!("PyHttp\n{:#?}", prj_cfg.py_http_reader_cfg),
547                Connection::Ssh => format!("Ssh\n{:#?}", prj_cfg.ssh),
548                Connection::Local => "Local".to_string(),
549            },
550            PrjSettingImportSection::WandServer => format!("{:#?}", prj_cfg.wand_server),
551        }
552    }
553    pub fn import_settings(
554        &mut self,
555        prj_path: &Path,
556        import_section: PrjSettingImportSection,
557    ) -> RvResult<()> {
558        tracing::info!("importing settings from {prj_path:?}");
559        let loaded = detail::load(prj_path)?;
560        let prj_cfg = loaded.cfg_prj;
561
562        match import_section {
563            PrjSettingImportSection::All => {
564                self.cfg.prj = prj_cfg;
565            }
566            PrjSettingImportSection::Connection => {
567                self.cfg.prj.connection = prj_cfg.connection;
568                self.cfg.prj.py_http_reader_cfg = prj_cfg.py_http_reader_cfg;
569                #[cfg(feature = "azure_blob")]
570                {
571                    self.cfg.prj.azure_blob = prj_cfg.azure_blob;
572                }
573            }
574            PrjSettingImportSection::WandServer => {
575                self.cfg.prj.wand_server = prj_cfg.wand_server;
576            }
577        }
578
579        Ok(())
580    }
581    pub fn import_both(
582        &mut self,
583        prj_path: &Path,
584        tools_data_map: &mut ToolsDataMap,
585    ) -> RvResult<()> {
586        self.import_annos(prj_path, tools_data_map)?;
587        self.import_settings(prj_path, PrjSettingImportSection::All)?;
588        Ok(())
589    }
590    pub fn import_from_coco(
591        &mut self,
592        coco_path: &str,
593        tools_data_map: &mut ToolsDataMap,
594        connection: ExportPathConnection,
595    ) -> RvResult<()> {
596        tracing::info!("importing from coco {coco_path:?}");
597
598        let meta_data = self.meta_data(None, None);
599        let path = ExportPath {
600            path: Path::new(coco_path).to_path_buf(),
601            conn: connection,
602        };
603        let (bbox_tool_data, brush_tool_data) = read_coco(&meta_data, &path, None)?;
604        let server_addresses = bbox_tool_data
605            .annotations_map
606            .keys()
607            .chain(brush_tool_data.annotations_map.keys())
608            .filter(|k| k.starts_with("http://"))
609            .flat_map(|k| k.rsplitn(2, '/').last())
610            .collect::<HashSet<_>>();
611        if !server_addresses.is_empty() {
612            self.cfg.prj.connection = Connection::PyHttp;
613
614            let server_addresses = server_addresses
615                .iter()
616                .map(|s| s.to_string())
617                .collect::<Vec<_>>();
618            self.cfg.prj.py_http_reader_cfg = Some(PyHttpReaderCfg { server_addresses });
619        }
620        let first_sa = server_addresses.iter().next().map(|s| s.to_string());
621        if let Some(sa) = first_sa {
622            self.open_relative_folder(sa.to_string())?;
623        }
624
625        tools_data_map.set_tools_specific_data(BRUSH_NAME, ToolSpecifics::Brush(brush_tool_data));
626        tools_data_map.set_tools_specific_data(BBOX_NAME, ToolSpecifics::Bbox(bbox_tool_data));
627        Ok(())
628    }
629
630    fn set_current_prj_path(&mut self, prj_path: PathBuf) -> RvResult<()> {
631        trace_ok_warn(detail::create_lock_file(&prj_path));
632        if prj_path != self.cfg.current_prj_path() {
633            trace_ok_warn(detail::remove_lock_file(self.cfg.current_prj_path()));
634        }
635        self.cfg.set_current_prj_path(prj_path);
636        Ok(())
637    }
638
639    pub fn save(
640        &mut self,
641        prj_path: PathBuf,
642        tools_data_map: &ToolsDataMap,
643        set_cur_prj: bool,
644    ) -> RvResult<()> {
645        tracing::info!("saving project to {prj_path:?}");
646        let prj_path = if let Some(of) = self.opened_folder() {
647            if DEFAULT_PRJ_PATH.as_os_str() == prj_path.as_os_str() {
648                PathBuf::from(of.path_relative()).join(DEFAULT_PRJ_NAME)
649            } else {
650                prj_path.clone()
651            }
652        } else {
653            prj_path.clone()
654        };
655
656        if set_cur_prj {
657            self.set_current_prj_path(prj_path.clone())?;
658            // update prj name in cfg
659            trace_ok_err(self.cfg.write());
660        }
661        let opened_folder = self.opened_folder().cloned();
662        let tdm = tools_data_map.clone();
663        let cfg = self.cfg.clone();
664        self.wait_for_save();
665        let wand_many_data = self.data.wand_many.clone();
666        let filter_string = self.data.filter_buffer.clone();
667        let handle = thread::spawn(move || {
668            trace_ok_err(detail::save(
669                opened_folder.as_ref().map(|of| of.path_relative()),
670                &tdm,
671                prj_path.as_path(),
672                &cfg,
673                &wand_many_data,
674                &filter_string,
675            ));
676        });
677        self.save_handle = Some(handle);
678        Ok(())
679    }
680
681    pub fn new() -> Self {
682        let cfg = Cfg::read(&DEFAULT_HOMEDIR).unwrap_or_else(|e| {
683            tracing::warn!("could not read cfg due to {e:?}, returning default");
684            Cfg::default()
685        });
686        if cfg.current_prj_path().exists() {
687            trace_ok_warn(detail::create_lock_file(cfg.current_prj_path()));
688        }
689        trace_ok_warn(create_lock_file(cfg.current_prj_path()));
690        let mut tmp = Self::default();
691        tmp.cfg = cfg;
692        tmp
693    }
694    pub fn new_prj(&mut self) -> ToolsDataMap {
695        let mut cfg = Cfg::read(&DEFAULT_HOMEDIR).unwrap_or_else(|e| {
696            tracing::warn!("could not read cfg due to {e:?}, returning default");
697            Cfg::default()
698        });
699        trace_ok_warn(detail::remove_lock_file(self.cfg.current_prj_path()));
700        cfg.unset_current_prj_path();
701        *self = Control::default();
702        self.cfg = cfg;
703        ToolsDataMap::new()
704    }
705
706    pub fn reader(&self) -> Option<&ReaderFromCfg> {
707        self.reader.as_ref()
708    }
709
710    pub fn read_image(&mut self, file_label_selected_idx: usize) -> AsyncResultImage {
711        let wrapped_image = self.reader.as_mut().and_then(|r| {
712            self.paths_navigator.paths_selector().as_ref().map(|ps| {
713                let ffp = ps.filtered_abs_file_paths();
714                r.read_image(file_label_selected_idx, &ffp)
715            })
716        });
717        match wrapped_image {
718            None => Ok(None),
719            Some(x) => Ok(x?),
720        }
721    }
722    pub fn read_cached_image(&mut self, file_label_selected_idx: usize) -> AsyncResultImage {
723        let wrapped_image = self.reader.as_mut().and_then(|r| {
724            self.paths_navigator.paths_selector().as_ref().map(|ps| {
725                let ffp = ps.filtered_abs_file_paths();
726                r.read_cached_image(file_label_selected_idx, &ffp)
727            })
728        });
729        match wrapped_image {
730            None => Ok(None),
731            Some(x) => Ok(x?),
732        }
733    }
734    pub fn cache_all_filtered(&mut self) -> RvResult<Option<f32>> {
735        if let (Some(ps), Some(r)) = (self.paths_navigator.paths_selector(), self.reader.as_mut()) {
736            let mut n_cached = 0;
737            for filtered_idx in 0..ps.len_filtered() {
738                let ffp = ps.filtered_abs_file_paths();
739                let res = r.cache_image(filtered_idx, &ffp)?;
740                n_cached += if res { 1 } else { 0 };
741            }
742            let amount = n_cached as f32 / ps.len_filtered() as f32;
743            Ok(Some(amount))
744        } else {
745            Ok(None)
746        }
747    }
748
749    fn make_reader(&mut self, cfg: Cfg) -> RvResult<()> {
750        self.paths_navigator = PathsNavigator::new(None, SortParams::default())?;
751        self.last_open_folder_job_id = Some(
752            self.tp
753                .apply(Box::new(move || ReaderFromCfg::from_cfg(cfg)))?,
754        );
755        Ok(())
756    }
757
758    pub fn remake_reader(&mut self) -> RvResult<()> {
759        let cfg = self.cfg.clone();
760        self.last_open_folder_job_id = Some(
761            self.tp
762                .apply(Box::new(move || ReaderFromCfg::from_cfg(cfg)))?,
763        );
764        Ok(())
765    }
766
767    pub fn start_wandserver(&mut self) -> RvResult<()> {
768        let ws = &self.cfg.prj.wand_server;
769        let mut wand_server = CmdServer::new(
770            ws.src.clone(),
771            ws.additional_files.clone(),
772            ws.setup_cmd.clone(),
773            ws.setup_args.clone(),
774            ws.install_uv,
775            ws.local_folder
776                .clone()
777                .unwrap_or(format!("{}/wand_server", self.cfg.home_folder())),
778        );
779        let cur_prj_path = self.cfg.current_prj_path();
780        wand_server.start_server(cur_prj_path)?;
781        self.wand_server = Some(wand_server);
782        Ok(())
783    }
784
785    pub fn stop_wandserver(&mut self) -> RvResult<()> {
786        if let Some(ws) = &mut self.wand_server {
787            tracing::info!("stopping wandserver...");
788            ws.stop_server()?;
789            tracing::info!("... done!");
790            Ok(())
791        } else {
792            Err(rverr!("Cannot stop wandserver, not running"))
793        }
794    }
795    pub fn cleanup_wandserver(&mut self) -> RvResult<()> {
796        if let Some(ws) = &mut self.wand_server {
797            tracing::info!("cleaning up wandserver...");
798            ws.cleanup_server()?;
799            tracing::info!("... cleaning up done.");
800            Ok(())
801        } else {
802            Err(rverr!("Cannot clean wandserver, not running"))
803        }?;
804        self.wand_server = None;
805        Ok(())
806    }
807
808    pub fn submit_files_to_wand(
809        &mut self,
810        tools_data_map: &ToolsDataMap,
811        files: &[String],
812        selected_file_idx: Option<usize>,
813        folders_to_exclude: &[String],
814    ) {
815        if self.is_wandmany_running() {
816            tracing::warn!("cannot submit files to wand, already running");
817        } else {
818            let tdm = tools_data_map.clone();
819            let (tx, rx) = mpsc::channel();
820            self.wand_many_rx = Some(rx);
821
822            let url = self.cfg.prj.wand_many.url.clone();
823            let headers = self.cfg.usr.wand_many_headers.clone();
824            let timeout = self.cfg.prj.wand_many.timeout_s;
825            let files = files.iter().map(|f| (*f).clone()).collect::<Vec<_>>();
826            let folders_to_exclude = folders_to_exclude
827                .iter()
828                .map(|f| (*f).clone())
829                .collect::<Vec<_>>();
830            let prj_name = if !self.cfg.prj.wand_many.prj_name.is_empty() {
831                self.cfg.prj.wand_many.prj_name.clone()
832            } else {
833                file_util::get_prj_name(
834                    self.cfg.current_prj_path(),
835                    self.opened_folder().map(|p| p.path_relative()),
836                )
837                .to_string()
838            };
839            let msgs = self.data.wand_many.messages.clone();
840            let param_map = self.data.wand_many.param_map.clone();
841            thread::spawn(move || {
842                let (input, files, selected_file_idx) = WandManyAnnotationsInput::from_tdm(
843                    &tdm,
844                    &files,
845                    &folders_to_exclude,
846                    selected_file_idx,
847                );
848
849                let wand_many = RestWandMany::new(url, headers.as_deref(), timeout);
850                tracing::info!("submitting files to wand...");
851                let output = trace_ok_err(wand_many.predict(
852                    &prj_name,
853                    input,
854                    &files,
855                    selected_file_idx,
856                    &msgs,
857                    Some(&param_map),
858                ));
859                if let Some(output) = output {
860                    trace_ok_err(tx.send(output));
861                } else {
862                    tracing::error!(
863                        "Processing files failed with wand failed. Prediction returned an error."
864                    );
865                    trace_ok_err(tx.send(WandManyOutput::default()));
866                }
867            });
868            tracing::info!("... submission done!");
869        }
870    }
871    pub fn check_wand_many_output(&mut self, tools_data_map: &mut ToolsDataMap) -> RvResult<bool> {
872        if let Some(rx) = &self.wand_many_rx {
873            match rx.try_recv() {
874                Ok(output) => {
875                    tracing::info!("received output from wand, applying to files...");
876                    let server_response = output.resolve_into_tdm(tools_data_map)?;
877                    tracing::info!("... applying done!");
878                    self.wand_many_rx = None;
879                    if let Some(al) = server_response
880                        .as_ref()
881                        .and_then(|sr| sr.artifact_link.as_ref())
882                    {
883                        let n_msgs = self.data.wand_many.messages.len();
884                        self.data
885                            .wand_many
886                            .artifact_links
887                            .push((n_msgs, al.clone()));
888                        tracing::info!("received artifact link\n{al}");
889                    }
890                    if let Some(smsg) = server_response.map(|sr| sr.msg)
891                        && let [.., last] = &mut self.data.wand_many.messages[..]
892                    {
893                        last.response = Some(smsg);
894                    }
895
896                    Ok(true)
897                }
898                Err(mpsc::TryRecvError::Empty) => Ok(false),
899                Err(mpsc::TryRecvError::Disconnected) => {
900                    self.wand_many_rx = None;
901                    Err(rverr!("wand-many channel disconnected"))
902                }
903            }
904        } else {
905            Ok(false)
906        }
907    }
908    pub fn cancel_wandmany(&mut self) {
909        self.wand_many_rx = None;
910        tracing::info!("Wand-many cancelled. Ignoring any output from wand-many server.");
911    }
912
913    pub fn export_logs(&self, dst: &Path) -> RvResult<()> {
914        let homefolder = self.cfg.home_folder();
915        let log_folder = get_log_folder(Path::new(homefolder));
916        tracing::info!("exporting logs from {log_folder:?} to {dst:?}");
917        tracing::info!("RV Image version {}", version_label());
918        let elf = log_folder.clone();
919        let dst = dst.to_path_buf();
920        thread::spawn(move || {
921            // zip log folder
922            let mut zip = zip::ZipWriter::new(fs::File::create(&dst).unwrap());
923
924            let walkdir = WalkDir::new(elf);
925            let iter_log = walkdir.into_iter();
926            for entry in iter_log {
927                if let Some(entry) = trace_ok_err(entry) {
928                    let path = entry.path();
929                    if path.is_file() {
930                        let file_name = osstr_to_str(path.file_name());
931                        trace_ok_err(file_name).and_then(|file_name| {
932                            trace_ok_err(zip.start_file::<&str, ExtendedFileOptions>(
933                                file_name,
934                                zip::write::FileOptions::default(),
935                            ));
936                            trace_ok_err(fs::read(path))
937                                .and_then(|buf| trace_ok_err(zip.write_all(&buf)))
938                        });
939                    }
940                }
941            }
942        });
943        Ok(())
944    }
945
946    pub fn open_relative_folder(&mut self, new_folder: String) -> RvResult<()> {
947        tracing::info!("new opened folder {new_folder}");
948        self.thumbnail_cache.clear();
949        self.make_reader(self.cfg.clone())?;
950        let current_prj_path = match self.cfg.prj.connection {
951            Connection::Local => Some(self.cfg.current_prj_path()),
952            _ => None,
953        };
954        self.data.opened_folder = Some(PathPair::from_relative_path(new_folder, current_prj_path));
955        Ok(())
956    }
957
958    pub fn load_opened_folder_content(&mut self, sort_params: SortParams) -> RvResult<()> {
959        if let (Some(opened_folder), Some(reader)) = (&self.data.opened_folder, &self.reader) {
960            let prj_folder = self.cfg.current_prj_path();
961            let selector = reader.open_folder(opened_folder.path_absolute(), prj_folder)?;
962            self.paths_navigator = PathsNavigator::new(Some(selector), sort_params)?;
963        }
964        Ok(())
965    }
966
967    pub fn check_if_connected(&mut self, sort_params: SortParams) -> RvResult<bool> {
968        if let Some(job_id) = self.last_open_folder_job_id {
969            let tp_res = self.tp.result(job_id);
970            if let Some(res) = tp_res {
971                self.last_open_folder_job_id = None;
972                res.and_then(|reader| {
973                    self.reader = Some(reader);
974                    self.load_opened_folder_content(sort_params)?;
975                    Ok(true)
976                })
977            } else {
978                Ok(false)
979            }
980        } else {
981            Ok(true)
982        }
983    }
984
985    pub fn opened_folder_label(&self) -> Option<&str> {
986        self.paths_navigator
987            .paths_selector()
988            .as_ref()
989            .map(|ps| ps.folder_label())
990    }
991
992    pub fn file_label(&self, idx: usize) -> &str {
993        match self
994            .paths_navigator
995            .paths_selector()
996            .and_then(|ps| ps.filtered_idx_file_label_pairs(idx))
997        {
998            Some(ffl) => ffl.filename.as_str(),
999            None => "",
1000        }
1001    }
1002
1003    pub fn cfg_of_opened_folder(&self) -> Option<&Cfg> {
1004        self.reader().map(|r| r.cfg())
1005    }
1006
1007    fn opened_folder(&self) -> Option<&PathPair> {
1008        self.data.opened_folder.as_ref()
1009    }
1010
1011    pub fn connection_data(&self) -> RvResult<ConnectionData> {
1012        let cfg = self
1013            .cfg_of_opened_folder()
1014            .ok_or_else(|| RvError::new("save failed, open folder first"));
1015        Ok(match self.cfg.prj.connection {
1016            Connection::Ssh => {
1017                let ssh_cfg = cfg.map(|cfg| cfg.ssh_cfg())?;
1018                ConnectionData::Ssh(ssh_cfg)
1019            }
1020            Connection::Local => ConnectionData::None,
1021            Connection::PyHttp => {
1022                let pyhttp_cfg = cfg
1023                    .map(|cfg| cfg.prj.py_http_reader_cfg.clone())?
1024                    .ok_or_else(|| RvError::new("cannot open pyhttp without pyhttp cfg"))?;
1025                ConnectionData::PyHttp(pyhttp_cfg)
1026            }
1027            #[cfg(feature = "azure_blob")]
1028            Connection::AzureBlob => {
1029                let azure_blob_cfg = cfg
1030                    .map(|cfg| cfg.azure_blob_cfg())?
1031                    .ok_or_else(|| RvError::new("cannot open azure blob without cfg"))?;
1032                ConnectionData::AzureBlobCfg(azure_blob_cfg)
1033            }
1034        })
1035    }
1036
1037    pub fn meta_data(
1038        &self,
1039        file_selected_idx: Option<usize>,
1040        is_loading_screen_active: Option<bool>,
1041    ) -> MetaData {
1042        let file_path =
1043            file_selected_idx.and_then(|fsidx| self.paths_navigator.file_path(fsidx).cloned());
1044        let open_folder = self.opened_folder().cloned();
1045        let connection_data = if self.reader.is_some() {
1046            ConnectionData::Ssh(self.cfg.ssh_cfg())
1047        } else {
1048            ConnectionData::None
1049        };
1050        let export_folder = self
1051            .cfg_of_opened_folder()
1052            .map(|cfg| cfg.home_folder().to_string());
1053        let is_file_list_empty = Some(file_path.is_none());
1054        let prj_path = self.cfg.current_prj_path();
1055        MetaData::new(
1056            file_path,
1057            file_selected_idx,
1058            connection_data,
1059            Some(self.cfg.ssh_cfg()),
1060            open_folder,
1061            export_folder,
1062            MetaDataFlags {
1063                is_loading_screen_active,
1064                is_file_list_empty,
1065            },
1066            Some(prj_path.to_path_buf()),
1067        )
1068    }
1069
1070    pub fn redo(&mut self, history: &mut History) -> Option<(World, Option<usize>)> {
1071        self.flags.undo_redo_load = true;
1072        detail::idx_change_check(
1073            self.file_selected_idx,
1074            history.next_world(&self.data.opened_folder),
1075        )
1076    }
1077    pub fn undo(&mut self, history: &mut History) -> Option<(World, Option<usize>)> {
1078        self.flags.undo_redo_load = true;
1079        detail::idx_change_check(
1080            self.file_selected_idx,
1081            history.prev_world(&self.data.opened_folder),
1082        )
1083    }
1084
1085    fn get_path(&self, idx: usize) -> Option<String> {
1086        trace_ok_err(
1087            self.paths_navigator
1088                .file_path(idx)
1089                .map(|p| p.path_absolute().to_string())
1090                .ok_or_else(|| rverr!("index does not have path")),
1091        )
1092    }
1093
1094    fn get_image_meta(&self, world: &World, idx: usize) -> Option<ImageMeta> {
1095        let file_label = self.paths_navigator.paths_selector().and_then(|ps| {
1096            ps.filtered_idx_file_label_pairs(idx)
1097                .map(|ffl| &ffl.filename)
1098        });
1099
1100        file_label.map(|file_label| {
1101            let attrmap = world
1102                .data
1103                .tools_data_map
1104                .get(ATTRIBUTES_NAME)
1105                .and_then(|d| trace_ok_err(d.specifics.attributes()))
1106                .and_then(|d| d.get_annos(file_label));
1107            ImageMeta {
1108                file_label: file_label.to_string(),
1109                attrs: attrmap.cloned(),
1110            }
1111        })
1112    }
1113
1114    fn load_thumbnails(&mut self, world: &World, start: usize, end: usize) -> Vec<ImageMetaPair> {
1115        (start..end)
1116            .flat_map(|idx| {
1117                let path = self.get_path(idx);
1118                let meta_data = self.get_image_meta(world, idx);
1119
1120                if let (Some(p), Some(meta)) = (path.as_deref(), meta_data) {
1121                    let im = if let Some(im) = self.thumbnail_cache.get(p) {
1122                        Some(im.clone())
1123                    } else {
1124                        let in_cache_im = self.read_cached_image(idx).map(|im| {
1125                            im.and_then(|im| {
1126                                let im_thumb = im.im.resize(200, 100, FilterType::Lanczos3);
1127                                let im_rotated_thumb =
1128                                    trace_ok_err(rotate90(world, im_thumb.clone(), p));
1129                                im_rotated_thumb.inspect(|im| {
1130                                    self.thumbnail_cache.insert(p.to_string(), im.clone());
1131                                })
1132                            })
1133                        });
1134                        trace_ok_err(in_cache_im.map(|im| {
1135                            im.unwrap_or(DynamicImage::ImageRgb8(ImageBuffer::new(10, 10)))
1136                        }))
1137                    };
1138                    im.map(|im| ImageMetaPair { im, meta })
1139                } else {
1140                    None
1141                }
1142            })
1143            .collect::<Vec<_>>()
1144    }
1145
1146    pub fn load_new_image_if_triggered(
1147        &mut self,
1148        world: &World,
1149        history: &mut History,
1150    ) -> RvResult<Option<(World, Option<usize>)>> {
1151        measure_time!("load image if new", {
1152            let menu_file_selected = measure_time!("before if", {
1153                self.paths_navigator.file_label_selected_idx()
1154            });
1155            let world_idx_pair = if self.file_selected_idx != menu_file_selected
1156                || self.flags.is_loading_screen_active
1157            {
1158                // load new image
1159                if let Some(selected) = &menu_file_selected {
1160                    let abs_file_path = menu_file_selected.and_then(|fs| {
1161                        Some(
1162                            self.paths_navigator
1163                                .file_path(fs)?
1164                                .path_absolute()
1165                                .replace('\\', "/"),
1166                        )
1167                    });
1168                    let im_read = self.read_image(*selected)?;
1169                    let meta = self.get_image_meta(world, *selected);
1170                    tracing::debug!("meta {}", meta.is_some());
1171                    tracing::debug!("im_read {}", im_read.is_some());
1172                    tracing::debug!("abs_file_path {}", abs_file_path.is_some());
1173                    let new_world_idx_pair = match (abs_file_path, im_read, meta) {
1174                        (Some(fp), Some(ri), Some(meta)) => {
1175                            tracing::info!("loading {} from {}", ri.info, fp);
1176                            let im = ri.im;
1177                            let im = ImageMetaPair { im, meta };
1178                            self.file_selected_idx = menu_file_selected;
1179                            self.file_info_selected = Some(ri.info);
1180                            let mut new_world = world.clone();
1181                            if !self.cfg.usr.hide_thumbs {
1182                                let prev_start = if *selected > self.cfg.usr.n_prev_thumbs {
1183                                    selected - self.cfg.usr.n_prev_thumbs
1184                                } else {
1185                                    0
1186                                };
1187                                let prev_images =
1188                                    self.load_thumbnails(&new_world, prev_start, *selected);
1189                                let n = self.paths_navigator.len_filtered().unwrap_or(0);
1190                                let next_end = if n > *selected + 1 + self.cfg.usr.n_next_thumbs {
1191                                    selected + 1 + self.cfg.usr.n_prev_thumbs
1192                                } else {
1193                                    n
1194                                };
1195                                let next_images =
1196                                    self.load_thumbnails(&new_world, *selected + 1, next_end);
1197                                let extra_ims = ThumbIms::new(
1198                                    prev_images,
1199                                    next_images,
1200                                    Some(&im),
1201                                    self.cfg.usr.thumb_w_max,
1202                                    self.cfg.usr.thumb_h_max,
1203                                );
1204                                new_world.set_extra_images(extra_ims);
1205                            }
1206                            new_world.set_background_image(im);
1207                            new_world.reset_updateview();
1208
1209                            if !self.flags.undo_redo_load {
1210                                history.push(Record {
1211                                    world: world.clone(),
1212                                    actor: LOAD_ACTOR_NAME,
1213                                    file_label_idx: self.file_selected_idx,
1214                                    opened_folder: self
1215                                        .data
1216                                        .opened_folder
1217                                        .as_ref()
1218                                        .map(|of| of.path_absolute().to_string()),
1219                                });
1220                            }
1221                            self.flags.undo_redo_load = false;
1222                            self.flags.is_loading_screen_active = false;
1223                            (new_world, self.file_selected_idx)
1224                        }
1225                        _ => {
1226                            thread::sleep(Duration::from_millis(2));
1227
1228                            tracing::debug!("still loading...");
1229                            self.file_selected_idx = menu_file_selected;
1230                            self.flags.is_loading_screen_active = true;
1231                            let mut new_world = world.clone();
1232
1233                            detail::draw_loading_dots(
1234                                new_world.data.im_background_mut(),
1235                                self.loading_screen_animation_counter,
1236                            );
1237                            new_world.reset_updateview();
1238                            (new_world, self.file_selected_idx)
1239                        }
1240                    };
1241                    Some(new_world_idx_pair)
1242                } else {
1243                    None
1244                }
1245            } else {
1246                None
1247            };
1248            self.loading_screen_animation_counter += 1;
1249            if self.loading_screen_animation_counter == u128::MAX {
1250                self.loading_screen_animation_counter = 0;
1251            }
1252            Ok(world_idx_pair)
1253        })
1254    }
1255}
1256
1257#[cfg(test)]
1258use {
1259    crate::{
1260        file_util::DEFAULT_TMPDIR,
1261        tools_data::{BboxToolData, ToolsData},
1262    },
1263    rvimage_domain::{ShapeI, make_test_bbs},
1264    std::str::FromStr,
1265};
1266#[cfg(test)]
1267pub fn make_data(image_file: &Path) -> ToolsDataMap {
1268    use crate::tools_data::VisibleInactiveToolsState;
1269
1270    let test_export_folder = DEFAULT_TMPDIR.clone();
1271
1272    match fs::create_dir(&test_export_folder) {
1273        Ok(_) => (),
1274        Err(e) => {
1275            println!("{e:?}");
1276        }
1277    }
1278
1279    let mut bbox_data = BboxToolData::new();
1280    bbox_data
1281        .label_info
1282        .push("x".to_string(), None, None)
1283        .unwrap();
1284    bbox_data
1285        .label_info
1286        .remove_catidx(0, &mut bbox_data.annotations_map);
1287    let mut bbs = make_test_bbs();
1288    bbs.extend(bbs.clone());
1289    bbs.extend(bbs.clone());
1290    bbs.extend(bbs.clone());
1291    bbs.extend(bbs.clone());
1292    bbs.extend(bbs.clone());
1293    bbs.extend(bbs.clone());
1294    bbs.extend(bbs.clone());
1295
1296    let annos = bbox_data.get_annos_mut(
1297        image_file.as_os_str().to_str().unwrap(),
1298        ShapeI::new(10, 10),
1299    );
1300    if let Some(a) = annos {
1301        for bb in bbs {
1302            a.add_bb(bb, 0, crate::InstanceLabelDisplay::IndexLr);
1303        }
1304    }
1305
1306    let data = HashMap::from([(
1307        BBOX_NAME.to_string(),
1308        ToolsData::new(
1309            ToolSpecifics::Bbox(bbox_data),
1310            VisibleInactiveToolsState::default(),
1311        ),
1312    )]);
1313    ToolsDataMap::from(data)
1314}
1315
1316impl Drop for Control {
1317    fn drop(&mut self) {
1318        trace_ok_warn(detail::remove_lock_file(self.cfg.current_prj_path()));
1319    }
1320}
1321
1322#[test]
1323fn test_save_load() {
1324    let tdm = make_data(&PathBuf::from_str("dummyfile").unwrap());
1325    let cfg = {
1326        let mut tmp = Cfg::default();
1327        tmp.usr.n_autosaves = Some(59);
1328        tmp
1329    };
1330    let opened_folder_name = "dummy_opened_folder";
1331    let export_folder = cfg.tmpdir();
1332    let export_file = PathBuf::new().join(export_folder).join("export.json");
1333    let opened_folder = Some(opened_folder_name.to_string());
1334    detail::save(
1335        opened_folder.as_deref(),
1336        &tdm,
1337        &export_file,
1338        &cfg,
1339        &WandManyData::default(),
1340        "",
1341    )
1342    .unwrap();
1343
1344    defer_file_removal!(&export_file);
1345
1346    let loaded = detail::load(&export_file).unwrap();
1347    assert_eq!(tdm, loaded.tdm);
1348    assert_eq!(cfg.prj, loaded.cfg_prj);
1349}