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