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 egui::ahash::HashSet;
21use image::imageops::FilterType;
22use image::{DynamicImage, ImageBuffer};
23use rvimage_domain::{RvError, RvResult, rverr, to_rv};
24use serde::{Deserialize, Serialize};
25use std::collections::HashMap;
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
682    fn make_reader(&mut self, cfg: Cfg) -> RvResult<()> {
683        self.paths_navigator = PathsNavigator::new(None, SortParams::default())?;
684        self.last_open_folder_job_id = Some(
685            self.tp
686                .apply(Box::new(move || ReaderFromCfg::from_cfg(cfg)))?,
687        );
688        Ok(())
689    }
690
691    pub fn remake_reader(&mut self) -> RvResult<()> {
692        let cfg = self.cfg.clone();
693        self.last_open_folder_job_id = Some(
694            self.tp
695                .apply(Box::new(move || ReaderFromCfg::from_cfg(cfg)))?,
696        );
697        Ok(())
698    }
699
700    pub fn start_wandserver(&mut self) -> RvResult<()> {
701        let ws = &self.cfg.prj.wand_server;
702        let mut wand_server = CmdServer::new(
703            ws.src.clone(),
704            ws.additional_files.clone(),
705            ws.setup_cmd.clone(),
706            ws.setup_args.clone(),
707            ws.install_uv,
708            ws.local_folder
709                .clone()
710                .unwrap_or(format!("{}/wand_server", self.cfg.home_folder())),
711        );
712        let cur_prj_path = self.cfg.current_prj_path();
713        wand_server.start_server(cur_prj_path)?;
714        self.wand_server = Some(wand_server);
715        Ok(())
716    }
717
718    pub fn stop_wandserver(&mut self) -> RvResult<()> {
719        if let Some(ws) = &mut self.wand_server {
720            tracing::info!("stopping wandserver...");
721            ws.stop_server()?;
722            tracing::info!("... done!");
723            Ok(())
724        } else {
725            Err(rverr!("Cannot stop wandserver, not running"))
726        }
727    }
728    pub fn cleanup_wandserver(&mut self) -> RvResult<()> {
729        if let Some(ws) = &mut self.wand_server {
730            tracing::info!("cleaning up wandserver...");
731            ws.cleanup_server()?;
732            tracing::info!("... cleaning up done.");
733            Ok(())
734        } else {
735            Err(rverr!("Cannot clean wandserver, not running"))
736        }?;
737        self.wand_server = None;
738        Ok(())
739    }
740
741    pub fn export_logs(&self, dst: &Path) -> RvResult<()> {
742        let homefolder = self.cfg.home_folder();
743        let log_folder = get_log_folder(Path::new(homefolder));
744        tracing::info!("exporting logs from {log_folder:?} to {dst:?}");
745        tracing::info!("RV Image version {}", version_label());
746        let elf = log_folder.clone();
747        let dst = dst.to_path_buf();
748        thread::spawn(move || {
749            // zip log folder
750            let mut zip = zip::ZipWriter::new(fs::File::create(&dst).unwrap());
751
752            let walkdir = WalkDir::new(elf);
753            let iter_log = walkdir.into_iter();
754            for entry in iter_log {
755                if let Some(entry) = trace_ok_err(entry) {
756                    let path = entry.path();
757                    if path.is_file() {
758                        let file_name = osstr_to_str(path.file_name());
759                        trace_ok_err(file_name).and_then(|file_name| {
760                            trace_ok_err(zip.start_file::<&str, ExtendedFileOptions>(
761                                file_name,
762                                zip::write::FileOptions::default(),
763                            ));
764                            trace_ok_err(fs::read(path))
765                                .and_then(|buf| trace_ok_err(zip.write_all(&buf)))
766                        });
767                    }
768                }
769            }
770        });
771        Ok(())
772    }
773
774    pub fn open_relative_folder(&mut self, new_folder: String) -> RvResult<()> {
775        tracing::info!("new opened folder {new_folder}");
776        self.thumbnail_cache.clear();
777        self.make_reader(self.cfg.clone())?;
778        let current_prj_path = match self.cfg.prj.connection {
779            Connection::Local => Some(self.cfg.current_prj_path()),
780            _ => None,
781        };
782        self.opened_folder = Some(PathPair::from_relative_path(new_folder, current_prj_path));
783        Ok(())
784    }
785
786    pub fn load_opened_folder_content(&mut self, sort_params: SortParams) -> RvResult<()> {
787        if let (Some(opened_folder), Some(reader)) = (&self.opened_folder, &self.reader) {
788            let prj_folder = self.cfg.current_prj_path();
789            let selector = reader.open_folder(opened_folder.path_absolute(), prj_folder)?;
790            self.paths_navigator = PathsNavigator::new(Some(selector), sort_params)?;
791        }
792        Ok(())
793    }
794
795    pub fn check_if_connected(&mut self, sort_params: SortParams) -> RvResult<bool> {
796        if let Some(job_id) = self.last_open_folder_job_id {
797            let tp_res = self.tp.result(job_id);
798            if let Some(res) = tp_res {
799                self.last_open_folder_job_id = None;
800                res.and_then(|reader| {
801                    self.reader = Some(reader);
802                    self.load_opened_folder_content(sort_params)?;
803                    Ok(true)
804                })
805            } else {
806                Ok(false)
807            }
808        } else {
809            Ok(true)
810        }
811    }
812
813    pub fn opened_folder_label(&self) -> Option<&str> {
814        self.paths_navigator
815            .paths_selector()
816            .as_ref()
817            .map(|ps| ps.folder_label())
818    }
819
820    pub fn file_label(&self, idx: usize) -> &str {
821        match self.paths_navigator.paths_selector() {
822            Some(ps) => ps.filtered_idx_file_label_pairs(idx).1,
823            None => "",
824        }
825    }
826
827    pub fn cfg_of_opened_folder(&self) -> Option<&Cfg> {
828        self.reader().map(|r| r.cfg())
829    }
830
831    fn opened_folder(&self) -> Option<&PathPair> {
832        self.opened_folder.as_ref()
833    }
834
835    pub fn connection_data(&self) -> RvResult<ConnectionData> {
836        let cfg = self
837            .cfg_of_opened_folder()
838            .ok_or_else(|| RvError::new("save failed, open folder first"));
839        Ok(match self.cfg.prj.connection {
840            Connection::Ssh => {
841                let ssh_cfg = cfg.map(|cfg| cfg.ssh_cfg())?;
842                ConnectionData::Ssh(ssh_cfg)
843            }
844            Connection::Local => ConnectionData::None,
845            Connection::PyHttp => {
846                let pyhttp_cfg = cfg
847                    .map(|cfg| cfg.prj.py_http_reader_cfg.clone())?
848                    .ok_or_else(|| RvError::new("cannot open pyhttp without pyhttp cfg"))?;
849                ConnectionData::PyHttp(pyhttp_cfg)
850            }
851            #[cfg(feature = "azure_blob")]
852            Connection::AzureBlob => {
853                let azure_blob_cfg = cfg
854                    .map(|cfg| cfg.azure_blob_cfg())?
855                    .ok_or_else(|| RvError::new("cannot open azure blob without cfg"))?;
856                ConnectionData::AzureBlobCfg(azure_blob_cfg)
857            }
858        })
859    }
860
861    pub fn meta_data(
862        &self,
863        file_selected_idx: Option<usize>,
864        is_loading_screen_active: Option<bool>,
865    ) -> MetaData {
866        let file_path =
867            file_selected_idx.and_then(|fsidx| self.paths_navigator.file_path(fsidx).cloned());
868        let open_folder = self.opened_folder().cloned();
869        let connection_data = if self.reader.is_some() {
870            ConnectionData::Ssh(self.cfg.ssh_cfg())
871        } else {
872            ConnectionData::None
873        };
874        let export_folder = self
875            .cfg_of_opened_folder()
876            .map(|cfg| cfg.home_folder().to_string());
877        let is_file_list_empty = Some(file_path.is_none());
878        let prj_path = self.cfg.current_prj_path();
879        MetaData::new(
880            file_path,
881            file_selected_idx,
882            connection_data,
883            Some(self.cfg.ssh_cfg()),
884            open_folder,
885            export_folder,
886            MetaDataFlags {
887                is_loading_screen_active,
888                is_file_list_empty,
889            },
890            Some(prj_path.to_path_buf()),
891        )
892    }
893
894    pub fn redo(&mut self, history: &mut History) -> Option<(World, Option<usize>)> {
895        self.flags.undo_redo_load = true;
896        detail::idx_change_check(
897            self.file_selected_idx,
898            history.next_world(&self.opened_folder),
899        )
900    }
901    pub fn undo(&mut self, history: &mut History) -> Option<(World, Option<usize>)> {
902        self.flags.undo_redo_load = true;
903        detail::idx_change_check(
904            self.file_selected_idx,
905            history.prev_world(&self.opened_folder),
906        )
907    }
908
909    fn get_path(&self, idx: usize) -> Option<String> {
910        trace_ok_err(
911            self.paths_navigator
912                .file_path(idx)
913                .map(|p| p.path_absolute().to_string())
914                .ok_or_else(|| rverr!("index does not have path")),
915        )
916    }
917
918    fn get_image_meta(&self, world: &World, idx: usize) -> Option<ImageMeta> {
919        let file_label = self.paths_navigator.paths_selector().map(|ps| {
920            let (_, file_label) = ps.filtered_idx_file_label_pairs(idx);
921            file_label
922        });
923
924        file_label.map(|file_label| {
925            let attrmap = world
926                .data
927                .tools_data_map
928                .get(ATTRIBUTES_NAME)
929                .and_then(|d| trace_ok_err(d.specifics.attributes()))
930                .and_then(|d| d.get_annos(file_label));
931            ImageMeta {
932                file_label: file_label.to_string(),
933                attrs: attrmap.cloned(),
934            }
935        })
936    }
937
938    fn load_thumbnails(&mut self, world: &World, start: usize, end: usize) -> Vec<ImageMetaPair> {
939        (start..end)
940            .flat_map(|idx| {
941                let path = self.get_path(idx);
942                let meta_data = self.get_image_meta(world, idx);
943
944                if let (Some(p), Some(meta)) = (path.as_deref(), meta_data) {
945                    let im = if let Some(im) = self.thumbnail_cache.get(p) {
946                        Some(im.clone())
947                    } else {
948                        let in_cache_im = self.read_cached_image(idx).map(|im| {
949                            im.and_then(|im| {
950                                let im_thumb = im.im.resize(200, 100, FilterType::Lanczos3);
951                                let im_rotated_thumb =
952                                    trace_ok_err(rotate90(world, im_thumb.clone(), p));
953                                im_rotated_thumb.inspect(|im| {
954                                    self.thumbnail_cache.insert(p.to_string(), im.clone());
955                                })
956                            })
957                        });
958                        trace_ok_err(in_cache_im.map(|im| {
959                            im.unwrap_or(DynamicImage::ImageRgb8(ImageBuffer::new(10, 10)))
960                        }))
961                    };
962                    im.map(|im| ImageMetaPair { im, meta })
963                } else {
964                    None
965                }
966            })
967            .collect::<Vec<_>>()
968    }
969
970    pub fn load_new_image_if_triggered(
971        &mut self,
972        world: &World,
973        history: &mut History,
974    ) -> RvResult<Option<(World, Option<usize>)>> {
975        measure_time!("load image if new", {
976            let menu_file_selected = measure_time!("before if", {
977                self.paths_navigator.file_label_selected_idx()
978            });
979            let world_idx_pair = if self.file_selected_idx != menu_file_selected
980                || self.flags.is_loading_screen_active
981            {
982                // load new image
983                if let Some(selected) = &menu_file_selected {
984                    let abs_file_path = menu_file_selected.and_then(|fs| {
985                        Some(
986                            self.paths_navigator
987                                .file_path(fs)?
988                                .path_absolute()
989                                .replace('\\', "/"),
990                        )
991                    });
992                    let im_read = self.read_image(*selected)?;
993                    let meta = self.get_image_meta(world, *selected);
994                    tracing::debug!("meta {}", meta.is_some());
995                    tracing::debug!("im_read {}", im_read.is_some());
996                    tracing::debug!("abs_file_path {}", abs_file_path.is_some());
997                    let new_world_idx_pair = match (abs_file_path, im_read, meta) {
998                        (Some(fp), Some(ri), Some(meta)) => {
999                            tracing::info!("loading {} from {}", ri.info, fp);
1000                            let im = ri.im;
1001                            let im = ImageMetaPair { im, meta };
1002                            self.file_selected_idx = menu_file_selected;
1003                            self.file_info_selected = Some(ri.info);
1004                            let mut new_world = world.clone();
1005                            let prev_start = if *selected > self.cfg.usr.n_prev_thumbs {
1006                                selected - self.cfg.usr.n_prev_thumbs
1007                            } else {
1008                                0
1009                            };
1010                            let prev_images =
1011                                self.load_thumbnails(&new_world, prev_start, *selected);
1012                            let n = self.paths_navigator.len_filtered().unwrap_or(0);
1013                            let next_end = if n > *selected + 1 + self.cfg.usr.n_next_thumbs {
1014                                selected + 1 + self.cfg.usr.n_prev_thumbs
1015                            } else {
1016                                n
1017                            };
1018                            let next_images =
1019                                self.load_thumbnails(&new_world, *selected + 1, next_end);
1020                            let extra_ims = ThumbIms::new(
1021                                prev_images,
1022                                next_images,
1023                                Some(&im),
1024                                self.cfg.usr.thumb_w_max,
1025                                self.cfg.usr.thumb_h_max,
1026                            );
1027                            new_world.set_extra_images(extra_ims);
1028                            new_world.set_background_image(im);
1029                            new_world.reset_updateview();
1030
1031                            if !self.flags.undo_redo_load {
1032                                history.push(Record {
1033                                    world: world.clone(),
1034                                    actor: LOAD_ACTOR_NAME,
1035                                    file_label_idx: self.file_selected_idx,
1036                                    opened_folder: self
1037                                        .opened_folder
1038                                        .as_ref()
1039                                        .map(|of| of.path_absolute().to_string()),
1040                                });
1041                            }
1042                            self.flags.undo_redo_load = false;
1043                            self.flags.is_loading_screen_active = false;
1044                            (new_world, self.file_selected_idx)
1045                        }
1046                        _ => {
1047                            thread::sleep(Duration::from_millis(2));
1048
1049                            tracing::debug!("still loading...");
1050                            self.file_selected_idx = menu_file_selected;
1051                            self.flags.is_loading_screen_active = true;
1052                            let mut new_world = world.clone();
1053
1054                            detail::draw_loading_dots(
1055                                new_world.data.im_background_mut(),
1056                                self.loading_screen_animation_counter,
1057                            );
1058                            new_world.reset_updateview();
1059                            (new_world, self.file_selected_idx)
1060                        }
1061                    };
1062                    Some(new_world_idx_pair)
1063                } else {
1064                    None
1065                }
1066            } else {
1067                None
1068            };
1069            self.loading_screen_animation_counter += 1;
1070            if self.loading_screen_animation_counter == u128::MAX {
1071                self.loading_screen_animation_counter = 0;
1072            }
1073            Ok(world_idx_pair)
1074        })
1075    }
1076}
1077
1078#[cfg(test)]
1079use {
1080    crate::{
1081        file_util::DEFAULT_TMPDIR,
1082        tools_data::{BboxToolData, ToolsData},
1083    },
1084    rvimage_domain::{ShapeI, make_test_bbs},
1085    std::str::FromStr,
1086};
1087#[cfg(test)]
1088pub fn make_data(image_file: &Path) -> ToolsDataMap {
1089    use crate::tools_data::VisibleInactiveToolsState;
1090
1091    let test_export_folder = DEFAULT_TMPDIR.clone();
1092
1093    match fs::create_dir(&test_export_folder) {
1094        Ok(_) => (),
1095        Err(e) => {
1096            println!("{e:?}");
1097        }
1098    }
1099
1100    let mut bbox_data = BboxToolData::new();
1101    bbox_data
1102        .label_info
1103        .push("x".to_string(), None, None)
1104        .unwrap();
1105    bbox_data
1106        .label_info
1107        .remove_catidx(0, &mut bbox_data.annotations_map);
1108    let mut bbs = make_test_bbs();
1109    bbs.extend(bbs.clone());
1110    bbs.extend(bbs.clone());
1111    bbs.extend(bbs.clone());
1112    bbs.extend(bbs.clone());
1113    bbs.extend(bbs.clone());
1114    bbs.extend(bbs.clone());
1115    bbs.extend(bbs.clone());
1116
1117    let annos = bbox_data.get_annos_mut(
1118        image_file.as_os_str().to_str().unwrap(),
1119        ShapeI::new(10, 10),
1120    );
1121    if let Some(a) = annos {
1122        for bb in bbs {
1123            a.add_bb(bb, 0, crate::InstanceLabelDisplay::IndexLr);
1124        }
1125    }
1126
1127    let data = HashMap::from([(
1128        BBOX_NAME.to_string(),
1129        ToolsData::new(
1130            ToolSpecifics::Bbox(bbox_data),
1131            VisibleInactiveToolsState::default(),
1132        ),
1133    )]);
1134    ToolsDataMap::from(data)
1135}
1136
1137impl Drop for Control {
1138    fn drop(&mut self) {
1139        trace_ok_warn(detail::remove_lock_file(self.cfg.current_prj_path()));
1140    }
1141}
1142
1143#[test]
1144fn test_save_load() {
1145    let tdm = make_data(&PathBuf::from_str("dummyfile").unwrap());
1146    let cfg = {
1147        let mut tmp = Cfg::default();
1148        tmp.usr.n_autosaves = Some(59);
1149        tmp
1150    };
1151    let opened_folder_name = "dummy_opened_folder";
1152    let export_folder = cfg.tmpdir();
1153    let export_file = PathBuf::new().join(export_folder).join("export.json");
1154    let opened_folder = Some(opened_folder_name.to_string());
1155    detail::save(opened_folder.as_deref(), &tdm, &export_file, &cfg).unwrap();
1156
1157    defer_file_removal!(&export_file);
1158
1159    let (tdm_imported, _, cfg_imported) = detail::load(&export_file).unwrap();
1160    assert_eq!(tdm, tdm_imported);
1161    assert_eq!(cfg.prj, cfg_imported);
1162}