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