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