rvlib/control/
mod.rs

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