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