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