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