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