Skip to main content

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