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