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