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