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