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