Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feature/3875-serializ…
Browse files Browse the repository at this point in the history
…e-new-otconfig

# Conflicts:
#	OTAnalytics/application/config.py
#	OTAnalytics/plugin_ui/main_application.py
  • Loading branch information
briemla committed May 21, 2024
2 parents 1e358ad + 01500fd commit f258ec7
Show file tree
Hide file tree
Showing 11 changed files with 392 additions and 44 deletions.
4 changes: 4 additions & 0 deletions OTAnalytics/adapter_ui/view_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,3 +417,7 @@ def get_weather_types(self) -> ColumnResources:
@abstractmethod
def set_svz_metadata_frame(self, frame: AbstractFrameSvzMetadata) -> None:
raise NotImplementedError

@abstractmethod
def get_save_path_suggestion(self, file_type: str, context_file_type: str) -> Path:
raise NotImplementedError
27 changes: 27 additions & 0 deletions OTAnalytics/application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
GetSectionsById,
)
from OTAnalytics.application.use_cases.start_new_project import StartNewProject
from OTAnalytics.application.use_cases.suggest_save_path import SavePathSuggester
from OTAnalytics.application.use_cases.track_repository import (
GetAllTrackFiles,
TrackRepositorySize,
Expand Down Expand Up @@ -129,6 +130,7 @@ def __init__(
load_otconfig: LoadOtconfig,
config_has_changed: ConfigHasChanged,
export_road_user_assignments: ExportRoadUserAssignments,
file_name_suggester: SavePathSuggester,
) -> None:
self._datastore: Datastore = datastore
self.track_state: TrackState = track_state
Expand Down Expand Up @@ -168,6 +170,7 @@ def __init__(
self._load_otconfig = load_otconfig
self._config_has_changed = config_has_changed
self._export_road_user_assignments = export_road_user_assignments
self._file_name_suggester = file_name_suggester

def connect_observers(self) -> None:
"""
Expand Down Expand Up @@ -640,6 +643,30 @@ def get_road_user_export_formats(
) -> Iterable[ExportFormat]:
return self._export_road_user_assignments.get_supported_formats()

def suggest_save_path(self, file_type: str, context_file_type: str = "") -> Path:
"""Suggests a save path based on the given file type and an optional
related file type.
The suggested path is in the following format:
<BASE FOLDER>/<FILE STEM>.<CONTEXT FILE TYPE>.<FILE TYPE>
The base folder will be determined in the following precedence:
1. First loaded config file (otconfig or otflow)
2. First loaded track file (ottrk)
3. First loaded video file
4. Default: Current working directory
The file stem suggestion will be determined in the following precedence:
1. The file stem of the loaded config file (otconfig or otflow)
2. <CURRENT PROJECT NAME>_<CURRENT DATE AND TIME>
3. Default: <CURRENT DATE AND TIME>
Args:
file_type (str): the file type.
context_file_type (str): the context file type.
"""
return self._file_name_suggester.suggest(file_type, context_file_type)


class MissingTracksError(Exception):
pass
10 changes: 9 additions & 1 deletion OTAnalytics/application/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
CLI_CUTTING_SECTION_MARKER: str = "#clicut"
DEFAULT_EVENTLIST_FILE_STEM: str = "events"
DEFAULT_EVENTLIST_FILE_TYPE: str = "otevents"
DEFAULT_COUNTS_FILE_STEM: str = "counts"
DEFAULT_COUNTS_FILE_TYPE: str = "csv"
DEFAULT_COUNT_INTERVAL_TIME_UNIT: str = "min"
DEFAULT_TRACK_FILE_TYPE: str = "ottrk"
Expand All @@ -23,6 +22,15 @@
DEFAULT_PROGRESSBAR_STEP_PERCENTAGE: int = 5
DEFAULT_NUM_PROCESSES = 4


# File Types
CONTEXT_FILE_TYPE_ROAD_USER_ASSIGNMENTS = "road_user_assignments"
CONTEXT_FILE_TYPE_EVENTS = "events"
CONTEXT_FILE_TYPE_COUNTS = "counts"
OTCONFIG_FILE_TYPE = "otconfig"
OTFLOW_FILE_TYPE = "otflow"


# OTConfig Default Values
DEFAULT_DO_EVENTS = True
DEFAULT_DO_COUNTING = True
Expand Down
113 changes: 113 additions & 0 deletions OTAnalytics/application/use_cases/suggest_save_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from datetime import datetime
from pathlib import Path
from typing import Callable

from OTAnalytics.application.state import FileState
from OTAnalytics.application.use_cases.get_current_project import GetCurrentProject
from OTAnalytics.application.use_cases.track_repository import GetAllTrackFiles
from OTAnalytics.application.use_cases.video_repository import GetAllVideos

DATETIME_FORMAT = "%Y-%m-%d_%H-%M-%S"


class SavePathSuggester:
"""
Class for suggesting save paths based on the config file, otflow file,
the first track file, and video file.
Args:
file_state (FileState): Holds information on files loaded in application.
get_all_track_files (GetAllTrackFiles): A use case that retrieves
all track files.
get_all_videos (GetAllVideos): A use case that retrieves all
video files.
get_project (GetCurrentProject): A use case that retrieves
the current project.
"""

@property
def __config_file(self) -> Path | None:
"""The path to the last loaded or saved configuration file."""
if config_file := self._file_state.last_saved_config.get():
return config_file.file
return None

@property
def __first_track_file(self) -> Path | None:
"""The path to the first track file."""

if track_files := self._get_all_track_files():
return next(iter(track_files))
return None

@property
def __first_video_file(self) -> Path | None:
"""The path to the first video file."""

if video_files := self._get_all_videos.get():
return video_files[0].get_path()
return None

def __init__(
self,
file_state: FileState,
get_all_track_files: GetAllTrackFiles,
get_all_videos: GetAllVideos,
get_project: GetCurrentProject,
provide_datetime: Callable[[], datetime] = datetime.now,
) -> None:
self._file_state = file_state
self._get_all_track_files = get_all_track_files
self._get_all_videos = get_all_videos
self._get_project = get_project
self._provide_datetime = provide_datetime

def suggest(self, file_type: str, context_file_type: str = "") -> Path:
"""Suggests a save path based on the given file type and an optional
related file type.
The suggested path is in the following format:
<BASE FOLDER>/<FILE STEM>.<CONTEXT FILE TYPE>.<FILE TYPE>
The base folder will be determined in the following precedence:
1. First loaded config file (otconfig or otflow)
2. First loaded track file (ottrk)
3. First loaded video file
4. Default: Current working directory
The file stem suggestion will be determined in the following precedence:
1. The file stem of the loaded config file (otconfig or otflow)
2. <CURRENT PROJECT NAME>_<CURRENT DATE AND TIME>
3. Default: <CURRENT DATE AND TIME>
Args:
file_type (str): the file type.
context_file_type (str): the context file type.
"""

base_folder = self._retrieve_base_folder()
file_stem = self._suggest_file_stem()
if context_file_type:
return base_folder / f"{file_stem}.{context_file_type}.{file_type}"
return base_folder / f"{file_stem}.{file_type}"

def _retrieve_base_folder(self) -> Path:
"""Returns the base folder for suggesting a new file name."""
if self.__config_file:
return self.__config_file.parent
if self.__first_track_file:
return self.__first_track_file.parent
if self.__first_video_file:
return self.__first_video_file.parent
return Path.cwd()

def _suggest_file_stem(self) -> str:
"""Generates a suggestion for the file stem."""

if self.__config_file:
return f"{self.__config_file.stem}"

current_time = self._provide_datetime().strftime(DATETIME_FORMAT)
if project_name := self._get_project.get().name:
return f"{project_name}_{current_time}"
return current_time
9 changes: 6 additions & 3 deletions OTAnalytics/plugin_ui/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
CountingSpecificationDto,
)
from OTAnalytics.application.config import (
CONTEXT_FILE_TYPE_COUNTS,
CONTEXT_FILE_TYPE_ROAD_USER_ASSIGNMENTS,
DEFAULT_COUNT_INTERVAL_TIME_UNIT,
DEFAULT_COUNTS_FILE_STEM,
DEFAULT_COUNTS_FILE_TYPE,
DEFAULT_SECTIONS_FILE_TYPE,
DEFAULT_TRACK_FILE_TYPE,
Expand Down Expand Up @@ -244,7 +245,9 @@ def _export_events(self, sections: Iterable[Section], save_path: Path) -> None:
event_list_exporter.export(events, sections, actual_save_path)
logger().info(f"Event list saved at '{actual_save_path}'")

assignment_path = save_path.with_suffix(".road_user_assignment.csv")
assignment_path = save_path.with_suffix(
f".{CONTEXT_FILE_TYPE_ROAD_USER_ASSIGNMENTS}.csv"
)
specification = ExportSpecification(
save_path=assignment_path, format=CSV_FORMAT.name
)
Expand All @@ -267,7 +270,7 @@ def _do_export_counts(self, save_path: Path) -> None:
raise ValueError("modes is None but has to be defined for exporting counts")
for count_interval in self._run_config.count_intervals:
output_file = save_path.with_suffix(
f".{DEFAULT_COUNTS_FILE_STEM}_{count_interval}"
f".{CONTEXT_FILE_TYPE_COUNTS}_{count_interval}"
f"{DEFAULT_COUNT_INTERVAL_TIME_UNIT}."
f"{DEFAULT_COUNTS_FILE_TYPE}"
)
Expand Down
63 changes: 33 additions & 30 deletions OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
from OTAnalytics.application.config import (
CUTTING_SECTION_MARKER,
DEFAULT_COUNTING_INTERVAL_IN_MINUTES,
OTCONFIG_FILE_TYPE,
OTFLOW_FILE_TYPE,
)
from OTAnalytics.application.logger import logger
from OTAnalytics.application.parser.flow_parser import FlowParser
Expand Down Expand Up @@ -174,14 +176,12 @@
LINE_SECTION: str = "line_section"
TO_SECTION = "to_section"
FROM_SECTION = "from_section"
OTFLOW = "otflow"
MISSING_TRACK_FRAME_MESSAGE = "tracks frame"
MISSING_VIDEO_FRAME_MESSAGE = "videos frame"
MISSING_VIDEO_CONTROL_FRAME_MESSAGE = "video control frame"
MISSING_SECTION_FRAME_MESSAGE = "sections frame"
MISSING_FLOW_FRAME_MESSAGE = "flows frame"
MISSING_ANALYSIS_FRAME_MESSAGE = "analysis frame"
OTCONFIG = "otconfig"


class MissingInjectedInstanceError(Exception):
Expand Down Expand Up @@ -515,16 +515,17 @@ def _show_current_project(self) -> None:
self._frame_project.update(name=project.name, start_date=project.start_date)

def save_otconfig(self) -> None:
title = "Save configuration as"
file_types = [(f"{OTCONFIG} file", f"*.{OTCONFIG}")]
defaultextension = f".{OTCONFIG}"
initialfile = f"config.{OTCONFIG}"
otconfig_file: Path = ask_for_save_file_path(
title, file_types, defaultextension, initialfile=initialfile
suggested_save_path = self._application.suggest_save_path(OTCONFIG_FILE_TYPE)
configuration_file = ask_for_save_file_path(
title="Save configuration as",
filetypes=[(f"{OTCONFIG_FILE_TYPE} file", f"*.{OTCONFIG_FILE_TYPE}")],
defaultextension=f".{OTCONFIG_FILE_TYPE}",
initialfile=suggested_save_path.name,
initialdir=suggested_save_path.parent,
)
if not otconfig_file:
if not configuration_file:
return
self._save_otconfig(otconfig_file)
self._save_otconfig(configuration_file)

def _save_otconfig(self, otconfig_file: Path) -> None:
logger().info(f"Config file to save: {otconfig_file}")
Expand Down Expand Up @@ -574,10 +575,10 @@ def load_otconfig(self) -> None:
askopenfilename(
title="Load configuration file",
filetypes=[
(f"{OTFLOW} file", f"*.{OTFLOW}"),
(f"{OTCONFIG} file", f"*.{OTCONFIG}"),
(f"{OTFLOW_FILE_TYPE} file", f"*.{OTFLOW_FILE_TYPE}"),
(f"{OTCONFIG_FILE_TYPE} file", f"*.{OTCONFIG_FILE_TYPE}"),
],
defaultextension=f".{OTFLOW}",
defaultextension=f".{OTFLOW_FILE_TYPE}",
)
)
if not otconfig_file:
Expand All @@ -596,7 +597,7 @@ def _load_otconfig(self, otconfig_file: Path) -> None:
)
if proceed.canceled:
return
logger().info(f"{OTCONFIG} file to load: {otconfig_file}")
logger().info(f"{OTCONFIG_FILE_TYPE} file to load: {otconfig_file}")
self._application.load_otconfig(file=Path(otconfig_file))
self._show_current_project()
self._show_current_svz_metadata()
Expand Down Expand Up @@ -727,17 +728,17 @@ def load_configuration(self) -> None: # sourcery skip: avoid-builtin-shadow
askopenfilename(
title="Load sections file",
filetypes=[
(f"{OTFLOW} file", f"*.{OTFLOW}"),
(f"{OTCONFIG} file", f"*.{OTCONFIG}"),
(f"{OTFLOW_FILE_TYPE} file", f"*.{OTFLOW_FILE_TYPE}"),
(f"{OTCONFIG_FILE_TYPE} file", f"*.{OTCONFIG_FILE_TYPE}"),
],
defaultextension=f".{OTFLOW}",
defaultextension=f".{OTFLOW_FILE_TYPE}",
)
)
if not configuration_file.stem:
return
elif configuration_file.suffix == f".{OTFLOW}":
elif configuration_file.suffix == f".{OTFLOW_FILE_TYPE}":
self._load_otflow(configuration_file)
elif configuration_file.suffix == f".{OTCONFIG}":
elif configuration_file.suffix == f".{OTCONFIG_FILE_TYPE}":
self._load_otconfig(configuration_file)
else:
raise ValueError("Configuration file to load has unknown file extension")
Expand All @@ -764,25 +765,22 @@ def _load_otflow(self, otflow_file: Path) -> None:
self.refresh_items_on_canvas()

def save_configuration(self) -> None:
initial_dir = Path.cwd()
if config_file := self._application.file_state.last_saved_config.get():
initial_dir = config_file.file.parent

suggested_save_path = self._application.suggest_save_path(OTFLOW_FILE_TYPE)
configuration_file = ask_for_save_file_path(
title="Save configuration as",
filetypes=[
(f"{OTFLOW} file", f"*.{OTFLOW}"),
(f"{OTCONFIG} file", f"*.{OTCONFIG}"),
(f"{OTFLOW_FILE_TYPE} file", f"*.{OTFLOW_FILE_TYPE}"),
(f"{OTCONFIG_FILE_TYPE} file", f"*.{OTCONFIG_FILE_TYPE}"),
],
defaultextension=f".{OTFLOW}",
initialfile=f"flows.{OTFLOW}",
initialdir=initial_dir,
defaultextension=f".{OTFLOW_FILE_TYPE}",
initialfile=suggested_save_path.name,
initialdir=suggested_save_path.parent,
)
if not configuration_file.stem:
return
elif configuration_file.suffix == f".{OTFLOW}":
elif configuration_file.suffix == f".{OTFLOW_FILE_TYPE}":
self._save_otflow(configuration_file)
elif configuration_file.suffix == f".{OTCONFIG}":
elif configuration_file.suffix == f".{OTCONFIG_FILE_TYPE}":
self._save_otconfig(configuration_file)
else:
raise ValueError("Configuration file to save has unknown file extension")
Expand Down Expand Up @@ -1398,6 +1396,7 @@ def _configure_event_exporter(
initial_position=(50, 50),
input_values=default_values,
export_format_extensions=export_format_extensions,
viewmodel=self,
).get_data()
file = input_values[toplevel_export_events.EXPORT_FILE]
export_format = input_values[toplevel_export_events.EXPORT_FORMAT]
Expand Down Expand Up @@ -1760,6 +1759,7 @@ def export_road_user_assignments(self) -> None:
input_values=default_values,
export_format_extensions=export_formats,
initial_file_stem="road_user_assignments",
viewmodel=self,
).get_data()
logger().debug(export_values)
save_path = export_values[toplevel_export_events.EXPORT_FILE]
Expand Down Expand Up @@ -1833,3 +1833,6 @@ def _show_current_svz_metadata(self) -> None:
self._frame_svz_metadata.update(metadata=metadata.to_dict())
else:
self._frame_svz_metadata.update({})

def get_save_path_suggestion(self, file_type: str, context_file_type: str) -> Path:
return self._application.suggest_save_path(file_type, context_file_type)
Loading

0 comments on commit f258ec7

Please sign in to comment.