rvlib/control/
mod.rs

1use crate::cfg::{get_log_folder, Connection, ExportPath, ExportPathConnection, PyHttpReaderCfg};
2use crate::file_util::{
3    osstr_to_str, to_stem_str, PathPair, SavedCfg, DEFAULT_HOMEDIR, DEFAULT_PRJ_NAME,
4    DEFAULT_PRJ_PATH,
5};
6use crate::history::{History, Record};
7use crate::meta_data::{ConnectionData, MetaData, MetaDataFlags};
8use crate::result::{trace_ok_err, trace_ok_warn};
9use crate::sort_params::SortParams;
10use crate::tools::{BBOX_NAME, BRUSH_NAME};
11use crate::tools_data::{coco_io::read_coco, ToolSpecifics, ToolsDataMap};
12use crate::world::World;
13use crate::{
14    cfg::Cfg, image_reader::ReaderFromCfg, threadpool::ThreadPool, types::AsyncResultImage,
15};
16use crate::{defer_file_removal, measure_time};
17use chrono::{DateTime, Utc};
18use detail::{create_lock_file, lock_file_path, read_user_from_lockfile};
19use egui::ahash::HashSet;
20use rvimage_domain::{rverr, to_rv, RvError, RvResult};
21use serde::{Deserialize, Serialize};
22use std::fmt::{Debug, Display};
23use std::io::Write;
24use std::path::{Path, PathBuf};
25use std::thread::{self, JoinHandle};
26use std::time::Duration;
27use std::{fs, mem};
28use zip::write::ExtendedFileOptions;
29mod filter;
30pub mod paths_navigator;
31use crate::image_reader::LoadImageForGui;
32use paths_navigator::PathsNavigator;
33use walkdir::WalkDir;
34
35mod detail {
36    use std::{
37        mem,
38        path::{Path, PathBuf},
39    };
40
41    use image::{DynamicImage, GenericImage};
42    use imageproc::drawing::Canvas;
43    use serde::{Deserialize, Serialize, Serializer};
44
45    use crate::{
46        cfg::{Cfg, CfgPrj},
47        control::SavePrjData,
48        defer_file_removal,
49        file_util::{self, tf_to_annomap_key, SavedCfg, DEFAULT_HOMEDIR},
50        result::trace_ok_err,
51        tools::{add_tools_initial_data, ATTRIBUTES_NAME, BBOX_NAME, BRUSH_NAME, ROT90_NAME},
52        tools_data::{merge, ToolsDataMap},
53        toolsdata_by_name,
54        util::version_label,
55        world::World,
56    };
57    use rvimage_domain::ShapeI;
58    use rvimage_domain::{result::RvResult, to_rv};
59
60    use super::UserPrjOpened;
61
62    pub fn serialize_opened_folder<S>(
63        folder: &Option<String>,
64        serializer: S,
65    ) -> Result<S::Ok, S::Error>
66    where
67        S: Serializer,
68    {
69        let cfg = trace_ok_err(Cfg::read(&DEFAULT_HOMEDIR));
70        let prj_path = cfg.as_ref().map(|cfg| cfg.current_prj_path());
71        let folder = folder
72            .clone()
73            .map(|folder| tf_to_annomap_key(folder, prj_path));
74        folder.serialize(serializer)
75    }
76    pub fn deserialize_opened_folder<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
77    where
78        D: serde::Deserializer<'de>,
79    {
80        let cfg = trace_ok_err(Cfg::read(&DEFAULT_HOMEDIR));
81        let prj_path = cfg.as_ref().map(|cfg| cfg.current_prj_path());
82        let folder: Option<String> = Option::deserialize(deserializer)?;
83
84        Ok(folder.map(|p| tf_to_annomap_key(p, prj_path)))
85    }
86
87    pub(super) fn lock_file_path(file_path: &Path) -> RvResult<PathBuf> {
88        let stem = file_util::osstr_to_str(file_path.file_stem()).map_err(to_rv)?;
89        Ok(file_path.with_file_name(format!(".{stem}_lock.json")))
90    }
91    pub(super) fn create_lock_file(file_path: &Path) -> RvResult<()> {
92        let lock_file = lock_file_path(file_path)?;
93        tracing::info!("creating lock file {lock_file:?}");
94        let upo = UserPrjOpened::new();
95        file_util::save(&lock_file, upo)
96    }
97    pub(super) fn remove_lock_file(prj_file_path: &Path) -> RvResult<()> {
98        let lock_file = lock_file_path(prj_file_path)?;
99        if lock_file.exists() {
100            tracing::info!("removing lock file {lock_file:?}");
101            defer_file_removal!(&lock_file);
102        }
103        Ok(())
104    }
105    pub(super) fn read_user_from_lockfile(prj_file_path: &Path) -> RvResult<Option<UserPrjOpened>> {
106        let lock_file = lock_file_path(prj_file_path)?;
107        let lock_file_content = file_util::read_to_string(lock_file).ok();
108        lock_file_content
109            .map(|lfc| serde_json::from_str(&lfc).map_err(to_rv))
110            .transpose()
111    }
112
113    pub(super) fn idx_change_check(
114        file_selected_idx: Option<usize>,
115        world_idx_pair: Option<(World, Option<usize>)>,
116    ) -> Option<(World, Option<usize>)> {
117        world_idx_pair.map(|(w, idx)| {
118            if idx != file_selected_idx {
119                (w, idx)
120            } else {
121                (w, None)
122            }
123        })
124    }
125
126    fn write<T>(
127        tools_data_map: &ToolsDataMap,
128        make_data: impl Fn(&ToolsDataMap) -> T,
129        export_path: &Path,
130    ) -> RvResult<()>
131    where
132        T: Serialize,
133    {
134        let tools_data_map = tools_data_map
135            .iter()
136            .map(|(k, v)| {
137                let mut v = v.clone();
138                v.menu_active = false;
139                (k.clone(), v)
140            })
141            .collect::<ToolsDataMap>();
142        let data = make_data(&tools_data_map);
143        file_util::save(export_path, data)
144    }
145
146    pub fn save(
147        opened_folder: Option<&str>,
148        tools_data_map: &ToolsDataMap,
149        file_path: &Path,
150        cfg: &Cfg,
151    ) -> RvResult<()> {
152        // we need to write the cfg for correct prj-path mapping during serialization
153        // of annotations
154        trace_ok_err(cfg.write());
155        let make_data = |tdm: &ToolsDataMap| SavePrjData {
156            version: Some(version_label()),
157            opened_folder: opened_folder.map(|of| of.to_string()),
158            tools_data_map: tdm.clone(),
159            cfg: SavedCfg::CfgPrj(cfg.prj.clone()),
160        };
161        tracing::info!("saved to {file_path:?}");
162        write(tools_data_map, make_data, file_path)?;
163        Ok(())
164    }
165
166    pub(super) fn draw_loading_dots(im: &mut DynamicImage, counter: u128) {
167        let shape = ShapeI::from_im(im);
168        let radius = 7u32;
169        let centers = [
170            (shape.w - 70, shape.h - 20),
171            (shape.w - 50, shape.h - 20),
172            (shape.w - 30, shape.h - 20),
173        ];
174        let off_center_dim = |c_idx: usize, counter_mod: usize, rgb: &[u8; 3]| {
175            let mut res = *rgb;
176            for (rgb_idx, val) in rgb.iter().enumerate() {
177                if counter_mod != c_idx {
178                    res[rgb_idx] = (*val as f32 * 0.7) as u8;
179                } else {
180                    res[rgb_idx] = *val;
181                }
182            }
183            res
184        };
185        for (c_idx, ctr) in centers.iter().enumerate() {
186            for y in ctr.1.saturating_sub(radius)..ctr.1.saturating_add(radius) {
187                for x in ctr.0.saturating_sub(radius)..ctr.0.saturating_add(radius) {
188                    let ctr0_x = x.abs_diff(ctr.0);
189                    let ctr1_y = y.abs_diff(ctr.1);
190                    let ctr0_x_sq = ctr0_x.saturating_mul(ctr0_x);
191                    let ctr1_y_sq = ctr1_y.saturating_mul(ctr1_y);
192                    if ctr0_x_sq + ctr1_y_sq < radius.pow(2) {
193                        let counter_mod = ((counter / 5) % 3) as usize;
194                        let rgb = off_center_dim(c_idx, counter_mod, &[195u8, 255u8, 205u8]);
195                        let mut pixel = im.get_pixel(x, y);
196                        pixel.0 = [rgb[0], rgb[1], rgb[2], 255];
197                        im.put_pixel(x, y, pixel);
198                    }
199                }
200            }
201        }
202    }
203    pub(super) fn load(file_path: &Path) -> RvResult<(ToolsDataMap, Option<String>, CfgPrj)> {
204        let s = file_util::read_to_string(file_path)?;
205
206        let save_data = serde_json::from_str::<SavePrjData>(s.as_str()).map_err(to_rv)?;
207        let cfg_prj = match save_data.cfg {
208            SavedCfg::CfgLegacy(cfg) => cfg.to_cfg().prj,
209            SavedCfg::CfgPrj(cfg_prj) => cfg_prj,
210        };
211        Ok((
212            add_tools_initial_data(save_data.tools_data_map),
213            save_data.opened_folder,
214            cfg_prj,
215        ))
216    }
217
218    #[derive(PartialEq)]
219    enum FillResult {
220        FilledCurWithLoaded,
221        LoadedEmpty,
222        BothNotEmpty,
223        BothEmpty,
224    }
225    fn fill_empty_curtdm(
226        tool: &str,
227        cur_tdm: &mut ToolsDataMap,
228        loaded_tdm: &mut ToolsDataMap,
229    ) -> FillResult {
230        if !cur_tdm.contains_key(tool) && loaded_tdm.contains_key(tool) {
231            cur_tdm.insert(tool.to_string(), loaded_tdm[tool].clone());
232            FillResult::FilledCurWithLoaded
233        } else if !loaded_tdm.contains_key(tool) {
234            FillResult::LoadedEmpty
235        } else if cur_tdm.contains_key(tool) {
236            FillResult::BothNotEmpty
237        } else {
238            FillResult::BothEmpty
239        }
240    }
241    pub fn import_annos(cur_tdm: &mut ToolsDataMap, file_path: &Path) -> RvResult<()> {
242        let (mut loaded_tdm, _, _) = load(file_path)?;
243
244        if fill_empty_curtdm(BBOX_NAME, cur_tdm, &mut loaded_tdm) == FillResult::BothNotEmpty {
245            let cur_bbox = toolsdata_by_name!(BBOX_NAME, bbox_mut, cur_tdm);
246            let loaded_bbox = toolsdata_by_name!(BBOX_NAME, bbox_mut, loaded_tdm);
247            let cur_annos = mem::take(&mut cur_bbox.annotations_map);
248            let cur_li = mem::take(&mut cur_bbox.label_info);
249            let loaded_annos = mem::take(&mut loaded_bbox.annotations_map);
250            let loaded_li = mem::take(&mut loaded_bbox.label_info);
251            let (merged_annos, merged_li) = merge(cur_annos, cur_li, loaded_annos, loaded_li);
252            cur_bbox.annotations_map = merged_annos;
253            cur_bbox.label_info = merged_li;
254        }
255
256        if fill_empty_curtdm(BRUSH_NAME, cur_tdm, &mut loaded_tdm) == FillResult::BothNotEmpty {
257            let cur_brush = toolsdata_by_name!(BRUSH_NAME, brush_mut, cur_tdm);
258            let loaded_brush = toolsdata_by_name!(BRUSH_NAME, brush_mut, loaded_tdm);
259            let cur_annos = mem::take(&mut cur_brush.annotations_map);
260            let cur_li = mem::take(&mut cur_brush.label_info);
261            let loaded_annos = mem::take(&mut loaded_brush.annotations_map);
262            let loaded_li = mem::take(&mut loaded_brush.label_info);
263            let (merged_annos, merged_li) = merge(cur_annos, cur_li, loaded_annos, loaded_li);
264            cur_brush.annotations_map = merged_annos;
265            cur_brush.label_info = merged_li;
266        }
267
268        if fill_empty_curtdm(ROT90_NAME, cur_tdm, &mut loaded_tdm) == FillResult::BothNotEmpty {
269            let cur_rot90 = toolsdata_by_name!(ROT90_NAME, rot90_mut, cur_tdm);
270            let loaded_rot90 = toolsdata_by_name!(ROT90_NAME, rot90_mut, loaded_tdm);
271            *cur_rot90 = mem::take(cur_rot90).merge(mem::take(loaded_rot90));
272        }
273
274        if fill_empty_curtdm(ATTRIBUTES_NAME, cur_tdm, &mut loaded_tdm) == FillResult::BothNotEmpty
275        {
276            let cur_attr = toolsdata_by_name!(ATTRIBUTES_NAME, attributes_mut, cur_tdm);
277            let loaded_attr = toolsdata_by_name!(ATTRIBUTES_NAME, attributes_mut, loaded_tdm);
278            *cur_attr = mem::take(cur_attr).merge(mem::take(loaded_attr));
279        }
280        Ok(())
281    }
282}
283const LOAD_ACTOR_NAME: &str = "Load";
284
285#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
286pub struct UserPrjOpened {
287    time: DateTime<Utc>,
288    username: String,
289    realname: String,
290}
291impl UserPrjOpened {
292    pub fn new() -> Self {
293        UserPrjOpened {
294            time: Utc::now(),
295            username: whoami::username(),
296            realname: whoami::realname(),
297        }
298    }
299}
300impl Display for UserPrjOpened {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        let s = format!(
303            "{}-{}_{}",
304            self.username,
305            self.realname,
306            self.time.format("%y%m%d-%H%M%S")
307        );
308        f.write_str(&s)
309    }
310}
311impl Default for UserPrjOpened {
312    fn default() -> Self {
313        Self::new()
314    }
315}
316#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
317pub struct SavePrjData {
318    pub version: Option<String>,
319    #[serde(serialize_with = "detail::serialize_opened_folder")]
320    #[serde(deserialize_with = "detail::deserialize_opened_folder")]
321    pub opened_folder: Option<String>,
322    pub tools_data_map: ToolsDataMap,
323    pub cfg: SavedCfg,
324}
325
326#[derive(Clone, Debug, Default)]
327pub enum Info {
328    Error(String),
329    Warning(String),
330    #[default]
331    None,
332}
333
334#[derive(Default)]
335pub struct ControlFlags {
336    pub undo_redo_load: bool,
337    pub is_loading_screen_active: bool,
338}
339
340#[derive(Default)]
341pub struct Control {
342    pub reader: Option<ReaderFromCfg>,
343    pub info: Info,
344    pub paths_navigator: PathsNavigator,
345    pub opened_folder: Option<PathPair>,
346    tp: ThreadPool<RvResult<ReaderFromCfg>>,
347    last_open_folder_job_id: Option<u128>,
348    pub cfg: Cfg,
349    pub file_loaded: Option<usize>,
350    pub file_selected_idx: Option<usize>,
351    pub file_info_selected: Option<String>,
352    flags: ControlFlags,
353    pub loading_screen_animation_counter: u128,
354    pub log_export_path: Option<PathBuf>,
355    save_handle: Option<JoinHandle<()>>,
356}
357
358impl Control {
359    pub fn http_address(&self) -> String {
360        self.cfg.http_address().to_string()
361    }
362    pub fn flags(&self) -> &ControlFlags {
363        &self.flags
364    }
365    pub fn reload(&mut self, sort_params: Option<SortParams>) -> RvResult<()> {
366        tracing::info!("reload");
367        if let Some(reader) = &mut self.reader {
368            reader.clear_cache()?;
369        }
370        if let Some(sort_params) = sort_params {
371            self.cfg.prj.sort_params = sort_params;
372        }
373        let label_selected = self.file_selected_idx.and_then(|idx| {
374            self.paths_navigator.len_filtered().and_then(|len_f| {
375                if idx < len_f {
376                    Some(self.file_label(idx).to_string())
377                } else {
378                    None
379                }
380            })
381        });
382        self.load_opened_folder_content(self.cfg.prj.sort_params)?;
383        if let Some(label_selected) = label_selected {
384            self.paths_navigator
385                .select_file_label(label_selected.as_str());
386        } else {
387            self.file_selected_idx = None;
388        }
389        Ok(())
390    }
391
392    pub fn replace_with_save(&mut self, input_prj_path: &Path) -> RvResult<ToolsDataMap> {
393        tracing::info!("replacing annotations with save from {input_prj_path:?}");
394        let cur_prj_path = self.cfg.current_prj_path().to_path_buf();
395        if let (Some(ifp_parent), Some(cpp_parent)) =
396            (input_prj_path.parent(), cur_prj_path.parent())
397        {
398            let loaded = if ifp_parent != cpp_parent {
399                // we need projects to be in the same folder for the correct resolution of relative paths
400                let copied_file_path = cpp_parent.join(
401                    input_prj_path
402                        .file_name()
403                        .ok_or_else(|| rverr!("could not get filename to copy to"))?,
404                );
405                defer_file_removal!(&copied_file_path);
406                trace_ok_err(fs::copy(input_prj_path, &copied_file_path));
407                let (tdm, _, _) = detail::load(input_prj_path)?;
408                tdm
409            } else {
410                // are in the same parent folder, i.e., we replace with the last manual save
411                let (tdm, _, _) = detail::load(input_prj_path)?;
412                tdm
413            };
414            self.set_current_prj_path(cur_prj_path)?;
415            self.cfg.write()?;
416            Ok(loaded)
417        } else {
418            Err(rverr!("{cur_prj_path:?} does not have a parent folder"))
419        }
420    }
421    pub fn load(&mut self, prj_path: PathBuf) -> RvResult<ToolsDataMap> {
422        tracing::info!("loading project from {prj_path:?}");
423
424        // check if project is already opened by someone
425        let lockusr = read_user_from_lockfile(&prj_path)?;
426        if let Some(lockusr) = lockusr {
427            let usr = UserPrjOpened::new();
428            if usr.username != lockusr.username || usr.realname != lockusr.realname {
429                let lock_file_path = lock_file_path(&prj_path)?;
430                let err = rverr!(
431                    "The project is opened by {} ({}). Delete {:?} to unlock.",
432                    lockusr.username,
433                    lockusr.realname,
434                    lock_file_path
435                );
436                Err(err)
437            } else {
438                Ok(())
439            }
440        } else {
441            Ok(())
442        }?;
443
444        // we need the project path before reading the annotations to map
445        // their path correctly
446        self.set_current_prj_path(prj_path.clone())?;
447        self.cfg.write()?;
448        let (tools_data_map, to_be_opened_folder, read_cfg) =
449            detail::load(&prj_path).inspect_err(|_| {
450                self.cfg.unset_current_prj_path();
451                trace_ok_err(self.cfg.write());
452            })?;
453        if let Some(of) = to_be_opened_folder {
454            self.open_relative_folder(of)?;
455        }
456        self.cfg.prj = read_cfg;
457        // save cfg of loaded project
458        trace_ok_err(self.cfg.write());
459        Ok(tools_data_map)
460    }
461
462    fn wait_for_save(&mut self) {
463        if self.save_handle.is_some() {
464            mem::take(&mut self.save_handle).map(|h| trace_ok_err(h.join().map_err(to_rv)));
465        }
466    }
467    pub fn import_annos(&self, prj_path: &Path, tools_data_map: &mut ToolsDataMap) -> RvResult<()> {
468        tracing::info!("importing annotations from {prj_path:?}");
469        detail::import_annos(tools_data_map, prj_path)
470    }
471    pub fn import_settings(&mut self, prj_path: &Path) -> RvResult<()> {
472        tracing::info!("importing settings from {prj_path:?}");
473        let (_, opened_folder, prj_cfg) = detail::load(prj_path)?;
474
475        self.cfg.prj = prj_cfg;
476        let info = UserPrjOpened::new();
477        let filename = format!("{}_{info}_imported.rvi", to_stem_str(prj_path)?);
478        let prj_path_imported = prj_path
479            .parent()
480            .ok_or_else(|| rverr!("prj path needs parent folder"))?
481            .join(filename);
482        self.cfg.set_current_prj_path(prj_path_imported);
483        if let Some(of) = opened_folder {
484            self.open_relative_folder(of)?;
485        }
486        Ok(())
487    }
488    pub fn import_both(
489        &mut self,
490        prj_path: &Path,
491        tools_data_map: &mut ToolsDataMap,
492    ) -> RvResult<()> {
493        self.import_annos(prj_path, tools_data_map)?;
494        self.import_settings(prj_path)?;
495        Ok(())
496    }
497    pub fn import_from_coco(
498        &mut self,
499        coco_path: &str,
500        tools_data_map: &mut ToolsDataMap,
501        connection: ExportPathConnection,
502    ) -> RvResult<()> {
503        tracing::info!("importing from coco {coco_path:?}");
504
505        let meta_data = self.meta_data(None, None);
506        let path = ExportPath {
507            path: Path::new(coco_path).to_path_buf(),
508            conn: connection,
509        };
510        let (bbox_tool_data, brush_tool_data) = read_coco(&meta_data, &path, None)?;
511        let server_addresses = bbox_tool_data
512            .annotations_map
513            .keys()
514            .chain(brush_tool_data.annotations_map.keys())
515            .filter(|k| k.starts_with("http://"))
516            .flat_map(|k| k.rsplitn(2, '/').last())
517            .collect::<HashSet<_>>();
518        if !server_addresses.is_empty() {
519            self.cfg.prj.connection = Connection::PyHttp;
520
521            let server_addresses = server_addresses
522                .iter()
523                .map(|s| s.to_string())
524                .collect::<Vec<_>>();
525            self.cfg.prj.py_http_reader_cfg = Some(PyHttpReaderCfg { server_addresses });
526        }
527        let first_sa = server_addresses.iter().next().map(|s| s.to_string());
528        if let Some(sa) = first_sa {
529            self.open_relative_folder(sa.to_string())?;
530        }
531
532        tools_data_map.set_tools_specific_data(BRUSH_NAME, ToolSpecifics::Brush(brush_tool_data));
533        tools_data_map.set_tools_specific_data(BBOX_NAME, ToolSpecifics::Bbox(bbox_tool_data));
534        Ok(())
535    }
536
537    fn set_current_prj_path(&mut self, prj_path: PathBuf) -> RvResult<()> {
538        trace_ok_warn(detail::create_lock_file(&prj_path));
539        if prj_path != self.cfg.current_prj_path() {
540            trace_ok_warn(detail::remove_lock_file(self.cfg.current_prj_path()));
541        }
542        self.cfg.set_current_prj_path(prj_path);
543        Ok(())
544    }
545
546    pub fn save(
547        &mut self,
548        prj_path: PathBuf,
549        tools_data_map: &ToolsDataMap,
550        set_cur_prj: bool,
551    ) -> RvResult<()> {
552        tracing::info!("saving project to {prj_path:?}");
553        let path = if let Some(of) = self.opened_folder() {
554            if DEFAULT_PRJ_PATH.as_os_str() == prj_path.as_os_str() {
555                PathBuf::from(of.path_relative()).join(DEFAULT_PRJ_NAME)
556            } else {
557                prj_path.clone()
558            }
559        } else {
560            prj_path.clone()
561        };
562
563        if set_cur_prj {
564            self.set_current_prj_path(path.clone())?;
565            // update prj name in cfg
566            trace_ok_err(self.cfg.write());
567        }
568        let opened_folder = self.opened_folder().cloned();
569        let tdm = tools_data_map.clone();
570        let cfg = self.cfg.clone();
571        self.wait_for_save();
572        let handle = thread::spawn(move || {
573            trace_ok_err(detail::save(
574                opened_folder.as_ref().map(|of| of.path_relative()),
575                &tdm,
576                path.as_path(),
577                &cfg,
578            ));
579        });
580        self.save_handle = Some(handle);
581        Ok(())
582    }
583
584    pub fn new() -> Self {
585        let cfg = Cfg::read(&DEFAULT_HOMEDIR).unwrap_or_else(|e| {
586            tracing::warn!("could not read cfg due to {e:?}, returning default");
587            Cfg::default()
588        });
589        if cfg.current_prj_path().exists() {
590            trace_ok_warn(detail::create_lock_file(cfg.current_prj_path()));
591        }
592        trace_ok_warn(create_lock_file(cfg.current_prj_path()));
593        let mut tmp = Self::default();
594        tmp.cfg = cfg;
595        tmp
596    }
597    pub fn new_prj(&mut self) -> ToolsDataMap {
598        let mut cfg = Cfg::read(&DEFAULT_HOMEDIR).unwrap_or_else(|e| {
599            tracing::warn!("could not read cfg due to {e:?}, returning default");
600            Cfg::default()
601        });
602        trace_ok_warn(detail::remove_lock_file(self.cfg.current_prj_path()));
603        cfg.unset_current_prj_path();
604        *self = Control::default();
605        self.cfg = cfg;
606        ToolsDataMap::new()
607    }
608
609    pub fn reader(&self) -> Option<&ReaderFromCfg> {
610        self.reader.as_ref()
611    }
612
613    pub fn read_image(&mut self, file_label_selected_idx: usize) -> AsyncResultImage {
614        let wrapped_image = self.reader.as_mut().and_then(|r| {
615            self.paths_navigator.paths_selector().as_ref().map(|ps| {
616                let ffp = ps.filtered_abs_file_paths();
617                r.read_image(file_label_selected_idx, &ffp)
618            })
619        });
620        match wrapped_image {
621            None => Ok(None),
622            Some(x) => Ok(x?),
623        }
624    }
625
626    fn make_reader(&mut self, cfg: Cfg) -> RvResult<()> {
627        self.paths_navigator = PathsNavigator::new(None, SortParams::default())?;
628        self.last_open_folder_job_id = Some(
629            self.tp
630                .apply(Box::new(move || ReaderFromCfg::from_cfg(cfg)))?,
631        );
632        Ok(())
633    }
634
635    pub fn remake_reader(&mut self) -> RvResult<()> {
636        let cfg = self.cfg.clone();
637        self.last_open_folder_job_id = Some(
638            self.tp
639                .apply(Box::new(move || ReaderFromCfg::from_cfg(cfg)))?,
640        );
641        Ok(())
642    }
643
644    pub fn export_logs(&self, dst: &Path) -> RvResult<()> {
645        let homefolder = self.cfg.home_folder();
646        let log_folder = get_log_folder(Path::new(homefolder));
647        tracing::info!("exporting logs from {log_folder:?} to {dst:?}");
648        let elf = log_folder.clone();
649        let dst = dst.to_path_buf();
650        thread::spawn(move || {
651            // zip log folder
652            let mut zip = zip::ZipWriter::new(fs::File::create(&dst).unwrap());
653
654            let walkdir = WalkDir::new(elf);
655            let iter_log = walkdir.into_iter();
656            for entry in iter_log {
657                if let Some(entry) = trace_ok_err(entry) {
658                    let path = entry.path();
659                    if path.is_file() {
660                        let file_name = osstr_to_str(path.file_name());
661                        trace_ok_err(file_name).and_then(|file_name| {
662                            trace_ok_err(zip.start_file::<&str, ExtendedFileOptions>(
663                                file_name,
664                                zip::write::FileOptions::default(),
665                            ));
666                            trace_ok_err(fs::read(path))
667                                .and_then(|buf| trace_ok_err(zip.write_all(&buf)))
668                        });
669                    }
670                }
671            }
672        });
673        Ok(())
674    }
675
676    pub fn open_relative_folder(&mut self, new_folder: String) -> RvResult<()> {
677        tracing::info!("new opened folder {new_folder}");
678        self.make_reader(self.cfg.clone())?;
679        let current_prj_path = match self.cfg.prj.connection {
680            Connection::Local => Some(self.cfg.current_prj_path()),
681            _ => None,
682        };
683        self.opened_folder = Some(PathPair::from_relative_path(new_folder, current_prj_path));
684        Ok(())
685    }
686
687    pub fn load_opened_folder_content(&mut self, sort_params: SortParams) -> RvResult<()> {
688        if let (Some(opened_folder), Some(reader)) = (&self.opened_folder, &self.reader) {
689            let prj_folder = self.cfg.current_prj_path();
690            let selector = reader.open_folder(opened_folder.path_absolute(), prj_folder)?;
691            self.paths_navigator = PathsNavigator::new(Some(selector), sort_params)?;
692        }
693        Ok(())
694    }
695
696    pub fn check_if_connected(&mut self, sort_params: SortParams) -> RvResult<bool> {
697        if let Some(job_id) = self.last_open_folder_job_id {
698            let tp_res = self.tp.result(job_id);
699            if let Some(res) = tp_res {
700                self.last_open_folder_job_id = None;
701                res.and_then(|reader| {
702                    self.reader = Some(reader);
703                    self.load_opened_folder_content(sort_params)?;
704                    Ok(true)
705                })
706            } else {
707                Ok(false)
708            }
709        } else {
710            Ok(true)
711        }
712    }
713
714    pub fn opened_folder_label(&self) -> Option<&str> {
715        self.paths_navigator
716            .paths_selector()
717            .as_ref()
718            .map(|ps| ps.folder_label())
719    }
720
721    pub fn file_label(&self, idx: usize) -> &str {
722        match self.paths_navigator.paths_selector() {
723            Some(ps) => ps.filtered_idx_file_label_pairs(idx).1,
724            None => "",
725        }
726    }
727
728    pub fn cfg_of_opened_folder(&self) -> Option<&Cfg> {
729        self.reader().map(|r| r.cfg())
730    }
731
732    fn opened_folder(&self) -> Option<&PathPair> {
733        self.opened_folder.as_ref()
734    }
735
736    pub fn connection_data(&self) -> RvResult<ConnectionData> {
737        let cfg = self
738            .cfg_of_opened_folder()
739            .ok_or_else(|| RvError::new("save failed, open folder first"));
740        Ok(match self.cfg.prj.connection {
741            Connection::Ssh => {
742                let ssh_cfg = cfg.map(|cfg| cfg.ssh_cfg())?;
743                ConnectionData::Ssh(ssh_cfg)
744            }
745            Connection::Local => ConnectionData::None,
746            Connection::PyHttp => {
747                let pyhttp_cfg = cfg
748                    .map(|cfg| cfg.prj.py_http_reader_cfg.clone())?
749                    .ok_or_else(|| RvError::new("cannot open pyhttp without pyhttp cfg"))?;
750                ConnectionData::PyHttp(pyhttp_cfg)
751            }
752            #[cfg(feature = "azure_blob")]
753            Connection::AzureBlob => {
754                let azure_blob_cfg = cfg
755                    .map(|cfg| cfg.azure_blob_cfg())?
756                    .ok_or_else(|| RvError::new("cannot open azure blob without cfg"))?;
757                ConnectionData::AzureBlobCfg(azure_blob_cfg)
758            }
759        })
760    }
761
762    pub fn meta_data(
763        &self,
764        file_selected_idx: Option<usize>,
765        is_loading_screen_active: Option<bool>,
766    ) -> MetaData {
767        let file_path =
768            file_selected_idx.and_then(|fsidx| self.paths_navigator.file_path(fsidx).cloned());
769        let open_folder = self.opened_folder().cloned();
770        let connection_data = if self.reader.is_some() {
771            ConnectionData::Ssh(self.cfg.ssh_cfg())
772        } else {
773            ConnectionData::None
774        };
775        let export_folder = self
776            .cfg_of_opened_folder()
777            .map(|cfg| cfg.home_folder().to_string());
778        let is_file_list_empty = Some(file_path.is_none());
779        let prj_path = self.cfg.current_prj_path();
780        MetaData::new(
781            file_path,
782            file_selected_idx,
783            connection_data,
784            Some(self.cfg.ssh_cfg()),
785            open_folder,
786            export_folder,
787            MetaDataFlags {
788                is_loading_screen_active,
789                is_file_list_empty,
790            },
791            Some(prj_path.to_path_buf()),
792        )
793    }
794
795    pub fn redo(&mut self, history: &mut History) -> Option<(World, Option<usize>)> {
796        self.flags.undo_redo_load = true;
797        detail::idx_change_check(
798            self.file_selected_idx,
799            history.next_world(&self.opened_folder),
800        )
801    }
802    pub fn undo(&mut self, history: &mut History) -> Option<(World, Option<usize>)> {
803        self.flags.undo_redo_load = true;
804        detail::idx_change_check(
805            self.file_selected_idx,
806            history.prev_world(&self.opened_folder),
807        )
808    }
809
810    pub fn load_new_image_if_triggered(
811        &mut self,
812        world: &World,
813        history: &mut History,
814    ) -> RvResult<Option<(World, Option<usize>)>> {
815        measure_time!("load image if new", {
816            let menu_file_selected = measure_time!("before if", {
817                self.paths_navigator.file_label_selected_idx()
818            });
819            let world_idx_pair = if self.file_selected_idx != menu_file_selected
820                || self.flags.is_loading_screen_active
821            {
822                // load new image
823                if let Some(selected) = &menu_file_selected {
824                    let abs_file_path = menu_file_selected.and_then(|fs| {
825                        Some(
826                            self.paths_navigator
827                                .file_path(fs)?
828                                .path_absolute()
829                                .replace('\\', "/"),
830                        )
831                    });
832                    let im_read = self.read_image(*selected)?;
833                    let read_image_and_idx = match (abs_file_path, im_read) {
834                        (Some(fp), Some(ri)) => {
835                            tracing::info!("loading {} from {}", ri.info, fp);
836                            self.file_selected_idx = menu_file_selected;
837                            self.file_info_selected = Some(ri.info);
838                            let mut new_world = world.clone();
839                            new_world.set_background_image(ri.im);
840                            new_world.reset_updateview();
841
842                            if !self.flags.undo_redo_load {
843                                history.push(Record {
844                                    world: world.clone(),
845                                    actor: LOAD_ACTOR_NAME,
846                                    file_label_idx: self.file_selected_idx,
847                                    opened_folder: self
848                                        .opened_folder
849                                        .as_ref()
850                                        .map(|of| of.path_absolute().to_string()),
851                                });
852                            }
853                            self.flags.undo_redo_load = false;
854                            self.flags.is_loading_screen_active = false;
855                            (new_world, self.file_selected_idx)
856                        }
857                        _ => {
858                            thread::sleep(Duration::from_millis(2));
859
860                            tracing::debug!("still loading...");
861                            self.file_selected_idx = menu_file_selected;
862                            self.flags.is_loading_screen_active = true;
863                            let mut new_world = world.clone();
864
865                            detail::draw_loading_dots(
866                                new_world.data.im_background_mut(),
867                                self.loading_screen_animation_counter,
868                            );
869                            new_world.reset_updateview();
870                            (new_world, self.file_selected_idx)
871                        }
872                    };
873                    Some(read_image_and_idx)
874                } else {
875                    None
876                }
877            } else {
878                None
879            };
880            self.loading_screen_animation_counter += 1;
881            if self.loading_screen_animation_counter == u128::MAX {
882                self.loading_screen_animation_counter = 0;
883            }
884            Ok(world_idx_pair)
885        })
886    }
887}
888
889#[cfg(test)]
890use {
891    crate::{
892        file_util::DEFAULT_TMPDIR,
893        tools_data::{BboxToolData, ToolsData},
894    },
895    rvimage_domain::{make_test_bbs, ShapeI},
896    std::collections::HashMap,
897    std::str::FromStr,
898};
899#[cfg(test)]
900pub fn make_data(image_file: &Path) -> ToolsDataMap {
901    use crate::tools_data::VisibleInactiveToolsState;
902
903    let test_export_folder = DEFAULT_TMPDIR.clone();
904
905    match fs::create_dir(&test_export_folder) {
906        Ok(_) => (),
907        Err(e) => {
908            println!("{e:?}");
909        }
910    }
911
912    let mut bbox_data = BboxToolData::new();
913    bbox_data
914        .label_info
915        .push("x".to_string(), None, None)
916        .unwrap();
917    bbox_data
918        .label_info
919        .remove_catidx(0, &mut bbox_data.annotations_map);
920    let mut bbs = make_test_bbs();
921    bbs.extend(bbs.clone());
922    bbs.extend(bbs.clone());
923    bbs.extend(bbs.clone());
924    bbs.extend(bbs.clone());
925    bbs.extend(bbs.clone());
926    bbs.extend(bbs.clone());
927    bbs.extend(bbs.clone());
928
929    let annos = bbox_data.get_annos_mut(
930        image_file.as_os_str().to_str().unwrap(),
931        ShapeI::new(10, 10),
932    );
933    if let Some(a) = annos {
934        for bb in bbs {
935            a.add_bb(bb, 0, crate::InstanceLabelDisplay::IndexLr);
936        }
937    }
938
939    let data = HashMap::from([(
940        BBOX_NAME.to_string(),
941        ToolsData::new(
942            ToolSpecifics::Bbox(bbox_data),
943            VisibleInactiveToolsState::default(),
944        ),
945    )]);
946    ToolsDataMap::from(data)
947}
948
949impl Drop for Control {
950    fn drop(&mut self) {
951        trace_ok_warn(detail::remove_lock_file(self.cfg.current_prj_path()));
952    }
953}
954
955#[test]
956fn test_save_load() {
957    let tdm = make_data(&PathBuf::from_str("dummyfile").unwrap());
958    let cfg = {
959        let mut tmp = Cfg::default();
960        tmp.usr.n_autosaves = Some(59);
961        tmp
962    };
963    let opened_folder_name = "dummy_opened_folder";
964    let export_folder = cfg.tmpdir();
965    let export_file = PathBuf::new().join(export_folder).join("export.json");
966    let opened_folder = Some(opened_folder_name.to_string());
967    detail::save(opened_folder.as_deref(), &tdm, &export_file, &cfg).unwrap();
968
969    defer_file_removal!(&export_file);
970
971    let (tdm_imported, _, cfg_imported) = detail::load(&export_file).unwrap();
972    assert_eq!(tdm, tdm_imported);
973    assert_eq!(cfg.prj, cfg_imported);
974}