diff --git a/OTGroundTruther/gui/frame_treeview_counts.py b/OTGroundTruther/gui/frame_treeview_counts.py index e291241..215f574 100644 --- a/OTGroundTruther/gui/frame_treeview_counts.py +++ b/OTGroundTruther/gui/frame_treeview_counts.py @@ -202,6 +202,8 @@ def add_next_column_sort_direction(self): def refresh_treeview(self, count_repository: CountRepository) -> None: self.delete(*self.get_children()) + if not count_repository.get_all_as_dict(): + return selected_classes = self._presenter.get_selected_classes_from_gui() for count in list(count_repository.get_all_as_dict().values()): if count.get_road_user_class().get_name() in selected_classes: diff --git a/OTGroundTruther/gui/gui.py b/OTGroundTruther/gui/gui.py index b0c2bf1..2c42db3 100644 --- a/OTGroundTruther/gui/gui.py +++ b/OTGroundTruther/gui/gui.py @@ -1,6 +1,7 @@ from typing import Any import customtkinter as ctk +from CTkMessagebox import CTkMessagebox from OTGroundTruther.gui.constants import PADX, PADY, tk_events from OTGroundTruther.gui.frame_canvas import FrameCanvas @@ -12,6 +13,32 @@ TITLE: str = "OTGroundTruther" DELETE_BUTTON_TXT: str = "Delete" +SUBW_KEEPEXISTINGCOUNTS_TITLE: str = "Keep existing Counts?" +SUBW_KEEPEXISTINGCOUNTS_QUESTION: str = ( + "Do you want to keep the already existing counts and sections?" +) +SUBW_KEEPEXISTINGCOUNTS_ICON: str = "question" +SUBW_KEEPEXISTINGCOUNTS_KEEP: str = "Yes" +SUBW_KEEPEXISTINGCOUNTS_CLEAR: str = "No" + +SUBW_ENTERSUFFIXCOUNTS_TITLE: str = "Suffix for the counts of the file" +SUBW_ENTERSUFFIXCOUNTS_INSTRUCTION: str = "Enter a suffix for the counts of the file." + + +SUBW_SECTIONS_NOT_COMPATIBLE_TITLE: str = "Sections are not compatible." +SUBW_SECTIONS_NOT_COMPATIBLE_INFO: str = ( + "The sections from the file are not compatible with the existing " + + "sections. Therefore the existing sections and counts got deleted." +) +SUBW_SECTIONS_NOT_COMPATIBLE_ICON: str = "info" + +SUBW_COUNTS_NOT_COMPATIBLE_TITLE: str = "Counts are not compatible." +SUBW_COUNTS_NOT_COMPATIBLE_INFO: str = ( + "Duplication of at least one counting ID. All already existing" + + " counts have been removed." +) +SUBW_COUNTS_NOT_COMPATIBLE_ICON: str = "info" + class Gui(ctk.CTk): def __init__(self, presenter: PresenterInterface, **kwargs: Any) -> None: @@ -46,13 +73,51 @@ def _place_widgets(self) -> None: side=ctk.RIGHT, fill=ctk.BOTH, expand=True, padx=PADX, pady=PADY ) - def build_key_assignment_window(self, key_assignment_text: dict[str, str]): + def build_key_assignment_window(self, key_assignment_text: dict[str, str]) -> None: self.key_assigment_window = KeyAssignmentWindow( master=self, key_assignment_text=key_assignment_text, presenter=self._presenter, ) + def ask_if_keep_existing_counts(self) -> bool: + msg = CTkMessagebox( + master=self, + title=SUBW_KEEPEXISTINGCOUNTS_TITLE, + message=SUBW_KEEPEXISTINGCOUNTS_QUESTION, + icon=SUBW_KEEPEXISTINGCOUNTS_ICON, + option_1=SUBW_KEEPEXISTINGCOUNTS_CLEAR, + option_2=SUBW_KEEPEXISTINGCOUNTS_KEEP, + ) + keep_existing = msg.get() + if keep_existing == SUBW_KEEPEXISTINGCOUNTS_KEEP: + return True + else: + return False + + def get_new_suffix_for_new_counts(self) -> None: + self.subwindow = EnteringStringSubwindow( + self, + title=SUBW_ENTERSUFFIXCOUNTS_TITLE, + label=SUBW_ENTERSUFFIXCOUNTS_INSTRUCTION, + ) + + def inform_user_sections_not_compatible(self) -> None: + self.subwindow = CTkMessagebox( + master=self, + title=SUBW_SECTIONS_NOT_COMPATIBLE_TITLE, + message=SUBW_SECTIONS_NOT_COMPATIBLE_INFO, + icon=SUBW_SECTIONS_NOT_COMPATIBLE_ICON, + ) + + def inform_user_counts_not_compatible(self) -> None: + self.subwindow = CTkMessagebox( + master=self, + title=SUBW_COUNTS_NOT_COMPATIBLE_TITLE, + message=SUBW_COUNTS_NOT_COMPATIBLE_INFO, + icon=SUBW_COUNTS_NOT_COMPATIBLE_ICON, + ) + class GuiEventTranslator: def __init__(self, gui: Gui, presenter: PresenterInterface): @@ -66,3 +131,28 @@ def _bind_events(self) -> None: def _on_delete_key(self, event: Any) -> None: self._presenter.delete_selected_counts() + + +class EnteringStringSubwindow(ctk.CTkToplevel): + def __init__(self, gui: Gui, title: str, label: str) -> None: + super().__init__(gui) + self._gui = gui + self.title(title) + ctk.CTkLabel(self, text=label).pack(pady=0) + self.entry = ctk.CTkEntry(self) + self.entry.pack(pady=5) + ctk.CTkButton(self, text="OK", command=self._get_suffix_and_add_counts).pack( + pady=10 + ) + self.protocol("WM_DELETE_WINDOW", self.on_close) + self.transient(gui) + self.grab_set() + gui.wait_window(self) + + def _get_suffix_and_add_counts(self) -> None: + user_input = self.entry.get() + self.destroy() + self._gui._presenter.add_new_counts(keep_existing_s_c=True, suffix=user_input) + + def on_close(self): + self.destroy() diff --git a/OTGroundTruther/gui/menu.py b/OTGroundTruther/gui/menu.py index 52ccfb5..aa8b103 100644 --- a/OTGroundTruther/gui/menu.py +++ b/OTGroundTruther/gui/menu.py @@ -29,7 +29,9 @@ def __init__(self, presenter: PresenterInterface, **kwargs: Any): def _get_and_place_widgets(self) -> None: self.add_command(label="Load Videos", command=self._presenter.load_video_files) self.add_command(label="Load otflow", command=self._presenter.load_otflow) - self.add_command(label="Load Events", command=self._presenter.load_events) + self.add_command( + label="Load Events", command=self._presenter.load_otevents_otgtevents + ) self.add_command(label="Save Events", command=self._presenter.save_events) diff --git a/OTGroundTruther/gui/presenter_interface.py b/OTGroundTruther/gui/presenter_interface.py index fd7d1be..925f864 100644 --- a/OTGroundTruther/gui/presenter_interface.py +++ b/OTGroundTruther/gui/presenter_interface.py @@ -15,7 +15,11 @@ def load_otflow(self) -> None: raise NotImplementedError @abstractmethod - def load_events(self) -> None: + def load_otevents_otgtevents(self) -> None: + raise NotImplementedError + + @abstractmethod + def add_new_counts(self, keep_existing_s_c: bool, suffix: str) -> None: raise NotImplementedError @abstractmethod diff --git a/OTGroundTruther/model/count.py b/OTGroundTruther/model/count.py index 9d6bef2..8d3ffd4 100644 --- a/OTGroundTruther/model/count.py +++ b/OTGroundTruther/model/count.py @@ -287,7 +287,11 @@ def to_event_list(self) -> list[EventForParsingSerializing]: event_list.append(event_for_save) return event_list - def from_event_list(self, event_list: list[EventForParsingSerializing]) -> None: + def event_list_to_count_dict( + self, + event_list: list[EventForParsingSerializing], + suffix: str, + ) -> tuple[bool, dict[str, Count]]: """ create count list from event list and the suitable list of the object ids set current id = 0 (only important for id naming of new events later) if the @@ -297,20 +301,33 @@ def from_event_list(self, event_list: list[EventForParsingSerializing]) -> None: event_list (list[Event_For_Saving]): List of events. """ events, classes = self._get_events_and_classes_by_id(event_list) - - self.clear() + counts = {} for id_ in events.keys(): if len(events[id_]) >= 2: - self._counts[id_] = Count( - road_user_id=id_, + counts[id_ + suffix] = Count( + road_user_id=id_ + suffix, events=events[id_], road_user_class=classes[id_], ) else: continue # TODO: Store in "SingleEventRepository" + compatible = not self.ids_already_exist_in_counts(counts=counts) + return compatible, counts + + def add_new_counts( + self, + new_counts: dict[str, Count], + keep_existing_counts: bool, + ) -> None: + if not keep_existing_counts: + self.clear() + self._counts = self._counts | new_counts if len(self._counts.keys()) > 0: self.set_current_id(list(self._counts.keys())[-1]) + def ids_already_exist_in_counts(self, counts: dict[str, Count]): + return bool(set(counts.keys()) & set(self._counts.keys())) + def _get_events_and_classes_by_id( self, event_list: list[EventForParsingSerializing] ) -> tuple[dict[str, list[Event]], dict[str, RoadUserClass]]: diff --git a/OTGroundTruther/model/model.py b/OTGroundTruther/model/model.py index 4efd26e..90938f6 100644 --- a/OTGroundTruther/model/model.py +++ b/OTGroundTruther/model/model.py @@ -1,5 +1,6 @@ import datetime as dt from pathlib import Path +from typing import Iterable, Sequence from OTGroundTruther.gui.constants import tk_events from OTGroundTruther.gui.key_assignment import ( @@ -65,21 +66,40 @@ def load_videos_from_files(self, files: list[Path]): self._video_repository.add_all(videos) print(f"Videos loaded: {files}") - def read_sections_from_file(self, file: Path) -> None: - self._section_repository.clear() + def read_sections_from_file(self, file: Path) -> tuple[bool, Sequence[LineSection]]: + self.last_file_path = file sections, otanalytics_file_content = self._section_parser.parse(file=file) - self._section_repository.add_all(sections) self._section_repository.set_otanalytics_file_content(otanalytics_file_content) - print(f"Sections read from {file}") + if self._count_repository.get_all_as_dict(): + print(f"Check compatibility sections {file}") + compatible, gates_already_existed = ( + self._section_repository.check_if_compatible(sections) + ) + else: + compatible = True + return compatible, sections - def read_events_from_file(self, file: Path) -> None: + def add_sections( + self, sections: Iterable[LineSection], keep_existing_sections: bool + ): + if not keep_existing_sections: + self._section_repository.clear() + self._section_repository.add_all(sections) + print("Sections added") + + def read_events_from_file(self, suffix: str) -> tuple[bool, dict[str, Count]]: event_list = self._eventlistparser.parse( - events_file=file, + events_file=self.last_file_path, sections=self._section_repository.to_dict(), valid_road_user_classes=self._valid_road_user_classes, ) - self._count_repository.from_event_list(event_list) - print(f"Events read from {file}") + + (compatible, counts) = self._count_repository.event_list_to_count_dict( + event_list=event_list, + suffix=suffix, + ) + print(f"Events read from {self.last_file_path}") + return (compatible, counts) def write_events_and_sections_to_file( self, diff --git a/OTGroundTruther/model/section.py b/OTGroundTruther/model/section.py index f45e00a..8a231f2 100644 --- a/OTGroundTruther/model/section.py +++ b/OTGroundTruther/model/section.py @@ -122,6 +122,23 @@ def __init__(self) -> None: self._sections: dict[str, LineSection] = {} self._otanalytics_file_content: dict = {} + def check_if_compatible( + self, sections: Iterable[LineSection] + ) -> tuple[bool, dict[str, dict[str, bool]]]: + gates_already_existed = {} + compatible = True + for section in sections: + + id_already_exist = section.id in list(self._sections.keys()) + is_equal = id_already_exist and self._sections[section.id] == section + if id_already_exist and not is_equal: + compatible = False + gates_already_existed[section.id] = { + "id_already_exist": id_already_exist, + "is_equal": is_equal, + } + return (compatible, gates_already_existed) + def add_all(self, sections: Iterable[LineSection]) -> None: """Add several sections at once to the repository. @@ -130,9 +147,10 @@ def add_all(self, sections: Iterable[LineSection]) -> None: """ for section in sections: self._add(section) + # TODO: Check if ellipses around different sections touch each other - def _add(self, section: LineSection) -> None: + def _add(self, section: LineSection): """Internal method to add sections without notifying observers. Args: diff --git a/OTGroundTruther/presenter/model_initializer.py b/OTGroundTruther/presenter/model_initializer.py index 3e99b07..df73e06 100644 --- a/OTGroundTruther/presenter/model_initializer.py +++ b/OTGroundTruther/presenter/model_initializer.py @@ -60,7 +60,11 @@ def _prefill_count_repository(self, file: Path) -> None: sections=self._section_repository.to_dict(), valid_road_user_classes=self._valid_road_user_classes, ) - self._count_repository.from_event_list(event_list) + (compatible, counts) = self._count_repository.event_list_to_count_dict( + event_list=event_list, + suffix="", + ) + self._count_repository.add_new_counts(counts, keep_existing_counts=False) def get(self) -> Model: return self._model diff --git a/OTGroundTruther/presenter/presenter.py b/OTGroundTruther/presenter/presenter.py index dc5a4fa..49085fd 100644 --- a/OTGroundTruther/presenter/presenter.py +++ b/OTGroundTruther/presenter/presenter.py @@ -67,13 +67,15 @@ def load_otflow(self) -> None: ("otevents", f"*{OTANALYTICS_FILE_SUFFIX}"), ], ) - if output_askfile: - self._model.read_sections_from_file(Path(output_askfile)) - if self._current_frame is None: - return - self._refresh_current_frame() + if not output_askfile: + return + self._read_compare_add_sections(output_askfile) + self.refresh_treeview() + if self._current_frame is None: + return + self._refresh_current_frame() - def load_events(self) -> None: + def load_otevents_otgtevents(self) -> None: output_askfile = askopenfilename( defaultextension=f"*{GROUND_TRUTH_EVENTS_FILE_SUFFIX}", filetypes=[ @@ -81,13 +83,51 @@ def load_events(self) -> None: ("otevents", f"*{OTEVENTS_FILE_SUFFIX}"), ], ) - if output_askfile: - self._model.read_sections_from_file(Path(output_askfile)) - self._model.read_events_from_file(Path(output_askfile)) - self.refresh_treeview() - if self._current_frame is None: - return - self._refresh_current_frame() + if not output_askfile: + return + keep_existing_s_c = self._read_compare_add_sections(output_askfile) + if keep_existing_s_c: + self._gui.get_new_suffix_for_new_counts() + else: + self.add_new_counts(keep_existing_s_c, suffix="") + + def _read_compare_add_sections(self, output_askfile: str) -> bool: + ( + sections_compatible, + new_sections, + ) = self._model.read_sections_from_file(Path(output_askfile)) + + if sections_compatible and self.counts_or_sections_already_exist(): + keep_existing_s_c = self._gui.ask_if_keep_existing_counts() + elif not sections_compatible and self.counts_or_sections_already_exist(): + self._gui.inform_user_sections_not_compatible() + keep_existing_s_c = False + else: + keep_existing_s_c = False + + self._model.add_sections( + sections=new_sections, keep_existing_sections=keep_existing_s_c + ) + + return keep_existing_s_c + + def add_new_counts(self, keep_existing_s_c: bool, suffix: str) -> None: + counts_compatible, new_counts = self._model.read_events_from_file(suffix=suffix) + if not counts_compatible and keep_existing_s_c: + self._gui.inform_user_counts_not_compatible() + keep_existing_s_c = False + self._model._count_repository.add_new_counts( + new_counts, keep_existing_counts=keep_existing_s_c + ) + self.refresh_treeview() + if self._current_frame is None: + return + self._refresh_current_frame() + + def counts_or_sections_already_exist(self) -> bool: + return bool(self._model._count_repository.get_all_as_dict()) or bool( + self._model._section_repository.to_dict() + ) def save_events(self) -> None: first_video_file = ( @@ -254,3 +294,7 @@ def show_key_assignment(self) -> None: self._gui.build_key_assignment_window( key_assignment_text=self._model.get_key_assignment_text() ) + + def later_use(self) -> None: + + self.update_canvas_image_with_new_overlay() diff --git a/requirements.txt b/requirements.txt index 15a0e96..58ae553 100644 Binary files a/requirements.txt and b/requirements.txt differ